這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
和大家聊一聊七層流量接入中介軟體。
1. 接入系統簡介與架構
1.1 Go反向 Proxy
用Go語言實現一個訂製化的反向 Proxy。
Go語言
近幾年在國內較流行,隨著docker的成名而愈加受人追捧。目前國內使用Go開發的團隊和系統越來越多,像百度的BFE、360的長串連推送、七牛雲端儲存、滴滴登入認證等,名單很長。
Go比較適合於中介軟體(反向 Proxy、訊息佇列等)以及旁路系統(儲存、長串連推送等)的開發,也有很多團隊開始使用Go來編寫Web
API(使用beego架構)。Go語言提供原生的協程(go
routine)支援,天生高並發,而且是用同步邏輯編寫非同步程式(語言底層封裝了非同步I/O),開發效率極高。
重複造輪子
業界有很多成熟的反向 Proxy(Nginx、Tengine),為何不基於這些開源的項目二次開發?主要考慮三點:1)開發效率;2)訂製化;3)維護成本。
Nginx和Tengine都基於C語言開發,C語言的開發效率相對較低,維護成本更大,在語言選型上更傾向於Go;考慮到訂製化以及精簡易懂(代碼量小、邏輯少),傾向於重新實現一個反向 Proxy,保持高擴充性的同時剔除不必要的邏輯。其實,主導思想就是:忌大而全。
1.2 一核多路
作為一個流量入口,其穩定性是設計時首要考慮的目標,於是提出了一核多路的思想。即一個涵蓋準系統且穩定啟動並執行核心 + 多個擴充功能的旁路系統。有點像微服務的理念。
核心Server
Server必須保證高並發且提供可以擴充的口子。
Nginx通過master-worker多進程 + 非同步I/O來實現高並發。藉助於Go語言的優勢,要實現高並發顯得更加容易。用一個master的goroutine + 多個worker的goroutine即可。確切地,是每來一個http的請求,就建立一個goroutine。
每個worker協程中提供加解密、分流、防抓取以及轉寄等邏輯。借鑒Nginx的請求處理過程(phase),Server將請求抵達到請求返回劃分為多個階段,選擇幾個具有典型的階段為回調註冊點。所謂回調註冊點,即允許在此點上註冊handler函數,當請求執行到此流程點後,Server回調此函數。每個回調點對應一個handlers數組,在回調時,handlers被順序執行。顯然回調點機制提供了極方便的擴充性。
核心Server
每個worker協程中提供加解密、分流、防抓取以及轉寄等邏輯。借鑒Nginx的請求處理過程(phase),Server將請求抵達到請求返回劃分為多個階段,選擇幾個具有典型的階段為回調註冊點。所謂回調註冊點,即允許在此點上註冊handler函數,當請求執行到此流程點後,Server回調此函數。每個回調點對應一個handlers數組,在回調時,handlers被順序執行。顯然回調點機制提供了極方便的擴充性。
Module模型
回調點上的handler代表一個功能,Module則代表一類功能實體。即,一個Module可以在多個回調點上分別註冊handler。譬如資料報表用戶端和訪問日誌的Module就在兩個回調點上分別註冊了handler。
Module模型的出現,就象徵著一核多路設計理念的實踐與落地。當監控到系統運行狀態負載過高、壓力過大後,可以採取停掉某些Module的方式保障核心的穩定,實現服務降級。
2. 配置熱更與優雅重啟
接入服務,分流規則的變更、業務後端機器的變更、新增接入業務都是不可避免的事情,況且服務本身的升級與迭代更是持續的過程。如何在進行變更的時候保證系統服務的持續運行以及變更效率是接下來要聊得話題。
2.1 配置熱更
在不停服務的情況下完成配置的變更叫配置熱更。熱更的唯一痛點在於更換前後資料的一致
性,即,當t時刻發生配置熱更,對於t時刻以前抵達正的請求應該使用變更前的配置;對於t時候後抵達的請求,應該採用變更後的配置。
業界一般有兩種解決方案:fork進程和指標切換。
fork進程
master進程fork出一個子進程來load配置,當load完成後,master進程使配置變更前的子進程優雅退出。此種方案的好處是不必考慮各配置間的耦合性;缺點是實現起來略複雜。Nginx採用此種方案。
指標切換
通過切換配置資料記憶體的地址,來實現變更。虛擬碼如下:
指標切換虛擬碼
此方案之所以簡單,依賴一個前提:語言本身提供gc機制。對於t時刻前的請求未服務完前,舊的配置記憶體不會被釋放;當所有t時刻的請求都服務完後,gc回收記憶體。基於Go實現的接入系統,自然選擇指標切換的方式實現配置熱更。
配置熱更其實還有一個潛在的巨大收益:平台化。將平台開放給各業務同學,從而解放接入系統維護人員的雙手(再也不用每天接收大量的配置變更任務了)。
2.2 優雅重啟
大家在進行系統迭代升級時,不得不面臨發布上線的問題。如何能不停服務的情況實現系統的升級了?一般業界有三種解決方案:reuse port、fork + exec和healthcheck + supervisord。
reuse port
linux核心3.9提供了SO_REUSEPORT的屬性,通過此選項可以讓不同的進程bind()/listen()同一個TCP/UDP連接埠。意味著,當我們進行代碼迭代時,可以線上上同時運行新舊兩份代碼,當新代碼服務穩定後,讓舊的進程退出從而完成代碼平滑升級。利用此方案有兩個點:1)核心要求;2)舊服務優雅退出。優雅退出並不困難,發送一個訊號給舊進程,舊進程關閉掉listen連接埠,等系統中殘留的請求服務完後退出即可。
fork + exec
master進程fork子進程,用exec調用自己,當系統服務運行後,發送訊號給master進程,master進程關閉listen然後退出,這是一種最優雅的重啟方式。偽碼如下:
優雅重啟
healthcheck + supervisord
發布前先摘掉機器上的healthcheck檔案,等系統流量乾淨後,開始替換系統代碼檔案(二進位或其它),然後kill掉服務進程,supervisord拉起,從而實現升級,發布完後,添加healthcheck檔案。
此種方案是業界使用最多的,很trik,但卻比較有效。
3. 最佳實務
3.1 GC最佳化
go的gc最佳化主要有如下幾種方法:小對象合并、棧上指派至、sync.pool、cgo、記憶體池和升級go版本。
其中cgo、記憶體池和升級go版本效果明顯。cgo是Go提供的調用c代碼的方式,運行效率相較於go要高;go1.7的release,和1.4.2版本對比測試,gc的停頓時間減少了30%。記憶體池(所示),即自行管理記憶體,在gc看來是一個對象,因此也能很好的緩解gc。
記憶體池模型
3.2 TCP + protobuf
基於TCP協議,設計私人協議時,一般需要定義一個header和msg,如所示。對於msg的內容一般希望接收端能夠直接映射成資料結構,即需要序列化和還原序列化。常見的序列化協議很多,譬如json、thrift、hessian、protobuf等。我個人比較推薦protobuf,主要protobuf支援的語言較多、協議欄位相容性好、序列化還原序列化速度快以及大廠(google)支援。
私人協議
3.3 UDP
UDP作為一個不需連線的協議,採用“儘力而為”的傳輸主旨,即不考慮網路狀況和對方的接收能力,只要能發就儘力發,毫無顧慮。UDP的這個特性就意味著它不保證資料準確送達也不保證有序,對於擁塞的網路可能會使得網路情況更糟糕,這是UDP相較於TCP的缺點。但,卻也正因為此,UDP傳輸效率高。
對於內網間的通訊,網路情況可控,評估業務的特點(譬如即時性要求高,但可以容忍一定程度的丟包),可以嘗試使用UDP作為傳輸協議。是我實測的內網跨機房間的udp丟包率。
udp丟包率測試資料
3.4 Unix Domain Socket
對於處理序間通訊,Unix Domain Socket相較於Socket通訊有較大的優勢,其不需要進行網路通訊協定棧的處理,簡單的通過記憶體間的拷貝,速度極快。對於同機部署的服務間的通訊,是個不錯的選擇。
至於Unix Domain Socket中連線導向的位元組流和面向無串連資料報,兩種都能保證資料準確抵達、且有序,唯一的差異只在於語義。譬如,Read操作時,位元組流的服務,可以多次調用Read操作來接收發送端發送的一個報文資料;但資料包的服務,則一個包只允許一次Read操作。
4. 服務降級與預案
接入系統作為一個流量入口,穩定性首當其衝,當發生攻擊和後端業務故障後,我們必須要有應對的方案。本章和大家討論此問題。
4.1 服務降級
入口流量控制
當遭遇攻擊導致流量突增後,可能導致系統資源瞬間耗盡而被作業系統殺掉進程。應對這種情況的比較粗暴的方法就是設定一個全域的計數器,此計數器記錄了當前系統中駐留的請求的數目,一旦其值超過某個閾值後,新進來的請求將被拒絕。
如果要優雅的解決上述問題,則需要一個旁路的DDos防攻擊的系統,其對請求進行多維度計數,當達到一個閾值後,下髮指令至接入系統,接入系統對新進來匹配上標記特性的請求實施拒絕。
業務隔離
當後端業務發生嚴重的逾時故障後,轉寄至此業務的請求出現逾時重試,一段時間內,系統中被消耗的資源隨著時間推移而線性增加,導致GC壓力過大,從而影響其它業務的回應時間。為了應對此種情況,設計了業務配額機制來使業務故障隔離,如所示。
業務隔離流程圖
設定各業務的配額有兩種機制:靜態和動態。靜態配額嚴格依賴實驗和經驗,不是最優配置,但實現簡單;動態配額,能最大程度的利用系統資源,但實現難度稍大。
4.2 預案
略