Yuanchieh's Blog

Yuanchieh's Blog

生命是長期而持續的累積

03 Dec 2021

《單體式系統到微服務》讀後分享 - 上

微服務因應容器化技術、持續整合與交付工具成熟,成為顯學好一段時間了,但這不代表我們就應該導入微服務,終究微服務只是一項技術(或說是架構),如果沒有設定一個正確的目標,並用適當的指標時時關注,那導入任何新技術最後都會是一場災難,微服務更是; 我很喜歡這篇分享,先檢視團隊的 CI/CD、monitoring 機制是否健全再來思考微服務 https://www.facebook.com/hatelove/posts/10223238347681539

這部影片是 Sam Newman (本書作者)與 Martin Fowler 對談,,主要從寫書的動機開始,後面談到了為什麼要導入微服務、導入理由以及資料庫、團隊如何應對等,基本上跟書的結構互相呼應,GOTO 蠻用心的影片一個段落就會打 Tag 以及統整,後續要回顧很方便;後來經歷才知道 Sam Newman 之前 在 ThoughtWorks 工作 12 年

以下針對每章節做一些分享與總結,並融合一些看過的資料

第一章:足夠的微服務

微服務是泛指圍繞業務領域建模的可獨立部署之服務,這邊的重點有兩個

  1. 圍繞業務領域
  2. 可獨立部署

1. 圍繞業務領域

Conway’s Law: 任何設計系統的組織,都將不可以避免的產生以組織通訊架構為副本之設計

組織的架構某種程度跟程式架構雷同,都應該追求高內聚低耦合,否則會因跨組別的高溝通成本,而讓組織的生產力下降,進而讓大家選擇對自己最方便的解法而非全局最佳解

把康威定律轉成工程師的比喻會是

If you have four groups working on a compiler, you’ll get a 4-pass compiler

過往組織的分組是透過職能,如 RD 組成一個 RD 部門負責、IT 部門負責部署與維運、業務部門負責產品端的其餘事項,所以業務端發起需求後,需要先與 RD 部門溝通、RD 部門開發後請 IT 部門部署,層層的跨部門溝通讓新功能上線很慢,此時組織的重點是以職能內聚而分業務內聚,延伸分享之前整理的筆記 Fred聊聊SOLID設計原則,SOLID 原則中的 Single Responsibility 可以從業務角度去思考

因應軟體需要更高頻次的功能部署,開始以業務領域來做組織的架構,例如 Amazon 的 two pizza team 追求you build it, you run it的精神,讓每個團隊從需求設計到上線反應速度更快

2. 獨立部署

組織依照業務領域切開後,會需要可以隨時按照團隊的節奏部署服務,並確保其餘服務不受影響,這也就是獨立部署的重要性

在設計服務,要確保服務的低耦合,否則會陷入更新一個服務要連帶更新其餘多個服務,變成可怕的分散式單體式架構,這就延伸到開頭分享的 FB 文章 - 團隊是否有良好的 CICD、是否有良好的 Log 架構與 Tracing 系統

3. 自身擁有的資料

微服務不應該共享資料庫,如果其餘服務需要資料,應該要透過服務介面而非直接存取資料,避免程式碼拆分最後卻在資料庫耦合,消抹了獨立部署的功用

微服務好處

主要體現在靈活性

  1. 部署獨立性:改善系統規模與強健性 (Robust)、可混用不同技術
  2. 更好分工:不同團隊專注不同的 code base

單體式架構

1. 單一程序

將所有程式碼放入單一程序部署的系統中

2. 模組化單體式

將單一程序模組化,讓每個模組都能獨立運作,只是部署會統一部署,甚至可以讓不同模組使用不同的資料庫;非常推薦 Shopify 的分享 Under Deconstruction: The State of Shopify’s Monolith,預計之後再展開討論

耦合與內聚

內聚是指「程式碼同變動、共留存」,當今天改 A 功能時,僅需要確保 A 功能相關的服務改動,降低變動成本

耦合是指「資訊隱藏」,把相對經常性變動的部分與靜態的部分分開,有個穩定的模組邊界,又可細分成實作耦合、時間耦合、部署耦合、領域耦合

小結

第一章定義了微服務、單體式架構,並分析背後設計原理與最終反應的架構優劣,但需要小心 單體式不等同於傳統,不要污名化單體式,應該要用更客觀的角度權衡架構選擇

最後作者推薦 DDD 協助業務領域的定義與拆分

第二章:遷移計畫

