實(shí)時(shí)事件聚合器rea:用Python構(gòu)建實(shí)時(shí)監(jiān)控與告警系統(tǒng))
如果你在做后端或者數(shù)據(jù)系統(tǒng)相關(guān)的工作大概率遇到過這種狀態(tài)服務(wù)上線了一堆日志散落在各個(gè)角落報(bào)表永遠(yuǎn)是第二天的領(lǐng)導(dǎo)或者運(yùn)營(yíng)突然問一句“現(xiàn)在支付成功率是多少”你只能打開幾個(gè)終端去數(shù)日志。我去年在某公司內(nèi)部就折騰了這樣一個(gè)小項(xiàng)目名字就叫rea全稱是Real-time Event Aggregator實(shí)時(shí)事件聚合器。rea 做的事不復(fù)雜把不同服務(wù)產(chǎn)生的結(jié)構(gòu)化事件統(tǒng)一收上來按業(yè)務(wù)規(guī)則做實(shí)時(shí)窗口聚合算出大家關(guān)心的指標(biāo)再在指標(biāo)異常時(shí)把消息推給負(fù)責(zé)人。文章后面我準(zhǔn)備把整個(gè)項(xiàng)目從需求拆解、架構(gòu)設(shè)計(jì)、核心實(shí)現(xiàn)到踩坑記錄完整講一遍如果你正在做數(shù)據(jù)采集、監(jiān)控看板或者想自己搭一套輕量級(jí)實(shí)時(shí)處理管線這里面的思路和代碼應(yīng)該能直接參考。1. 項(xiàng)目背景與需求拆解為什么會(huì)有 rea1.1 最初的痛點(diǎn)數(shù)據(jù)很多答案很少項(xiàng)目開始前我面對(duì)的情況很典型。支付鏈路的成功率算不出來實(shí)時(shí)值因?yàn)橹Ц妒录诮灰追?wù)扣款結(jié)果在賬務(wù)服務(wù)前端埋點(diǎn)在客戶端三個(gè)地方的數(shù)據(jù)分散在不同的存儲(chǔ)里。運(yùn)營(yíng)想第二天看數(shù)據(jù)數(shù)據(jù)分析那邊要凌晨跑批任務(wù)等結(jié)果出來已經(jīng)是早上六七點(diǎn)。而當(dāng)時(shí)最常被問到的問題恰恰都是“現(xiàn)在怎么樣”現(xiàn)在下單成功率是多少、現(xiàn)在告警數(shù)多不多、新版功能上線后點(diǎn)擊率有沒有變化。我以前也做過一些臨時(shí)腳本從日志文件里 grep 關(guān)鍵詞再用時(shí)間戳統(tǒng)計(jì)。這種辦法在請(qǐng)求量小的時(shí)候勉強(qiáng)能用但事件一多就亂了而且每次都要重新寫一遍解析邏輯完全沒有積累。說到底不是沒有數(shù)據(jù)是缺一條能實(shí)時(shí)匯總結(jié)論的鏈路。rea 的核心訴求就是把“散落的事件”變成“實(shí)時(shí)的指標(biāo)”。1.2 需求收斂三個(gè)核心能力項(xiàng)目一開始很容易貪大想一個(gè)系統(tǒng)搞定采集、清洗、計(jì)算、存儲(chǔ)、展示、告警、權(quán)限管理。后來我把需求砍到只保留真正要用的三塊第一統(tǒng)一接入。所有來源的事件不管原來是日志、HTTP 回調(diào)還是消息隊(duì)列里的數(shù)據(jù)都通過同一個(gè)協(xié)議進(jìn)入 rea統(tǒng)一成一種結(jié)構(gòu)。這一步解決了“多系統(tǒng)重復(fù)對(duì)接”的麻煩。第二實(shí)時(shí)聚合。對(duì)特定事件按時(shí)間窗口做計(jì)數(shù)、求和、平均、去重、分位數(shù)等計(jì)算窗口能精細(xì)到 10 秒或 1 分鐘級(jí)別結(jié)果要能立刻查到。第三規(guī)則告警。預(yù)設(shè)閾值比如“最近 5 分鐘支付失敗率大于 10%”一旦命中就通過 webhook 推到團(tuán)隊(duì)群里替代人工盯日志。查詢展示是后加的但很輕量只做了一個(gè)簡(jiǎn)單的指標(biāo)接口讓運(yùn)營(yíng)那邊能用表格工具拉數(shù)。當(dāng)時(shí)如果加太多權(quán)限、審計(jì)、多租戶的需求項(xiàng)目大概率會(huì)爛尾。小團(tuán)隊(duì)做內(nèi)部工具先把主鏈路打通比什么功能都重要。1.3 技術(shù)選型思考為什么不直接上重型方案討論方案時(shí)難免有人提議用現(xiàn)成的流處理框架加時(shí)序數(shù)據(jù)庫再配一套監(jiān)控告警組件。不能說這個(gè)思路不對(duì)但對(duì)當(dāng)時(shí)的數(shù)據(jù)規(guī)模不劃算。每天事件量在幾百萬條級(jí)別規(guī)則只有十幾條團(tuán)隊(duì)就兩三個(gè)人還需要同時(shí)維護(hù)多個(gè)節(jié)點(diǎn)學(xué)習(xí)成本和運(yùn)維成本反而吞掉了業(yè)務(wù)收益。rea 走的是一條中間路線用輕量消息中間件接收事件用 Python 做聚合邏輯用內(nèi)存存儲(chǔ)保存近期窗口結(jié)果定期把快照和明細(xì)落到磁盤。這樣的組合勝在簡(jiǎn)單一個(gè)進(jìn)程就能跑起來出現(xiàn)問題我能直接看代碼定位不需要翻一堆分布式系統(tǒng)的日志。我列過一個(gè)對(duì)比也貼在當(dāng)時(shí)的方案文檔里對(duì)比維度重型方案rea 輕量方案部署節(jié)點(diǎn)數(shù)至少 5 個(gè)以上1 個(gè)主進(jìn)程學(xué)習(xí)成本需要理解流處理概念和運(yùn)維細(xì)節(jié)只需掌握 Python 和基礎(chǔ)隊(duì)列擴(kuò)展能力適合每天億級(jí)事件百萬級(jí)事件夠用故障排查涉及多組件協(xié)調(diào)鏈路長(zhǎng)單進(jìn)程日志清晰迭代速度改一行邏輯可能要重新提交任務(wù)改完重啟即可這個(gè)選擇的核心邏輯是控制復(fù)雜度先把業(yè)務(wù)問題解決掉。如果將來事件規(guī)模漲到億級(jí)再遷到重型架構(gòu)也不遲而且遷移時(shí)聚合規(guī)則和告警邏輯是可以原樣帶過去的。2. 整體架構(gòu)設(shè)計(jì)從一個(gè)采集口到完整處理管線2.1 模塊劃分與數(shù)據(jù)流向rea 的架構(gòu)沒有特別玄乎數(shù)據(jù)流是單向的接入端收到事件經(jīng)過校驗(yàn)和規(guī)范化放進(jìn)一個(gè)有界隊(duì)列聚合進(jìn)程從隊(duì)列里取事件按事件名和維度分組更新對(duì)應(yīng)時(shí)間窗口的累加值規(guī)則引擎定時(shí)掃描窗口狀態(tài)命中閾值就觸發(fā)告警查詢接口直接讀窗口狀態(tài)返回近實(shí)時(shí)指標(biāo)。展開成模塊就是五個(gè)部分接入適配器、事件隊(duì)列、聚合內(nèi)核、規(guī)則引擎、存儲(chǔ)與查詢。接入適配器負(fù)責(zé)把不同來源的數(shù)據(jù)格式轉(zhuǎn)成統(tǒng)一事件事件隊(duì)列起到緩沖作用避免瞬時(shí)流量沖垮聚合端聚合內(nèi)核是核心后面重點(diǎn)講規(guī)則引擎不參與計(jì)算只做判斷存儲(chǔ)層管窗口狀態(tài)的快照和查詢。我特意沒有引入外部存儲(chǔ)依賴來存每條原始事件只在內(nèi)存里維護(hù)聚合狀態(tài)。因?yàn)橹笜?biāo)計(jì)算需要的是窗口內(nèi)的匯總值不是每條明細(xì)明細(xì)另外落一份磁盤文件就夠了平時(shí)沒有人會(huì)去逐條查。2.2 數(shù)據(jù)接入層統(tǒng)一協(xié)議與適配器接入層要解決的核心問題是格式混亂。有的服務(wù)發(fā) JSON有的發(fā)純文本日志有的通過回調(diào)帶一串加密參數(shù)。rea 的做法是定一個(gè)內(nèi)部標(biāo)準(zhǔn)事件結(jié)構(gòu)所有外部數(shù)據(jù)都通過適配器轉(zhuǎn)換成標(biāo)準(zhǔn)結(jié)構(gòu)。標(biāo)準(zhǔn)事件包含五類字段事件名、發(fā)生時(shí)間、來源服務(wù)、維度標(biāo)簽、數(shù)值。事件名決定算哪類指標(biāo)維度標(biāo)簽用于拆分統(tǒng)計(jì)數(shù)值用于求和或平均。這樣設(shè)計(jì)有一個(gè)好處新接入一個(gè)數(shù)據(jù)源時(shí)只需要寫一個(gè)適配器函數(shù)不需要改聚合邏輯。之前新業(yè)務(wù)接入平均半天就能完成大部分時(shí)間花在溝通字段含義上而不是寫代碼。2.3 聚合內(nèi)核為什么不用每條事件都寫庫聚合最忌諱的做法是每來一條事件就寫一次數(shù)據(jù)庫。之前我用關(guān)系表做過一版每秒一千條事件時(shí)數(shù)據(jù)庫寫并發(fā)就把服務(wù)拖垮了。后來改成窗口預(yù)聚合事件只寫內(nèi)存窗口關(guān)閉時(shí)才把匯總結(jié)果寫一次。具體做法是用事件名和維度組合作為桶的 key每個(gè)桶里保存若干時(shí)間窗口的累加值。比如一個(gè)桶 key 可能是pay_result:pay_success:dcsz同一秒內(nèi)所有到達(dá)事件累加到一個(gè)計(jì)數(shù)器上。查詢時(shí)直接取桶內(nèi)當(dāng)前窗口的值不用掃描明細(xì)。這條思路其實(shí)就是流式聚合里的“增量更新”rea 只是用最簡(jiǎn)單的方式實(shí)現(xiàn)了它。2.4 存儲(chǔ)與查詢熱數(shù)據(jù)在內(nèi)存快照落磁盤窗口聚合結(jié)果屬于熱數(shù)據(jù)讀取頻繁且過期很快放在內(nèi)存里最合適。rea 用一個(gè)全局字典保存所有桶的窗口狀態(tài)字典的 key 是維度組合value 是窗口數(shù)組。窗口過期后不是立刻刪除而是把它轉(zhuǎn)成一行匯總記錄寫入磁盤上的日志文件方便以后按小時(shí)回看。查詢接口讀取內(nèi)存字典返回當(dāng)前窗口的指標(biāo)值。如果某個(gè)維度組合沒有數(shù)據(jù)接口返回空而不是報(bào)錯(cuò)這樣在看板工具里顯示成 0 或 null 都由前端決定。存儲(chǔ)層的核心取舍是為查詢速度犧牲少量一致性。重啟或崩潰時(shí)最多丟幾秒窗口數(shù)據(jù)對(duì)業(yè)務(wù)影響可以接受。3. 關(guān)鍵技術(shù)細(xì)節(jié)與實(shí)現(xiàn)3.1 統(tǒng)一事件模型字段與約定rea 的事件模型長(zhǎng)這樣{ event_name: pay_result, occurred_at: 1727600000000, source: trade-service, dimensions: { result: pay_success, dc: sz }, value: 1, trace_id: 8f3a2b }event_name 用“業(yè)務(wù)動(dòng)作”而不是“日志級(jí)別”來命名比如 pay_result、order_created、button_click。occurred_at 使用毫秒級(jí) UTC 時(shí)間戳這是整個(gè)系統(tǒng)統(tǒng)一時(shí)間的基礎(chǔ)。dimensions 是維度標(biāo)簽的集合聚合時(shí)會(huì)把整個(gè) dimensions 字典序列化后拼進(jìn)桶 key這意味著不同維度組合會(huì)獨(dú)立計(jì)算互不干擾。value 默認(rèn)是 1如果需要統(tǒng)計(jì)金額就傳具體數(shù)值。時(shí)間戳有一個(gè)容易被忽略的細(xì)節(jié)客戶端和服務(wù)端時(shí)鐘可能不一致。如果客戶端提前兩秒上報(bào)按處理時(shí)間統(tǒng)計(jì)會(huì)落到錯(cuò)誤的窗口。rea 統(tǒng)一用 occurred_at 作為事件時(shí)間處理進(jìn)程只負(fù)責(zé)記賬不負(fù)責(zé)打時(shí)間戳。3.2 窗口聚合算法滾動(dòng)窗口與滑動(dòng)窗口窗口是實(shí)時(shí)聚合的基本單位。我實(shí)現(xiàn)了兩種窗口。滾動(dòng)窗口最直觀固定時(shí)間長(zhǎng)度比如每分鐘一個(gè)桶。實(shí)現(xiàn)上依賴取整函數(shù)把 occurred_at 除以窗口毫秒數(shù)再取整就得到窗口序號(hào)同一個(gè)窗口序號(hào)的事件歸到同一批。代碼如下def window_key(event_time_ms: int, window_ms: int) - int: return event_time_ms // window_ms滑動(dòng)窗口更復(fù)雜一點(diǎn)比如“最近 5 分鐘”每秒滑動(dòng)一次。如果每個(gè)窗口都單獨(dú)存一份累加值內(nèi)存消耗會(huì)很大。rea 的簡(jiǎn)化做法是維護(hù)一個(gè)按秒拆分的數(shù)組長(zhǎng)度等于窗口秒數(shù)每秒一個(gè)計(jì)數(shù)器。計(jì)算窗口總量時(shí)把數(shù)組里所有元素相加而不是存多份窗口狀態(tài)。class SlidingWindow: def __init__(self, window_seconds300): self.buckets [0] * window_seconds self.window_seconds window_seconds def add(self, occurred_at_ms: int, delta: float): idx (occurred_at_ms // 1000) % self.window_seconds self.buckets[idx] delta def total(self) - float: return sum(self.buckets)這個(gè)實(shí)現(xiàn)把內(nèi)存占用壓到最低代價(jià)是總量計(jì)算需要遍歷數(shù)組。窗口切成 300 個(gè)一秒桶時(shí)每次求和是 300 次加法在百萬級(jí)事件下完全可接受。3.3 事件亂序與遲到數(shù)據(jù)用延遲緩沖兜底分布式環(huán)境下事件亂序是必然的。A 服務(wù)先發(fā)出事件但因?yàn)榫W(wǎng)絡(luò)重試比 B 服務(wù)的事件晚到如果直接按事件時(shí)間累加窗口狀態(tài)就會(huì)出現(xiàn)偏差。rea 的解決方案是一個(gè)簡(jiǎn)單的延遲緩沖器接收到事件后先不立刻累加而是放進(jìn)一個(gè)按事件時(shí)間排序的待處理列表停留 2 秒。如果事件遲到時(shí)間小于緩沖就能歸到正確的窗口。大于緩沖時(shí)間的遲到事件會(huì)單獨(dú)進(jìn)入“l(fā)ate_events”計(jì)數(shù)器。這樣做不是完美方案但能覆蓋大部分亂序情況。想完全處理亂序需要引入水印機(jī)制和更復(fù)雜的窗口觸發(fā)邏輯對(duì)這些規(guī)則的場(chǎng)景來說不劃算。3.4 狀態(tài)快照與恢復(fù)重啟不丟全部狀態(tài)聚合狀態(tài)全在內(nèi)存進(jìn)程一重啟就全沒了這不滿足實(shí)用性要求。rea 實(shí)現(xiàn)了兩個(gè)輔助機(jī)制窗口快照和事件日志。窗口快照每隔 30 秒全量把內(nèi)存字典寫入本地文件文件名帶時(shí)間戳。事件日志則是把進(jìn)入聚合內(nèi)核的每一條事件追加寫入一個(gè)滾動(dòng)文件。重啟時(shí)先加載最近一個(gè)快照再重放快照之后的事件日志就能恢復(fù)到接近實(shí)時(shí)狀態(tài)。這個(gè)機(jī)制有點(diǎn)像數(shù)據(jù)庫的 checkpoint binlog只不過做得粗糙直接。實(shí)際操作中我一般直接手動(dòng)重啟恢復(fù)時(shí)間控制在幾秒內(nèi)。涉及丟失的數(shù)據(jù)最多是快照之后、重啟之前到達(dá)的少量事件業(yè)務(wù)上能容忍。4. 實(shí)操過程從零搭起一套最小可用 rea4.1 環(huán)境準(zhǔn)備與依賴選擇rea 原型只用 Python 標(biāo)準(zhǔn)庫加一個(gè)輕量 HTTP 處理模塊沒有引入復(fù)雜的流處理框架。要做到快速?gòu)?fù)現(xiàn)你需要 Python 3.10 以上環(huán)境能跑python -m http.server就行。我建議再準(zhǔn)備一個(gè)本地目錄存快照和事件日志后續(xù)查看調(diào)試方便。因?yàn)椴幌胱屧鸵蕾囂嗤獠糠?wù)消息隊(duì)列我用一個(gè)簡(jiǎn)單的內(nèi)存有界隊(duì)列代替。生產(chǎn)環(huán)境可以換成成熟的消息中間件但對(duì)驗(yàn)證流程來說不影響核心邏輯。4.2 定義配置與規(guī)則rea 的配置分為兩塊全局配置和規(guī)則配置。全局配置定義端口、窗口大小、快照間隔。規(guī)則配置定義監(jiān)控什么事件、按什么維度聚合、觸發(fā)什么條件。規(guī)則我設(shè)計(jì)成 JSON方便接入方自己改。{ rules: [ { name: pay_failure_rate_5min, event_name: pay_result, window_seconds: 300, metric: rate, condition: 0.10 } ] }規(guī)則里 metric 支持 count、sum、avg、rate。rate 會(huì)先算出窗口內(nèi)失敗數(shù)除以總數(shù)再和閾值比較。寫規(guī)則時(shí)最關(guān)鍵的是想清楚窗口大小太短容易誤報(bào)太長(zhǎng)反饋慢我通常建議從 5 分鐘起步再微調(diào)。4.3 核心代碼實(shí)現(xiàn)下面是一個(gè)濃縮版的核心實(shí)現(xiàn)只保留接入、聚合、規(guī)則判斷三塊邏輯。import json import time import threading from collections import defaultdict from http.server import BaseHTTPRequestHandler, HTTPServer class WindowAggregator: def __init__(self, window_seconds300): self.window_seconds window_seconds self.counters defaultdict(lambda: defaultdict(float)) self.lock threading.Lock() def add(self, event): event_name event[event_name] dim_key json.dumps(event.get(dimensions, {}), sort_keysTrue) bucket event[occurred_at] // 1000 // self.window_seconds with self.lock: self.counters[event_name][dim_key][bucket] event.get(value, 1) def snapshot(self): with self.lock: return json.dumps(self.counters)這個(gè)類把事件累加到對(duì)應(yīng)窗口桶。注意并發(fā)安全HTTP 線程和聚合線程同時(shí)訪問 counters所以加了一把鎖。規(guī)則引擎負(fù)責(zé)掃描窗口狀態(tài)class RuleEngine: def __init__(self, rules, aggregator): self.rules rules self.aggregator aggregator def evaluate(self): now time.time() * 1000 current_bucket now // 1000 // self.aggregator.window_seconds for rule in self.rules: event_name rule[event_name] dim_key rule.get(dim_key) for dims, buckets in self.aggregator.counters.get(event_name, {}).items(): if dim_key and dim_key ! dims: continue total sum(v for k, v in buckets.items() if k current_bucket) if rule[metric] count and total rule[threshold]: print(ftrigger rule: {rule[name]})這里為了展示邏輯閾值直接通過參數(shù)簡(jiǎn)化了實(shí)際項(xiàng)目里應(yīng)當(dāng)從 JSON 配置讀取。接入 HTTP 服務(wù)使用標(biāo)準(zhǔn)庫 BaseHTTPRequestHandler接口路徑/collect接收 POST JSON 事件。收到后解析、校驗(yàn)、轉(zhuǎn)為標(biāo)準(zhǔn)事件放進(jìn)隊(duì)列。聚合線程每秒從隊(duì)列取一批事件調(diào)用 aggregator再執(zhí)行規(guī)則評(píng)估。4.4 模擬數(shù)據(jù)與結(jié)果驗(yàn)證為了驗(yàn)證鏈路我寫了腳本模擬一萬次支付事件其中失敗事件約 300 次每次事件隨機(jī)分布在 5 分鐘內(nèi)。模擬完成后調(diào)用指標(biāo)接口返回五個(gè)窗口的失敗率和我預(yù)設(shè)的概率基本一致。整個(gè)處理流程耗時(shí)約 2.8 秒內(nèi)存占用不到 50MB說明這套輕量方案在小規(guī)模數(shù)據(jù)下完全跑得動(dòng)。壓測(cè)數(shù)據(jù)如下事件總量處理耗時(shí)峰值內(nèi)存16 核 CPU1 萬2.8 秒45 MB~5%10 萬26.1 秒168 MB~12%100 萬約 5 分鐘1.1 GB~35%一百萬條時(shí)內(nèi)存接近 1GB原因是窗口桶和維度 key 數(shù)量多。這是純 Python 實(shí)現(xiàn)的正常表現(xiàn)對(duì)于內(nèi)部工具是可接受的。如果數(shù)據(jù)量再漲第一件要做的事不是優(yōu)化代碼而是加維度基數(shù)限制。5. 常見問題與排查實(shí)錄5.1 事件積壓寫入慢拖垮消費(fèi)原型上線后遇到第一個(gè)問題是事件積壓。HTTP 接口收到事件后直接落日志文件每一條都觸發(fā)一次磁盤寫入壓力一大處理線程消費(fèi)不過來隊(duì)列長(zhǎng)度持續(xù)增長(zhǎng)。后來把日志寫入改成批量緩沖每攢夠 512 條或者間隔 1 秒才寫一次問題立刻緩解。排查這類問題的思路很簡(jiǎn)單先看隊(duì)列長(zhǎng)度是不是持續(xù)增長(zhǎng)如果是就分階段量每段的耗時(shí)。當(dāng)時(shí)量化結(jié)果是磁盤寫入占了 80% 以上耗時(shí)改掉就好。如果你在自建系統(tǒng)建議第一時(shí)間就把“批量讀寫”作為默認(rèn)設(shè)計(jì)不要寫出逐條落盤的版本。5.2 數(shù)據(jù)亂序兩個(gè)服務(wù)的時(shí)間差引發(fā)誤統(tǒng)計(jì)我用一個(gè)模擬客戶端同時(shí)發(fā)送事件發(fā)現(xiàn)服務(wù) A 的事件總是比服務(wù) B 晚 3 秒。原因是 A 服務(wù)里的日志時(shí)間用的是本機(jī)時(shí)間該機(jī)器時(shí)鐘慢了 3 秒。結(jié)果按事件時(shí)間統(tǒng)計(jì)時(shí)部分事件被分到錯(cuò)誤窗口導(dǎo)致指標(biāo)時(shí)高時(shí)低。解決辦法分兩步客戶端統(tǒng)一改用 rea 下發(fā)的標(biāo)準(zhǔn)時(shí)間戳同時(shí)保留事件發(fā)生時(shí)間rea 內(nèi)部對(duì)遲到事件做延遲緩沖覆蓋網(wǎng)絡(luò)抖動(dòng)和輕微時(shí)鐘偏差。如果偏差超過幾分鐘再精確處理就不劃算了應(yīng)該直接從數(shù)據(jù)源修時(shí)鐘而不是在聚合層硬扛。5.3 內(nèi)存溢出維度基數(shù)失控事件里有一個(gè)維度叫request_id本來是想做鏈路追蹤但被誤加進(jìn)了聚合維度。每條請(qǐng)求的 request_id 都不同導(dǎo)致桶 key 數(shù)量飛速增長(zhǎng)內(nèi)存直接暴漲。這個(gè)問題很隱蔽因?yàn)闇y(cè)試數(shù)據(jù)量小看不出問題一上真實(shí)流量就暴露了。解決方法是兩招一是在規(guī)則配置里顯式聲明參與聚合的維度白名單不在白名單里的字段不拼進(jìn) key二是給桶數(shù)量設(shè)置上限超過上限時(shí)輸出告警而不是繼續(xù)建新桶。自那以后我對(duì)“維度基數(shù)”就特別敏感制定新規(guī)則前必須先問一句這個(gè)維度會(huì)有多少個(gè)不同的值。5.4 窗口邊界時(shí)間取整帶來的誤差另一個(gè)經(jīng)典問題是窗口邊界。假設(shè)窗口是 5 分鐘我用occurred_at / 1000 / 300取整剛好整點(diǎn) 12:00:00 的事件會(huì)被分到下一個(gè)窗口嗎不會(huì)整除邏輯是歸到當(dāng)前窗口。但查詢端展示時(shí)如果前端自己按“當(dāng)前時(shí)間 -5 分鐘”做聚合就跟 rea 的窗口錯(cuò)位了。這里我踩過坑之后總結(jié)了兩條約定所有時(shí)間顯示統(tǒng)一使用 UTC不能直接用本地時(shí)間拼字符串所有窗口的起止時(shí)間由 rea 接口返回前端只渲染不在查詢端二次計(jì)算窗口??缣臁⒖缧r(shí)的邊界問題在統(tǒng)一時(shí)間基準(zhǔn)后基本都消失了。6. 落地效果與擴(kuò)展空間6.1 實(shí)際收益從十分鐘到五秒rea 上線之后最直觀的變化是業(yè)務(wù)方問“現(xiàn)在怎么樣”時(shí)能在幾秒內(nèi)給出答案。支付成功率、錯(cuò)誤數(shù)、關(guān)鍵埋點(diǎn)點(diǎn)擊量這些指標(biāo)不再等第二天的報(bào)表。告警方面原來靠人工盯日志平均發(fā)現(xiàn)一次異常要十分鐘左右rea 把瓶頸壓到 5 秒級(jí)別而且直接在團(tuán)隊(duì)群推送比人肉監(jiān)控可靠得多。從成本上看這一整套系統(tǒng)只占一個(gè)低配容器每月資源開銷可以忽略不計(jì)。對(duì)于兩三個(gè)人的小組rea 的投入產(chǎn)出比相當(dāng)高。它沒有解決所有數(shù)據(jù)問題但解決了當(dāng)時(shí)最痛的那一個(gè)。6.2 可擴(kuò)展方向規(guī)則沉淀與多級(jí)聚合如果繼續(xù)演進(jìn)我會(huì)做三件事。第一把規(guī)則從 JSON 升級(jí)成可以熱加載的配置中心業(yè)務(wù)方自助提交規(guī)則不用每次改代碼重啟。第二增加多級(jí)聚合能力先按原始維度聚合再按省份、機(jī)房等上級(jí)維度合并這樣能省掉大量重復(fù)計(jì)算。第三把窗口結(jié)果定時(shí)導(dǎo)出到離線數(shù)倉(cāng)做趨勢(shì)分析讓實(shí)時(shí)指標(biāo)和歷史報(bào)表在同一套口徑下對(duì)齊。還有一個(gè)方向是預(yù)警自愈。告警觸發(fā)后自動(dòng)附帶最近十分鐘的趨勢(shì)圖鏈接甚至觸發(fā)一個(gè)預(yù)置的處置腳本比如限流或者切流量。這些能力并不難加因?yàn)榫酆蠣顟B(tài)已經(jīng)在內(nèi)存里了只是多一步動(dòng)作。6.3 個(gè)人體會(huì)簡(jiǎn)單系統(tǒng)也能解決真問題做完 rea我最大的體會(huì)是不要總想著一步到位。當(dāng)時(shí)如果堅(jiān)持上全家桶方案大概率到現(xiàn)在還在調(diào)集群業(yè)務(wù)問題一個(gè)都沒解決。反而是一個(gè)單進(jìn)程、代碼能一眼看穿的輕量系統(tǒng)實(shí)實(shí)在在跑了大半年。另外對(duì)實(shí)時(shí)系統(tǒng)的監(jiān)控、告警、恢復(fù)機(jī)制一定要在一開始預(yù)留好位置。哪怕用最簡(jiǎn)單的日志打印、文件快照也比完全沒有強(qiáng)。我就是因?yàn)橐婚_始沒有快照機(jī)制第一次重啟時(shí)全狀態(tài)丟失才回頭補(bǔ)的。如果讓我現(xiàn)在重新面對(duì)三個(gè)字母“rea”我還是會(huì)選同樣的路先想清楚要解決誰的問題用最小閉環(huán)跑通再讓系統(tǒng)隨著真實(shí)需求慢慢長(zhǎng)。很多系統(tǒng)不是死在性能上而是死在復(fù)雜度失控上。