在 Go 中使用 TCP keepalive · Felix Geisendörfer

发表:
如果您曾经编写过一些 TCP 套接字代码,您可能会想:“如果拔掉网线或远程计算机崩溃,我的连接会发生什么情况?”。
简短的回答是:什么也没有。连接的远端将无法发送 FIN 数据包,本地操作系统也不会检测到连接丢失。因此,由您作为开发人员来解决这种情况。
在 Go 中,您可以使用多种方法来帮助解决此问题。也许第一个要考虑的是 SetReadDeadline net.Conn 接口的方法。假设您的连接预计会定期接收数据,您可以简单地将超时读取视为相当于 io.EOF 错误和 Close 连接。许多现有的 TCP 协议通过定义某种心跳机制来支持这种错误处理方式,该机制要求每个端点定期发送 PING/PONG 探测,以便检测网络问题以及服务运行状况1。此外,此类心跳还可以帮助处理寻找网络活动以确定连接健康状况的代理服务器。
因此,如果您的协议支持心跳,或者您有能力将心跳添加到自己的协议中,那么这应该是解决未插入网线场景的首选。
但是,如果您无法控制协议并且不支持心跳,会发生什么情况?
现在是时候了解 TCP keepalive 以及如何在 Go 中使用它了。 TCP keepalive 在 RFC 1122 中定义,并不是 TCP 规范本身的一部分。可以为单个连接启用它,但默认情况下必须关闭。启用它将导致网络堆栈在默认持续时间(必须不少于两个小时)后探测空闲连接的运行状况。探测数据包将不包含任何数据2,并且未能回复单个探测不能被解释为死连接,因为单个探测数据包没有可靠地传输。
Go 允许您使用以下命令启用 TCP keepalive net.TCPConn的 SetKeepAlive。在 OSX 和 Linux 上,这将导致在连接空闲 2 小时后以 75 秒的间隔发送最多 8 个 TCP keepalive 探测。或者换句话说, Read 将返回一个 io.EOF 2 小时 10 分钟后出现错误 (7200 + 8 * 75)。
根据您的应用程序,超时可能会太长。在这种情况下你可以打电话 SetKeepAlivePeriod。然而,目前此方法对于不同的操作系统表现不同。在 OSX 上,它将修改发送探测之前的空闲时间。然而,在 Linux 上,它将修改空闲时间以及发送探测的时间间隔。所以打电话
SetKeepAlivePeriod 使用 30 秒的参数将导致 OSX 上的总超时为 10 分 30 秒 (30 + 8 * 75),但在 Linux 上将导致 4 分 30 秒 (30 + 8 * 30)。
我发现这种情况相当令人不满意,因此我最终创建了一个名为 tcpkeepalive 的小包,它可以为您提供更多控制:
kaConn, _ := tcpkeepalive.EnableKeepAlive(conn)
kaConn.SetKeepAliveIdle(30*time.Second)
kaConn.SetKeepAliveCount(4)
kaConn.SetKeepAliveInterval(5*time.Second)
目前仅支持 Linux 和 OSX,但我很乐意合并其他平台的拉取请求。如果 Go 核心团队有兴趣,我也会尝试将这些新方法贡献给 Go 本身。
如果您认为本文有用、有任何疑问或发现任何错误,请告诉我,以便我纠正它们。
附录
-
调整心跳机制以尽早检测故障并降低误报率是一件棘手的事情。查看 phi 应计故障检测器以获得统计上合理的模型,以及 Damian Gryski 的 go-failure 实现。不幸的是,我无法想到将它与 TCP keepalive 一起使用。
-
根据 RFC 1122,保活探针可能包含单个垃圾八位组,以与损坏的实现兼容。但是,我不确定这是否被操作系统网络堆栈过滤掉,如果您知道,请发表评论。
——菲利克斯·盖森多夫
通过 RSS 或电子邮件订阅此博客,或者通过以下方式获取我的小更新 叽叽喳喳。
