99999久久久久久亚洲,欧美人与禽猛交狂配,高清日韩av在线影院,一个人在线高清免费观看,啦啦啦在线视频免费观看www

熱線電話:13121318867

登錄
首頁精彩閱讀攜程實時用戶行為系統(tǒng)實踐
攜程實時用戶行為系統(tǒng)實踐
2017-04-17
收藏
攜程實時用戶行為服務作為基礎(chǔ)服務,目前普遍應用在多個場景中,比如猜你喜歡(攜程的推薦系統(tǒng)),動態(tài)廣告,用戶畫像,瀏覽歷史等等。

以猜你喜歡為例,猜你喜歡為應用內(nèi)用戶提供潛在選項,提高成交效率。旅行是一項綜合性的需求,用戶往往需要不止一個產(chǎn)品。作為一站式的旅游服務平臺,跨業(yè)務線的推薦,特別是實時推薦,能實際滿足用戶的需求,因此在上游提供打通各業(yè)務線之間的用戶行為數(shù)據(jù)有很大的必要性。

攜程原有的實時用戶行為系統(tǒng)存在一些問題,包括:1)數(shù)據(jù)覆蓋不全;2)數(shù)據(jù)輸出沒有統(tǒng)一格式,對眾多使用方提高了接入成本;3)日志處理模塊是web service,比較難支持多種數(shù)據(jù)處理策略和實現(xiàn)方便擴容應對流量洪峰的需求等。

而近幾年旅游市場高速增長,數(shù)據(jù)量越來越大,并且會持續(xù)快速增長。有越來越多的使用需求,對系統(tǒng)的實時性,穩(wěn)定性也提出了更高的要求??偟膩碚f,當前需求對系統(tǒng)的實時性/可用性/性能/擴展性方面都有很高的要求。

一、架構(gòu)

這樣的背景下,我們按照如下結(jié)構(gòu)重新設計了系統(tǒng):

圖1:實時用戶行為系統(tǒng)邏輯視圖 

新的架構(gòu)下,數(shù)據(jù)有兩種流向,分別是處理流和輸出流。

在處理流,行為日志會從客戶端(App/Online/H5)上傳到服務端的Collector Service。Collector Service將消息發(fā)送到分布式隊列。數(shù)據(jù)處理模塊由流計算框架完成,從分布式隊列讀出數(shù)據(jù),處理之后把數(shù)據(jù)寫入數(shù)據(jù)層,由分布式緩存和數(shù)據(jù)庫集群組成。

輸出流相對簡單,web service的后臺會從數(shù)據(jù)層拉取數(shù)據(jù),并輸出給調(diào)用方,有的是內(nèi)部服務調(diào)用,比如推薦系統(tǒng),也有的是輸出到前臺,比如瀏覽歷史。系統(tǒng)實現(xiàn)采用的是Java+Kafka+Storm+Redis+Mysql+Tomcat+Spring的技術(shù)棧。

Java:目前公司內(nèi)部Java化的氛圍比較濃厚,并且Java有比較成熟的大數(shù)據(jù)組件
Kafka/Storm:Kafka作為分布式消息隊列已經(jīng)在公司有比較成熟的應用,流計算框架Storm也已經(jīng)落地,并且有比較好的運維支持環(huán)境。
Redis: Redis的HA,SortedSet和過期等特性比較好地滿足了系統(tǒng)的需求。
MySQL: 作為基礎(chǔ)系統(tǒng),穩(wěn)定性和性能也是系統(tǒng)的兩大指標,對比nosql的主要選項,比如hbase和elasticsearch,十億數(shù)據(jù)級別上mysql在這兩方面有更好的表現(xiàn),并且經(jīng)過設計能夠有不錯的水平擴展能力。

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

二、實時性

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

突發(fā)流量洪峰,怎么應對;
出現(xiàn)失敗數(shù)據(jù)或故障模塊,如何保證失敗數(shù)據(jù)重試并同時保證新數(shù)據(jù)的處理;
環(huán)境問題或bug導致數(shù)據(jù)積壓,如何快速消解;
程序bug,舊數(shù)據(jù)需要重新處理,如何快速處理同時保證新數(shù)據(jù);

系統(tǒng)從設計之初就考慮了上述情況。

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

圖2:Storm特性

作為一個流計算框架,和早期大數(shù)據(jù)處理的批處理框架有明顯區(qū)別。批處理框架是執(zhí)行完一次任務就結(jié)束運行,而流處理框架則持續(xù)運行,理論上永不停止,并且處理粒度是消息級別,因此只要系統(tǒng)的計算能力足夠,就能保證每條消息都能第一時間被發(fā)現(xiàn)并處理。

對當前系統(tǒng)來說,通過storm處理框架,消息能在進入kafka之后毫秒級別被處理。此外,storm具有強大的scale out能力。只要通過后臺修改worker數(shù)量參數(shù),并重啟topology(storm的任務名稱),可以馬上擴展計算能力,方便應對突發(fā)的流量洪峰。

