博客 / 詳情

返回

深度實踐:得物算法域全景可觀測性從 0 到 1 的演進之路

一、前言

在得物(Poizon)業務場景中,算法生態已演進為涵蓋交易搜索、社區推薦、圖像識別及廣告策略的多維複雜系統。請求從Java網關下發,進入 C++ 構建的高性能算法核心(DSearch檢索、DGraph圖計算、DFeature特徵提取等)。

隨着系統複雜度的指數級增長,我們對現有系統的可觀測性進行了全面梳理,為了提高穩定性,我們希望建設一個業務場景維度全鏈路變更事件中心, 以“聚焦做好可觀測性”為核心目標,通過建設監控平台的事件中心與全鏈路可觀測的核心產品,整合各平台資源與數據,提升系統的整體透明度和穩定性,從而提升業務穩定性和故障止血效率,為產品迭代奠定堅實的基礎。

二、可觀測性的“四大支柱”與聯動願景

在業界,可觀測性通常被定義為Trace、Metric和Log三位一體。我們的目標是打造一套 “以場景為魂,以聯動為骨” 的可觀測體系,打破數據孤島,實現算法治理的智能化轉型。提出了 “四大支柱聯動”:

  • Trace為徑: 超越單純的拓撲記錄。通過Baggage機制,將複雜的業務語義與算法策略注入鏈路,實現調用流與業務流的深度耦合。
  • Metric為脈: 通過Trace自動生成場景化的性能指標。並結合元數據關聯服務端業務指標,實現指標間的聯動。
  • Log為證: 推動全鏈路日誌格式化治理。規範異常碼和業務碼。
  • Event為源: 算法系統的靈魂在於演進。打通算法側10+個變更平台, 將日均上萬+的變更事件實時映射至鏈路拓撲。

三、核心攻堅:可觀測性標準化

Trace標準化

在得物算法生態中,DMerge、DScatter、DGraph、DSearch、DFeature等核心組件承載着極致的性能訴求。由於C++側Trace SDK的長期缺失,算法服務曾處於微服務觀測體系的“孤島”,難以與上下游實現全鏈路串聯。

C++ Trace2.0(得物分佈式鏈路追蹤Trace2.0基於OpenTelemetry二次開發,目前已經支持Java/Go/JS/Python語言)並沒有基於OpenTelemetry CPP進行二次開發主要考慮以下幾點:

  • 極致性能與可控開銷要求: C++側服務位於請求鏈路關鍵路徑,對RT與尾延遲極其敏感,需要對Span創建、上下文傳播、屬性寫入等操作進行嚴格的CPU與內存開銷控制,並對內存分配、鎖競爭及線程切換具備嚴格可控性。相比之下,OpenTelemetry C++ SDK更偏向通用性與標準完備性, 其抽象層次與擴展點在部分高QPS場景下存在不可忽略的性能不確定性。
  • 原生SDK行為不透明帶來的工程風險: OpenTelemetry C++ SDK 內部實現較為複雜,可能包含隱式線程、後台任務或複雜生命週期管理,在極端併發或異常場景下的問題定位與邊界控制成本較高,而對源碼完整評估的成本同樣高昂。
  • brpc+bthread運行模型的兼容性擔憂: C++ 服務大量基於brpc與bthread用户態調度模型,若SDK內部依賴pthread或引入額外系統線程,可能影響bthread worker的調度行為,存在運行時的兼容風險。
  • 工程依賴與符號衝突風險(尤其是Protobuf): 現有工程依賴特定版本的protobuf,而OpenTelemetry C++ SDK對其依賴棧有獨立版本要求,在靜態或混合鏈接場景下存在符號泄漏與ABI衝突風險,整體工程穩定性不可控。

SDK框架

  • APM Cpp SDK: 實現Span的創建、採集和上報,同時與控制平面對接實現心跳和配置熱更新,基於kafka上報Trace。
  • brpc-tracer: brpc框架適配層,支持http與baidu-std協議的自動上報探針。
  • 引擎接入: 業務側通過依賴brpc-tracer,支持鏈路上報。

報文壓縮方案


