七層流量接入系統(tǒng)

和大家聊一聊七層流量接入中間件。

1. 接入系統(tǒng)簡介與架構(gòu)

1.1 Go反向代理

Go語言實現(xiàn)一個訂制化的反向代理。

Go語言

近幾年在國內(nèi)較流行,隨著docker的成名而愈加受人追捧。目前國內(nèi)使用Go開發(fā)的團隊和系統(tǒng)越來越多,像百度的BFE、360的長連接推送、七牛云存儲、滴滴登錄認(rèn)證等,名單很長。

Go比較適合于中間件(反向代理、消息隊列等)以及旁路系統(tǒng)(存儲、長連接推送等)的開發(fā),也有很多團隊開始使用Go來編寫Web

API(使用beego框架)。Go語言提供原生的協(xié)程(go

routine)支持,天生高并發(fā),而且是用同步邏輯編寫異步程序(語言底層封裝了異步I/O),開發(fā)效率極高。

重復(fù)造輪子

業(yè)界有很多成熟的反向代理(Nginx、Tengine),為何不基于這些開源的項目二次開發(fā)?主要考慮三點:1)開發(fā)效率;2)訂制化;3)維護成本。

Nginx和Tengine都基于C語言開發(fā),C語言的開發(fā)效率相對較低,維護成本更大,在語言選型上更傾向于Go;考慮到訂制化以及精簡易懂(代碼量小、邏輯少),傾向于重新實現(xiàn)一個反向代理,保持高擴展性的同時剔除不必要的邏輯。其實,主導(dǎo)思想就是:忌大而全。

1.2 一核多路

作為一個流量入口,其穩(wěn)定性是設(shè)計時首要考慮的目標(biāo),于是提出了一核多路的思想。即一個涵蓋基本功能且穩(wěn)定運行的核心 + 多個擴展功能的旁路系統(tǒng)。有點像微服務(wù)的理念。

核心Server

Server必須保證高并發(fā)且提供可以擴展的口子。

Nginx通過master-worker多進程 + 異步I/O來實現(xiàn)高并發(fā)。借助于Go語言的優(yōu)勢,要實現(xiàn)高并發(fā)顯得更加容易。用一個master的goroutine + 多個worker的goroutine即可。確切地,是每來一個http的請求,就新建一個goroutine。

每個worker協(xié)程中提供加解密、分流、防抓取以及轉(zhuǎn)發(fā)等邏輯。借鑒Nginx的請求處理過程(phase),Server將請求抵達到請求返回劃分為多個階段,選擇幾個具有典型的階段為回調(diào)注冊點。所謂回調(diào)注冊點,即允許在此點上注冊handler函數(shù),當(dāng)請求執(zhí)行到此流程點后,Server回調(diào)此函數(shù)。每個回調(diào)點對應(yīng)一個handlers數(shù)組,在回調(diào)時,handlers被順序執(zhí)行。顯然回調(diào)點機制提供了極方便的擴展性。

核心Server

每個worker協(xié)程中提供加解密、分流、防抓取以及轉(zhuǎn)發(fā)等邏輯。借鑒Nginx的請求處理過程(phase),Server將請求抵達到請求返回劃分為多個階段,選擇幾個具有典型的階段為回調(diào)注冊點。所謂回調(diào)注冊點,即允許在此點上注冊handler函數(shù),當(dāng)請求執(zhí)行到此流程點后,Server回調(diào)此函數(shù)。每個回調(diào)點對應(yīng)一個handlers數(shù)組,在回調(diào)時,handlers被順序執(zhí)行。顯然回調(diào)點機制提供了極方便的擴展性。

Module模型

回調(diào)點上的handler代表一個功能,Module則代表一類功能實體。即,一個Module可以在多個回調(diào)點上分別注冊handler。譬如數(shù)據(jù)報表客戶端和訪問日志的Module就在兩個回調(diào)點上分別注冊了handler。

Module模型的出現(xiàn),就象征著一核多路設(shè)計理念的實踐與落地。當(dāng)監(jiān)控到系統(tǒng)運行狀態(tài)負載過高、壓力過大后,可以采取停掉某些Module的方式保障核心的穩(wěn)定,實現(xiàn)服務(wù)降級。

2. 配置熱更與優(yōu)雅重啟

