断了,span 还是留着
一句话总结
即使传播中断,各服务自己的 span 仍会完整保留。因此,症状不是“追踪失效”,而是“看不到请求后半段”。
为什么必须有真实请求往来
前面的模块讲过传播和 Collector,但那些实验环境中没有请求往来,所以根本不会发生传播中断。尽管这是 OTCA 最常考的部分。
中断是安静的
传播中断后,各服务的 span 仍完好存在。打开页面能看到 trace,名称和时间也正常,只是没有连成一条。
因此症状不是“追踪失效”,而是**“为什么看不到这个请求的后半段?”** 原因更难查找。
1. 라이브러리가 헤더를 안 붙인다 직접 만든 HTTP 클라이언트가 흔하다
2. 프록시가 헤더를 지운다 허용 목록에 traceparent 가 없다
3. 큐를 건너갈 때 잃는다 메시지 본문에 넣어 손으로 이어야 한다
4. 스레드·비동기 경계에서 끊긴다 컨텍스트가 따라가지 않는다
后两种尤其安静。HTTP 还能检查 header,队列与异步边界则没有明显可查看的位置。
要发现它,必须把无父 span 的比例做成指标。某个服务持续产生 root span,说明传播在它前面中断。
span 已到达,页面却看不到时
制作实验时确实遇到过:Collector 日志显示收到 span,页面却空无一物。
原因是时间戳。 填入过小数值后,数据被索引到 1970 年,默认查询范围(最近)找不到。Jaeger 查询 API 不给时间范围时也会返回空结果。
手工连接跨队列传播
HTTP 的计量库会添加 header,消息队列却没人代劳。发布时要把上下文写入消息,消费时再提取。
from opentelemetry import propagate, trace
# 발행 쪽 — 현재 컨텍스트를 헤더 맵에 주입한다
def publish(body):
carrier = {}
propagate.inject(carrier) # traceparent 가 여기 들어간다
queue.send({"body": body, "otel": carrier})
# 소비 쪽 — 꺼낸 컨텍스트를 부모로 삼는다
def consume(msg):
ctx = propagate.extract(msg.get("otel") or {})
with tracer.start_as_current_span("handle", context=ctx,
kind=trace.SpanKind.CONSUMER):
handle(msg["body"])
若没有向 start_as_current_span 传递 context=,就会生成新的 root span,trace 在此处中断。这是无父 span 增加最常见的原因。
异步边界还应正确设置 kind。标记 PRODUCER 和 CONSUMER 后,UI 会以不同方式绘制队列区间,队列等待时间也能看见。
基数决定成本
span 属性中放什么,会左右存储成本和查询速度。
| 可以放入 | 不应放入 |
|---|---|
http.route (/users/{id}) |
完整 http.target (/users/48213) |
db.system, db.operation |
完整 SQL(含参数) |
messaging.destination |
消息正文 |
| 租户 ID(数百个值) | 用户 ID(数百万个值) |
直接放完整路径,属性值会按请求数增长,索引随之爆炸。应使用按模板归一化的路由。个人信息在任何情况下都不应放入属性,因为 trace 通常保留策略宽松且可被多人查看。
先计量什么
试图一次计量全部内容,会根本无法开始。顺序如下。
- 入口——HTTP 服务器和队列 consumer。只有这些也能回答“哪个请求慢”。
- 外部调用——HTTP 客户端、DB、缓存。到这里可回答“在哪里慢”。
- 应用内部——只选择繁重计算区间。每个函数都建 span 只会增加成本并让界面难读。
第 1、2 项大多可通过自动计量获得,手工工作从第 3 项开始。
实务中真正重要的事
把无父 span 比例作为指标。 这是人发现传播断裂的实际唯一方法。某服务持续创建 root span,就能定位中断发生在其前方。
队列与异步边界必须手工连接。 HTTP 计量库会添加 header,但把上下文写入消息再提取,没人会代做。有队列的区间,传播必须进入设计阶段。
span 不可见时先检查时间戳。 若填入的不是纳秒值,数据会索引到 1970 年,在默认查询范围中永远找不到。Collector 日志显示“已接收”而页面为空时,几乎就是这个原因。
下一实验将在真正的 Collector 和真实请求上亲自验证这些现象。