快的打車從2013年年底到2014年下半年,系統(tǒng)訪問(wèn)量迅速膨脹,很多復(fù)雜的問(wèn)題要在短時(shí)間內(nèi)解決,且不能影響線上業(yè)務(wù),這是比較大的挑戰(zhàn),本文將會(huì)闡述快的打車架構(gòu)演變過(guò)程遇到的一些有代表性的問(wèn)題和解決方案。
LBS的瓶頸和方案
先看看基本的系統(tǒng)模型,如圖1所示。
圖1 系統(tǒng)模型示意圖
司機(jī)每隔幾秒鐘上報(bào)一次經(jīng)緯度,存儲(chǔ)在MongoDB里;
乘客發(fā)單時(shí),通過(guò)MongoDB圈選出附近司機(jī);
將訂單通過(guò)長(zhǎng)連接服務(wù)推送給司機(jī);
司機(jī)接單,開(kāi)始服務(wù)。
MongoDB集群是一主多從的復(fù)制集方式,讀寫(xiě)都很密集(4w+/s寫(xiě)、1w+/s讀)時(shí)出現(xiàn)以下問(wèn)題:
從服務(wù)器CPU負(fù)載急劇上升;
查詢性能急劇降低(大量查詢耗時(shí)超過(guò)800毫秒);
查詢吞吐量大幅降低;
主從復(fù)制出現(xiàn)較大的延遲。
原因是當(dāng)時(shí)的MongoDB版本(2.6.4)是庫(kù)級(jí)別的鎖每次寫(xiě)都會(huì)鎖庫(kù),還有每一次LBS查詢會(huì)分解成許多單獨(dú)的子查詢,增大整個(gè)查詢的鎖等待概率。我們最后將全國(guó)分為4個(gè)大區(qū),部署多個(gè)獨(dú)立的MongoDB集群,每個(gè)大區(qū)的用戶存儲(chǔ)在對(duì)應(yīng)的MongoDB集群里。
長(zhǎng)連接服務(wù)穩(wěn)定性
我們的長(zhǎng)連接服務(wù)通過(guò)Socket接收客戶端心跳、推送消息給乘客和司機(jī)。打車大戰(zhàn)期間,長(zhǎng)連接服務(wù)非常不穩(wěn)定。
先說(shuō)說(shuō)硬件問(wèn)題,現(xiàn)象是CPU的第一個(gè)核經(jīng)常使用率100%,其他的核卻非常空閑,系統(tǒng)吞吐量上不去,對(duì)業(yè)務(wù)的影響很大。經(jīng)過(guò)較長(zhǎng)時(shí)間排查,最終發(fā)現(xiàn)這是因?yàn)榉?wù)器用了單隊(duì)列網(wǎng)卡,I/O中斷都被分配到了一個(gè)CPU核上,大量數(shù)據(jù)包到來(lái)時(shí),單個(gè)CPU核無(wú)法全部處理,導(dǎo)致LVS不斷丟包連接中斷。最后解決這個(gè)問(wèn)題其實(shí)很簡(jiǎn)單,換成多隊(duì)列網(wǎng)卡就行。
再看軟件問(wèn)題,長(zhǎng)連接服務(wù)當(dāng)時(shí)用Mina實(shí)現(xiàn),Mina本身存在一些問(wèn)題:內(nèi)存使用控制粒度不夠細(xì)、垃圾回收難以有效控制、空閑連接檢查效率不高、大量連接時(shí)周期性CPU使用率飆高。快的打車的長(zhǎng)連接服務(wù)特點(diǎn)是:大量的廣播、消息推送具有不同的優(yōu)先級(jí)、細(xì)粒度的資源監(jiān)控。最后我們用AIO重寫(xiě)了這個(gè)長(zhǎng)連接服務(wù)框架,徹底解決了這個(gè)問(wèn)題。主要有以下特性:
針對(duì)快的場(chǎng)景定制開(kāi)發(fā);
資源(主要是ByteBuffer)池化,減少GC造成的影響;
廣播時(shí),一份ByteBuffer復(fù)用到多個(gè)通道,減少內(nèi)存拷貝;
使用TimeWheel檢測(cè)空閑連接,消除空閑連接檢測(cè)造成的CPU尖峰;
支持按優(yōu)先級(jí)發(fā)送數(shù)據(jù)。
其實(shí)Netty已經(jīng)實(shí)現(xiàn)了資源池化和TimeWheel方式檢測(cè)空閑連接,但無(wú)法做到消息優(yōu)先級(jí)區(qū)分和細(xì)粒度監(jiān)控,這也算是快的自身的定制特性吧,通用的通信框架確實(shí)不好滿足。選用AIO方式僅僅是因?yàn)?/span>AIO的編程模型比較簡(jiǎn)單而已,其實(shí)底層的性能并沒(méi)有多大差別。
系統(tǒng)分布式改造
快的打車最初只有兩個(gè)系統(tǒng),一個(gè)提供HTTP服務(wù)的Web系統(tǒng),一個(gè)提供TCP長(zhǎng)連接服務(wù)的推送系統(tǒng),所有業(yè)務(wù)運(yùn)行在這個(gè)Web系統(tǒng)里,代碼量非常龐大,代碼下載和編譯都需要花較長(zhǎng)時(shí)間。
業(yè)務(wù)代碼都混在一起,頻繁的日常變更導(dǎo)致并行開(kāi)發(fā)的分支非常多,測(cè)試和代碼合并以及發(fā)布驗(yàn)證的效率非常低下,常常一發(fā)布就通宵。這種情況下,系統(tǒng)的伸縮性和擴(kuò)展性非常差,關(guān)鍵業(yè)務(wù)和非關(guān)鍵業(yè)務(wù)混在一起,互相影響。
因此我們Web系統(tǒng)做了拆分,將整個(gè)系統(tǒng)從上往下分為3個(gè)大的層次:業(yè)務(wù)層、服務(wù)層以及數(shù)據(jù)層。
我們?cè)诓鸱值耐瑫r(shí),也仔細(xì)梳理了系統(tǒng)之間的依賴。對(duì)于強(qiáng)依賴場(chǎng)景,用Dubbo實(shí)現(xiàn)了RPC和服務(wù)治理。對(duì)于弱依賴場(chǎng)景,通過(guò)RocketMQ實(shí)現(xiàn)。Dubbo是阿里開(kāi)源的框架,在阿里內(nèi)部和國(guó)內(nèi)大型互聯(lián)網(wǎng)公司有廣泛的應(yīng)用,我們對(duì)Dubbo源碼比較了解。RocketMQ也是阿里開(kāi)源的,在內(nèi)部得到了非常廣泛的應(yīng)用,也有很多外部用戶,可簡(jiǎn)單將RocketMQ理解為Java版的Kafka,我們同樣也對(duì)RocketMQ源碼非常了解,快的打車所有的消息都是通過(guò)RocketMQ實(shí)現(xiàn)的,這兩個(gè)中間件在線上運(yùn)行得非常穩(wěn)定。
借著分布式改造的機(jī)會(huì),我們對(duì)系統(tǒng)全局也做了梳理,建立研發(fā)流程、代碼規(guī)范、SQL規(guī)范,梳理鏈路上的單點(diǎn)和性能瓶頸,建立服務(wù)降級(jí)機(jī)制。
無(wú)線開(kāi)放平臺(tái)
當(dāng)時(shí)客戶端與服務(wù)端通信面臨以下問(wèn)題。
每新增一個(gè)業(yè)務(wù)請(qǐng)求,Web工程就要改動(dòng)發(fā)布。
請(qǐng)求和響應(yīng)格式?jīng)]有規(guī)范,導(dǎo)致服務(wù)端很難對(duì)請(qǐng)求做統(tǒng)一處理,而且與第三方集成的方式非常多,維護(hù)成本高。
來(lái)多少請(qǐng)求就處理多少,根本不考慮后端服務(wù)的承受能力,而某些時(shí)候需要對(duì)后端做保護(hù)。
業(yè)務(wù)邏輯比較分散,有的在Web應(yīng)用里,有的在Dubbo服務(wù)里。提供新功能時(shí),工程師關(guān)注的點(diǎn)比較多,增加了系統(tǒng)風(fēng)險(xiǎn)。
業(yè)務(wù)頻繁變化和快速發(fā)展,文檔無(wú)法跟上,最后沒(méi)人能說(shuō)清到底有哪些協(xié)議,協(xié)議里的字段含義。
針對(duì)這些問(wèn)題,我們?cè)O(shè)計(jì)了快的無(wú)線開(kāi)放平臺(tái)KOP,以下是一些大的設(shè)計(jì)原則。
接入權(quán)限控制
為接入的客戶端分配標(biāo)示和密鑰,密鑰由客戶端保管,用來(lái)對(duì)請(qǐng)求做數(shù)字簽名。服務(wù)端對(duì)客戶端請(qǐng)求做簽名校驗(yàn),校驗(yàn)通過(guò)才會(huì)執(zhí)行請(qǐng)求。流量分配和降級(jí)
同樣的API,不同接入端的訪問(wèn)限制可以不一樣。可按城市、客戶端平臺(tái)類型做ABTest。極端情況下,優(yōu)先保證核心客戶端的流量,同時(shí)也會(huì)優(yōu)先保證核心API的服務(wù)能力,例如登錄、下單、接單、支付這些核心的API。被訪問(wèn)被限制時(shí),返回一個(gè)限流錯(cuò)誤碼,客戶端根據(jù)不同場(chǎng)景酌情處理。流量分析
從客戶端、API、IP、用戶多個(gè)維度,實(shí)時(shí)分析當(dāng)前請(qǐng)求是否惡意請(qǐng)求,惡意的IP和用戶會(huì)被凍結(jié)一段時(shí)間或永久封禁。實(shí)時(shí)發(fā)布
上線或下線API不需要對(duì)KOP進(jìn)行發(fā)布,實(shí)時(shí)生效。當(dāng)然,為了安全,會(huì)有API的審核機(jī)制。實(shí)時(shí)監(jiān)控
能統(tǒng)計(jì)每個(gè)客戶端對(duì)每個(gè)API每分鐘的調(diào)用總量、成功量、失敗量、平均耗時(shí),能以分鐘為單位查看指定時(shí)間段內(nèi)的數(shù)據(jù)曲線,并且能對(duì)比歷史數(shù)據(jù)。當(dāng)響應(yīng)時(shí)間或失敗數(shù)量超過(guò)閾值時(shí),系統(tǒng)會(huì)自動(dòng)發(fā)送報(bào)警短信。
實(shí)時(shí)計(jì)算與監(jiān)控
我們基于Storm和HBase設(shè)計(jì)了自己的實(shí)時(shí)監(jiān)控平臺(tái),分鐘級(jí)別實(shí)時(shí)展現(xiàn)系統(tǒng)運(yùn)行狀況和業(yè)務(wù)數(shù)據(jù)(架構(gòu)如圖2所示),包含以下幾個(gè)主要部分。
圖2 監(jiān)控系統(tǒng)架構(gòu)圖
核心計(jì)算模型
求和、求平均、分組。基于Storm的實(shí)時(shí)計(jì)算
Storm的邏輯并不復(fù)雜,只有兩個(gè)Bolt,一個(gè)將一條日志解析成KV對(duì),另外一個(gè)基于KV和設(shè)定的規(guī)則進(jìn)行計(jì)算。每隔一分鐘將數(shù)據(jù)寫(xiě)入RocketMQ。基于HBase的數(shù)據(jù)存儲(chǔ)
只有插入沒(méi)有更新,避免了HBase行鎖競(jìng)爭(zhēng)。rowkey是有序的,因?yàn)橐鶕?jù)維度和時(shí)間段查詢,這樣會(huì)形成HBase Region熱點(diǎn),導(dǎo)致寫(xiě)入比較集中,但是沒(méi)有性能問(wèn)題,因?yàn)槊總€(gè)維度每隔1分鐘定時(shí)插入,平均每秒的插入很少。即使前端應(yīng)用的日志量突然增加很多,HBase的插入頻度仍然是穩(wěn)定的。基于RocketMQ的數(shù)據(jù)緩沖
收集的日志和Storm計(jì)算的結(jié)果都先放入MetaQ集群,無(wú)論Storm集群還是存儲(chǔ)節(jié)點(diǎn),發(fā)生故障時(shí)系統(tǒng)仍然是穩(wěn)定的,不會(huì)將故障放大;即使有突然的流量高峰,因?yàn)橛邢㈥?duì)列做緩沖,Storm和HBase仍然能以穩(wěn)定的TPS處理。這些都極大的保證了系統(tǒng)的穩(wěn)定性。RocketMQ集群自身的健壯性足夠強(qiáng),都是物理機(jī)。SSD存儲(chǔ)盤(pán)、高配內(nèi)存和CPU、Broker全部是M/S結(jié)構(gòu)。可以存儲(chǔ)足夠多的緩沖數(shù)據(jù)。
某個(gè)系統(tǒng)的實(shí)時(shí)業(yè)務(wù)指標(biāo)(關(guān)鍵數(shù)據(jù)被隱藏),見(jiàn)圖3。
圖3 某個(gè)業(yè)務(wù)系統(tǒng)大盤(pán)截圖
數(shù)據(jù)層改造
隨著業(yè)務(wù)發(fā)展,單數(shù)據(jù)庫(kù)單表已經(jīng)無(wú)法滿足性能要求,特別是發(fā)券和訂單,我們選擇在客戶端分庫(kù)分表,自己做了一個(gè)通用框架解決分庫(kù)分表的問(wèn)題。但是還有以下問(wèn)題:
數(shù)據(jù)同步
快的原來(lái)的數(shù)據(jù)庫(kù)分為前臺(tái)庫(kù)和后臺(tái)庫(kù),前臺(tái)庫(kù)給應(yīng)用系統(tǒng)使用,后臺(tái)庫(kù)只供后臺(tái)使用。不管前臺(tái)應(yīng)用有多少庫(kù),后臺(tái)庫(kù)只有一個(gè),那么前臺(tái)的多個(gè)庫(kù)多個(gè)表如何對(duì)應(yīng)到后臺(tái)的單庫(kù)單表?MySQL的復(fù)制無(wú)法解決這個(gè)問(wèn)題。離線計(jì)算抽取
還有大數(shù)據(jù)的場(chǎng)景,大數(shù)據(jù)同事經(jīng)常要dump數(shù)據(jù)做離線計(jì)算,都是通過(guò)Sqoop到后臺(tái)庫(kù)抽數(shù)據(jù),有的復(fù)雜SQL經(jīng)常會(huì)使數(shù)據(jù)庫(kù)變得不穩(wěn)定。而且,不同業(yè)務(wù)場(chǎng)景下的Sqoop會(huì)造成數(shù)據(jù)重復(fù)抽取,給數(shù)據(jù)庫(kù)添加了更多的負(fù)擔(dān)。
我們最終實(shí)現(xiàn)了一個(gè)數(shù)據(jù)同步平臺(tái),見(jiàn)圖4。
圖4 數(shù)據(jù)同步平臺(tái)架構(gòu)圖
數(shù)據(jù)抽取用開(kāi)源的canal實(shí)現(xiàn),MySQL binlog改為Row模式,將canal抽取的binlog解析為MQ消息,打包傳輸給MQ;
一份數(shù)據(jù),多種消費(fèi)場(chǎng)景,之前是每種場(chǎng)景都抽取一份數(shù)據(jù);
各個(gè)消費(fèi)端不需要關(guān)心MySQL,只需要關(guān)心MQ的Topic;
支持全局有序,局部有序,并發(fā)亂序;
可以指定時(shí)間點(diǎn)回放數(shù)據(jù);
數(shù)據(jù)鏈路監(jiān)控、報(bào)警;
通過(guò)管理平臺(tái)自動(dòng)部署節(jié)點(diǎn)。
分庫(kù)分表解決了前臺(tái)應(yīng)用的性能問(wèn)題,數(shù)據(jù)同步解決了多庫(kù)多表歸一的問(wèn)題,但是隨著時(shí)間推移,后臺(tái)單庫(kù)的問(wèn)題越來(lái)越嚴(yán)重,迫切需要一種方案解決海量數(shù)據(jù)存儲(chǔ)的問(wèn)題,同時(shí)又要讓現(xiàn)有的上層應(yīng)用不會(huì)有太大改動(dòng)。因此我們基于HBase和數(shù)據(jù)同步設(shè)計(jì)了實(shí)時(shí)數(shù)據(jù)中心,如圖5所示。
圖5 實(shí)時(shí)數(shù)據(jù)中心架構(gòu)圖
將前臺(tái)MySQL多庫(kù)多表通過(guò)同步平臺(tái),都同步到了HBase;
為減少后臺(tái)應(yīng)用層的改動(dòng),設(shè)計(jì)了一個(gè)SQL解析模塊,將SQL查詢轉(zhuǎn)換為HBase查詢;
支持二級(jí)索引。
說(shuō)說(shuō)二級(jí)索引,HBase并不支持二級(jí)索引,對(duì)它而言只有一個(gè)索引,那就是Rowkey。如果要按其它字段查詢,那就要為這些字段建立與Rowkey的映射關(guān)系,這就是所謂的二級(jí)索引。HBase二級(jí)索引可以通過(guò)Coprocessor在數(shù)據(jù)插入之前執(zhí)行一段代碼,這段代碼運(yùn)行在HBase服務(wù)端(Region Server),可以讓這段代碼負(fù)責(zé)插入二級(jí)索引。實(shí)時(shí)數(shù)據(jù)中心的二級(jí)索引是在客戶端負(fù)責(zé)插入的,并沒(méi)有使用Coprocessor,主要原因是Coprocessor不容易實(shí)現(xiàn)索引的批量插入,而批量插入,實(shí)踐證明,是提升HBase插入性能非常有效的手段。二級(jí)索引的應(yīng)用其實(shí)還有些條件,如下:排序
在HBase中,只有一種排序,就是按Rowkey排序,因此,在建立索引的時(shí)候,實(shí)際上就定死了將來(lái)查詢結(jié)果的排序。某個(gè)索引字段的reverse屬性為true,則按這個(gè)字段倒序排序,否則正序排序。打散
單調(diào)變化的Rowkey讀寫(xiě)壓力很難均勻分布到多個(gè)Region上,而打散將會(huì)使讀寫(xiě)均勻分布到多個(gè)Region,因此提升了讀寫(xiě)性能。但打散也有局限性,主要的是,經(jīng)過(guò)打散的字段將無(wú)法支持范圍查詢。而且,hash和reverse這兩個(gè)屬性是互斥的,且hash優(yōu)先級(jí)高,就是說(shuō)一旦設(shè)置了hash=true,則會(huì)忽略reverse這個(gè)屬性。串聯(lián)
另外需要特別強(qiáng)調(diào)的是,索引配置也影響到多表歸一,作為“串聯(lián)”的字段,必須建立唯一索引,如果串聯(lián)字段上沒(méi)有建立唯一索引,將無(wú)法完成多表歸一。
我們還實(shí)現(xiàn)了一套將SQL語(yǔ)句轉(zhuǎn)換成HBase API的引擎,可以通過(guò)SQL語(yǔ)句直接操作HBase。這里需要指出的是HSQL引擎和Hive是不同的,Hive主要用于將SQL語(yǔ)句轉(zhuǎn)換成Hadoop的Map/Reduce任務(wù),當(dāng)然也可以轉(zhuǎn)換成HBase的查詢。但Hive無(wú)法利用二級(jí)索引(HBase本來(lái)就不存在二級(jí)索引這個(gè)概念),Hive主要面向的是大批量、低頻度、高延遲、順序讀的訪問(wèn)場(chǎng)景,而HSQL可以有效利用二級(jí)索引,它面向的是小批量、高頻度、低延遲、隨機(jī)讀的訪問(wèn)場(chǎng)景。