谈谈 HTTP 连接管理

3,619 阅读10分钟
原文链接: www.jianshu.com

1 HTTP连接管理概述

最近重读了《HTTP权威指南》部分章节,结合apache来对部分内容进行印证并记录下来。HTTP连接管理我们大体会谈到如下内容:串行连接,并行连接,持久连接以及管道化连接。现在流行的浏览器如chrome,firefox都采用了并行的持久连接来提升性能,减少加载延时。本文只针对HTTP/1.0和HTTP/1.1,HTTP/2不在讨论范围。

HTTP/1.1允许在持久连接上使用管道,但是在浏览器上却几乎不会开启管道功能,chrome之前的老版本还可以在 chrome://flags里面来选择开启或关闭管道功能,最新的版本已经移除了管道相关选项。为什么不加入管道化连接提升性能呢,主要有几个原因:其一是管道化持久连接实现复杂,容易出bug,且不易调试。其二是一些不标准的代理导致管道化容易出现很多难以预料的问题,导致开发人员调试十分复杂。更详细的一些分析参见stackoverflow: why-is-pipelining-disabled-in-modern-browsers

1.1 串行连接

串行连接性能最差,因为每次http事务(http事务由一次完整的http请求加上http响应构成)都要建立一个新的TCP连接,而且每个新的http事务都要串行执行,比如一个页面嵌入了3个图片,则浏览器需要串行地发起4个HTTP事务来显示页面。除了串行加载引入的延迟外,加载一副图片时,页面其他地方都没有动静也会让人心理上觉得速度慢。


图1 串行连接

1.2 并行连接

正是由于串行连接的问题才出现了并行连接。HTTP允许客户端(如浏览器)打开多条连接,并行执行多个HTTP事务,加快加载速度。当然这里有一点要说明,并行连接并不是一定更快,如果并行的进程数太多会消耗很多内存资源,此外,如果每个用户的客户端打开100个连接,则100个用户同时访问时会有10000个连接需要服务器处理,加重服务器的负担。


图2 并行连接

因此,现代浏览器虽然用了并行连接,但是一般会将同一个域名的请求并行连接数限制到一个较小的值,此外浏览器的总的连接数也会有最大限制。具体数目如下表所示,[数据来源]www.browserscope.org/?category=n…


图3 浏览器连接数以及其他限制

1.3 持久连接

虽然并行连接可以提升加载速度,但是每个HTTP事务都要新建一个连接有点浪费了。
一个WEB站点通常会打开到同一个站点的连接,比如一个WEB页面的大部分内嵌资源(如图片,js文件,样式文件)都是来自同一个WEB站点的,因此,初始化对某服务器的HTTP请求的应用程序很可能会在不久后向那台服务器发起更多的请求,于是,HTTP/1.1允许HTTP设备在事务处理结束后将TCP连接保持在打开状态,以便未来重用。在事务处理完成之后仍然保持打开状态的TCP连接称之为持久连接,持久连接会在不同事务之间保持打开状态直到客户端或服务器决定将其关闭。


图4 持久连接和串行连接对比

持久连接和并行连接共用可能是目前最高效的方式,现代浏览器都是打开少量的并行连接,然后每个连接都是持久连接的方式。持久连接包括老的HTTP/1.0+的“Keep-Alive”连接以及HTTP/1.1的“persistent”连接。注意一下持久连接和并行连接不要搞混了,持久连接指的是在同一个连接不关闭情况下处理多个HTTP事务,而并行连接是指多个连接并行的执行(当然实际情况是各个连接执行有点延迟,但是在执行时间上是重叠的)

管道化连接示意图如下,可以在第一个事务的请求还没有返回的时候发送后续请求,性能会有所提升,但是由于实现麻烦和容易出BUG,现代浏览器默认都会关闭管道化连接,这里就不再过多讨论。


图5 管道化连接示意图

老的Keep-Alive方式使用非常广泛,虽然在HTTP/1.1中已经不再使用。使用Keep-Alive有一些需要注意的地方:

  • 在HTTP/1.0中,Keep-Alive默认并不会启用。客户端必须发送一个Connection:Keep-Alive请求头部来激活Keep-Alive连接。注意,客户端的Keep-Alive请求头部只是请求将连接保持在活跃状态,服务端不一定答应启用Keep-Alive会话。HTTP/1.1中默认就是持久连接,除非在请求头显示的加了Connection:close来关闭持久连接。服务端如果启用Keep-Alive连接,一般会在响应头部中带上如下内容:

      Connection:Keep-Alive
      Keep-Alive:timeout=1800, max=5

    表示服务器最多还能为5个HTTP事务保持持久连接,Keep-Alive的超时时间为1800秒。在Apache中对应配置如下:

      # KeepAlive: Whether or not to allow persistent connections (more than
      KeepAlive On
      # MaxKeepAliveRequests: The maximum number of requests to allow
      MaxKeepAliveRequests 5
      # KeepAliveTimeout: Number of seconds to wait for the next request from the
      KeepAliveTimeout 1800
  • Connection:Keep-Alive必须随每个希望保持持久连接的请求的头部发送,如果某个请求没有带Keep-Alive头部,则服务器会在这个请求后关闭该连接。此外,如果客户端想关闭持久连接了,在请求头部带上一个Connection:close即可。

  • Keep-Alive和哑代理之间有一些问题要注意下。一些代理无法理解Connection头部,应该在转发请求前将Connection头部去掉(其他需要去掉的头部还有Prxoy-Authenticate,Proxy-Connection,Transfer-Encoding等),不然容易造成浏览器挂起。因为这种哑代理什么都不做,直接转发所有请求内容,会让服务器误以为自己跟客户端建立了Keep-Alive连接,而哑代理对此一无所知,发送数据后等待服务器关闭该连接,哑代理认为这个连接上不会再有请求,客户端新的请求都会被忽略。网景公司提出的方法是增加一个非标准的Proxy-Connection头部来可以解决单个哑代理的问题,虽然Proxy-Connection没有纳入到标准中,但是现代浏览器大都在使用,比如我在Chrome上安装了穿越(大赞这个插件,翻墙速度杠杠的)这个插件后可以翻墙,也就是加了一层代理,这时候我访问其他站点就会带上Proxy-Connection头部而不是Connection头部。虽然如此,Proxy-Connection对于聪明代理(可以理解Connection的代理称之为聪明代理)和哑代理共存的情况仍然有问题。

