三个相连的方块,依次标注观察、决策与行动。

Baggage 通过明确的边界传播上下文

OpenTelemetry 的 Baggage 是与上下文关联的一组字符串键值,可以让请求入口已经知道的信息继续到达下游,例如部署区域或发布分组。它与跨度属性不同,主要用途是传播;它也与链路上下文不同,承载的是应用选择的信息,而不是连接跨度的身份标识和标志。这些机制可以一起出现在一个请求里,但不能因此把它们理解为同一份数据,也不能假定它们具有相同的存储和信任语义。 跨进程传播必须落实到具体边界。发送客户端把上下文注入 HTTP 请求头等载体,接收端再从载体中提取成本地上下文。配置正确的插桩库可以自动执行这些动作,手工插桩时则要明确调用。仅在一个进程中创建行李字段,并不会把信息自动送到另一个进程、任意后台任务或外部服务。判断传播是否成功,应检查真实客户端与接收端,而不是只检查入口是否调用过设置函数。 决定哪些信息可以传播、哪些需要记录 行李字段不会自动成为跨度、日志或指标的属性。下游必须显式读取并复制,或者使用配置好的处理器执行这一映射策略。团队如果只打开传播,就期待在查询后台找到对应属性,通常会遇到数据看似消失的问题。反过来,把所有入站字段无差别复制进遥测,也可能造成语义不清、数据过大或指标基数增长。传播和记录是两个独立决定,应当分别明确目的与范围。 上下文操作返回新的上下文,不会把所有既有引用同时修改。调用者需要保留返回值,并在预期的位置传递或附加它。为某个出站请求删除不合适的行李字段时,也应保留该请求仍然需要的链路上下文。出站字段允许列表和遥测属性允许列表都应当是明确策略:某个接收者需要的值,不一定适合随后经过的全部目的地,更不一定适合永久写入所有观测记录。 坏掉的样子 行李上下文本身没有内置机制,能够证明某个值确实由可信组件签发。调用方完全可以提交请求头,自称属于昂贵账户等级或拥有特权角色。解析成功只能说明数据表示可被读取,不能说明其声明具有授权效力。认证与权限判断必须依赖各自可信的输入。即使字段很短、内容看起来合理,也不能跳过信任来源判断;本地解析器接受字符串,不等于业务系统应该接受其中的身份声明。 不受限制的行李字段会增加请求头大小,也可能把敏感信息传播给原本不该接收它的对象,包括自动插桩所覆盖的第三方接口。需要限制键名与值的范围,检查实际传播目的地,并由传播器负责线上编码。两个相同 SDK 实例之间往返成功,也不等于所有语言实现和代理组合都能够互操作。真正跨服务使用时,应验证实际途经的边界,而不是把一个进程内的演示扩大成对整个部署的保证。 快速验证 建立 Python 虚拟环境,安装 opentelemetry-sdk==1.35.0,把脚本保存为 baggage.py,并使用该环境的解释器运行。本次实际使用 Python 3.14.4。实验调用真实的 W3C 链路上下文传播器和行李传播器,并把跨度导出到内存;没有启动 HTTP 服务,字典代表客户端接下来将发送的请求头载体。因此结果直接证明编解码与上下文行为,不把尚未执行的跨服务通信写成已完成验证。 import json from opentelemetry import baggage, trace from opentelemetry.context import Context from opentelemetry.baggage.propagation import W3CBaggagePropagator from opentelemetry.propagators.composite import CompositePropagator from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator base = Context() raw = baggage.set_baggage("region", "eu-west", context=base) raw = baggage.set_baggage("release", "canary/2", context=raw) raw = baggage.set_baggage("internal_note", "synthetic-only", context=raw) assert not baggage.get_all(base) propagator = CompositePropagator([TraceContextTextMapPropagator(), W3CBaggagePropagator()]) provider = TracerProvider() exporter = InMemorySpanExporter() provider.add_span_processor(SimpleSpanProcessor(exporter)) tracer = provider.get_tracer("baggage-lab") with tracer.start_as_current_span("parent", context=Context()) as parent: outgoing = trace.set_span_in_context(parent, baggage.clear(context=raw)) for key in ("region", "release"): outgoing = baggage.set_baggage(key, baggage.get_baggage(key, raw), context=outgoing) carrier = {} propagator.inject(carrier, context=outgoing) received = propagator.extract(carrier, context=Context()) assert set(baggage.get_all(received)) == {"region", "release"} assert baggage.get_baggage("region", received) == "eu-west" assert baggage.get_baggage("internal_note", raw) == "synthetic-only" assert trace.get_current_span(received).get_span_context().trace_id == parent.get_span_context().trace_id with tracer.start_as_current_span("inherited", context=received): pass with tracer.start_as_current_span("copied", context=received) as span: span.set_attribute("deployment.region", baggage.get_baggage("region", received)) assert provider.force_flush() spans = {span.name: span for span in exporter.get_finished_spans()} assert not spans["inherited"].attributes assert spans["copied"].attributes == {"deployment.region": "eu-west"} independent = propagator.extract({"traceparent": "invalid", "baggage": "region=eu-west"}, context=Context()) assert not trace.get_current_span(independent).get_span_context().is_valid assert baggage.get_baggage("region", independent) == "eu-west" untrusted = propagator.extract({"baggage": "tier=gold"}, context=Context()) assert baggage.get_baggage("tier", untrusted) == "gold" print(json.dumps({"wire_fields": sorted(carrier), "wire_baggage": carrier["baggage"], "automatic_span_attributes": dict(spans["inherited"].attributes), "explicit_span_attributes": dict(spans["copied"].attributes), "baggage_survives_invalid_traceparent": True, "caller_supplied_tier_was_parsed": True}, indent=2)) provider.shutdown() 原始上下文包含区域、发布分组和一条完全虚构的内部备注。出站上下文只保留前两个字段,同时携带原父跨度。接收后区域应为 eu-west,发布值 canary/2 经过请求头编码后还原一致。断言还检查了筛选过程没有删除原上下文中的备注,也没有改变最初的空上下文。这说明清理出站载体与修改其他引用不是同一件事,后续代码必须使用正确的上下文对象。 ...

