序言
時間回到2008年,還在上海交通大學上學的張旭豪、康嘉等人在上海創辦了餓了么,從校園外賣場景出發,餓了么一步一步發展壯大,成為外賣行業的領頭羊。2017年8月餓了么並購百度外賣,強強合並,繼續開疆擴土。2018年餓了么加入阿里巴巴大家庭,與口碑融合成立阿里巴巴本地生活公司。“愛什么,來什么”,是餓了么對用戶不變的承諾。
餓了么的技術也伴隨着業務的飛速增長也不斷突飛猛進。據公開報道,2014年5月的日訂單量只有10萬,但短短幾個月之后就沖到了日訂單百萬,到當今日訂單上千萬單。在短短幾年的技術發展歷程上,餓了么的技術體系、穩定性建設、技術文化建設等都有長足的發展。各位可查看往期文章一探其中發展歷程,在此不再贅述:
而可觀測性作為技術體系的核心環節之一,也跟隨餓了么技術的飛速發展,不斷自我革新,從“全鏈路可觀測性ETrace”擴展到“多活下的可觀測性體系ETrace”,發展成目前“一站式可觀測性平台EMonitor”。
EMonitor經過5年的多次迭代,現在已經建成了集指標數據、鏈路追蹤、可視化面板、報警與分析等多個可觀測性領域的平台化產品。EMonitor每日處理約1200T的原始可觀測性數據,覆蓋餓了么絕大多數中間件,可觀測超5萬台機器實例,可觀測性數據時延在10秒左右。面向餓了么上千研發人員,EMonitor提供精准的報警服務和多樣化的觸達手段,同時運行約2萬的報警規則。本文就細數餓了么可觀測性的建設歷程,回顧下“餓了么可觀測性建設的那些年”。
1.0:混沌初開,萬物興起
翻看代碼提交記錄,ETrace項目的第一次提交在2015年10月24日。而2015年,正是餓了么發展的第七個年頭,也是餓了么業務、技術、人員開始蓬勃發展的年頭。彼時,餓了么的可觀測性系統依賴Zabbix、Statsd、Grafana等傳統的“輕量級”系統。而“全鏈路可觀測性”正是當時的微服務化技術改造、后端服務Java化等技術發展趨勢下的必行之勢。
我們可觀測性團隊,在調研業界主流的全鏈路可觀測性產品--包括著名的開源全鏈路可觀測性產品“CAT”后,吸取眾家之所長,在兩個多月的爆肝開發后,推出了初代ETrace。我們提供的Java版本ETrace-Agent隨着新版的餓了么SOA框架“Pylon”在餓了么研發團隊中的推廣和普及開來。ETrace-Agent能自動收集應用的SOA調用信息、API調用信息、慢請求、慢SQL、異常信息、機器信息、依賴信息等。下圖為1.0版本的ETrace頁面截圖。
在經歷了半年的爆肝開發和各中間件兄弟團隊的鼎力支持,我們又開發了Python版本的Agent,更能適應餓了么當時各語言百花齊放的技術體系。並且,通過和餓了么DAL組件、緩存組件、消息組件的密切配合與埋點,用戶的應用增加了多層次的訪問信息,鏈路更加完整,故障排查過程更加清晰。
整體架構體系
ETrace整體架構如下圖。通過SDK集成在用戶應用中的Agent定期將Trace數據經Thrift協議發送到Collector(Agent本地不落日志),Collector經初步過濾后將數據打包壓縮發往Kafka。Kafka下游的Consumer消費這些Trace數據,一方面將數據寫入HBase+HDFS,一方面根據與各中間件約定好的埋點規則,將鏈路數據計算成指標存儲到時間序列數據庫-- LinDB中。在用戶端,Console服務提供UI及查詢指標與鏈路數據的API,供用戶使用。
全鏈路可觀測性的實現
所謂全鏈路可觀測性,即每次業務請求中都有唯一的能夠標記這次業務完整的調用鏈路,我們稱這個ID為RequestId。而每次鏈路上的調用關系,類似於樹形結構,我們將每個樹節點上用唯一的RpcId標記。
如圖,在入口應用App1上會新建一個隨機RequestId(一個類似UUID的32位字符串,再加上生成時的時間戳)。因它為根節點,故RpcId為“1”。在后續的RPC調用中,RequestId通過SOA框架的Context傳遞到下一節點中,且下一節點的層級加1,變為形如“1.1”、“1.2”。如此反復,同一個RequestId的調用鏈就通過RpcId還原成一個調用樹。
也可以看到,“全鏈路可觀測性的實現”不僅依賴與ETrace系統自身的實現,更依托與公司整體中間件層面的支持。如在請求入口的Gateway層,能對每個請求生成“自動”新的RequestId(或根據請求中特定的Header信息,復用RequestId與RpcId);RPC框架、Http框架、Dal層、Queue層等都要支持在Context中傳遞RequestId與RpcId。
ETrace Api示例
在Java或Python中提供鏈路埋點的API:
/* 記錄一個調用鏈路 / Transaction trasaction = Trace.newTransaction(String type, String name); // business codes transaction.complete(); /* 記錄調用中的一個事件 / Trace.logEvent(String type, String name, Map<String,String> tags, String status, String data) /* 記錄調用中的一個異常 / Trace.logError(String msg, Exception e)
Consumer的設計細節
Consumer組件的核心任務就是將鏈路數據寫入存儲。主要思路是以RequestId+RpcId作為主鍵,對應的Data數據寫入存儲的Payload。再考慮到可觀測性場景是寫多讀少,並且多為文本類型的Data數據可批量壓縮打包存儲,因此我們設計了基於HDFS+HBase的兩層索引機制。
如圖,Consumer將Collector已壓縮好的Trace數據先寫入HDFS,並記錄寫入的文件Path與寫入的Offset,第二步將這些“索引信息”再寫入HBase。特別的,構建HBase的Rowkey時,基於ReqeustId的Hashcode和HBase Table的Region數量配置,來生成兩個Byte長度的ShardId字段作為Rowkey前綴,避免了某些固定RequestId格式可能造成的寫入熱點問題。(因RequestId在各調用源頭生成,如應用自身、Nginx、餓了么網關層等。可能某應用錯誤設置成以其AppId為前綴RequestId,若沒有ShardId來打散,則它所有RequestId都將落到同一個HBase Region Server上。)
在查詢時,根據RequestId + RpcId作為查詢條件,依次去HBase、HDFS查詢原始數據,便能找到某次具體的調用鏈路數據。但有的需求場景是,只知道源頭的RequestId需要查看整條鏈路的信息,希望只排查鏈路中狀態異常的或某些指定RPC調用的數據。因此,我們在HBbase的Column Value上還額外寫了RPCInfo的信息,來記錄單次調用的簡要信息。如:調用狀態、耗時、上下游應用名等。
此外,餓了么的場景下,研發團隊多以訂單號、運單號作為排障的輸入,因此我們和業務相關團隊約定特殊的埋點規則--在Transaction上記錄一個特殊的"orderId={實際訂單號}"的Tag--便會在HBase中新寫一條“訂單表”的記錄。該表的設計也不復雜,Rowkey由ShardId與訂單號組成,Columne Value部分由對應的RequestId+RpcId及訂單基本信息(類似上文的RPCInfo)三部分組成。
如此,從業務鏈路到全鏈路信息到詳細單個鏈路,形成了一個完整的全鏈路排查體系。
Consumer組件的另一個任務則是將鏈路數據計算成指標。實現方式是在寫入鏈路數據的同時,在內存中將Transaction、Event等數據按照既定的計算邏輯,計算成SOA、DAL、Queue等中間件的指標,內存稍加聚合后再寫入時序數據庫LinDB。
指標存儲:LinDB 1.0
應用指標的存儲是一個典型的時間序列數據庫的使用場景。根據我們以前的經驗,市面上主流的時間序列數據庫-- OpenTSDB、InfluxDB、Graphite--在擴展能力、集群化、讀寫效率等方面各有缺憾,所以我們選型使用RocksDB作為底層存儲引擎,借鑒Kafka的集群模式,開發了餓了么的時間序列數據庫--LinDB。
指標采用類似Prometheus的“指標名+鍵值對的Tags”的數據模型,每個指標只有一個支持Long或Double的Field。某個典型的指標如:
COUNTER: eleme_makeorder{city="shanghai",channel="app",status="success"} 45
我們主要做了一些設計實現:
- 指標寫入時根據“指標名+Tags”進行Hash寫入到LinDB的Leader上,由Leader負責同步給他的Follower。
- 借鑒OpenTSDB的存儲設計,將“指標名”、TagKey、TagValue都轉化為Integer,放入映射表中以節省存儲資源。
- RocksDB的存儲設計為:以"指標名+TagKeyId + TagValueId+時間(小時粒度)“作為Key,以該小時時間線內的指標數值作為Value。
- 為實現Counter、Timer類型數據聚合邏輯,開發了C++版本RocksDB插件。
這套存儲方案在初期很好的支持了ETrace的指標存儲需求,為ETrace大規模接入與可觀測性數據的時效性提供了堅固的保障。有了ETrace,餓了么的技術人終於能從全鏈路的角度去排查問題、治理服務,為之后的技術升級、架構演進,提供了可觀測性層面的支持。
其中架構的幾點說明
1. 是否保證所有可觀測性數據的可靠性?
不,我們承諾的是“盡可能不丟”,不保證100%的可靠性。基於這個前提,為我們設計架構時提供了諸多便利。如,Agent與Collector若連接失敗,若干次重試后便丟棄數據,直到Collector恢復可用;Kafka上下游的生產和消費也不必Ack,避免影響處理效率。
2. 為什么在SDK中的Agent將數據發給Collector,而不是直接發送到Kafka?
- 避免Agent與Kafka版本強綁定,並避免引入Kafka Client的依賴。
- 在Collector層可以做數據的分流、過濾等操作,增加了數據處理的靈活性。並且Collector會將數據壓縮后再發送到Kafka,有效減少Kafka的帶寬壓力。
- Collector機器會有大量TCP連接,可針對性使用高性能機器。
3. SDK中的Agent如何控制對業務應用的影響?
- 純異步的API,內部采用隊列處理,隊列滿了就丟棄。
- Agent不會寫本地日志,避免占用磁盤IO、磁盤存儲而影響業務應用。
- Agent會定時從Collector拉取配置信息,以獲取后端Collector具體IP,並可實時配置來開關是否執行埋點。
4. 為什么選擇侵入性的Agent?
選擇寄生在業務應用中的SDK模式,在當時看來更利於ETrace的普及與升級。而從現在的眼光看來,非侵入式的Agent對用戶的集成更加便利,並且可以通過Kubernates中SideCar的方式對用戶透明部署與升級。
5. 如何實現“盡量不丟數據”?
- Agent中根據獲得的Collector IP周期性數據發送,若失敗則重試3次。並定期(5分鍾)獲取Collector集群的IP列表,隨機選取可用的IP發送數據。
- Collector中實現了基於本地磁盤的Queue,在后端的Kafka不可用時,會將可觀測性數據寫入到本地磁盤中。待Kafak恢復后,又會將磁盤上的數據,繼續寫入Kafka。
6. 可觀測性數據如何實現多語言支持?
Agent與Collector之間選擇Thrift RPC框架,並定制整個序列化方式。Java/Python/Go/PHP的Agent依數據規范開發即可。
2.0:異地多活,大勢初成
2016年底,餓了么為了迎接業務快速增長帶來的調整,開始推進“異地多活”項目。新的多數據中心架構對既有的可觀測性架構也帶來了調整,ETrace亦經過了一年的開發演進,升級到多數據中心的新架構、拆分出實時計算模塊、增加報警功能等,進入ETrace2.0時代。
異地多活的挑戰
隨着餓了么的異地多活的技術改造方案確定,對可觀測性平台提出了新的挑戰:如何設計多活架構下的可觀測性系統?以及如何聚合多數據中心的可觀測性數據?
經過一年多的推廣與接入,ETrace已覆蓋了餓了么絕大多數各語言的應用,每日處理數據量已達到了數十T以上。在此數據規模下,決不可能將數據拉回到某個中心機房處理。因此“異地多活”架構下的可觀測性設計的原則是:各機房處理各自的可觀測性數據。
我們開發一個Gateway模塊來代理與聚合各數據中心的返回結果,它會感知各機房間內Console服務。圖中它處於某個中央的雲上區域,實際上它可以部署在各機房中,通過域名的映射機制來做切換。
如此部署的架構下,各機房中的應用由與機房相綁定的環境變量控制將可觀測性數據發送到該機房內的ETrace集群,收集、計算、存儲等過程都在同一機房內完成。用戶通過前端Portal來訪問各機房內的數據,使用體驗和之前類似。
即使考慮極端情況下--某機房完全不可用(如斷網),“異地多活”架構可將業務流量切換到存活的機房中,讓業務繼續運轉。而可觀測性上,通過將Portal域名與Gateway域名切換到存活的機房中,ETrace便能繼續工作(雖然會缺失故障機房的數據)。在機房網絡恢復后,故障機房內的可觀測性數據也能自動恢復(因為該機房內的可觀測性數據處理流程在斷網時仍在正常運作)。
可觀測性數據實時處理的挑戰
在1.0版本中的Consumer組件,既負責將鏈路數據寫入到HBase/HDFS中,又負責將鏈路數據計算成指標存儲到LinDB中。兩個流程可視為同步的流程,但前者可接受數分鍾的延遲,后者要求達到實時的時效性。當時HBase集群受限於機器性能與規模,經常在數據熱點時會寫入抖動,進而造成指標計算抖動,影響可用性。因此,我們迫切需要拆分鏈路寫入模塊與指標計算模塊。
在選型實時計算引擎時,我們考慮到需求場景是:
- 能靈活的配置鏈路數據的計算規則,最好能動態調整;
- 能水平擴展,以適應業務的快速發展;
- 數據輸出與既有系統(如LinDB與Kafka)無縫銜接;
很遺憾的是,彼時業界無現成的拿來即用的大數據流處理產品。我們就基於復雜事件處理(CEP)引擎Esper實現了一個類SQL的實時數據計算平台--Shaka。Shaka包括“Shaka Console”和“Shaka Container”兩個模塊。Shaka Console由用戶在圖形化界面上使用,來配置數據處理流程(Pipeline)、集群、數據源等信息。用戶完成Pipeline配置后,Shaka Console會將變更推送到Zookeeper上。無狀態的Shaka Container會監聽Zookeeper上的配置變更,根據自己所屬的集群去更新內部運行的Component組件。而各Component實現了各種數據的處理邏輯:消費Kafka數據、處理Trace/Metric數據、Metric聚合、運行Esper邏輯等。
Trace數據和Metric格式轉換成固定的格式后,剩下來按需編寫Esper語句就能生成所需的指標了。如下文所示的Esper語句,就能將類型為Transaction的Trace數據計算成以“{appId}.transaction”的指標(若Consumer中以編碼方式實現,約需要近百行代碼)。經過這次的架構升級,Trace數據能快速的轉化為實時的Metric數據,並且對於業務的可觀測性需求,只用改改SQL語句就能快速滿足,顯著降低了開發成本和提升了開發效率。
@Name('transaction') @Metric(name = '{appId}.transaction', tags = {'type', 'name', 'status', 'ezone', 'hostName'}, fields = {'timerCount', 'timerSum', 'timerMin', 'timerMax'}, sampling = 'sampling') select header.ezone as ezone, header.appId as appId, header.hostName as hostName, type as type, name as name, status as status, trunc_sec(timestamp, 10) as timestamp, f_sum(sum(duration)) as timerSum, f_sum(count(1)) as timerCount, f_max(max(duration)) as timerMax, f_min(min(duration)) as timerMin, sampling('Timer', duration, header.msg) as sampling from transaction group by header.appId, type, name, header.hostName, header.ezone, status, trunc_sec(timestamp, 10);
新的UI、更豐富的中間件數據
1.0版本的前端UI,是集成在Console項目中基於Angular V1開發的。我們迫切希望能做到前后端分離,各司其職。於是基於Angular V2的若干個月開發,新的Portal組件登場。得益於Angular的數據綁定機制,新的ETrace UI各組件間聯動更自然,排查故障更方便。
餓了么自有中間件的研發進程也在不斷前行,在可觀測性的打通上也不斷深化。2.0階段,我們進一步集成了--Redis、Queue、ElasticSearch等等,覆蓋了餓了么所有的中間件,讓可觀測性無死角。
殺手級功能:指標查看與鏈路查看的無縫整合
傳統的可觀測性系統提供的排障方式大致是:接收報警(Alert)--查看指標(Metrics)--登陸機器--搜索日志(Trace/Log),而ETrace通過Metric與Trace的整合,能讓用戶直接在UI上通過點擊就能定位絕大部分問題,顯著拔高了用戶的使用體驗與排障速度。
某個排查場景如:用戶發現總量異常突然增加,可在界面上篩選機房、異常類型等找到實際突增的某個異常,再在曲線上直接點擊數據點,就會彈出對應時間段的異常鏈路信息。鏈路上有詳細的上下游信息,能幫助用戶定位故障。
它的實現原理如上圖所示。具體的,前文提到的實時計算模塊Shaka將Trace數據計算成Metric數據時,會額外以抽樣的方式將Trace上的RequsetId與RpcId也寫到Metric上(即上文Esper語句中,生成的Metric中的sampling字段)。這種Metric數據會被Consumer模塊消費並寫入到HBase一張Sampling表中。
用戶在前端Portal的指標曲線上點擊某個點時,會構建一個Sampling的查詢請求。該請求會帶上:該曲線的指標名、數據點的起止時間、用戶選擇過濾條件(即Tags)。Consumer基於這些信息構建一個HBase的RegexStringComparator的Scan查詢。查詢結果中可能會包含多個結果,對應着該時間點內數據點(Metric)上發生的多個調用鏈路(Trace),繼而拿着結果中的RequestId+RpcId再去查詢一次HBase/HDFS存儲就能獲得鏈路原文。(注:實際構建HBase Rowkey時Tag部分存的是其Hashcode而不是原文String。)
眾多轉崗、離職的餓了么小伙伴,最念念不忘不完的就是這種“所見即所得”的可觀測性排障體驗。
報警Watchdog 1.0
在應用可觀測性基本全覆蓋之后,報警的需求自然成了題中之義。技術選型上,根據我們在實時計算模塊Shaka上收獲的經驗,決定再造一個基於實時數據的報警系統--Watchdog。
實時計算模塊Shaka中已經將Trace數據計算成指標Metrics,報警模塊只需消費這些數據,再結合用戶配置的報警規則產出報警事件即可。因此,我們選型使用Storm作為流式計算平台,在Spount層次根據報警規則過濾和分流數據,在Bolt層中Esper引擎運行着由用戶配置的報警規則轉化成Esper語句並處理分流后的Metric數據。若有符合Esper規則的數據,即生成一個報警事件Alert。Watchdog Portal模塊訂閱Kafka中的報警事件,再根據具體報警的觸達方式通知到用戶。默認Esper引擎中數據聚合時間窗口為1分鍾,所以整個數據處理流程的時延約為1分鍾左右。
Metrics API與LinDB 2.0:
在ETrace 1.0階段,我們只提供了Trace相關的API,LinDB僅供內部存儲使用。用戶逐步的意識到如果能將“指標”與“鏈路”整合起來,就能發揮更大的功用。因此我們在ETrace-Agent中新增了Metrics相關的API:
// 計數器類型 Trace.newCounter(String metricName).addTags(Map<String, String> tags).count(int value); // 耗時與次數 Trace.newTimer(String metricName).addTags(Map<String, String> tags).value(int value); // 負載大小與次數 Trace.newPayload(String metricName).addTags(Map<String, String> tags).value(int value); // 單值類型 Trace.newGauge(String metricName).addTags(Map<String, String> tags).value(int value);
基於這些API,用戶可以在代碼中針對他的業務邏輯進行指標埋點,為后來可觀測性大一統提供了實現條件。在其他組件同步開發時,我們也針對LinDB做了若干優化,提升了寫入性能與易用性:
- 增加Histogram、Gauge、Payload、Ratio多種指標數據類型;
- 從1.0版本的每條指標數據都調用一次RocksDB的API進行寫入,改成先在內存中聚合一段時間,再通過RocksDB的API進行批量寫入文件。
3.0:推陳出新,融會貫通
可觀測性系統大一統
在2017年的餓了么,除了ETrace外還有多套可觀測性系統:基於Statsd/Graphite的業務可觀測性系統、基於InfluxDB的基礎設施可觀測性系統。后兩者都集成Grafana上,用戶可以去查看他的業務或者機器的詳細指標。但實際排障場景中,用戶還是需要在多套系統間來回切換:根據Grafana上的業務指標感知業務故障,到ETrace上查看具體的SOA/DB故障,再到Grafana上去查看具體機器的網絡或磁盤IO指標。雖然,我們也開發了Grafana的插件來集成LinDB的數據源,但因本質上差異巨大的系統架構,還是讓用戶“疲於奔命”式的來回切換系統,用戶難以有統一的可觀測性體驗。因此2018年初,我們下定決心:將多套可觀測性系統合而為一,打通“業務可觀測性+應用可觀測性+基礎設施可觀測性”,讓ETrace真正成為餓了么的一站式可觀測性平台。
LinDB 3.0:
所謂“改造”未動,“存儲”先行。想要整合InfluxDB與Statsd,先要研究他們與LinDB的異同。我們發現,InfluxDB是支持一個指標名(Measurement)上有多個Field Key的。如,InfluxDB可能有以下指標:
measurement=census, fields={butterfiles=12, honeybees=23}, tags={location=SH, scientist=jack}, timestamp=2015-08-18T00:06:00Z
若是LinDB 2.0的模式,則需要將上述指標轉換成兩個指標:
measurement=census, field={butterfiles=12}, tags={location=SH, scientist=jack}, timestamp=2015-08-18T00:06:00Z measurement=census, field={honeybees=23}, tags={location=SH, scientist=jack}, timestamp=2015-08-18T00:06:00Z
可以想見在數據存儲與計算效率上,單Field模式有着極大的浪費。但更改指標存儲的Schema,意味着整個數據處理鏈路都需要做適配和調整,工作量和改動極大。然而不改就意味着“將就”,我們不能忍受對自己要求的降低。因此又經過了幾個月的爆肝研發,LinDB 3.0開發完成。
這次改動,除了升級到指標多Fields模式外,還有以下優化點:
- 集群方面引入Kafka的ISR設計,解決了之前機器故障時查詢數據缺失的問題。
- 存儲層面支持更加通用的多Field模式,並且支持對多Field之間的表達式運算。
- 引入了倒排索引,顯著提高了對於任意Tag組合的過濾查詢的性能。
- 支持了自動化的Rollup操作,對於任意時間范圍的查詢自動選取合適的粒度來聚合。
經過這次大規模優化后,從最初的每日5T指標數據漲到如今的每日200T數據,LinDB 3.0都經受住了考驗。指標查詢的響應時間的99分位線為200ms。詳細設計細節可參看文末的分布式時序數據庫 - LinDB。
將Statsd指標轉成LinDB指標
Statsd是餓了么廣泛使用的業務指標埋點方案,各機房有一個數十台機器規模的Graphite集群。考慮到業務的核心指標都在Statsd上,並且各個AppId以ETrace Metrics API替換Statsd是一個漫長的過程(也確實是,前前后后替換完成就花了將近一年時間)。為了減少對用戶與NOC團隊的影響,我們決定:用戶更新代碼的同時,由ETrace同時“兼容”Statsd的數據。
得益於餓了么強大的中間件體系,業務在用Statsd API埋點的同時會“自動”記一條特殊的Trace數據,攜帶上Statsd的Metric數據。那么只要處理Trace數據中的Statsd埋點,我們就能將大多數Statsd指標轉化為LinDB指標。如下圖:多個Statsd指標會轉為同一個LinDB指標。
// statsd: stats.app.myAppName.order.from_ios.success 32 stats.app.myAppName.order.from_android.success 29 stats.app.myAppName.order.from_pc.failure 10 stats.app.myAppName.order.from_openapi.failure 5 // lindb: MetricName: myAppName.order Tags: "tag1"=[from_ios, from_android,from_pc, from_openapi] "tag2"=[success, failure]
之前我們的實時計算模塊Shaka就在這里派上了大用場:只要再新增一路數據處理流程即可。如下圖,新增一條Statsd數據的處理Pipeline,並輸出結果到LinDB。在用戶的代碼全部從Statsd API遷移到ETrace API后,這一路處理邏輯即可移除。
將InfluxDB指標轉成LinDB指標
InfluxDB主要用於機器、網絡設備等基礎設施的可觀測性數據。餓了么每台機器上,都部署了一個ESM-Agent。它負責采集機器的物理指標(CPU、網絡協議、磁盤、進程等),並在特定設備上進行網絡嗅探(Smoke Ping)等。這個數據采集Agent原由Python開發,在不斷需求堆疊之后,已龐大到難以維護;並且每次更新可觀測邏輯,都需要全量發布每台機器上的Agent,導致每次Agent的發布都令人心驚膽戰。
我們從0開始,以Golang重新開發了一套ESM-Agent,做了以下改進:
- 可觀測性邏輯以插件的形式,推送到各宿主機上。不同的設備、不同應用的機器,其上運行的插件可以定制化部署。
- 制定插件的交互接口,讓中間件團隊可定制自己的數據采集實現,解放了生產力。
- 移除了etcd,使用MySql做配置數據存儲,減輕了系統的復雜度。
- 開發了便利的發布界面,可灰度、全量的推送與發布Agent,運維工作變得輕松。
- 最重要的,收集到的數據以LinDB多Fields的格式發送到Collector組件,由其發送到后續的處理與存儲流程上。
從ETrace到EMonitor,不斷升級的可觀測性體驗
2017年底,我們團隊終於迎來了一名正式的前端開發工程師,可觀測性團隊正式從后端開發寫前端的狀態中脫離出來。在之前的Angular的開發體驗中,我們深感“狀態轉換”的控制流程甚為繁瑣,並且開發的組件難以復用(雖然其后版本的Angular有了很大的改善)。在調用當時流行的前端框架后,我們在Vue與React之中選擇了后者,輔以Ant Design框架,開發出了媲美Grafana的指標看版與便利的鏈路看板,並且在PC版本之外還開發了移動端的定制版本。我們亦更名了整個可觀測性產品,從“ETrace”更新為“EMonitor”:不僅僅是鏈路可觀測性系統,更是餓了么的一站式可觀測性平台。
可觀測性數據的整合:業務指標 + 應用鏈路 + 基礎設施指標 + 中間件指標
在指標系統都遷移到LinDB后,我們在EMonitor上集成了“業務指標 + 應用鏈路 + 基礎設施指標 + 中間件指標”的多層次的可觀測性數據,讓用戶能在一處觀測它的業務數據、排查業務故障、深挖底層基礎設施的數據。
可觀測性場景的整合:指標 + 鏈路 + 報警
在可觀測性場景上,“指標看板”用於日常業務盯屏與宏觀業務可觀測性,“鏈路”作為應用排障與微觀業務邏輯透出,“報警”則實現可觀測性自動化,提高應急響應效率。
靈活的看板配置與業務大盤
在指標配置上,我們提供了多種圖表類型--線圖、面積圖、散點圖、柱狀圖、餅圖、表格、文本等,以及豐富的自定義圖表配置項,能滿足用戶不同數據展示需求。
在完成單個指標配置后,用戶需要將若干個指標組合成所需的指標看板。用戶在配置頁面中,先選擇待用的指標,再通過拖拽的方式,配置指標的布局便可實時預覽布局效果。一個指標可被多個看板引用,指標的更新也會自動同步到所有看板上。為避免指標配置錯誤而引起歧義,我們也開發了“配置歷史”的功能,指標、看板等配置都能回滾到任意歷史版本上。
看板配置是靜態圖表組合,而業務大盤提供了生動的業務邏輯視圖。用戶可以根據他的業務場景,將指標配置整合成一張宏觀的業務圖。
第三方系統整合:變更系統 + SLS日志
因每條報警信息和指標配置信息都與AppId關聯,那么在指標看板上可同步標記出報警的觸發時間。同理,我們拉取了餓了么變更系統的應用變更數據,將其標注到對應AppId相關的指標上。在故障發生時,用戶查看指標數據時,能根據有無變更記錄、報警記錄來初步判斷故障原因。
餓了么的日志中間件能自動在記錄日志時加上對應的ETrace的RequestId等鏈路信息。如此,用戶查看SLS日志服務時,能反查到整條鏈路的RequestId;而EMonitor也在鏈路查看頁面,拼接好了該應用所屬的SLS鏈接信息,用戶點擊后能直達對應的SLS查看日志上下文。
使用場景的整合:桌面版 + 移動版
除提供桌面版的EMonitor外,我們還開發了移動版的EMonitor,它也提供了大部分可觀測性系統的核心功能--業務指標、應用指標、報警信息等。移動版EMonitor能內嵌於釘釘之中,打通了用戶認證機制,幫助用戶隨時隨地掌握所有的可觀測性信息。
為了極致的體驗,精益求精
為了用戶的極致使用體驗,我們在EMonitor上各功能使用上細細打磨,這里僅舉幾個小例子:
- 我們為極客開發者實現了若干鍵盤快捷鍵。例如,“V”鍵就能展開查看指標大圖。
- 圖上多條曲線時,點擊圖例是默認單選,目的是讓用戶只看他關心的曲線。此外,若是“Ctrl+鼠標點擊”則是將其加選擇的曲線中。這個功能在一張圖幾十條曲線時,對比幾個關鍵曲線時尤為有用。
- 為了讓色弱開發者更容易區分成功或失敗的狀態,我們針對性的調整了對應顏色的對比度。
成為餓了么一站式可觀測性平台
EMonitor開發完成后,憑借優異的用戶體驗與產品集成度,很快在用戶中普及開來。但是,EMonitor要成為餓了么的一站式可觀測性平台,還剩下最后一戰--NOC可觀測性大屏。
NOC可觀測性大屏替換
餓了么有一套完善的應急處理與保障團隊,包括7*24值班的NOC(Network Operation Center)團隊。在NOC的辦公區域,有一整面牆上都是可觀測性大屏,上面顯示着餓了么的實時的各種業務曲線。下圖為網上找的一張示例圖,實際餓了么的NOC大屏比它更大、數據更多。
當時這個可觀測大屏是將Grafana的指標看版投影上去。我們希望將NOC大屏也替換成EMonitor的看版。如前文所說,我們逐步將用戶的Statsd指標數據轉換成了LinDB指標,在NOC團隊的協助下,一個一個將Grafana的可觀測性指標“搬”到EMonitor上。此外,在原來白色主題的EMonitor之上,我們開發了黑色主題以適配投屏上牆的效果(白色背景投屏太刺眼)。
終於趕在2018年的雙十一之前,EMonitor正式入駐NOC可觀測大屏。在雙十一當天,眾多研發擠在NOC室看着牆上的EMonitor看版上的業務曲線不斷飛漲,作為可觀測性團隊的一員,這份自豪之情由衷而生。經此一役,EMonitor真正成為了餓了么的“一站式可觀測性平台”,Grafana、Statsd、InfluxDB等都成了過去時。
報警Watchdog 2.0
同樣在EMonitor之前,亦有Statsd與InfluxDB對應的多套報警系統。用戶若想要配置業務報警、鏈路報警、機器報警,需要輾轉多個報警系統之間。各系統的報警的配置規則、觸達體驗亦是千差萬別。Watchdog報警系統也面臨着統一融合的挑戰。
- 在調研其他系統的報警規則實現后,Watchdog中仍以LinDB的指標作為元數據實現。
- 針對其他報警系統的有顯著區別的訂閱模式,我們提出了"報警規則+一個規則多個訂閱標簽+一個用戶訂閱多個標簽"的方式,完美遷移了幾乎其他系統所有的報警規則與訂閱關系。
- 其他各系統在報警觸達與觸達內容上也略有不同。我們統一整合成“郵件+短信+釘釘+語音外呼”四種通知方式,並且提供可參數化的自定義Markdown模板,讓用戶可自己定時報警信息。
經過一番艱苦的報警配置與邏輯整合后,我們為用戶“自動”遷移了上千個報警規則,並最終為他們提供了一個統一的報警平台。
報警,更精准的報警
外賣行業的業務特性是業務的午高峰與晚高峰,在業務曲線上便是兩個波峰的形狀。這樣的可觀測數據,自然難以簡單使用閾值或比率來做判斷。即使是根據歷史同環比、3-Sigma、移動平均等規則,也難以適應餓了么的可觀測性場景。因為,餓了么的業務曲線並非一成不變,它受促銷、天氣因素、區域、壓測等因素影響。開發出一個自適應業務曲線變化的報警算法,勢在必行。
我們經過調研既有規則,與餓了么的業務場景,推出了全新的“趨勢”報警。簡要算法如下:
- 計算歷史10天的指標數據中值作為基線。其中這10天都取工作日或非工作日。不取10天的均值而取中值是為了減少壓測或機房流量切換造成的影響。
- 根據二階滑動平均算法,得到滑動平均值與當前實際值的差值。
- 將基線與差值相加作為預測值。
- 根據預測值的數量級,計算出波動的幅度(如上界與下界的數值)。
- 若當前值不在預測值與波動幅度確定的上下界之中,則觸發報警。
如上圖所示,22點01分的實際值因不在上下界所限定的區域之中,會觸發報警。但從后續趨勢來看,該下降趨勢符合預期,因此實際中還會輔以“偏離持續X分鍾”來修正誤報。(如該例中,可增加“持續3分鍾才報警”的規則,該點的數據便不會報警)算法中部分參數為經驗值,而其中波動的閾值參數用戶可按照自己業務調整。用戶針對具備業務特征的曲線,再也不用費心的去調整參數,配置上默認的“趨勢”規則就可以覆蓋大多數的可觀測性場景,目前“趨勢”報警在餓了么廣泛運用。
智能可觀測性:根因分析,大顯神威
作為AIOPS中重要的一環,根因分析能幫助用戶快速定位故障,縮短故障響應時間,減少故障造成的損失。2020年初,我們結合餓了么場景,攻堅克難,攻破“指標下鑽”、“根因分析”兩大難關,在EMonitor上成功落地。
根因分析最大的難點在於:包含復雜維度的指標數據難以找到真正影響數據波動的具體維度;孤立的指標數據也難以分析出應用上下游依賴引起的故障根因。例如,某個應用的異常指標突增,當前我們只能知道突增的異常名、機房維度的異常分布、機器維度的異常分布等,只有用戶手工去點擊異常指標看來鏈路之后,才能大致判斷是哪個SOA方法/DB請求中的異常。繼而用戶根據異常鏈路的環節,去追溯上游或下游的應用,重復類似的排查過程,最后以人工經驗判斷出故障點。
因此,在“指標下鑽”上,我們針對目標指標的曲線,細分成最精細的每個維度數據(指標group by待分析的tag維度),使用KMeans聚類找出故障數據的各維度的最大公共特征,依次計算找到最優的公共特征,如此便能找到曲線波動對應的維度信息。
其次,在鏈路數據計算時,我們就能將額外的上下游附加信息附加到對應的指標之中。如,可在異常指標中追加一個維度來記錄產生異常的SOA方法名。這樣在根據異常指標分析時,能直接定位到是這個應用的那個SOA方法拋出的異常,接下來“自動”分析是SOA下游故障還是自身故障(DB、Cache、GC等)。
在2020.3月在餓了么落地以來,在分析的上百例故障中,根因分析的准確率達到90%以上,顯著縮短的故障排查的時間,幫助各業務向穩定性建設目標向前跨進了一大步。
4.0:繼往開來,乘勢而上
經過4、5年的發展,風雲變幻但團隊初心不改,為了讓用戶用好可觀測性系統,EMonitor沒有停下腳步,自我革新,希望讓“天下沒有難用的可觀測性系統”。我們向集團的可觀測性團隊請教學習,結合本地生活自己的技術體系建設,力爭百尺竿頭更進一步,規划了以下的EMonitor 4.0的設計目標。
一、進行多租戶化改造,保障核心數據的時延和可靠性
在本地生活的技術體系與阿里巴巴集團技術體系的不斷深入的融合之中,單元化的部署環境以及對可觀測性數據不同程度的可靠性要求,催生了“多租戶化”的設計理念。我們可以根據應用類型、數據類型、來源等,將可觀測性數據分流到不同的租戶中,再針對性配置數據處理流程及分配處理能力,實現差異化的可靠性保障能力。
初步我們可以划分為兩個集群--核心應用集群與非核心應用集合,根據在應用上標記的“應用等級”將其數據自動發送到對應集群中。兩套集群在資源配置上優先側重核心集群,並且完全物理隔離。此外通過配置開關可動態控制某個應用歸屬的租戶,實現業務的柔性降級,避免當下偶爾因個別應用的不正確埋點方式會影響整體可觀測可用性的問題。
未來可根據業務發展進一步發展出業務相關的租戶,如到家業務集群、到店業務集群等。或者按照區域的划分,如彈內集群、彈外集群等。
二、打通集團彈內、彈外的可觀測性數據,成為本地生活的一站式可觀測性平台
目前本地生活很多業務領域已經遷入集團,在Trace鏈路可觀測方面,雖然在本地生活上雲的項目中,EMonitor已經通過中間件改造實現鷹眼TraceId在鏈路上的傳遞,並記錄了EMonitor RequestId與鷹眼TraceId的映射關系。但EMonitor與鷹眼在協議上的天然隔閡仍使得用戶需要在兩個平台間跳轉查看同一條Trace鏈路。因此,我們接下來的目標是與鷹眼團隊合作,將鷹眼的Trace數據集成到EMonitor上,讓用戶能一站式的排查問題。
其次,本地生活上雲后,眾多中間件已遷移到雲上中間件,如雲Redis、雲Kafka、雲Zookeeper等。對應的可觀測性數據也需要額外登陸到阿里雲控制台去查看。雲上中間的可觀測性數據大多已存儲到Prometheus之中,因此我們計划在完成Prometheus協議兼容后,就與雲上中間件團隊合作,將本地生活的雲上可觀測性數據集成到EMonitor上。
三、擁抱雲原生,兼容Prometheus、OpenTelemetry等開源協議。
雲原生帶來的技術革新勢不可擋,本地生活的絕大多數應用已遷移到集團的容器化平台--ASI上,對應帶來的新的可觀測環節也亟需補全。如,ASI上Prometheus協議的容器可觀測性數據、Envoy等本地生活PaaS平台透出的可觀測性數據與Trace數據等。
因此,我們計划在原先僅支持LinDB數據源的基礎上,增加對Prometheus數據源的支持;擴展OpenTelemetry的otel-collector exporter實現,將Open Telemetry協議的Trace數據轉換成EMonitor的Trace格式。如此便可補全雲原生技術升級引起的可觀測性數據缺失,並提供高度的適配性,滿足本地生活的可觀測性建設。
結語
縱觀各大互聯網公司的產品演進,技術產品的走向與命運都離不開公司業務的發展軌跡。我們餓了么的技術人是幸運的,能趕上這一波技術變革的大潮,能夠發揮聰明才智,打磨出一些為用戶津津樂道的技術產品。我們EMonitor可觀測性團隊也為能參與到這次技術變更中深感自豪,EMonitor能被大家認可, 離不開每位參與到餓了么可觀測性體系建設的同伴,也感謝各位對可觀測性系統提供幫助、支持、建議的伙伴!
作者簡介:
柯聖,花名“炸天”,餓了么監控技術組負責人。自2016年加入餓了么,長期深耕於可觀測性領域,全程參與了ETrace到EMonitor的餓了么可觀測性系統的發展歷程。
本文為阿里雲原創內容,未經允許不得轉載。