天天看點

滴滴打的架構

先看看基本的系統模型,如圖1所示。

滴滴打的架構

圖1 系統模型示意圖

司機每隔幾秒鐘上報一次經緯度,存儲在MongoDB裡;

乘客發單時,通過MongoDB圈選出附近司機;

将訂單通過長連接配接服務推送給司機;

司機接單,開始服務。

MongoDB叢集是一主多從的複制集方式,讀寫都很密集(4w+/s寫、1w+/s讀)時出現以下問題:

從伺服器CPU負載急劇上升;

查詢性能急劇降低(大量查詢耗時超過800毫秒);

查詢吞吐量大幅降低;

主從複制出現較大的延遲。

原因是當時的MongoDB版本(2.6.4)是庫級别的鎖每次寫都會鎖庫,還有每一次LBS查詢會分解成許多單獨的子查詢,增大整個查詢的鎖等待機率。我們最後将全國分為4個大區,部署多個獨立的MongoDB叢集,每個大區的使用者存儲在對應的MongoDB叢集裡。

我們的長連接配接服務通過Socket接收用戶端心跳、推送消息給乘客和司機。打車大戰期間,長連接配接服務非常不穩定。

先說說硬體問題,現象是CPU的第一個核經常使用率100%,其他的核卻非常空閑,系統吞吐量上不去,對業務的影響很大。經過較長時間排查,最終發現這是因為伺服器用了單隊列網卡,I/O中斷都被配置設定到了一個CPU核上,大量資料包到來時,單個CPU核無法全部處理,導緻LVS不斷丢包連接配接中斷。最後解決這個問題其實很簡單,換成多隊列網卡就行。

再看軟體問題,長連接配接服務當時用Mina實作,Mina本身存在一些問題:記憶體使用控制粒度不夠細、垃圾回收難以有效控制、空閑連接配接檢查效率不高、大量連接配接時周期性CPU使用率飙高。快的打車的長連接配接服務特點是:大量的廣播、消息推送具有不同的優先級、細粒度的資源監控。最後我們用AIO重寫了這個長連接配接服務架構,徹底解決了這個問題。主要有以下特性:

針對快的場景定制開發;

資源(主要是ByteBuffer)池化,減少GC造成的影響;

廣播時,一份ByteBuffer複用到多個通道,減少記憶體拷貝;

使用TimeWheel檢測空閑連接配接,消除空閑連接配接檢測造成的CPU尖峰;

支援按優先級發送資料。

其實Netty已經實作了資源池化和TimeWheel方式檢測空閑連接配接,但無法做到消息優先級區分和細粒度監控,這也算是快的自身的定制特性吧,通用的通信架構确實不好滿足。選用AIO方式僅僅是因為AIO的程式設計模型比較簡單而已,其實底層的性能并沒有多大差别。

快的打車最初隻有兩個系統,一個提供HTTP服務的Web系統,一個提供TCP長連接配接服務的推送系統,所有業務運作在這個Web系統裡,代碼量非常龐大,代碼下載下傳和編譯都需要花較長時間。

業務代碼都混在一起,頻繁的日常變更導緻并行開發的分支非常多,測試和代碼合并以及釋出驗證的效率非常低下,常常一釋出就通宵。這種情況下,系統的伸縮性和擴充性非常差,關鍵業務和非關鍵業務混在一起,互相影響。

是以我們Web系統做了拆分,将整個系統從上往下分為3個大的層次:業務層、服務層以及資料層。

我們在拆分的同時,也仔細梳理了系統之間的依賴。對于強依賴場景,用Dubbo實作了RPC和服務治理。對于弱依賴場景,通過RocketMQ實作。Dubbo是阿裡開源的架構,在阿裡内部和國内大型網際網路公司有廣泛的應用,我們對Dubbo源碼比較了解。RocketMQ也是阿裡開源的,在内部得到了非常廣泛的應用,也有很多外部使用者,可簡單将RocketMQ了解為Java版的Kafka,我們同樣也對RocketMQ源碼非常了解,快的打車所有的消息都是通過RocketMQ實作的,這兩個中間件線上上運作得非常穩定。

