TCP & HTTP的keep-alive的详解

TCP协议keepalive


概述

TCP keepalive在很多场景下都不是必须的,但是在某些特定的的场景下,这个特性却会是非常有用。我们从TCP keepalive这个名字就可以大概理解这个特性的作用:keep tcp alive。我们可以通过检查socket来判断网络连接是不是正常的(Running or Broken)

Keepalive的概念非常的简单:当你建立一个TCP连接的时候,便有一组定时器(timer)与之绑定在一起,其中的一些定时器就用于处理keepalive过程。当keepalive定时器到0的时候,我们便会给对端发送一个不包含数据部分的keepalive探测包,然后打开ACK标志。我们可以这样做(发送一个不包含数据部分的数据包)是因为TCP/IP协议里面有重复确认(duplicate ACK)机制,而且由于TCP是基于流的协议,所以对端也是没有任何参数。另一方面,我们会受到对端(对端可以不支持keepalive选项,只要是TCP/IP就行)的确认消息,该消息也没有数据部分,只有ACK。
如果我们收到了keepalive探测包的回复消息,那么我们就可以断定连接依然是OK的,也不用担心用户层的实现、实际上,TCP允许我们处理没有数据包的流,而且数据长度为0的数据包对于用户程序来说也是没有太多的网络开销带来的影响。如果我们没有收到对端keepalive探测包的ACK消息,我们便可以断定连接已经不可用,进而采取一些措施,比如断开连接。keepalive除了会额外产生一些网络数据包外(这些包将加大网络流量,对路由器和防火墙造成一定的负担,但由于数据包本身比较小,对网络的影响然后再可控范围之内),其他并没有太多的影响

TCP Keepalive使用场景

检测对端的死链接

这里所谓的对端连接已经挂掉非两种场景:

  • 对端还没有来得及通知我们就已经死掉了。比如系统内核突然挂掉,或者进程被直接终止等。
  • 对端进程虽然是正常的,但是网络链路却出了故障。这种场景下,如果网络链路不恢复正常的话,对我们来说,对端依旧是挂掉的。
    在这两种场景下,对端在挂掉之前都是无法通知我们的,这些场景,一般的TCP操作是检测不出来连接状态的

我们假设一种A和B连接场景,参考下图:

 _____                                                     _____
|     |                                                   |     |
|  A  |                                                   |  B  |
|_____|                                                   |_____|
   ^                                                         ^
   |--->--->--->-------------- SYN -------------->--->--->---|
   |---<---<---<------------ SYN/ACK ------------<---<---<---|
   |--->--->--->-------------- ACK -------------->--->--->---|
   |                                                         |
   |                                       system crash ---> X
   |
   |                                     system restart ---> ^
   |                                                         |
   |--->--->--->-------------- PSH -------------->--->--->---|
   |---<---<---<-------------- RST --------------<---<---<---|
   |                                                         |

A和B已经通过三次握手建立了连接,此时我们认为连接已经稳定,我们可以在这条链路上发送数据包了。但此时突然发生了一个意外:B端机器突然断电,而B还没有来得及通知A连接出问题了。而再看此时的A端,A已经准备好接受B端发来的数据,却根本不知道B端已经crash了。此时,我们在恢复B端的电源等待系统重启,这时的状态就是A和B都正常运行,并且A知道它和B之间有一条已经建立好的连接,但是B却不知道,这个时候,如果A试图通过这个连接向B发送数据,B将回复一个RST数据包(在一个已关闭的socket上收到的数据时,将发送RST数据包,要求对端关闭异常连接且对端不需要回复ACK),这样将导致A最终关闭这个连接,至此,这个死连接才算清理掉

Keepalive可以帮助我们判断出对端变得不可达,并且不会误报、实际上,如果是因为两段的网络导致的问题,keepalive会过一些时间再重试一下,多次尝试之后才会将这个连接标记为不可用

防止因为网络不活动而断连

keepalvie的另外一个目标就是防止因为网络不活动而断开网络连接。当我们在NAT代理或者使用防火墙的时候,经常会出现这种问题。这是由于NAT代理和防火墙内部的实现导致的:NAT代理和防火墙一般会记录所有通过他们的连接,但是由于机器的物理资源限制,它们只能在内存中保存数量有限数量的连接。最常见的策略就是保持最新的连接,丢弃掉老的或者不活动的连接。
我们再来看以下的实际场景:

 _____           _____                                     _____
|     |         |     |                                   |     |
|  A  |         | NAT |                                   |  B  |
|_____|         |_____|                                   |_____|
  ^               ^                                         ^
  |--->--->--->---|----------- SYN ------------->--->--->---|
  |---<---<---<---|--------- SYN/ACK -----------<---<---<---|
  |--->--->--->---|----------- ACK ------------->--->--->---|
  |               |                                         |
  |               | <--- connection deleted from table      |
  |               |                                         |
  |--->- PSH ->---| <--- invalid connection                 |
  |               |                                         |

