閒談軟體設計: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
53會員
104內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
Spirit的沙龍 的其他內容
任何語言特性用與不用,其實要看是否提升了生產力?是否提升可讀性?是否提升可維護性?這些都是在三個月甚至半年後回來修改程式時,才能明顯感受到的,而不是寫程式的當下。Java 8 的 CompletableFuture、Stream 和 Optional 都很好,但用的不好反而畫蛇添足又沒提高可讀性。
不管用哪種語言開發軟體,除非是那種一個 function 寫個幾萬行的人 (來人啊,把這種人拖去砍了),不然,一般都會根據某些因素,切割成模組或是特定功能的區塊 (一個 class 或是一個 function),但要完成一個特定功能,這些模組或區塊勢必要一起合作,因此這些模組與區塊就發生了關係。
不重新造輪子,我們使用第三方函式庫,聽起來很合理,但每個被引入的函式庫意味著一種 coupling,看到套件管理工具下載眾多第三方函式庫,意味著不用重寫這些東西,開發效率能提升數倍甚至數百倍,但我們真的都能掌握這些 coupling 嗎?當這其中任何一個環節出錯,我們的系統架構真的很優雅地應付嗎?
Offline first 的設計最近有越來越多的感覺,但好的 Offline first 設計要解決蠻多的問題,是否使用 offline first 設計真的需要好好思考,不然可能得不到好處,反而還引起一堆 bug,本篇先探討在 client 端可能會遇到的問題與一些可能的解法。
任何語言特性用與不用,其實要看是否提升了生產力?是否提升可讀性?是否提升可維護性?這些都是在三個月甚至半年後回來修改程式時,才能明顯感受到的,而不是寫程式的當下。Java 8 的 CompletableFuture、Stream 和 Optional 都很好,但用的不好反而畫蛇添足又沒提高可讀性。
不管用哪種語言開發軟體,除非是那種一個 function 寫個幾萬行的人 (來人啊,把這種人拖去砍了),不然,一般都會根據某些因素,切割成模組或是特定功能的區塊 (一個 class 或是一個 function),但要完成一個特定功能,這些模組或區塊勢必要一起合作,因此這些模組與區塊就發生了關係。
不重新造輪子,我們使用第三方函式庫,聽起來很合理,但每個被引入的函式庫意味著一種 coupling,看到套件管理工具下載眾多第三方函式庫,意味著不用重寫這些東西,開發效率能提升數倍甚至數百倍,但我們真的都能掌握這些 coupling 嗎?當這其中任何一個環節出錯,我們的系統架構真的很優雅地應付嗎?
Offline first 的設計最近有越來越多的感覺,但好的 Offline first 設計要解決蠻多的問題,是否使用 offline first 設計真的需要好好思考,不然可能得不到好處,反而還引起一堆 bug,本篇先探討在 client 端可能會遇到的問題與一些可能的解法。
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
起源是當時 Facebook 有篇文章討論不少人分不清楚上述二者的差別,當時寫了首部曲《閒談軟體設計:API Naming Style》,接著是《閒談軟體設計:內部函式庫》,但始終沒談到 library 和 framework 的差別,主要是沒有好的例子,這次這例子還蠻不錯的。
Thumbnail
我自己偏好用 Repository 搭配 decorator 來管理 cache,而不是在 controller 層或是到處都有快取的邏輯,如果程式都是透過 Repository 更新資料,Repository 就會是一個不錯的地方更新快取,邏輯也就不會散亂在各處了。
Thumbnail
......究其根本動畫版與漫畫版的癥結點是相同的,也就是配角人數過多使得情節處理難以做到盡善盡美,其中又以漫畫版的問題更為明顯......
Thumbnail
前言須知-《閒人隨筆》 看官沒錯,歡迎來到閒人的復耕系列《閒人隨筆》,沒錯本系列所有作品是閒人將以往作品重新經查證,彙整的新文章。白話說法 補足故事彙整或翻譯失誤等。 今日配合之前的拔劍篇在轉一篇,
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
Thumbnail
這篇文章探討了在軟體開發中的技術債可能來自哪些原因,以及如何自動化偵測與修復技術債。作者透過分享不同情境下的技術債選擇,提供了對於技術債的思考與建議,針對開發人員在需要做出無奈的技術決策時,提供了一些建議。此外,還提供了一些在做出技術決策時的方法,如保留抽象層和避免vendor lock-in。
Thumbnail
今天來聊個最近很夯的主題 DDD,但不是 DDD 的本尊 Domain Driven Design,而是無所不在的 Database Driven Design,Database Driven Design 不是不好,只是你的模型容易變成貧血模型,邏輯都集中在 service 層等等。
Thumbnail
有趣的是,Model 其實沒什麼嚴格的定義,所以每個人對 Model 的解讀也不盡相同,有人覺得資料怎麼儲存屬於 Model 的一部份 (受 ORM 工具的影響),有人覺得工作流程 (workflow) 是 Model 的一部份,我個人也有自己的想法,而且隨專案的規模和特性,也不是總是一樣的。
Thumbnail
起源是當時 Facebook 有篇文章討論不少人分不清楚上述二者的差別,當時寫了首部曲《閒談軟體設計:API Naming Style》,接著是《閒談軟體設計:內部函式庫》,但始終沒談到 library 和 framework 的差別,主要是沒有好的例子,這次這例子還蠻不錯的。
Thumbnail
我自己偏好用 Repository 搭配 decorator 來管理 cache,而不是在 controller 層或是到處都有快取的邏輯,如果程式都是透過 Repository 更新資料,Repository 就會是一個不錯的地方更新快取,邏輯也就不會散亂在各處了。
Thumbnail
......究其根本動畫版與漫畫版的癥結點是相同的,也就是配角人數過多使得情節處理難以做到盡善盡美,其中又以漫畫版的問題更為明顯......
Thumbnail
前言須知-《閒人隨筆》 看官沒錯,歡迎來到閒人的復耕系列《閒人隨筆》,沒錯本系列所有作品是閒人將以往作品重新經查證,彙整的新文章。白話說法 補足故事彙整或翻譯失誤等。 今日配合之前的拔劍篇在轉一篇,
Thumbnail
最近身旁有幾位正在懷孕、或剛生產完的朋友,讓我想起自己在懷孕期間印象最深刻的三件「怪事」,其中又以第三件事最誇張。
Thumbnail
不知道大家在買房之前是不是都會參考親朋好友的意見,或是上網看一些買房注意事項,有時候考慮了這塊就忘了那塊,考慮的那塊又忘了這塊.......
Thumbnail
前幾年因為身體的關係,當了幾年的律師逃兵,當時開了之前的事務所以後,一時間也沒有特別想要做甚麼事情,所以就邊讀一點書、早晚運動一下,剛好聽到當年同梯朋友進去金融業工作,因此也抱著嘗(ㄊㄠˊ)試(ㄅㄧˋ)的心態,找了份銀行法令遵循的工作