接入服務(wù),分流規(guī)則的變更、業(yè)務(wù)后端機器的變更、新增接入業(yè)務(wù)都是不可避免的事情,況且服務(wù)本身的升級與迭代更是持續(xù)的過程。如何在進行變更的時候保證系統(tǒng)服務(wù)的持續(xù)運行以及變更效率是接下來要聊得話題。

2.1 配置熱更

在不停服務(wù)的情況下完成配置的變更叫配置熱更。熱更的唯一難點在于更換前后數(shù)據(jù)的一致

性,即,當(dāng)t時刻發(fā)生配置熱更,對于t時刻以前抵達正的請求應(yīng)該使用變更前的配置;對于t時候后抵達的請求,應(yīng)該采用變更后的配置。

業(yè)界一般有兩種解決方案:fork進程和指針切換。

fork進程

master進程fork出一個子進程來load配置,當(dāng)load完成后,master進程使配置變更前的子進程優(yōu)雅退出。此種方案的好處是不必考慮各配置間的耦合性;缺點是實現(xiàn)起來略復(fù)雜。Nginx采用此種方案。

指針切換

通過切換配置數(shù)據(jù)內(nèi)存的地址,來實現(xiàn)變更。偽代碼如下:

指針切換偽代碼


此方案之所以簡單,依賴一個前提:語言本身提供gc機制。對于t時刻前的請求未服務(wù)完前,舊的配置內(nèi)存不會被釋放;當(dāng)所有t時刻的請求都服務(wù)完后,gc回收內(nèi)存。基于Go實現(xiàn)的接入系統(tǒng),自然選擇指針切換的方式實現(xiàn)配置熱更。

配置熱更其實還有一個潛在的巨大收益:平臺化。將平臺開放給各業(yè)務(wù)同學(xué),從而解放接入系統(tǒng)維護人員的雙手(再也不用每天接收大量的配置變更任務(wù)了)。

2.2 優(yōu)雅重啟

大家在進行系統(tǒng)迭代升級時,不得不面臨發(fā)布上線的問題。如何能不停服務(wù)的情況實現(xiàn)系統(tǒng)的升級了?一般業(yè)界有三種解決方案:reuse port、fork + exec和healthcheck + supervisord。

reuse port

linux內(nèi)核3.9提供了SO_REUSEPORT的屬性,通過此選項可以讓不同的進程bind()/listen()同一個TCP/UDP端口。意味著,當(dāng)我們進行代碼迭代時,可以在線上同時運行新舊兩份代碼,當(dāng)新代碼服務(wù)穩(wěn)定后,讓舊的進程退出從而完成代碼平滑升級。利用此方案有兩個點:1)內(nèi)核要求;2)舊服務(wù)優(yōu)雅退出。優(yōu)雅退出并不困難,發(fā)送一個信號給舊進程,舊進程關(guān)閉掉listen端口,等系統(tǒng)中殘留的請求服務(wù)完后退出即可。

fork + exec

master進程fork子進程,用exec調(diào)用自己,當(dāng)系統(tǒng)服務(wù)運行后,發(fā)送信號給master進程,master進程關(guān)閉listen然后退出,這是一種最優(yōu)雅的重啟方式。偽碼如下:

優(yōu)雅重啟

healthcheck + supervisord

發(fā)布前先摘掉機器上的healthcheck文件,等系統(tǒng)流量干凈后,開始替換系統(tǒng)代碼文件(二進制或其它),然后kill掉服務(wù)進程,supervisord拉起,從而實現(xiàn)升級,發(fā)布完后,添加healthcheck文件。

此種方案是業(yè)界使用最多的,很trik,但卻比較有效。

3. 最佳實踐

3.1 GC優(yōu)化

go的gc優(yōu)化主要有如下幾種方法:小對象合并、棧上分配對象、sync.pool、cgo、內(nèi)存池和升級go版本。

其中cgo、內(nèi)存池和升級go版本效果明顯。cgo是Go提供的調(diào)用c代碼的方式,運行效率相較于go要高;go1.7的release,和1.4.2版本對比測試,gc的停頓時間減少了30%。內(nèi)存池(下圖所示),即自行管理內(nèi)存,在gc看來是一個對象,因此也能很好的緩解gc。

內(nèi)存池模型

3.2 TCP + protobuf

