上一篇 下一篇 分享链接 返回 返回顶部

香港服务器运行 CentOS 7 时,我是如何把 TCP_NODELAY 玩到极致,让实时对战“指令秒到”的

发布人:Minchunlin 发布时间:2025-09-14 09:49 阅读量:180


那天是周五凌晨两点,我在香港机房 5 楼的工作间盯着战队训练服上滚动的延迟曲线,心里“咯噔”一下:技能键从按下到服务端确认,P99 已经飙到 85 ms,而平时我们能稳在 35 ms 左右。周末有公开内测,延迟再这样抖,我们就要“出圈”——以最糟的方式。

我打开一台前端网关的 strace,看到关键端口上从未设置 TCP_NODELAY。这下有谱了:Nagle 把我们的“微小指令包”攒成大包,延迟被硬生生堆了上去。下面就是那一夜到天亮,我在 CentOS 7 + 香港裸金属 上一步步把 TCP_NODELAY 和全链路低延迟栈打磨到能“直接上场”的全过程。

环境与目标

目标:优化“实时对战类游戏”指令往返延迟(特别是小包),让 P50 ≤ 20 ms、P99 ≤ 40 ms(港到华南/华东跨境),并且在高并发(≥ 5k 并发连接/节点)下稳定。

现场环境(实配)

规格
机房 香港葵涌区,双线(HGC/PCCW)接入
服务器 1U 裸金属,Intel Xeon Silver 4210R(10C/20T)×2
内存 64 GB DDR4
存储 NVMe(Samsung PM983)1 TB ×2(RAID1)
网卡 Intel X710 10 GbE(驱动 i40e),独占上联
系统 CentOS 7.9(3.10 内核)tuned 启用
业务 自研 TCP 协议(指令小包 64–512 B),网关(Nginx stream)→ 游戏服(Go/Netty/C++ 混部)

说明:如果你运行在云上或虚拟化,后面 NIC/IRQ 的一部分操作需要改为在宿主或云厂商控制台里等效处理。

1. 先校准:用数据确认“问题就是 Nagle”

1.1 快速看包型(现场)

# 抓 10 秒关键端口(例如 4001)的小包占比
tcpdump -i eth0 'tcp port 4001' -w cap.pcap -G 10 -W 1
tshark -r cap.pcap -q -z io,stat,1,"MIN(frame.len) COUNT(frame.len) AVG(frame.len)"

若大多为 < 200 B 的请求/响应小包,但首包/回包间隔异常大,多半是 Nagle(未开 TCP_NODELAY)+ ACK 行为叠加导致。

1.2 直接跟踪应用是否设置了 TCP_NODELAY

# 盯住网关/游戏服进程(替换 pid)
strace -f -e trace=setsockopt -p <pid>
# 期待出现:setsockopt(fd, SOL_TCP, TCP_NODELAY, [1], 4) = 0

看到没有调用或返回 -1,就能坐实应用侧没设,或被上游代理“吞了”。

2. 应用层:必须显式开启 TCP_NODELAY

核心原则:TCP_NODELAY 是应用层开关;操作系统无法“全局强制”替应用设置。你的每一段“持久 TCP 连接”代码,都应该在 accept/连接建立后立刻开启它。

2.1 C/C++(游戏逻辑服/网关常见)

#include <netinet/tcp.h>
#include <sys/socket.h>

void set_nodelay(int fd) {
    int one = 1;
    if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one)) < 0) {
        perror("setsockopt TCP_NODELAY");
    }
    // 建议:交互型短消息流可以开 QUICKACK,减少延迟
#ifdef TCP_QUICKACK
    if (setsockopt(fd, IPPROTO_TCP, TCP_QUICKACK, &one, sizeof(one)) < 0) {
        perror("setsockopt TCP_QUICKACK");
    }
#endif
}

2.2 Go(我们的一条逻辑服链路)

ln, _ := net.Listen("tcp", ":4000")
for {
    conn, _ := ln.Accept()
    if tcp, ok := conn.(*net.TCPConn); ok {
        tcp.SetNoDelay(true)                  // 关键
        tcp.SetKeepAlive(true)
        tcp.SetKeepAlivePeriod(30 * time.Second)
        tcp.SetReadBuffer(64*1024)
        tcp.SetWriteBuffer(64*1024)
    }
    go handle(conn)
}

2.3 Java / Netty(匹配我们网关)

ServerBootstrap b = new ServerBootstrap();
b.childOption(ChannelOption.TCP_NODELAY, true)
 .childOption(ChannelOption.SO_KEEPALIVE, true)
 .childOption(ChannelOption.SO_REUSEADDR, true);

2.4 Node.js(用于运维小服务/机器人)

server.on('connection', (socket) => {
  socket.setNoDelay(true);
});

2.5 Python(调试/工具)

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

检查点:代码上线后,再用 strace 抽查,或在 C/C++ 里 getsockopt(TCP_NODELAY) 打日志确认。

3. 代理/边界:Nginx stream 必开 tcp_nodelay + TFO

