07 | 保活机制:心跳包异常导致应用重启?

你好,我是胜辉。这节课,咱们来聊聊TCP的保活机制。

以前的电视剧里经常会有这样的剧情:女主因为车祸失去了记忆,男主一边摇着女主的肩膀,一边痛苦地问道:“还记得我吗?我是欧巴啊!”可是女主已经对此毫无记忆,迷茫地反问道:“欧巴是谁?”

类似地,TCP其实也需要一种机制,让双方能保持这种“记忆”。Keep-alive这个词,你可能也听说过。特别是当遇到一些连接方面的报错的时候,可能有人会告诉你“嗯,你需要设置下Keep-alive”,然后问题确实解决了。

不过,你有没有深入思考过这样几个问题呢:

  • Keep-alive跟长连接是什么关系?
  • 它是应用层代码独立实现,还是依赖操作系统的TCP协议栈去实现?
  • 在HTTP层面也有一个Keep-alive的概念,它跟TCP的Keep-alive,是同一个东西吗?

如果你对这几个问题的答案还不清楚,那么这节课,我就来帮助你厘清这些概念。以后你再遇到长连接失效、被重置、异常关闭等问题的时候,就知道如何通过抓包分析,解读出心跳包相关的信息,然后运用Keep-alive的相关知识点,去真正解决前面说的一系列问题。

好,按惯例,我们还是从案例说起。

TCP长连接为何总中断?

当时我在云计算公司就职,有个客户的应用基于TCP长连接,但长连接经常中断,引起了应用方面的大量报错。由于客户的业务是支付相关的,对实时性和安全性要求很高,这类报错就产生了较大的负面影响,所以急需解决。

应用概况

这个应用的架构比较简单:客户在云平台上部署了多台云主机,其中一台云主机专门做加解密,称之为加密服务器。另外几台云主机作为这台加密服务器的客户端,跟这台加密服务器保持TCP长连接。这些客户端会不时地跟加密服务器进行通信,完成加密操作。每45秒,客户端还会发送一次心跳包,这有两个作用:

  • 维持这个长连接不被中断,即心跳保活,让长连接在两端保持下去;
  • 探测加密服务器的可用性,即健康检查,一旦服务不可用,客户端就要去掉这个失效的连接。

就像下图这样:

如果加密服务器能在1秒内对心跳包进行回复,那么客户端就认为服务端正常可用,后续的数据交互(即加密请求)将继续在这条长连接上进行。而如果服务端未能在1秒内回复,那么客户端会认为该长连接已经中断,于是重启应用,发起一条新的长连接,并在日志中记录一次报错。

用类Python语法来描述,大体是这样的:

while true:
    sleep(45)
    if Keep_alive_probe() is true:
        continue
    else:
        restart()
        log_error()

排查思路

整体的排查思路跟网络分层模型有点类似,逐层往下。我们需要先从应用层查找原因,然后是操作系统层面,最后是网络层分析。排查的顺序就是这样的:

应用层代码 -> 操作系统的时间配置 -> 网络的抓包分析

首先,客户自查了应用,没有发现可疑代码,并且尝试过重新部署代码、重装系统、更换IP等,但都未奏效。

那么,会不会是时间服务(ntp)的问题呢?我们检查了两侧机器的ntpd服务的状态,都是正常的。对比了两端机器的实际时间,也没有发现明显的误差。于是这个可能性也被排除了。

然后就需要排查网络了。我们在一台客户端上启动了抓包程序tcpdump,过滤条件是对端加密服务器的IP和端口。抓多久呢?因为问题大约十几分钟出现一次,那么按照这个频率,我们设定抓包时长为半个小时,得到了抓包文件。这样就能覆盖到一到两次的错误了。

补充:抓包示例文件已经上传至Gitee,建议用Wireshark打开文件,结合文稿学习。

因此,通过检查应用日志,我们发现在抓包时间段内,应用日志又记下了两次报错,比如下面这个:

图片

从日志上看,17:08:02这个时间点发生了一次报错,17:08:10发生了一次程序的重启。

有了应用层的信息,接下来,我们就需要在抓包文件中,找到传输层和网络层的信息来与应用层信息对应,这样排查工作才能继续推进。

但是日志记录的时间戳粒度太粗了,只精确到了秒级,而网络报文在Wireshark中都是微秒级的,一秒内可能会有成百上千个报文。所以,仅凭精确到秒的一个时间戳,还不足以让我们在几十个报文中,精确地找到对应的报文。这个时候,我们需要调整一下抓包文件分析工作的切入点。

一个常规的做法是,跳过案例本身的问题特殊性不谈,先从宏观上把握一下排查思路。也就是先不纠结于根据日志时间来寻找报文的难题,而是先看Expert Information,找一找有没有比较可疑的报文。打开Expert Information窗口,如下:

图片

可见,有80个Error级别的Malformed Packet(格式错误)报文和2个RST报文,都很可疑。