A和B已经通过三次握手建立了稳定的连接,但是在较长时间间隔之后,A和B会实际向对方发送数据,此时,A和B的连接是有效的(建立连接后,如果不断开,则该连接一直保持)。此时如果发生了以下两种场景会导致连接断开

  • 代理或者防火墙的有限的连接满了(该连接已经在他们的内存中被新的连接淘汰掉了)。当A发出数据后,代理将不能正确处理我们的数据,最终导致连接断开
  • 过了很长时间,没有通过该连接发送数据,代理或者防火墙由于连接的超时机制将该连接淘汰掉。当A发出数据后,代理将不能正确处理我们的数据,最终导致连接断开

Linux下相关的内核参数

tcp_keepalive_time
表示TCP连接在多少秒之后没有数据报文传输时启动探测报文(发送空的报文,单位秒)

sysctl -n net.ipv4.tcp_keepalive_time
1200

tcp_keepalive_intvl
表示前一个探测报文和后一个探测报文之间的时间间隔,单位秒

sysctl -n net.ipv4.tcp_keepalive_intvl
75

tcp_keepalive_probes
表示探测的次数

sysctl -n net.ipv4.tcp_keepalive_probes
9

上述3个参数,结合起来解释有如下的关系:
当网络两端建立了TCP连接之后,闲置 idle(双方没有任何数据流发送往来)了tcp_keepalive_time秒后,服务器内核就会尝试向客户端发送侦测包,来判断TCP连接状况(有可能客户端崩溃、强制关闭了应用、主机不可达等等)。如果没有收到对方的ack,则会在tcp_keepalive_intvl后再次尝试发送侦测包,直到收到对对方的ack ,如果一直没有收到对方的ack ,一共会尝试tcp_keepalive_probes次,如果依然没有收到对方的ack包,则会丢弃该TCP连接。

Nginx 当中的 TCP keepalive 配置

Nginx涉及到TCP层面的keepalive只有一个:so_keepalive,它属于listen指令的配置参数,具体配置如下:

so_keepalive=on|off|[keepidle]:[keepintvl]:[keepcnt]

this parameter (1.1.11) configures the “TCP keepalive” behavior for the listening socket. If this parameter is omitted then the operating system’s settings will be in effect for the socket. If it is set to the value “on”, the SO_KEEPALIVE option is turned on for the socket. If it is set to the value “off”, the SO_KEEPALIVE option is turned off for the socket. Some operating systems support setting of TCP keepalive parameters on a per-socket basis using the TCP_KEEPIDLE, TCP_KEEPINTVL, and TCP_KEEPCNT socket options. On such systems (currently, Linux 2.4+, NetBSD 5+, and FreeBSD 9.0-STABLE), they can be configured using the keepidle, keepintvl, and keepcnt parameters. One or two parameters may be omitted, in which case the system default setting for the corresponding socket option will be in effect. For example,

下面是一个配置实例

so_keepalive=30m::10

will set the idle timeout (TCP_KEEPIDLE) to 30 minutes, leave the probe interval (TCP_KEEPINTVL) at its system default, and set the probes count (TCP_KEEPCNT) to 10 probes.

Nginx 的实现代码,可以参考: http://hg.nginx.org/nginx/file/tip

HTTP Keep-Alive


概述

HTTP keep-alive在我们平时工作中可能知道的相对多一些,但是到底它和TCP的keepalive有什么区别了?

短连接 & 长连接 & 多连接

  • 短连接:每次请求一个资源就建立连接,请求完成后连接立马关闭。每次请求都经过"创建TCP连接->请求资源->响应资源->释放连接"这样的过程
  • 长连接(persistent connection):只建立一次连接,多次资源请求都复用该连接,完成后关闭。例如,有一个http请求一个页面上的十张图,只需要建立一次tcp连接,然后依次请求十张图,等待资源响应,释放连接
  • 多连接(multiple connections):并发的短连接
    下面的图比较了多连接和长连接的区别:


    http-connection-comparation.png

Keep-Alive

Http协议通过如下的规范来将短连接转变成长连接:

  • client在Request http header中增加Connection: Keep-Alive。在HTTP/1.0协议中,需要显式的在request http header中增加Connection: Keep-Alive,而在HTTP/1.1以上默认就是开启的
  • server如果能够识别Connection: Keep-Alive字段,就会response的http header中返回Connection: Keep-Alive,告诉客户端,服务端能支持keep-alive服务,并且服务端暂时不会关闭socket连接
  • 如果需要关闭连接时,会在http header中指定Connection: Close

Nginx相关的配置

  • keepalive_timeout
Syntax: keepalive_timeout timeout [header_timeout];
Default:    keepalive_timeout 75s;
Context:    http, server, location
The first parameter sets a timeout during which a keep-alive client connection will stay open on the server side. The zero value disables keep-alive client connections. The optional second parameter sets a value in the “Keep-Alive: timeout=time” response header field. Two parameters may differ.
  • keepalive_requests
