如何設(shè)計(jì)一個(gè)麻雀般的微型分布式架構(gòu)?
序言(初衷)
設(shè)計(jì)該系統(tǒng)初衷是基于描繪業(yè)務(wù)(或機(jī)器集群)存儲(chǔ)模型,分析代理緩存服務(wù)器磁盤存儲(chǔ)與回源率的關(guān)系。系統(tǒng)意義是在騰訊云成本優(yōu)化過(guò)程中,量化指導(dǎo)機(jī)房設(shè)備擴(kuò)容。前半部分是介紹背景,對(duì)CDN緩存模型做一些理論思考。后半部分會(huì)實(shí)際操作搭建一個(gè)微型但是五臟俱全的分布式通用系統(tǒng)架構(gòu),最后賦予該系統(tǒng)一些跟背景相關(guān)的功能,解決成本優(yōu)化中遇到的實(shí)際問(wèn)題。
緩存服務(wù)器存儲(chǔ)模型架構(gòu)(背景):

img
圖1 存儲(chǔ)模型
騰訊CDN的線上路由是用戶à分布于各地區(qū)各運(yùn)營(yíng)商的OC->SOC->SMid->源站。各個(gè)層級(jí)節(jié)點(diǎn)部署的都是緩存服務(wù)器。來(lái)自用戶的部分請(qǐng)求流量命中服務(wù)器,另一部分產(chǎn)生回源流量。
隨著業(yè)務(wù)帶寬自然增長(zhǎng),用戶端帶寬增長(zhǎng),假設(shè)業(yè)務(wù)回源率不變的情況下,磁盤緩存淘汰更新(淘汰)速率變快,表現(xiàn)為以下業(yè)務(wù)瓶頸(iowait變高、回源帶寬變高,由于磁盤空間大小受限的緩存淘汰導(dǎo)致回源率變高)。
為了說(shuō)明這個(gè)原理。我們假設(shè)兩個(gè)極端:一個(gè)是設(shè)備磁盤容量無(wú)限大,業(yè)務(wù)過(guò)來(lái)的流量緩存只受源站緩存規(guī)則受限。只要緩存沒(méi)過(guò)期,磁盤可以無(wú)限緩存,回源流量只需要首次訪問(wèn)的流量,所以這個(gè)回源量(率)只跟業(yè)務(wù)特性(重復(fù)率)有關(guān)系。另一個(gè)極端是磁盤極限?。w零),那么無(wú)論業(yè)務(wù)設(shè)置緩存是否過(guò)期,客戶端訪問(wèn)量都是1比1的回源量。假設(shè)業(yè)務(wù)平均的緩存周期是1個(gè)小時(shí)。那么這1個(gè)小時(shí)的首次緩存帶寬(同一cache key的多次訪問(wèn),我們認(rèn)為是一次)將是這個(gè)硬盤的所需要的空間。這個(gè)大小是合理的,可以保證磁盤足夠容納業(yè)務(wù)的量。假設(shè)這個(gè)量達(dá)不到,或者本來(lái)達(dá)到了,但是由于業(yè)務(wù)自然增長(zhǎng)了,1個(gè)小時(shí)內(nèi)地首次緩存帶寬變多,硬盤空間也不夠用。
設(shè)備擴(kuò)容是個(gè)解決辦法。但是壓測(cè)系統(tǒng)在這之前,沒(méi)有客觀數(shù)據(jù)證明需要擴(kuò)容多大設(shè)備?;蛘邤U(kuò)容多少設(shè)備沒(méi)有進(jìn)行灰度驗(yàn)證,設(shè)備到位拍腦袋直接線上部署機(jī)器。我們?cè)趯?shí)驗(yàn)機(jī)器進(jìn)行線上日志的重放,模擬出存儲(chǔ)模擬曲線,來(lái)指導(dǎo)線上機(jī)房合理的設(shè)備存儲(chǔ)。這就是建設(shè)重放日志系統(tǒng)的意義。
麻雀雖小,五臟俱全的重放日志模型(總覽)
這一章,我們定義了下列模塊:
模擬日志服務(wù)器:下載線上某個(gè)機(jī)房的一段時(shí)間周期的訪問(wèn)日志。一個(gè)日志存放10分鐘訪問(wèn)記錄。機(jī)房有幾臺(tái)機(jī)器就下載幾份日志。日志服務(wù)器同時(shí)提供任務(wù)分片信息的查詢服務(wù)。假設(shè)我們需要重放任務(wù)id為pig_120t的任務(wù)切片。下圖既為任務(wù)切片詳情。

