ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Ubuntu 20.04下Redis系统级调优实战:从内核参数到CPU绑定

Ubuntu 20.04下Redis系统级调优实战:从内核参数到CPU绑定 写这篇东西之前我先简单交代一下背景。我在生产环境维护过不少 Ubuntu 20.04 上的 Redis 实例遇到过 Redis 本身配置看起来没什么问题、内存也不紧张但响应延迟就是忽高忽低的情况。折腾了一圈才发现真正卡脖子的往往不在 Redis 进程内部而在于操作系统这一层没有配合好。这篇文章就把我在 Ubuntu 20.04 服务器上做 Redis 系统级调优的完整思路和操作记录分享出来从内存、网络、磁盘、CPU 再到验证手段一条线捋清楚。内容面向运维工程师、SRE 和需要自己维护缓存服务的后端开发不求你一次全抄但至少能帮你建立一个该往哪个方向调、为什么这样调的判断框架。1. 为什么瓶颈常在 Redis 进程之外先建立正确的调优顺序1.1 一个让我改变思路的线上案例之前接手过一套 Ubuntu 20.04 服务器上的 Redis 6.2 集群业务方反馈缓存偶尔慢得像卡死但看 Redis 自身的 INFO 数据hit 率正常、内存没满、慢日志只有零星几条。当时我第一反应是网络问题然后怀疑客户端连接数最后才想到去翻系统内核参数。结果发现问题出在 TCP backlog 队列溢出让客户端频繁重连以及 THP 开启导致 fork 产生极高延迟毛刺。那次之后我养成了一个习惯Redis 性能不对劲优先查系统层而不是一上来就改 maxmemory-policy 或者加副本。这个顺序背后的逻辑其实很简单。Redis 是单线程事件循环模型绝大多数操作是纯内存计算正常情况下单实例跑十万级 QPS 并不难。一旦你发现 CPU 没打满、内存没爆但延迟上去了说明请求根本没能顺畅地到达 Redis 进程或者在到达之前就被某些内核机制拖慢了。这些阻塞点恰恰集中在系统调优可以覆盖的范围内。1.2 调优前必须收集的三类基线数据调优不是拍脑袋先花半小时把基线数据拉齐。我用得最多的三样第一类Redis 自身视角redis-cli info stats里的 instantaneous_ops_per_sec、total_net_input_bytesredis-cli --latency -h 127.0.0.1 -p 6379跑几分钟拿到延迟分布再开SLOWLOG GET 50看看是否有持久化或大 key 操作混进来。第二类系统资源视角vmstat 1 30看 si/so 是否非零mpstat -P ALL 1看每个核的使用率ss -s看 socket 状态统计iostat -x 1看磁盘 util 和 await。这些命令能帮你快速排除资源瓶颈。第三类内核参数视角sysctl -a | grep -E somaxconn|overcommit|swappiness|tcp_tw|max_map_count记录当前值。很多调优文章给了参数就直接让你敲但实际调优之前你得先搞清楚默认值是什么、是否已经被改过否则出了问题都不知道回滚到哪。基线数据收集完才能进入下一步。记住一个原则只调你能解释清楚的参数解释不清楚的宁可不碰。2. 内存子系统THP、overcommit 与 swap 的三个关键开关2.1 关闭 Transparent Huge Pages别让 fork 成为延迟毛刺的元凶Transparent Huge PagesTHP是 Linux 内核的自动大页机制初衷是减少 TLB miss对某些大内存顺序访问的应用是好事但对 Redis 来说却是实打实的性能杀手。原因在于 Redis 做 RDB 持久化或主从全量同步时要调用 fork()而 fork 之后依赖写时复制Copy-On-Write来共享内存页。默认 4KB 页表下父进程改写一个页只需要复制 4KB但如果开启了 THP内核可能把连续的 2MB 区域合并成一个 Huge Page那么 COW 的最小单位就变成了 2MB。也就是说本来只改一个 4KB 的 key却要复制 2MB 的内存fork 后的瞬时内存占用暴涨轻则产生明显延迟重则触发 OOM。在 Ubuntu 20.04 上我推荐直接在 GRUB 层面永久关闭。编辑/etc/default/grub在GRUB_CMDLINE_LINUX_DEFAULT里加上transparent_hugepagenever然后执行sudo update-grub sudo reboot重启后确认生效cat /sys/kernel/mm/transparent_hugepage/enabled # 输出应包含 [never]如果你不想重启可以临时关闭并写入 sysfsecho never | sudo tee /sys/kernel/mm/transparent_hugepage/enabled echo never | sudo tee /sys/kernel/mm/transparent_hugepage/defrag但注意这只是临时方案重启后必须依赖 GRUB 或 systemd 服务来重新设置。更稳妥的方式是在/etc/rc.local或 systemd 单元里做持久化不过 GRUB 方法最干净我实测下来没有副作用。顺嘴提一句defrag 这个开关我也建议一并关掉它控制的是 THP 的内存规整行为有时候会比 THP 本身带来更隐蔽的延迟抖动。2.2 vm.overcommit_memory1 背后的内存账本逻辑Redis 官方文档对vm.overcommit_memory的建议是设为 1这不是拍脑袋。Linux 默认的 overcommit 策略是 0即启发式过度分配内核根据当前空闲内存和映射情况猜测性地允许或拒绝内存申请。对于普通应用来说够用但 Redis 在 fork() 做后台保存时需要为整个进程地址空间预留 COW 空间如果内核认为剩余内存不足以覆盖 fork 的潜在需求就可能拒绝 fork导致 BGSAVE 失败。把 overcommit 设为 1 意味着内核无条件允许内存超卖把有没有足够物理内存的判断权完全交给应用自己。Redis 对内存的管理本来就比内核更清楚它启动时会根据 maxmemory 做自我约束所以设为 1 是合理的。echo vm.overcommit_memory 1 | sudo tee /etc/sysctl.d/99-redis-tuning.conf sudo sysctl -p /etc/sysctl.d/99-redis-tuning.conf这里有一个很容易被忽略的细节如果服务器的物理内存本身就不大而 Redis 的 maxmemory 又设得很贴近物理内存总量那么即使 overcommit 设为 1fork 瞬间仍然可能因为内存不足而导致系统进入极端状况。所以我在规划 Redis 内存上限时通常会给系统预留至少 20% 的物理内存余量专门用于应付 fork 的 COW 峰值。尤其是开启了 AOF 重写或频繁做 BGSAVE 的场景余量更要给足。2.3 swappiness 与 NUMA 内存分配策略Ubuntu 20.04 默认的vm.swappiness 60这个值对 Redis 来说太高了。60 意味着内核在内存压力不大时也可能把一些不常用的内存页换到 swap而 Redis 一旦有内存页被换出访问延迟会直接飙升好几个数量级。缓存系统的核心竞争力就是快任何换页行为都不可接受所以生产环境我统一设为 0 或 1echo vm.swappiness 1 | sudo tee -a /etc/sysctl.d/99-redis-tuning.conf设成 0 是完全禁止主动换出但内核在极端情况下仍可能换出内存页设成 1 是允许非常轻微地使用 swap避免 OOM。对于 Redis 我倾向设 1既能防止被换页又留了一点缓冲空间。还有一个和内存强相关的点是 NUMA。如果服务器是双路 CPURedis 默认可能在 Node 0 上分配内存但进程运行时被调度到 Node 1导致跨 NUMA 节点访问内存延迟会比本地访问高出不少。最直接的检查方法是numactl --hardware如果确实是多 NUMA 节点架构我会给 Redis 做 node 绑定具体方法留在第 5 部分详细说。这里先记住结论Redis 这种低延迟服务内存访问位置至关重要放任内核自由调度在 NUMA 架构下很容易吃暗亏。3. 网络栈参数从 accept 队列到 TIME_WAIT 的逐层疏通3.1 somaxconn 与 tcp-backlog两者必须同步修改Redis 通过 TCP 对外提供服务时内核为每个监听 socket 维护两个队列一个是 SYN 队列半连接一个是 accept 队列全连接。应用层调用 accept() 取走连接之前全连接直接排在内核的 accept 队列中。net.core.somaxconn就是这个队列长度的上限而 Redis 配置文件里的tcp-backlog参数则是 Redis 自己请求的队列长度。内核最终取的是两者中的较小值。Ubuntu 20.04 默认的somaxconn是 128Redis 6.x 默认tcp-backlog是 511。如果保持默认实际队列长度只有 128。在高并发短连接场景下accept 队列一旦打满新连接会被内核直接丢弃客户端只能重试表现为连接建立慢、请求延迟升高。解决办法是两边同时调大echo net.core.somaxconn 1024 | sudo tee -a /etc/sysctl.d/99-redis-tuning.conf同时在 redis.conf 中设置tcp-backlog 1024修改后重启 Redis用ss -ltnp | grep 6379查看 Send-Q 确认队列长度变成 1024。我一般建议 1024 起步如果短连接特别多可以调到 4096但要注意 somaxconn 太大会占用内核内存不要无脑拉高。3.2 TIME_WAIT 复用与回收的现实取舍短连接场景下客户端主动断开连接后服务端 socket 会进入 TIME_WAIT 状态并保持 2MSL默认约 60 秒。高 QPS 时大量 TIME_WAIT 连接堆积可能导致端口资源耗尽或新连接建连变慢。我常用的一组参数是net.ipv4.tcp_tw_reuse 1 net.ipv4.ip_local_port_range 1024 65535 net.ipv4.tcp_fin_timeout 30tcp_tw_reuse允许内核在安全条件下复用 TIME_WAIT 状态的连接对 Redis 这种服务端被动关闭的场景效果明显。这里我必须专门提醒一句千万不要开net.ipv4.tcp_tw_recycle。这个参数在 Linux 4.12 内核中已经被移除但在 Ubuntu 20.04 之前的教程里还大量被提到。它依赖时间戳在 NAT 网络后会因为不同客户端的时间戳不一致导致服务端丢弃合法数据包。我在测试环境踩过这个坑症状是一会通一会不通排查起来极其折磨人。Ubuntu 20.04 用的内核是 5.4虽然参数还在兼容列表里但它已经不具备实际效果开了还可能引发怪问题。ip_local_port_range默认是 32768 到 60999对高并发客户端来说可用源端口偏少。扩大到1024 65535能降低端口耗尽风险。tcp_fin_timeout调短一点可以让内核更快回收关闭的连接。3.3 网卡中断与软中断均衡容易被忽略的隐性瓶颈很多人只调 sysctl却忘了物理层的中断处理。现在服务器网卡基本都是多队列每个队列可以绑定不同的 CPU 核心来处理收包中断。如果中断都堆在同一个核心上软中断处理会抢占 Redis 用户态线程的执行时间延迟就会出现周期性抖动。检查方法cat /proc/interrupts | grep eth0按 CPU 列看每个核的中断数是否均衡。如果明显偏向某一个核优先开启网卡的 Receive Side ScalingRSS。用 ethtool 查看sudo ethtool -l eth0如果 Combined 队列数大于 1说明多队列可用否则可以开启ethtool -L eth0 combined N来启用多队列N 不超过 CPU 核数。另外也可以检查 RPS/XPS 是否配置。对大多数业务量没到百万级 QPS 的场景做好多队列均衡就足够了不需要再手动绑中断亲和性。4. 持久化与文件系统RDB 与 AOF 隐藏的磁盘放大问题4.1 磁盘调度器选型与预读窗口调整Redis 本身是内存数据库但 RDB 快照和 AOF 日志的写入依然依赖磁盘。磁盘调度器决定了 IO 请求在内核队列里的排队策略选错了会让持久化操作拖慢主线程。Ubuntu 20.04 上查看当前调度器cat /sys/block/sda/queue/scheduler如果是 SSD建议设为none也就是 noop因为 SSD 没有机械寻道问题不需要复杂的调度排序如果是机械硬盘建议保留或设为mq-deadline保证读写请求的公平性。修改方法echo none | sudo tee /sys/block/sda/queue/scheduler这个修改重启后失效需要借助 udev 规则或 systemd 服务持久化。在/etc/udev/rules.d/60-iosched.rules中写ACTIONadd|change, KERNELsd*, ATTR{queue/scheduler}none另一个容易被忽视的是块设备预读read-ahead大小。Redis 的持久化文件大多是顺序写、少量随机读预读窗口默认 128KB 到 256KB 反而不一定有好处。对 Redis 场景我会用sudo blockdev --setra 16 /dev/sda调成 16KB 左右减少不必要的预读放大。这个同样需要持久化配置建议写成开机脚本。4.2 noatime 挂载参数与 AOF 落盘策略每次文件访问都会更新 atime 元数据虽然现代内核有 relatime 优化但对 Redis 这种高频率小文件读写的场景减少不必要的元数据更新依然有意义。在/etc/fstab中给 Redis 数据盘挂载参数加上noatime然后重新挂载sudo mount -o remount,noatime /dataAOF 的落盘策略我一般建议保持appendfsync everysec这是性能和可靠性的平衡点。如果一个请求一个 fsyncalwaysRedis 主线程会被磁盘 IO 拖死如果完全交给内核no崩溃时最多丢一秒到数秒的数据对缓存系统来说可能还能接受。但在系统调优层面真正值得做的是把 AOF 文件放到独立的磁盘上。如果 RDB、AOF、系统日志、swap 全挤在同一块盘上持久化高峰时 IO 竞争会立刻反映在 Redis 响应延迟上。4.3 单机多实例时的目录布局与 IO 隔离一台物理机上跑多个 Redis 实例是省成本的做法但如果所有实例的 RDB/AOF 都写在同一个目录磁盘 IO 会互相干扰。我的做法是为每个实例配置独立的 AOF 目录尽量分散到不同磁盘或分区。RDB 保存目录和 AOF 目录分离避免同一时间 fork 和 fsync 争抢。如果只有一块 SSD至少把 Redis 数据文件放在单独分区与系统日志、临时文件隔离。有条件的情况下用 cgroup 的 blkio 控制器限制低优先级实例的 IO 带宽防止某个实例做 BGSAVE 时拖垮同机其他实例。很多人以为 Redis 优化就是调内存和网络磁盘这层经常被忽略直到某次 AOF 重写把所有请求都打到几百毫秒才后悔。提前做好 IO 隔离收益是实打实的。5. CPU 与进程调度让 Redis 待在它该待的核心上5.1 NUMA 绑定避免跨节点内存访问惩罚Redis 对内存延迟极度敏感在双路 CPU 的服务器上如果进程跑在 Node 0 但内存分配在 Node 1访问延迟会明显增加。numactl --hardware能看到节点的内存和 CPU 分布。绑定命令示例sudo numactl --cpunodebind0 --membind0 -- /usr/bin/redis-server /etc/redis/redis.conf含义是进程的 CPU 和内存都优先从 Node 0 分配。但这里有一个矛盾点如果 Redis 实例的内存占用非常大比如超过单个 NUMA 节点的物理内存硬绑一个节点会导致内存不足。这种情况下你要么拆分实例要么干脆不绑内存、只绑 CPU。我自己遇到超过 64GB 内存的单实例时倾向于关掉 NUMA 绑定靠内核的默认策略分配因为跨节点访问的延迟损失比 swap 或 OOM 小得多。5.2 对 irqbalance 的重新审视默认服务不一定是好服务Ubuntu 20.04 默认启用 irqbalance它的职责是把硬件中断尽可能均匀地分配到各个 CPU 核心避免中断集中在单核。听起来很合理但对 Redis 这类低延迟服务来说irqbalance 的做法可能帮倒忙它每隔 10 秒重新评估中断分配把网卡中断从一个核心迁移到另一个核心迁移瞬间缓存亲和性被破坏Redis 的延迟就会出现周期性尖刺。我在压测时见过 P99 稳定在 2ms但每 10 秒冒出一个 50ms 的尖刺最后锁定到 irqbalance 的中断迁移上。处理方案有两种一种直接停掉 irqbalancesudo systemctl disable --now irqbalance另一种更精细手动把网卡队列中断绑定到 Redis 所在核心之外的专用核上彻底隔离。对大多数中小型服务直接停掉 irqbalance 就够了中断均衡交给网卡自身的 RSS 能力处理。5.3 用 systemd CPUAffinity 固定 Redis 核心生产环境我不太喜欢直接 taskset 启动 Redis因为重启后容易忘。更规范的做法是在 systemd 单元里配置 CPUAffinity。Ubuntu 的 Redis 服务默认使用/lib/systemd/system/redis-server.service覆盖配置写在/etc/systemd/system/redis-server.service.d/override.conf[Service] CPUAffinity2,3 CPUSchedulingPolicyrr CPUSchedulingPriority99CPUAffinity2,3表示 Redis 只允许在 CPU 2 和 3 上运行CPUSchedulingPolicyrr配合高优先级可以让 Redis 线程优先获得 CPU 时间片。配置完执行sudo systemctl daemon-reload sudo systemctl restart redis-server需要注意如果 Redis 所在服务器还有 NGINX、业务进程等其他负载不要把 CPU 全部绑给 Redis否则其他服务会缺乏 CPU 时间片引发摘桃子式的全局问题。我一般是把 Redis 单独安排在物理核上和业务进程错开。另外如果 CPU 支持调频建议把 governor 设为 performance 模式减少 CPU 频率动态变化带来的延迟抖动sudo cpupower frequency-set -g performanceUbuntu 20.04 上可能需要先安装linux-tools-common和对应版本的linux-tools-$(uname -r)。6. 参数落地与效果验证从 sysctl 到 redis-benchmark 的完整闭环6.1 sysctl 配置文件规范与原子生效我习惯把所有 Redis 相关内核参数集中在一个文件里而不是分散修改/etc/sysctl.conf。这样便于版本管理和回滚。推荐创建/etc/sysctl.d/99-redis-tuning.conf# Redis 系统调优参数 # 内存 vm.overcommit_memory 1 vm.swappiness 1 vm.max_map_count 262144 # 网络 net.core.somaxconn 1024 net.ipv4.tcp_tw_reuse 1 net.ipv4.ip_local_port_range 1024 65535 net.ipv4.tcp_fin_timeout 30 net.ipv4.tcp_max_syn_backlog 1024然后统一加载sudo sysctl -p /etc/sysctl.d/99-redis-tuning.confvm.max_map_count默认是 65530当 Redis 实例的 key 数量极多或者使用碎片整理时可能触及映射数量上限调大到 262144 是常见做法但如果你确认没有遇到映射数问题也可以不调。要注意 sysctl 修改是即时生效的要确认当前运行时值可以用sysctl vm.overcommit_memory校验。所有参数应该先在测试环境跑 48 小时以上再上生产别一股脑全推到线上。6.2 用 redis-benchmark 量化调优前后差异没有数据支撑的调优都是心理安慰。我在完成上面所有修改后会用 redis-benchmark 做一个标准化的前后对比。常用命令redis-benchmark -h 127.0.0.1 -p 6379 -t set,get -n 1000000 -c 50 -P 16参数说明-t set,get只测这两个最核心操作-n 1000000是请求总数-c 50是并发连接数-P 16是管道批量数。管道数别设太大否则测的是网络吞吐而不是真实延迟我一般限制在 16。关键看两个指标requests per second和延迟分布。如果只关心最差情况直接在客户端对 Redis 的 IP 跑redis-cli --latency -h server-ip -p 6379 -i 1运行几分钟观察 avg、p50、p99。调优前后各录一份对比延迟和吞吐变化。下面是我在某台 4C8G 机器上做过的部分参数调整记录仅供参考参数调优前调优后对 Redis 的影响THPalwaysnever消除 fork 阶段延迟毛刺vm.swappiness601避免内存页被换出net.core.somaxconn1281024降低高并发连接丢弃率tcp-backlog5111024与 somaxconn 匹配磁盘调度器mq-deadlinenone减少 SSD 排队延迟CPUAffinity无2,3降低上下文切换和跨核缓存失效6.3 一个让我印象深刻的误调参现场zone_reclaim_mode 的教训调优过程中我也翻过车。有一台 Redis 机器频繁发生内存分配延迟升高当时网上说打开vm.zone_reclaim_mode1可以加速 NUMA 节点内部的内存回收我照着改了结果延迟不降反升。后来查内核文档才明白zone_reclaim_mode 开启后内存分配会优先尝试回收当前 node 的页但 Redis 的大块内存分配在这种模式下容易触发频繁的页面扫描和迁移反而拖慢分配速度。对 Redis 这类需要快速分配内存的应用保持默认值 0 才合理。这个教训让我在后面调参时多了一个心眼任何参数都先去查内核文档或源码搞清楚它在什么场景下有用、什么场景下有害。网上那些推荐配置清单只能作为起点不能作为终点。另外一个容易被忽略的点是操作系统版本和内核版本差异。Ubuntu 20.04 默认内核是 5.4某些参数在新内核里已经改名或不再生效比如tcp_tw_recycle在 4.12 之后就已经被移除。如果你用的是 HWE 内核或者自行更新的更高版本内核一定要逐个参数确认当前内核是否真的认识了它否则配置文件写了一大堆实际生效的没几个。最后再分享一个实用小技巧调优完成之后把/etc/sysctl.d/99-redis-tuning.conf、GRUB 配置、systemd override 文件都纳入版本管理并在文件顶部注明修改日期和原因。半年后你再翻回来能清清楚楚知道当初每一条改动是为了解决什么问题而不是面对一堆不知道谁加的配置干瞪眼。这比任何一次调优本身都重要。
返回列表