借着分布式改造的機會,我們對系統全局也做了梳理,建立研發流程、代碼規範、SQL規範,梳理鍊路上的單點和性能瓶頸,建立服務降級機制。

當時用戶端與服務端通信面臨以下問題。

每新增一個業務請求,Web工程就要改動釋出。

請求和響應格式沒有規範,導緻服務端很難對請求做統一處理,而且與第三方內建的方式非常多,維護成本高。

來多少請求就處理多少,根本不考慮後端服務的承受能力,而某些時候需要對後端做保護。

業務邏輯比較分散,有的在Web應用裡,有的在Dubbo服務裡。提供新功能時,工程師關注的點比較多,增加了系統風險。

業務頻繁變化和快速發展,文檔無法跟上,最後沒人能說清到底有哪些協定,協定裡的字段含義。

針對這些問題,我們設計了快的無線開放平台KOP,以下是一些大的設計原則。

接入權限控制 

為接入的用戶端配置設定标示和密鑰,密鑰由用戶端保管,用來對請求做數字簽名。服務端對用戶端請求做簽名校驗,校驗通過才會執行請求。

流量配置設定和降級 

同樣的API,不同接入端的通路限制可以不一樣。可按城市、用戶端平台類型做ABTest。極端情況下,優先保證核心用戶端的流量,同時也會優先保證核心API的服務能力,例如登入、下單、接單、支付這些核心的API。被通路被限制時,傳回一個限流錯誤碼,用戶端根據不同場景酌情處理。

流量分析 

從用戶端、API、IP、使用者多個次元,實時分析目前請求是否惡意請求,惡意的IP和使用者會被當機一段時間或永久封禁。

實時釋出 

上線或下線API不需要對KOP進行釋出,實時生效。當然,為了安全,會有API的稽核機制。

實時監控 

能統計每個用戶端對每個API每分鐘的調用總量、成功量、失敗量、平均耗時,能以分鐘為機關檢視指定時間段内的資料曲線,并且能對比曆史資料。當響應時間或失敗數量超過門檻值時,系統會自動發送報警短信。

我們基于Storm和HBase設計了自己的實時監控平台,分鐘級别實時展現系統運作狀況和業務資料(架構如圖2所示),包含以下幾個主要部分。

滴滴打的架構

圖2 監控系統架構圖

核心計算模型 

求和、求平均、分組。

基于Storm的實時計算 

Storm的邏輯并不複雜,隻有兩個Bolt,一個将一條日志解析成KV對,另外一個基于KV和設定的規則進行計算。每隔一分鐘将資料寫入RocketMQ。

基于HBase的資料存儲 

隻有插入沒有更新,避免了HBase行鎖競争。rowkey是有序的,因為要根據次元和時間段查詢,這樣會形成HBase Region熱點,導緻寫入比較集中,但是沒有性能問題,因為每個次元每隔1分鐘定時插入,平均每秒的插入很少。即使前端應用的日志量突然增加很多,HBase的插入頻度仍然是穩定的。

基于RocketMQ的資料緩沖 

收集的日志和Storm計算的結果都先放入MetaQ叢集,無論Storm叢集還是存儲節點,發生故障時系統仍然是穩定的,不會将故障放大;即使有突然的流量高峰,因為有消息隊列做緩沖,Storm和HBase仍然能以穩定的TPS處理。這些都極大的保證了系統的穩定性。RocketMQ叢集自身的健壯性足夠強,都是實體機。SSD存儲盤、高配記憶體和CPU、Broker全部是M/S結構。可以存儲足夠多的緩沖資料。

某個系統的實時業務名額(關鍵資料被隐藏),見圖3。

滴滴打的架構

圖3 某個業務系統大盤截圖

随着業務發展,單資料庫單表已經無法滿足性能要求,特别是發券和訂單,我們選擇在用戶端分庫分表,自己做了一個通用架構解決分庫分表的問題。但是還有以下問題:

