Skip to content

分布式追踪与指标

可用性:两个变体(Full / Tiny)均支持。

OnePath 内置分布式链路追踪(tracing)与指标(metrics)。二者共享同一份消息附件 (attachment,OnePath TLV 编码),不影响用户 payload,完全通过环境变量开关,用户代码 无需任何修改即可启用。

一、设计要点

  • 非侵入:仅借助消息附件承载追踪信息,不影响 payload 与正常收发。
  • 附件与 payload 分离:追踪信息与用户附件、payload 互不干扰;你照常使用 attachment 携带业务数据。
  • 全部在 OnePath 层:追踪与指标完全由 OnePath 实现,对上层应用透明。
  • TLV 编码 + 向后兼容:attachment 以 magic 0xD1 0x07 + version 0x01 开头;接收端 遇到非 OnePath TLV 的附件时,自动将其整体视为纯用户附件——因此与未启用 OnePath 追踪的 对端零侵入互通。
  • 环境变量开关:用户不改代码即可启用 / 关闭所有功能。

二、环境变量速查

完整说明见 环境变量

变量默认作用
ONEPATH_TRACE_ENABLE0tracing 总开关;设为 1 启用
ONEPATH_TRACE_SAMPLE_RATIO0.0头部采样率 [0.0, 1.0]1.0 = 全采
ONEPATH_TRACE_NDJSON_PATH(空)NDJSON 导出路径;空 = 禁用文件导出
ONEPATH_TRACE_SERVICE_NAME"onepath"resource 属性 service.name
ONEPATH_TRACE_AUTO_INJECT1自动把追踪信息注入到 attachment;0 = 仅透传用户附件
ONEPATH_TRACE_FLUSH_MS500NDJSON 异步刷盘间隔(毫秒)
ONEPATH_METRICS_ENABLE0metrics 总开关;设为 1 启用
ONEPATH_METRICS_NDJSON_PATH(空)metrics NDJSON 路径
ONEPATH_METRICS_FLUSH_MS1000metrics 异步刷盘间隔(毫秒)

所有变量在 onepath_trace_init() / onepath_metrics_init() 首次调用时读取一次并缓存。 若用户代码不显式调用 init,首次 pub/sub/get 会触发 lazy 初始化(读取环境变量),保证 「不改代码即可启用」。之后修改环境变量无效(已缓存);要重新读取须先 onepath_trace_shutdown()onepath_trace_init(NULL)

三、快速启用

零代码修改,仅设环境变量:

bash
# 进程 A: 数据采集端
ONEPATH_TRACE_ENABLE=1 \
ONEPATH_TRACE_SAMPLE_RATIO=1.0 \
ONEPATH_TRACE_NDJSON_PATH=/tmp/sensor.ndjson \
ONEPATH_METRICS_ENABLE=1 \
ONEPATH_METRICS_NDJSON_PATH=/tmp/sensor.metrics.ndjson \
./your_app sensor

# 进程 B: 转发 / 处理端
ONEPATH_TRACE_ENABLE=1 \
ONEPATH_TRACE_SAMPLE_RATIO=1.0 \
ONEPATH_TRACE_NDJSON_PATH=/tmp/router.ndjson \
./your_app router

跑完后用 jq 看链路:

bash
jq -s 'group_by(.trace_id) | map({
    trace: .[0].trace_id,
    hops: [.[] | {svc: .resource["service.name"], span: .name, kind: .kind, dur_ms: (.duration_ns/1e6)}]
})' /tmp/*.ndjson

四、自动埋点覆盖范围

下列函数无需用户调用任何 trace API,会自动起 PRODUCER / CLIENT span 并把 traceparent 注入 attachment;接收端自动解 attachment 起 child span:

通路注入端(起 span)提取端(起 child)span 类型
pub → subonepath_put, onepath_put_str, onepath_put_with_opts, onepath_publisher_put, onepath_publisher_put_str, onepath_publisher_write订阅回调(含端到端耗时)PRODUCER → CONSUMER
get → replyonepath_get, onepath_requester_get应答器回调CLIENT → SERVER
reply → 原客户端onepath_request_reply(注入 attachment)onepath_reply_recv / onepath_reply_try_recv(含端到端耗时)(SERVER 续) → CONSUMER

拉取模式暂未埋点

拉取模式 onepath_sample_recv / onepath_sample_try_recv 暂未埋点:span 生命周期无法跨 用户处理逻辑。需要追踪请用回调模式(onepath_subscribe)。