通過對報文進行壓縮,顯著降低Trace上報過程中的帶寬消耗,減少鏈路數據與業務請求在帶寬上的競爭,避免對正常請求響應時延產生干擾,保障業務服務穩定性。

壓縮策略:

長度過濾: 對寫的屬性、事件、異常進行key、value長度限制,對Span的整體進行長度限制,超出閾值部分進行截斷,閾值實現了控制平面的動態更新。

字段壓縮: 儘可能的對協議中的所有字段進行了壓縮,例如,16進制字符串打包為2進制,通用字段省略key,通過差值替代結束時間等。

批量聚合: 將多條Span進行合併,作為一條報文進行上報,增加吞吐量的同時,減少kafka集羣和帶寬壓力。聚合閾值也實現了控制平面動態更新。

靜態信息抽取: 對進程級別的靜態信息從Span對象中剝離,每個聚合報文只添加一個靜態信息副本。

Snappy壓縮: 先對聚合後的消息序列化,再進行Snappy壓縮,經驗壓縮比是30%左右。

異步上報和MPSC無鎖環隊列

  • 異步上報: Span採集後寫入隊列,由異步線程批量處理並投遞至Kafka;當隊列已滿或上報失敗時直接丟棄,避免阻塞業務線程及內存膨脹。
  • MPSC無鎖循環隊列: MPSC是支持多生產者單消費者的無鎖隊列結構,利用循環數組實現高效數據傳遞。通過原子操作避免加鎖,減少線程競爭帶來的上下文切換和性能開銷。在高併發場景下能提供更穩定的吞吐量和更低的延遲,保證隊列操作的高效性和可預測性。

RPC探針

RPC 探針實現了在協議層對請求生命週期的統一感知與Trace自動化處理,支持BRpc客户端與服務端在無業務侵入的前提下完成Trace的自動採樣與上報。

針對不同通信場景,在協議層引入統一的RPC探針,通過埋點回調對請求生命週期進行攔截,實現Trace的自動採樣與埋點。

上線效果

  • 支持trace_id鏈路查詢。

  • 支持指標維度(異常,RT範圍等)的鏈路查詢。

Log標準化

在全鏈路可觀測體系中,日誌是還原業務現場的最終證據。針對算法側Java 側規範、C++ 側雜亂的現狀,我們實施了深度對齊與語義重構。

  • 跨語言語義對齊:以Java側成熟的標準化日誌為標杆,通過自研C++ Log SDK推行結構化日誌協議。
  • 業務語義錨定:在日誌規範中首次引入了“場景 (Scene) + 異常碼 (Error Code)”。
  • 場景化建模: 將具體的業務上下文(如推薦、搜索)注入日誌元數據,使日誌具備了清晰的業務屬性。
  • 異常碼標準化: 建立算法側統一的錯誤字典,實現從“模糊描述”到“精確指紋”的跨越。

日誌格式規範

1.統一文件名

 /logs/應用名/{應用名}-error.log
  • 文件目錄在/logs/應用名/
  • 統一文件名叫{應用名}-error.log,比如引擎的叫:doe-server-error.log
  • 日誌採集時按pattern: *-error.log採集

2.日誌格式

  • 按照豎線 “|”分隔符分隔
 時間戳|進程ID:線程ID|日誌等級|[應用名,trace_id,span_id,scene,errCode,]|接口名|代碼行號|[可用區,集羣名,,]|異常名|message
  • 字段詳細介紹

日誌模板聚類算法

模板聚類流程

規則:以正則掩碼+Drain解析樹為基礎

  • 正則掩碼: 通過正則對日誌進行預處理,如時間,IP地址,數字,等等。例如“2025-12-01 10:20:30 ERROR host 10.0.1.2 connect timeout”經過正則掩碼後,得到“<:TIME:> ERROR host <:IP:> connect timeout”
  • Drain算法: Drain算法是一種用於處理日誌數據的結構化分析算法,廣泛應用於日誌解析和日誌模板抽取領域。它是一種基於層次聚類的在線日誌解析算法,其主要目標是從原始日誌中提取日誌模板,從而將非結構化日誌轉換為半結構化數據格式,這有助於後續的日誌分析、故障檢測和系統監控。

