一、引言:当ssd成为瓶颈,内存才是终极答案
在绝大多数nginx缓存教程中,proxy_cache_path 总是指向一个磁盘目录。对于普通web应用,nvme ssd的iops足以应付。但在以下场景中,磁盘io会迅速成为系统天花板:
- api网关:qps > 5万,缓存命中率90%,但每次hit仍需一次ssd读取,延迟从0.1ms飙升至1ms;
- 高频交易/实时竞价:对p99延迟要求<5ms,磁盘io抖动导致尾延迟不可接受;
- 小文件海量访问:百万级kb级对象,inode查找和元数据操作比数据传输本身更耗时;
- 容器化环境:云盘iops受限且延迟不稳定,本地盘容量不足。
这些场景的共同特征是:缓存的价值不在于“存”,而在于“快”。当存储介质的速度跟不上请求的速度时,将缓存完全迁入内存是唯一解法。
但“内存缓存”不是一个nginx指令,而是一套涉及存储后端选择、内存管理策略、持久化兜底和监控验证的工程方案。本文将从三种主流实现路径出发,给出生产级配置、性能对比和避坑指南。
二、nginx内存缓存的三种实现路径
2.1 路径对比
| 路径 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| tmpfs挂载 | 将cache_path指向tmpfs文件系统 | 零代码改动、兼容所有模块 | 受限于可用ram、重启丢失 | 通用加速、中小规模 |
| proxy_cache + memory zone | keys_zone纯内存索引 + 磁盘存储 | 元数据极速查找、数据可持久化 | 数据体仍走磁盘io | 大key空间+热数据加速 |
| 第三方内存模块 | ngx_shm / lua shared dict / njs | 完全内存操作、微秒级延迟 | 非标准模块、功能受限 | 计数器、限流、小型kv |
📌 核心认知:nginx原生没有“全内存proxy_cache”指令。所谓“内存缓存”是通过操作系统层(tmpfs)或架构分层(索引内存+数据磁盘)实现的。理解这一点,才能避免在生产中踩到“以为在内存实际在磁盘”的致命陷阱。
2.2 选型决策树
你的缓存对象平均大小? ├─ < 10kb → 第三方内存模块(lua/njs)或 tmpfs ├─ 10kb ~ 1mb → tmpfs(首选)或 proxy_cache + 大keys_zone └─ > 1mb → proxy_cache + 大keys_zone + ssd(内存放不下全量) 你的qps和延迟要求? ├─ qps < 1万 & p99 < 10ms → tmpfs足够 ├─ qps > 5万 & p99 < 2ms → tmpfs + 多worker绑定numa └─ 需要持久化 + 内存速度 → proxy_cache + keys_zone + tmpfs混合
三、方案一:tmpfs挂载(生产最常用)
3.1 原理
tmpfs是基于ram的虚拟文件系统,对nginx而言与普通目录无异,但所有读写都在内存中完成,无磁盘io。
3.2 创建与挂载
# 创建挂载点 mkdir -p /var/cache/nginx/memory # 挂载tmpfs,限制最大使用内存为8gb mount -t tmpfs -o size=8g,noatime,nodiratime tmpfs /var/cache/nginx/memory # 写入fstab实现开机自动挂载 echo "tmpfs /var/cache/nginx/memory tmpfs size=8g,noatime,nodiratime 0 0" >> /etc/fstab
⚠️ 关键参数:
- size=8g:硬上限,防止oom杀死nginx。永远不要设为100%内存,预留至少30%给系统和worker进程。
- noatime,nodiratime:禁止更新访问时间戳,减少不必要的内存写入。
- mode=0755,uid=nginx,gid=nginx:确保nginx worker有读写权限。
3.3 nginx配置
http {
proxy_cache_path /var/cache/nginx/memory
levels=1:2
keys_zone=mem_cache:100m # 索引仍在内存
max_size=7g # ⭐ 必须小于tmpfs size
inactive=1h # 内存寸土寸金,缩短inactive
use_temp_path=off; # 避免临时文件拷贝
server {
listen 80;
location /api/ {
proxy_cache mem_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 10m; # 内存缓存ttl宜短
proxy_cache_lock on;
proxy_cache_use_stale error timeout updating;
add_header x-cache-status $upstream_cache_status always;
add_header x-cache-backend "memory" always;
proxy_pass http://backend;
}
}
}3.4 六个生产要点
① max_size必须严格小于tmpfs size
tmpfs满后写入会返回enospc错误,nginx直接500。建议 max_size = tmpfs_size × 0.85,预留缓冲空间供manager线程清理。
② inactive要比磁盘缓存短得多
内存是稀缺资源。磁盘缓存可以设24h inactive,内存缓存建议10min~1h,让冷数据快速淘汰,把空间留给热数据。
③ 监控内存使用率
# 查看tmpfs实际使用量
df -h /var/cache/nginx/memory
# prometheus node_exporter自动采集mountpoint指标
# grafana面板添加: node_filesystem_avail_bytes{mountpoint="/var/cache/nginx/memory"}设置告警阈值:使用率 > 80% 预警,> 90% 紧急。
④ numa感知(超高并发场景)
多路服务器上,跨numa节点访问内存延迟翻倍。将worker绑定到特定numa节点,并将tmpfs挂载到对应节点的本地内存:
# 查看numa拓扑 numactl --hardware # 在指定numa节点上分配tmpfs numactl --membind=1 mount -t tmpfs -o size=4g tmpfs /var/cache/nginx/memory-numa1
配合 worker_cpu_affinity 将处理该缓存的worker绑定到同一numa节点。
⑤ 重启预案
tmpfs内容在重启后丢失。对于可重建的缓存(api响应、计算结果),这是可接受的;对于不可重建的内容,需配合预热脚本:
# systemd service: nginx-cache-warmup.service [unit] after=nginx.service [service] execstart=/opt/scripts/warmup-cache.sh type=oneshot
⑥ 与磁盘缓存分层
对于混合负载,可同时配置内存和磁盘两级缓存:
# 热数据走内存
location /api/hot/ {
proxy_cache mem_cache;
proxy_cache_valid 200 5m;
}
# 温数据走ssd
location /api/ {
proxy_cache disk_cache;
proxy_cache_valid 200 1h;
}四、方案二:keys_zone优化(元数据内存加速)
即使数据存储在磁盘,keys_zone 本身就在内存中。增大keys_zone可以显著减少磁盘元数据查找:
# 经验公式:每1mb keys_zone ≈ 8000个缓存条目
# 100万条目需要约125mb
proxy_cache_path /var/cache/nginx/ssd
keys_zone=disk_cache:200m # 支撑160万条目
max_size=500g
inactive=24h;4.1 验证keys_zone是否充足
# 在日志中添加缓存状态细节
log_format cache_detail '$upstream_cache_status $request_time '
'$upstream_response_time $body_bytes_sent';如果hit请求的 $request_time 仍然 > 1ms,说明keys_zone可能过小,导致频繁的磁盘元数据扫描。逐步增大并观察延迟变化。
4.2 keys_zone vs tmpfs的选择
| 维度 | 大keys_zone + ssd | tmpfs |
|---|---|---|
| 容量上限 | 受磁盘限制(tb级) | 受ram限制(百gb级) |
| hit延迟 | 0.1~1ms | 0.01~0.1ms |
| 持久化 | ✅ 重启保留 | ❌ 重启丢失 |
| 成本 | 低 | 高 |
| 适用数据量 | 百万~亿级条目 | 十万~百万级条目 |
📌 最佳实践:大多数生产环境采用“大keys_zone + ssd”作为基线,仅对延迟敏感的热点路径叠加tmpfs层。
五、方案三:lua/njs共享内存(微型kv缓存)
当缓存对象极小(<1kb)、数量可控、且不需要http语义时,lua shared dict或njs shared memory提供真正的纯内存kv存储:
5.1 lua shared dict示例
lua_shared_dict api_config 10m;
server {
location /config {
content_by_lua_block {
local config = ngx.shared.api_config
local val, err = config:get("feature_flag")
if not val then
-- 回源加载并缓存60秒
local res = ngx.location.capture("/internal/config")
config:set("feature_flag", res.body, 60)
val = res.body
end
ngx.say(val)
}
}
}5.2 适用边界
- ✅ 配置项、特性开关、限流计数器、会话令牌
- ❌ http响应体、大文件、需要range支持的内容
- ⚠️ 内存固定分配,无法动态扩展;超出容量后lru淘汰或写入失败
六、性能基准测试
以下为单机测试数据(amd epyc 7t83, 256gb ram, nvme ssd),供参考:
| 指标 | ssd proxy_cache | tmpfs proxy_cache | lua shared dict |
|---|---|---|---|
| hit p50延迟 | 0.3ms | 0.03ms | 0.005ms |
| hit p99延迟 | 1.2ms | 0.08ms | 0.01ms |
| 吞吐量(1kb对象) | 80k qps | 350k qps | 900k qps |
| 吞吐量(100kb对象) | 45k qps | 180k qps | n/a |
| 内存效率 | 高(按需) | 中(预分配) | 高(紧凑) |
| 重启恢复 | 即时 | 需预热 | 需预热 |
📌 注意:实际性能受cpu、网络、后端延迟等多因素影响。以上数据仅反映存储介质差异的量级关系。务必在自己的硬件和业务负载下做压测。
七、监控与排障
7.1 必采指标
| 指标 | 采集方式 | 健康阈值 |
|---|---|---|
| tmpfs使用率 | node_filesystem_* | < 80% |
| keys_zone使用率 | nginx-vts-exporter | < 75% |
| hit p99延迟 | access_log + histogram | < 业务sla |
| enospc错误数 | error.log grep | = 0 |
| 缓存淘汰速率 | manager日志 | 平稳,无突增 |
7.2 常见问题速查
| 现象 | 根因 | 解决方案 |
|---|---|---|
| 500错误突增 | tmpfs满 | 增大size或缩短inactive |
| hit延迟未改善 | cache_path仍指向磁盘 | 确认mount类型:df -t |
| 重启后大量miss | tmpfs未预热 | 添加warmup脚本 |
| oom killer杀nginx | tmpfs size过大 | 缩减至总ram的50%以下 |
| keys_zone频繁淘汰 | 内存索引不足 | 增大keys_zone |
| numa跨节点延迟高 | worker与tmpfs不在同节点 | numactl绑定 |
| lua shared dict写入失败 | 容量耗尽 | 增大lua_shared_dict或优化key设计 |
八、结语
到此这篇关于nginx内存缓存的实现示例的文章就介绍到这了,更多相关nginx内存缓存内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论