背景与目标
在复杂的算法生态中,系统往往涵盖交易搜索、社区推荐、图像识别及广告策略等多个维度。请求通常从 Java 网关下发,进入 C++ 构建的高性能算法核心(如 DSearch 检索、DGraph 图计算、DFeature 特征提取等)。
随着系统复杂度指数级增长,现有的可观测性已难以支撑全链路的稳定性保障。我们需要建设一个业务场景维度的全链路变更事件中心,以'聚焦做好可观测性'为核心,整合各平台资源与数据,提升系统的整体透明度和故障止血效率。
可观测性的四大支柱
业界常提 Trace、Metric 和 Log 三位一体,我们的目标是打造一套'以场景为魂,以联动为骨'的可观测体系,打破数据孤岛,实现算法治理的智能化转型。具体包括以下四个支柱:
- Trace 为径:超越单纯的拓扑记录。通过 Baggage 机制,将复杂的业务语义与算法策略注入链路,实现调用流与业务流的深度耦合。
- Metric 为脉:基于 Trace 自动生成场景化的性能指标,结合元数据关联服务端业务指标,实现指标间的联动。
- Log 为证:推动全链路日志格式化治理,规范异常码和业务码,还原现场证据。
- Event 为源:打通算法侧 10+ 个变更平台,将日均上万次的变更事件实时映射至链路拓扑。
核心攻坚:可观测性标准化
Trace 标准化
在算法生态中,DMerge、DScatter、DGraph、DSearch、DFeature 等核心组件承载着极致的性能诉求。由于 C++ 侧 Trace SDK 的长期缺失,算法服务曾处于微服务观测体系的'孤岛',难以与上下游实现全链路串联。
我们最终没有直接基于 OpenTelemetry C++ SDK 进行二次开发,而是自研了 Trace2.0。主要考量如下:
- 极致性能与可控开销:C++ 服务位于请求链路关键路径,对 RT 与尾延迟极其敏感。OpenTelemetry C++ SDK 偏向通用性与标准完备性,在高 QPS 场景下存在不可忽略的性能不确定性。我们需要对 Span 创建、上下文传播、属性写入等操作进行严格的 CPU 与内存开销控制。
- 原生 SDK 行为不透明:OpenTelemetry C++ SDK 内部实现复杂,可能包含隐式线程或后台任务,在极端并发下的问题定位成本较高。
- 运行模型兼容性:C++ 服务大量基于 brpc 与 bthread 用户态调度模型,若 SDK 依赖 pthread 或引入额外系统线程,可能影响 bthread worker 的调度行为。
- 工程依赖风险:现有工程依赖特定版本的 protobuf,而 OpenTelemetry C++ SDK 对其依赖栈有独立版本要求,静态链接场景下存在符号泄漏与 ABI 冲突风险。
SDK 框架设计

- APM Cpp SDK:实现 Span 的创建、采集和上报,同时与控制平面对接实现心跳和配置热更新,基于 Kafka 上报 Trace。
- brpc-tracer:brpc 框架适配层,支持 http 与 baidu-std 协议的自动上报探针。
- 引擎接入:业务侧通过依赖 brpc-tracer,支持链路上报。
报文压缩方案
为了降低带宽消耗,避免链路数据与业务请求竞争,我们对报文进行了深度优化:

- 长度过滤:对属性、事件、异常的 key/value 长度进行限制,Span 整体超出阈值部分截断,阈值支持控制平面动态更新。
- 字段压缩:协议字段尽可能压缩,例如 16 进制字符串打包为二进制,通用字段省略 key,通过差值替代结束时间等。
- 批量聚合:多条 Span 合并为一条报文上报,增加吞吐量,减少 Kafka 集群压力,聚合阈值同样支持动态更新。
- Snappy 压缩:先序列化再压缩,经验压缩比约为 30%。
异步上报与无锁队列

- 异步上报:Span 采集后写入队列,由异步线程批量处理投递至 Kafka;队列满或上报失败时直接丢弃,避免阻塞业务线程。
- MPSC 无锁循环队列:利用循环数组实现高效数据传递,原子操作避免加锁,减少线程竞争带来的上下文切换开销。
RPC 探针

RPC 探针实现了在协议层对请求生命周期的统一感知与 Trace 自动化处理。针对不同通信场景,在协议层引入统一的 RPC 探针,通过埋点回调拦截请求生命周期,实现 Trace 的自动采样与上报,无需业务代码侵入。
上线后支持 trace_id 链路查询,以及按异常、RT 范围等指标维度的链路查询。
Log 标准化
在全链路可观测体系中,日志是还原业务现场的最终证据。针对算法侧 Java 侧规范、C++ 侧杂乱的现状,我们实施了深度对齐与语义重构。
- 跨语言语义对齐:以 Java 侧成熟的标准化日志为标杆,推行结构化日志协议。
- 业务语义锚定:引入'场景 (Scene) + 异常码 (Error Code)'。
- 场景化建模:将具体的业务上下文(如推荐、搜索)注入日志元数据。
- 异常码标准化:建立统一的错误字典,实现从模糊描述到精确指纹的跨越。
日志格式规范
-
统一文件名
/logs/应用名/{应用名}-error.log文件目录在
/logs/应用名/,统一文件名为{应用名}-error.log,采集时按 pattern*-error.log匹配。 -
日志格式 按照竖线
|分隔符分隔:时间戳 | 进程 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 算法:一种基于层次聚类的在线日志解析算法,从原始日志中提取模板,将非结构化日志转换为半结构化数据。
流程包括预处理、构建 parse tree(根据长度和关键字分类)、叶子节点聚类判断相似度。如果相似则匹配,否则创建新聚类。

上线效果支持按应用名、集群名、异常名、code 码、异常日志模版等维度聚类。

以'场景'为魂:构建算法知识图谱
场景化建模 (AlgoScene)
在 APP 中,用户每一次搜索或进入社区频道,底层都会触发一次复杂的 RPC 调用流。流量在算法域内穿梭时,会经历多次不同'场景'算子的串行与并行计算。正是由于这种调用路径极其复杂且具备高度的业务特性,我们决定打破传统的物理链路视角,转而以'场景'为核心单元构建知识图谱。

如图所示:
- 一个场景由多个算子组合。
- 每个算子由 0…多个组件构成。
- 组件一般通过 RPC(HTTP/GRPC/Dubbo/Redis/BRPC/场景)方式调用。
AlgoScene 场景名
考虑到算法任务编排天然以场景为基本单元,我们通过在 Trace SDK 中封装 putAlgoSceneToBaggage 方法,利用 Baggage 机制将场景信息透传至全链路。在每个服务的场景入口处,只需通过以下代码即可实现场景上下文的注入:
Context ctx = AlgoBaggageOperator.putAlgoSceneToBaggage("trans_product");
try (Scope scope = ctx.activate()) {
// 业务逻辑执行
}
在数据清洗阶段,通过对 algo_scene 字段进行逗号切分,解析出完整的场景路径链:
- algoScene:记录全链路经过的所有场景名(逗号分隔)。
- rootScene:切分后的第一个场景名,代表流量进入算法域的原始触发源。
- currentScene:切分后的最后一个场景名,代表当前节点所属的具体算子场景。

最终 Trace 效果可见,每个 Span 都携带了场景信息。


