跳转到主要内容

为什么 PID 和 TID 不足以关联并发 TLS、HTTP/2 和 SSE 流量?

简短回答: 进程或线程标识说明探针在哪里执行,并不能说明这些字节属于哪个连接或请求。函数入口与返回之间的配对可以使用线程级临时状态,但协议重组需要独立的连接生命周期标识。在连接内部,HTTP/2 的请求状态按 stream 区分,压缩状态则属于连接及其方向。随后,Server-Sent Events(SSE)还必须在正确的 HTTP 响应体内部解析。

即使应用只有一个线程,这个区别也很重要:一个事件循环可以交替处理多个连接;反过来,应用也可能把同一连接交给不同工作线程。按 PID 分组会合并无关流量,按 TID 分组既可能混入其他连接,也可能把同一连接拆成几段不完整的历史。延长解析超时无法修复这种身份错误。

把探针调用与协议对象分开

Linux 的 bpf_get_current_pid_tgid() helper 把当前任务的 thread-group ID 放在高半部,把 task ID 放在低半部。按通常的用户态叫法,它们分别是进程 ID 和线程 ID。这两个字段都不描述 TLS 对象,具体定义见内核 helper 源码

对于常规同步 TLS 库调用,可以用线程级临时槽位保存入口参数,直到返回探针读取它们。如果选定的 hook 存在嵌套,或者同时观察包装函数和底层函数,就需要有界的调用序号或深度区分,或者只选择一个不重叠的 hook 层。否则,后一次调用可能覆盖前一次的参数,同一批字节也可能被重复计数。

这个临时槽位不是协议重组缓冲区。可以先从概念上划分状态:

状态标识及作用域
尚未返回的函数调用进程实例、TID、操作类型,以及必要时的调用序号或深度
明文字节重组连接生命周期和方向
HTTP/2 请求与响应连接生命周期和 stream ID
HPACK 解码器连接生命周期和观察方向;供该方向的多个 stream 共用
SSE 解析器某一个具体的 HTTP 响应体

这里描述的是设计边界,不是可以直接复制的 ABI。采集器仍然需要定义标识如何创建、事件如何排序、缺口如何检测,以及状态何时回收。

TLS handle 需要生命周期,而不仅是地址

对于 OpenSSL 的 TLS-over-TCP hook,SSL * 参数可以区分连接。应在入口保留它,同时记录缓冲区和操作元数据,而不是丢掉它,再根据 TID 猜连接。SSL_read 接口明确包含这个对象参数。

但地址不是永久身份。不同进程可以使用相同虚拟地址,分配器可以复用已经释放的内存,OpenSSL 也能通过 SSL_clear 重置现有对象以建立另一个连接。因此,采集器内部可以采用这样的概念模型:

process_instance = collector_scope + process_lifetime
connection       = process_instance + tls_object + connection_epoch
request          = connection + http2_stream_id

epoch 必须跟随观察到的连接创建或复用变化,不能简单定义为“任意超时之后看到的第一个字节”。需要根据实际支持的库和 hook,处理进程退出与 exec、对象重置、最终销毁,以及采集器重启。文件描述符或 socket cookie 有助于关联库调用与 socket 观察,但不能代替两个层次之间经过验证的映射。文件描述符也会复用,而且 TLS BIO 不一定是 socket BIO。

如果错过了生命周期事件,应将关联标为不确定,并限制或回收旧状态。不能因为地址相同,就把新连接默默接到旧解析器上。同样,中途挂载到一个已经运行的连接,只能说明覆盖不完整,不能证明它此前的协议状态为空。

TLS 调用成功,仍不等于拿到了一个 HTTP 消息

SSL_read 文档区分了请求的缓冲区容量与实际返回的字节数。对于 SSL_read_ex,返回值表示是否成功,字节数通过输出参数给出。只有成功之后才能读取有效返回数据,不能把请求容量当成有效载荷长度。

写入也有类似区别。SSL_writeSSL_write_ex 可能需要重试,partial-write 模式还会改变一次成功操作接受的数据量。重组流程应提交成功接受的字节数,而不是每次尝试写入时都再次追加整个入口缓冲区。返回结果确认之前,入口观察只能算一次写入尝试;TLS 库成功接受数据,也不等于远端应用已经收到。

解析器应按连接方向消费有序字节流,在多次调用之间保留尚不完整的输入。TLS 调用边界不能充当 HTTP frame 或事件边界。同时捕获包装函数和底层实现、遗漏返回探针、截断缓冲区、丢失输出事件,都需要显式记录。如果插桩没有保住相关的连接内顺序,仅按时间戳排序也无法证明结果正确。

HTTP/2 与 SSE 分别增加了什么状态

HTTP/2 在一个连接内复用多个 stream。另一个连接可以再次使用相同 stream 编号,所以 (TID, stream_id) 不能安全替代 (connection, stream_id)。请求和响应归属同一个 stream 身份,方向则决定当前处理哪一侧的字节和解析状态。

HPACK 的两个方向拥有独立压缩上下文。被动解码器需要为每个连接方向保留对应上下文,由该方向的多个 stream 共用。进程级全局解码器会污染无关连接;每个 stream 都新建解码器,又会丢失共享的头部表历史。如果采集缺口遮住了一次表更新,后续数据即使看起来像合法 frame,也不能证明解码出的头部可信。在完整捕获的新连接或经过协议证明的恢复过程重建状态之前,不应再宣称解码完整。

SSE 是 HTTP 响应体内的格式,不是另一种线程级传输。HTML 标准规定了逐行处理和以空行划分事件的规则。应跨输入块保留半行和不完整事件,并且只输入属于该响应的字节。SSE 的 id 是应用提供的重连状态,不是全局唯一的连接或请求键。即使应用继续同一个逻辑事件序列,重连也会建立新的 HTTP 响应关联。它可以通过新 stream 复用已有 HTTP/2 连接;只有传输生命周期确实改变时,才应递增传输 epoch。

扩展解析覆盖之前,先验证身份

使用带有不同非敏感标记的本地合成工作负载。下面是建议的回归检查清单,不表示本文已经运行了这些实验:

测试情形必须观察到的结果
一个工作线程交替处理两个 TLS 连接载荷和解析状态不会跨连接混合
同一连接交给另一个工作线程TID 改变后仍保持相同连接身份
两个 HTTP/2 连接使用相同 stream 编号两个请求仍然独立
多个 stream 使用动态头部压缩同方向共享历史正常工作,不污染其他连接
SSE 的行跨越多次读取每个完整事件只归属一个响应
对象被复用,或采集中途开始旧解析状态被回收,或明确报告覆盖不完整
注入重试、截断或事件丢失不重复追加,也不伪造完整消息

把应用边界上的预期请求归属与采集器输出进行比较。首先检查匿名连接 ID、方向、字节数、缺口标志和状态转换。载荷检查应当是显式且范围受限的选项:TLS uprobe 能看到明文,因此 map、缓冲区、日志或导出路径中的任何临时载荷副本,都需要明确的访问与保留策略。下游脱敏不能撤销上游已经发生的采集。

上述连接模型服务于特定观察层,并非通用追踪身份。其他 TLS 库、语言运行时、QUIC 和未支持的 I/O 路径,都需要各自的映射与覆盖测试。如果能取得应用请求 ID 或 trace context,可以获得更强的语义关联。缺失的上下文应该保持缺失;PID/TID 的接近程度是有用的诊断元数据,不是虚构请求关系的依据。

参考资料

当日社区讨论

本次通过普通可见浏览器检查了全部 6 个批准社区、15 个白名单频道或公开页面,所有目标均可访问。选题出现在严格的过去 24 小时窗口内,没有使用七天回退。以下综合分析已移除参与者和频道身份、消息链接、精确时间、私有部署信息及原始措辞;没有保留原始 transcript,也没有执行社区互动。

抓到了数据,关联仍然可能是错的

最明确的调试问题涉及并发加密应用流量:数据可以被观察到,却不能可靠地分配给正确请求。问题边界位于字节捕获与归属判断之间,探针本身正确,并不能弥补解析器选错状态键。优先诊断步骤是两个连接的隔离测试,然后检查连接复用和事件丢失,再考虑增加协议启发式。前文的 OpenSSL 接口与 HTTP/2 状态模型支持这种划分。现有讨论不能确定每个采集器中哪一种故障占主导,因此 hook 覆盖不完整、事件丢失和身份冲突仍应作为不同假设分别验证。

网络恢复需要应用层成功标准

另一类问题是怎样有说服力地评估 eBPF 辅助的网络恢复设计。数据包重新开始转发、TCP 连接恢复、应用操作成功完成,是三种不同结果。TCP 传输控制块承载连接状态,仅仅把包转发到另一个端点,不能证明那里已经有了原连接状态或应用状态。

我们建议把临时路径中断与端点重启分成独立测试。每种情形都在相同负载下比较未修改的基线与待测机制,记录恢复时间分布和 CPU 开销,再通过合成序列标记检查应用可见的缺口、重复和失败操作。内核兼容性应记录到具体构建版本,不能由一次成功运行推断通用最低版本。观察到的是对评估方法的征询,不是透明恢复已经有效的独立验证。

卸载 hook 与回收代码不是同一个事件

内核审查继续关注由生成 trampoline 调用的程序生命周期,并要求可复现的并发测试。通用机制是区分“阻止新的执行进入”和“等待已经在途的执行结束”。RCU 文档解释了移除与回收的差别,但具体证明仍必须覆盖真实执行路径及其同步域,包括仍可能到达另一个对象的中间代码。

有价值的测试应让反复挂载、卸载与繁忙 hook 重叠,再同时检查陈旧执行和永远无法回收的资源。审查活动还涉及别名失效、有界迭代及类型校验边界。这些都是提案或审查信号,不能据此宣称某个发行版内核已经包含全部修复。部署结论需要目标源码版本、回补内容和对应 selftest 的证据。

有依赖关系的插桩改动需要明确审查顺序

活跃的插桩回复主要讨论如何拆分相互依赖的改动,同时避免反复展示同一份前置 diff。实际问题是比较基线和合并顺序,而不是新增遥测字段需求。依赖清单、仓库允许时的增量比较,以及整组改动的集成检查,有助于表达预期顺序。GitHub 的基线文档也提醒,更改基线可能使既有审查上下文失效。分支权限和发布协调仍取决于各仓库,讨论没有证明存在普遍可用的流程。

安静的目标也完成了检查

多数项目帮助与功能区,以及调度器支持区,在窗口内没有新的实质技术问题。通用 eBPF 聊天主要是审查协调,并非新的故障排查问题;eBPF 插桩区没有窗口内新增的技术交流。自动开发动态和文章分享没有被算作额外用户需求。这些属于可访问但安静、或只有协调活动的观察结果,不是把访问缺口伪装成零活动。