img
圖2 日志服務(wù)器的日志分片文件
任務(wù)控制器:?jiǎn)?dòng)任務(wù)或者結(jié)束任務(wù)總開(kāi)關(guān)。任務(wù)分配均勻分配給具體的肉雞和代理服務(wù)器。插入任務(wù)到Task Pool中,收集服務(wù)端的實(shí)時(shí)總流量、回源流量、總請(qǐng)求次數(shù)和回源次數(shù)數(shù)據(jù)并插入到回源率結(jié)果數(shù)據(jù)表。
肉雞:輪詢Task Pool的任務(wù)表。如果有任務(wù),則按照任務(wù)明細(xì)(時(shí)間、線上機(jī)房ip)向日志服務(wù)器請(qǐng)求下載該分片的日志。重放請(qǐng)求到指定的代理服務(wù)器。
代理服務(wù)端:提供實(shí)時(shí)回源數(shù)據(jù)查詢服務(wù)。并且安裝nws緩存服務(wù)器等組件,該機(jī)器等同于線上機(jī)房的軟件模塊。
實(shí)時(shí)展示界面:可隨時(shí)查看實(shí)時(shí)回源率和一些任務(wù)異常狀態(tài)信息。
圖3為客戶端和服務(wù)端的互動(dòng)圖。圖4是任務(wù)控制端在任務(wù)進(jìn)行中和其他模塊的聯(lián)動(dòng)過(guò)程。

img
圖3 肉雞和代理服務(wù)端的架構(gòu)

img
圖4 控制端的任務(wù)聯(lián)動(dòng)過(guò)程
分布式系統(tǒng)特點(diǎn)
日志重放模型核心是一個(gè)高性能壓測(cè)系統(tǒng),但是需要添加一些邏輯:日志下載、日志分析重構(gòu)、結(jié)果數(shù)據(jù)收集、數(shù)據(jù)上報(bào)展示。分布式系統(tǒng)核心是:是否做到了可拓展、可恢復(fù)、簡(jiǎn)易搭建、容錯(cuò)、自動(dòng)化。以下內(nèi)容會(huì)一一展開(kāi)。
先說(shuō)說(shuō)高性能:在一個(gè)通用模型中。我們模擬線上日志,這個(gè)系統(tǒng)要做到高效、因?yàn)槲覀兊闹胤湃罩舅俣纫染€上的qps還要快。機(jī)器的重放速度決定了分析結(jié)果的速度。同時(shí)更快的速度,所需要的肉雞資源更少。筆者在python各個(gè)url請(qǐng)求庫(kù)和golang中,最終敲定使用了golang實(shí)現(xiàn)肉雞。golang做到了和原生c+epoll一樣快的速度,但是代碼實(shí)現(xiàn)容易多了。理論上我們對(duì)一臺(tái)做過(guò)代理端性能瓶頸分析。線上日志比模擬日志更復(fù)雜,qps適度下降是必然的。Golang這個(gè)客戶端達(dá)到預(yù)期目標(biāo)。
可擴(kuò)展:在我們可能會(huì)隨時(shí)增加模擬機(jī)器集群的肉雞數(shù)量,或者更多的閑置代理服務(wù)器資源加入壓測(cè)任務(wù)。所以系統(tǒng)在可用機(jī)器數(shù)據(jù)表隨時(shí)加入新的機(jī)器。