五、追踪信息在 attachment 中的布局(OnePath TLV)

追踪信息以 OnePath 自定义的 TLV 结构编码进消息附件;这是 OnePath 在附件层之上新增的格式, 不影响用户自己的附件内容。

+--------+--------+--------+--------+--------+--------+--------+--------+
| magic 0xD1 0x07 | version 0x01 | <TLV items ...>                     |
+--------+--------+--------+--------+--------+--------+--------+--------+

每个 TLV item: [tag:1][len:N][value:len]
  TAG_TRACEPARENT (0x01)  26 字节 W3C traceparent 二进制
  TAG_TRACESTATE  (0x02)  (保留)
  TAG_SEND_TS_NS  (0x11)  8 字节大端 uint64 (发送时刻, ns)
  TAG_USER        (0x10)  用户原始附件字节
  • 发送时:OnePath 把当前活跃 span 的 traceparent + 发送时间戳 + 用户原始附件拼成上述 TLV,写入消息附件。
  • 接收时:OnePath 从 TLV 取出 traceparent 作为父上下文起 child span,用 send_ts 计算 端到端耗时,并把 TAG_USER 内容作为用户附件原样返回给你。

接收端遇到非 OnePath TLV(magic 不匹配)的附件时,会把整段附件视为纯用户附件,从而与 未启用 OnePath 追踪的对端零侵入互通。

六、多跳链路透传(转发场景)

6.1 关键设计:调用上下文自动透传

OnePath 在每个线程内维护一个 span 调用栈。只要中继进程也用 OnePath,不需要任何特殊 代码,trace_id 自动跨跳透传

[sensor 进程]                    [中继进程]                       [storage 进程]
publisher_put(msg)                订阅回调(msg)                    订阅回调(msg)
   │                                  │                                │
   ├─ 起 PRODUCER span (栈顶)         ├─ 解 TLV → 父上下文 (sensor's)  ├─ 解 TLV → 父上下文 (中继's)
   ├─ TLV 注入 traceparent            ├─ span_start(CONSUMER,          ├─ span_start(CONSUMER,
   │   (trace_id=T, span_id=A)        │     parent=ctx) → 压栈         │     parent=ctx) → 压栈
   └─ put 出去                        ├─ user_cb(...)                  ├─ user_cb(...)
                                      │  └─ onepath_forward()          │     (用户业务逻辑)
                                      │        或 publisher_put        │
                                      │        └─ 起 PRODUCER child    │
                                      │           (trace_id=T 不变,    │
                                      │            span_id=B)          │
                                      │        └─ TLV 注入新 traceparent
                                      │        └─ put 出去
                                      └─ span_end(CONSUMER) 出栈

6.2 一行式转发助手

onepath_forward() 是这种模式的语法糖:

c
int onepath_forward(onepath_sample_t *in_sample, onepath_publisher_t out_pub);

static void router_cb(onepath_sample_t *sample, void *userdata) {
    onepath_publisher_t next_hop = (onepath_publisher_t)userdata;
    /* 一行完成转发: payload + 用户附件透传, PRODUCER child span 自动起 */
    onepath_forward(sample, next_hop);
    onepath_sample_release(sample);
}

onepath_forward 仅适合在订阅回调或应答器回调内调用——此时线程调用栈顶为活跃 CONSUMER/SERVER span。它会:

  1. 取当前线程调用栈顶的活跃 span(中继进程的 CONSUMER)作 parent。
  2. 起 PRODUCER child span(trace_id 不变,span_id 变化)。
  3. 把 child 的 traceparent + 新 send_ts + 用户附件拼成 TLV,注入到新消息的附件。

payload 与用户附件透传,编码沿用 publisher 默认值。返回 ONEPATH_OK 成功。

6.3 异步 / 跨线程场景

自动透传仅在同步回调内有效。跨线程 / 异步队列需要用显式 Span API (onepath_span_start 会自动把新 span 压入当前线程调用栈,onepath_span_end 自动出栈):

c
onepath_trace_ctx_t ctx;
onepath_trace_current_ctx(&ctx);   /* 在 sub 回调内: 抓取当前 trace 上下文 */
/* ... 入队、跨线程 ... */

/* 在目标线程: 以抓取到的 ctx 为父起 span (自动压栈), 随后的发送会自动续接 */
onepath_span_t sp = onepath_span_start("forward",
                                       ONEPATH_SPAN_KIND_PRODUCER,
                                       &ctx);
