第四章: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 我在行情网关中的调优经验总结

说了这么多理论,最后分享几个我在实战中总结的经验:

  1. 先做基准测试:别上来就调参数。先跑一遍基准,看看瓶颈到底在哪。我见过有人调了半天窗口大小,结果瓶颈是CPU。
  2. 一次只改一个参数:TCP参数之间会相互影响。同时改多个参数,出了问题你都不知道是哪个引起的。
  3. 关注尾延迟:平均延迟好看没用,P99.9延迟才是行情系统的命门。我习惯用直方图记录延迟分布。
  4. 监控连接状态:ss -tie命令实时查看TCP连接状态,重点关注retrans(重传数)和rtt(往返时间)。
  5. 不要迷信参数:再好的参数也救不了糟糕的架构。TCP优化是锦上添花,不是雪中送炭。

最后说一句:TCP优化没有银弹。每个系统都有自己的特点,你得去测量、去实验、去调整。我花了整整三个月才把行情网关的TCP延迟从500微秒压到80微秒。这条路,没有捷径。

TCP优化知识体系 TCP优化 实战 TCP_NODELAY 快速重传 窗口缩放 关闭Nagle算法 减少重传等待 扩大缓冲区 长连接池 减少握手开销 连接复用策略

公众号:蓝海资料掘金营,微信deep3321