《軟體架構設計》讀書筆記

Remember發表於2022-03-20

圖片拍攝於2022-03-19 杭州良渚大屋頂

最近看了一本書,書名叫《軟體架構設計:大型網站技術架構與業務架構融合之道》,特做一些筆記分享。

這本書整體分為五個部分。

  • 什麼是架構

  • 計算機功底

  • 技術架構之道

  • 業務架構之道

  • 從架構得到技術管理

圍繞這五個部分總計17章節。

接下來,我會分享部分我感興趣的章節。

第一部分:什麼是架構?

一句話:架構是針對所有重要問題做出的重要決策。

不同公司或者相同公司在不同的階段所面臨的問題不同,架構自然也會有所不同。

個人認為,不存在稱之為完美的架構,只會存在最適合的。面對的場景,著重的目的不同,那麼相應的決策也會不同(有點廢話)。

架構的分類。

作者從技術的角度,把軟體從底向上分層,做了架構的分類。

第一層:基礎架構

基礎架構指的是雲平臺、作業系統、網路、儲存、資料庫和編譯等。

第二層:中介軟體和大資料平臺

中介軟體,例如分散式服務中介軟體、訊息中介軟體、資料庫中介軟體、快取中介軟體等。

第三層:業務系統架構
  • 通用軟體系統。例如常用辦公軟體、播放器。

  • 離線業務。比如各種基於資料的離線計算、資料探勘。

  • 大型線上系統。比如電商、廣告、搜尋、推薦、ERP或者CRM等。

整體就像這樣,

從上面你也可以看出,只有大廠這三層都有。像小公司可能只有第三層,或者小量的第二層。 印象裡,我前司是沒有第一層的,第二層是有的。

一般情況下,每一層都會有專門的人去幹活。比如第二層會有專門的中介軟體部門, 對應又分為幾個組,每個組負責對應的中介軟體開發。

業務部門在第三層,一般情況下,他們只負責業務的curd,如果有場景需要用到一些中介軟體時, 這時候通常會去找負責中介軟體的人對接,使用他們的sdk等。(ps:好不好用那就是另外一回事了)

還有一個有意思點,作者在書中提到架構的道與術。

什麼是架構的道?

抽象點說,對於技術問題,主要是指高併發、高可用和一致性方面。對於業務問題,主要指業務需求分析和建模。

那麼,我們在面對這些問題的時候, 是通過大量的業務系統實踐,在實踐基礎上進行的思考和總結,進而提煉出的一些方法論,這就是道。

更具體的說,比如,

  • 資料庫如何分庫分表?

  • 分庫分表的時機如何確定?

  • 快取一致性問題如何解決?

  • 如何拆分服務?

  • ……

等等問題,這些問題解決方案並不是憑空出現的,而是通過大量的實踐落地進而總結產生的一套解決方案核心思路。

所以道很多時候是”虛”的東西,越虛意味著就越抽象,如果兩個人在討論某個問題,而對一些專業理論的認知還未處於同一水平上,那聽起來就只能離譜了。

所以要講道之前,得先有術。術就是指對應具體的語言,框架或者中介軟體使用姿勢。這些都是比較具體的東西,實操性強,方便大家理解。

架構的道和術,都不能偏廢,一方面需要不斷實踐(術),在實踐中深入原理。進而把實踐的東西抽象,總結出來,形成方法論(道)。 不斷地用道來指導新的術,在新的術中再總結出新的道,如此迴圈往復。

以上是第一部分內容。

第二部分:計算機功底

主要講解的是術。計算機功底、語言、框架、網路、資料庫、作業系統等。

印象最深刻的是框架那一章。作者提到,熟悉一個框架之後,更多的是應該去關注它的缺點,而不是優點。更應該關注它不能做什麼,而不是它能做什麼。 它不能做什麼往往是別的框架的改進點。

細想,如果你不關注它不能做什麼,在你們拍板決定使用框架時,做了一半發現, 核心的一塊需求它支援不了,這時候只能欲哭無淚了。

第三部分:技術架構之道

主要講解的是道。 裡面分為:

  • 高併發問題

  • 高可用與穩定性

  • 事務一致性

  • 多副本一致性

  • CAP理論

因為這一部分主要是關於道方面的,所以很多地方是抽象化的。讀者在讀這一部分時候,針對一些問題的解決方案,需要自行去思考部分細節。

我主要分享高併發和事務一致性的筆記。其他讀者可以自行檢視。

高併發問題

要讓各種各樣的功能和邏輯在計算機系統中實現,只能通過讀和寫兩個操作。

基於這個基礎,在高併發問題上,書中對問題進行了分類。

側重”高併發讀”的系統

比如,搜尋引擎、電商的商品搜尋、微博熱點、知乎熱點等。

側重”高併發寫”的系統

比如,廣告系統計費等

兩者兼顧。

比如,電商的庫存系統、秒殺系統、IM、朋友圈等。

之所以這樣區分,是因為處理的策略會不同。

如果是側重高併發讀的,一般可以採取以下策略。

策略1:加快取
案例一:本地快取或Memcached/Redis