我们先看这80个Malformed Packet报文,这传递了什么信息呢?在Protocol栏显示SIGCOMP,说明Wireshark认为这80个报文是SIGCOMP协议的(SIP的信令压缩协议)。不过,客户的应用不是SIP相关的,显然这些报文应该是被Wiresharek误会了。

在这里,我也给你一个小小的提醒:毕竟只有被公开广泛支持的数据格式和协议才能在Wireshark中正确展示,所以如果你看到类似这种Malformed Packet的时候,还是要统筹考虑当前案例的实际情况,未必Malformed报文就真的是格式错误,也可能只是Wireshark不了解某种“方言”而已。

让我们看看这些报文究竟是什么。点开Error,选中一个报文。比如我这里选择37号报文:

图片

随后光标会自动定位到37号报文这里:

图片

然后选了另外几个Malformed Packet,发现它们都是成对出现的。它们还有一个特征是:

  • 前一个报文的TCP载荷数据是01(十六进制);
  • 后一个报文的TCP载荷数据是41(十六进制)。

这就像是某种“联动”机制了,会不会就是心跳报文呢?你注意下TCP Seglen这一栏(这是我添加的表示TCP载荷长度的列),有没有发现这类报文的长度都是1 ?我们就以 tcp.len eq 1 为过滤器,过滤出报文:

图片

可见,37和38是0秒发生的,76和77是45秒后发生的,然后97和98以及后面每一对报文,也都符合45秒间隔的规律,正好跟客户程序的45秒心跳包机制对上了。我们可以确认这些报文就是心跳报文了!更确切地说,每次成对出现的两个报文中:

  • 前一个是心跳探测包,它的TCP载荷数据为01(十六进制);
  • 后一个是心跳回复包,它的TCP载荷数据为41(十六进制)。

这是第一个比较明显的进展。也就是说,我们已经可以找到应用层的心跳包在网络层的展示形式了,这对于后续的排查非常有帮助。

不过,也不要忘了,我们是来排查“心跳包失败导致连接中断”的问题的,现在只是找到了心跳包的特征,还需要找到真正的跟日志报错相关的报文,而这个报文是跟“连接中断”现象有关的。

那么会是什么样的报文呢?其实,我们在第4讲就研究过TCP挥手。你应该还记得,TCP的连接断开,无非跟两种报文有关:FIN和RST

我们先尝试寻找FIN报文。输入过滤器:

tcp.flags.fin eq 1

结果发现什么报文都没有出来。可见,这次抓包里,连接的断开并不是用FIN完成的。

图片

接着就是寻找RST报文。其实,在Expert Information里面,就有2个RST报文,我们可以直接从那里入手。当然,用过滤器也同样方便,输入:

tcp.flags.reset eq 1

我们能找到2个RST报文:

图片

我们选中575号报文,然后Follow -> TCP Stream,得到了这个RST所在的TCP流的全部报文:

图片

报文很多,上图显示的只是一部分报文。我往前翻阅了更前面的报文,也能看到很多相对长的时间间隔,有17秒、39秒、11秒的,但也没有什么规律。这时候,我们就需要结合前面刚分析到的一些信息,要不然就要在报文的海洋里迷失方向了。

我们通过前面的分析发现,心跳探测包的报文数据是01,心跳回复包的报文数据是41。所以,让我们再次借助强大的过滤器。在 tcp.stream eq 0 后面添加 and tcp.payload eq 01,即整体过滤器变为:

tcp.stream eq 0 and tcp.payload eq 01

这样就过滤出了这个TCP流里面,所有的心跳探测包

图片

可见,这些心跳探测包也是很明显遵循了45秒间隔的规律。不过,等一下,为什么572号心跳探测包跟它的上一次心跳探测,间隔的时间是58秒,而不是45秒呢?

图片

这很反常,也是第二个很重要的发现。要知道,这45秒的机制是在代码里实现的,照理说不可能出现13秒这么大的误差。现在,我们把 tcp.payload eq 01 这个条件去掉,回到这个TCP流的完整报文区域来综合分析:

图片

我来给你解读一下:

  • 572号报文(客户端发出)是心跳探测包;
  • 573号报文(服务端发出)是心跳回复包;
  • 574号报文(客户端发出)是客户端对573号报文的确认;
  • 575号报文(客户端发出)是一个RST报文,它跟客户端日志报错有直接的关系。

以上4个报文之间几乎没有时间上的停顿。这里,你可能会有个小疑问:为什么572号报文跟上一个报文的间隔是17秒而不是45秒呢?其实,这是Time列的类型导致的,我这里用的是Delta time displayed类型,是跟前一个被显示的报文的间隔时间。

之前我们看到很多个整齐的45秒,是因为那个窗口里显示的报文,都是过滤过的心跳报文,跟这里显示的报文不同,所以显示的间隔时间也不同。Wireshark里的Time一列有多种配置可选,所以你一方面要理解这里面各种Time的区别,一方面自己做分析的时候,也可以根据实际需要,灵活选择Time类型。

