背景与问题界定

某在线广告平台的实时竞价系统(RTB)运行在 100+ 台物理服务器上,Linux 内核版本已升级到 5.15 LTS,但性能表现始终达不到预期。一方面,netstat -s 显示大量的 TCPLossTCPTimeouts,网络协议栈参数均为操作系统默认值——这些默认值针对通用桌面场景设计,不适合高并发、低延迟的广告竞价场景。另一方面,容器技术从 Docker 切换到 containerd 后,cgroup v1 的层级复杂导致资源统计不准确,同一个 Pod 内的 Java 进程和 Sidecar 代理在 CPU 资源上相互争抢,缺乏精细化的限制能力。需要一套系统性的内核参数调优和 cgroup v2 迁移方案。

目标拆解与工程约束

  1. 网络协议栈极致优化:RTB 系统对网络延迟苛刻到微秒级,TCP 连接的延迟确认(delayed ack)、Nagle 算法、TIME_WAIT 累积等默认行为必须被调整,目标是单次请求的 TCP 握手延迟降低 30%。
  2. 内存管理精细化:广告竞价算法使用大量 mmap 文件映射和共享内存,需要调整 vm.swappinessvm.dirty_ratio 和 NUMA 内存分配策略(numa_balancing)来减少内存回收抖动和跨 NUMA 节点访问延迟。
  3. cgroup v2 统一资源管控:从 cgroup v1 迁移到 v2,利用其单一层级树(Single Hierarchy)消除 v1 中不同子系统可能挂在不同控制器下的混乱,实现 CPU、内存、IO 的统一 resource controller。
  4. IO 优先级隔离:容器内的日志写入、监控数据采集和业务读写共享同一块 SSD,需要通过 cgroup v2 的 IO Controller(io.weightio.max)实现 IO 带宽的 QoS 保障,避免日志洪峰冲垮数据库 IO。

方案设计

网络协议栈调优是我们投入最多的领域。根据 RTB 业务的流量特征(短连接、高并发、单次数据量小),我们将以下 sysctl 参数纳入集群基线配置:

# 网络核心参数
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 50000

# TCP 优化
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
net.ipv4.tcp_sack = 0      # 广告场景下丢包率低,禁用 SACK 减少 CPU 开销
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 10
net.ipv4.tcp_slow_start_after_idle = 0

# 内存管理
vm.swappiness = 10
vm.dirty_ratio = 20
vm.dirty_background_ratio = 5
vm.max_map_count = 262144

关键在于 tcp_sack = 0 这个决策——虽然 RFC 2018 建议启用选择性确认,但在我们的内网低丢包环境中,SACK 处理的 CPU 开销超过了其收益。通过 A/B 测试,关闭 SACK 后内核 TCP 栈的 CPU 占用降低了 12%。

cgroup v2 的迁移采用全节点统一升级策略。首先确认内核支持 cgroup v2(5.2+)且 systemd 版本 >= 242。修改 /etc/default/grub 添加 systemd.unified_cgroup_hierarchy=1,重启后统一启用 cgroup v2。Kubernetes 从 1.25 开始默认支持 cgroup v2,我们利用 cgroupfs 驱动感知容器内的资源限制情况。

在 cgroup v2 的 IO 管控上,我们使用 io.weight 为不同优先级的容器分配 IO 带宽权重:

# 给数据库容器高 IO 权重 (100) vs 日志采集器低权重 (10)
echo "8:0 weight=100" > /sys/fs/cgroup/kubepods/besteffort/pod-db-123/io.weight
echo "8:0 weight=10" > /sys/fs/cgroup/kubepods/besteffort/pod-logger-456/io.weight

实施路径与关键决策

  • 第一步:搭建内核参数基线管理平台,通过 Ansible Playbook 统一管理所有节点的 sysctl 配置,使用 sysctl -p 热加载,关键参数(如 vm.swappiness)变更后观察 48 小时确认无副作用。
  • 第二步:在测试环境中验证 cgroup v2 兼容性——重点测试 Java JVM 的 -XX:+UseContainerSupport-XX:ActiveProcessorCount 在 cgroup v2 下的行为是否符合预期。
  • 第三步:灰度迁移 10% 的节点到 cgroup v2,对比节点的内存使用统计精度——在 cgroup v2 中通过 memory.current 获取更准确的内存使用量(相比 v1 的 memory.usage_in_bytes 减少了 kmem 和 page cache 的双重计数问题)。
  • 第四步:配置 Prometheus node_exporter 采集 cgroup v2 指标,特别是 node_cgroup_* 用于对比 v1 和 v2 下资源统计的差异,监控迁移前后的 OOM Killer 触发频率和 Pod 稳定性。

验证指标与可持续迭代

调优效果量化指标:网络 P99 延迟从 12ms 降至 7ms;内核软中断(/proc/softirqs)的 NET_RX 数量降低 35%;OOM Killer 触发频率从每月 15 次降至 2 次;全节点内存使用统计精度偏差从 12% 降至 2%(cgroup v2 改进了计数精度)。每季度执行一次内核参数健康度评估,使用 sysctl -a 对比基线和当前值,自动检测被应用或运维人员临时修改的参数并恢复。

工程落地思考

内核调优最容易被忽视的是"默认值为什么是那个数字"——大多数默认值是 20 年前在 64MB 内存的硬件上设定的,在现代服务器上完全不适用。但调优也不是数字越大越好,例如 net.core.somaxconn = 65535 意味着内核要为每个 listen socket 分配接近 2MB 的预分配内存,在连接数高的场景下值得,但对小规模服务反而浪费。cgroup v2 的迁移解决了 v1 时代一个经典问题:在 v1 中,memory.limit_in_bytes 不会限制 kmem(内核内存),导致容器内运行的内核相关操作(如 fork 大量子进程)可能消耗主机内存而不被 cgroup 约束——v2 的 memory.max 统一限制用户和内核内存,这才是真正意义上的资源隔离。