为什么你的API总是慢成蜗牛?可能不是代码问题,是TCP在搞你

2026-07-16 11 0

为什么你的API总是慢成蜗牛?可能不是代码问题,是TCP在搞你

上周帮一个朋友看接口性能问题,他的Go服务RT(响应时间)中位数800ms,P99直接飙到2秒。代码review了一圈,没什么问题,数据库慢查询也没有,Redis也在线。最后怎么解决的?改了三行TCP配置,RT降到50ms。

这不是玄学,这是TCP协议的基础知识没到位。今天我们来聊聊那些后端工程师天天在用、却很少深入理解的TCP细节。


一、连接复用:你以为reuse了就真的复用了?

所有人都在说HTTP连接要复用,Keep-Alive是基操。但我见过太多项目,嘴上说着连接池,实际上每次请求都在创建新连接。

来看一个经典的误解:

# 伪代码,展示一个常见的假复用
client = HTTPClient(keep_alive=True)  # 以为开了keep-alive就万事大吉

for url in urls:
    response = client.get(url)  # 实际上每个URL可能都是新连接

问题出在哪?大多数HTTP客户端库的连接复用是按host:port维度的。如果你请求的是同一个域名的不同路径,复用没问题。但如果你的服务有多个后端实例,负载均衡器后面跑着多台机器——恭喜你,每次请求可能是不同的TCP连接。

更隐蔽的问题是HTTP/1.1的pipeline并没有被广泛实现。你以为可以像流水线一样发多个请求,实际上大多数客户端是串行等待响应的。要真正并发?你需要HTTP/2的多路复用或者每个请求独立连接(然后用连接池)。


二、Nagle算法:那个让你RT暴涨200ms的小恶魔

TCP Nagle算法是为了减少小包数量设计的,原理很简单:凑够一批数据再发送,别一点一点往外吐。这对ssh这种交互式会话是好事,但对你的API是噩梦。

想象这个场景:

# 你返回了一个200字节的JSON响应
# 由于Nagle算法,这200字节可能不会立即发送
# 客户端在等,RT就这么上去了

# 禁用Nagle(setsockopt TCP_NODELAY)
import socket
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

我知道你在想什么:200字节等一下怎么了?但问题是,如果你的服务QPS是10000,每个请求多等20ms,就是200秒的等待时间浪费。放大到集群规模,这就是灾难。

一个无意识的测试:禁用NODELAY后,某些场景下P99延迟直接降40%。这不是我编的,是真实测出来的。


三、TIME_WAIT陷阱:那个你从未注意过的端口耗尽

当TCP连接关闭时,主动关闭的一方会进入TIME_WAIT状态,持续2MSL(通常60秒)。这意味着这个端口在60秒内无法重用。

如果你每秒建立10000个短连接,2分钟后你的可用端口就耗尽了。新连接建不了,只能等待或者失败。

# 查看当前连接状态
netstat -an | grep TIME_WAIT | wc -l

# 经典解决思路:
# 1. 客户端使用连接池,不要每次都新建连接
# 2. 服务端合理设置tcp_tw_reuse(允许重用TIME_WAIT状态的端口)
# 3. 如果用Go,检查你的http.Transport是否正确配置了

Go的http.Transport默认配置其实还行,但很多人会手动配置然后配出问题。比如把MaxIdleConns设成0——恭喜你,连接池完全失效。


四、TCP缓冲区:被遗忘的性能洼地

每个TCP连接都有发送缓冲区和接收缓冲区,默认大小通常很小(几KB到几十KB)。当你传大请求体或者下载大文件时,这个缓冲区会频繁触发流控。

# Linux下查看和调整
# 查看当前缓冲区大小
cat /proc/sys/net/ipv4/tcp_rmem  # 接收缓冲区分3个值:min, default, max
cat /proc/sys/net/ipv4/tcp_wmem  # 发送缓冲区分3个值

# 调整示例(需要root)
sysctl -w net.ipv4.tcp_rmem="4096 87380 6291456"
sysctl -w net.ipv4.tcp_wmem="4096 16384 4194304"

一个实际案例:有个服务上传图片,文件稍大就超时。追查发现是接收缓冲区太小,滑动窗口卡住了。调大缓冲区,完事。

但要注意,不是越大越好。内存是有限的,每个连接都在消耗资源。Nginx的worker_connections默认是1024,如果每个连接缓冲区4KB,光连接就占4MB内存,看起来不多。但如果你把缓冲区设成1MB,1024个连接就是1GB——分分钟OOM。


五、连接池的正确姿势:大多数人都在瞎配

说到连接池,我见过最离谱的配置是这样的:maxIdle=100, maxOpen=100,然后问为什么性能上不去。

来,解释一下这两个参数:

  • maxIdle:最大空闲连接数,连接池里最多躺多少个空闲连接
  • maxOpen:最大打开的连接数,包括使用中的和空闲的

你maxIdle=100, maxOpen=100意味着什么?连接池永远不会有超过100个连接,所有连接都在idle和open之间摇摆。当并发上来,连接不够用,频繁创建销毁,性能就崩了。

正确的配置思路:

# 根据你的QPS和平均响应时间推算
# 公式:需要的连接数 ≈ QPS * 平均响应时间(秒)
# 假设QPS=1000,平均RT=50ms
# 需要的连接数 ≈ 1000 * 0.05 = 50

# 所以你可以这样配
maxOpen = 100  # 留点余量
maxIdle = 80   # 接近实际需要的数量,避免频繁创建销毁
idleTimeout = 30s  # 空闲连接30秒后关闭,别让连接僵死

六、实战排查:我的三板斧

遇到TCP相关的性能问题,我是这样排查的:

第一板斧:ss而不是netstat

# 查看连接状态统计
ss -s
# 输出类似:
# Total: 256 (kernel 512)
# TCP:   124 (estab 12, closed 98, orphaned 0, synrecv 0, timewait 98/10)

# 查看具体连接,特别关注TIME_WAIT数量
ss -tan state time-wait | wc -l

第二板斧:抓包看RTT

# 抓取SYN和 handshake包,看实际RTT
tcpdump -i eth0 'tcp[tcpflags] and tcp-syn != 0' -c 100

# 或者用更现代的工具
ss -ti dst 10.0.0.1  # 看到这个IP的连接详细信息,包括rtt、cwnd

第三板斧:内核参数巡检

# 检查关键TCP参数
sysctl net.ipv4.tcp_tw_reuse  # 应该是1
sysctl net.ipv4.tcp_nodelay   # 这个不是sysctl,是setsockopt
sysctl net.core.somaxconn     # 监听队列长度,太小会丢连接
sysctl net.ipv4.tcp_max_syn_backlog

结语

TCP是个老协议,1981年的RFC 793。但即使是老协议,大多数工程师对它的理解也停留在三次握手四次挥手的八股文水平。等到线上出问题了,才发现自己连ss命令都不会用。

所以我的建议是:别光顾着追框架、学新语言。基础知识学扎实了,你能解决别人解决不了的问题。

当然,如果你读到这里发现卧槽我好像踩过这些坑,那这篇文章就值了。

相关文章

API版本管理:当我第18次被问这个接口什么时候废弃时的崩溃与反思
当AI开始整活:这些新奇玩法让我差点忘了正经工作
还在为部署AI工具掉头发?我来帮你搞定一切
为什么你的API总被前端骂?5个让我后悔三年的设计失误
被开除的DBA告诉你:为什么我赌上职业生涯也要选PostgreSQL
你的接口正在被恶意刷?来聊聊接口限流的正确打开方式

发布评论