在web开发中,为了提高网站的响应速度和降低服务器负载,常常会使用缓存技术。nginx 自身支持静态文件缓存,但对于动态内容,通常需要配合其他缓存解决方案,如 memcached。下面是如何配置 nginx 来使用外部缓存(memcached)的一个基本步骤。
一、引言:当redis“太重”,memcached才是正解
在《nginx外置缓存》系列中,我们多次以redis作为分布式缓存的代表。但在某些极端追求低延迟、高吞吐、纯kv语义的场景下,redis反而成了“过度设计”:
- 会话缓存(session):只需get/set/delete,不需要list/set/hash等复杂数据结构;
- api响应片段缓存:对象大小固定<10kb,qps > 10万,redis的单线程模型和持久化开销成为瓶颈;
- 计数器/限流令牌桶:仅需原子incr/decr,无需aof/rdb带来的io抖动;
- 边缘节点缓存:内存资源受限,无法承担redis fork子进程时的内存翻倍风险。
这些场景的共同特征是:数据是临时的、结构是扁平的、操作是原子的、对延迟极其敏感。这正是memcached的主场——一个为纯kv缓存而生的、多线程的、无持久化的内存引擎。
但“nginx + memcached”绝非简单的proxy_pass替换。原生nginx的memcached_module功能残缺(只读不写),生产环境必须借助ngx_memc模块或openresty的lua-resty-memcached库才能实现完整的读写闭环。本文将从协议特性、模块选型、生产配置到性能调优,构建一套真正可用的nginx + memcached外置缓存体系。
二、为什么选memcached而非redis?
2.1 核心差异对比
| 维度 | memcached | redis |
|---|---|---|
| 数据模型 | 纯kv字符串 | string/list/hash/set/zset/stream |
| 线程模型 | 多线程(cpu利用率高) | 单线程(6.0后io多线程,命令仍单线程) |
| 持久化 | ❌ 无 | ✅ rdb/aof |
| 集群模式 | 客户端一致性哈希 | 服务端cluster/sentinel |
| 内存管理 | slab allocator(无碎片) | jemalloc(可能有碎片) |
| 最大value | 默认1mb(可调至128mb) | 512mb |
| 过期策略 | 惰性+定期淘汰 | 惰性+定期淘汰 |
| 适用场景 | 临时kv缓存、session、计数器 | 复杂数据结构、持久化、消息队列 |
2.2 选型决策树
你的缓存需求? ├─ 需要复杂数据结构(list/hash/set) → redis ├─ 需要持久化/主从复制 → redis ├─ value > 1mb → redis └─ 纯kv + 临时数据 + 极致低延迟 → memcached ✅ 你的qps和延迟要求? ├─ qps < 5万 & p99 < 1ms → redis足够 ├─ qps > 10万 & p99 < 0.3ms → memcached ✅ └─ cpu密集型序列化/反序列化 → memcached(多线程优势)
📌 核心认知:memcached不是“简化版redis”,而是专为kv缓存优化的专用引擎。它的价值不在于功能多,而在于把“简单的事做到极致”。
三、nginx对接memcached的三种路径
3.1 路径对比
| 路径 | 模块 | 读 | 写 | 删 | 连接池 | 适用场景 |
|---|---|---|---|---|---|---|
| 原生memcached_module | ngx_http_memcached_module | ✅ | ❌ | ❌ | ❌ | 仅读取预填充缓存(已废弃) |
| ngx_memc | ngx_memc_module | ✅ | ✅ | ✅ | ❌ | 传统nginx、简单读写 |
| lua-resty-memcached | openresty | ✅ | ✅ | ✅ | ✅ | 生产首选、灵活逻辑 |
⚠️ 重要提醒:原生
memcached_module不支持写入和删除,且无连接池,生产环境严禁使用。本文聚焦lua-resty-memcached方案。
3.2 环境准备
# 安装openresty(内置lua-resty-memcached) wget https://openresty.org/package/openresty-1.27.1.tar.gz tar xzf openresty-1.27.1.tar.gz && cd openresty-1.27.1 ./configure --with-luajit --with-http_lua_module make && make install # 验证库可用 /usr/local/openresty/bin/resty -e 'print(require "resty.memcached")'
四、lua-resty-memcached生产级实现
4.1 核心缓存模块
创建 /usr/local/openresty/lualib/mcache.lua:
local memcached = require "resty.memcached"
local cjson = require "cjson.safe"
local _m = {}
-- memcached连接池配置
local mc_conf = {
host = "memcached.internal",
port = 11211,
pool_size = 200, -- 每worker连接池大小
backlog = 500, -- 等待队列
connect_timeout = 100, -- ms
read_timeout = 200, -- ms
}
-- 获取连接(带连接池)
local function get_mc()
local mc, err = memcached:new()
if not mc then
ngx.log(ngx.err, "memcached new failed: ", err)
return nil, err
end
mc:set_timeouts(mc_conf.connect_timeout,
mc_conf.read_timeout,
mc_conf.read_timeout)
local ok, err = mc:connect(mc_conf.host, mc_conf.port)
if not ok then
ngx.log(ngx.err, "memcached connect failed: ", err)
return nil, err
end
return mc, nil
end
-- 释放连接到池中
local function release_mc(mc)
local ok, err = mc:set_keepalive(10000, mc_conf.pool_size)
if not ok then
ngx.log(ngx.err, "memcached keepalive failed: ", err)
end
end
-- 读取缓存
function _m.get(key)
local mc, err = get_mc()
if not mc then return nil, err end
local res, flags, err = mc:get(key)
release_mc(mc)
if not res then
if err == "not found" then
return nil, "miss"
end
return nil, err
end
return res, nil -- memcached返回原始字符串,自行解码
end
-- 写入缓存(带ttl)
function _m.set(key, value, ttl)
local mc, err = get_mc()
if not mc then return false, err end
local ok, err = mc:set(key, value, ttl or 300)
release_mc(mc)
return ok, err
end
-- 删除缓存
function _m.delete(key)
local mc, err = get_mc()
if not mc then return false, err end
local ok, err = mc:delete(key)
release_mc(mc)
return ok, err
end
-- 原子递增(计数器场景)
function _m.incr(key, delta, init_ttl)
local mc, err = get_mc()
if not mc then return nil, err end
-- 先尝试incr,若key不存在则set初始值
local val, err = mc:incr(key, delta)
if not val and err == "not found" then
local ok, serr = mc:set(key, tostring(delta), init_ttl or 60)
if ok then
release_mc(mc)
return delta, nil
end
release_mc(mc)
return nil, serr
end
release_mc(mc)
return val, err
end
return _m4.2 nginx配置集成
http {
lua_shared_dict mc_local_cache 50m; -- l1本地缓存
init_by_lua_block {
mcache = require "mcache"
}
server {
listen 80;
location /api/session/ {
content_by_lua_block {
local session_id = ngx.var.cookie_session_id
if not session_id then
ngx.status = 401
return ngx.say('{"error":"unauthorized"}')
end
local key = "sess:" .. session_id
-- l1: 本地共享字典(微秒级)
local local_cache = ngx.shared.mc_local_cache
local val = local_cache:get(key)
if val then
ngx.header["x-cache"] = "l1-hit"
ngx.say(val)
return
end
-- l2: memcached
local data, err = mcache.get(key)
if data then
local_cache:set(key, data, 3) -- l1 ttl极短
ngx.header["x-cache"] = "mc-hit"
ngx.say(data)
return
end
-- miss: 回源
local res = ngx.location.capture("/internal/session_backend")
if res.status == 200 then
mcache.set(key, res.body, 1800) -- 30分钟
local_cache:set(key, res.body, 3)
ngx.header["x-cache"] = "miss"
ngx.say(res.body)
else
ngx.status = res.status
ngx.say(res.body)
end
}
}
# 计数器接口(利用memcached原子incr)
location /api/counter/ {
content_by_lua_block {
local key = "cnt:" .. ngx.var.uri
local val, err = mcache.incr(key, 1, 3600)
if val then
ngx.say(tostring(val))
else
ngx.log(ngx.err, "counter incr failed: ", err)
ngx.status = 500
ngx.say('{"error":"counter unavailable"}')
end
}
}
location /internal/session_backend {
internal;
proxy_pass http://session_service;
}
}
}4.3 两层缓存架构解析
| 层级 | 存储 | ttl | 作用 | 延迟 |
|---|---|---|---|---|
| l1 | lua_shared_dict | 3s | 拦截热点session,避免网络开销 | <10μs |
| l2 | memcached | 30min | 集群共享session存储 | 0.05~0.2ms |
📌 设计要点:session场景下l1 ttl设为3秒而非更长,因为session可能被其他节点修改(如登出),过长的本地缓存会导致一致性问题。
五、关键生产调优要点
5.1 memcached服务端优化
# /etc/sysconfig/memcached 或启动参数 options="-m 8192 -c 4096 -t 16 -i 2m -r 100 -o modern"
| 参数 | 含义 | 生产建议 |
|---|---|---|
-m | 最大内存(mb) | 物理内存的60%~70%,预留系统空间 |
-c | 最大连接数 | ≥ nginx workers × pool_size × 1.5 |
-t | 工作线程数 | = cpu核数,不超过32 |
-i | 最大value大小 | 默认1mb,按需调整(最大128mb) |
-r | 单连接最大请求数 | 防止慢客户端占用连接,建议100~500 |
-o modern | 启用现代优化 | 禁用旧协议兼容,提升性能 |
5.2 连接池 sizing 公式
总并发连接 = nginx worker数 × pool_size 推荐值:worker=8, pool_size=200 → 总连接=1600 memcached -c 应 ≥ 1600 × 1.5 = 2400
⚠️ 避坑:pool_size过大导致memcached连接耗尽;过小导致nginx排队等待。通过
stats curr_connections监控实际连接数,动态调整。
5.3 key设计规范
-- ✅ 推荐:命名空间 + 业务标识 + 版本 local key = "sess:v2:" .. session_id local key = "cnt:api:/users:list" -- ❌ 避免:过长key(memcached限制250字节) local key = "session:user:profile:data:" .. long_uuid -- 可能超限 -- ❌ 避免:特殊字符(空格、换行、控制符) local key = "key with space" -- 协议解析错误
5.4 二进制安全与序列化
memcached是二进制安全的,可直接存储任意字节序列,无需base64编码:
-- ✅ 直接存储二进制数据(如图片缩略图、protobuf)
mcache.set("thumb:" .. id, binary_data, 3600)
-- ✅ json文本也可直接存储
mcache.set("api:" .. key, cjson.encode(data), 300)
-- ⚠️ 注意:get返回的是原始字符串,需自行判断是否解码
local raw = mcache.get(key)
local data = cjson.decode(raw) -- 若确定是json才解码📌 优势:相比redis的lua脚本中处理二进制需额外编码,memcached天然支持,减少cpu开销和数据膨胀。
六、一致性与故障处理
6.1 无持久化的应对策略
memcached重启后数据全部丢失,这是特性而非bug。应对方式:
| 策略 | 实现 | 适用场景 |
|---|---|---|
| 接受冷启动 | 预热脚本 + 渐进式回源 | session、临时缓存 |
| 双写兜底 | 同时写mysql/redis,mc仅作加速层 | 配置项、元数据 |
| 本地备份 | lua_shared_dict保留最近n条 | 极端低延迟要求 |
6.2 故障降级
local data, err = mcache.get(key)
if err and (err == "timeout" or err == "connection refused") then
ngx.log(ngx.warn, "memcached degraded: ", err)
-- 降级:跳过缓存,直接回源
-- 或返回预设默认值
res = ngx.location.capture("/internal/backend")
ngx.header["x-cache-degraded"] = "mc-unavailable"
ngx.say(res.body)
return
end📌 原则:memcached是加速层,不是数据源。永远不要让缓存故障变成服务故障。
6.3 批量操作限制
memcached协议不支持原子批量操作。mget可批量读取,但写入/删除必须逐个执行:
-- ✅ 批量读取
local keys = {"k1", "k2", "k3"}
local results, err = mc:get(keys) -- 返回table
-- ❌ 无批量写入
-- 只能循环set,或使用pipeline(lua-resty-memcached不原生支持)对于高频批量写入场景,考虑改用redis或拆分请求。
七、监控体系
7.1 memcached关键指标
| 指标 | 命令 | 健康阈值 | 说明 |
|---|---|---|---|
| 命中率 | stats → get_hits/get_cmds | >80% | <60%需排查key设计或容量 |
| 连接数 | stats → curr_connections | < max_connections×80% | 接近上限需扩容或调pool |
| 内存使用 | stats → bytes/limit_maxbytes | <90% | >90%触发lru淘汰 |
| 驱逐率 | stats → evictions | =0 | >0表示内存不足 |
| 线程繁忙 | stats → busy_threads | < threads×50% | 过高需增加-t或扩容 |
7.2 nginx侧指标
| 指标 | 采集方式 | 告警阈值 |
|---|---|---|
| mc操作p99延迟 | access_log histogram | >0.5ms |
| 连接池使用率 | lua_shared_dict stats | >80% |
| mc错误率 | error.log聚合 | >0.1% |
| l1 hit率 | shared_dict hits/misses | 热点接口>30% |
7.3 grafana面板建议
- 缓存漏斗:request → l1 hit → mc hit → backend
- memcached命中率趋势:突降关联发布或流量变化
- 连接池水位:峰值是否触及pool_size上限
- 驱逐率曲线:非零即告警,内存规划失误的信号
八、常见踩坑速查表
| 现象 | 根因 | 解决方案 |
|---|---|---|
| set成功但get返回nil | key含特殊字符或超长 | 校验key长度≤250,过滤非法字符 |
| 连接超时频发 | pool_size过小或mc -c不足 | 增大两端配置,检查网络 |
| 命中率持续低迷 | ttl过短或key设计不合理 | 分析get_misses日志,优化key |
| 内存未满但频繁eviction | slab分配不均(大/小对象混存) | 分离不同大小对象的mc实例 |
| incr返回not found | key未初始化 | incr前先set初始值(见4.1代码) |
| 二进制数据损坏 | 误用json解码 | 区分文本/二进制,按需解码 |
| lua代码修改不生效 | lua_code_cache未开启 | 确认lua_code_cache on; |
| 多节点数据不一致 | 期望memcached提供一致性 | mc是无状态缓存,一致性靠应用层 |
| 重启后大量miss | 未做预热 | 部署前执行预热脚本 |
| stats命令被拒绝 | 未开启统计或防火墙拦截 | 检查-u参数和网络策略 |
九、结语
到此这篇关于nginx外置缓存-nginx + memcached的文章就介绍到这了,更多相关nginx外置缓存内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论