閒談軟體設計:Offline first (Server 篇)

更新於 發佈於 閱讀時間約 15 分鐘
圖片來源:www.freepik.com

圖片來源:www.freepik.com

如果您以為上一篇閒談軟體設計:Offline first (Client 篇) 已經是所有需要考慮的眉角,那可就錯了,實作 offline first 不是只有 client 要注意,server 也需要下功夫的。

前言

在上一篇提到 offline first 能帶來更好的使用者體驗,但考慮的眉角很多,client 端要有本地資料,需要有同步機制與衝突排除原則,可是光是這樣還不夠,在 server 設計上也有不少眉角,光是衝突排除原則就需要 client 與 server 相互配合才能夠完成。因此,本篇就來談談 server 需要考慮的幾個要素吧:

  • ID 的產生
  • 歷程記錄 (audit log)
  • Long polling vs. WebSocket
  • 衝突排除原則

ID 的產生

傳統上,ID 的產生大都是由 server 負責,尤其是使用關聯式資料庫的開發者都喜歡用 auto incremental ID,因為最簡單,而且不用擔心有衝突,但實際上,這容易讓程式的核心依賴在 persistence layer,例如下圖,TaskRepositorysave 函式就需要回傳 ID,但如果 ID 是 service layer 可以自己產生,是否意謂著 save 就不需要回傳 ID 了呢?這是一種實作引導或是影響抽象的例子。

若我們把視角拉高,為什麼 client 需要 server 產生 ID?或多或少,client 只是把 server 當作是資料庫?我不確定,但我確實看過有人用 Repository 來實作與 server 的串接。

稍微扯遠了,回到 offline,一般來說,offline 的應用程式在操作上的感覺,會蠻像閒談軟體設計:Client Server 中提到的類型 (e),client 端已經有大多數的 application 邏輯,而且也有部分的資料儲存在 local 端。

圖片來源:《Distributed Systems: Principles and Paradigms》

圖片來源:《Distributed Systems: Principles and Paradigms》

由於 client 即便尚未完成 API 呼叫,需要在本地儲存資料才有辦法正常顯示與操作,此時 ID 的產生大致有兩種方式:

  • 直接交由 client 處理,這比較簡單,但選擇不多,為了確保全域的唯一性,大概只能選擇 UUID (參閱 閒談軟體設計:UUID 三部曲),或是類似的 ID 生成演算法來產生 ID。
  • ID 交換,這讓 server 有獨立的 ID 選擇,不受限於 UUID,client 也可以選擇 UUID 以外的類型,讓資料可以先儲存在 local 端,等到完成 API 呼叫,再替換成 server 回傳的 ID,但還有一些事情要考慮,例如,何時該用 client 的 ID?何時該用 server 配的 ID?交換時有衝突怎麼辦?這都會大幅增加 client 存取資料的複雜度。

歷程記錄 (audit log)

在上一回 client 篇有提到,將資料同步到 server 大致有兩種方式:視作檔案的方式同步以及以 request queue 的方式同步,當時有提到,視作檔案的方式有個缺點是較難留有完整的歷程。

但為什麼視做檔案的方式比較難保留歷程呢?假設,在網路不是很好的情況下,客人來到餐廳,餐廳的服務人員詢問電話後找到訂位,於是將狀態改成「已報到」。不久後,保留的位子已經清理完畢,服務人員帶客人到座位上,於是將狀態又改為「已入座」。此時網路恢復,client 開始同步資料到 sever,由於該筆資料最後的狀態是「已入座」,server 並不知道中間原來曾經有過「已報到」的狀態,因此也就少了一筆歷程。

反之,request 的方式,報到是一次 API request,入座是一次 API request,server 是透過處理兩次的 API requests 將資料變成與 client 一致的狀態,同時,server 也能分別記錄兩次 API requests 的歷程。

視應用的使用情境,歷程有時候是非常重要的,一般來說 B2B 或是金融相關的應用,都會很在意誰在什麼時間點做了什麼。這讓我想起之前在 GSS 用 Spring framework 的 interceptor 攔截 API endpoint 的呼叫,把誰呼叫哪個 API,帶了什麼參數,得到什麼內容,下載的檔案內容,通通存到資料庫,然後開發一個介面查詢所有的歷程記錄。

但,為什麼 git 能保留檔案的歷程呢?這就留給讀者去思考了,提示,想想什麼是 commit?

