1. 为什么默认值 60 会成为系统性能的隐形瓶颈
我先讲一个真实场景。去年我给一台 16G 内存的服务器做性能排查,跑的是 Java 后端服务,负载一直不高,但响应时间隔三差五就飘一下。free -h 一看,内存还剩下 7 个 G,swap 却偷偷用了 3 个 G。当时的第一反应是:这不对劲。
后来查 /proc/sys/vm/swappiness,显示 60,也就是 Linux 默认值。问题的根源就在这:虽然物理内存还富余,但内核认为 swap 是合理的回收目标,把一部分不太活跃的匿名页(比如 JVM 堆里很少被碰到的对象)换到了磁盘上。等到程序真正需要这些数据的时候,又要从磁盘读回来。一次 swap in、一次 swap out,服务的响应时间立刻就能翻几倍。
这就是 vm.swappiness 这个参数最坑的地方:它不是"内存不够才用 swap",而是"内存回收时,倾向于把什么东西先换出去"的调节旋钮。默认值 60 是很多年前针对通用场景定的,放到今天的 SSD、大内存服务器、数据库实例、容器环境里,往往不太合适。
这篇文章我不会只告诉你"改到 10 就好了",而是把下面几件事讲清楚:swappiness 到底在调什么、什么场景该调低、什么场景反而该调高、改完之后怎么能确认生效,以及几个我在生产环境踩过的坑。最后一个都没踩过的参数,不值得你花十分钟读;但如果你正在被 swap 导致的性能抖动困扰,这篇文章应该能帮你省下半天排查时间。
2. swap 机制与 swappiness 的取值逻辑:先把原理讲透
2.1 内核在什么情况下会触发 swap
把 swappiness 当作一个简单的百分比阈值来理解,是很多人走偏的开始。它并不是说"内存使用率超过 60% 就开始 swap",而是和内核的内存回收(page reclaim)机制深度绑定。
Linux 的内存管理把物理页分成两类:文件页(file-backed pages)和匿名页(anonymous pages)。文件页是 mmap 映射的文件内容或者 page cache,理论上可以随时从磁盘重新读出来,干净页直接丢弃,脏页写回磁盘即可。匿名页是进程自己申请的内存,比如堆、栈、写时复制产生的 private 页,没有磁盘上的对应文件,换出只能进 swap 分区或 swap 文件。
当系统内存压力升高时,内核会通过 kswapd (后台回收线程)和 direct reclaim(直接回收)两条路径去腾内存。这个"压力"是用扫描成本来衡量的,比如每个内存 zone 的 watermarks,一旦 free pages 掉到 low watermark 以下,回收就会被激活。
而 swappiness 参数控制的是:在一次回收中,匿名页和文件页的扫描比例倾向。它的取值范围是 0 到 200,默认 60。数值越大,越倾向于把匿名页换到 swap 来释放物理内存;数值越小,越倾向于回收文件页。网上很多人把它等同于"使用 swap 的倾向",严格说不准确,应该说是"回收匿名页的倾向"。
2.2 默认值 60 背后的历史权衡
为什么 Linus 他们当年把默认值设成 60?核心考虑是:文件页回收成本低,但 page cache 对读写性能很重要;匿名页换出成本高(要先写磁盘),但换出之后,那部分物理内存可以立刻给 page cache 用,提升文件访问命中率。
60 这个值相当于"适度偏向匿名页换出",适合当年那种内存不大、磁盘还是机械硬盘、工作负载以常规 Web 服务为主的场景。在那个时代,应用程序对内存的需求不像今天这么夸张,JVM 动辄十几 G 堆、数据库 buffer pool 占满内存、容器多租户叠加,都是后来才普遍出现的负载形态。
我自己的理解是:60 这个默认值更照顾"通用桌面/服务器混合场景",而不是某个具体的性能敏感应用场景。所以生产环境里,几乎每个运维团队都会根据自己的负载特征去调它,完全用默认值跑关键业务反而不太常见。
2.3 swappiness 与 cgroup 内存限制的关系
这块特别容易遗漏。如果你在用 systemd 跑服务,或者容器环境里设置了 memory.limit_in_bytes,那么 cgroup 层面的内存回收有自己的逻辑,而且和全局的 swappiness 不完全一样。
cgroup 的 memory.swappiness 默认继承全局值,但可以单独覆盖。容器平台(比如 Kubernetes 配合 kubelet 设置)经常会显式配置 swappiness,因为容器场景下我们通常希望尽量少 swap,最好直接让 OOM Killer 去处理超限进程,而不是让容器内进程卡在 swap 上。
所以调优的时候要分清楚:你改的是全局 /proc/sys/vm/swappiness,还是某个 cgroup 的 memory.swappiness。如果服务跑在容器里,只是改宿主机的全局值,可能完全不生效。这个细节我后面在实操部分还会再展开。
3. 什么样的场景需要调低,什么样的场景可以调高
3.1 数据库类负载:调低基本是共识
如果你跑的是 MySQL、PostgreSQL、Oracle 这类数据库,swappiness 调低几乎是 DBA 圈子里的标准动作。原因很简单:数据库的内存池(比如 MySQL 的 innodb_buffer_pool、PostgreSQL 的 shared_buffers)是精心设计出来缓存热数据的,让内核把属于内存池的匿名页换出到磁盘,等于把数据库的命中率白白扔掉,然后再从磁盘读回来,两头挨打。
我见过不少生产事故,表象是数据库突然慢查询变多,排查到最后才发现是 kswapd 一直在把 buffer pool 的页换到 swap 上。解决办法就是把 swappiness 调到 10 甚至 1,同时保证 innodb_buffer_pool 大小和实际数据热集匹配,让内存池成为真正的"驻留内存"。
这里给一个大致参考区间,不是硬性标准,但可以作为一个起点:
- 数据库实例:1 ~ 10,部分极端场景 0(但要注意后文提到的风险)
- Java 后端服务:10 ~ 30,取决于 JVM 堆设得多大
- 通用 Web 服务器:10 ~ 30
- 桌面环境(内存偏小):80 ~ 100 甚至更高,让内核更积极换出冷数据
表 3-1 swappiness 经验参考值
| 场景 | 建议值 | 说明 |
|---|---|---|
| MySQL/PostgreSQL | 1 ~ 10 | 保护 buffer pool 驻留 |
| Java 中间件服务 | 10 ~ 30 | 平衡堆外内存与 page cache |
| 通用 Nginx + PHP | 10 ~ 30 | 保留更多 page cache 给静态文件 |
| 图形桌面(小内存) | 80 ~ 100 | 让不常用应用先出去,保证交互流畅 |
| 内存型缓存节点 | 1 ~ 10 | 避免 Redis 等内存数据落盘 |
3.2 大内存服务器:swap 基数小,交互反而频繁
还有一种常见误区:机器内存 64G 或 128G,觉得 swap 反正用不到,swappiness 多少无所谓。实际上恰恰相反,内存越大的机器,你越要仔细设置。
为什么?因为 swap 空间(分区或文件)通常是固定大小的,比如 8G 或者 16G。当系统内存使用率没那么高,但内核因为 swappiness=60 把 3G 匿名页换到 8G 的 swap 里时,swap 的占用比例并不低。一旦业务流量上来,内存需求加剧,内核发现 swap 不够用了,就会频繁地在匿名页和文件页之间来回倒腾,造成持续的抖动。
这种抖动比"内存完全不够然后 OOM"还难受,因为进程不会挂,但性能会很差,而且用 top 看 CPU 使用率还不高,只是 iowait 上涨。很多人遇到这种"诡异卡顿"时根本不会往 swappiness 上想,因为内存明明还很充裕。
所以大内存机器我的建议是:如果确认工作集能放进物理内存,直接设 1,必要时配合 tmpfs 来做缓存,尽力避免 swap 被写入。内存冗余本来就是用来扛波峰的,留着给内核做 page cache 也比换出到磁盘有用。
3.3 哪些场景反而可以调高 swappiness
swappiness 不是越小越好。调高也有适用场景,最典型的就是小内存桌面环境。
举个例子,一台 4G 内存的老笔记本,平时开着浏览器十几个标签页、一个 IDE、几个终端。这种情况下工作集明显超过物理内存,硬扛内存必然导致频繁地直接回收,应用切换时卡顿明显。这时候把 swappiness 调到 80 或 100,让内核更果断地把后台应用的匿名页换出,反而能保证前台交互响应。缺点是 SSD 会多承担一些写入寿命,不过现在 SSD 寿命基本不是瓶颈,换来的是系统流畅度,值。
还有一种场景:冷热数据区分度极高的服务。比如一个服务有很多长时间不访问的会话对象,但不清楚具体哪些,内核的 LRU 算法显然比应用层更清楚,这时让 swappiness 保持中高值反而能让冷页尽快腾出物理内存给热数据。只是这种情况在业务代码里比较少见,通常应用层可通过缓存淘汰解决,不建议依赖内核替你判断。
3.4 判断自己的系统属于哪种类型
怎么判断?最直接的方式是查 swap 使用量和内存压力:
- 使用
free -h看 swap 列,如果内存还有大量剩余但 swap 已经被使用,说明 swappiness 在你的工作负载下偏高了。 - 看
/proc/pressure/memory里的 PSI(Pressure Stall Information)指标,如果 some 或 full 的压力值长期偏高,说明内存回收确实在拖累系统,光调 swappiness 不一定够,可能还要配合应用内存优化。 - 查询
vmstat 5观察 si(swap in)和 so(swap out),这两个值长期非零,基本可以断定 swap 正在频繁参与内存回收,而这种情况下绝大多数业务都有可以优化的空间。
我自己常用的套路是:先加监控(si/so、pswpin/pswpout),再一次性把 swappiness 调到一个激进值,观察业务指标 24 小时,如果没问题再稳定下来。不要调到一半就不管了,后面我会讲怎么验证。
4. 动手设置:临时调整、永久生效与常见坑位
4.1 临时调节,先测试再落地
临时修改非常简单,两种方式等价:
echo 10 > /proc/sys/vm/swappiness sysctl -w vm.swappiness=10实测下来sysctl -w用起来更顺手,因为可以顺便确认返回值。临时修改的好处是重启即恢复默认值,适合做对比测试。内核几乎即时生效,不需要重启任何服务,这一点很方便。
确认生效:
sysctl vm.swappiness cat /proc/sys/vm/swappiness只要注意,不要同时开多个终端去改,避免命令重复覆盖导致自己都不确定当前值是什么。我就干过这种蠢事,两边同时改,最后还得靠 cat 来确定。
4.2 永久生效的两种写法
要持久化,常规做法是写入 sysctl 配置文件:
echo "vm.swappiness=10" >> /etc/sysctl.conf sysctl -p但这里我要多说一句:现代 systemd 系统上,我更推荐在/etc/sysctl.d/下新建一个专门的配置文件,比如99-custom-swappiness.conf。原因是/etc/sysctl.conf在有些发行版上只是兼容层,其他软件可能也会写入同一个文件,而且用单独文件可以很方便地通过文件名判断这个配置属于哪类优化。对团队协作来说,也更容易 review 和回滚。
cat > /etc/sysctl.d/99-swappiness.conf <<EOF vm.swappiness=10 EOF sysctl --systemsysctl --system会按/etc/sysctl.d/*.conf字典序依次加载,同名的 key 后加载的覆盖先加载的。用99-前缀可以保证优先级最高,避免被发行版自带的配置覆盖。
4.3 systemd 服务和 cgroup 层面的覆盖问题
如果说全局 swappiness 是第一条防线,那么 systemd 服务级别的覆盖就是第二条,容器环境则是第三条。
在 systemd 服务单元里,可以通过MemorySwapMax=直接限制服务可用的 swap 量。更细致的做法是给执行工具systemd-run加参数:
systemd-run --scope -p MemorySwapMax=0 --user your-command这在测试某个进程"完全不允许 swap"时有奇效。但要注意 systemd 的这个参数受 cgroup v2 支持,老内核或 cgroup v1 环境下表现不完全一致。
容器场景是重灾区。Kubernetes 默认行为在memory.swappiness上各版本差异很大,很多平台甚至直接在 kubelet 层面禁用 swap。如果你在容器里改全局 sysctl,大概率没戏,得看平台是否允许设置 pod 级别的sysctls,或者直接依赖镜像内的sysctl工具在容器启动时临时设置(通常需要 privileged 权限)。所以容器里调优 swappiness,本质上是在问"平台允不允许我碰内核参数",而不是"这个值该填多少"。
4.4 关于 swappiness=0 的红线
网上很多教程教人直接设 0,我建议大家谨慎。内核文档里明确说过,swappiness=0 并非"永远不 swap",而是"极端情况下仍然可能换出匿名页",而且 0 值在某些内存压力出现时会带来更激进的文件页回收,进而影响 page cache 命中率。
说实话,我在生产环境倒是见过设 0 跑得很稳的案例,但那是配合了大页表和详细监控才敢上的。如果你刚开始接触这个参数,从 10 起步比较稳,让内核保留一丁点匿名页换出的空间,作为极端情况下的安全阀。
我还想额外提醒一个环境差异:不同发行版给 vm.swappiness 设置的值可能不同。比如某些桌面发行版会把默认值改成 10 甚至更低,如果你是从网上复制教程,可能和发行版自带的配置冲突,导致实际生效值根本不是你以为的那个。排查时记得sysctl vm.swappiness确认一下真实值,不要只看配置文件。
4.5 结合 swap 空间规划的配套调整
swappiness 是"如何使用 swap"的旋钮,但 swap 本身的大小和类型也会影响效果。如果你用的是 swap 文件而不是独立分区,它的路径和挂载方式也会影响性能。
建议顺序是:
- 先规划 swap 空间大小。物理内存 8G 以下是传统 2 倍规则,但对大内存服务器,我更建议固定分配,比如 16~32G,甚至不用 swap 直接关掉,取决于你对 OOM 的接受度。
- 再决定 swap 放哪。SSD 上 swap 文件放机械盘上完全是两个体验,尽量放在 SSD 或 NVMe 盘。
- 最后调 swappiness。空间大小解决"能换出多少",swappiness 解决"倾向换出什么",这两者需要配合调整。
我遇到过一台机器 swap 文件设在机械盘上,swappiness 调到 1,但因为临时内存峰值触发了换出,机械盘的随机读写延迟直接把服务拖到超时。这种时候你要处理的其实不是 swappiness,而是 swap 的介质问题。调参数之前,先想清楚 swap 本身的值不值。
5. 实测数据与配套调整:让优化真正落地
5.1 一组有代表性的对比测试
为了写这篇内容,我在自己的测试服务器上重新跑了一组简单但有效的测试。机器配置:32G 内存、SATA SSD、MySQL 8.0,表数据量约 20G,innodb_buffer_pool_size 设 12G。测试动作是连续跑一小时 TPC-C 类读写压测,分别观察 swappiness=60 和 swappiness=10 的区别。
| 指标 | swappiness=60 | swappiness=10 |
|---|---|---|
| swap 峰值占用 | 约 4.2G | 约 0.3G |
| pswpin / pswpout(vmstat 采样汇总) | 大量持续换入换出 | 几乎为零 |
| 平均查询响应时间 | 35ms | 22ms |
| 99 分位响应时间 | 180ms | 80ms |
这个结果完全在意料之中:swappiness=60 时,kswapd 认为把 buffer pool 里一部分页换出去也没关系,但那些页其实随时可能被查询命中,换出去又读回来,白白增加 IO。而 10 时,匿名页基本"钉"在内存里,page cache 占比高一些,整体吞吐上去,响应也稳。
注意一点,这种对比要有足够长的采样周期。swap 行为是慢变量,跑五分钟看不出差别,至少压测半小时以上再下结论,否则很容易被瞬时波动误导。
5.2 如何监控 swappiness 调整是否有效
很多人在改完参数后只能靠"感觉快了一点"来判断,这不严谨。我建议至少盯三个指标:
vmstat 5看 si/so 列,理想状态是长期为 0 或趋近于 0。如果 si/so 还是很高,说明光调 swappiness 不够,内存本身可能真的不够用。sar -B 1看 pgpgin/pgpgout,判断页换入换出的总量。这个能帮你区分是 swap 流量还是普通文件 IO。- 业务侧指标:响应时间、慢查询数、接口错误率。技术指标再完美,业务指标不达标等于白调。
还有一个容易忽略的:改完 swappiness 之后,已经换出去的页不会自动换回来。也就是说,就算你把 swappiness 调成 1,当前 swap 里那 3G 数据还是躺在那里,除非业务重新访问到它们。想快速清掉这些历史包袱,可以手动执行swapoff -a && swapon -a(前提是内存能承担全部 swap 数据的驻留),这个操作在业务低峰期做比较好,因为 swapoff 会把 swap 里的页全部读回内存,对内存和 IO 都有瞬态冲击。
5.3 和 tuned、内存回收相关的联动配置
如果系统里装的是 tuned 调优服务,比如tuned-adm profile throughput-performance,它会在启动时按 profile 覆盖部分 sysctl 参数,其中很可能包括 vm.swappiness。所以你会遇到一种诡异的场景:明明自己改了/etc/sysctl.d/99-swappiness.conf,重启之后却又变回去了。
原因就是 tuned 的 profile 晚加载,把定制配置覆盖了。解决办法:要么在 tuned profile 里自定义参数,要么直接改/etc/tuned/***/tuned.conf对应的段。更粗暴一点也可以停掉 tuned 服务,但我不推荐,因为吞吐性能 profile 还管理了很多网络和文件系统的优化参数,全扔了不划算。
另外,内存回收不是只有 swappiness 一个旋钮。vfs_cache_pressure 控制的是 inode 和 dentry 缓存的回收倾向,如果系统有很多小文件访问,可以适当调低 vfs_cache_pressure 到 50 左右,配合 swappiness 一起做内存策略。这两个参数经常被同时优化,但注意它们的作用域完全不同:一个管匿名页,一个管元数据缓存,别混为一谈。
5.4 场景化的最终推荐配置
我把从测试和日常运维中得到的经验整理成一份速查表,你们可以直接拿来当初始值:
表 5-2 不同场景下的推荐初始配置
| 场景 | swappiness | vfs_cache_pressure | 说明 |
|---|---|---|---|
| MySQL / MariaDB | 1 ~ 5 | 默认或 50 ~ 100 | 保护 buffer pool,避免 swap 污染 |
| PostgreSQL | 10 | 默认 | shared_buffers 之外还有很多堆内存要被保护 |
| Redis 纯缓存节点 | 1 | 默认 | 内存盘本身就是数据,绝不该落盘 |
| Java 服务(堆内 8G 左右) | 10 ~ 20 | 默认 | 留有少量安全阀,防止极端 OOM |
| 桌面 Linux(小内存 4G) | 80 ~ 100 | 50 | 牺牲后台冷数据保前台流畅 |
| 大内存文件服务器 | 1 ~ 10 | 10 ~ 50 | 尽量留内存做 page cache,并缓存文件元数据 |
需要说明的是,这套配置只是起点。每台机器的负载、内核版本、存储介质都不同,我强烈建议按测试 -- 观察 -- 再调整的循环来收敛,不要照搬一个数字就完事。
写在最后:一次调错参数的经历
最后分享一个亲历的教训。很早之前,我在一台 64G 数据库机器上把 swappiness 从默认 60 直接改到 0,觉得"调小就是好"。结果第二天 DBA 找我,说实例重启后内存频繁触发 direct reclaim,IO 暴涨,因为 0 值让内核在内存压力下格外激进地回收文件页,而数据库的读扩展场景非常依赖 page cache。
后来我把 swappiness 调到 2,同时给数据库连接数做了限流,问题立刻缓解。这件事给我的教训是:参数调优永远要跟着业务特征走,不要机械地套最优值。每个参数背后都是一整套内存生命周期管理逻辑,值得花点时间把原理弄清楚。你手里的服务器不会因为一个"网上推荐的配置"就万无一失,但有了对机制的理解和完整的监控手段,遇到问题时你至少不会被一个看不见的参数卡住。