基于TCP協(xié)議,設(shè)計私有協(xié)議時,一般需要定義一個header和msg,如下圖所示。對于msg的內(nèi)容一般希望接收端能夠直接映射成數(shù)據(jù)結(jié)構(gòu),即需要序列化和反序列化。常見的序列化協(xié)議很多,譬如json、thrift、hessian、protobuf等。我個人比較推薦protobuf,主要protobuf支持的語言較多、協(xié)議字段兼容性好、序列化反序列化速度快以及大廠(google)支持。

私有協(xié)議

3.3 UDP

UDP作為一個無連接的協(xié)議,采用“盡力而為”的傳輸主旨,即不考慮網(wǎng)絡(luò)狀況和對方的接收能力,只要能發(fā)就盡力發(fā),毫無顧慮。UDP的這個特性就意味著它不保證數(shù)據(jù)準(zhǔn)確送達也不保證有序,對于擁塞的網(wǎng)絡(luò)可能會使得網(wǎng)絡(luò)情況更糟糕,這是UDP相較于TCP的缺點。但,卻也正因為此,UDP傳輸效率高。

對于內(nèi)網(wǎng)間的通信,網(wǎng)絡(luò)情況可控,評估業(yè)務(wù)的特點(譬如實時性要求高,但可以容忍一定程度的丟包),可以嘗試使用UDP作為傳輸協(xié)議。下圖是我實測的內(nèi)網(wǎng)跨機房間的udp丟包率。

udp丟包率測試數(shù)據(jù)

3.4 Unix Domain Socket

對于進程間通信,Unix Domain Socket相較于Socket通信有較大的優(yōu)勢,其不需要進行網(wǎng)絡(luò)協(xié)議棧的處理,簡單的通過內(nèi)存間的拷貝,速度極快。對于同機部署的服務(wù)間的通信,是個不錯的選擇。

至于Unix Domain Socket中面向連接的字節(jié)流和面向無連接數(shù)據(jù)報,兩種都能保證數(shù)據(jù)準(zhǔn)確抵達、且有序,唯一的差異只在于語義。譬如,Read操作時,字節(jié)流的服務(wù),可以多次調(diào)用Read操作來接收發(fā)送端發(fā)送的一個報文數(shù)據(jù);但數(shù)據(jù)包的服務(wù),則一個包只允許一次Read操作。

4. 服務(wù)降級與預(yù)案

接入系統(tǒng)作為一個流量入口,穩(wěn)定性首當(dāng)其沖,當(dāng)發(fā)生攻擊和后端業(yè)務(wù)故障后,我們必須要有應(yīng)對的方案。本章和大家討論此問題。

4.1 服務(wù)降級

入口流量控制

當(dāng)遭遇攻擊導(dǎo)致流量突增后,可能導(dǎo)致系統(tǒng)資源瞬間耗盡而被操作系統(tǒng)殺掉進程。應(yīng)對這種情況的比較粗暴的方法就是設(shè)置一個全局的計數(shù)器,此計數(shù)器記錄了當(dāng)前系統(tǒng)中駐留的請求的數(shù)目,一旦其值超過某個閾值后,新進來的請求將被拒絕。

如果要優(yōu)雅的解決上述問題,則需要一個旁路的DDos防攻擊的系統(tǒng),其對請求進行多維度的計數(shù),當(dāng)達到一個閾值后,下發(fā)指令至接入系統(tǒng),接入系統(tǒng)對新進來匹配上標(biāo)記特性的請求實施拒絕。

業(yè)務(wù)隔離

當(dāng)后端業(yè)務(wù)發(fā)生嚴(yán)重的超時故障后,轉(zhuǎn)發(fā)至此業(yè)務(wù)的請求出現(xiàn)超時重試,一段時間內(nèi),系統(tǒng)中被消耗的資源隨著時間推移而線性增加,導(dǎo)致GC壓力過大,從而影響其它業(yè)務(wù)的響應(yīng)時間。為了應(yīng)對此種情況,設(shè)計了業(yè)務(wù)配額機制來使業(yè)務(wù)故障隔離,如下圖所示。


業(yè)務(wù)隔離流程圖

設(shè)置各業(yè)務(wù)的配額有兩種機制:靜態(tài)和動態(tài)。靜態(tài)配額嚴(yán)格依賴實驗和經(jīng)驗,不是最優(yōu)配置,但實現(xiàn)簡單;動態(tài)配額,能最大程度的利用系統(tǒng)資源,但實現(xiàn)難度稍大。

4.2 預(yù)案

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容