三十次请求开了三十条连接,关掉以后端口还要占两分钟
给本机跑的一个小服务压三十个请求,本来只想知道平均耗时。量完发现两个数差了将近九倍: 每次都新建连接的写法,每一次 11.64 毫秒;一条连接连发三十次的写法,每一次 1.34 毫秒。 同一台机器,同一个进程,服务端处理逻辑完全一样。
差的全在「把套接字建起来」这一段。
先把 accept 数出来
只看客户端的耗时不够,还得知道到底开了几条连接。给服务加一个计数器,覆写 get_request, 每接受一个套接字就加一:
class Srv(socketserver.ThreadingTCPServer):
def get_request(self):
conn, addr = super().get_request()
accepts["n"] += 1
return conn, addr
客户端两种写法各发三十个请求。第一种每次都 new 一个连接,请求完立刻 close; 第二种是一个连接连发三十个。量出来的数字是这样的:
| 写法 | accept 次数 | 总耗时 | 单次均值 | 最慢 / 最快 |
|---|---|---|---|---|
| 每次新建连接 | 30 | 349.3 ms | 11.64 ms | 25.39 / 1.75 ms |
| 一条连接发三十次 | 1 | 40.1 ms | 1.34 ms | 26.21 / 0.38 ms |
复用那一行,最慢的 26 毫秒是第一次请求,它包含建连接;后面二十九次都在 0.4 到 2 毫秒之间。 新建那一行反过来:没有一个请求超过 26 毫秒,但最慢和最快差 14 倍。
所以短连接的「平均耗时」这个指标基本没用。它把一个固定的建连成本和一次随机的线程调度 混在一起平均,看着挺平稳,实际两个来源完全不是一回事。
关掉不等于释放
跑完立刻数一次套接字状态:
netstat -ano -p TCP
// 三十行 TIME_WAIT,本地端口是客户端的临时端口,对端端口是服务端口
三十条连接,客户端侧 30 条 TIME_WAIT,服务端侧 0 条。谁先发 FIN,这个状态就记在谁头上。
然后我盯着它们什么时候消失。起五条连接、全部关掉,每五秒数一次:
刚关完 5 条连接: TIME_WAIT=5
t=124s -> 0
一百二十四秒。这个数来自两倍的报文最大生存时间,本机默认就是两分钟左右。 也就是说,一次请求结束之后,那个四元组还被锁着两分钟,谁都不能用。
端口池的大小是硬的
顺手把动态端口范围读出来:
netsh int ipv4 show dynamicport tcp
启动端口 : 49152
端口数 : 16384
一万六千多个端口,每条短连接占一个、占两分钟。对着同一个目标地址,能持续跑的新建连接 就是 16384 ÷ 120,约 136 条每秒。过了这个数,报错不会出现在服务端,而是本机这边 分配不到临时端口。
这个上限跟带宽、跟对方快不快都没关系,纯粹是端口池除以回收时间。 调大端口范围能挪一点,但只是在把一个除法里的分子改大,回收时间那一段动不了。
有些「复用」是假的
最后说一个把我骗过的现象。如果服务端在响应头里写 Connection: close, 客户端的连接对象并不会报错,它会丢掉旧套接字,下次请求时悄悄重建一条。 代码里只 new 了一个连接对象,服务端实际接受了三次:
第1次 status=200 累计 accept=1
第2次 status=200 累计 accept=2
第3次 status=200 累计 accept=3
返回值里一点异常都没有,状态码全是 200。我当时看着这个循环,以为它在复用, 其实每轮都付了一次建连的钱。
三条可以照抄的判断
- 想知道自己开了几条连接,就在服务端数 accept,或者抓包数 SYN。客户端的代码结构和对象状态都不算证据。
- 短连接的成本有两块:一次额外的建连,和两分钟的端口占用。平时只算前一块,会把成本低估一半。
- 要复用,先确认响应头里没有 Connection: close,并且准备好对端提前关闭后重发一次。没有重试的复用,等于把偶发的连接错误直接变成请求失败。
这次量完最想记住的一句:把总耗时拆开之后,那 11 毫秒里有 10 毫秒是本机在跟自己做握手, 服务端一毫秒都没花。
- 点赞
- 收藏
- 关注作者
评论(0)