對消息的處理storm支持多種數(shù)據(jù)保證策略,at least once,at most once,exactly once。對實時用戶行為來說,首先是保證數(shù)據(jù)盡可能少丟失,另外要支持包括重試和降級的多種數(shù)據(jù)處理策略,并不能發(fā)揮exactly once的優(yōu)勢,反而會因為事務支持降低性能,所以實時用戶行為系統(tǒng)采用的atleast once的策略。這種策略下消息可能會重發(fā),所以程序處理實現(xiàn)了冪等支持。

storm的發(fā)布比較簡單,上傳更新程序jar包并重啟任務即可完成一次發(fā)布,遺憾的是沒有多版本灰度發(fā)布的支持。

圖3:Storm架構(gòu)

在部分情況下數(shù)據(jù)處理需要重試,比如數(shù)據(jù)庫連接超時,或者無法連接。連接超時可能馬上重試就能恢復,但是無法連接一般需要更長時間等待網(wǎng)絡或數(shù)據(jù)庫的恢復,這種情況下處理程序不能一直等待,否則會造成數(shù)據(jù)延遲。實時用戶行為系統(tǒng)采用了雙隊列的設計來解決這個問題。

圖4:雙隊列設計

生產(chǎn)者將行為紀錄寫入Queue1(主要保持數(shù)據(jù)新鮮),Worker從Queue1消費新鮮數(shù)據(jù)。如果發(fā)生上述異常數(shù)據(jù),則Worker將異常數(shù)據(jù)寫入Queue2(主要保持異常數(shù)據(jù))。

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


圖5:補償重試策略
另外,數(shù)據(jù)發(fā)生積壓的情況下,可以調(diào)整Worker的消費游標,從最新的數(shù)據(jù)重新開始消費,保證最新數(shù)據(jù)得到處理。中間未經(jīng)處理的一段數(shù)據(jù)則啟動backupWorker,指定起止游標,在消費完指定區(qū)間的數(shù)據(jù)之后,backupWorker會自動停止。(如下圖)

圖6:積壓數(shù)據(jù)消解

三、可用性

作為基礎(chǔ)服務,對可用性的要求比一般的服務要高得多,因為下游依賴的服務多,一旦出現(xiàn)故障,有可能會引起級聯(lián)反應影響大量業(yè)務。項目從設計上對以下問題做了處理,保障系統(tǒng)的可用性:

1.系統(tǒng)是否有單點?
2.DB擴容/維護/故障怎么辦?
3.Redis維護/升級補丁怎么辦?
4.服務萬一掛了如何快速恢復?如何盡量不影響下游應用?

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

另外系統(tǒng)在部分模塊不可用時通過降級處理保障整個系統(tǒng)的可用性。先看看正常數(shù)據(jù)處理流程:(如下圖)

圖7:正常數(shù)據(jù)流程

在系統(tǒng)正常狀態(tài)下,storm會從kafka中讀取數(shù)據(jù),分別寫入到redis和mysql中。服務從redis拉?。ㄈ〔坏綍r從db補償),輸出給客戶端。DB降級的情況下,數(shù)據(jù)流程也隨之改變(如下圖)

圖8:系統(tǒng)降級-DB

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

圖9:系統(tǒng)降級-Redis

唯一有點不同的是redis的服務能力要遠超過mysql。所以在redis降級時系統(tǒng)的吞吐能力是下降的。這時我們會監(jiān)控db壓力,如果發(fā)現(xiàn)mysql壓力較大,會暫時停止數(shù)據(jù)的寫入,降低mysql的壓力,從而保證查詢服務的穩(wěn)定。

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

圖10:Circuit Breaker Pattern

在該模式下,一旦服務失敗請求在給定時間內(nèi)超過一個閾值,就會打開熔斷開關(guān)。在開關(guān)開啟情況下,服務對后續(xù)請求直接返回失敗響應,不會再讓請求經(jīng)過業(yè)務模塊處理,從而避免服務器進一步增加壓力引起雪崩,也不會因為響應時間延長拖累調(diào)用方。

開關(guān)打開之后會開始計時,timeout后會進入Half Open的狀態(tài),在該狀態(tài)下會允許一個請求通過,進入業(yè)務處理模塊,如果能正常返回則關(guān)閉開關(guān),否則繼續(xù)保持開關(guān)打開直到下次timeout。這樣業(yè)務恢復之后就能正常服務請求。

另外,為了防止單個調(diào)用方的非法調(diào)用對服務的影響,服務也支持了多個維度限流,包括調(diào)用方AppId/ip限流和服務限流,接口限流等。

四、性能&擴展

由于在線旅游行業(yè)近幾年的高速增長,攜程作為行業(yè)領(lǐng)頭羊也蓬勃發(fā)展,因此訪問量和數(shù)據(jù)量也大幅提升。公司對業(yè)務的要求是可以支撐10倍容量擴展,擴展最難的部分在數(shù)據(jù)層,因為涉及到存量數(shù)據(jù)的遷移。