Drain算法主要分為以下幾個步驟

  • 預處理

首先需要對日誌進行預處理,包括前文的正則掩碼,以減少冗餘信息對解析的影響。另外,需要對日誌進行分詞,按空格和其他分割符劃分為多個片段。

  • drain解析樹

接下來構建了一種層次結構的樹,稱為parse tree,用於記錄和組織日誌消息。

  • 在樹的第一級節點,日誌將會依據其長度(分詞後片段數目)進行分類。不同長度的日誌會被分配到不同的路徑上。
  • 然後在樹的後續層級中,每一層級都嘗試根據其他的靜態關鍵字對日誌消息進行進一步細化分類。
  • 樹的葉子節點為日誌聚類桶,逐個遍歷桶中的聚類,分別判斷當前日誌與對應日誌聚類的相似度是否達到閾值,相似度算法為相同位置的相同token佔token總數的比例。
  • 如果相似則將判斷當前的日誌匹配該聚類,如果都不相似則創建新的聚類並加入桶中。

上線效果

日誌模板聚類維度支持:應用名、集羣名、異常名、code碼、異常日誌模版等。

四、以“場景”為魂:構建算法知識圖譜

場景化建模 (AlgoScene)

在得物APP中,用户每一次搜索或進入社區頻道,底層都會觸發一次複雜的RPC調用流。流量在算法域內穿梭時,會經歷多次不同“場景”算子的串行與並行計算,最終才將推薦結果反饋給客户端。正是由於這種調用路徑極其複雜且具備高度的業務特性,我們決定打破傳統的物理鏈路視角,轉而以 “場景”為核心單元構建知識圖譜。

如圖所示,

  • 一個場景由多個算子組合
  • 每個算子由0..多個組件構成
  • 組件一般通過RPC(HTTP/GRPC/Dubbo/Redis/BRPC/場景)方式調用下

AlgoScene場景名

在確定以“場景”為核心的串聯邏輯後,由於單次 RPC 調用往往橫跨多個算法節點,我們必須實現對多場景動態鏈的支持。

考慮到算法任務編排天然以場景為基本單元,我們通過在Trace SDK中封裝putAlgoSceneToBaggage方法,利用Baggage機制將場景信息透傳至全鏈路。在每個服務的場景入口處,只需通過以下代碼即可實現場景上下文的注入,確保全鏈路中的每個Span都能自動攜帶algo_scene字段:

Context ctx = AlgoBaggageOperator.putAlgoSceneToBaggage("trans_product");
try (Scope scope = ctx.activate()) {
    // 業務邏輯執行
}

在數據清洗階段,我們通過對algo_scene字段進行逗號切分,解析出完整的場景路徑鏈:

  • algoScene: 記錄全鏈路經過的所有場景名(逗號分隔)。
  • rootScene: 切分後的第一個場景名,代表流量進入算法域的原始觸發源。
  • currentScene: 切分後的最後一個場景名,代表當前節點所屬的具體算子場景。

最終Trace效果


傳播鏈“Baggage” VS “InnerBaggage”

Baggage是OpenTelemetry觀測標準中的一個核心組件。如果説TraceID是用來串聯整個調用鏈的“身份證”,那麼Baggage就像是隨身攜帶的“行李箱”。

它允許開發人員在整個請求鏈路中攜帶自定義的鍵值對(Key-Value Pairs)。 這些數據會隨着HTTP Header或RPC元數據在各個微服務之間自動“漂流”,確保下游服務能夠感知上游傳遞的業務上下文。

核心原理

Baggage是基於HTTP Header協議實現的。根據W3C標準,它會將數據存放在名為baggage的Header中進行透傳:

  • 格式: baggage: algoScene=recommend_v1,isTest=true
  • 傳播方式: 自動隨請求從Service A流轉至Service B、C,無需在每個服務的業務代碼中手動添加參數。

底層實現

如何將baggage信息應用到每個span呢?我們增強了spanProcessor代碼如下:

Baggage baggage = Baggage.fromContext(parentContext);
baggage.forEach((s, baggageEntry) -> {
    if (s.startsWith(OTEL_TO_SPAN_BAGGAGE_PREFIX)) {
        String value = baggageEntry.getValue();
        if (value == null) {
            value = NULL_VALUE;
        } else if (value.isEmpty()) {
            value = EMPTY_VALUE;
        }
        span.setAttribute("baggage:" + s.substring(OTEL_TO_SPAN_BAGGAGE_PREFIX.length()), value);
    }
});

InnerBaggage

在全鏈路追蹤中,如果説Baggage解決了服務之間的跨站傳遞,確保業務信息能跨越機器送達下游;那麼InnerBaggage則負責服務內部的進程傳遞,確保在同一個進程裏,無論業務邏輯經過多少個組件,當前的“算子名”等信息都能自動同步到每一個執行步驟中,無需在代碼裏層層手動傳遞參數。

示例

// 在算子入口處,定義一個 InnerBaggage 作用域
try (Scope ignored = InnerBaggage.with("search_processor_biz_component", "content_agg")) {     
    // 這裏的邏輯無論是調用數據庫還是計算,生成的 Span 都會自動帶上 search_processor_biz_component=content_agg     
    runComponentLogic();  
}  
// 作用域結束,InnerBaggage 自動清理,防止污染下一個算子

最終效果

一個遠程Dubbo-client被成功標記algo_scene和業務算子名“content_agg”。

動態元數據與流式計算

配置中心元數據

在複雜的算法場景中,由於變更頻率極高,硬編碼顯然無法滿足需求,我們構建了一套基於配置中心的動態元數據訂閲體系。

  • 建立“應用-配置集”訂閲關係

  • 元數據模型定義

為了支撐應用與配置之間的多對多關係,我們設計瞭如下核心表結構,用於記錄訂閲邏輯與元數據畫像:

場景拓撲圖 (Neo4j)

在完成業務側的全鏈路埋點後,後端數據清洗層負責將海量的原始Trace數據進行結構化處理:它實時解析並提取Baggage中的全局場景信息與InnerBaggage中的局部算子標籤,從而將離散的鏈路信息轉化為標準化的業務計算流。

流式計算引擎

藉助流式計算引擎強大的EPL能力,我們通過類SQL的聲明式語法,精煉地實現了從實時多維聚合到複雜模式匹配的邏輯表達,目前已沉澱出12個覆蓋核心業務場景的標準SQL算子,顯著提升了實時數據處理的開發效率與靈活性。SQL示例如下:

@TimeWindow(10)
@Metric(name = 'algo_redis_client', tags = {'algoScene','rootScene','currentScene','props','env','clusterName','serviceName','redisUrl','statusCode'}, fields = {'timerCount', 'timerSum', 'timerMax'}, sampling='sampling')
SELECT algoScene                                as algoScene,
       rootScene                                as rootScene,
       currentScene                             as currentScene,
       get_value(origin.props)                  as props,
       env                                      as env,
       serviceName                              as serviceName,
       clusterName                              as clusterName,
       statusCode                               as statusCode,
       redisUrl                                 as redisUrl,
       trunc_sec(startTime, 10)                 as timestamp,
       max(duration)                            as timerMax,
       sum(duration)                            as timerSum,
       count(1)                                 as timerCount,
       sampling(new Object[]{duration,traceId})                   as sampling
FROM algoRedisSpan as origin
GROUP BY algoScene, rootScene, currentScene, props,env,serviceName, clusterName, redisUrl,statusCode,trunc_sec(startTime, 10)
  • @TimeWindow(10): 定義了一個10秒的滾動窗口,引擎會把這10秒內產生的所Redis訪問記錄(Span)攢在一起進行一次計算
  • @Metric(...): 這定義了輸出結果的結構。將計算結果轉化為指標(Metric),其中tags是維度,fields是數值。
  • sampling(...): 採樣功能,通過採樣邏輯記錄耗時最大的traceId。

場景拓撲圖

前面構造了以“場景”為中心的算法域調用指標,後面構造怎樣的數據模型決定了用户從什麼角度去觀察和分析數據。我們摒棄了不夠直觀的傳統的表格式展示,藉助強大的圖數據存儲數據庫Neo4j,實時存儲和更新算法場景的算子調用拓撲圖。實時調用指標關係存儲時序數據庫Victoriametrics,實時調用關係存儲Neo4j。

