1. 程式人生 > >日處理20億資料,實時使用者行為服務系統架構實踐

日處理20億資料,實時使用者行為服務系統架構實踐

攜程實時使用者行為服務作為基礎服務,目前普遍應用在多個場景中,比如猜你喜歡(攜程的推薦系統)、動態廣告、使用者畫像、瀏覽歷史等等。

以猜你喜歡為例,猜你喜歡為應用內使用者提供潛在選項,提高成交效率。旅行是一項綜合性的需求,使用者往往需要不止一個產品。作為一站式的旅遊服務平臺,跨業務線的推薦,特別是實時推薦,能實際滿足使用者的需求,因此在上游提供打通各業務線之間的使用者行為資料有很大的必要性。

攜程原有的實時使用者行為系統存在一些問題,包括:1)資料覆蓋不全;2)資料輸出沒有統一格式,對眾多使用方提高了接入成本;3)日誌處理模組是web service,比較難支援多種資料處理策略和實現方便擴容應對流量洪峰的需求等。

而近幾年旅遊市場高速增長,資料量越來越大,並且會持續快速增長。有越來越多的使用需求,對系統的實時性,穩定性也提出了更高的要求。總的來說,當前需求對系統的實時性/可用性/效能/擴充套件性方面都有很高的要求。

一、架構 

這樣的背景下,我們按照如下結構重新設計了系統:

架構

圖1:實時使用者行為系統邏輯檢視

新的架構下,資料有兩種流向,分別是處理流和輸出流。

在處理流,行為日誌會從客戶端(App/Online/H5)上傳到服務端的Collector Service。Collector Service將訊息傳送到分散式佇列。資料處理模組由流計算框架完成,從分散式佇列讀出資料,處理之後把資料寫入資料層,由分散式快取和資料庫叢集組成。

輸出流相對簡單,Web Service的後臺會從資料層拉取資料,並輸出給呼叫方,有的是內部服務呼叫,比如推薦系統,也有的是輸出到前臺,比如瀏覽歷史。系統實現採用的是Java+Kafka+Storm+Redis+MySQL+Tomcat+Spring的技術棧。

  • Java:目前公司內部Java化的氛圍比較濃厚,並且Java有比較成熟的大資料元件
  • Kafka/Storm:Kafka作為分散式訊息佇列已經在公司有比較成熟的應用,流計算框架Storm也已經落地,並且有比較好的運維支援環境。
  • Redis: Redis的HA,SortedSet和過期等特性比較好地滿足了系統的需求。
  • MySQL: 作為基礎系統,穩定性和效能也是系統的兩大指標,對比NoSQL的主要選項,比如HBase和ElasticSearch,十億資料級別上MySQL在這兩方面有更好的表現,並且經過設計能夠有不錯的水平擴充套件能力。

目前系統每天處理20億左右的資料量,資料從上線到可用的時間在300毫秒左右。查詢服務每天服務8000萬左右的請求,平均延遲在6毫秒左右。下面從實時性/可用性/效能/部署幾個維度來說明系統的設計。

二、實時性 

作為一個實時系統,實時性是首要指標。線上系統面對著各種異常情況。例如如下幾種情況:

  • 突發流量洪峰,怎麼應對;
  • 出現失敗資料或故障模組,如何保證失敗資料重試並同時保證新資料的處理;
  • 環境問題或bug導致資料積壓,如何快速消解;
  • 程式bug,舊資料需要重新處理,如何快速處理同時保證新資料;

系統從設計之初就考慮了上述情況。

首先是用storm解決了突發流量洪峰的問題。storm具有如下特性:

storm

圖2:Storm特性

作為一個流計算框架,和早期大資料處理的批處理框架有明顯區別。批處理框架是執行完一次任務就結束執行,而流處理框架則持續執行,理論上永不停止,並且處理粒度是訊息級別,因此只要系統的計算能力足夠,就能保證每條訊息都能第一時間被發現並處理。

對當前系統來說,通過storm處理框架,訊息能在進入kafka之後毫秒級別被處理。此外,storm具有強大的scale out能力。只要通過後臺修改worker數量引數,並重啟topology(storm的任務名稱),可以馬上擴充套件計算能力,方便應對突發的流量洪峰。

對訊息的處理storm支援多種資料保證策略,at least once,at most once,exactly once。對實時使用者行為來說,首先是保證資料儘可能少丟失,另外要支援包括重試和降級的多種資料處理策略,並不能發揮exactly once的優勢,反而會因為事務支援降低效能,所以實時使用者行為系統採用的at least once的策略。這種策略下訊息可能會重發,所以程式處理實現了冪等支援。

storm的釋出比較簡單,上傳更新程式jar包並重啟任務即可完成一次釋出,遺憾的是沒有多版本灰度釋出的支援。

Storm架構

圖3:Storm架構

在部分情況下資料處理需要重試,比如資料庫連線超時,或者無法連線。連線超時可能馬上重試就能恢復,但是無法連線一般需要更長時間等待網路或資料庫的恢復,這種情況下處理程式不能一直等待,否則會造成資料延遲。實時使用者行為系統採用了雙佇列的設計來解決這個問題。

雙佇列設計

圖4:雙佇列設計

生產者將行為紀錄寫入Queue1(主要保持資料新鮮),Worker從Queue1消費新鮮資料。如果發生上述異常資料,則Worker將異常資料寫入Queue2(主要保持異常資料)。

這樣Worker對Queue1的消費進度不會被異常資料影響,可以保持消費新鮮資料。RetryWorker會監聽Queue2,消費異常資料,如果處理還沒有成功,則按照一定的策略(如下圖)等待或者重新將異常資料寫入Queue2。

補償重試策略