微服務是個技術選擇而非目的,再導入前應該先謹慎思考

  1. 想要解決的問題是什麼?想要解決的問題跟公司利益是否一致?用戶是否會因此受益?
  2. 是否比較過替代方案?是不是其他「無聊的技術」就能解決問題?
  3. 如何衡量導入微服務的成效?怎麼再導入過程知道方向是否正確?

為什麼選擇微服務

如果是因為下列原因而想要導入微服務,可以先思考看看替代方案是否可行

導入目的 微服務的潛在優點 替代方案
增加團隊自主性 拆分微服務讓團隊的規模小、擁有相對權力,可以更有效的工作 分配責任有很多方式,並不一定要改架構,可以將程式碼所有權分屬不同的團隊,像是採取模組化單體式結構,例如 shopify
縮短上市時間 單獨針對微服務進行變更與部署 在思考生產力問題時,應該要先檢視開發流程的貧頸在何處,作者提到他的顧問生涯中發現是需求傳遞過程耗費最多時間而非開發,所以應該先審視與量測軟體開發的每個步驟
有效擴展負載 單個微服務可以獨立擴展,更動態的調整個別服務的規模 單體式的水平擴展依然是很有效的擴展方式,如果當下遇到效能貧頸,直接擴展而非導入微服務可能是相對快速又有效的選擇
增進強健性 拆分微服務後,可個別依據業務需求而區分出核心與非核心的微服務,進而提供不同的強健性調整 透過 Load Balancer、Event Queue 搭配單體式系統的水平擴展,一樣可以增進強健性,把資源投注在診斷系統原因可能會更有幫助
擴展開發人員 在《人月神話》中只有把項目拆分成可獨立作業的項目,增加人力才有辦法加速開發否則多餘的人力是沒有搬著的,微服務透過良好的介面隔離實作,讓團隊要加人相對容易 重點擺在團隊與服務的所有權要保持一致,可以透過模組化單體式分工合作
擁抱新技術 微服務因為是獨立部署,不同服務間可以採取完全不同的技術架構 這是微服務很明顯的優勢,替代方案可以考慮向 JVM 支援 Java, Scala, Kotlin 等同家族語言,或是像 Graal VM 同一個 Runtime 支援多種語言

微服務何時是不好的主意

  1. 模糊領域:錯誤定義服務邊界會很可怕,因為會造成跨服務的連鎖改動
  2. 新創公司:可能有點爭議,但這呼應到上一點,如果公司的商業模式還沒有穩定,那自然就無法切出正確的服務邊界
  3. 客戶安裝與軟體管理:如果是要把軟體打包給庫戶,使用微服務可能會不太好,要考量到客戶的管理能力
  4. 沒有充分理由時

組織變革

作者較哨如何在組織內導入新的技術,分享 John Kother 博士的組織變革步驟,大致上溝通、小幅嘗試、看到初步效益、融入組織

再產生變革時,要注意到變革成本,Jeff Bezos 將變革分成兩類:第一類是必然、不可逆的,第二類是可逆、可變化的,第一類需要長期討論、謹慎的思考,第二類可以讓高判斷力的個人或組織迅速做出; 沒有經常下決策的人通常會誤把第二類當作第一類,讓所有決策都停滯不前

這也呼應到之前工作時公司導入敏捷開發,固然看到快速迭代、持續驗證與微調的優勢,但內部也時常在爭論如小規模實驗是否真的有效、用戶會不會因為實驗而大量流失等等,現在反思起來就是沒有把第一類與第二類決策溝通清楚的緣故

如何開始

作者推崇透過 DDD 幫助定義服務邊界,並採用 Event Storming 讓大家對於模型有共同的理解

接著往內檢視團隊的組成,團隊成員是否有足夠的技能?例如說微服務要透過事件溝通導入 Kafka,那團隊是否有 Kafka 熟悉的人才?如果沒有要花多少時間跟資源調整?

如何確定轉移是否有效

可以設立定期檢查點,再一定時間檢查定性與定量措施,定量措施包含發布週期、故障率、部署次數、性能監控等,但要小心定量的指標會變成陷阱,例如團隊追求部署次數而盲目更新 (就跟 RD 貢獻度看程式碼行數壹樣的可笑),所以需要搭配定性措施,訪談成員對於轉移過程的感受

第三章:分割單體式架構

當通過上面兩章的討論,確定要導入微服務時,回首現實要面對的時龐大、架構混亂的單體式架構

1. 重組單體式系統