图片

考虑到这里的问题就是跟心跳包和挥手包直接相关,排除掉无关报文,可以让我们的分析思路也变得更加清晰。所以,我们再一次调整过滤器,改为:

tcp.stream eq 0 and (tcp.len == 1 or tcp.flags.reset == 1)

就得到这个TCP流里面的心跳包和挥手包:

图片

我们最后再确认一下时间戳。这个隔了58秒才发出的心跳包,发送的时间是在17:08:10:

图片

跟日志中restart时间一致:

图片

现在,事实已经很清楚了:客户端在连接被断开之前的心跳探测包,并没有遵循客户声称的“每隔45秒”,而是很意外地隔了58秒。我们结合程序逻辑,已经可以推断出真实的状况了。看示意图:

结论:排除网络,追查代码

现在,我们已经清楚了这个报错的原因:客户端程序出了一个Bug,某一次心跳探测包发出的时候是在上一次心跳的58秒以后(也就是相对正常情况,迟了13秒)。那么,服务端虽然立即发送了心跳回复包,但也一样是在58秒以后了。程序的逻辑非常简单粗暴:一旦心跳回复包的到达时间超过了第46秒(也就是45秒+1秒超时),就认为是心跳探测失败。

为什么探测包本身会比预定的时间晚了13秒才发出呢?根据这个很明确的信息,客户再次检查了应用代码,终于定位到了出问题的代码段。修复代码后,问题随之解决。

补充一下,在这个案例中,异常时的心跳包探测和回复的耗时本身是正常的。我们可以看到包号572是客户端发出的心跳探测包,包号573是服务端发出的心跳回复包。两者之间,间隔只有0.000370秒,即0.37毫秒,完全是同机房内网时延的正常水平。

理解心跳机制的原理

显然,这个案例是关于应用层自己实现的心跳机制的,有一定的特殊性。但在机制上,也体现了心跳包的一些特点,比如:

  • 定时发送心跳探测包;
  • 对于心跳回复包有超时限制。

而从更普适的尺度上来看,其实TCP本身提供的Keep-alive机制更为安全易用。

一般来说,对于操作系统已经实现的特性,我们最好直接去利用,而不是自己创造一个类似的轮子。这好比你想基于UDP,在应用层实现类似TCP的种种传输保障机制,也不是不可以,但实现起来会相当复杂(参考QUIC协议)。TCP心跳机制看似简单,但从上面这个案例来看,稍有不慎,还是很容易发生错误的。

TCP Keep-alive

那么TCP自身的Keep-alive,究竟是怎样的一个存在呢?

其实,如果不做显式的配置,默认创建出来的TCP Socket是不启用Keep-alive的,也就是都不会发送心跳包。不过,大部分应用程序已经在代码里启用了Keep-alive,所以你平时不太会遇到连接失效的问题。比如我稍后要演示的一个含心跳包的抓包文件,抓取的就是Chrome浏览器的流量,里面就有很多心跳包,因为Chrome浏览器启用了TCP心跳保活机制。

要打开这个TCP Keep-alive特性,你需要使用setsockopt()系统调用,对已经创建的Socket进行配置,启用Keep-alive。具体的调用方法,你可以参考man setsockopt

在Linux操作系统层级,也有三个跟Keep-alive有关的全局配置项。

  • 间隔时间:net.ipv4.tcp_keepalive_time,其值默认为7200(秒),也就是2个小时。
  • 最大探测次数:net.ipv4.tcp_keepalive_probes,在探测无响应的情况下,可以发送的最多连续探测次数,其默认值为9(次)。
  • 最长间隔:net.ipv4.tcp_keepalive_intvl,在探测无响应的情况下,连续探测之间的最长间隔,其值默认为75(秒)。

补充:你可以在Linux系统里面,执行man tcp,查看内核对TCP协议栈的详细文档。这里我摘录一下关于Keep-alive的部分:
 

  • tcp_keepalive_intvl (integer; default: 75; since Linux 2.4)
     
    The number of seconds between TCP keep-alive probes.
     
  • tcp_keepalive_probes (integer; default: 9; since Linux 2.2)
     
    The  maximum number of TCP keep-alive probes to send before giving up and killing the connection if no response is obtained from the other end.
     
  • tcp_keepalive_time (integer; default: 7200; since Linux 2.2)
     
    The number of seconds a connection needs to be idle before TCP  begins  sending  out  keep-alive probes.   Keep-alives are sent only when the SO_KEEPALIVE socket option is enabled.  The default value is 7200 seconds (2 hours).  An idle connection is terminated after approximately an  additional 11 minutes (9 probes an interval of 75 seconds apart) when keep-alive is enabled.

如果我们连接启用了Keep-alive,但没有设定自定义的数值,那么就会使用上面这些默认值,即:当连接闲置(没有数据交互)达到7200秒(2小时)时发送心跳包,每次心跳包超时时间为75秒,最多重试9次。

