Nginx篇05——http长连接和keeplive

nginx中http模块使用http长连接的相关配置(主要是keepalive指令)和http长连接的原理解释。

1、http长连接

1.1 预备知识

连接管理是一个 HTTP 的关键话题:打开和保持连接在很大程度上影响着网站和 Web 应用程序的性能。在 HTTP/1.x 里有多种模型:短连接, 长连接, 和 HTTP 流水线。在解释这三种模型之前,我们需要先明确一些前提知识:

  • HTTP是属于应用层(七层)的协议,同时它的传输层(四层)使用的是TCP协议,那么也就是说,HTTP的长连接和短连接,其本质就是TCP的长连接和短连接;
  • HTTP是一个无状态的面向连接的协议(使用TCP,面向连接、可靠传输),指的是协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态,无状态不代表HTTP不能保持TCP连接,更不能代表HTTP使用的是UDP协议(无连接);
  • TCP建立连接和断开连接是需要三握四挥的,由于这个属于计算机网络基本知识,所以原理这里不再赘述;

接下来我们开始解释。

1.2 HTTP短连接模型

在早期,HTTP 使用一个简单的模型来处理这样的连接。这些连接的生命周期是短暂的:每发起一个请求时都会创建一个新的连接,并在收到应答时立即关闭。这就是类似上面说的三次握手,在互联网发展的早期一个网页的资源并没有现在这么多,很多可能只是一个简单的静态页面而已,所以这样的模型显然很OK。客户端获取完所需资源之后,就断开连接,不再占用服务器的资源。

套用TCP连接的三握四挥的模型来举例:

三次握手:

A→B:今晚下班一起吃饭吗?

B→A:好的,今晚下班一起吃饭。

A→B:好的,我知道你答应我今晚下班一起吃饭的邀请了。


然后开始去吃饭,吃完饭到了两个人需要各自回家的时候:


四次挥手:

A→B:我吃完饭准备走了

B→A:等一下,我快吃完了

B→A:好了,我吃完了可以走了

A→B:好的,我知道你吃完了我们可以走了


然后两人吃完饭就各回各家了

HTTP 短连接模型是最早期的模型,也是 HTTP/1.0 的默认模型。每一个 HTTP 请求都由它自己独立的连接完成;这意味着发起每一个 HTTP 请求之前都会有一次 TCP 握手,而且是连续不断的。实际上,TCP 协议握手本身就是耗费时间的,所以 TCP 可以保持更多的热连接来适应负载。短连接破坏了 TCP 具备的能力,新的冷连接降低了其性能。

在 HTTP/1.0 中如果没有指定 Connection协议头,或者是值被设置为 close就会启用短连接模型,要在 HTTP/1.0 中启用长连接模型,需要在协议头中指定Connection: Keep-Alive ,不过并不建议这样操作。

而在 HTTP/1.1 中,默认使用长连接模型,只有当 Connection被设置为 close 时才会用到这个短连接模型,协议头都不用再去声明它(但是一般还是会把它加上,以防万一因为某种原因要退回到 HTTP/1.0 )。

1.3 HTTP长连接模型

后来,网页需要请求的资源越来越多,短连接模型显然已经十分吃力了。因为短连接有两个比较大的问题:创建新连接耗费的时间尤为明显(三次握手很耗费时间),另外 TCP 连接的性能只有在该连接被使用一段时间后(热连接)才能得到改善。因此在HTTP/1.1中引入了长连接模型和流水线模型。

在 HTTP/1.1 之前,长连接也被称为keep-alive 连接。

一个长连接会保持一段时间,重复用于发送一系列请求,节省了新建 TCP 连接握手的时间,还可以利用 TCP 的性能增强能力。当然这个连接也不会一直保留着:连接在空闲一段时间后会被关闭(服务器可以使用 Keep-Alive 协议头来指定一个最小的连接保持时间)。

套用上面的例子来进一步解释:

三次握手:

A→B:今晚下班一起吃饭吗?

B→A:好的,今晚下班一起吃饭。

A→B:好的,我知道你答应我今晚下班一起吃饭的邀请了。


然后开始去吃饭,但是这时吃完饭就不是马上四次挥手断开连接,AB两人还顺便去逛街、看电影(相当于省去了三次握手使用已建立的连接来传输多个资源)


此处省略四次挥手


最后两人就各回各家了

长连接也还是有缺点的;也就是前面提到的资源占用问题,就算是在空闲状态,它还是会消耗服务器资源,也更容易被DDoS攻击。本质上长连接是因为不断地三次握手建立连接消耗的资源要大于维持连接所需要的资源才使用的,如果服务器处于高负载时段或者被DDoS,可以使用非长连接,即尽快关闭那些空闲的连接,也能对性能有所提升。

1.4 HTTP流水线模型

流水线模型的实现要复杂很多,而已效果也并不是特别好,主要还要考虑到各种兼容性,所以默认是不启用这个流水线模型的,而在HTTP/2中,流水线已经被更好的算法给代替,如multiplexing

