线程池、协程、消息队列里 span 的父子关系怎么保持?哪些 Python 写法会悄悄丢掉上下文?消息晚消费了,span 还连得上吗?一次处理多条消息的 span 该用 link 还是 parent?这篇用 OpenTelemetry Python SDK 和 RabbitMQ 逐项试出来。
做法 | 子 span 的归属 ---------------------------------+---------------------- (a)threading.Thread | 另起新 trace(根 span) (b)ThreadPoolExecutor.submit | 另起新 trace(根 span) (c)submit(copy_context().run, f) | 同一个 trace,父 span 是调用方 (d)线程里 attach 保存的上下文 | 同一个 trace,父 span 是调用方 新线程的 contextvars 是空的:线程池和 threading.Thread 都不会把调用方的上下文带过去,子 span 就成了新 trace 的根;要显式带上(copy_context().run 或者 attach)
新线程的 contextvars 是空的,线程池不带调用方的上下文;要用 copy_context().run 或 attach。
做法 | 子 span 的归属 -------------------------------------+---------------------- asyncio.create_task | 同一个 trace,父 span 是调用方 asyncio.gather(两个协程) | 同一个 trace,父 span 是调用方 loop.run_in_executor | 另起新 trace(根 span) run_in_executor + copy_context().run | 同一个 trace,父 span 是调用方 asyncio.to_thread | 同一个 trace,父 span 是调用方 loop.call_soon | 同一个 trace,父 span 是调用方 另一个线程里 call_soon_threadsafe | 另起新 trace(根 span) 事件循环线程里 call_soon_threadsafe | 同一个 trace,父 span 是调用方 协程之间(create_task、gather)和 to_thread 会复制调用方的上下文;run_in_executor 不会;call_soon 取决于调用时所在线程的上下文,从另一个线程调用拿到的是那个线程自己的(空)上下文
create_task、gather、to_thread 会复制上下文;run_in_executor 和别的线程里的 call_soon_threadsafe 不会。
情形 | 结果 | 备注
-------------------------+-----------------------------------------------------------------+----------------------------------------------------------
(a)消息头不带 traceparent | 消费者 span 是新 trace 的根 | -
(b)带 traceparent,1 s 后消费 | 消费者 span 是生产者 span 的子 span(同一个 trace) | 生产者 span 在 0.09 ms 时结束,消费者 span 在它结束之后 1014 ms 才开始
(c)3 条消息批量处理 | 批量 span 自己是新 trace 的根(parent=无),带 3 个 link,各自指向对应的生产者 span:True | 3 个生产者的 trace_id 互不相同:True;批量 span 的 trace_id 与它们都不同:True
parent-child 表示“这个 span 是那个 span 的一部分”;消息被延迟消费、或者一个 span 同时处理多个来源时,用 link 只记录“因果相关”,不放进同一棵树里
ERRORS {}消息头带 traceparent 就能让消费者 span 成为生产者的子 span,不管晚了多久;一次处理多条消息用 link。