这样的话,对于一个已经失效的TCP连接,最大需要7200+75*9=7875秒(约等于2小时11分钟)才能探测到。

毫无疑问,这个时间是相当长的。不过结合时代背景,这个其实也可以理解:TCP Keep-alive被设计的时候是八十年代,当时因特网还很初级,所以设计者们并不想让心跳包占据太多的网络资源。从而,就有了这么一个感知时间很长的心跳机制。关于TCP Keep-alive的一些更多信息在RFC1122里,你有兴趣的话可以去研究一下。

另外一个值得注意的地方,是Keep-alive报文本身的特点。在上面的案例中,这个特定的应用层代码设定的心跳包特征是这样的:

  • 心跳探测包,载荷为1个字节,其值为01。
  • 心跳回复包,载荷也为1个字节,其值为41。

但是TCP本身提供的Keep-alive报文特征就非常不同了。首先,它的序列号就很奇特,是上一个报文的序列号减1,载荷为0。回复的报文也同样特别,确认号为收到的序列号加1。而且,无论是探测包还是回复包,其载荷长度都为0

文字描述不是很容易理解,我给你看一个实际的例子。这是某一次我用Chrome浏览器访问网站时做的抓包:

图片

上图的红色底色的多个报文,就是Wireshark识别出来的TCP心跳包。我也把需要关注的信息用红色方框标注出来了。

25号报文是离心跳包最近的一个常规报文,Wireshark告诉我们:它的下一个序列号(图中的NextSeq)是1578。也就是说,如果下一个是常规报文,那么这个常规报文的序列号就是1578。然后看同是这个客户端发出的报文27,这就是一个心跳包,它的序列号却是1577(也就是1578-1),载荷为0(Len=0)。对端对这个心跳包做了回应(包号28),确认号为1578(1577+1),载荷也为0。

我们再来看一下示意图:

要是你了解TCP握手和挥手阶段的确认号的话,你对这个+1机制是不是感觉很熟悉?可见,TCP认为心跳包也是十分重要的,它跟握手和挥手一样,都属于控制报文,它的确认号机制也体现了这一特点。

有趣的是,RFC1122里并没有规定心跳探测包的载荷一定是0,它也可以是1。只是从我有限的抓包经验来看,心跳包都是载荷为0的,看来这是比较常见的实现方式。

HTTP Keep-alive

那么我们也经常听到的HTTP Keep-alive,又是一个什么东西呢?难道是应用层实现的心跳保活机制?意味着HTTP也有心跳包这种东西吗?

其实没有那么复杂。HTTP的Keep-alive,是用Connection这样一个HTTP header来实现的。你应该知道,HTTP报文的header形式是Key: value。比如常见的header有:

  • User-Agent: curl/7.68.0 (客户端发出)
  • Host: www.baidu.com (客户端发出)
  • Content-Type: text/html (服务端发出)
  • Server: bfe/1.0.8.18 (服务端发出)

而Keep-alive头部,就是这个形式:Connection: Keep-alive(注意这里有一个“-”符号)。

客户端和服务端都可以发送这个保活头部。表达的意思也跟外交人员的语言一样优雅专业:“我方真诚地希望,贵方能切实履行我们的协议,按照长连接待遇来处理本次连接,谢谢配合”。

你应该也知道,HTTP的版本有0.9、1.0、1.1、2.0。HTTP/0.9现在基本不再使用了,而HTTP/1.0占的流量比例也已经很低了(在公网上小于1%)。目前占主流的是HTTP/1.1和HTTP/2,而这两者都默认使用长连接

那既然HTTP/1.1默认是长连接了,为什么还要有这个Connection头部呢?在我看来,这有两个原因。

  • 协议兼容性

在HTTP/1.0版本中,默认用的是短连接。这导致了一个明显的问题:每次HTTP请求都需要创建一个新的TCP连接。随着因特网带宽和各类资源迅速增长,每次建立TCP连接的开销变成了主要矛盾。

为了克服这个不足,Connection: Keep-alive头部被扩充进了HTTP/1.0,用来显式地声明这应该是一次长连接。当然,从HTTP/1.1开始,长连接已经是默认设置了,但为了兼容1.0版本的HTTP请求,Connection这个头部在HTTP/1.1里得到了保留。

  • 关闭连接的现实需求

即使在HTTP/1.1里,也并非所有的长连接都需要永久维持。有时候任意一方想要关闭这个连接,又想用一种“优雅”(graceful)的方式,那么就可以用 Connection: Close 这个头部来达到“通知对端关闭连接”的目的,体面而有效。另外,在HTTP/2里,连接关闭是通过另外的机制实现的,与Connection头部无关。

这个Connection头部看似简单,其实有时候也能起到很大的作用。在eBay,我们每年有大量的流量迁移的工作。其中,这个Connection头部就在迁移的平滑度和收敛速度上,起到了很关键的作用。