默认情况下,HTTP 请求是按顺序发出的。下一个请求只有在当前请求收到应答过后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。流水线是在同一条长连接上发出连续的请求,而不用等待应答返回。这样可以避免连接延迟。理论上讲,性能还会因为两个 HTTP 请求有可能被打包到一个 TCP 消息包中而得到提升。就算 HTTP 请求不断的继续,尺寸会增加,但设置 TCP 的 MSS(Maximum Segment Size) 选项,仍然足够包含一系列简单的请求。

并不是所有类型的 HTTP 请求都能用到流水线:只有 idempotent方式,比如 GETHEADPUTDELETE能够被安全的重试:因为有故障发生时,流水线的内容要能被轻易的重试,即出现了问题重试的成本要尽可能低,否则还不如使用长连接模型。

正确的实现流水线是复杂的:传输中的资源大小,多少有效的 RTT 会被用到,还有有效带宽,流水线带来的改善有多大的影响范围。不知道这些的话,重要的消息可能被延迟到不重要的消息后面。这个重要性的概念甚至会演变为影响到页面布局!因此 HTTP 流水线在大多数情况下带来的改善并不明显。此外,流水线受制于 HOL 问题。

摘自wiki

队头阻塞Head-of-line blocking或缩写为HOL blocking)在计算机网络的范畴中是一种性能受限的现象。它的原因是一列的第一个数据包(队头)受阻而导致整列数据包受阻。例如它有可能在缓存式输入的交换机中出现,有可能因为传输顺序错乱而出现,亦有可能在HTTP流水线中有多个请求的情况下出现。

我们还是使用上面的例子来进行解释,这次的握手请求就变了,A一次向B发出了三个请求:

三次握手:

A→B:今晚下班一起吃饭、逛街、看电影吗?

B→A:好的,今晚下班一起吃饭、逛街、看电影。

A→B:好的,我知道你答应我今晚下班一起吃饭、逛街、看电影的邀请了。


实际上这样子是有很大的风险的

如果是按照长连接模型,A可以根据B在吃饭的时候的反应来决定要不要继续去逛街看电影,也就是如果传输完了一次数据之后还保持连接就继续传输,万一连接突然断开或者是不稳定,那可能就要重新建立连接。(万一B在吃饭的时候吃的不开心不想继续逛街看电影那就等下次再吃饭逛街看电影)

但是如果按照流水线模型,A一次发送三个请求,虽然发送请求的时候省事儿了(三次握手的时候TCP打包传输请求更省事),但是谁也不知道吃饭逛街看电影的过程中会发生什么意外,时间越长越不稳定,而且还容易出现万一B想减肥不想吃饭,只想逛街看电影的情况呢?(HOL问题

最后这里补充一张图片来对比三种模型之间的差别:

2、Nginx中的keepalive指令

当我们配置Nginx作为代理服务器的时候,想要支持HTTP长连接,需要client到Nginx和Nginx到server都是长连接,因为此时Nginx既是client的server也是server的client。

了解了上面的原理之后,Nginx中的keepalive指令我们就非常好理解了,相关的指令主要有三个,我们逐个进行解释:

2.1 keepalive

1
2
3
4
Syntax:	keepalive connections;
Default: —
Context: upstream
This directive appeared in version 1.1.4.

在upstream模块中配置,启用连接到upstream中的服务器的缓存,connections参数的主要作用是设定每个Nginx的单个worker进程(each worker process)对于upstream中的server的最大空闲连接数,当超过该数字的时候,会关闭使用得最少的连接。

  • 对于HTTP,应将proxy_http_version指令设置为“ 1.1”,并且应清除Connection标题字段

  • 对于FastCGI服务器,需要设置fastcgi_keep_conn以启用keepalive连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
upstream http_backend {
server 127.0.0.1:8080;
keepalive 16;
}

server {
location /http/ {
proxy_pass http://http_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}

lcaotion /FastCGI/ {
fastcgi_pass fastcgi_backend;
fastcgi_keep_conn on;
}
}

需要注意的是,keepalive指令并不会限制Nginx的所有worker进程能开启的连接到upstream服务器中的连接总数(total number)。也就是如果设得太大了,会导致过多的空闲连接占满了upstream中的server资源,导致新的连接无法建立,因此这个数值的设定需要根据worker进程数量来调整。

2.2 keepalive_requests

1
2
3
4
Syntax:	keepalive_requests number;
Default: keepalive_requests 100;
Context: upstream
This directive appeared in version 1.15.3.

keepalive_requests设定可以通过一个连接(connection)发送的请求(request)数量,超过最大请求数量之后,该连接会被关闭。为了释放每个连接的内存分配,定期关闭连接是很有必要的。因此,不建议将keepalive_requests设定过大,否则可能会导致过高的内存占用。

2.3 keepalive_timeout

1
2
3
4
Syntax:	keepalive_timeout timeout;
Default: keepalive_timeout 60s;
Context: upstream
This directive appeared in version 1.15.3.

设定连接超时时间,在此设定的时间内,client与upstream中的server的空闲keepalive连接将保持打开状态(open)。此外,虽然官方文档说的默认值是60s,但是1.17.9版本的Nginx在安装之后配置文件nginx.conf上面设定的是65s。