2 apache实例分析

在ubuntu14.04中安装apache2.4,为了测试方便,以prefork模式运行,设置参数如下:

#apache2.conf
KeepAliveTimeout 1800 #一般设置5秒以内即可,以减少内存浪费。
KeepAlive On
MaxKeepAliveRequests 2 #设置

#mpm_prefork.conf
# prefork MPM
# StartServers: number of server processes to start
# MinSpareServers: minimum number of server processes which are kept spare
# MaxSpareServers: maximum number of server processes which are kept spare
# MaxRequestWorkers: maximum number of server processes allowed to start
# MaxConnectionsPerChild: maximum number of requests a server process serves

<IfModule mpm_prefork_module>
    StartServers             1
    MinSpareServers          1
    MaxSpareServers         1 
    MaxRequestWorkers     1
    MaxConnectionsPerChild   2
</IfModule>

这里我们开启KeepAlive,然后设置最多保持的请求数为2个(加上初始的请求,一个连接可以最多发送3个请求),KeepAlive超时为1800秒。注意下prefork的配置,为了方便测试,我们设置apache子进程数为1,注意这里的MaxConnectionPerChild,设置的是2,这是什么意思呢,apache的prefork模式下,可以指定一个子进程最多处理的请求数,到了数目则杀掉这个子进程,重新创建一个子进程,以防止内存泄露。

开启Keep-Alive的优点是可以加快网页加载速度,减少频繁建立连接来降低CPU的使用率。缺点是会多占用内存,所以如果开启KeepAlive,那么KeepAliveTimeout不能设置太大,以免持久连接太多耗光服务器资源。至于线上环境是否开启,视情况而定,如果服务器内存很大配置很高,开不开启没多少影响,如果内存较小,则建议关闭节省内存。

要注意的地方来了,开启KeepAlive后,MaxConnectionPerChild的这里的请求数怎么算呢?一个持久连接的两次请求在apache这里算一个请求还是两个呢?答案是1个,也就是一个持久连接的多次请求在apache的子进程这里只是同1个连接,也就是说,在我们这样的设置下,只要间隔没有超过1800秒发起请求,前3个请求会共用一个持久连接。

使用chrome(版本55.0.2883.95)发送一个请求,我们可以看到请求头和响应头如下(不同浏览器头部可能有所不同,请自行查证):


图6 Keep-Alive请求头和响应头

请求的头部会带上一个 Connection:keep-alive,响应头部会带上 Connection:Keep-Alive; Keep-Alive:timeout=1800, max=2,表示服务器支持Keep-Alive,且持久连接最大请求数为2,超时时间为1800秒。接下来继续发送2个请求,可以看到响应头分别带有Connection:Keep-Alive; Keep-Alive:timeout=1800, max=1Connection:close,也就是第3次请求之后,这个持久连接关闭,接下来浏览器需要另外跟apache建立起一个新的连接。

注意,由于一个持久连接的3次请求是同一个连接,我们在prefork中设置的apache一个子进程最多可以处理2个请求,那么apache什么时候才会杀死这个子进程并创建一个新的子进程呢?答案是在浏览器在1800秒内发起6次请求后,因为浏览器的6次请求是在2个持久连接里面发送的,在apache这里计数只是2次连接。

如果我们把KeepAliveTimeout改成3秒,发送一次请求后,间隔超过3秒才发下一次请求,则由于超时,此时第二个请求就是一个新的连接了,由前面的实验可以知道,apache那边还是同一个子进程来处理这个新的连接。再过3秒后发送一个请求,这个时候apache就会杀死之前的那个子进程并创建一个新的子进程来建立连接处理请求了。

如果关闭KeepAlive,则每次请求都是一个新的连接,那么apache子进程建立的连接数就和你发送请求数是一样的了。

PS: 可以通过watch命令来定时查看apache的进程变化 watch -n 1 "ps aux|grep apache|grep -v grep"

参考资料