为什么个人站长该认识 tmpfs:把高频读写目录搬进内存
很多草根站长的小服务器都是 1核1G 或者 2核2G 的便宜 VPS,磁盘往往是最廉价的 SATA SSD 甚至机械盘。网站跑久了会发现一个怪现象:CPU 和内存都还好,但页面生成、Session 读写、缓存文件一多,响应就肉眼可见地变慢。这时候问题多半出在磁盘 IO 上,而不是 CPU 算力不够。tmpfs 是一个被严重低估的工具——它让你把一部分内存挂载成"磁盘目录",读写速度是 SSD 的几十倍,而且是内核级的、零额外软件。
本文不堆砌概念,而是从一个真实的内容站场景出发:Typecho/WordPress 这类 PHP 站点,每次请求都要读写 Session 文件、编译模板缓存、写日志。这些文件有个共同特点——丢了也没关系,重启后重建即可。它们恰好是 tmpfs 的完美适用对象。
tmpfs 到底是什么,和 ramdisk 有什么本质区别
tmpfs 不是"把内存伪装成一块硬盘"这么简单。传统的 ramdisk(比如 /dev/ram0)会预先分配一整块固定内存,不管你用不用都占着;而 tmpfs 是**用到多少才占多少**,并且底层由内核的页缓存和 swap 共同管理。这意味着:
- 你挂一个 512M 的 tmpfs,实际只写了 20M,那么只有 20M 内存被占用。
- 内存紧张时,tmpfs 里不常访问的内容可以被交换到 swap,不会直接触发 OOM Killer。
- 它天生支持所有 POSIX 文件操作:权限、软硬链接、mmap 全部可用,程序完全无感知。
查看已有的 tmpfs 挂载非常简单,很多系统默认就给 /dev/shm 挂了一个:
df -h -t tmpfs mount | grep tmpfs
你会看到 /run、/dev/shm、/sys/fs/cgroup 这些系统目录本身就在 tmpfs 上——内核自己都在用,说明它足够稳。
第一步:选对要放进内存的目录
不是所有目录都适合 tmpfs。判断标准就三条:高频读写、体积可控、可重建。对个人站长来说,典型的候选是:
- PHP Session 目录:
/var/lib/php/sessions,每个访客一个文件,读写极其频繁。 - 应用缓存目录:如 Typecho 的
/usr/cache、WordPress 的wp-content/cache。 - Nginx 的临时缓冲:
proxy_temp_path、fastcgi_temp_path、client_body_temp_path。反代大流量时这几个目录写得很凶。 - 临时上传/解压目录:
/tmp,编译、打包、插件安装时用。
反过来,数据库文件、网站源码、用户上传的图片绝对不能放 tmpfs——一重启就全没了,那是事故。
第二步:动手挂载,用 fstab 保证开机生效
临时挂载可以先试水,确认没问题再写进 fstab。先看内存余量,别把 tmpfs 撑爆:
free -h
假设你有 2G 内存,日常占用 600M,那给 tmpfs 分 256M 是安全的。先手动挂一个 Session 目录试试:
# 备份原目录内容 mkdir -p /var/lib/php/sessions.tmp cp -a /var/lib/php/sessions/. /var/lib/php/sessions.tmp/ 2>/dev/null # 挂载 tmpfs mount -t tmpfs -o size=256M,mode=1733,nosuid,nodev,noexec,noatime tmpfs /var/lib/php/sessions df -h /var/lib/php/sessions mount | grep sessions
参数逐个解释一下,这几个是安全关键:
size=256M:硬上限,写满就报No space left on device,绝不会蚕食系统内存。mode=1733:PHP Session 目录的标准权限,粘滞位 + 所有用户可写但只能删自己的文件。nosuid,nodev,noexec:禁止在这个目录里执行程序,防止有人上传脚本然后运行,这是最容易被忽略的安全加固。noatime:不更新访问时间戳,减少无谓的写入。
确认站点 Session 读写正常后,写进 /etc/fstab 让它开机自动挂载。注意 fstab 格式用 tmpfs 作为设备名即可:
tmpfs /var/lib/php/sessions tmpfs defaults,size=256M,mode=1733,nosuid,nodev,noexec,noatime 0 0
写完后务必先验证再重启,否则可能开不了机:
mount -a echo $? # 必须输出 0
第三步:给 Nginx 临时目录搬家(反代站提速最明显)
如果你的小站做了反向代理(比如前面挂 Cloudflare,或者 Nginx 反代后端 PHP/Node),那 Nginx 会大量读写临时缓冲文件。默认它们在 /var/lib/nginx 下。把它迁移到 tmpfs:
# nginx.conf 的 http 块里配置这三行 proxy_temp_path /dev/shm/nginx_proxy_temp 1 2; fastcgi_temp_path /dev/shm/nginx_fastcgi_temp 1 2; client_body_temp_path /dev/shm/nginx_client_temp 1 2;
注意这里直接用 /dev/shm,它本身就是 tmpfs,不用额外挂载,最省事。但记得给这几个子目录建好并赋权:
mkdir -p /dev/shm/nginx_proxy_temp /dev/shm/nginx_fastcgi_temp /dev/shm/nginx_client_temp chown -R www-data:www-data /dev/shm/nginx_* nginx -t && systemctl reload nginx
很多人会忽略 /dev/shm 也有大小上限(默认通常是内存的一半)。如果你要跑大文件代理,得显式调整:
# /etc/fstab 中 tmpfs /dev/shm tmpfs defaults,size=512M,noexec,nosuid,nodev 0 0
第四步:验证提速效果,用数据说话
挂完之后不能靠感觉,要压测对比。用 ab 或 wrk 打一个动态页面,对比 tmpfs 前后的每秒请求数:
# 安装压测工具(Debian/Ubuntu) apt install -y apache2-utils ab -n 2000 -c 50 https://你的域名/
重点看 Requests per second 和 Time per request 两个数字。同时用 iostat 观察磁盘写量是否下降:
iostat -x 1 10
如果配置生效,你会发现 %util(磁盘繁忙度)明显降低,await(平均等待)也下来了——这就是把 IO 从磁盘搬到内存的直接证据。
把 /tmp 也搬进内存:最容易见效的一步
如果你只想动一个目录就见效,那答案往往是 /tmp。PHP 会话、编译临时文件、图片处理中间产物、插件解压、Composer 依赖安装——全都经过 /tmp。把它搬到 tmpfs 有两条路:一是直接给 /tmp 挂 tmpfs,二是用 systemd 的 tmp.mount 单元做管理。
手动挂载的方式和前面一样:
# 先把 /tmp 里还在用的文件清一清,避免挂载后被遮挡 systemctl stop 依赖 /tmp 的服务 # 视情况 mount -t tmpfs -o size=512M,nosuid,nodev,mode=1777 tmpfs /tmp # 确认 df -h /tmp mount | grep ' /tmp '
注意 /tmp 的权限是 1777(粘滞位,人人可写但只能删自己的),写错了会导致很多程序报权限错误。更推荐的做法是用 systemd 管理,创建 /etc/systemd/system/tmp.mount:
[Unit] Description=Temporary Directory in RAM DefaultDependencies=no Conflicts=umount.target Before=local-fs.target umount.target [Mount] What=tmpfs Where=/tmp Type=tmpfs Options=mode=1777,strictatime,nosuid,nodev,size=512M [Install] WantedBy=local-fs.target
然后 systemctl daemon-reload && systemctl enable --now tmp.mount,并 systemctl mask tmp.mount... 等等,注意这里是启用不是 mask。启用后 systemd 会在每次开机自动重建 /tmp,比 fstab 更干净。好处是重启即清空,天然的"每日大扫除",还避免了 fstab 写错开不了机的风险。但要提醒:如果某些应用依赖 /tmp 里的文件在重启后仍然存在(很少见但存在),这一步会让它失效。
用 rsync 在重启前回捞数据(防丢保险)
tmpfs 天生易失,但有些场景你希望在重启前把内存里的某些文件落到磁盘。比如一个正在处理的队列状态、一个还没上传完的大文件。可以写一个关机前钩子,用 rsync 把 tmpfs 内容同步到持久目录。先用 systemd 服务绑定:
# /etc/systemd/system/save-tmpfs.service [Unit] Description=Persist tmpfs contents before shutdown DefaultDependencies=no Before=shutdown.target umount.target [Service] Type=oneshot ExecStart=/usr/bin/rsync -a --delete /var/lib/php/sessions/ /var/backup/sessions/ [Install] WantedBy=shutdown.target
配合一条 cron 定时同步也行,比如每 5 分钟落一次地。核心思路是:tmpfs 负责"快",磁盘负责"久",两者各司其职,用 rsync 把关键状态桥接起来。注意这种同步不追求一致性,只求"丢了能从上一个点恢复"。
用 vmtouch 验证文件是否真的在内存里
挂完 tmpfs,你可能好奇"这个文件现在到底在内存还是磁盘"。除了 tmpfs 天然在内存外,普通文件也可以被缓存进内存,用 vmtouch 能看清页缓存状态:
apt install -y vmtouch vmtouch /var/www/html/index.php vmtouch -v /var/www/html/ # 整个目录
输出里的 Resident Pages 就是常驻内存的页数。vmtouch -t 还能主动把文件"touch"进内存做预热。这对理解了 tmpfs 只是"必定在内存的文件系统"而页缓存是"临时借内存"的区别很有帮助——前者稳定占用,后者可能被回收。
资源监控:让 tmpfs 用量可视化
tmpfs 有一个隐蔽风险——它悄悄吃内存,top 里又看不出是哪个目录吃的。用 df -h -t tmpfs 可以列出所有 tmpfs 的已用和上限。想持续监控,可以写一个小脚本定期记录,配合 node_exporter 的 textfile collector 输出到 Prometheus:
#!/bin/bash
# /usr/local/bin/tmpfs_metrics.sh
OUT=/var/lib/node_exporter/textfile/tmpfs.prom
TMP=$(mktemp)
for mp in $(df -t tmpfs --output=target | tail -n +2); do
read size used < <(df -B1 --output=size,used "$mp" | tail -1)
echo "tmpfs_used_bytes{mount=\"$mp\"} $used" >> "$TMP"
echo "tmpfs_size_bytes{mount=\"$mp\"} $size" >> "$TMP"
done
mv "$TMP" "$OUT"再加一条 cron 每分钟跑一次,你就能在 Grafana 上看到每个 tmpfs 的使用率曲线,一旦某天接近上限,立刻知道是哪个目录在膨胀。
五个必踩的坑,提前避开
- tmpfs 撑爆导致 OOM:虽然 tmpfs 有 size 上限,但如果多个 tmpfs 加起来超过物理内存,系统仍会紧张。全局加起来别超过内存的 30%。
- Session 重启丢失导致全员登出:这其实是"预期行为"。如果你的站登录态很重要,可以把 Session 后端换成 Redis,而不是文件。tmpfs 只适合可丢的会话。
- noexec 导致某些程序跑不起来:极少数应用会在缓存目录里执行脚本。如果挂了 noexec 后报
Permission denied,就把它去掉,但要权衡安全。 - 忘记清理旧文件:tmpfs 不会自动删文件。Session 目录要配好 PHP 的
session.gc_maxlifetime,Nginx 临时目录靠它自己回收,一般没问题。 - fstab 写错导致无法开机:最后一项参数保持
0 0,且一定要先mount -a验证通过再重启,这是个价值连城的习惯。
总结:小内存 VPS 的性价比之选
tmpfs 的哲学很朴素——把最需要速度的数据放在最快的地方。对预算有限、买不起 NVMe 的个人站长来说,用几十行配置换来的是实打实的 IO 提速,还有 noexec/nosuid 附赠的安全加固。记住那条铁律:只放可重建的数据,绝不碰数据库和源码。先从小流量站点的一个 Session 目录开始试,用压测数据验证收益,再逐步推广到 Nginx 临时目录,稳扎稳打才是运维的正道。