你打开了一个运行缓慢的对话追踪。模型调用竟然在用户点击发送前的 800 毫秒就开始了。你责怪了用户的笔记本电脑,关闭了标签页,继续处理其他事情。
这不仅仅是一个用户的时钟出了问题。这涉及大约三分之一的流量,而且每一个跨越客户端边界的调试会话都在读取一个根本不存在的时间线。浏览器时钟是用户可设置的,经常不同步,偶尔甚至会偏差好几天。大多数可观测性技术栈附带的检测 SDK 会使用设备报告的任何时间来标记客户端 span,通过 traceparent ID 将它们与同步服务器时钟标记的服务器 span 链接成一棵树,然后将结果交给你的值班工程师,就好像这两半具有可比性一样。它们并不具有可比性。
这里的 bug 并不是说某些时钟会漂移。Bug 在于分布式系统的基本原理——时钟会撒谎、排序需要显式同步、权威时间源是你控制的那个——在被引入 AI 可观测性技术栈时,却没有带上相应的警示。单进程追踪教会了一代工程师像对待绝对真理一样阅读火焰图。当 span 来自同一个进程时,这确实是真理。但当第一个 span 源自不受控的浏览器时,火焰图就变成了一个包装成事实的假设。
这种失败模式是继承的,而非发明的
对话式 AI 系统跨越的信任边界比追踪模式最初设计的系统要多。一个典型的用户回合通常涉及浏览器、边缘工作节点或 CDN、API 网关、模型提供商、向量存储、几个内部工具以及数据库。每一个都属于不同的管理域。浏览器的时钟归用户所有。提供商的时钟归提供商所有。你的网关是这条路径中你唯一可以权威信任的时钟。
标准的检测模式会在每一层发出一个带有 start_time 和 end_time 的 span,取自该层恰好拥有的任何时钟。浏览器中的 OpenTelemetry JavaScript SDK 使用 performance.now() 作为单调时间源来计算持续时间,但将其锚定到 Date.now() 以获取墙上时钟时间戳,这意味着该锚点与用户的系统时钟一样错误。设备休眠一夜后,锚点不会刷新,时间戳继续漂移;SDK 仓库中有一个长期存在的问题记录,记录了出现在数小时或数天前的 span。
大多数后端提供的后期修复——重新调整子 span 到父 span 窗口内的时钟偏移调整——被 Jaeger 维护者自己称为“被认为是有害的”。调整器假设父 span 和子 span 具有相似的持续时间,这对于紧凑的 RPC 是成立的,但对于 LLM 生成几乎从未成立。它通过悄悄改写时间戳隐藏了潜在问题。它依赖于未记录的主机标识标签,并在标签缺失时退而补偿几乎所有的 span。值班工程师在阅读清理后的追踪时,已经丢失了时钟最初出错的信号。
对话式追踪让问题变得更糟
LLM 工作负载的三个特点使得这种失败模式比传统的请求-响应追踪更严重。
Span 足够长,使得偏移变得至关重要。 一个正常的 RPC 追踪由亚毫秒级的网络跳转主导,其中 50 毫秒的客户端时钟漂移只是舍入误差。一个智能体回合则由多秒钟的 LLM 调用主导,随后是每个以百毫秒计的工具调用。在 4 秒的模型调用中,客户端标记的“用户点击发送” span 中 50 毫秒的漂移是看不见的,但当你试图在相同粒度下推论首标记(time-to-first-token)延迟时,它就变成了一个调试泥潭。
因果关系很难仅从结构中读取。 在同步 RPC 树中,父子关系由调用图强制执行:父节点必须在子节点之前开始,子节点必须在父节点之前结束。智能体追踪经常违反这一点。一个与模型生成并行启动的投机性工具调用与用户 span 没有清晰的父子关系。对话中的第二个助手回复是上一个回合的子节点,但在追踪中却是它的兄弟节点。时钟偏移调整算法所依赖的结构性支撑并不存在。
流式传输反转了延迟预算的支出位置。 在流式响应中,“模型调用”不是一个具有开始和结束的事件;它是在两个端点之间发射的一系列 token。首标记时间、token 间延迟、完成时间是衡量同一个 span 中三个不同事件的三个不同 SLO。如果开始事件是由网关标记的,而首标记事件是由浏览器上的流式 SDK 标记的,那么你就是在跨越两个时钟计算你最受关注的延迟指标。仪表盘上的数字是真实值加上或减去客户端漂移,而你对此一无所知。
弥合差距的模式 修复的方法不是去寻找一种信任客户端时钟的方法,而是停止假装你信任它。这里有四个动作,全部借鉴自早于 AI 数十年的分布式系统文献。
以网关标记的服务器接收时间戳作为权威排序键。 每个源自客户端的 span 都应在第一个服务器端边界处,使用你控制的时钟重新标记一个 server_received_at 值。原始客户端时间戳不会被删除——它作为 client_reported_at 保留用于取证分析——但权威排序使用服务器标记。当需要比较两个事件时,比较是针对服务器时间戳轴进行的。当你绘制火焰图时,X 轴是服务器时间。客户端时间戳是一个元数据字段,而不是坐标。
针对每个会话的时钟偏移估算。 当浏览器首次建立会话时,模仿 NTP 的时钟同步算法,与网关交换几对时间戳:客户端发送其时间戳,服务器记录接收和回复时间戳,客户端记录接收时间戳,这四个值可以得出网络延迟范围内的单向偏移估算。将估算的偏移量存储在会话中,并在摄取客户端 span 时将其作为修正值应用。这并不能使客户端时钟变得准确,但它使修正后的时间戳可用于亚秒级调试,而不是毫无用处。OpenTelemetry 浏览器 SDK 并不开箱即用支持此功能;你必须自行添加。
客户端与服务器 span 之间的显式因果链路注解。 不要通过挂钟时间相减来推断因果关系。浏览器知道它何时发送了请求,网关知道它何时接收了请求。这两个事件是同一个事件被观察了两次;在 span 树中显式注解该链路,以便当你的 trace 查看器重建排序时,它使用的是因果边(causal edge)而不是时间戳差异。OpenTelemetry 语义约定支持针对这种跨上下文引用的 span link;很少有技术栈真正将它们用于客户端-服务器的时钟调解。
针对每个 trace 的时钟置信度评分。 阅读火焰图的值班工程师需要知道他们盯着的时间线是否值得信赖。根据会话的估算漂移量、自上次漂移估算以来的时间、应用的任何修正幅度来计算置信度值,并将其显示在 trace 上。低置信度的 trace 会显示黄色横幅;修正后的漂移超过某个阈值的 trace 会显示红色横幅,并建议仅根据服务器端 span 进行调试。这与在图表上显示置信区间是同样的准则——拒绝渲染一个假装比底层测量更精确的数字。
当你内化了这些之后的样貌 吸收了这些知识的团队不再问“延迟是多少?”,而是开始问“在谁的时钟上?”仪表板会为每个延迟指标标注时钟域——ttft_server 与 ttft_client——这样阅读的人就知道什么是相对于什么进行测量的。用于把控发布的 SLO 是根据服务器标记的事件定义的,因为这些是团队可以确信的。客户端标记的数字被保留用于产品分析——从用户角度感知的延迟是真实存在的,用户的时钟确实是正确的参考——但它们不用于故障排查。
Trace 查看器在视觉上不同的泳道中渲染客户端 span,并在页眉显示时钟置信度评分。当工程师悬停在 span 上时,工具提示会同时显示 client_reported_at 和 server_received_at 以及它们之间的偏移量。当偏移量很大时,UI 会显示每个会话的漂移估算,以便工程师可以进行心理调节。这些都不稀奇;这与金融系统在十年前接受交易公司时钟与交易所时钟永远不一致这一事实时,在其订单撮合流水线中建立的规范是一样的。
架构上的领悟是朴实无华的。一个 AI agent 的 trace 跨越了与任何分布式系统的 trace 相同的信任边界。时间线的两半由不同的权威机构标记,通过不同的视角观察,并且只能通过在网关处进行的刻意工作来调解。如果团队将客户端 SDK 视为权威时间戳的被动发射器,而不是将其视为报告其自身主观时间的不可信观察者,那么他们构建的可观测性栈就会自信地误导其操作人员。修复方法不是更好的插桩(instrumentation)。修复方法是记住时钟是一个分布式系统问题,SDK 不会为你解决它,而网关是 trace 中你真正拥有的唯一时间戳。
会员专享
余下内容仅对会员开放。 会员可读完整内容 —— 每一个公开发布的观点背后,那些框架、决策与推理的全貌。
— 完整文章,包含未公开存档的部分 — 可落地的工作框架,附带权衡与决策依据 — 新文章抢先看,先于公开发布 登录以继续阅读→ 随时取消 · 一次订阅,畅读全部