對應的加快取需要考慮以下問題,

  • 單點問題

  • 快取雪崩

  • 快取穿透

  • 快取擊穿

單點問題,那麼就需要多節點叢集部署,通過類似心跳、哨兵模型等能自動剔除掛掉的節點機制。

後面三個問題一般和回源策略有關。

一種是不回源,只查詢快取。快取沒有,響應空。這種一般需要主動更新快取,並且不設定過期時間,不會存在快取擊穿,大量key過期問題。

一種是快取失效,需要回源。就需要考慮上面的問題。 對應快取雪崩的問題,一般我們會在快取本身時間的基礎上,加上隨機值,不讓大批key同一時間失效。 對於快取穿透的問題,一般上游做引數的校驗,防止惡意請求。

對於符合規則的key,如果資料庫沒有, 一般會直接把這個key快取起來,設定一個短暫的時間,值對應設定空。當然還可以通過布隆過濾器來實現,這時候,資料量已經很大了。

對應快取擊穿問題,一般我們採取的方式是保證同一時刻只有第一個請求打到db層,剩下的等待第一個同步結果即可。

案例二:Mysql的Master/Slave

上面提到的是簡單的<key,val>快取,還有一些場景會涉及到多表關聯查詢(如果避不開的話),如果直接查業務庫,高併發訪問肯定頂不住的。 一般情況下,往往是通過主從部署,業務直接查從庫關聯就行了。分攤主庫的壓力(前提是沒有進行分庫)。

當前這種情況,高併發下還是會有問題,從庫的壓力還是太大了。這時候也可以把查詢的結果快取起來,下次直接從快取拿。不過需要注意的是, 當關聯的資料發生變化的時候,快取就得更新了。

案例三:CDN靜態檔案加速

這個不用再描述了吧。

策略2:併發讀
案例1:併發呼叫

核心思想就是,如果一個請求需要呼叫不同的三個服務a,b,c,且這三個服務相互之間不存在依賴關係,也就是請求c不依賴a或者b的結果, 那麼請求就可以併發呼叫。總時間從(a+b+c)到max(a,b,c)。

案例2:Google 的”冗餘請求”

假設一個使用者的請求需要100臺伺服器同時聯合處理,每臺伺服器有1%的概率發生呼叫延遲(假設定義響應時間大於1s為延遲), 那麼響應時間大於1s的概率是63%(計算規則可以自行檢視) 咋麼解決這個問題? 冗餘請求。簡單點就是客戶端同時向多臺伺服器傳送請求,哪個返回的快就用哪個,其他丟棄。

策略3:重寫輕讀

重寫輕讀的核心思路就是,

為什麼需要這樣? 因為有時候一個資料來源往往不能滿足複雜的業務查詢,或者說無法滿足業務查詢多樣性的問題。那麼我們就要多寫,專業術語叫寫擴散。

首先我們保證主業務寫入成功,然後通過訊息佇列這樣的非同步手段,寫入不同需求維度的庫。不同維度的業務只需要去對應的資料來源查詢即可。

對於分庫分表後的關聯查詢,需要從多個庫查詢資料再聚合,但是無法利用資料原始的join功能,只能在記憶體中做資料的聚合。

這時候存在一個問題,無法使用資料庫本身的分頁功能。 一般情況下,解決方案還是重寫輕讀的策略,提前把資料關聯好資料,存在一個地方。業務直接讀取聚合好的資料。

也可以利用es類的搜尋引擎來實現。把多個表的join結果做成一個個文件,放在搜尋引擎裡面。

如果是側重高併發寫,自己看去吧。

資料一致性問題

資料一致性問題無處不在。這麼說吧,只要一個場景存在需要操作兩個或者兩個以上的服務,那麼就必然存在一致性問題。 比如,A服務和B服務。無論是先操作A,再操作B,還是先操作B,再操作A,都存在第一步成功,第二步失敗的可能性。

又比如,資料庫的Master-Salve非同步複製,如果Master當機,還有部分資料未來得及同步給Salve,此時切換到Salve,會導致部分資料丟失。

這樣的一致性問題在分散式服務中無處不在。

一個訂單服務,一個庫存服務,下訂單的時候無論是先扣庫存,後建立訂單,還是先建立訂單,再扣庫存,都存在第二步失敗的可能,如何保證服務之間的 資料一致性問題?

分散式事務解決方案
資料庫層面的2PC

2PC有兩個角色:事務參與者和事務協調者。具體到資料庫層面,每一個資料庫就是一個參與者,呼叫方就是協調者。 同時2PC分為兩個階段。

  • 準備階段一

  • 提交階段二

準備階段。協調者向每個服務發起詢問,執行一個事務,每個參與者需要回復yes、no或者呼叫超時。

提交階段。如果所有參與者回覆yes,那麼事務協調者向所有參與者傳送事務commit操作。所有參與者執行各自事務,然後傳送ACK。 如果有一個事務參與者回覆No或者超時,那麼事務協調者向所有參與者傳送rollback操作,所有參與者各自回滾事務,然後回覆ACK。

