第四章:TCP优化实战
TCP这个协议,说实话,在行情系统里又爱又恨。爱的是它可靠,恨的是它太「老实」了——非要三次握手、慢启动、确认重传。但咱们做行情网关的,延迟就是生命。今天我就把我在TCP调优上踩过的坑、总结的经验,一次性讲清楚。
核心观点:TCP不是不能用于低延迟场景,关键是你得知道怎么「调教」它。我见过太多团队上来就换UDP,其实TCP优化到位了,延迟能压到微秒级。
4.1 TCP_NODELAY:别让Nagle算法坑了你
先说说最基础的一个优化点——TCP_NODELAY。这个选项关闭的是Nagle算法。Nagle算法干嘛用的?说白了就是把多个小包攒成一个再发,减少网络中的小包数量。
听起来挺合理对吧?但行情系统里,这玩意儿就是灾难。
我举个例子:你发一个行情报价,可能就几十个字节。Nagle算法一看,「这么小的包?先别发,等会儿看看还有没有数据要发」。这一等,可能就是几十毫秒。在行情系统里,几十毫秒够行情变三回了。
// 正确的打开方式
int flag = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(flag));
我的经验:我在做某交易所行情网关时,一开始没开TCP_NODELAY,结果行情延迟比预期高了40%。查了半天才发现是Nagle在作祟。从那以后,所有TCP连接我必开这个选项,成了肌肉记忆。
但要注意一点:开了TCP_NODELAY后,如果你频繁发送极小的包(比如1字节的keepalive),确实会增加网络开销。不过行情系统里,数据包大小通常在几百字节以上,这个顾虑可以忽略。
4.2 快速重传:别等超时,主动出击
TCP的重传机制,默认是等超时。超时时间是多少?Linux默认是200ms到3秒不等。你想想看,行情都变了几个量级了,你还在等重传?
快速重传的原理很简单:接收方收到乱序包时,会立即发送重复ACK。发送方收到3个重复ACK后,不等超时,直接重传丢失的包。
但默认的快速重传触发条件是3个重复ACK。这个值能不能调?能。
| 参数 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
| tcp_retries1 | 3 | 2 | 放弃前尝试次数 |
| tcp_retries2 | 15 | 5 | 超时前重传次数 |
| tcp_syn_retries | 6 | 2 | SYN重试次数 |
注意:调低重传次数会提高连接断开的概率。在行情系统里,我宁愿连接断开后快速重建,也不愿等十几秒的重传。这个取舍,你得想清楚。
4.3 窗口缩放:让带宽不再是瓶颈
TCP窗口大小决定了发送方一次能发多少数据而不等确认。默认窗口是64KB,对于千兆网络来说,这太小了。
窗口缩放选项(Window Scaling)允许窗口扩大到1GB。怎么开?Linux默认是开启的,但需要确认一下:
# 查看当前窗口缩放设置
sysctl net.ipv4.tcp_window_scaling
# 如果返回1,表示已开启
# 如果返回0,执行:
sysctl -w net.ipv4.tcp_window_scaling=1
我建议在行情网关的服务器上,把接收和发送缓冲区都调大:
// 设置接收缓冲区
int rcvbuf = 4 * 1024 * 1024; // 4MB
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
// 设置发送缓冲区
int sndbuf = 4 * 1024 * 1024; // 4MB
setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
避坑指南:我曾经把缓冲区设成16MB,结果内存占用飙升,反而因为缓存太大导致延迟抖动。后来发现4MB是个甜点值——既能充分利用带宽,又不会引入额外延迟。
4.4 减少握手开销:长连接池实战
TCP三次握手的开销有多大?我算过一笔账:
- 一次三次握手:约1个RTT(网络往返时间)
- 在局域网内:约0.1-0.5ms
- 在跨机房场景:可能2-5ms
如果每次行情请求都新建连接,光握手时间就占了延迟的大头。解决方案?长连接池。
我设计的行情网关长连接池,核心逻辑是这样的:
// 伪代码:长连接池核心逻辑
class ConnectionPool {
// 预建立N条TCP连接
vector<TCPConnection> connections;
// 连接复用策略
Connection* getConnection() {
// 1. 优先使用空闲连接
// 2. 如果没有空闲,等待(不新建)
// 3. 如果等待超时,返回错误
}
// 健康检查
void healthCheck() {
// 定期发送心跳包
// 检测到断开的连接,立即重建
}
}
具体实现时,有几个关键点:
- 连接数控制:不是越多越好。我一般设为核心数的2-4倍
- 心跳间隔:建议3-5秒一次,太频繁浪费带宽,太慢检测不到断连
- 断连重建:异步重建,不要阻塞业务线程
我的做法:在行情网关里,我用了双缓冲池设计。一个池子处理当前请求,另一个池子做健康检查和重建。这样重建连接不会影响正在处理的行情数据。
4.5 我在行情网关中的调优经验总结
说了这么多理论,最后分享几个我在实战中总结的经验:
- 先做基准测试:别上来就调参数。先跑一遍基准,看看瓶颈到底在哪。我见过有人调了半天窗口大小,结果瓶颈是CPU。
- 一次只改一个参数:TCP参数之间会相互影响。同时改多个参数,出了问题你都不知道是哪个引起的。
- 关注尾延迟:平均延迟好看没用,P99.9延迟才是行情系统的命门。我习惯用直方图记录延迟分布。
- 监控连接状态:用
ss -tie命令实时查看TCP连接状态,重点关注retrans(重传数)和rtt(往返时间)。 - 不要迷信参数:再好的参数也救不了糟糕的架构。TCP优化是锦上添花,不是雪中送炭。
最后说一句:TCP优化没有银弹。每个系统都有自己的特点,你得去测量、去实验、去调整。我花了整整三个月才把行情网关的TCP延迟从500微秒压到80微秒。这条路,没有捷径。
公众号:蓝海资料掘金营,微信deep3321