在社交網(wǎng)絡(luò)數(shù)據(jù)分析中的實(shí)踐:批流融合與性能優(yōu)化)
我之前在社交平臺做數(shù)據(jù)架構(gòu)時(shí)接過一個(gè)挺棘手的活用戶量在漲、內(nèi)容量在漲運(yùn)營那邊天天催“實(shí)時(shí)熱點(diǎn)榜怎么還不更新”“昨晚的活動(dòng)效果怎么到現(xiàn)在才出報(bào)表”。當(dāng)時(shí)團(tuán)隊(duì)里已經(jīng)有一套離線數(shù)倉在跑也試著讓它同步扛實(shí)時(shí)查詢結(jié)果兩邊都不痛快。后來把Lambda架構(gòu)的系統(tǒng)性思路理清楚才發(fā)現(xiàn)問題不在某一套引擎上而在“批處理”和“實(shí)時(shí)處理”這兩條邏輯鏈路始終被強(qiáng)行揉在一起互相拖累。這篇文章就把我在這類社交網(wǎng)絡(luò)數(shù)據(jù)分析場景里的完整實(shí)踐思路和技術(shù)取舍做一個(gè)復(fù)盤。內(nèi)容適合剛要上手實(shí)時(shí)數(shù)倉、或者已經(jīng)在用Lambda架構(gòu)但處處別扭的朋友。Lambda架構(gòu)的核心思路說穿了只有一句話把數(shù)據(jù)和計(jì)算拆成“批層”和“速度層”分別處理歷史全量問題和實(shí)時(shí)增量問題最終在服務(wù)層把兩邊的結(jié)果合并給應(yīng)用。這句話看起來簡單可一旦落到社交數(shù)據(jù)里從數(shù)據(jù)建模到代碼實(shí)現(xiàn)再到上線補(bǔ)償背后全是細(xì)節(jié)。1. 社交數(shù)據(jù)到底特殊在哪批處理和實(shí)時(shí)為什么非要分開算1.1 大多數(shù)社交數(shù)據(jù)分析場景的真實(shí)畫像先別急著上架構(gòu)想清楚數(shù)據(jù)長什么樣。社交網(wǎng)絡(luò)的數(shù)據(jù)有個(gè)非常鮮明的特點(diǎn)同一份數(shù)據(jù)在不同時(shí)間尺度上價(jià)值完全不同。一個(gè)用戶發(fā)了一條動(dòng)態(tài)這條動(dòng)態(tài)在發(fā)布后幾分鐘內(nèi)能不能進(jìn)入推薦流、能帶來多少曝光這是秒級甚至毫秒級問題。同一批互動(dòng)數(shù)據(jù)到了晚上運(yùn)營想看的卻是“今天哪個(gè)話題的累計(jì)討論量最高”“哪個(gè)城市的參與度漲了”這又變成小時(shí)級問題。到月底復(fù)盤團(tuán)隊(duì)要算的是內(nèi)容生命周期曲線、用戶留存、關(guān)系鏈演變那是天級甚至周級問題。而Lambda架構(gòu)恰好承認(rèn)了這一點(diǎn)你不需要在一個(gè)引擎里同時(shí)滿足所有時(shí)間尺度的需求。批處理層負(fù)責(zé)把已經(jīng)發(fā)生的、不再變動(dòng)的歷史數(shù)據(jù)用最穩(wěn)妥的方式算一遍速度層負(fù)責(zé)抓住此刻正在發(fā)生的增量變化服務(wù)層負(fù)責(zé)把兩者拼成一個(gè)完整視圖。社交數(shù)據(jù)里的“長尾效應(yīng)”特別明顯一條三個(gè)月前的內(nèi)容可能突然因?yàn)槟硞€(gè)外部事件被撈出來二次傳播這種靠實(shí)時(shí)流是很難長期盯住的但批層天然適合周期性地重新審視歷史。1.2 為什么單獨(dú)用離線數(shù)倉或單獨(dú)用流處理都不行我見過太多團(tuán)隊(duì)走了兩種極端要么全量離線跑把“實(shí)時(shí)性”壓到分鐘級去做微批調(diào)度結(jié)果在熱點(diǎn)爆發(fā)時(shí)數(shù)據(jù)永遠(yuǎn)晚十分鐘要么干脆全上流處理所有指標(biāo)都在Kafka流上算一遇到上游字段變更、歷史數(shù)據(jù)回補(bǔ)就徹底抓瞎。這兩種方案的痛點(diǎn)各有代表性純離線的問題在于數(shù)據(jù)新鮮度和計(jì)算代價(jià)的沖突。社交數(shù)據(jù)里熱門話題的討論量可以在一小時(shí)內(nèi)翻十倍離線任務(wù)如果每五分鐘調(diào)度一次每一次都要重新讀入全量歷史計(jì)算資源浪費(fèi)嚴(yán)重而且調(diào)度鏈路稍有問題就積壓延遲。更關(guān)鍵的是離線任務(wù)天然看不到“還沒采集到”的數(shù)據(jù)比如用戶端埋點(diǎn)延遲。純流處理的問題在于狀態(tài)管理和歷史重算的成本。社交網(wǎng)絡(luò)里用戶的關(guān)注關(guān)系、內(nèi)容的刪改、評論的折疊全都是可變狀態(tài)。流計(jì)算要維護(hù)的狀態(tài)會隨著時(shí)間膨脹而且一旦發(fā)現(xiàn)計(jì)算邏輯有bug你沒法只修過去半小時(shí)的數(shù)據(jù)只能從頭重放重放期間整個(gè)處理的準(zhǔn)確性都受影響。Lambda架構(gòu)把“新鮮度”和“準(zhǔn)確性”分開解決正是對這兩類痛點(diǎn)的直接回應(yīng)流層就算偶爾有誤差也能被批層每天用全量計(jì)算矯正回來批層就算再慢也保證最終答案是完整和可回溯的。1.3 一個(gè)衡量是否適合Lambda架構(gòu)的簡單標(biāo)準(zhǔn)不是所有社交場景都需要Lambda架構(gòu)。我自己會做三個(gè)快速判斷是否存在“同一指標(biāo)既要有準(zhǔn)實(shí)時(shí)結(jié)果又要有歷史可重算結(jié)果”的雙重需求。典型如“今日熱門話題榜”實(shí)時(shí)榜給運(yùn)營看當(dāng)下趨勢歷史榜給推薦算法調(diào)參。實(shí)時(shí)鏈路是否允許在批量結(jié)果產(chǎn)生后被覆蓋。如果業(yè)務(wù)方堅(jiān)持所有看板數(shù)據(jù)必須完全一致不能接受先實(shí)時(shí)后修正那要管理好預(yù)期Lambda天然就是“先快后準(zhǔn)”。參與實(shí)時(shí)計(jì)算的維度是否已經(jīng)穩(wěn)定建模。如果指標(biāo)體系還沒定你就上流計(jì)算后面每一個(gè)維度變更都要改流式任務(wù)成本很高。2. 批處理層的落地策略把全部歷史打包成可信資產(chǎn)2.1 主數(shù)據(jù)集的設(shè)計(jì)原則不可變?nèi)罩九c事件溯源批處理層的第一件事不是寫計(jì)算邏輯而是把數(shù)據(jù)存儲做成“不可變?nèi)罩尽钡男螒B(tài)。社交數(shù)據(jù)分析里有一種很常見的錯(cuò)誤就是設(shè)計(jì)表結(jié)構(gòu)時(shí)直接把業(yè)務(wù)庫的“用戶表”“動(dòng)態(tài)表”同步過來每次用戶改了頭像就把整行覆蓋掉。這在離線分析里會埋下大隱患你永遠(yuǎn)不知道某個(gè)時(shí)間點(diǎn)上用戶長什么樣也沒法計(jì)算“用戶從A狀態(tài)變成B狀態(tài)的路徑”。我在項(xiàng)目里推薦的做法是事實(shí)表一律保存事件流不保存最后狀態(tài)。用戶改頭像記錄一個(gè)user_profile_updated事件內(nèi)容被刪除記錄一個(gè)content_deleted事件點(diǎn)贊行為記錄一個(gè)content_liked事件而不是在事實(shí)表里維護(hù)一個(gè)不斷變化的“當(dāng)前值”。這樣批處理層可以在任何一個(gè)時(shí)間點(diǎn)回溯重建當(dāng)時(shí)的數(shù)據(jù)快照。這個(gè)設(shè)計(jì)直接決定了Lambda架構(gòu)中的批處理視圖能否可靠地與實(shí)時(shí)視圖對齊。因?yàn)樗俣葘拥牧魈幚硪彩菑耐环萑罩玖骼镒x數(shù)據(jù)兩層的源頭一致合并時(shí)才不會出現(xiàn)對不上賬的情況。2.2 定期全量重算的調(diào)度粒度與計(jì)算策略批處理層最核心的工作是周期性全量重算。調(diào)度粒度的選擇要結(jié)合業(yè)務(wù)容忍度和計(jì)算成本來定。社交網(wǎng)絡(luò)里我的經(jīng)驗(yàn)是日級全量重算保底小時(shí)級增量重算做修正周級深層次分析另跑一套。日級全量重算時(shí)要處理好兩個(gè)問題數(shù)據(jù)分區(qū)。按天分區(qū)是最基礎(chǔ)的但社交數(shù)據(jù)的批量任務(wù)經(jīng)常要計(jì)算“近30日熱度”如果每次都是按當(dāng)前日期往前推30個(gè)分區(qū)掃描分區(qū)多了以后會很慢。我的做法是用“累積分區(qū)表”每天的任務(wù)直接基于昨天的全量結(jié)果追加當(dāng)天增量同時(shí)定期做一次徹底的全量重建。這樣兼顧了查詢效率和數(shù)據(jù)的最終準(zhǔn)確性。計(jì)算冪等性。批處理任務(wù)必須保證可以跑多次而結(jié)果一致。實(shí)操中我會給每個(gè)批任務(wù)生成一個(gè)batch_id輸出結(jié)果表里帶上這個(gè)批次標(biāo)識。排查數(shù)據(jù)對不上的問題時(shí)可以通過batch_id快速定位哪個(gè)批次產(chǎn)生了污染。2.3 批視圖怎么給服務(wù)層提供查詢支撐批處理層出來的是數(shù)據(jù)結(jié)果但應(yīng)用不會直接查Hive或數(shù)倉表太慢了。所以批層下面還會跟一個(gè)服務(wù)層視圖通常會落到OLAP引擎或KV存儲里。針對社交場景我比較常用的批視圖形式有三種視圖類型典型用途存儲建議全量聚合視圖用戶總粉絲數(shù)、內(nèi)容累計(jì)互動(dòng)量、關(guān)系鏈總量寬表按主鍵分片時(shí)間切片視圖小時(shí)級趨勢、熱點(diǎn)話題變化曲線時(shí)間序列存儲或OLAP列存行為序列視圖用戶路徑分析、內(nèi)容傳播鏈分析圖數(shù)據(jù)庫或列存嵌套結(jié)構(gòu)批視圖的更新邏輯是整體替換不是逐條更新。一個(gè)批次跑完之后把新視圖直接切上去這樣可以避免外部應(yīng)用讀到“算到一半”的數(shù)據(jù)。3. 速度層的工程細(xì)節(jié)滑動(dòng)窗口、遲來消息與首屏指標(biāo)3.1 速度層的定位不是“正確”而是“及時(shí)”很多人以為速度層的職責(zé)是把實(shí)時(shí)指標(biāo)算準(zhǔn)其實(shí)這是一個(gè)誤解。在Lambda架構(gòu)里速度層的定位是在批結(jié)果還沒出來之前先提供一份近似正確的結(jié)果。社交數(shù)據(jù)里的實(shí)時(shí)指標(biāo)實(shí)例熱點(diǎn)事件的“當(dāng)前討論量”你不需要它精確到個(gè)位數(shù)但你需要它在事件發(fā)生之后五秒內(nèi)就能看到一個(gè)量級的增長。如果用批處理任務(wù)最快也要五分鐘調(diào)度一次等跑完熱點(diǎn)都涼了。速度層的流處理或者更常見的微批處理就是來補(bǔ)這個(gè)時(shí)間空檔的。流處理里窗口的選擇很考驗(yàn)人。社交數(shù)據(jù)的爆發(fā)期和沉寂期差異極大固定窗口很難同時(shí)照顧好兩種狀態(tài)。以“話題熱度”為例20秒的滾動(dòng)窗口在高頻討論時(shí)能捕捉到峰值但在討論稀疏時(shí)就全是零。我最終用的是“滑動(dòng)窗口”大小設(shè)置成5分鐘滑動(dòng)間隔30秒這樣既平滑了噪聲又保持了和批層小時(shí)級趨勢數(shù)據(jù)的對齊可能。3.2 遲來數(shù)據(jù)與亂序數(shù)據(jù)的處理策略社交數(shù)據(jù)的流處理最讓人頭疼的不是高吞吐而是遲來和亂序。用戶手機(jī)斷網(wǎng)幾分鐘重新連上后埋點(diǎn)事件才補(bǔ)報(bào)上來多個(gè)上游數(shù)據(jù)源的時(shí)間戳來自不同服務(wù)器時(shí)鐘本身可能就有偏差。我在流處理里統(tǒng)一采用事件時(shí)間(event time)作為指標(biāo)計(jì)算依據(jù)而不是處理時(shí)間(processing time)。比如話題熱度里的“當(dāng)前”更準(zhǔn)確的含義是“用戶在什么時(shí)刻做了這個(gè)行為”而不是“服務(wù)器在什么時(shí)刻收到了這條日志”。處理時(shí)間在批處理和實(shí)時(shí)對賬時(shí)會出現(xiàn)偏差因?yàn)檠舆t到達(dá)的舊事件會被算進(jìn)錯(cuò)誤的時(shí)間窗口。使用Watermark水印機(jī)制來界定“遲來多久就不等了”。社交數(shù)據(jù)里我一般把水印延遲設(shè)成窗口大小的1/10到1/5比如一個(gè)5分鐘的滑動(dòng)窗口水印延遲設(shè)在60秒左右。等批處理層在某天凌晨全量重算時(shí)這些遲到的數(shù)據(jù)會被歸入正確的時(shí)間桶修正實(shí)時(shí)流的偏差。遲來數(shù)據(jù)最典型的處理方式有兩種允許遲到但直接丟棄適用于對結(jié)果精度要求不高的首屏流量型指標(biāo)。遲到的數(shù)據(jù)打側(cè)面標(biāo)記把延遲數(shù)據(jù)分到一個(gè)單獨(dú)的旁路批處理重算時(shí)再合入適用于運(yùn)營看板、結(jié)算類分析。3.3 流計(jì)算的去重是個(gè)隱形成本這一點(diǎn)很多初切流處理的人會忽略流處理引擎本身是不保證“只算一次”的。Kafka在極端情況下可能重復(fù)消費(fèi)網(wǎng)絡(luò)重試也可能導(dǎo)致事件被重復(fù)投遞。在社交數(shù)據(jù)分析里一個(gè)“點(diǎn)贊”事件被重復(fù)計(jì)算最終影響的可能是一個(gè)熱門內(nèi)容的實(shí)時(shí)熱度值。雖然批處理最終會矯正但在批任務(wù)還沒跑出來之前實(shí)時(shí)榜單可能已經(jīng)因?yàn)橹貜?fù)計(jì)算而排錯(cuò)名次。我的去重方案是基于事件的唯一ID做狀態(tài)去重。每一類事件在埋點(diǎn)源頭就生成一個(gè)全局唯一的event_id流處理端用這個(gè)ID維護(hù)一個(gè)滑動(dòng)去重集合。成本可控但能擋住大部分重復(fù)問題。如果兩個(gè)鏈路都要消費(fèi)同一個(gè)事件流一定要各自維護(hù)去重不能共享一個(gè)去重狀態(tài)否則狀態(tài)鎖會成為瓶頸。4. 服務(wù)層的合并真相批結(jié)果與實(shí)時(shí)結(jié)果的覆蓋與去重4.1 合并不是簡單的“實(shí)時(shí)加批量”Lambda架構(gòu)里服務(wù)層經(jīng)常被畫成一個(gè)“將兩個(gè)結(jié)果merge起來”的方框但真實(shí)的合并遠(yuǎn)沒有示意圖那么輕松。最簡單的場景是“批結(jié)果沒有產(chǎn)出前用實(shí)時(shí)結(jié)果批結(jié)果產(chǎn)出了直接替換成批結(jié)果”。以全站累計(jì)互動(dòng)量為例實(shí)時(shí)鏈路維護(hù)了一個(gè)從啟動(dòng)以來的累計(jì)值批處理每天凌晨全量算一次。那服務(wù)層當(dāng)天白天就用實(shí)時(shí)累計(jì)值次日凌晨批任務(wù)完成后把實(shí)時(shí)值整個(gè)替換掉。但很多指標(biāo)不能這么簡單替換。比如“近24小時(shí)話題熱度”凌晨零點(diǎn)批任務(wù)重算時(shí)實(shí)時(shí)流里可能還有從“昨天”時(shí)間段劃過來的數(shù)據(jù)在窗口內(nèi)。如果批任務(wù)直接覆蓋實(shí)時(shí)鏈路里還沒跑完的窗口數(shù)據(jù)就會造成一次跳動(dòng)。我采用的做法是物理隔離時(shí)間域?qū)崟r(shí)視圖負(fù)責(zé)當(dāng)前窗口或最近N分鐘批視圖負(fù)責(zé)N分鐘之前的所有數(shù)據(jù)。服務(wù)層查詢時(shí)按時(shí)間切分合并。打個(gè)比方實(shí)時(shí)資料負(fù)責(zé)“此時(shí)此刻發(fā)生了什么”批資料負(fù)責(zé)“從過去到現(xiàn)在累計(jì)的情況”兩者以某個(gè)時(shí)間點(diǎn)為界各行其道前端展示時(shí)拼起來即可。這并不是學(xué)術(shù)定義的唯一標(biāo)準(zhǔn)合流方式是我在社交項(xiàng)目里驗(yàn)證過的一種實(shí)用拆法。4.2 同一事件同時(shí)被兩層計(jì)算時(shí)的冪等性保障有時(shí)候一個(gè)“用戶評論”事件實(shí)時(shí)層算了批處理層也會算。如果兩層都往同一個(gè)輸出表里寫就會產(chǎn)生重復(fù)數(shù)據(jù)。為了避免這種問題我給每個(gè)計(jì)算單元定義了唯一輸出鍵。拿“內(nèi)容互動(dòng)量”舉例實(shí)時(shí)輸出鍵是content_id interval_start_time source_flag(realtime)批輸出鍵是content_id date source_flag(batch)。服務(wù)層根據(jù)這兩個(gè)標(biāo)識選擇優(yōu)先級永遠(yuǎn)只信任批結(jié)果當(dāng)批結(jié)果存在時(shí)不看實(shí)時(shí)結(jié)果。另一個(gè)問題是數(shù)據(jù)回填批處理層修復(fù)了一個(gè)之前算錯(cuò)的歷史指標(biāo)服務(wù)層需要同步更新。如果服務(wù)層用的是緩存要確保更新緩存時(shí)先讓舊緩存過期否則外部應(yīng)用還會讀到舊值。這一點(diǎn)可以通過緩存版本號來實(shí)現(xiàn)批任務(wù)的每次全量重算都對應(yīng)一個(gè)新的version。4.3 服務(wù)層對外暴露的查詢接口設(shè)計(jì)社交數(shù)據(jù)分析應(yīng)用對服務(wù)層的查詢模式其實(shí)集中在幾個(gè)固定模式上TopN榜、時(shí)間序列、單實(shí)體詳情、聚合分布。我給服務(wù)層設(shè)計(jì)了五類標(biāo)準(zhǔn)查詢接口get_trending_topics(start_ts, end_ts, limit)熱點(diǎn)話題趨勢實(shí)時(shí)和批結(jié)果按時(shí)間域合并后返回。get_entity_stats(entity_type, entity_id, period)單實(shí)體的統(tǒng)計(jì)量優(yōu)先批結(jié)果沒有再走實(shí)時(shí)。get_timeseries(metric, granularity, range)時(shí)間序列聚合兩個(gè)層的數(shù)據(jù)并去重。get_rank(metric, dimension, top_n)排行類查詢直接使用批處理層預(yù)聚結(jié)果實(shí)時(shí)結(jié)果只在批結(jié)果未產(chǎn)出時(shí)臨時(shí)補(bǔ)充。get_relationship_graph(node_id, depth)關(guān)系圖譜查詢批處理層建好圖快照服務(wù)層只做圖遍歷。正式上線前我建議做一次“數(shù)據(jù)對賬測試”取一個(gè)固定的歷史時(shí)間段把批處理結(jié)果和實(shí)時(shí)結(jié)果分別算出來對比差異率。差異率超過1%的指標(biāo)必須查明原因不允許帶著偏差上線。5. 上線前必須想清楚的內(nèi)容回填與補(bǔ)償機(jī)制5.1 從零搭建時(shí)怎么處理“歷史存量數(shù)據(jù)”很多團(tuán)隊(duì)在引入Lambda架構(gòu)時(shí)系統(tǒng)已經(jīng)跑了很久Kafka里只有最近幾天的日志歷史數(shù)據(jù)都在傳統(tǒng)的MySQL或者老數(shù)倉里。直接起一個(gè)流處理任務(wù)從當(dāng)前時(shí)間開始消費(fèi)會丟掉之前所有的狀態(tài)。我給新系統(tǒng)上線定義了一個(gè)三步走的遷移方案第一步數(shù)據(jù)補(bǔ)全。把所有歷史行為數(shù)據(jù)從老庫里按天導(dǎo)出成事件日志格式補(bǔ)齊必要字段寫回一個(gè)新的數(shù)據(jù)湖目錄。這個(gè)補(bǔ)全動(dòng)作的核心是統(tǒng)一schema尤其是時(shí)間字段的統(tǒng)一、實(shí)體ID的統(tǒng)一。第二步預(yù)計(jì)算偏移。批處理層先基于補(bǔ)全數(shù)據(jù)建一次全量視圖算出“昨天及之前”的所有指標(biāo)基線。速度層從上線時(shí)間點(diǎn)開始跑增量但初始狀態(tài)下速度層的累計(jì)值不能為空要把批視圖里的基線值作為流處理狀態(tài)的初始值注入。第三步雙跑校驗(yàn)。上線后頭三天流和批同時(shí)運(yùn)行不切換流量只做后臺校驗(yàn)。每天對比實(shí)時(shí)結(jié)果和批結(jié)果的差異等差異收斂到一個(gè)可接受范圍之后再開放線上查詢。這套方案里最耗時(shí)的是歷史數(shù)據(jù)補(bǔ)全社交場景尤其容易遇到“數(shù)據(jù)在不同時(shí)期的結(jié)構(gòu)不一樣”的問題比如半年前的用戶行為沒有攜帶統(tǒng)一設(shè)備ID。我的建議是不要追求100%補(bǔ)全先保證核心實(shí)體和核心行為能對上非核心字段留空也比硬造一個(gè)假值好。5.2 業(yè)務(wù)邏輯變更時(shí)的“重新計(jì)算完整鏈路”Lambda架構(gòu)相對傳統(tǒng)架構(gòu)的一大優(yōu)勢是修改歷史計(jì)算邏輯時(shí)不需要?jiǎng)訉?shí)時(shí)鏈路。操作步驟大概是這樣的修改批處理層計(jì)算代碼生成新的批視圖。將新批視圖接入服務(wù)層切換版本標(biāo)號。實(shí)時(shí)鏈路保持不變繼續(xù)服務(wù)當(dāng)前數(shù)據(jù)。下一次批處理重算自然覆蓋上次結(jié)果。但這個(gè)流程有個(gè)隱含前提批處理層本身的設(shè)計(jì)要支持“多版本并存”。我在實(shí)際項(xiàng)目里會讓批處理輸出表的表名帶版本號或分區(qū)帶版本號比如content_metrics_v2。切換時(shí)跑一個(gè)“頁面版本切換”操作應(yīng)用層不感知變化。如果業(yè)務(wù)邏輯變更影響的是流處理本身的指標(biāo)口徑那就不能只改批處理層了。這種時(shí)候需要同時(shí)改流處理的指標(biāo)定義并且流任務(wù)要從一個(gè)特定時(shí)間點(diǎn)重新消費(fèi)數(shù)據(jù)重算。風(fēng)險(xiǎn)明顯更高所以重大口徑變更我一般放在凌晨低峰期操作。5.3 任務(wù)失敗時(shí)的補(bǔ)償與止血預(yù)案就算架構(gòu)設(shè)計(jì)得再好分布式系統(tǒng)總會出故障。批處理任務(wù)跑掛了實(shí)時(shí)任務(wù)打滿CPU了數(shù)據(jù)源斷流了總有一款失敗在等著你。我在每個(gè)社交項(xiàng)目上線時(shí)都會寫一份專門的“數(shù)據(jù)補(bǔ)償預(yù)案文檔”里面重點(diǎn)寫清楚三種故障場景的處置方式批任務(wù)失敗不影響實(shí)時(shí)指標(biāo)的對外展示但要把失敗任務(wù)納入監(jiān)控下一調(diào)度周期自動(dòng)重試。如果連續(xù)重試三次失敗告警到人手工干預(yù)。實(shí)時(shí)任務(wù)失敗服務(wù)層自動(dòng)降級為“只讀批結(jié)果”雖然數(shù)據(jù)新鮮度下降但不會出現(xiàn)空接口。實(shí)時(shí)任務(wù)恢復(fù)后自查斷流期間的遲到數(shù)據(jù)補(bǔ)一個(gè)“重放窗口”再重新接入。批和實(shí)時(shí)同時(shí)故障服務(wù)層啟動(dòng)備用策略直接查詢底層明細(xì)數(shù)據(jù)返回未聚合的原始結(jié)果。這個(gè)方案只保證可用性不保證指標(biāo)口徑所以必須在接口上明確標(biāo)注“原始模式”。這些預(yù)案雖然很少真的用到但準(zhǔn)備好之后團(tuán)隊(duì)處理故障的心態(tài)會完全不一樣不怕出事就怕出事時(shí)手足無措。6. 我個(gè)人踩過的幾個(gè)坑和最終調(diào)參參考6.1 坑一小文件問題把批處理拖到崩潰這是我在數(shù)據(jù)湖場景里最慘痛的一次教訓(xùn)。批處理層每天全量重算輸出到HDFS/對象存儲時(shí)因?yàn)镾park的分區(qū)設(shè)置不當(dāng)生成了一堆幾KB大小的小文件。小文件的元數(shù)據(jù)開銷大后續(xù)讀數(shù)據(jù)時(shí)任務(wù)啟動(dòng)極慢整個(gè)批處理流程被拖到無法按時(shí)完成。解決方案很土但有效輸出前做一次重分區(qū)把文件數(shù)量控制在一個(gè)合理范圍。我當(dāng)時(shí)的經(jīng)驗(yàn)值是每個(gè)輸出文件控制在64MB到256MB之間。如果每天數(shù)據(jù)量約5億條先按照ID哈希分成2000個(gè)分區(qū)輸出后再做一輪合并最終控制在200個(gè)文件左右。文件數(shù)下來了后續(xù)讀取快了好幾倍。6.2 坑二流處理窗口和批處理時(shí)間桶對不齊這是一個(gè)特別隱蔽的坑。我用Flink做流處理批處理用Spark。兩邊的時(shí)間窗口定義看著一樣都是“按事件時(shí)間取小時(shí)桶”但當(dāng)時(shí)我在流處理里用的是TumblingProcessingTimeWindow在批處理里用的是事件時(shí)間字段按小時(shí)截?cái)?。結(jié)果就是兩個(gè)系統(tǒng)對同一個(gè)事件算出來的小時(shí)歸屬不一致服務(wù)層合并時(shí)數(shù)據(jù)憑空多了一倍。后來統(tǒng)一改了規(guī)則所有系統(tǒng)時(shí)間字段一律按事件時(shí)間取整禁止用處理時(shí)間。并且在這個(gè)基礎(chǔ)上給分區(qū)字段明確命名比如hour_bucket 2024-01-15-14避免“這個(gè)小時(shí)”在不同系統(tǒng)里有不同理解。6.3 坑三初始狀態(tài)注入的“偏移量”沒有做歷史對齊前面提到的三步走遷移方案我第一版實(shí)現(xiàn)時(shí)就漏了一個(gè)細(xì)節(jié)。當(dāng)時(shí)只把批視圖的基線值注入了流處理狀態(tài)但流處理任務(wù)的起點(diǎn)是從當(dāng)前時(shí)間開始的這導(dǎo)致流處理算出的“今日累計(jì)值”和批處理算出的“今日累計(jì)值”差了一個(gè)“昨夜到今日零點(diǎn)”之間的量。后來我把起點(diǎn)調(diào)整為“當(dāng)前小時(shí)的對齊邊界”并且把基線快照的截止時(shí)間、流處理可消費(fèi)的最早日志時(shí)間對齊到同一個(gè)時(shí)間點(diǎn)才徹底化解這個(gè)偏移。6.4 給新項(xiàng)目的一份參考參數(shù)表每個(gè)項(xiàng)目都不一樣不能直接抄一套配置但可以參考下面這個(gè)量級設(shè)置做初步初始化再按實(shí)際表現(xiàn)調(diào)整參數(shù)我常用的值說明滑動(dòng)窗口大小5分鐘社交話題熱度的典型平滑窗口滑動(dòng)間隔30秒兼顧延遲與計(jì)算量水印延遲60秒容忍大部分網(wǎng)絡(luò)延遲又不至于把窗口拖太久批處理全量重算頻率每天1次凌晨低峰期執(zhí)行批處理增量修正頻率每小時(shí)1次服務(wù)白天熱點(diǎn)趨勢數(shù)據(jù)更新服務(wù)層緩存有效期5分鐘太短會擊穿后端太長影響新鮮度實(shí)時(shí)與批結(jié)果時(shí)間分界當(dāng)前時(shí)刻向前偏移2小時(shí)給批任務(wù)留出緩沖時(shí)間避免邊界數(shù)據(jù)競爭這套參數(shù)在百萬級日活、日增上億條事件的社交產(chǎn)品里驗(yàn)證過延遲和計(jì)算成本都還在可控范圍內(nèi)。如果你的量級明顯不同窗口大小和緩存有效期這兩項(xiàng)優(yōu)先調(diào)整。6.5 成本控制的一些個(gè)人心得Lambda架構(gòu)常被吐槽的一點(diǎn)是“同一份邏輯跑兩遍資源翻倍”。這確實(shí)是它的代價(jià)但也有辦法緩解。我的實(shí)踐體會是不要把所有指標(biāo)都放進(jìn)Lambda里只放真正需要實(shí)時(shí)性的指標(biāo)。在社交數(shù)據(jù)分析里真正需要秒級響應(yīng)的指標(biāo)其實(shí)很少我接手的項(xiàng)目里大約只有20%的指標(biāo)需要走速度層。其余80%的指標(biāo)純批處理就能滿足完全沒必要為它們維護(hù)一套流任務(wù)。另外就是壓縮和序列化。社交日志里的信息密度其實(shí)很低大量字段是重復(fù)或空的。接入Kafka或數(shù)據(jù)湖時(shí)做好壓縮吞吐能提升好幾倍掃描成本也隨之下降。說到底Lambda架構(gòu)在社交網(wǎng)絡(luò)數(shù)據(jù)分析里之所以好用是因?yàn)樗姓J(rèn)了“實(shí)時(shí)”和“準(zhǔn)確”這對矛盾無法在一個(gè)系統(tǒng)里同時(shí)完美解決而把它拆成兩個(gè)各司其職的模塊再由服務(wù)層解釋成統(tǒng)一答案。這套思路從我第一次落地到現(xiàn)在已經(jīng)幫我在至少三個(gè)社交產(chǎn)品項(xiàng)目里扛過了流量翻倍的時(shí)期。我無法保證它適合所有場景但如果你正在處理的數(shù)據(jù)也有“又要求新鮮又要求歷史可追溯”的雙重性格Lambda這條路值得認(rèn)真走一遍。