具体来说,我们在新老两个VIP之间迁移流量,用的是名称解析(基于DNS/GSLB)的返回值的比例,来控制真实的HTTP流量比例,这就可以逐步从新老比例为1:99,到50:50,最后到100:0也就是全部到新VIP。

但是这样会有这么个问题:因为流量基本都是HTTP/1.1(即默认是长连接),客户端依然坚持使用着老的VIP,造成DNS/GSLB的比值调整没有起到应有的效果,真正观察到的流量经常跟DNS/GSLB的设置值相差甚远,或者说有很强的滞后性

从上图中可以看到,客户端在查询GSLB(相当于智能DNS)的时候,拿到的新老IP的比例为2:2,那么访问量应该也是符合这个比例。但是因为长连接的存在,这些拥有长连接的客户端连问都不会去问GSLB,而是会继续往老的连接(也就是连着老VIP的连接)上发送流量,那么我们迁移流量的工作,就受到影响了。

为了解决这个问题,我们当然可以选择等待(比如几小时甚至几天)它自然收敛,但其实我们找到了更好的办法:在新老VIP上都添加了rewrite policy,使得每一定比例的HTTP响应里面,都带上Connection: Close这个头部。

客户端收到这个头部后,按照协议规定,它必须关闭这条长连接。在下一次HTTP请求的时候,客户端就会遵循“DNS解析->发起新连接->发送HTTP请求”这样的工作路径,于是新发起的连接数跟DNS解析数量基本对齐,也就达到了我们的目的。

下图是在迁移尾声时候的流量趋势图。在这个阶段,老VIP已经从DNS/GSLB里禁用了。但原先不插入这种Connection: Close头部的话,总还是有很多请求在老的VIP上。在插入了Connection: Close头部后,老VIP上的流量几乎立刻停止,可谓立竿见影。

图片

小结

这节课,我们通过一个奇特的案例,详细探究了一种应用层保活机制的Bug引发的报错。在这个排查过程中,Wireshark过滤器的使用,很大程度上帮助了这次排查。所以,这里我推荐你要熟悉以下这些过滤器,在你以后的网络排查工作中,应该能给到不少的帮助:

tcp.len eq 长度
tcp.flags.fin eq 1
tcp.flags.reset eq 1
tcp.payload eq 数据

抓包分析中,面对Wireshark里千奇百怪的报文,有时候也会遇到不知道从何下手的窘况,那么你可以直接查看Expert Information,从那里寻找线索也不失为一个有效的办法。

另外,在原理部分,我还给你介绍了TCP层面的Keep-alive和HTTP层面的Keep-alive的联系和区别。应该说,这确实是两个容易令人困惑的概念,不仅名称一样,作用也接近。通过这次讲解,希望能帮助你彻底理解这两个概念。

最后,我再给你梳理提炼一下这节课的关键知识点。

首先,对于TCP Keep-alive,你需要掌握:

  • 默认TCP连接并不启用Keep-alive,若要打开的话要显式地调用setsockopt(),来设置保活包的发送间隔、等待时间、重试个数等配置。在全局层面,Linux还默认有3个跟Keep-alive相关的内核配置项可以调整:tcp_Keepalive_time,tcp_Keepalive_probes,还有tcp_Keepalive_intvl。
  • TCP心跳包的特点是,它的序列号是上一个包的下个序列号-1,而心跳回复包的确认号是这个序列号-1+1(即还是这个序列号)。

然后,对于HTTP Keep-alive的知识点,你需要理解:

  • HTTP/1.0默认是短连接,HTTP/1.1和2默认是长连接。
  • Connection: Keep-alive 在HTTP/1.0里,能起到维持长连接的作用,而在HTTP/1.1里面没有这个作用(因为默认就是长连接)。
  • Connection: Close 在HTTP/1.1里,可以起到优雅关闭连接的作用。这个头部在流量调度场景下也很有用,能明显加快基于DNS/GSLB的流量调整的收敛速度。

思考题

最后再给你留两道思考题:

  1. tcp.payload eq abc,这个过滤器可以搜索到精确匹配“abc”字符串的报文。那么,如果是模糊匹配,比如只要包含“abc”的报文我都想搜到,这个过滤器又该如何写呢?
  2. 在工作中,你遇到过跟TCP Keep-alive相关的问题吗?你是怎么解决的呢?

欢迎你把答案分享到留言区,我们一起进步成长。

附录

抓包示例文件:https://gitee.com/steelvictor/network-analysis/tree/master/07