傳統程式碼通常以技術分類而非業務領域分類,例如 Model / Controller / View 等資料夾拆分,要重新以業務領域拆分並找出對應的程式碼會是一項大工程,此時可以參考市面上重構與管理 legacy code 的方法

2. 模組化單體式

可以考慮將功能建立獨立的模組,例如 Java 的 Jar 檔、Ruby 的 Gem 等,拆出獨立的模組未來要獨立部署成微服務也會比較容易

3. 漸進重寫

試著先重構現有的程式碼,接著在重新實現功能,如果原本程式碼中的邏輯沒有先梳理清楚就貿然重寫,只會把過時且複雜的邏輯重新復刻,並拖累轉移的速度

遷移模式

模式一:絞殺榕 (Strangler Fig)

由 Martin Fowler 在看到絞殺榕生長聯想的系統遷移模式 StranglerFigApplication,一開始種子在樹上生長,接著落地後持續成長,最後母樹會死亡

套用類似的邏輯在軟體系統上,最直覺的做法是重寫一個新系統直接遷移就好,但往往事情更複雜,一次性重寫新系統需要更多的時間並冒著更大的風險;
學習絞殺榕,讓新系統可以相容於舊系統,接著再逐步抽離新系統,最後完成轉移汰換舊系統,讓每一個步驟更小並降低風險,如果中間犯錯可以很快退返,或是終止轉移也不會有任何問題

方法適用於上層的服務,如果是系統內比較底層的服務會比較難套用 (後續介紹),實作上可以在單體式架構前多一個代理器如 HTTP Proxy 或是 Message Queue,遷移步驟大概是

  1. 當新系統實作尚未完成時,原本的 Request 持續流向舊系統
  2. 新系統完成後可先部署,但此時還未對外開放
  3. 透過金絲雀部署,先放手部分流量到新系統
  4. 確保沒問題,流量完整切換至新系統,汰換舊系統

在導入代理器時,要注意 Smart Endpoints and Dumb Pipes,不要讓代理器承擔過多的職責,例如說因為舊系統支援 soap 但新系統要支援 grpc,與其讓代理器判斷 soap -> 舊系統 / grpc 走新系統,還不如直接對外公開兩個協定,而舊系統再把 soap 格式轉換到新系統的 grpc 延伸閱讀 Microservice Principles: Smart Endpoints and Dumb Pipes

模式二:抽象分支

當要抽離的組件是系統的底層,例如使用者通知功能,如果要直接改寫會必須把實作端與呼叫端一次改動,此時最好是

  1. 為要替換的功能建立抽象
  2. 讓舊系統的其他功能依賴介面而非實作
  3. 提供微服務的實作
  4. 切換抽象並使用實作
  5. 刪除舊實作

在安德魯大叔的部落格中有提到相同的概念 微服務架構 #2, 按照架構,重構系統,身份驗證組件幾乎是所有功能都會相依的組件,要從單體式架構抽出來前,記得先抽象化,確保功能引用都依賴抽象化,最後才替換微服務實作,這樣最後失敗要切換回去也非常方便,這又呼應回 Martin Fowler 在絞殺榕文章說的

when designing a new application you should design it in such a way as to make it easier for it to be strangled in the future.

果然大師們說的道理都是相通的

模式三:平行模式

如果是非常核心、重要的功能如金流,在重寫成新微服務時,可以採用平行模式雙邊寫入,每日進行比對直到沒有出錯時在切換,降低微服務上線的風險
github 有開源一個 Ruby gem scientist,專門做平行化實驗的比較

裝飾者模式

如果當前系統無法被修改,卻需要攔截呼叫並額外增加行為,可以考慮用裝飾者模式,透過代理服務器在呼叫完原系統後,額外呼叫微服務;但要小心違反 dump pipe 的風險

模式四:變更資料擷取

同樣是不修改當前系統,也不想用裝飾者模式,但同樣希望在行為變動時觸發微服務,如會員註冊後,要打印會員卡,可以透過資料儲存的方式,例如資料庫的 trigger、定期 polling 資料庫;但要小心耦合在資料庫

模式五:使用者介面組成

UI 端也可以把頁面拆解成不同的組件,例如 Micro Frontend,因應 Web 標準的發展支援 custom element,讓不同的團隊可以採用不同的技術實做不同的組件,透過 DOM event 跨組件通信

總結

前三張談了組織與程式碼架構,評估導入微服務的優劣、大方向上導入的方式,後面兩張會繼續談最難拆分的資料庫搬移以及導入後隨著組織成長會遇到的困境

Categories