Long polling vs. WebSocket

到剛剛為止,談得比較多的都是如何將資料送到 server 端,但同步機制哩,如何讓 client 端知道有新資料也是很重要,過去 fat client 或是傳統桌面應用程式大多可能選擇建立一個持續的 socket 連線,讓 client 與 server 可以雙向互動。

到網頁時代,由於瀏覽器的限制,發展出 long polling 的機制,建立一個 HTTP 連線,但 server 故意讓它連著一段時間,等到有資料才回覆。更後期,則出現了 WebSocket,建立在 HTTP 協議上,建立持續的連線,可以雙向互動。我想很多人都知道這些背景,也不少人選擇 WebSocket。

替 WebSocket 的 server 實作 horizontal scale 時要注意一件事情, HTTP 連線即便有 keep alive,連線也不會真的一直持續不斷,但 WebSocket 則是真的持續保持連線,這會導致即便開了新機器 (這邊用比較容易理解的說法,機器可能是一個新的 pod 或是一台新的 VM),連線仍會集中在最一開始的幾台機器。

以下圖為例,一開始有三台機器,最初的連線進來 (藍色),平均分散到三台機器,後來根據設定自動加開了第四台機器,於是連線又分散到四台機器 (紅色),但最一開始的三台機器的連線並不會斷,於是這三台的負載仍然很高,第五台機器加開後,後續的連線分散到五台機器 (綠色),第六台機器加開後,又有更多的連線進來分散到六台機器 (紫色)。

scale up 與連線分布

scale up 與連線分布

要讓連線能平均分散到加開的機器,需要不同的 load balance 機制,否則,連線其實很容易集中在最初的機器上。剛剛提到的是 scale up,但 scale down 也很重要,不然,費用會居高不下。一般來說,當要 graceful shutdown 一台機器時,會讓 load balancer 知道不要再讓請求進到準備關閉的機器,等既有的請求都完成後才真正的關機。但尷尬的是 WebSocket 沒有結束的時候,因此,要記得處理 OS 送進來的訊號,強制讓連線中斷,讓 client 知道要重新連線。

衝突排除原則

在上回其實就討論了衝突排除原則,當時有個前提:永遠要以 server 作為最終的資料來源,以及一定要有可以辨識版本的資訊。因此,其實 server 在衝突排除原則中扮演的角色更吃重。

版號

為了讓 client 與 server 處理好衝突,需要同步的資源勢必要有能識別的版號,這裡的版號不一定要是連續的數字,雖然連續的數字版號在樂觀鎖中很常被使用,但在 offline first 的應用中,樂觀鎖適不適用取決於排除原則。

用樂觀鎖,是假設當要更新資料庫時,資料庫中資源的版號正是被編輯的版號。因此,若兩個 client 同時對資源 A 的版本 5 進行修改,這兩個修改都想同步到 server 上時,一定有某個請求先被處理,另一個請求會因為 transaction 的關係會等到正在處理中的請求完成後才會執行。第一個請求在更新資料庫時發現資料庫中的資源 A 也是版本 5,所以可以安心更新,並將版本提升為 6。第二個請求在更新資料庫時發現,資源 A 已經是版本 6 了,表示有其他請求在它之前修改了資源 A。

這時問題來了,若不是 offline first 的應用,server 最簡單的方式就是捨棄修改並回傳錯誤給 client,讓 client 決定該怎麼處理衝突。但 offline first 的應用,client 通常不會等待 server 回傳的結果,假設 server 回傳錯誤,client 當下也許早就不在當時的畫面,跳出錯誤訊息,在情境不對稱的情況下,反而讓使用者一頭霧水。因此,多數介紹如何開發 offline first 的文章都會提到 Last write wins,這是最容易處理的方式,可惜的是,樂觀鎖就無法發揮原有的功能了。

所以,就不需要版號了?要,Last write wins 只是簡化 server 怎麼處理衝突,要讓 client 端有較充足的資訊,優化回溯可能造成的體驗,版號仍是一個有用的資訊。在上回提到,client 端是可以有條件無視 server 回傳的資料,當時的說法是,client 可以等自己的 request 送達 server 後再接受 server 的回傳值,這時版號就是一個可參考的資訊。

注意,即便有版號,在極端的情境下:多個 client 在網路不佳的環境中仍無法避免回溯,只是有可能減少回溯的次數,或是讓回溯發生時不要那麼的突兀。

