有贊使用storm已經(jīng)有將近3年時(shí)間,穩(wěn)定支撐著實(shí)時(shí)統(tǒng)計(jì)、數(shù)據(jù)同步、對(duì)賬、監(jiān)控、風(fēng)控等業(yè)務(wù)。訂單實(shí)時(shí)統(tǒng)計(jì)是其中一個(gè)典型的業(yè)務(wù),對(duì)數(shù)據(jù)準(zhǔn)確性、性能等方面都有較高要求,也是上線時(shí)間最久的一個(gè)實(shí)時(shí)計(jì)算應(yīng)用。通過(guò)訂單實(shí)時(shí)統(tǒng)計(jì),描述使用storm時(shí),遇到的準(zhǔn)確性、性能、可靠性等方面的問(wèn)題。
訂單實(shí)時(shí)統(tǒng)計(jì)的演進(jìn)
第一版:流程走通
在使用storm之前,顯示實(shí)時(shí)統(tǒng)計(jì)數(shù)據(jù)一般有兩種方案:
在數(shù)據(jù)庫(kù)里執(zhí)行count、sum等聚合查詢,是簡(jiǎn)單快速的實(shí)現(xiàn)方案,但容易出現(xiàn)慢查詢。
在業(yè)務(wù)代碼里對(duì)統(tǒng)計(jì)指標(biāo)做累加,可以滿足指標(biāo)的快速查詢,但統(tǒng)計(jì)邏輯耦合到業(yè)務(wù)代碼,維護(hù)不方便,而且錯(cuò)誤數(shù)據(jù)定位和修正不方便。
既要解耦業(yè)務(wù)和統(tǒng)計(jì),也要滿足指標(biāo)快速查詢,基于storm的實(shí)時(shí)計(jì)算方案可以滿足這兩點(diǎn)需求。
一個(gè)storm應(yīng)用的基本結(jié)構(gòu)有三部分:數(shù)據(jù)源、storm應(yīng)用、結(jié)果集。storm應(yīng)用從數(shù)據(jù)源讀取數(shù)據(jù),經(jīng)過(guò)計(jì)算后,把結(jié)果持久化或發(fā)送消息給其他應(yīng)用。
第一版的訂單實(shí)時(shí)統(tǒng)計(jì)結(jié)構(gòu)如下圖。在數(shù)據(jù)源方面,最早嘗試在業(yè)務(wù)代碼里打日志的方式,但總有業(yè)務(wù)分支無(wú)法覆蓋,采集的數(shù)據(jù)不全。我們的業(yè)務(wù)數(shù)據(jù)庫(kù)是mysql,隨后嘗試基于mysql binlog的數(shù)據(jù)源,采用了阿里開源的canal,可以做到完整的收集業(yè)務(wù)數(shù)據(jù)變更。
在結(jié)果數(shù)據(jù)的處理上,我們把統(tǒng)計(jì)結(jié)果持久化到了mysql,并通過(guò)另一個(gè)后臺(tái)應(yīng)用的RESTful API對(duì)外提供服務(wù),一個(gè)mysql就可以滿足數(shù)據(jù)的讀寫需求。
為了提升實(shí)時(shí)統(tǒng)計(jì)應(yīng)用吞吐量,需要提升消息的并發(fā)度。spout里設(shè)置了消息緩沖區(qū),只要消息緩沖區(qū)不滿,就會(huì)源源不斷從消息源canal拉取數(shù)據(jù),并把分發(fā)到多個(gè)bolt處理。
第二版:性能提升
第一版的性能瓶頸在統(tǒng)計(jì)結(jié)果持久化上。為了確保數(shù)據(jù)的準(zhǔn)確性,把所有的統(tǒng)計(jì)指標(biāo)持久化放在一個(gè)數(shù)據(jù)庫(kù)事務(wù)里。一筆訂單狀態(tài)更新后,會(huì)在一個(gè)事務(wù)里有兩類操作:
訂單的歷史狀態(tài)也在數(shù)據(jù)庫(kù)里存著,要與歷史狀態(tài)對(duì)比決定統(tǒng)計(jì)邏輯,并把最新的狀態(tài)持久化。storm的應(yīng)用本身是無(wú)狀態(tài)的,需要使用存儲(chǔ)設(shè)備記錄狀態(tài)信息
當(dāng)大家知道實(shí)時(shí)計(jì)算好用后,各產(chǎn)品都希望有實(shí)時(shí)數(shù)據(jù),統(tǒng)計(jì)邏輯越來(lái)越復(fù)雜。店鋪、商品、用戶等多個(gè)指標(biāo)的寫操作都是在一個(gè)事務(wù)里commit,這一簡(jiǎn)單粗暴的方式早期很好滿足的統(tǒng)計(jì)需求,但是對(duì)于update操作持有鎖時(shí)間過(guò)長(zhǎng),嚴(yán)重影響了并發(fā)能力。
為此做了數(shù)據(jù)庫(kù)事務(wù)的瘦身:
去除歷史狀態(tài)的mysql持久化,而是通過(guò)單條binlog消息的前后狀態(tài)對(duì)比,決定統(tǒng)計(jì)邏輯,這樣就做到了統(tǒng)計(jì)邏輯上的無(wú)狀態(tài)。但又產(chǎn)生了新問(wèn)題,如何保證消息有且只有處理一次,為此引入了一個(gè)redis用于保存最近24小時(shí)內(nèi)已成功處理的消息binlog偏移量,而storm的消息分發(fā)機(jī)制又可以保證相同消息總是能分配到一個(gè)bolt,避免線程安全問(wèn)題。
統(tǒng)計(jì)業(yè)務(wù)拆分,先是線上業(yè)務(wù)和公司內(nèi)部業(yè)務(wù)分離,隨后又把線上業(yè)務(wù)按不同產(chǎn)品拆分。這個(gè)不僅僅是bolt級(jí)別的拆分,而是在spout就完全分開
隨著統(tǒng)計(jì)應(yīng)用拆分,在canal和storm應(yīng)用之間加上消息隊(duì)列。canal不支持多消費(fèi)者,而實(shí)時(shí)統(tǒng)計(jì)業(yè)務(wù)也不用關(guān)系數(shù)據(jù)庫(kù)底層遷移、主從切換等維護(hù)工作,加上消息隊(duì)列能把底層數(shù)據(jù)的維護(hù)和性能優(yōu)化交給更專業(yè)的團(tuán)隊(duì)來(lái)做。
熱點(diǎn)數(shù)據(jù)在mysql里做了分桶。比如,通常一個(gè)店鋪天級(jí)別的統(tǒng)計(jì)指標(biāo)在mysql里是一行數(shù)據(jù)。如果這個(gè)店鋪有突發(fā)的大量訂單,會(huì)出現(xiàn)多個(gè)bolt同時(shí)去update這行數(shù)據(jù),出現(xiàn)數(shù)據(jù)熱點(diǎn),mysql里該行數(shù)據(jù)的鎖競(jìng)爭(zhēng)異常激烈。我們把這樣的熱點(diǎn)數(shù)據(jù)做了分桶,實(shí)驗(yàn)證明在特定場(chǎng)景下可以有一個(gè)數(shù)量級(jí)吞吐量提升。
最終,第二版的訂單實(shí)時(shí)統(tǒng)計(jì)結(jié)構(gòu)如下,主要變化在于引入了MQ,并使用redis作為消息狀態(tài)的存儲(chǔ)。而且由最初的一個(gè)應(yīng)用,被拆成了多個(gè)應(yīng)用。