分布式追踪与指标
可用性:两个变体(Full / Tiny)均支持。
OnePath 内置分布式链路追踪(tracing)与指标(metrics)。二者共享同一份消息附件 (attachment,OnePath TLV 编码),不影响用户 payload,完全通过环境变量开关,用户代码 无需任何修改即可启用。
一、设计要点
- 非侵入:仅借助消息附件承载追踪信息,不影响 payload 与正常收发。
- 附件与 payload 分离:追踪信息与用户附件、payload 互不干扰;你照常使用 attachment 携带业务数据。
- 全部在 OnePath 层:追踪与指标完全由 OnePath 实现,对上层应用透明。
- TLV 编码 + 向后兼容:attachment 以 magic
0xD1 0x07+ version0x01开头;接收端 遇到非 OnePath TLV 的附件时,自动将其整体视为纯用户附件——因此与未启用 OnePath 追踪的 对端零侵入互通。 - 环境变量开关:用户不改代码即可启用 / 关闭所有功能。
二、环境变量速查
完整说明见 环境变量。
| 变量 | 默认 | 作用 |
|---|---|---|
ONEPATH_TRACE_ENABLE | 0 | tracing 总开关;设为 1 启用 |
ONEPATH_TRACE_SAMPLE_RATIO | 0.0 | 头部采样率 [0.0, 1.0];1.0 = 全采 |
ONEPATH_TRACE_NDJSON_PATH | (空) | NDJSON 导出路径;空 = 禁用文件导出 |
ONEPATH_TRACE_SERVICE_NAME | "onepath" | resource 属性 service.name |
ONEPATH_TRACE_AUTO_INJECT | 1 | 自动把追踪信息注入到 attachment;0 = 仅透传用户附件 |
ONEPATH_TRACE_FLUSH_MS | 500 | NDJSON 异步刷盘间隔(毫秒) |
ONEPATH_METRICS_ENABLE | 0 | metrics 总开关;设为 1 启用 |
ONEPATH_METRICS_NDJSON_PATH | (空) | metrics NDJSON 路径 |
ONEPATH_METRICS_FLUSH_MS | 1000 | metrics 异步刷盘间隔(毫秒) |
所有变量在 onepath_trace_init() / onepath_metrics_init() 首次调用时读取一次并缓存。 若用户代码不显式调用 init,首次 pub/sub/get 会触发 lazy 初始化(读取环境变量),保证 「不改代码即可启用」。之后修改环境变量无效(已缓存);要重新读取须先 onepath_trace_shutdown() 再 onepath_trace_init(NULL)。
三、快速启用
零代码修改,仅设环境变量:
# 进程 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 看链路:
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 → sub | onepath_put, onepath_put_str, onepath_put_with_opts, onepath_publisher_put, onepath_publisher_put_str, onepath_publisher_write | 订阅回调(含端到端耗时) | PRODUCER → CONSUMER |
| get → reply | onepath_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() 是这种模式的语法糖:
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。它会:
- 取当前线程调用栈顶的活跃 span(中继进程的 CONSUMER)作 parent。
- 起 PRODUCER child span(trace_id 不变,span_id 变化)。
- 把 child 的 traceparent + 新 send_ts + 用户附件拼成 TLV,注入到新消息的附件。
payload 与用户附件透传,编码沿用 publisher 默认值。返回 ONEPATH_OK 成功。
6.3 异步 / 跨线程场景
自动透传仅在同步回调内有效。跨线程 / 异步队列需要用显式 Span API (onepath_span_start 会自动把新 span 压入当前线程调用栈,onepath_span_end 自动出栈):
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 类型与状态常量
#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 2Span 上下文结构
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 字节)。
子系统初始化
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 操作
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_start:parent非 NULL 时以其为父;为 NULL 时自动取线程调用栈顶;都无父 则创建新 trace。采样决策仅在创建新 trace 时做一次,子 span 继承父的 sampled flag。 新 span 自动压入当前线程的 span 栈,onepath_span_end时出栈。返回 NULL (tracing 未启用 / 采样不命中)时,后续所有 API 均 no-op。
上下文传播
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.sent | counter | 成功发送的消息数 |
onepath.msgs.recv | counter | 接收到的消息数 |
onepath.msgs.dropped | counter | 发送失败丢弃数 |
onepath.bytes.sent | counter | 成功发送的字节数 |
onepath.bytes.recv | counter | 接收到的字节数 |
onepath.publish.latency | histogram | 发布路径耗时分布(μs) |
onepath.e2e.latency | histogram | 端到端跨跳耗时分布(μs,依赖时钟同步) |
直方图分桶:[0,1,2,3,4,5,10,20,50,100,200,500,1000,2000,5000,10000,+Inf] μs。
自定义指标
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)。
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);
}快照拉取
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),否则负耗时会被丢弃。
- 调用上下文与异步:自动透传仅在同步回调内有效;异步需用显式 Span API(见 §6.3)。
- NDJSON 性能:高频 trace 下文件 I/O 是瓶颈。
- 无独立转发进程:OnePath 不提供独立的转发可执行文件,仅提供
onepath_forward助手; 用户自行拼装「订阅 +onepath_forward+ 发布」。 - 采样率:
SAMPLE_RATIO=0.0时仅维护调用栈,不导出 span;1.0时全采。 - 跨节点部署:两个变体均会自动选择合适的组播网络接口进行节点发现,跨机部署通常 无需额外配置;多网卡环境下如需指定特定网卡,可通过相应的网络接口环境变量覆盖。
- 性能预期:
*_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。