狀態機

一般來說,server 在設計功能時,大多會考慮狀態轉換是否合法,這時,會透過狀態機來描述,例如,下圖是個訂單的簡單狀態機,happy path 是消費者完成付費訂餐,一張新訂單進來 [new],接著餐廳人員接單 [accepted],廚房開始準備 [preparing],餐點完成通知消費者 [ready],然後消費者取餐 [picked-up],就一般很常見的訂單處理流程。

圖中 new -> accepted -> preparing -> ready -> picked-up 都是單向箭頭,也就是無法從 ready -> preparing 或是 ready -> accepted。通常,server 會將試圖把 ready 的訂單改成 accepted 的請求視為無效或是違法的請求,這對一般的應用程式來說,很合理 (先不討論奇怪的商業考量),確保訂單不會進入有問題的狀態。

在搭配金流的情況下,狀態的確保會更重要,例如:餐廳因為廚房的問題,拒絕接單,系統於是發動退款,並將狀態改為 [rejected],如果允許 rejected -> new,這時,訂單就會處在有問題的狀態 (款項已退),而且無法修正 (消費者通常不願意再付一次)。
簡單的訂單狀態機

簡單的訂單狀態機

但在 offline first 的情況下,可能會因為多個 client 同步的順序,導致無法滿足狀態機的限制,例如,餐廳人員在 client A 接單,但因為同步尚未完成,另一位餐廳人員在廚房使用 client B 時發現訂單還沒接,於是也按了接單,同時按下開始準備,將狀態移到準備中。

這裡會有兩種可能,第一種是 client A 的請求先送到 server,server 於是將狀態改為 accepted,接著 client B 的請求送到 server,因訂單已經 accepted ,server 可以忽略將 new 改為 accepted 的請求,接著,處理將 accepted 改為 preparing 的請求,這是較好處理的可能。

第二種可能是 client B 的請求先送到 server,於是訂單狀態被改成 preparing,此時,client A 的請求才送到,此時 server 是要忽略 preparing -> accepted 的修改?還是拒絕 client A 的請求?在這個例子中,狀態的修改不會觸發金流的服務,但如果會觸發外部服務,就會更難以處理,例如,一位餐廳人員在 client A 上拒接訂單,另一位餐廳人在 client B 上接受訂單,這時該怎麼處理?

因此,在需要用狀態機嚴格控制物件狀態的情況下,盡量避免使用 offline first 的設計。

權限控管

上回提到依序執行可能會引起阻塞,導致某些可以很快就能完成的 request 被尚未完成的長 request 擋在後面,這時可以實作特殊的 queue,將會互相影響的 request 排到同個 queue 中,不相關的 request 則排到不同的 queue 中,增加平行處理的能力。

即便真的會互相影響的 request 排到同個 queue 中,在多個 client 的情況下,仍有可能發生問題,權限控管便是一個例子。

假設,使用者 A 在 POS 系統中有建立訂單的權限,使用者 A 在 client X 建立了一筆訂單,client X 在建立訂單時,也用 local 已同步的權限設定確認沒問題,於是訂單建立,並準備同步到 server,這時,管理者因為一些原因,決定將使用者 A 的權限移除,即便訂單在移除權限前就已經建立,但同步並不保證順序。

若建立訂單的請求比移除權限的請求先送達 server,那兩個請求都能順利完成。但如果移除權限的請求比建立訂單的請求早,server 就很難處理了,是要直接相信 client 的權限檢查結果呢?還是要回絕請求?前者是較使用者友善,但不安全的作法,反之,後者是安全的作法,只是使用者可能較痛苦,例如,使用者 A 找另外一位有權限的使用者,再重新輸入一次訂單資料。

雖然,剛剛的例子,可以靠時間戳記進一步檢查訂單建立的時間與移除權限的時間,判斷是否允許訂單的建立,但這牽扯到另一個問題,client 給的時間戳記可以信任嗎?

因此,同樣的,若權限控管是非常嚴格要求的,那可能也盡量避免使用 offline first 的設計。

總結

花了兩篇的篇幅,探討 offline first 在實作上的很多眉角,大多來自身的經驗,我也不知道為什麼?過去開發的產品有很高的比例都是支援 offline first,只能說,使用者體驗是要靠非常多的工夫去設計與開發的。但 offline first 不見得是唯一解法,在沒有對應的需求下,不會輕易說要導入 offline first 的設計。