資料同步 

快的原來的資料庫分為前台庫和背景庫,前台庫給應用系統使用,背景庫隻供背景使用。不管前台應用有多少庫,背景庫隻有一個,那麼前台的多個庫多個表如何對應到背景的單庫單表?MySQL的複制無法解決這個問題。

離線計算抽取 

還有大資料的場景,大資料同僚經常要dump資料做離線計算,都是通過Sqoop到背景庫抽資料,有的複雜SQL經常會使資料庫變得不穩定。而且,不同業務場景下的Sqoop會造成資料重複抽取,給資料庫添加了更多的負擔。

我們最終實作了一個資料同步平台,見圖4。

滴滴打的架構

圖4 資料同步平台架構圖

資料抽取用開源的canal實作,MySQL binlog改為Row模式,将canal抽取的binlog解析為MQ消息,打包傳輸給MQ;

一份資料,多種消費場景,之前是每種場景都抽取一份資料;

各個消費端不需要關心MySQL,隻需要關心MQ的Topic;

支援全局有序,局部有序,并發亂序;

可以指定時間點回放資料;

資料鍊路監控、報警;

通過管理平台自動部署節點。

分庫分表解決了前台應用的性能問題,資料同步解決了多庫多表歸一的問題,但是随着時間推移,背景單庫的問題越來越嚴重,迫切需要一種方案解決海量資料存儲的問題,同時又要讓現有的上層應用不會有太大改動。是以我們基于HBase和資料同步設計了實時資料中心,如圖5所示。

滴滴打的架構

圖5 實時資料中心架構圖

将前台MySQL多庫多表通過同步平台,都同步到了HBase;

為減少背景應用層的改動,設計了一個SQL解析子產品,将SQL查詢轉換為HBase查詢;

支援二級索引。 

說說二級索引,HBase并不支援二級索引,對它而言隻有一個索引,那就是Rowkey。如果要按其它字段查詢,那就要為這些字段建立與Rowkey的映射關系,這就是所謂的二級索引。HBase二級索引可以通過Coprocessor在資料插入之前執行一段代碼,這段代碼運作在HBase服務端(Region Server),可以讓這段代碼負責插入二級索引。實時資料中心的二級索引是在用戶端負責插入的,并沒有使用Coprocessor,主要原因是Coprocessor不容易實作索引的批量插入,而批量插入,實踐證明,是提升HBase插入性能非常有效的手段。二級索引的應用其實還有些條件,如下:

排序 

在HBase中,隻有一種排序,就是按Rowkey排序,是以,在建立索引的時候,實際上就定死了将來查詢結果的排序。某個索引字段的reverse屬性為true,則按這個字段倒序排序,否則正序排序。

打散 

單調變化的Rowkey讀寫壓力很難均勻分布到多個Region上,而打散将會使讀寫均勻分布到多個Region,是以提升了讀寫性能。但打散也有局限性,主要的是,經過打散的字段将無法支援範圍查詢。而且,hash和reverse這兩個屬性是互斥的,且hash優先級高,就是說一旦設定了hash=true,則會忽略reverse這個屬性。

串聯 

另外需要特别強調的是,索引配置也影響到多表歸一,作為“串聯”的字段,必須建立唯一索引,如果串聯字段上沒有建立唯一索引,将無法完成多表歸一。

我們還實作了一套将SQL語句轉換成HBase API的引擎,可以通過SQL語句直接操作HBase。這裡需要指出的是HSQL引擎和Hive是不同的,Hive主要用于将SQL語句轉換成Hadoop的Map/Reduce任務,當然也可以轉換成HBase的查詢。但Hive無法利用二級索引(HBase本來就不存在二級索引這個概念),Hive主要面向的是大批量、低頻度、高延遲、順序讀的通路場景,而HSQL可以有效利用二級索引,它面向的是小批量、高頻度、低延遲、随機讀的通路場景。