我们前面有一层 Nginx 作为 TCP 四层转发(避免直接暴露游戏服,顺便做健康检查/限流)。

/etc/nginx/nginx.conf(摘)

stream {
    upstream game {
        server 127.0.0.1:4000 max_fails=3 fail_timeout=3s;
    }

    server {
        listen 0.0.0.0:4001 fastopen=4096;   # TCP Fast Open,减少握手
        proxy_connect_timeout 1s;
        proxy_timeout 30s;
        proxy_pass game;
        tcp_nodelay on;                      # 关键:Nginx 侧也开
        proxy_responses 1;                   # 单往返型小包场景
    }
}

提示:TFO 需要内核与应用支持(下一节会开启内核开关)。如果你用的是 HAProxy,默认对 HTTP 会 NODELAY,但 TCP 模式需确认版本与配置;现场不确定时,用 strace 看最靠谱。

4. 操作系统:CentOS 7 的低延迟网络基线

新建 /etc/sysctl.d/99-game-lowlatency.conf:

# 更偏“交互延迟”,不极限追吞吐
net.core.somaxconn = 1024
net.core.netdev_max_backlog = 250000
net.ipv4.tcp_max_syn_backlog = 8192

net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1

# 小包+交互,有帮助;代价是某些场景吞吐略降
net.ipv4.tcp_low_latency = 1

# TCP Fast Open:客户端+服务端
net.ipv4.tcp_fastopen = 3

# 拥塞算法维持 cubic(3.10 默认),稳定保守
net.ipv4.tcp_congestion_control = cubic

# 缓冲区:别设过大,避免排队延迟
net.core.rmem_max = 8388608
net.core.wmem_max = 8388608
net.ipv4.tcp_rmem = 4096 87380 4194304
net.ipv4.tcp_wmem = 4096 65536 4194304

# 默认队列:fq_codel,抑制队列抖动(3.10 可用)
net.core.default_qdisc = fq_codel

# 端口范围足够
net.ipv4.ip_local_port_range = 10000 65535

# 交互型建议:避免闲置后慢启动
net.ipv4.tcp_slow_start_after_idle = 0

生效:

sysctl --system

4.1 tuned 选择低延迟档

yum install -y tuned
systemctl enable --now tuned
tuned-adm profile latency-performance
tuned-adm active

4.2 NIC 与中断亲和(X710 / i40e)

关闭大聚合(对小包延迟有利):

ethtool -K eth0 gro off lro off gso off tso off

环形缓冲略放大(防止突发丢包):

ethtool -g eth0
ethtool -G eth0 rx 4096 tx 4096

多队列中断绑定 CPU(避免抖动):

# 查看队列
ls -1 /sys/class/net/eth0/queues/ | cat

# 绑定示例:rx-0/tx-0 → CPU0,rx-1/tx-1 → CPU1 ...(NUMA 就近)
echo 1 > /proc/irq/<irq_of_rx0>/smp_affinity
echo 2 > /proc/irq/<irq_of_rx1>/smp_affinity
# ... 按位掩码分配

经验:在我们场景关闭 irqbalance 后手工绑核,P99 抖动更小;若你没有时间盯这块,保留 irqbalance 也能过关。

5. 启用 TCP Fast Open(配合上游 TTFB)

对于频繁新连的移动端玩家,TFO 能显著减少首包握手延迟。

内核开:上面 sysctl 已设 net.ipv4.tcp_fastopen=3

Nginx listen ... fastopen=4096; 已配置

自研服务端(C/C++ 示例):

#ifdef TCP_FASTOPEN
int qlen = 4096;
if (setsockopt(listen_fd, IPPROTO_TCP, TCP_FASTOPEN, &qlen, sizeof(qlen)) < 0) {
    perror("setsockopt TCP_FASTOPEN");
}
#endif

注意:iOS/Android 客户端是否支持 TFO 取决于系统栈与你用的 SDK;未支持时不影响。

6. 部署脚本(可复制落地)

6.1 一键系统基线(bash)

cat >/root/game_low_latency.sh <<'EOF'
#!/usr/bin/env bash
set -e

yum install -y epel-release tuned tcpdump perf htop numactl ethtool
systemctl enable --now tuned
tuned-adm profile latency-performance

cat >/etc/sysctl.d/99-game-lowlatency.conf <<'EOC'
# (同上 sysctl 配置,略)
EOC
sysctl --system

# NIC 调优
ethtool -K eth0 gro off lro off gso off tso off || true
ethtool -G eth0 rx 4096 tx 4096 || true

systemctl restart nginx || true
echo "[OK] Baseline applied."
EOF

bash /root/game_low_latency.sh

6.2 Nginx stream 模块安装(若你用编译版略过)

yum install -y nginx
# 确认 nginx --version 输出包含 "with stream"

7. 验证:延迟对比测试(实测模板)

我们用 tcpkali 做小包回显延迟基准(把游戏服开个 echo 路径/或临时 echo 服)。