圖模型

  • 節點(Node):代表實體,如:App、AppCluster、ArkGroup、ArkDataId、AlgoComponent、AlgoDGraph等
  • 關係(Relationship):連接節點,如:SceneRelation、AppRelation等
  • 屬性(Properties):存儲在節點和關係上的鍵值對,如:appName、clusterName、scene、componentName、updateTimestamp等

數據模型設計

// app節點
CREATE (a:App {
    id: 1,
    hash: -6545781662466553124,
    appName: "sextant"
})
// appCluster節點
CREATE (c:AppCluster {
    id: 23,
    hash: -8144086133777820909,
    appName: "sextant",
    clusterName: "sextant-csprd-01"
})
// index
CREATE INDEX index_app_name FOR (a:App) ON (a.appName)
// 關係
MATCH (a:App {id: 1}),(c:AppCluster {id:23})
MERGE (a)-[r:HAS_CLUSTER]->(c)
ON CREATE SET r.updateTs = timestamp()
ON MATCH SET r.updateTs = timestamp()
return r;

時序指標設計

{
    "metric": {                 
        "__name__":"algo_client_metric_timerCount",
        "from":"hashcodexxx",
        "to":"hashcodexxx",
        "statusCode": 0,
        "type": "Dgraph"
    },
    "values":[42,32,15],
    "timestamps":[1767573600,1767573620,1767573640]
}

上線效果

  • 通過apoc獲取實體間的調用關係
CALL apoc.meta.graph()

  • 通過cypher語句查詢某場景下的調用拓撲
MATCH 
    p = (entry {appName: 'app'})-[r:USES_SCENE*1..]->(to) 
WHERE all(rel IN r WHERE rel.type = 'CURRENT_SCENE' AND rel.scene CONTAINS 'scene'          and rel.updateTs >= 1767675780000 and rel.updateTs <= 1767679380000) 
RETURN nodes(p) AS allNodes, relationships(p) AS allRels LIMIT 1000
sum(sum_over_time(algo_client_metric_timerSum{scene="xxx"}[1m] offset 1m)) by (to) / sum(sum_over_time(otel_algo_client_metric_timerCount{scene="xxx"}[1m] offset 1m)) by (to) 
/ 1000
sum(sum_over_time(algo_client_metric_timerCount{scene="xxx"}[1m] offset 1m) / 60) by (to)

五、智能化演進:異常檢測與事件聯動

異常檢測:改進型IQR算法

通過構建以“場景”為核心的監控維度,我們可以精準捕捉異常總數及其演進趨勢。接下來聚焦週期性規律識別與異常檢測算法優化兩大核心領域:

週期性規律:從傅里葉變換到自適應識別

在電商微服務架構中,指標波動深度耦合人類行為的“晝夜節律”;而在算法業務場景下,頻繁的實驗任務使週期性特徵更趨複雜且多變;

  • 通用方案:傳統的傅里葉變換(FFT)雖能捕捉頻域特徵,但在時域噪聲干擾下難以推導出高精度的物理週期;
  • 落地方案:採用自適應週期識別算法, 能夠根據時序數據的動態演變,自動、精確地推測出各場景特有的週期步長;

給定一些候選週期,通過計算時間序列的滯後1週期的自相關性,驗證時間序列是否匹配候選週期。對不同的候選週期,取不同長度的歷史數據,候選週期越大,需要歷史數據越久遠,相關性要求較低。

週期識別算法示意圖

異常檢測算法:從 3-Sigma 到改進型 IQR

面對流量激增產生的“隨機突刺”以及低流量場景下的“零水位”常態,檢測算法需要具備極高的魯棒性。

  • 通用方案:標準3-Sigma算法預設數據符合正態分佈,而錯誤數指標往往呈現正偏態、高峯度特徵,直接應用會導致虛假告警頻繁,產生大量“告警噪音”;
  • 落地方案:基於四分位距(IQR)算法進行深度改進。通過動態調整比例係數與閾值邊界,完美適配非正態分佈的錯誤數指標,在確保靈敏度的同時顯著降低了誤報率;


