做 TiDB 部署和调优这些年,我养成一个习惯:遇到集群性能不对劲,先不急着看 SQL 慢日志,也不急着调 TiKV 的 RocksDB 参数,而是先翻一遍操作系统参数。尤其是你刚装完一台 64G 内存、NVMe 固态的 TiKV 节点,压测时延迟却忽高忽低,CPU 占用也不规律,这种时候八成不是数据库配置的问题,而是内核参数还在用默认值扛着数据库负载。TiDB 是分布式架构,底层依赖操作系统提供的文件、内存、网络、进程调度能力,这部分短板不补,上层再优化也像在沙地上盖楼。
这篇文章我直接把“tidb-操作系统参数优化”这件事拆开讲清楚。我会按照内存与 CPU、磁盘与文件系统、网络协议栈、配置落地与验证、问题排查五个层面来梳理,全部给到可以直接抄的配置值和验证命令。无论你是在测试环境做功能验证,还是在生产环境准备正式上线,这部分内容都值得你对着实际操作一遍。
1. 先弄明白:TiDB 到底对操作系统敏感在哪
1.1 三种节点角色,对系统资源的需求完全不同
很多人一上来就照着网上找的“通用高并发服务器优化”一通乱改,这是最容易踩坑的开端。TiDB 集群里有三种角色:TiDB Server 负责 SQL 解析和计算下推,对 CPU 和网络延迟敏感;TiKV 负责实际数据存储,对磁盘 IOPS、内存分配、文件句柄数极其敏感;PD 负责全局时间戳和调度,对磁盘延迟和时钟稳定性敏感。三种角色混用一套系统参数,必然顾此失彼。
举个例子,TiKV 底层用 RocksDB 做存储引擎,RocksDB 重度依赖操作系统的 page cache、文件系统 writeback 机制以及内存映射效率。如果系统的 vm.swappiness 保持默认值 60,当内存使用率波动时,内核会倾向把 TiKV 的部分热数据页换到 swap 分区上,一次换入换出的时间消耗可能是几毫秒甚至几十毫秒,对 TiKV 这种延迟敏感的存储节点就是灾难。而同样的问题在 TiDB Server 节点上就没那么致命,因为 TiDB Server 本身不落盘,主要消耗的是计算和网络资源。
所以在做系统参数优化之前,首先要明确一个原则:针对不同的节点角色做差异化配置。存储节点优先考虑磁盘和内存回收行为,计算节点优先考虑 CPU 调度和网络栈吞吐,PD 节点优先考虑磁盘延迟和文件系统挂载方式。不要一套配置走天下。这里我给的建议是,生产环境至少准备两套模板:一套面向 TiKV/PD,一套面向 TiDB Server,如果条件允许,PD 单独再调细一点。
1.2 为什么默认内核参数扛不住 TiDB 负载
Linux 默认内核参数是面向通用服务器和应用场景设计的。默认情况下,系统不会为一类数据库预留大量文件句柄,也不会把磁盘 I/O 调度器设置成延迟优先模式,更不会特意扩大 TCP 连接队列。这些默认值对应的是“通用的 web 服务器、文件服务器、离线计算任务”等常规负载,它们的特点是吞吐量优先、容忍延迟抖动、连接数相对可控。
但 TiDB 是另一类负载。TiKV 的 Raft log 写入是强同步的,写入路径上一个 fsync 的延迟波动会直接反映到事务提交耗时上;TiKV 之间的 gRPC 通信有频繁的短连接和长连接混合流量,网络栈处理能力不足会出现连接建立慢、 Waiting 队列堆积;TiDB 的每个 SQL 查询都会产生大量临时文件和内存映射,文件描述符上限不够时,错误日志里会出现 "too many open files" 之类的问题。这些都不是 TiDB 自身参数能解决的,只有操作系统层面把它当作数据库服务器来专门配置,才能把底层能力释放出来。
另一个容易被忽略的点是,TiDB 集群通常部署在云虚拟机或物理机的数据盘上,这些盘的性能已经非常强,但内核参数如果不配合,比如磁盘预读策略和脏页回写比例设置不合适,再强的硬件也发挥不出应有性能。这就好比给赛车配了顶级轮胎,但悬挂系统还是轿车设定,过弯照样慢。
2. 内存与 CPU:TiKV 延迟问题的第一来源
2.1 vm.swappiness:到底设 0 还是 1
说到 TiKV 节点调优,vm.swappiness 是绕不开的第一个参数。这个参数控制内核在使用匿名内存时,有多大的倾向性去把内存页换到 swap。默认值是 60,含义是内核会相对积极地使用 swap 来释放内存页缓存。对大多数应用来说这个值没问题,但对 TiKV 来说问题大了:TiKV 的 block cache 和 RocksDB 的 memtable 都是热数据,被换出到 swap 后再读回来,代价非常高。
关于这个参数有两个流派:一派主张直接设 0,彻底禁用 swap 倾向;另一派主张设 1,保留极小的 swap 行为以便应对极端内存压力。从 TiDB 官方文档和社区实践来看,普遍建议是设 1。原因很简单,设 0 意味着内核在内存压力极大时宁可 OOM kill 也不会换页,而 TiKV 这种大内存应用一旦触发 OOM,后果是整个 Raft group 的副本短暂掉线,多副本场景下虽然能恢复,但瞬间的抖动对上层业务是有感知的。设 1 则保留了一个非常保守的兜底空间,在极端情况下内核能缓慢回收 swap 中的冷页,而不是直接杀进程。
实操命令:
sysctl -w vm.swappiness=1 echo 'vm.swappiness = 1' >> /etc/sysctl.d/99-tidb.conf顺便说一句,很多人在云主机上根本没有配置 swap 分区,这种情况下设 0 和设 1 其实差别不大,但为了统一配置管理和可预期性,建议还是统一设 1 并保留一个 swap 文件兜底,容量不用大,几个 G 就够。
2.2 透明大页必须关,CPU 调频模式必须切到性能优先
透明大页(Transparent Huge Pages,THP)是 Linux 内核的一个特性,它试图自动把连续的内存页合并成 2MB 的大页,从而减少 TLB miss。听起来很美,但在数据库场景下它经常成为性能杀手。原因是 THP 的内存分配和合并过程需要后台内核线程做内存规整(memory compaction),这个过程会触发大量的 CPU 占用和锁竞争,导致分配延迟剧烈抖动。RocksDB 对内存分配延迟极其敏感,一块内存分配的抖动就可能让 Raft 的心跳超时。
关闭 THP 有两种方式:一是运行时直接修改 sysfs 接口,二是通过内核启动参数彻底禁用。推荐两种都做,因为有些发行版即使修改了 sysfs,重启后会被系统服务重新开启。
echo never > /sys/kernel/mm/transparent_hugepage/enabled echo never > /sys/kernel/mm/transparent_hugepage/defrag然后在 GRUB 的内核启动参数里追加:
transparent_hugepage=never修改 grub 配置后需要重启系统才能生效,如果你在云环境不方便重启,至少保留 sysfs 的运行时修改,确保当前会话生效。
CPU 调频模式同样值得关注。现在很多服务器 CPU 都支持动态调频,默认的 powersave 或 ondemand 模式会根据负载动态调整 CPU 主频。对 TiDB 这种负载波动大的系统来说,CPU 主频变化带来的性能差异可能达到 20% 以上,而且调频过程本身有延迟,高负载突然到来时 CPU 不能立即升到最高频率,会造成响应延迟。建议把 TiKV 和 TiDB Server 节点 CPU 调频模式固定到 performance。
cpupower frequency-set -g performance如果你的系统没有 cpupower 工具,也可以在 BIOS 层面或虚拟化平台的安全组配置里把 CPU 性能模式固定。物理机优先在 BIOS 设置,云主机则确认下虚拟化平台是否允许设置性能模式。
2.3 文件描述符和进程数限制:不设够直接翻车
TiDB 集群跑起来之后,文件描述符消耗远比想象中快。每个 gRPC 连接至少占用一个 fd,TiKV 打开的数据文件可能上百个,加上 WAL、临时排序文件、日志文件,一台 TiKV 节点轻松突破几万个 fd。系统默认的 ulimit -n 通常是 1024 或 65535,这个量级在数据库集群面前完全不够。
我建议的配置是把 soft 和 hard 限制都调到很大,同时把内核级的 file-max 也调上去。注意,/etc/security/limits.conf 的配置只对通过 PAM 登录的会话生效,如果你的 TiDB 进程是用 systemd 管理的,还需要在 service 文件里显式声明 LimitNOFILE。
cat >> /etc/security/limits.conf <<'EOF' * soft nofile 1048576 * hard nofile 1048576 * soft nproc 1048576 * hard nproc 1048576 EOF同时修改 sysctl:
fs.file-max = 2097152 fs.nr_open = 2097152这里有个容易忽略的坑:nproc 的限制在某些容器环境或 systemd 环境下不会走 limits.conf,而是受 cgroup pids 限制影响。如果部署在容器里,要检查容器的 pids.max 是否足够。不然你会看到明明 limits.conf 已经改到了很大,进程一启动还是报 Resource temporarily unavailable。
vm.max_map_count 这个参数也很多人漏掉。默认值是 65530,对 TiDB 这种大量使用 mmap 的应用来说,很容易触发 “max_map_count” 超限的错误。建议调高到 262144 甚至 1048576,这是一个怎么调大都不会有副作用的内核参数。
3. 磁盘与文件系统:TiKV 写入路径的隐形瓶颈
3.1 挂载参数优化:noatime 和 nodiratime 是基础
文件系统挂载参数听起来很底层,但对 TiKV 这种大量小文件读写的应用影响明显。Linux 默认挂载通常会记录文件访问时间(atime),每次读取文件都要更新这个时间戳,产生一次额外的写操作。对数据库数据文件这种读多写少的场景,这个行为纯粹是浪费 IO。
建议在 /etc/fstab 中对数据盘挂载参数追加 noatime,nodiratime。如果数据盘已经挂载,可以重新挂载生效:
mount -o remount,noatime,nodiratime /data这块还要考虑文件系统本身的选择。TiDB 官方对 TiKV 数据盘的推荐是 ext4 或者 xfs,这两者在数据库场景下都有成熟的应用案例。ext4 胜在稳定和兼容性,xfs 胜在大文件和高并发下表现更稳。从我自己的实际测试来看,同样是 NVMe 盘,xfs 在 64 线程并发小文件写入时抖动更小,但 ext4 在单线程顺序读的性能峰值更高。如果你拿不准,就用 ext4,它是最稳妥的选择。
3.2 I/O 调度器:NVMe 时代选 none 几乎没悬念
I/O 调度器负责把块设备的读写请求排序和合并。传统机械硬盘时代,我们习惯用 deadline 或 cfq,因为磁头寻道需要优化请求顺序。但到了 NVMe SSD 时代,IOPS 足够高,随机读写能力已经接近内存,调度器反而会带来额外的 CPU 开销和排队延迟。对 TiKV 这种延迟敏感的数据库,NVMe 盘使用 none 调度器基本是共识。
查看和修改调度器:
cat /sys/block/nvme0n1/queue/scheduler echo none > /sys/block/nvme0n1/queue/scheduler但这个修改重启后会失效,要持久化的话需要写一个 systemd 服务,或者在启动脚本中执行。网上有现成的 udev 规则写法,可以绑定设备名执行:
cat > /etc/udev/rules.d/60-io-scheduler.rules <<'EOF' ACTION=="add|change", KERNEL=="nvme*", ATTR{queue/scheduler}="none" EOF如果用的是普通 SATA SSD,deadline 也可以,但实测下来 none 在大多数场景下依然更稳。这里我建议做个微基准测试,用 fio 分别压测两种调度器下的 4K 随机写延迟,用数据说话,比听别人推荐靠谱得多。
3.3 脏页回写参数:别让内核憋大招
vm.dirty_ratio 和 vm.dirty_background_ratio 控制脏页占内存的比例。默认值分别是 20 和 10,也就是说脏页占到内存 20% 时进程写操作会被阻塞强制回写,占到 10% 时后台内核线程开始回写。这个默认值对数据库有个负面效应:文件系统会攒着大量脏页一次性刷盘,造成 IO 尖峰,写入延迟出现周期性恶化。
TiKV 的写入是持续性负载,我们希望脏页尽快回写而不是积攒到阈值后集中爆发。所以建议把 dirty_background_ratio 调低,dirty_ratio 也适度调低,让内核更积极地把脏页刷下去。
vm.dirty_background_ratio = 5 vm.dirty_ratio = 10不过这里有个细节,当系统总内存很大(比如 512G)时,比值类型的参数对应的绝对内存量还是很大。现代内核支持以字节为单位的 vm.dirty_background_bytes 和 vm.dirty_bytes,这两个参数是互斥的,设置了 bytes 就不能设置 ratio。大数据量机器上建议使用 bytes 精确控制,例如设置 background_bytes 为 8G,dirty_bytes 为 16G,具体值需要结合数据盘性能和业务写入速度来调整。
调整后建议观察一段时间,如果 TiKV 的 write stall 现象减少、写入延迟的 p99 更稳定,说明方向是对的。
4. 网络协议栈:分布式通信的生命线
4.1 TCP 连接队列和端口范围,决定集群连接的承载上限
TiDB 集群内每对节点之间都会建立多个 gRPC 连接,PD 与 TiKV、TiDB 与 TiKV、TiDB 与 PD 之间都有长期连接。同时还要处理来自应用端的短连接和连接池里的连接。默认的 TCP 连接队列长度和端口范围都很保守,连接数上来之后会出现排队和端口被占满的问题。
关键参数是 net.core.somaxconn 和 net.ipv4.tcp_max_syn_backlog。somaxconn 默认是 128,这是 accept 队列的最大长度,连接建立速度快但应用处理不过来时,新建连接会在这里堆积。TiDB 的 connection 处理协程通常很高效,但突发流量下 128 明显不够看。建议:
net.core.somaxconn = 4096 net.ipv4.tcp_max_syn_backlog = 8192端口范围参数 net.ipv4.ip_local_port_range 默认在 32768-60999,当节点间通信频繁建立新连接时,这个范围的端口数可能不够用。客户端程序会在这个范围内取临时端口,端口耗尽会表现为 connect timeout 或者 connect: cannot assign requested address。建议扩大到 1024-65535:
net.ipv4.ip_local_port_range = 1024 65535这里要提醒一句,端口范围调大后会占用更多内核内存用于 socket 管理,但现代服务器上这点内存消耗可以忽略不计。
4.2 网络缓冲区:告别的“太小”问题
Linux 网络协议栈默认的读写缓冲区相对保守。net.core.rmem_max 默认 212992(约 208KB),wmem_max 同样。对于 TiKV 之间需要传输的大批量数据块(比如一次批量扫表返回的 KV 数据),这个缓冲区大小会导致吞吐量受限和延迟增加。
建议把最大缓冲区调大到 16MB 或 32MB:
net.core.rmem_max = 16777216 net.core.wmem_max = 16777216 net.core.rmem_default = 262144 net.core.wmem_default = 262144注意调整的是 max 而不是 default,因为 TCP 连接会根据实际负载动态调节缓冲区到合理范围,但上限如果卡死,性能就上不去。之前踩过坑,只调了 default 没调 max,结果吞吐量上去了缓冲区却上不去,白白踩了几个月。
TCP 拥塞控制算法也讲一讲。默认的 cubic 算法是通用场景设计,延迟敏感的内部网络环境可以尝试 bbr。BBR 在高带宽、高延迟链路上效果明显,内网低延迟场景下提升没那么夸张,但也不会变差。
net.core.default_qdisc = fq net.ipv4.tcp_congestion_control = bbr内核支持 bbr 需要 4.9 以上版本,云虚拟机 4.14 或更高内核基本都没问题。验证方式:
sysctl net.ipv4.tcp_congestion_control输出的应该是 bbr。
4.3 网卡队列和中断绑定,多核利用率的关键
现代服务器网卡普遍支持多队列 RSS,默认情况下网卡中断会分配到一个 CPU 核上,这在网络流量大时会形成单核瓶颈。TiDB 节点间的 gRPC 流量可能达到几 Gbps,单核处理中断很容易把 CPU 打满。
先看网卡支持多少队列:
ethtool -l eth0如果 Combined 队列数大于等于 CPU 核数,可以直接设置多队列并用 irqbalance 服务做动态负载均衡。但 irqbalance 有时会把中断调来调去,数据库场景下更推荐手工给中断绑定固定的 CPU 核心,减少切换开销。
ethtool -L eth0 combined 8中断号可以查看 /proc/interrupts 找到网卡对应的那一行,然后用 echo 把 CPU 亲和性写入 /proc/irq/N/smp_affinity。这类配置是硬件相关的,每台机器可能不同,我一般是写一个初始化脚本,在系统启动时自动执行。
还有一个参数容易忽略,就是网卡队列的长度 net.core.netdev_max_backlog。默认值 1000 在突增流量下会丢包。建议:
net.core.netdev_max_backlog = 163845. 可落地的配置模板与验证方法
5.1 sysctl.conf 完整模板
下面给一个标准化配置模板,适合 TiKV 节点使用。TiDB Server 节点可以去掉磁盘相关的部分,其他保持一致。
cat > /etc/sysctl.d/99-tidb.conf <<'EOF' # 内存管理 vm.swappiness = 1 vm.max_map_count = 1048576 vm.dirty_background_ratio = 5 vm.dirty_ratio = 10 # 文件与进程 fs.file-max = 2097152 fs.nr_open = 2097152 # 网络 net.core.somaxconn = 4096 net.ipv4.tcp_max_syn_backlog = 8192 net.ipv4.ip_local_port_range = 1024 65535 net.core.rmem_max = 16777216 net.core.wmem_max = 16777216 net.core.rmem_default = 262144 net.core.wmem_default = 262144 net.core.netdev_max_backlog = 16384 net.ipv4.tcp_fin_timeout = 15 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_slow_start_after_idle = 0 EOF sysctl --systemtcp_tw_reuse 这里补充说明,它允许内核在安全前提下复用 TIME_WAIT 状态的连接,对减少短连接场景下的端口占用和连接建立时间有帮助。tcp_slow_start_after_idle 设为 0 是为了让空闲后的连接不降低初始拥塞窗口,对长连接的周期请求有帮助。
5.2 参数生效与持久化验证,别改完就跑
修改完参数后一定逐项验证,不要以为 sysctl --system 执行成功就万事大吉。我见过太多次配置写进去了,但实际值没变的情况。一个经典案例是 /etc/sysctl.d/ 目录下多个配置文件冲突,后加载的配置覆盖了先加载的,或者你有同名 key 写在 /etc/sysctl.conf 里优先级不同。
正确的验证姿势是逐个确认:
| 参数 | 期望值 | 验证命令 |
|---|---|---|
| vm.swappiness | 1 | sysctl vm.swappiness |
| vm.max_map_count | 1048576 | sysctl vm.max_map_count |
| fs.file-max | 2097152 | sysctl fs.file-max |
| net.core.somaxconn | 4096 | sysctl net.core.somaxconn |
| net.ipv4.tcp_congestion_control | bbr | sysctl net.ipv4.tcp_congestion_control |
| THP enabled | never | cat /sys/kernel/mm/transparent_hugepage/enabled |
对于文件描述符限制,要确认真正运行 TiKV 的用户是否生效:
su - tidb -c 'ulimit -n'如果显示的不是预期值,检查 /etc/security/limits.conf 是否写对了用户段,以及是否有 systemd service 覆盖了 LimitNOFILE。
5.3 用监控数据验证优化收益,不要凭感觉
调优后如何确认有效,最直观的手段是用压测工具做前后对比。TiDB 社区常用的压测方式有两种:一种是用 SynchroBench 跑 tpcc 或 tpc-h 类负载,另一种是直接用 sysbench 对单表做点查和写入。我更推荐先做资源层验证,再跑业务层负载。
资源层的验证命令如下:
# 查看 swap 使用情况 vmstat 1 # 查看内存回收产生的 CPU 消耗 cat /proc/vmstat | grep -E 'pgscan|pgsteal' # 查看网络重传率 netstat -s | grep retrans观察 vmstat 的 si 和 so 列,正常情况下优化后这两列应该保持为零或者非常低。网络重传如果是 0.01% 以内属于正常,如果超过 0.1%,就要考虑网卡队列、缓冲区配置是否还有问题。
还有一个更直接的指标是 TiDB 监控面板里的 TiKV gRPC 延迟和 Raft propose 延迟。优化前如果 Raft propose 的 p99 曲线有明显周期性尖峰,优化后尖峰应该变得平缓,这正是脏页回写和 I/O 调度器优化的功劳。
6. 常见问题排查与经验沉淀
6.1 配置不生效的排查思路
遇到参数配置不生效,先按下面的路径排查。首先确认优先级问题,/etc/sysctl.d/ 下面的文件按文件名顺序加载,99-tidb.conf 的优先级最高,如果有其他文件在它之后覆盖了同一参数,就会被冲掉。用 sysctl --system 可以看到所有生效文件及其顺序。
其次要注意有些参数是只读的,例如 fs.nr_open 在某些内核版本下不能动态修改,需要重启。net.ipv4.tcp_congestion_control 也有个坑,如果内核模块没加载 tcp_bbr,写入 bbr 会报错,需要先 modprobe tcp_bbr。
最容易被忽略的是系统安全软件或虚拟化平台自带的优化配置。比如某些云厂商的镜像里已经内置了一个 tuned 服务,tuned 会周期性覆盖 sysctl 参数,你手动改完,tuned 一刷新又变回原值。解决办法是把 TiDB 节点加入 tuned 的例外列表,或者直接停用 tuned 服务。
6.2 优化过度引发的连锁反应
参数不是越大越好,这是我在实际运维中反复验证过的教训。有次我把 vm.dirty_bg_ratio 调得过低(1%),后台刷盘线程几乎不停工,导致 CPU 的 iowait 暴涨,TiKV 的 RocksDB compaction 频繁被刷盘抢占 IO。后来调回到 5% 配合 dirty_bg_bytes 精确控制才稳定下来。
还有一个典型问题是 TCP 缓冲区开得过大。如果 rmem_max 设置到 32MB 而机器内存只有 32G,当有几百个连接同时活跃时,单连接占用的内核内存可能达到十几 MB,整体内存消耗立刻上去。缓冲区大小要结合带宽和延迟计算,公式是 BDP = 带宽 × 延迟,例如万兆网卡内网延迟 0.2ms,BDP 大约是 2MB,设置 16MB 已经留了很大余量,不必继续翻倍。
6.3 我的避坑清单
最后整理一份我自己踩过坑后沉淀下来的清单,给要上生产环境的同学参考。
第一,数据盘和系统盘分离,操作系统参数中涉及挂载选项和 I/O 调度器的配置只针对数据盘,不要对系统盘做同样的特殊处理,系统盘的默认配置更稳妥。系统盘跑日志、监控、工具链,其 IO 特征和数据盘完全不同。
第二,修改内核启动参数前务必备份 grub 配置。之前某次在云主机上给 grub 追加了 transparent_hugepage=never,结果云厂商的启动脚本不兼容,重启后起不来了。后来统一改成在系统服务启动阶段写入 sysfs 的方式,虽然晚了几秒钟生效,但至少不会起不来机器。
第三,不要在一台机器上混跑 TiDB 节点和负载生成工具。压测机如果和生产节点共用同一台物理机,IO 和网络互相干扰,优化验证的结果根本没有参考价值。
第四,每次调整完参数,把前后两天的监控截图存档,标注调整了哪些参数,方便出问题时回滚对比。这能省去很多“玄学”排查时间。分布式系统的问题往往是多个因素叠加,记录能帮你定位到具体是哪个参数起了正面或负面作用。
操作系统参数优化这块,急不得,也懒不得。每个参数背后都有明确的机制和适用场景,建议在你自己的测试环境里先压一轮,把不适合理论共识的参数找出来,再上生产。毕竟每个团队的磁盘类型、网络环境、负载模型都不一样,只有亲手验证过的配置,你才敢在半夜被叫起来处理故障的时候,心里有底地说一句“这个值就是对的”。