# 客户端在内地机器(跨境)或香港另一台
tcpkali <hk_eip>:4001 -c 1000 -r 10 -T 30s --latency-marker "PING\n"

变更前/后的对比(真实一晚的数据缩编)

指标 变更前(未开 NODELAY) 变更后(全链路开启)
P50 RTT 28 ms 17 ms
P90 RTT 54 ms 28 ms
P99 RTT 85 ms 39 ms
丢包(tcpkali 估计) 0.35% 0.09%

观感:玩家的“搓招”终于不再黏手,语音里“出刀怎么慢半拍”的抱怨消失了。

8. 生产细节 & 经验表(值与理由)

模块 建议值 理由/备注
应用 TCP_NODELAY ON 小包实时交互必须;每条连接启用
应用 TCP_QUICKACK ON 请求-响应型受益,内核会动态调整
Nginx tcp_nodelay on 代理层也要确保不攒包
Nginx fastopen 4096 新连密集场景减少首包延迟
sysctl tcp_low_latency 1 倾向低延迟而非最大吞吐
sysctl default_qdisc fq_codel 抑制队列尾延迟与抖动
NIC GRO/LRO/TSO/GSO off 减少小包聚合延迟(吞吐换延迟)
tuned profile latency-performance 统一 CPU governor/功耗/中断策略
监控 P50/P90/P99 仪表盘必配 玩家体感的是尾延迟
回滚 开关位 灰度 预留开关,防止非预期副作用

9. 容易踩的坑(我都踩过)

  • “只改了系统,不改应用”:别妄想 sysctl 能替你把 Nagle 关掉,必须应用层设置。
  • TLS 误解:加了 TLS 并不会“挡住” TCP_NODELAY,你设置的仍然作用在底层 socket。
  • GRO/LRO 关闭后吞吐下降:这很正常——我们要的是交互延迟;若你有文件分发/重同步流量,请走独立网卡或在那条链路上打开 offload。
  • 云环境看不到中断:KVM/云上只看到 virtio_net,不要徒劳去绑硬件队列;改为关注队列深度与 vCPU 亲和。
  • TFO 生效误判:客户端不支持时你也能“开起来”,但抓包会发现没 TFO 标记。别把 TFO 的收益当成 NODELAY 的收益。
  • ACK/Win 掉队:极端丢包时 QUICKACK 也救不了,优先排查跨境链路抖动(MTR 连续跑 10 分钟),必要时多线回源+BGP 优选。
  • HAProxy 误配:不同版本/模式下是否 nodelay、何时启,差异很大;用 strace 看实际 setsockopt,别只看文档。

10. 回归业务:把优化固化到 CI/CD 与探针

  • CI 检查:静态扫描(或单元测试)中直接 grep 关键模块是否调用 setNoDelay/setsockopt(TCP_NODELAY)。
  • 启动自检:服务启动时打印 TCP_NODELAY=ON,并在健康检查里校验。
  • 黑盒探针:在香港/广州/上海各放一个 tcpkali 定时任务,把 P50/P99 上报到 Prometheus。
  • 回滚预案:Nginx 支持热开关 tcp_nodelay,应用层开关做成动态配置(Apollo/Consul)。

11. 可选进阶:更“新”的内核与 BBR(按需)

CentOS 7 默认 3.10 内核,足够稳定。如果你的对战架构里有带宽较高、跨境抖动的长连,换 ELRepo kernel-ml(≥ 4.9)启用 BBR,在我们某些分区能把 尾延迟再收 3–5 ms。

注意:变更内核属于重大变更,务必灰度+回滚。

12. 复盘与结尾:凌晨 4 点的那次稳定

到 4 点,机房窗外的货柜车开始多起来。我们把最后一个区的网关灰度完,tcpkali 的 P99 在 40 ms 边缘稳定下来。语音频道里,前线的主播喊“顺了!”。我合上工具箱,手还在冰,但心里不再凉。
这套方法没有魔法:应用层明确 TCP_NODELAY + 代理层确认 + OS/NIC 的低延迟基线,再用可重复的脚本固化和探针监控。
如果你也在香港机房里被延迟咬过,按照这篇一步步做,你会看到曲线往下拐——那拐点,通常出现在你把 strace 打到“setsockopt(TCP_NODELAY)=0”的那一刻。

附:一页纸清单(拿去就用)

  •  应用:所有持久 TCP 连接强制 TCP_NODELAY=on,必要时 TCP_QUICKACK=on
  •  Nginx stream:tcp_nodelay on;,listen ... fastopen=4096;
  •  OS 基线:上文 sysctl 全量;tuned-adm profile latency-performance
  •  NIC:gro/lro/gso/tso off,环形缓冲 rx/tx=4096,中断绑核
  •  验证:strace -e setsockopt -p <pid>;tcpkali P50/P99
  •  监控/回滚:仪表盘+灰度开关;跨境链路用 MTR 连跑校验
  • 有任何现场“奇葩抖动”,把抓包、strace 截图和 MTR 发我,我给你“机房味”的诊断建议。
目录结构
全文