圖5:補償重試策略

另外,資料發生積壓的情況下,可以調整Worker的消費遊標,從最新的資料重新開始消費,保證最新資料得到處理。中間未經處理的一段資料則啟動backupWorker,指定起止遊標,在消費完指定區間的資料之後,backupWorker會自動停止。(如下圖)

積壓資料消解

圖6:積壓資料消解

三、可用性 

作為基礎服務,對可用性的要求比一般的服務要高得多,因為下游依賴的服務多,一旦出現故障,有可能會引起級聯反應影響大量業務。專案從設計上對以下問題做了處理,保障系統的可用性:

  • 系統是否有單點?
  • DB擴容/維護/故障怎麼辦?
  • Redis維護/升級補丁怎麼辦?
  • 服務萬一掛了如何快速恢復?如何儘量不影響下游應用?

首先是系統層面上做了全棧叢集化。kafka和storm本身比較成熟地支援叢集化運維;web服務支援了無狀態處理並且通過負載均衡實現叢集化;Redis和DB方面攜程已經支援主備部署,使用過程中如果主機發生故障,備機會自動接管服務;通過全棧叢集化保障系統沒有單點。

另外系統在部分模組不可用時通過降級處理保障整個系統的可用性。先看看正常資料處理流程:(如下圖)

資料流程

圖7:正常資料流程

在系統正常狀態下,storm會從kafka中讀取資料,分別寫入到redis和mysql中。服務從redis拉取(取不到時從db補償),輸出給客戶端。DB降級的情況下,資料流程也隨之改變(如下圖)

圖8:系統降級-DB

當mysql不可用時,通過開啟db降級開關,storm會正常寫入redis,但不再往mysql寫入資料。資料進入reids就可以被查詢服務使用,提供給客戶端。另外storm會把資料寫入一份到kafka的retry佇列,在mysql正常服務之後,通過關閉db降級開關,storm會消費retry佇列中的資料,從而把資料寫入到mysql中。redis和mysql的資料在降級期間會有不一致,但系統恢復正常之後會通過retry保證資料最終的一致性。redis的降級處理也類似(如下圖)

系統降級-Redis

圖9:系統降級-Redis

唯一有點不同的是Redis的服務能力要遠超過MySQL。所以在Redis降級時系統的吞吐能力是下降的。這時我們會監控db壓力,如果發現MySQL壓力較大,會暫時停止資料的寫入,降低MySQL的壓力,從而保證查詢服務的穩定。

為了降低故障情況下對下游的影響,查詢服務通過Netflix的Hystrix元件支援了熔斷模式(如下圖)。

圖10:Circuit Breaker Pattern

在該模式下,一旦服務失敗請求在給定時間內超過一個閾值,就會開啟熔斷開關。在開關開啟情況下,服務對後續請求直接返回失敗響應,不會再讓請求經過業務模組處理,從而避免伺服器進一步增加壓力引起雪崩,也不會因為響應時間延長拖累呼叫方。

開關開啟之後會開始計時,timeout後會進入Half Open的狀態,在該狀態下會允許一個請求通過,進入業務處理模組,如果能正常返回則關閉開關,否則繼續保持開關開啟直到下次timeout。這樣業務恢復之後就能正常服務請求。

另外,為了防止單個呼叫方的非法呼叫對服務的影響,服務也支援了多個維度限流,包括呼叫方AppId/ip限流和服務限流,介面限流等。

四、效能&擴充套件 

由於 線上旅遊 行業近幾年的高速增長,攜程作為行業領頭羊也蓬勃發展,因此訪問量和資料量也大幅提升。公司對業務的要求是可以支撐10倍容量擴充套件,擴充套件最難的部分在資料層,因為涉及到存量資料的遷移。

實時使用者行為系統的資料層包括Redis和MySQL,Redis因為實現了一致性雜湊,擴容時只要加機器,並對分配到新分割槽的資料作讀補償就可以。

MySQL方面,我們也做了水平切分作為擴充套件的準備,分片數量的選擇考慮為2的n次方,這樣做在擴容時有明顯的好處。因為攜程的mysql資料庫現在普遍採用的是一主一備的方式,在擴容時可以直接把備機拉平成第二臺(組)主機。假設原來分了2個庫,d0和d1,都放在伺服器s0上,s0同時有備機s1。擴容只需要如下幾步:

  • 確保s0 -> s1同步順利,沒有明顯延遲
  • s0暫時關閉讀寫許可權
  • 確認s1已經完全同步s0更新
  • s1開放讀寫許可權
  • d1的dns由s0切換到s1
  • s0開放讀寫許可權

遷移過程利用MySQL的複製分發特性,避免了繁瑣易錯的人工同步過程,大大降低了遷移成本和時間。整個操作過程可以在幾分鐘完成,結合DB降級的功能,只有在DNS切換的幾秒鐘時間會產生異常。

整個過程比較簡單方便,降低了運維負擔,一定程度也能降低過多操作造成類似GitLab式悲劇的可能性。

五、部署 

前文提到Storm部署是比較方便的,只要上傳重啟就可以完成部署。部署之後由於程式重新啟動上下文丟失,可以通過Kafka記錄的遊標找到之前處理位置,恢復處理。
另外有部分情況下程式可能需要多版本執行,比如行為紀錄暫時有多個版本,這種情況下我們會新增一個backupJob,在backupJob中執行歷史版本。

作者:陳清渠,畢業於武漢大學,多年軟體及網際網路行業開發經驗。14年加入攜程,先後負責了訂單查詢服務重構,實時使用者行為服務搭建等專案的架構和研發工作,目前負責攜程技術中心基礎業務研發部訂單中心團隊。

文章來自微信公眾號:網際網路架構師