Syntax: keepalive_requests number;
Default:    keepalive_requests 100;
Context:    http, server, location
Sets the maximum number of requests that can be served through one keep-alive connection. After the maximum number of requests are made, the connection is closed

HTTP Keeep-Alive启用后带来的问题

启用Keep-Alive,可以避开缓慢的三次握手,还可以避免遇上TCP慢启动的拥塞适应阶段,能够提升性能。但是却会引入额外的问题。
让我们使用node.js编写一个demo来进行一下验证。

require('net').createServer(function(sock) {
    sock.on('data', function(data) {
        sock.write('HTTP/1.1 200 OK\r\n');
        sock.write('Connection: keep-alive\r\n');
        sock.write('\r\n');
        sock.write('hello world!');
        sock.destroy();
    });
}).listen(9090, '127.0.0.1');

我们使用node.js的net包去构建了一个最简单的http服务器,此时我们通过浏览器访问http://localhost:9090 ,发现能够得到正确的输出。另外我们也可以验证出,HTTP/1.1协议中,默认的http request请求是带有 Connection: keep-alive http header的。
当我们去掉上面的代码中sock.destory(); 时,以模拟一个HTTP的持久连接,这时我们会发现,浏览器一直处于Pending状态,无法正确的返回结果。这是因为,对于非持久连接(短连接),浏览器可以通过连接是否关闭来界定请求或响应实体的边界;而对于持久连接,这种方法显然不奏效。因为尽管服务器端已经发送完所有的数据,但浏览器并不知道这一点,它无法得知这个打开的连接上是否还会有新数据进来,只能一直处于等待状态。

Content-Length

要解决上述的问题,我们需要有一种协商机制,用于服务器端告知客户端,已经完成了数据的传输。比如我们可以通过返回http body的长度来让客户端判断,是否数据已经传输完成了。HTTP协议中的 Response Header:Content-Length 用于标识Body的实际长度。

让我们来改造之前的 node.js 代码

require('net').createServer(function(sock) {
    sock.on('data', function(data) {
        sock.write('HTTP/1.1 200 OK\r\n');
        sock.write('Connection: keep-alive\r\n');
        sock.write('Content-Length: 12\r\n');
        sock.write('\r\n');
        sock.write('hello world!');
    });
}).listen(9090, '127.0.0.1');

我们增加了Content-Length的response header,并将其值赋值为“hello world!”的字符串长度(12),此时浏览器是能够正常 work 的,因为浏览器可以通过Content-Length的长度信息,判断出响应实体已结束。

那么如果我们将长度计算错误了会出现什么问题了?我们来验证下

Content-Length比实际长度小

Content-Length修改为10之后,response body 的内容被截断了2个长度的内容,即:”hello worl” 。

Content-Length比实际长度大

Content-Length 修改为13之后,会发现浏览器又一直处于Pending状态,原因是客户端认为Body的长度小于
Content-Length,内容还没有发送完,因此傻傻再等着服务器端发送剩余的一个字节内容。

Content-Length的其他问题

由于 Content-Length 字段必须真实反映 HTTP Body 的实际长度,但某些场景下,实际长度无法进行计算,例如 HTTP Body 在服务器端动态的生成。这时候要想准确获取长度,只能在内核中开启一个足够大的 buffer ,等内容全部生成好再计算。但这样做一方面需要更大的内存开销,另一方面也会让客户端等更久。

我们在做 WEB 性能优化时,有一个重要的指标叫 TTFB(Time To First Byte),它代表的是从客户端发出请求到收到响应的第一个字节所花费的时间。比如 chrome 浏览器的 Network 面板都可以看到每一个 HTTP 请求的 TTFB,越短的 TTFB 意味着用户可以越早看到页面内容,体验越好。
服务端如果为了计算响应实体长度而缓存所有内容,就会与更短的 TTFB 时间背道而驰。此外,HTTP Body 一定要在 Header 之后,顺序不能颠倒,为此我们需要一个新的机制:不依赖头部的长度信息,也能知道 HTTP Body 的边界。

Transfer-Encoding: chunked

为了解决上述Content-Length的相关问题,HTTP协议定义了一个新的HTTP Header:Transfer-Ecoding,其中chunked表示分块编码

分块编码的规则是,报文中的实体需要改为用一系列分块来传输,每个分块包含十六进制的长度值和数据,长度值独占一行,长度不包括它结尾的CRLF(\r\n),也不包括分块数据结尾的CRLF。最后一个分块长度值必须为0,对应的分块数据没有内容,表示实体结束

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,123评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,031评论 2 384
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,723评论 0 345
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,357评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,412评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,760评论 1 289
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,904评论 3 405
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,672评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,118评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,456评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,599评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,264评论 4 328
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,857评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,731评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,956评论 1 264
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,286评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,465评论 2 348