onepath_publisher_put(pub, data, len);
onepath_span_end(sp);              /* end 自动出栈 */

七、Span API 与上下文

Span 类型与状态常量

c
#define ONEPATH_SPAN_KIND_INTERNAL 0  /* 内部逻辑 */
#define ONEPATH_SPAN_KIND_PRODUCER 1  /* 发送消息 */
#define ONEPATH_SPAN_KIND_CONSUMER 2  /* 接收消息 */
#define ONEPATH_SPAN_KIND_CLIENT   3  /* 发出 RPC (get/request) */
#define ONEPATH_SPAN_KIND_SERVER   4  /* 处理 RPC (应答器) */

#define ONEPATH_SPAN_STATUS_UNSET 0
#define ONEPATH_SPAN_STATUS_OK    1
#define ONEPATH_SPAN_STATUS_ERROR 2

Span 上下文结构

c
typedef struct {
    uint8_t version;       /* 当前为 0x00 */
    uint8_t trace_id[16];  /* 全局 trace id, 全零表示无效 */
    uint8_t span_id[8];    /* 父 span id, 全零表示无效 */
    uint8_t flags;         /* bit0 = sampled */
} onepath_trace_ctx_t;

对应 W3C Trace Context 二进制 traceparent(26 字节)。

子系统初始化

c
typedef struct {
    const char *service_name;   /* 服务名, 记录到每个 span 的 resource */
    const char *ndjson_path;    /* NDJSON 输出路径, NULL 禁用导出 */
    double      sample_ratio;   /* 头部采样率 [0.0, 1.0] */
    size_t      ring_capacity;  /* 内部环形缓冲容量, 0 = 默认 1024 */
} onepath_trace_opts_t;

#define ONEPATH_TRACE_OPTS_DEFAULT { NULL, NULL, 0.0, 0 }

int  onepath_trace_init(const onepath_trace_opts_t *opts);
void onepath_trace_shutdown(void);
  • onepath_trace_init(opts):可多次调用,第二次起 no-op。opts 各字段为 NULL/0 时自动从 环境变量读取。返回 ONEPATH_OK 成功。
  • onepath_trace_shutdown():关闭子系统,flush exporter 并释放资源;之后允许再次 init。

Span 操作

c
onepath_span_t onepath_span_start(const char *name, int kind,
                                  const onepath_trace_ctx_t *parent);
void onepath_span_end(onepath_span_t span);
void onepath_span_set_attr_str(onepath_span_t span, const char *key, const char *value);
void onepath_span_set_attr_i64(onepath_span_t span, const char *key, int64_t value);
void onepath_span_set_attr_f64(onepath_span_t span, const char *key, double value);
void onepath_span_set_status(onepath_span_t span, int status, const char *msg);
void onepath_span_add_event(onepath_span_t span, const char *name);
  • onepath_span_startparent 非 NULL 时以其为父;为 NULL 时自动取线程调用栈顶;都无父 则创建新 trace。采样决策仅在创建新 trace 时做一次,子 span 继承父的 sampled flag。 新 span 自动压入当前线程的 span 栈onepath_span_end 时出栈。返回 NULL (tracing 未启用 / 采样不命中)时,后续所有 API 均 no-op。

上下文传播

c
int onepath_trace_current_ctx(onepath_trace_ctx_t *out);
int onepath_trace_ctx_from_string(const char *s, onepath_trace_ctx_t *out);
int onepath_trace_ctx_to_string(const onepath_trace_ctx_t *ctx, char *out);
  • onepath_trace_current_ctx(out):返回 1 表示线程调用栈顶有活跃 span(写入 *out), 0 表示无。
  • onepath_trace_ctx_from_string:解析 W3C traceparent 字符串 "00-<32hex>-<16hex>-<2hex>"(55 字符)。
  • onepath_trace_ctx_to_string:将二进制 ctx 格式化为 55 字符字符串,out 至少 56 字节。

八、内置指标

ONEPATH_METRICS_ENABLE=1 时,以下指标在首次 pub/sub 时 lazy 注册:

名称类型含义
onepath.msgs.sentcounter成功发送的消息数
onepath.msgs.recvcounter接收到的消息数
onepath.msgs.droppedcounter发送失败丢弃数
onepath.bytes.sentcounter成功发送的字节数
onepath.bytes.recvcounter接收到的字节数
onepath.publish.latencyhistogram发布路径耗时分布(μs)
onepath.e2e.latencyhistogram端到端跨跳耗时分布(μs,依赖时钟同步)