精选留言

  • 江山如画

    2022-01-26 19:58:27

    问题1:
    方法1: frame contains abc
    方法2: ip contains abc
    方法3: tcp contains abc
    方法4: http contains abc
    方法5: tcp.payload contains 61-62-63


    问题2:
    工作中目前还没有碰到过和 tcp keep-alive 有关的问题,不过之前看一些建立网络隧道的软件有 keep-alive 这个参数,查询说和 SNAT 有关,一值很迷惑。
    今天学习了老师的课又深究了下,virtual tunnel 会建立虚拟网卡,和物理网卡之间需要做流量的桥接,进而需要做 SNAT,SNAT 会维护一个端口映射表,因为链接太多可能会占满本地端口,如果没有设置 keep-alive,在一段时间内都没有传输数据,会把端口转换的记录给删掉,这时候通信双方想再通信就不行了。所以需要每隔一段时间发送心跳包,保证 SNAT 端口映射表中的记录不被删掉,从而保证连接存活。


    总结二者的区别:
    1. http keep-alive 是七层特性,tcp keep-alive 是四层特性
    2. http keep-alive 旨在较短时间内,复用 tcp 链接,快速传输大量数据,减少重复握手挥手带来的时延消耗和网络拥堵,比如用浏览器访问一个带有大量 js,css 或图片的网页;
    tcp keep-alive 旨在相对较长的一段时间内,检查 tcp 链接是否存活,它的传输频率很低,传输数据量也很小,感觉类似网络隧道软件,语音,聊天软件中可能会用到 tcp keep-alive。
    3. http keep-alive 的内部实现不依赖于 tcp keep-alive, 二者实现上没有关系。
    作者回复

    你回复的很全面,给你点赞!
    如你所述,NAT一般会维护一个连接映射表,而这个表里的表项是有有效期的,也就是过了一定的时间就从表里删除,那么后续两端在这个连接上的报文经过NAT的时候,因为找不到表项,就被判定为无效报文,就会被丢弃。

    2022-01-26 22:48:33

  • Geek_535c45

    2022-03-07 14:09:09

    老师好,关于TCP的keep-alive,有个疑惑
    1、是否开启keep-alive 取决于客户端程序是否调用setsockopt(),而与服务端(被连接方)无关 ?
    2、Linux 上的net.ipv4.tcp_keepalive 参数是控制运行在linux上的客户端进程和目标服务之间的长连接保活的参数,与服务器端(假设也为linux)设置的net.ipv4.tcp_keepalive 参数无关 ?
    3、HTTP keep-alive 是否依赖TCP keep-alive,如果HTTP的 keep_alive_request_time 比TCP 的keep-alive 时长配置的长,HTTP在开启长连接的情况下客户端请求发起一次之后中间就一直没有再请求,中间这个间隔时长大于了TCP 的keep-alive,是否会中断?
    作者回复

    您好,这几个问题质量很高,我说下自己的理解:
    1. TCP是全双工的协议,所以两个方向都可以自己选择是否要做keep-alive。如果客户端setsockopt()启用了Keep-alive,那客户端会定时发送保活报文,服务端会回复ack(这个是必做的,跟服务端是否启用保活没有关系)。如果服务端也开启了TCP keep-alive,那就相互发送保活报文。但是,考虑一方的idle timeout会随着另一方发送的保活报文而被刷新,所以是相互有影响的。
    比如,客户端10分钟发一次保活,服务端是15分钟发一次保活。那么在第10分钟时,客户端发送了报文,对服务端来说,因为此时接收到了报文,所以idle timeout就被刷新为0了,服务端计划等到第25分钟再发它自己的保活报文。实际上在第20分钟,客户度再次发送保活报文,服务端的idle timeout再次被刷新,所以服务端就不再发送它这端的保活报文了,因为没有机会等满15分钟空闲。

    2. 跟1类似,因为是两个方向上自己的选择,所以双方依然可以各自设置保活参数。但实际上我们只会看到间隔更小的那个保活报文。

    3. HTTP Keep-alive是告知对方:“我想这次用TCP长连接”,它本身不是定时周期性发送的保活报文。如果HTTP请求耗时超过了TCP保活的时长,那TCP保活报文会按时发送TCP保活报文并得到服务端的ack,这跟HTTP响应什么时候回复出来不矛盾,也无关。

    我们还发现,有些web服务器会选择在一个长连接服务了一定数量的HTTP请求之后(比如1000个),选择发送一次Connection:close给客户端,让客户端结束这次连接,这给了客户端一次重新选择服务端的机会(比如LB会新选一个后端服务器给它)。

    2022-03-08 00:13:30

  • 那时刻

    2022-01-26 10:39:28

    请问老师,例子中的心跳消息显示push ack是为何呢?
    作者回复

    你的疑问是为什么有PSH标志位吗?按照TCP协议(参考RFC793)规定,操作系统收到有PSH标志位的报文后,这些报文不会被驻留在接收缓冲区,而是要立即通知用户空间程序来接收。这样对于用户空间程序及时收发处理心跳报文是有利的。

    2022-01-27 00:02:26

  • webmin

    2022-01-26 12:48:11

    为什么要心跳检查,因为目前讨论的数据连接场景,都是无源连接,排除NAT的情况,连接就是存在于src和dest两端OS中的状态机,为什么会要用无源连接呢,有源是连接建立带宽就分配好了,不传有效数据这个带宽也被占用着,这不就浪费了,虚拟信号时代的电话就是有源的。
    心跳检查是两端都要做的,不做的那一端一样存在状态不对而不自知的情况。
    作者回复

    没错,状态机在两端是有可能不一致的,比如一端认为这条连接已经销毁,另外一端可能认为仍有效。心跳机制的作用之一就是解决这种不一致的情况,类似“校对”的作用。

    2022-01-27 01:23:37

  • Cavalier

    2022-04-02 12:10:05

    老师您好,我有一台设备网络会时不时就断,但是只要上面一直跑着ping就不会断开。这个有什么思路?
    作者回复

    你说的断开,有没有抓到过TCP报文?是收到了RST还是什么呢?RST报文的TTL跟正常连接的TTL有什么不一样吗?

    有一个可能性你路径上有防火墙或者NAT,它们会对TCP连接信息进行一定时间的缓存,如果你的长连接中的报文隔了很久才传送,此时可能防火墙/NAT上这个连接的信息已经没有了。

    一直跑着ping,虽然跟TCP连接没有直接关系,但不排除这个中间设备会因为持续ping的存在,也把这个源IP相关的TCP连接做了维持。这只是我的一个猜测,供参考~

    2022-04-04 11:57:30

  • 天问

    2022-01-26 13:30:46

    tcp contains "abc"显示包含abc的数据包
    作者回复

    正确:)

    2022-01-26 18:53:09

  • LEON

    2022-08-03 11:11:54

    您好,这句话我感觉描述的有问题-----------”客户端收到这个头部后,按照协议规定,它必须关闭这条长连接。在下一次 HTTP 请求的时候,客户端就会遵循“DNS 解析 -> 发起新连接 -> 发送 HTTP 请求”------我理解Connection: Close的调整只会影响HTTP的行为,和DNS 无关,如果客户端LDNS或本地有缓存,也不会重新到GSLB发起新的解析,会基于缓存的老的VS地址链接到老的VS上,从而影响切换。
    作者回复

    谢谢您的意见。总的来说,“在下一次 HTTP 请求的时候,客户端就会遵循“DNS 解析 -> 发起新连接 -> 发送 HTTP 请求”这一句没有太大问题。这里说的“DNS解析”可以是从本地解析缓存中获取,也可以到DNS服务器上获取,这都属于解析行为。
    不同的操作系统行为不完全一样。windows默认启用dns client service,它会缓存DNS解析记录。linux默认没有dns缓存,但是可以通过安装nscd或者dnsmasq来实现类似的解析缓存。
    在ebay的环境里,启用和未启用本地解析缓存的情况都存在。对于没有解析缓存的情况,Connection:close起效快;如果有缓存,起效会慢一点,但最差情况也能在DNS record TTL时间后发起新的解析请求。
    当然,这里的讨论没有考虑应用程序做解析缓存的情况(包括jvm也有相关的dns解析缓存的配置),否则可能文章写不完:)

    2022-08-04 00:39:03

  • Realm

    2022-01-26 08:00:04

    1 tcp.payload include abc
    2 我们遇到一个web应用,在浏览器上请求一个需要长时间才得到结果的请求,最后返回超时错误,估计被中间网络设备给reset掉了.
    作者回复

    1. 意思对了:) 表达式是tcp.payload contains "abc"
    2. 你说的情况我们经常遇到,我估计你说的就是客户端自己的超时设置,也就是如果http响应无法在限定时间内完成(比如1秒内),那么客户端应用程序自己会报timeout错误。这应该不是中间设备做了reset,因为如果是那样,这个中间设备应该会给客户端和服务端都发TCP RST,然后客户端报错会变成ECONNRESET这种(ECONNRESET是linux的网络协议栈报给用户空间程序的报错)

    2022-01-27 00:14:13

  • 文远

    2025-06-11 21:15:23

    我理解心跳包机制,是不是可以应用在游戏加速器中:自动切换节点能保证游戏不断线,每隔10秒ping节点,如果没有没有反应,自动切换节点。
  • Yuri

    2025-01-02 17:31:05

    老师好,关于插入Connection: Close解决流量收敛的问题,我有个疑问,插入了Connection: Close这个操作是临时的吗?在流量收敛后会移除吗?因为我担心插入这个header会影响客户端的连接重用的行为,使得每次都需要重新创建连接,开销会比较大。
    作者回复

    你能提出这个问题说明有去思考,很好。这个header插入的行为是短时间的,等流量收敛后就去掉了,这样就不继续对客户端产生影响。其实很多选择是权衡,在收敛阶段,尽快收敛的收益超过客户端增加的一定程度的计算开销,而收敛完成后,不存在收敛收益了,那么客户端开销又变得重要了,就要选择去掉这个header。

    2025-04-05 11:59:24

  • Wade_阿伟

    2024-05-07 20:15:00

    分享一个之前在工作场景中遇到的关于TCP Keep-alive的问题。公司是国内其中一家手机厂商,业务是push场景。一次因为机器上的TCP Keep-alive间隔参数设置的太小,从而导致用户手机端耗电明细增加。经过排查,将net.ipv4.tcp_keepalive_time设置为合理的值后,问题得到解决。
    作者回复

    谢谢分享!你说的net.ipv4.tcp_keepalive_time是在服务端对吗,也就是服务端往客户手机发送TCP keepalive报文过频,导致客户端手机必须频繁回应,增加耗电。是一个有意思的案例!

    2024-10-22 00:01:12

  • Summer

    2023-11-06 15:51:26

    老师,问下 Delta time 和Delta time displayed 的区别和应用场景各是什么呢
    Delta time displayed的意思是[Time delta from previous displayed frame],直观上看到后者在应用过滤器之后会发生变化
    作者回复

    您好,其实很简单。
    delta time displayed:只看显示出来的(displayed)报文之间的delta time
    delta time: 完整报文之间的delta time,无论前一个报文是否被显示
    我们假设一个场景,一个抓包文件里就3个报文,分别是:
    报文编号 报文抓取时间
    报文1:time 0.000秒
    报文2:time 0.500秒 delta time 0.500
    报文3:time 0.800秒 delta time 0.300

    以上是delta time的情况。
    如果应用了某个filter后只有报文1和3被显示,那么报文3的delta time (dispalyed)就变成了报文3和1之间的间隔,也就是0.800秒;而它的delta time不随filter变化而变化,恒定是0.300秒,无论报文2是否被显示。

    2024-10-20 15:03:04

  • Ironshuai

    2023-09-13 21:57:20

    老师问个k8s pod优雅删除的问题:
    当Pod处于Terminating,kube-proxy 删除NAT规则,将 Pod 从 service 的 endpoint 列表中摘除掉,新的流量不再转发到该 Pod。此时,已经建立的连接受影响不,数据包能正常从service到这个即将删除的Pod,还能正常回来不?
  • Geek_49b49f

    2023-09-06 20:55:26

    如何在vip级别上插入connection close 头部呢?
    作者回复

    负载均衡(有时候叫做应用网关、反向代理,其实都是差不多的)一般都带有设置第七层规则的能力。在这里,就是需要插入一个HTTP header,其键是Connection,值是Close。
    Connection: Close

    具体做法取决于你用的是哪一种产品,查阅相应的产品文档即可。

    2023-09-17 15:59:04

  • Geek_02db84

    2022-12-06 16:44:51

    老师好,案例中用到的数据包方便贴上来,我们自己实践吗
  • 斯蒂芬.赵

    2022-12-02 01:09:34

    看完还是没
    明白心跳包为啥异常?能说一下具体跟因是啥么
    作者回复

    跟你另外一个问题类似,参考那个的答案:)

    2022-12-04 00:05:21

  • 斯蒂芬.赵

    2022-12-02 00:33:47

    为什么探测包本身会比预定的时间晚了 13 秒才发出呢?根据这个很明确的信息,客户再次检查了应用代码,终于定位到了出问题的代码段。修复代码后,问题随之解决。
    想问一下老师这个问题的原因是啥,客户端为啥完了13秒,客户端的代码有啥问题?根因是啥?
    作者回复

    这就是代码方面的问题了,跟我们的网络排查的主题离的有点远,这是一个原因。
    第二个原因,这个问题算是个例,代码上的错误也不是很复杂,不具有很好的普遍性。
    我们应该掌握的是分析的思路和方法,以后能使用这种方法,不止解决一个问题,而是一类问题都能解决。

    2022-12-03 11:48:21

  • 上杉夏香

    2022-08-23 15:09:47

    知晓了TCP keep-alive时心跳包的特点,老师的『TCP 心跳包的特点是,它的序列号是上一个包的序列号 -1』,是不是应该改成『它的序列号应该是此次普通包的序列号-1,而不是上一个包的序列号-1』
    作者回复

    谢谢!你看的很仔细。应该是“它的序列号是上一个包的(下个)序列号-1”的,不知为何疏漏了“下个”这两个字,我们来纠正下。

    2022-08-25 01:00:03

  • piboye

    2022-08-02 19:59:49

    nginx 优雅重启是不是也可以达到 connection:close 的效果?
    作者回复

    有相似的地方:都可以让客户端断开长连接并重新做DNS解析,使得客户端可以获取到更新过的地址。

    不同的地方:插入connection:close头部这个操作是可以在VIP级别做的,也就是不影响其他VIP(我们一台LB上可能有几百个VIP)。而nginx优雅重启是会影响到所有的VIP的。

    2022-12-03 23:42:00

  • JianXu

    2022-03-20 05:50:29

    老师,能帮忙扫盲一下 操作系统时间配置是怎么影响网络传输的吗