當然2PC也存在問題。

  • 問題1:效能問題。在階段一鎖定資源後,要等所有的參與者回覆完,然後才能一起進入階段二,不能很好應對高併發場景。

  • 問題2:階段一完成後,如果此時協調者當機,所有參與者收不到階段二commit或者rollback請求,狀態處在尷尬的位置。

  • 問題3:階段一完成,階段二中,事務協調者向所有參與者傳送了指令,但是有一個參與者超時或者沒有正確返回ACK。這時候,其他參與者應該咋麼辦?

訊息中介軟體(最終一致性)

簡單地說,系統A收到使用者扣錢請求,系統A先自己扣錢,也就是操作自己的資料庫DB1。 然後通過訊息中介軟體給系統B傳送一條加錢的訊息。系統B收到這條訊息,給自己賬戶加錢,也就是運算元據庫DB2。

問題來了,運算元據庫A和投遞訊息到訊息中介軟體是兩個動作,兩個網路請求。無論你是先投遞訊息,再更新資料庫,還是先更新資料庫再投遞訊息, 都存在第二步可能失敗的場景。

如果第一步運算元據庫成功, 投遞訊息失敗,重試還是失敗咋麼辦?

如果第一步運算元據庫成功,投遞訊息失敗,重試中還是失敗,而且剛好pod又重啟了,現場都沒了咋麼辦?

如果第一次運算元據庫成功,投遞訊息失敗(只是中介軟體響應的時候失敗,其實訊息是投遞出去了),訊息重發了咋麼辦?

業務方自己實現

我們擔心無非是兩個問題。

  • 第二步投遞失敗,重試次數上限現場丟失或者pod重啟現場丟失。

  • 訊息重複被消費

我們可以這樣,系統A新增一個訊息表(落盤的意思),系統A不再直接給訊息中介軟體傳送訊息,而是把要傳送的資料寫入訊息表,把扣錢和寫訊息表的動作放在同一個動作裡,保證兩者原子性。

系統A開啟一個後臺程式,把訊息表的訊息投遞給訊息中介軟體,如果失敗,不斷嘗試,除非訊息中介軟體明確響應ACK。可以確保訊息最終一定會傳送成功。

對於消費者系統B來說,從訊息中介軟體獲取訊息,除非明確響應ACK,否則的話中介軟體會認為這條訊息未被消費,會導致重複消費的問題。一般情況下,通過業務的唯一值去重即可。

這個方案的缺點是,業務方要新增一個訊息表,同時需要一個後臺任務。

基於事務訊息的中介軟體

比如RocketMQ會把訊息的傳送分成兩個階段:Prepare階段(訊息預傳送)和Confirm(確認傳送)。

步驟一:系統A呼叫Prepare介面,預傳送訊息。此時訊息儲存在訊息中介軟體中,不會傳送給消費者。

步驟二:系統A更新資料庫,進行扣錢操作。

步驟三:系統A呼叫Confirm介面,確認操作,確認之後訊息中介軟體才會把訊息投遞給消費者消費。

這就會涉及到,

  • 步驟一成功,步驟二成功,步驟三呼叫失敗,咋麼辦?

  • 步驟一成功,步驟二失敗,步驟三不會執行,咋麼辦?

RocketMQ關鍵點在於,它會定期去掃描那些所有預傳送但是還沒確認的訊息,回撥給傳送方,詢問訊息是傳送還是取消。傳送方根據具體的情況決定。 這個方案和上面自己實現的最大特點在於,掃描訊息表的工作被中介軟體接替了。

但是對應訊息表,業務還是需要儲存的,否則訊息回撥確認的時候,業務不知曉訊息具體該如何操作?

其他例如 TCC、Saga、對賬等方案可以自行檢視。

第四部分:業務架構之道

書中業務架構之道有一段對優秀的分層架構的描述還是非常認可的。

越底層的系統越單一、越簡單、越固化;越上層的系統花樣越多、越容易變化,要做到這一點,需要層與層之間有很好的隔離和判斷。

關於邊界思維,書中提到,

架構強調的不是系統能支援什麼,而是系統的”約束”是什麼,不管是業務架構,還是技術架構,沒有”約束”,就沒有架構。 一個設計或者系統,如果無所不能,則意味著一無所能。

在”為何很難設計一個好的領域模型” 討論中,我們普遍意識中的幾個因素:

  • 現實業務的複雜性,職責的分化,短期內很少深入理解業務。

  • 業務迭代速度太快。

書中提到一個可能很多人都沒想過的問題:意識問題,也可以稱為思維問題。

在使用者、產品人員、運營人員眼中,溝通的語言是流程而不是模型。開發人員在和他們溝通中,慢慢形成以流程為主導,而不是以模型為主導的思維方式。這就導致開發過程是流程驅動的,而不是領域驅動。大家在討論業務和系統解決方案的時候,大多時間都花在了業務流程、業務規則上,而不是挖掘流程背後的不變因素。

我突然意識到,這個也是決定普通程式設計師和高階程式設計師的因素之一。

分享完畢。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
吳親庫裡

相關文章