img
圖5 系統(tǒng)的動(dòng)態(tài)可擴(kuò)展
可恢復(fù):分布式系統(tǒng)不同于單機(jī)模式。不能避免可能有各種故障,有時(shí)候系統(tǒng)部分節(jié)點(diǎn)出錯(cuò)了,我們更傾向于不用這個(gè)節(jié)點(diǎn),而不是繼續(xù)使用未處理完成的結(jié)果。即非0即1,無(wú)中間狀態(tài)。還有分布式系統(tǒng)網(wǎng)絡(luò)傳輸延遲不可控。所以壓測(cè)系統(tǒng)設(shè)計(jì)了一套容錯(cuò)機(jī)制:包括心跳檢測(cè)失敗,自動(dòng)在數(shù)據(jù)表剔除肉雞服務(wù)端。接口異常容錯(cuò)。超時(shí)過(guò)期未完成任務(wù)去除。crontab定時(shí)拉取退出進(jìn)程等。
簡(jiǎn)易搭建:使用ajs接口,和批處理安裝腳本。自動(dòng)化部署肉雞和服務(wù)端。配置dns解析ip(日志服務(wù)器,任務(wù)池、回源率結(jié)果所在的數(shù)據(jù)庫(kù)ip),tcp time_wait狀態(tài)的復(fù)用,千萬(wàn)別忘了還有一些系統(tǒng)限制放開(kāi)(放開(kāi)ulimit fd limit,這里設(shè)置100000,永久設(shè)置需要編輯/etc/security/limits.conf)。如果肉雞有依賴程序運(yùn)行庫(kù)需要同時(shí)下載。在肉雞機(jī)器下載肉雞客戶端和配置、在服務(wù)端機(jī)器下載服務(wù)端和配置,下載定時(shí)拉起程序腳本,并添加到crontab定時(shí)執(zhí)行。以上都用批處理腳本自動(dòng)執(zhí)行。
一些設(shè)計(jì)范式的思考
Single-productor and Multi-consumer
在肉雞客戶端的設(shè)計(jì)中:讀日志文件一行一條記錄,添加到消息管道,然后多個(gè)執(zhí)行worker從消息管道取url,執(zhí)行模擬請(qǐng)求。消息管道傳送的是一條待執(zhí)行的日志url。IO消耗型程序指的是如果consumer執(zhí)行訪問(wèn)日志并瞬間完成結(jié)果,但是productor需要對(duì)日志進(jìn)行復(fù)雜的字符串處理(例如正則之類的),那么它下次取不到數(shù)據(jù),就會(huì)被管道block住。另外一種是CPU消耗型程序,如果日志url已經(jīng)預(yù)先處理好了,productor只是簡(jiǎn)單的copy數(shù)據(jù)給消息管道。而consumer訪問(wèn)url,經(jīng)過(guò)不可預(yù)知的網(wǎng)絡(luò)延遲。那么多個(gè)consumer(因?yàn)槭前ňW(wǎng)絡(luò)訪問(wèn)時(shí)間,consumer個(gè)數(shù)設(shè)計(jì)超過(guò)cpu核數(shù),比如2倍)同時(shí)訪問(wèn),讀端速度慢于寫(xiě)端數(shù)度。在對(duì)一個(gè)日志文件進(jìn)行實(shí)驗(yàn),我們發(fā)現(xiàn)處理18w條記錄日志的時(shí)間是0.3s,而執(zhí)行完這些url的訪問(wèn)任務(wù)則需要3分鐘。那么很顯然這是一個(gè)CPU消耗性進(jìn)程。如果是IO消耗型的程序。Golang有種叫fan out的消息模型。我們可以這樣設(shè)計(jì):多個(gè)讀端去讀取多個(gè)chan list的chan,一個(gè)寫(xiě)端寫(xiě)一個(gè)chan。Fanout則將寫(xiě)端的chan,循環(huán)寫(xiě)到chan list的chan中。
Map-reduce
我們有時(shí)會(huì)做一個(gè)地理位置一個(gè)運(yùn)營(yíng)商的機(jī)房日志分析。一個(gè)機(jī)房包含數(shù)臺(tái)機(jī)器ip。合理的調(diào)度多個(gè)肉雞客戶端并行訪問(wèn)日志,可以更快速得到合并回源率數(shù)據(jù)。
并行機(jī)制,經(jīng)典的map-reduce,日志文件按機(jī)房機(jī)器ip緯度切片分發(fā)任務(wù),啟動(dòng)N個(gè)肉雞同時(shí)并行訪問(wèn),等最后一臺(tái)肉雞完成任務(wù)時(shí),歸并各個(gè)肉雞數(shù)據(jù)按成功請(qǐng)求數(shù)量、成功請(qǐng)求流量、失敗請(qǐng)求數(shù)量、失敗請(qǐng)求流量等方式做統(tǒng)計(jì)。同時(shí)用于和線上日志做校樣。這里的mapper就是肉雞,產(chǎn)生的數(shù)據(jù)表,我們按照關(guān)注的類型去提取就是reducer。
簡(jiǎn)化的map-reducer(不基于分布式文件系統(tǒng)),map和reduce中間的數(shù)據(jù)傳遞用數(shù)據(jù)表實(shí)現(xiàn)。每個(gè)mapper產(chǎn)生的日志數(shù)據(jù)先放在本地,然后再上報(bào)給數(shù)據(jù)表。但是數(shù)據(jù)表大小的限制,我們只能上傳頭部訪問(wèn)url。所以如果用這個(gè)辦法實(shí)現(xiàn),數(shù)據(jù)是不完整的,或者不完全正確的數(shù)據(jù)。因?yàn)橐苍S兩臺(tái)肉雞合并的頭部數(shù)據(jù)正好就包括了某肉雞未上傳的日志(該日志因?yàn)闆](méi)有到達(dá)單機(jī)肉雞訪問(wèn)量top的標(biāo)準(zhǔn))。
那么如何解決這個(gè)問(wèn)題呢,根本原因在于匯總數(shù)據(jù)所在的文件系統(tǒng)是本地的,不是分布式的(hadoop的hdfs大概就是基于這種需求發(fā)明的把)。如果是狀態(tài)碼緯度,這種思路是沒(méi)問(wèn)題的,因?yàn)閔ttp狀態(tài)碼總量就那么少。那么如果是url緯度,比如說(shuō)某機(jī)房給單肉雞的單次任務(wù)在10分鐘的url總數(shù)據(jù)量達(dá)到18萬(wàn)條。只看日志重復(fù)數(shù)>100的肉雞數(shù)據(jù)。這樣誤差最大值是100*肉雞數(shù),所以對(duì)于10臺(tái)肉雞的機(jī)房,只要是綜合合并結(jié)果>1000。都是可信任的。如果是域名緯度,少數(shù)頭部客戶流量占比大多數(shù)帶寬。 這也就是所謂的hot-key,少數(shù)的hot-key占據(jù)了大多數(shù)比例的流量。所以域名緯度時(shí),這個(gè)時(shí)候可以把關(guān)注點(diǎn)縮放在指定域名的url列表。如果本地上報(bào)給數(shù)據(jù)表的數(shù)據(jù)量太大,url也可以考慮進(jìn)行短地址壓縮。當(dāng)然如果不想彎道超車的話,需要硬解決這個(gè)問(wèn)題,那可能得需要hdfs這種分布式文件系統(tǒng)。
Stream-Processing
我們進(jìn)行日志客戶端系統(tǒng),需要向日志服務(wù)器下載此次任務(wù)所需要的日志(一般是一個(gè)機(jī)器10分鐘的訪問(wèn)日志)。首先本地日志會(huì)去任務(wù)服務(wù)器查詢重放任務(wù)。接著去日志服務(wù)器下載。如果該模擬集群是在DC網(wǎng)絡(luò)組建,那么下載一個(gè)10分鐘(約150M左右的文件)日志幾乎在1兩秒內(nèi)搞定,但是如果這個(gè)分布式系統(tǒng)是組建于OC網(wǎng)絡(luò),那么OC網(wǎng)絡(luò)的肉雞服務(wù)器要去DC(考慮機(jī)房可靠性,日志服務(wù)器架設(shè)在DC網(wǎng)絡(luò))下載,經(jīng)過(guò)nat轉(zhuǎn)化內(nèi)網(wǎng)到外網(wǎng),下載則需要10s左右。如果為了等待日志服務(wù)器下載完,也是一筆時(shí)間開(kāi)銷。
在分布式系統(tǒng)中,所謂的stream-processing,和batch processing不同的是,數(shù)據(jù)是無(wú)邊界的。你不知道什么時(shí)候日志下載完。而batch processing的前后流程關(guān)系,好比生產(chǎn)流水線的工序,前一道完成,后一道才開(kāi)始,對(duì)于后一道是完全知道前一道的輸出結(jié)果有多少。
所謂的流式處理則需要在前一道部分輸出結(jié)果到達(dá)時(shí),啟動(dòng)后一道工序,前一道工序繼續(xù)輸出,后一道則需要做出處理事件響應(yīng)。后一道需要頻繁調(diào)度程序。
消息系統(tǒng)(message broker):前一道的部分輸出,輸入給消息系統(tǒng)。消息系統(tǒng)檢測(cè)到是完整的一條日志,則可以產(chǎn)生后一道工序的輸入。這里我們會(huì)碰到一個(gè)問(wèn)題。下載日志的速度(10s)會(huì)遠(yuǎn)遠(yuǎn)快于執(zhí)行重放這些日志的速度(3min)。按照一個(gè)消息系統(tǒng)可能的動(dòng)作是:無(wú)buffer則丟棄,按照隊(duì)列緩存住,執(zhí)行流控同步后一道工序和前一道工序的匹配速度。這里我們選擇了按照隊(duì)列緩存住這個(gè)方案。當(dāng)然在一個(gè)嚴(yán)謹(jǐn)?shù)姆植际綌?shù)據(jù)庫(kù)設(shè)計(jì),message broker是一個(gè)能考率到數(shù)據(jù)丟失的節(jié)點(diǎn)。Broker會(huì)把完整數(shù)據(jù)發(fā)給后道工序,同時(shí)會(huì)把buffer數(shù)據(jù)緩存到硬盤備份,以防程序core dump。如果對(duì)于慢速前道工序,可以進(jìn)行綜合方案配置,丟棄或者流控。這里消息broker不同于數(shù)據(jù)庫(kù),他的中間未處理數(shù)據(jù)是暫時(shí)存儲(chǔ),處理過(guò)的消息要清除存儲(chǔ)。
總結(jié)
當(dāng)然:現(xiàn)實(shí)中的生產(chǎn)線的分布式系統(tǒng)會(huì)遠(yuǎn)比這個(gè)復(fù)雜,但是本文實(shí)現(xiàn)的從0到1的迷你麻雀分布式系統(tǒng)有一定的實(shí)踐意義。它不是一蹴而就的,不斷地版本迭代。當(dāng)然該系統(tǒng)也完成了作者的kpi-存儲(chǔ)模型分析,在中途遇到問(wèn)題時(shí),進(jìn)行的設(shè)計(jì)思考和改良,在此總結(jié)分享給大家。