實時用戶行為系統(tǒng)的數(shù)據(jù)層包括Redis和Mysql,Redis因為實現(xiàn)了一致性哈希,擴容時只要加機器,并對分配到新分區(qū)的數(shù)據(jù)作讀補償就可以。

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

1.確保s0 -> s1同步順利,沒有明顯延遲
2.s0暫時關(guān)閉讀寫權(quán)限
3.確認s1已經(jīng)完全同步s0更新
4.s1開放讀寫權(quán)限
5.d1的dns由s0切換到s1
6.s0開放讀寫權(quán)限

遷移過程利用mysql的復制分發(fā)特性,避免了繁瑣易錯的人工同步過程,大大降低了遷移成本和時間。整個操作過程可以在幾分鐘完成,結(jié)合DB降級的功能,只有在dns切換的幾秒鐘時間會產(chǎn)生異常。

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

五、部署

前文提到storm部署是比較方便的,只要上傳重啟就可以完成部署。部署之后由于程序重新啟動上下文丟失,可以通過Kafka記錄的游標找到之前處理位置,恢復處理。

另外有部分情況下程序可能需要多版本運行,比如行為紀錄暫時有多個版本,這種情況下我們會新增一個backupJob,在backupJob中運行歷史版本。



作者 陳清渠
本文轉(zhuǎn)載自攜程技術(shù)中心(ID:ctriptech),轉(zhuǎn)載需授權(quán)


數(shù)據(jù)分析咨詢請掃描二維碼

若不方便掃碼,搜微信號:CDAshujufenxi

數(shù)據(jù)分析師資訊
更多

OK
客服在線
立即咨詢
客服在線
立即咨詢
') } function initGt() { var handler = function (captchaObj) { captchaObj.appendTo('#captcha'); captchaObj.onReady(function () { $("#wait").hide(); }).onSuccess(function(){ $('.getcheckcode').removeClass('dis'); $('.getcheckcode').trigger('click'); }); window.captchaObj = captchaObj; }; $('#captcha').show(); $.ajax({ url: "/login/gtstart?t=" + (new Date()).getTime(), // 加隨機數(shù)防止緩存 type: "get", dataType: "json", success: function (data) { $('#text').hide(); $('#wait').show(); // 調(diào)用 initGeetest 進行初始化 // 參數(shù)1:配置參數(shù) // 參數(shù)2:回調(diào),回調(diào)的第一個參數(shù)驗證碼對象,之后可以使用它調(diào)用相應的接口 initGeetest({ // 以下 4 個配置參數(shù)為必須,不能缺少 gt: data.gt, challenge: data.challenge, offline: !data.success, // 表示用戶后臺檢測極驗服務器是否宕機 new_captcha: data.new_captcha, // 用于宕機時表示是新驗證碼的宕機 product: "float", // 產(chǎn)品形式,包括:float,popup width: "280px", https: true // 更多配置參數(shù)說明請參見:http://docs.geetest.com/install/client/web-front/ }, handler); } }); } function codeCutdown() { if(_wait == 0){ //倒計時完成 $(".getcheckcode").removeClass('dis').html("重新獲取"); }else{ $(".getcheckcode").addClass('dis').html("重新獲取("+_wait+"s)"); _wait--; setTimeout(function () { codeCutdown(); },1000); } } function inputValidate(ele,telInput) { var oInput = ele; var inputVal = oInput.val(); var oType = ele.attr('data-type'); var oEtag = $('#etag').val(); var oErr = oInput.closest('.form_box').next('.err_txt'); var empTxt = '請輸入'+oInput.attr('placeholder')+'!'; var errTxt = '請輸入正確的'+oInput.attr('placeholder')+'!'; var pattern; if(inputVal==""){ if(!telInput){ errFun(oErr,empTxt); } return false; }else { switch (oType){ case 'login_mobile': pattern = /^1[3456789]\d{9}$/; if(inputVal.length==11) { $.ajax({ url: '/login/checkmobile', type: "post", dataType: "json", data: { mobile: inputVal, etag: oEtag, page_ur: window.location.href, page_referer: document.referrer }, success: function (data) { } }); } break; case 'login_yzm': pattern = /^\d{6}$/; break; } if(oType=='login_mobile'){ } if(!!validateFun(pattern,inputVal)){ errFun(oErr,'') if(telInput){ $('.getcheckcode').removeClass('dis'); } }else { if(!telInput) { errFun(oErr, errTxt); }else { $('.getcheckcode').addClass('dis'); } return false; } } return true; } function errFun(obj,msg) { obj.html(msg); if(msg==''){ $('.login_submit').removeClass('dis'); }else { $('.login_submit').addClass('dis'); } } function validateFun(pat,val) { return pat.test(val); }