綜合考慮,使用IQR異常檢測:

  • IQR是指:上四分位數與下四分位數(25%分位數)之差,即箱型圖中箱體的高度。
  • IQR異常檢測是指:超過上四分位數1.5倍的IQR,或低於下四分位數1.5倍的IQR,則為異常。

結合錯誤數指標特徵,對IQR異常檢測進行了一些改進:

  • 零基線自適應處理:當時間序列大量為0時,自動排除0值計算基線,避免誤報。
  • 雙閾值約束:錯誤數超過多少必為異常,超過基線多少必為異常。
  • 圖中高亮部分(75%, 25%, +1.5, -1.5 )均設置為可調參數,針對不同算法場景做微調。

落地效果

一般異常檢測

零基指標的異常檢測:噪音顯著降低

週期性指標的異常檢測:能發現局部異常點

事件標準化:因果關聯的最後一公里

在得物算法生態中,日均變更次數達萬級,涵蓋了模型迭代、配置分發、代碼部署等多個維度。事件標準化的核心目標是:讓每一次變更都有跡可循,並能自動與鏈路抖動建立因果關聯。

統一事件協議

我們對來自配置中心、發佈平台、算法實驗平台等10+個源頭的事件進行了協議標準化。每一個進入可觀測底座的事件都必須具備以下條件:

  • Source (變更源): 變更的平台(配置中心 / 發佈平台 / AB實驗平台 / 特徵平台 / 機器學習平台等 )
  • ChangeObject (主體): 變更對象(如:某個應用名、某個配置文件)
  • ChangeStatus (狀態): PENDING / APPROVED / CANCELED / FINISHED 等
  • StartTime(時間): 變更開始時間 
  • ChangeName (標題): 變更主體
  • Severity (等級): 評估變更風險等級(P0-P3)
  • beforeChangeContent (上一次版本): 記錄變更前的內容
  • changeContent (版本): 記錄變更後的內容
  • extraInfo (附加信息): 可選字段如下:

    • <scene: 場景名>,<isGlobal: 全局變更>,<isReboot: 自動變更> ...

事件流

  • 各平台通過OpenAPI方式上報到事件中心,數據存儲在ES中
  • 算法域累計10+個平台100+種變更入口類型,每天10+萬的變更事件

場景事件關聯

算法側一些核心的平台的事件只能串聯上業務域,這一期我們用在線Trace埋點的方式,串聯通了核心平台從一/多個場景,比如:社區搜索主搜索,通過在線Trace清洗後就可以關聯上,搜推AB實驗管理平台、索引平台、無矩機器學習平台等等。

上線效果

六、總結—算法域全景可觀測性的 0 到 1

算法域全景可觀測性的構建,從零開始摸索,我們經歷了多次技術方案的迭代與修正。這讓我們意識到,監控建設不能不結合業務場景,否則產生的數據很難在實際排查中發揮價值。

一期建設中,我們聚焦於實用性,通過整合鏈路(Trace)、指標(Metric)、日誌(Log)以及變更事件,打通了從基礎架構到業務應用的縱向關聯。這套體系為二線運維提供了清晰的下鑽能力,使得故障邊界的鎖定更加快速準確。

進入二期階段,我們將重點解決存量離線變更的接入以及ErrLog/業務碼的標準化問題。同時,我們將觀測維度延伸至業務效果指標,通過構建集SLA監控、事件中心與異常大盤於一體的“算法業務場景NOC-SLA保障體系”,實現從“系統運行可見”到“業務運行穩定”的閉環。

往期回顧

1.前端平台大倉應用穩定性治理之路|得物技術

2.RocketMQ高性能揭秘:承載萬億級流量的架構奧秘|得物技術 

3.PAG在得物社區S級活動的落地

4.Ant Design 6.0 嚐鮮:上手現代化組件開發|得物技術

5.Java 設計模式:原理、框架應用與實戰全解析|得物技術

文 /南風

關注得物技術,每週更新技術乾貨

要是覺得文章對你有幫助的話,歡迎評論轉發點贊~

未經得物技術許可嚴禁轉載,否則依法追究法律責任。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.