後記

在狀態機一節有提到,不討論奇怪的商業考量,然而,真正的狀態機完全不是圖中那樣簡單,因為餐廳實際營運時會有各種問題出現。最常發生的便是誤操作,常常遇到客服請工程師改狀態,因為餐廳接錯單或是按錯。有人可能會說 UI 多點提示,或是要再次確認,就可以避免誤操作,但有在餐廳現場待過的,就會知道很多現場工作人員是忙到沒有時間確認,跳對話框,直接按確定,等到發現時再找客服。有時,都會猶豫,這樣還要設計狀態機嗎?設計好的狀態機會出現各種特例,這邊不做討論,問題就留給大家思考了。

留言
avatar-img
留言分享你的想法!
avatar-img
Spirit的沙龍
53會員
104內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
Spirit的沙龍的其他內容
2024/03/23
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
2024/03/23
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
2024/03/09
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
2024/03/09
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
2024/03/02
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
2024/03/02
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
看更多
你可能也想看
Thumbnail
TOMICA第一波推出吉伊卡哇聯名小車車的時候馬上就被搶購一空,一直很扼腕當時沒有趕緊入手。前陣子閒來無事逛蝦皮,突然發現幾家商場都又開始重新上架,價格也都回到正常水準,估計是官方又再補了一批貨,想都沒想就立刻下單! 同文也跟大家分享近期蝦皮購物紀錄、好用推薦、蝦皮分潤計畫的聯盟行銷!
Thumbnail
TOMICA第一波推出吉伊卡哇聯名小車車的時候馬上就被搶購一空,一直很扼腕當時沒有趕緊入手。前陣子閒來無事逛蝦皮,突然發現幾家商場都又開始重新上架,價格也都回到正常水準,估計是官方又再補了一批貨,想都沒想就立刻下單! 同文也跟大家分享近期蝦皮購物紀錄、好用推薦、蝦皮分潤計畫的聯盟行銷!
Thumbnail
每年4月、5月都是最多稅要繳的月份,當然大部份的人都是有機會繳到「綜合所得稅」,只是相當相當多人還不知道,原來繳給政府的稅!可以透過一些有活動的銀行信用卡或電子支付來繳,從繳費中賺一點點小確幸!就是賺個1%~2%大家也是很開心的,因為你們把沒回饋變成有回饋,就是用卡的最高境界 所得稅線上申報
Thumbnail
每年4月、5月都是最多稅要繳的月份,當然大部份的人都是有機會繳到「綜合所得稅」,只是相當相當多人還不知道,原來繳給政府的稅!可以透過一些有活動的銀行信用卡或電子支付來繳,從繳費中賺一點點小確幸!就是賺個1%~2%大家也是很開心的,因為你們把沒回饋變成有回饋,就是用卡的最高境界 所得稅線上申報
Thumbnail
本篇將說明關於StatefulSet的基本概念
Thumbnail
本篇將說明關於StatefulSet的基本概念
Thumbnail
如果您以為上一篇 已經是所有需要考慮的眉角,那可就錯了,實作 offline first 不是只有 client 要注意,server 也需要下功夫的。
Thumbnail
如果您以為上一篇 已經是所有需要考慮的眉角,那可就錯了,實作 offline first 不是只有 client 要注意,server 也需要下功夫的。
Thumbnail
不管用哪種語言開發軟體,除非是那種一個 function 寫個幾萬行的人 (來人啊,把這種人拖去砍了),不然,一般都會根據某些因素,切割成模組或是特定功能的區塊 (一個 class 或是一個 function),但要完成一個特定功能,這些模組或區塊勢必要一起合作,因此這些模組與區塊就發生了關係。
Thumbnail
不管用哪種語言開發軟體,除非是那種一個 function 寫個幾萬行的人 (來人啊,把這種人拖去砍了),不然,一般都會根據某些因素,切割成模組或是特定功能的區塊 (一個 class 或是一個 function),但要完成一個特定功能,這些模組或區塊勢必要一起合作,因此這些模組與區塊就發生了關係。
Thumbnail
我們在「【資料庫寶典】什麼是NoSQL?能吃嗎?」有談到一些NoSQL的特性,雖然本質上有所差異,但兩方技術發展的產品也都開始互相支援了,比如說MongoDB後來也發展出類SQL語法讓熟悉SQL的開發者可以降低進入門檻,而SQL、postgresql…等也紛紛納入一些NoSQL的元素,雙方都有開始接
Thumbnail
我們在「【資料庫寶典】什麼是NoSQL?能吃嗎?」有談到一些NoSQL的特性,雖然本質上有所差異,但兩方技術發展的產品也都開始互相支援了,比如說MongoDB後來也發展出類SQL語法讓熟悉SQL的開發者可以降低進入門檻,而SQL、postgresql…等也紛紛納入一些NoSQL的元素,雙方都有開始接
Thumbnail
上一篇【瀏覽器與伺服器是如何溝通的?】有說瀏覽器和伺服器的溝通橋樑是「網路請求」,這篇就來記錄一下,我目前學習到的網路請求是什麼~
Thumbnail
上一篇【瀏覽器與伺服器是如何溝通的?】有說瀏覽器和伺服器的溝通橋樑是「網路請求」,這篇就來記錄一下,我目前學習到的網路請求是什麼~
Thumbnail
身為一個非本科新手工程師,對於網路知識還是有許多疑惑之處,像是我在編輯器上寫了數十行的程式碼,那它們是怎麼透過終端機的指令被運行起來,讓我可以一邊開發一邊預覽結果呢?
Thumbnail
身為一個非本科新手工程師,對於網路知識還是有許多疑惑之處,像是我在編輯器上寫了數十行的程式碼,那它們是怎麼透過終端機的指令被運行起來,讓我可以一邊開發一邊預覽結果呢?
Thumbnail
程式開發有趣的地方,同樣的目標,不同的團隊會因不同的因素做出不同的設計抉擇。而這往往也是為什麼一個資深的工程師在開發速度上不一定比較快的原因之一,一個越是資深的工程師,思考的因素會更多,不過,不是考慮得越多就結果就一定越好,有時還會變成 over design 較糟的結果。
Thumbnail
程式開發有趣的地方,同樣的目標,不同的團隊會因不同的因素做出不同的設計抉擇。而這往往也是為什麼一個資深的工程師在開發速度上不一定比較快的原因之一,一個越是資深的工程師,思考的因素會更多,不過,不是考慮得越多就結果就一定越好,有時還會變成 over design 較糟的結果。
Thumbnail
相信有在開發Web應用的朋友應該對於Postman這套工具相當熟悉, 這套工具可以協助我們在產品尚未完成之前可以先進行一些基本的介接測試,甚至我們可以使用Postman去呼叫雲端的API,像是Google的語音辨識、文字翻譯、字典查詢…,這類大廠相信也都開放許多標準API(Application P
Thumbnail
相信有在開發Web應用的朋友應該對於Postman這套工具相當熟悉, 這套工具可以協助我們在產品尚未完成之前可以先進行一些基本的介接測試,甚至我們可以使用Postman去呼叫雲端的API,像是Google的語音辨識、文字翻譯、字典查詢…,這類大廠相信也都開放許多標準API(Application P
Thumbnail
What is WebSocket? WebSocket 是 HTML5 提供的一種網路傳輸協定,是瀏覽器(Client)與伺服器(Server)交換資料的方式之一。 與我們較為熟知的 HTTP 或 HTTPS 協定,同樣位於 OSI 模型的應用層,且基於傳輸層的 TCP 協定。
Thumbnail
What is WebSocket? WebSocket 是 HTML5 提供的一種網路傳輸協定,是瀏覽器(Client)與伺服器(Server)交換資料的方式之一。 與我們較為熟知的 HTTP 或 HTTPS 協定,同樣位於 OSI 模型的應用層,且基於傳輸層的 TCP 協定。
Thumbnail
資料庫複製 不知道大家有沒有聽過負載均衡或者水平擴展呢?在網站佈署中我們會透過這些技術把網站架在多台 server 上,以避免萬一某台 server 掛掉,讓網頁服務仍能維持運作,或者去分擔負載 MongoDB 這邊有一個叫資料庫複製的技術,建立多個相同的 MongoDB service 在不同的
Thumbnail
資料庫複製 不知道大家有沒有聽過負載均衡或者水平擴展呢?在網站佈署中我們會透過這些技術把網站架在多台 server 上,以避免萬一某台 server 掛掉,讓網頁服務仍能維持運作,或者去分擔負載 MongoDB 這邊有一個叫資料庫複製的技術,建立多個相同的 MongoDB service 在不同的
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News