直方图分桶:[0,1,2,3,4,5,10,20,50,100,200,500,1000,2000,5000,10000,+Inf] μs。

自定义指标

c
typedef struct onepath_counter   *onepath_counter_t;
typedef struct onepath_gauge     *onepath_gauge_t;
typedef struct onepath_histogram *onepath_histogram_t;

/* 注册(按 name 幂等;metrics 未初始化时返回 NULL,后续 API 均 no-op) */
onepath_counter_t   onepath_counter_register(const char *name, const char *description);
onepath_gauge_t     onepath_gauge_register(const char *name, const char *description);
onepath_histogram_t onepath_histogram_register(const char *name, const char *description);

/* 写入 */
void onepath_counter_add(onepath_counter_t c, int64_t delta);
void onepath_counter_inc(onepath_counter_t c);             /* 等价于 add(c, 1) */
void onepath_gauge_set(onepath_gauge_t g, int64_t value);
void onepath_gauge_add(onepath_gauge_t g, int64_t delta);
void onepath_histogram_observe(onepath_histogram_t h, uint64_t value);

直方图为固定 32 个对数桶(1, 2, 4, …, 2^31,单位由用户决定,通常 μs)。

c
onepath_counter_t   msgs = onepath_counter_register("my_app.msgs.processed", "已处理消息数");
onepath_histogram_t proc = onepath_histogram_register("my_app.proc.latency", "处理耗时 (μs)");

void on_msg(onepath_sample_t *s, void *ud) {
    uint64_t t0 = now_us();
    /* ... 处理 ... */
    onepath_histogram_observe(proc, now_us() - t0);
    onepath_counter_inc(msgs);
    onepath_sample_release(s);
}

快照拉取

c
typedef struct {
    const char *name;
    const char *description;
    int         type;           /* ONEPATH_METRIC_COUNTER / _GAUGE / _HISTOGRAM */
    int64_t     value;          /* counter 累计值; gauge 当前值 */
    uint64_t    hist_count;
    uint64_t    hist_sum;
    uint64_t    hist_min;
    uint64_t    hist_max;
    const uint64_t *hist_buckets;
    size_t          hist_bucket_count;
} onepath_metric_sample_t;

typedef void (*onepath_metric_cb)(const onepath_metric_sample_t *sample, void *userdata);

void onepath_metrics_snapshot(onepath_metric_cb cb, void *userdata);

同步遍历所有已注册指标,对每个调用 cb。遍历期间持读锁,允许并发 observe。可对接外部 监控系统 / 日志 / 共享内存等。

九、限制与注意事项

时钟同步

跨节点端到端耗时(onepath.e2e.latency 及 span 内 send_ts / recv_ts)依赖系统实时时钟。 生产环境务必同集群、同步钟(NTP / PTP),否则负耗时会被丢弃。

  1. 调用上下文与异步:自动透传仅在同步回调内有效;异步需用显式 Span API(见 §6.3)。
  2. NDJSON 性能:高频 trace 下文件 I/O 是瓶颈。
  3. 无独立转发进程:OnePath 不提供独立的转发可执行文件,仅提供 onepath_forward 助手; 用户自行拼装「订阅 + onepath_forward + 发布」。
  4. 采样率SAMPLE_RATIO=0.0 时仅维护调用栈,不导出 span;1.0 时全采。
  5. 跨节点部署:两个变体均会自动选择合适的组播网络接口进行节点发现,跨机部署通常 无需额外配置;多网卡环境下如需指定特定网卡,可通过相应的网络接口环境变量覆盖。
  6. 性能预期*_ENABLE=0 时所有 span / register 退化为 no-op,零开销; TRACE_ENABLE=1 + SAMPLE_RATIO=0.0 仅维护调用栈,吞吐下降 < 5%; SAMPLE_RATIO=1.0 + NDJSON 导出含文件 I/O,吞吐下降 < 30%。

十、配套示例

配套示例程序 onepath_tracing_demo 演示三进程端到端链路:sensor → router → storage, 启动后各自输出 NDJSON,三份记录中可看到同一 trace_id 串联起三跳 span。

OnePath™ 以预构建库形式交付,运行时零外部依赖。