Skip to content

Instantly share code, notes, and snippets.

@liuxd
Last active June 20, 2024 23:09
Show Gist options
  • Save liuxd/53991ff3b5be630e01b7c4cbb1818a9b to your computer and use it in GitHub Desktop.
Save liuxd/53991ff3b5be630e01b7c4cbb1818a9b to your computer and use it in GitHub Desktop.
[#16 網站開發心法:架構篇] #編程之道

網站開發心法:架構篇

初稿日期:2023-03-31

架構之義,在於對抗衰老。

前言

增長,意味著衰老。硬件會衰老,時間久了,CPU、硬盤、內存等等都會慢慢壞掉;軟件會衰老,隨著業務增長邏輯規模越發龐大,內部就會滋生不協調,以至問題變多,修復困難,增加新功能就像俄羅斯輪盤賭一樣刺激。

系統架構,就是對抗這些系統老化問題而做的努力。架構不是一成不變的,就像軟件工程沒有銀彈一樣,系統架構也沒有萬能的。架構是隨著系統規模和業務特點而一起成長的。但是追根溯源,問題是相似的,方法也是類似的。

根本問題是規模變大。請求規模變大、數據規模變大、邏輯規模變大。應對方法是分而治之。請求分流,數據分層,邏輯分塊。具體的分法有一些套路可以參考,這裡總結出來,作為系統架構設計的武器庫,隨用隨取。

1 請求分流

請求之道,將洪流化百川。

1.1 負載均衡

一個負載均衡服務器(Load Balance, 簡稱LB),後面跟著一堆應用服務器。LB接受全部用戶請求,但是不直接處理,而是分發給後面的應用服務器做具體的工作。得到結果後再通過LB反饋給用戶。

心跳檢查。既然要分配工作,LB就需要知道自己身後有哪些小弟可以使喚。所以,LB需要維護一份應用服務器列表。不僅如此,還要定時檢查是否所有小弟都健康,可以接受新工作。所以LB會以固定頻率請求所有應用服務器,就像查崗。如果反饋錯誤或者超時,那就說明該服務器不健康,需要踢出分配工作的列表,就像開除。這一查崗、開除的機制稱為“心跳檢查”。

自動擴容。應用服務器都是克隆人,每台機器的軟件硬件都相同。這一標準化實踐有利於彈性擴容。當LB感知到壓力超過一定閾值時,可以自動部署新的應用服務器,並加入列表。這樣就可以實現自動擴容,無縫應對暴漲的請求量,而無需等待人工干預。

1.2 主從同步

讀操作分散。主從同步是數據庫層面的作法。一個主庫用來接受寫操作,多個從庫即時同步數據,接受查詢請求。這樣就可以實現讀操作的壓力分散。

寫操作分散。那麼如果寫操作壓力很大怎麼辦?可以從數據層面進行劃分,將數據按照不同維度劃分到多個庫中,實現多個主庫帶各自多個從庫。每個主從群稱為一個 “簇” 。由於數據體量大幅變小了,各自的壓力也就都減小了。但是聯合使用多個庫也比較麻煩,需要建立一個中間層來整合多個數據來源,以實現虛擬的數據整體感。這個中間層,就叫作“中間件”。

1.3 消息隊列

大型系統架構中必然存在多個實體,彼此需要互相通信。通信方式有多種,可推、可拉、可隊列,其中隊列最特別。

消息隊列有兩端。一端是生產者,往隊列中塞請求;另一端是消費者,從隊列中領請求。兩端都可以是複數的,多個生產者塞進來請求,多個消費者前來完成請求。這就像多個餐館把外賣放到同一個地方,多個快遞員前來領外賣並完成配送。這個“同一個地方”就是所謂的消息隊列。消息隊列相比直來直去的通信,犧牲了實時性,換來了可控性。

2 數據分層

數據之道,按溫度分等級。

2.1 全數據:索引層

海量複雜數據的查詢向來是一大難題。基於用戶查詢維度建立索引相比直接查詢數據不僅更快,而且功能更強,因為可以加入一些原生不支持的篩選條件。這一層通常叫做“搜索引擎”,顧名思義,就是專注於搜索數據。但是,它只找到數據的地址,而不是完整的數據內容。有了地址,找數據就簡單了。

2.2 熱數據:緩存層

本質。緩存的價值,在於將複雜過程的結果保存起來,當請求時直接給出結果,而不是一再重複昂貴的計算。但是保存結果是需要空間的,所以本質上,緩存就是拿空間換時間。

要點。緩存的要點在於命中率和數據更新。如果不命中,那就是單純浪費空間。如果不更新,那結果就是過時的。數據更新可以是懶加載模式,當請求發生了,計算一次,就納入緩存。這種方式經濟實惠,但是第一個請求的用戶比較倒霉,他沒有享受到緩存的快速。而且有可能造成數據庫請求高峰,非常危險。另一種方式是提前預熱,在緩存面向用戶之前,把所有需要的數據都更新好。這樣過程略長,但是用戶體驗好,而且數據庫很安全,沒有暴露在用戶請求的可能。

踢出。如果緩存空間無法覆蓋完整數據集合的話,還要考慮踢出策略。是按更新時間,還是按命中次數,或者按命中時間。策略選擇的依據是命中率。在空間固定的情況下,命中率最高的那個就是最好的選擇。

2.3 溫數據:數據庫

專注存儲。數據庫保存一切,因此往往是瓶頸所在。為了減少它的壓力,應堅持簡單使用原則,即只專注保存數據,盡可能不負責任何邏輯運算。不要搞什麼過程、觸發器、聯表查詢、視圖等亂七八糟的東西。那些東西讓代碼做,讓搜索引擎做。數據庫已經很累了,而且是最累的那個,不要再給它增加不必要的壓力了。

表的設計。此外,數據表設計也需要針對性能進行優化。包括兩點:定長字段與變長字段分離,索引設計。定長字段速度快於變長字段,如果兩者混用,快的會被慢的拖後腿。索引則需要配合業務需求進行設計,確保查詢可以用得上索引,避免全表掃描。

2.4 冷數據:歸檔層

有些用戶數據半死不活,甚至死透了,躺在數據庫裏只是單純浪費資源,拖慢系統。這些疑似垃圾應該拉出來,丟在一邊,以減少數據體量,減輕系統壓力。

萬一用戶又回來,可以走個流程把數據再恢復到數據庫。這個流程是他離家出走、遲遲不歸的代價。但是這個等待期限不是永遠,它是有限的。在即將到期時給用戶發出最後通牒,如果還是不回來,那說明緣分已盡了,或者那個用戶已經掛了。此時,就可以徹底刪除數據了。萬一該用戶哪天詐屍又活了,並想再回來,就得重新註冊,開始全新生活。

3 邏輯分塊

邏輯之道,先切小再打包。

龐大的代碼集合拆成多個更小點的塊,再有機的組合起來。組合方式其實可以有多種,比如:子代碼庫、自定義依賴庫、SDK等等,但是最常用的還是服務。

3.1 通用技術

無關具體業務,僅專注提供技術支持。

第三方服務。多個產品可能用到同一個第三方服務,沒必要每個產品都集成一遍。技術服務可以集成之後再接入多個產品。這樣還有利於統一管理。

通用類操作。比如:文件轉換、圖片處理、日誌管理等等,與具體業務無關,又被廣泛需要。這類服務往往屬於運算密集型服務,計算量巨大。有鑑於此,往往會使用編譯語言來實現,並通過並行運算、Map/Reduce來提高對海量數據的計算效率。

3.2 通用業務

它是從業務邏輯層面將可復用的邏輯抽象出來,打包成服務供多個產品調用。說白了就是大號的函數,主要是為了避免多個產品之間的複製粘貼,或者互相請求API。

相比技術服務,業務邏輯的復用範圍就不那麼清晰了,需要對業務有深刻理解和長遠預判。封裝粒度過細會導致過度設計,做大量無用功,拖慢項目進度;封裝粒度不足會導致靈活性不足,進而之後陷入兩難境地——要麼另外開坑做類似的事情,乾淨、安全,但是重複度高;要麼修改原有的系統以適應新的邏輯,但是容易產生附加風險,破壞已有功能,測試非常困難。

一個比較安全的方案,是建立新的靈活性更合適的服務來做新業務,然後逐步將舊業務分批搬過來。這樣既能讓新業務輕裝上陣,又能安全的把舊系統逐漸淘汰,實現系統架構升級。

結語

如此三分,規模可控。

總之,就是一個“拆”字訣。什麼多了拆什麼,粗的拆成細的、厚的拆成薄的、大的拆成小的,把巨無霸拆成小單元。拆吧拆吧,處理不了的東西就變得能處理了。咱們不玩英雄單挑,咱們玩人海戰術。一堆小兵,每人做一件小事,湊在一起便成就了偉業。

系統架構圖是一棵樹。隨著系統體量增長,這棵樹也會生長。漸漸地變得枝繁葉茂,從一棵弱不禁風的小樹苗變成遮天蔽日的參天大樹。這是業務的勝利,也是運維的壓力。

這裡只提供了一些常見的套路,每個套路都有自己的應用場景,能解決若干問題,但是沒有哪個是完美的。實踐中按需索取,謹慎應用,只有對這些套路有深層的理解才能合理使用。要知道它們都不是免費的,每個套路除了可能解決一些問題外,還需要付出一些代價。如果用錯了,不僅問題沒解決,反而讓狀況更加糟糕了,那就快更新簡歷吧。

架構之妙,存乎一心。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment