藏川线前段

--- 摄于 2017 年 9 月 藏川线前段

从 CLOSE_WAIT 状态说起(二)

接着上一篇,这篇来研究一下导致 backlog 不再被 accept 的原因、修复、复现手法。

理解 Future 的 waker 机制

要看懂修复的逻辑,首先要理解(解释) Rust Future 的工作机制,以及知道怎么手工写一个行为正确的 Future,这是一个比较复杂的事情,这也是为什么会存在 async/await 语法的原因。

Rust 的 Future 整个工作机制,被我称之为就绪通知模型,任何事件的就绪,都会通过 waker 这个 Context 去通知 Executer 拿到对应的 Future 执行 poll 操作。如果该 Future 返回 Ready,这个 task 就完成了,就地清理就可以了,如果 Future 返回 pending,这个 task 还需要等待下一次 waker 进行 poll 操作。从模型来看,相对简单,但是到实现的时候,会有很多 trick 的地方。

在实现 Future 的时候,大概有几种情况需要考虑:

同时,要考虑到一个问题,waker 是由具体 Runtime 自行实现的,在多线程的 Runtime 实现中,每一次 poll 同一个 Future 带入的 waker 可能因为上层对 同一个 stream 的读写分离而不一致,就是说,buffer 层至少需要考虑到由于存在读写分离的可能性,需要对 读写 操作的 waker 分开存储,以防因为丢失 waker 导致无法通知上层相应操作的可能性。

其实不止是读写分离问题,更激进一点,同一个操作因为实现的不同,有可能会导致 waker 也不同,包括但不限于因为 线程切换 导致的上下文切换,这时候, buffer 层需要考虑的问题是,只能存 pending 发生时,对应的 waker 进行下次唤醒操作,也就是说,底层的 waker 实际上是由每一次 poll 带入的 Context 决定的,底层大概率不会永久保存第一次传入的 waker 永久使用,因为这个假设是完全错误的。

原因及修复方法

关注这个 PR ,标题和描述似乎与本问题无关,这也是线上未升级的原因之一,因为这个 PR 除了修复了 issue 的问题之外,顺手还修复了 accept 会因为异常而不再被调用的问题,也就是说,这个 PR 隐藏了一个修复细节。

修复的核心变化是:将调用 peer_addr 产生 error 返回 pending 的操作改成了 loop 执行,忽略该 error,直到底层返回 pending 时才对应返回 pending 状态

这么修复的原因可以参见上一节的注解理解,这里重复一下:

修复之前,因为在不该返回 pending 的时候返回了 pending,导致底层事件发生的时候无法通知对应的 Future,也就是说,accept 这个 Future 在该状态下,永远无法唤醒,backlog 的溢出也就无法避免了。

复现方法

我们已经知道了,调用 peer_addr 错误可以复现该问题,那么怎么才能复现 peer_addr 调用错误呢?

其实 peer_addr 方法就是 getpeername,这个方法返回的是远端地址,正常连接状态下是不会报错的,只有从 accept 中拿到已经断开的连接才会报错,想要复现这个问题,需要做的事情是:

这样的操作,本地应该可以完成,但是在不知道有这个问题的情况下,想要在本地复现是非常难的一件事,本地的延时可以忽略不计,而真实环境可能就因为一次网络抖动就会触发这样的问题。

小结

值得高兴的是,这个 bug 早在两个月之前就已经被发现并且修复了,大部分生产环境的代码都早已升级了,这次报送问题的环境是一套相对比较旧的代码,因为没有同步到这个 Fix 而导致了线上存在大量 CLOSE_WAIT 状态的 TCP 连接。虽然从时间上看,这个问题代码大概潜伏了几年之久,但是最终修复也是值得高兴的事情。

最新的代码,不仅意味着最新的 feature 和 bug,也意味着最新的 fix,诸君且行且珍惜。

评论区

加载更多

登录后评论