三月 27, 2028 · 2 分钟 · XB Field Notes
以极低代价、无需重启地实时观察内核事件

BPF tracepoint 能窥探正在运行的内核

BPF 与内核 tracepoint 能在源头观察行为,且占用极小:不加 agent、不重启、不打断正在跑的工作负载。传统剖析意味着装东西、带 flag 重启、或暂停进程——每样都扰动被研究对象,或在生产根本不被允许。BPF 探针挂到跑着的内核上,收走答案就摘掉,工作负载原样归还。 BPF 给的是日志给不了的答案:哪些 syscall 烧时间,哪些文件卡磁盘,TCP 重传聚在哪,全从主机每个进程实时采样。“哪里慢"“现在发生什么”,对的一行命令一分钟,胜过日志考古一小时。 不断流地探测 一个很小的 eBPF 程序挂到某个既有 tracepoint 上,一边累计计数或直方图统计数据,工作负载照常跑。它运行在内核上下文,探针几乎不会成为瓶颈。程序加载前先过验证器——内核检查内存安全与可终止——有 bug 的探针被拒绝,而不是带崩机器。聚合发生在内核 map 里,几百万事件先坍成小直方图再出用户态;常见开销个位数百分比,往往比团队本来就跑着的日志还便宜。 从问题出发,不从工具出发。“哪个进程开文件最多"是一种 trace 形态,“按设备看 p99 磁盘延迟"是另一种,bpftrace 一行命令画廊两种都覆盖,不用写 C。优先高级前端——bpftrace、bcc 工具——真需要定制逻辑再碰 raw BPF;故障时写得出的一行,胜过下周才写完的完美程序。 tracepoint 是更稳定的名字 内核 tracepoint 标记 block I/O、调度、syscall 进出这类事件,名称比裸 kprobe 更不易碎,跨内核升级依旧健壮。kprobe 钉内核函数地址,版本一换就漂;tracepoint 命名内核承诺保持的语义事件,周二补丁重启后同一探针照跑。挂离行为最近的:磁盘问题挂 block,离 CPU 延迟挂调度,“谁调什么"挂 syscall。 ...

三月 6, 2028 · 1 分钟 · XB Field Notes