作者:崔杰

前语:http 协议是互联网中最常使用的应用层协议,它的绝大多数实现是基于 TCP 协议的。

一、问题描述

某天,在对一个提供 http 接口的后台服务进行压力测试过程中,我们设定了几百 qps(每秒请求数)开始测试几分钟后,请求一端(我们后续简称为:客户端)的压力结果统计日志中开始连续出现大量的报错信息:

在压力测试前,根据之前的经验,同类服务的单机性能一般能够达到几千 QPS,然而此时测试设定的压力值还不足 200qps,这与预期存在 1 个数量级以上的性能差距,难道是被测服务存在问题么?

二、问题跟踪

为了确认被测服务的状态,我们首先登录了服务所在的机器,检查了服务资源的占用情况,结果是:CPU、内存、硬盘、I/O、网卡、fd、socket 等各项资源都不存在较大负载。看来服务本身还远没有达到它的负载瓶颈。

在排除服务端问题后,我们重新分析了统计日志中的错误--"can not assign requested address",这是一个常见的 socket 的 error,报错信息说明无法为 socket 创建新的连接,很可能是:tcp 层的连接端口已经耗尽,无法为新的 http 请求分配端口建立连接。通过 netstat 命令,我们检查客户端,发现确实存在大量请求连接处于 TIME_WAIT 状态下:

这里要说明一下,虽然理论上 tcp 连接可用端口号为 0~65535--大约 65536 个,但是实际在不指定端口情况下连接服务时可用端口默认为 32768~61000--大约只有 28000 多个,在 linux 系统中这个限制可以通过
/proc/sys/net/ipv4/ip_local_port_range 文件进行修改。

我们知道 http 协议主要是基于 tcp 协议之上的,为了解决 tcp 层连接通道复用的问题,在 http 协议中通过 header 中的 Connection 字段定义了对于 tcp 长连接的支持:

在压力测试过程中,我们模拟发送 http 请求的代码中使用的是 http/1.1 协议,应该会默认使用长连接,看来很可能是服务端不支持长连接,才会引起客户端频繁的创建 TCP 连接。通过 tcpdump 抓包,我们对此进行了证实:

三、跟进分析

到此,我们发现服务端确实返回不支持长连接的信息(header 中 connection:close),导致客户端每次发起请求都会重新创建 tcp 通道。但是根据以往测试经验来看,比较常见的是在服务端出现大量 time_wait 状态的,那么为什么大量的 time_wait 状态会在客户端出现呢?

了解这个问题我们之前,可以先来看一下 TCP 正常连接建立和关闭连接时的状态变化图:

上图是 TCP"三次握手"和"四次挥手"的过程,相信很多读者都比较了解,下面我们来说说为什么要存在 TIME_WAIT 状态吧:

明白了 time_wait 的存在原因和出现时机,可以看到 TIME_WAIT 状态总是出现的主动关闭连接的一方,也就是说在我们压力测试过程中每次都是客户端主动关闭 tcp 连接的。从实际的抓包结果来看,确实如此:

但是我们实际遇到的多是 time_wait 出现在服务一端出现的,那么在 http 协议规定中,服务端返回connection:close 的信息后,到底是应该由客户端还是服务端来主动关闭连接呢?

Connection: close 是一个 general-header( RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1 )即:既可以作为 request header 也可以作为 response header。Connection: close 的作用在于"协商 (signal)"。在 RFC2616 14.10 中:

HTTP/1.1 defines the "close" connection option for the sender to
signal that the connection will be closed after completion of the
response.

通过 RFC 可以发现:请求和响应的双方都可以主动关闭 TCP 连接。

但是大多数的 web Service 实现是返回 connection:close 内容之后服务端会主动关闭连接。至于这样设计的原因,网上找到 2 个比较靠谱的解释:

也许会有读者担心:如果客户端也不主动关闭 TCP 连接,服务端的 socket 资源会不会很快用完呢。这里留给读者们一个问题进行思考:在单个服务器上的服务端理论上能支持的最大 TCP 连接数是多少呢?

四、解决方法

根据分析,我们知道了客户端请求报错的原因在于:服务端拒绝了客户端的 HTTP 长连接请求,同时服务端没有主动关闭 tcp 连接,而是由客户端主动关闭网络连接,导致在客户端出现大量 time_wait,在压测进行到一段时候后由于没有新的 socket 端口可用而开始报错。

了解了原因后,解决方法就比较简单了,需要我们修改客户端所在 linux 环境下的 tcp 相关参数,编辑/etc/sysctl.conf 文件,增加三行:



再执行以下命令,让修改结果立即生效即可:

然后,我们的压力测试的客户端就不会再受 time_wait 问题困扰了。


参考资料:

①《O'Reilly - HTTP - The Definitive Guide.pdf》

https://www.zhihu.com/question/24338653

本章完~~

本文连接:http://tmq.qq.com/2016/08/pressure-test-after-suffering-a-lot-time_wait/


TMQ(腾讯移动品质中心)是腾讯最早专注在移动 APP 测试的团队
我们专注于移动测试技术精华,饱含腾讯多款亿级 APP 的品质秘密,文章皆独家原创,我们不谈虚的,只谈干货!

扫码关注我们

扫一扫 关注 TMQ
精彩分享不断


↙↙↙阅读原文可查看相关链接,并与作者交流