Nginx外置缓存-nginx + memcached

发布时间:2026/8/3 9:03:02
Nginx外置缓存-nginx + memcached 一、引言当Redis“太重”Memcached才是正解在《Nginx外置缓存》系列中我们多次以Redis作为分布式缓存的代表。但在某些极端追求低延迟、高吞吐、纯KV语义的场景下Redis反而成了“过度设计”会话缓存Session只需GET/SET/DELETE不需要List/Set/Hash等复杂数据结构API响应片段缓存对象大小固定10KBQPS 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而非Redis2.1 核心差异对比维度MemcachedRedis数据模型纯KV字符串String/List/Hash/Set/ZSet/Stream线程模型多线程CPU利用率高单线程6.0后IO多线程命令仍单线程持久化❌ 无✅ RDB/AOF集群模式客户端一致性哈希服务端Cluster/Sentinel内存管理Slab Allocator无碎片jemalloc可能有碎片最大Value默认1MB可调至128MB512MB过期策略惰性定期淘汰惰性定期淘汰适用场景临时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_modulengx_http_memcached_module✅❌❌❌仅读取预填充缓存已废弃ngx_memcngx_memc_module✅✅✅❌传统Nginx、简单读写lua-resty-memcachedOpenResty✅✅✅✅生产首选、灵活逻辑⚠️重要提醒原生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.lualocal 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作用延迟L1lua_shared_dict3s拦截热点Session避免网络开销10μsL2Memcached30min集群共享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 推荐值worker8, pool_size200 → 总连接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 -- ❌ 避免过长KeyMemcached限制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/RedisMC仅作加速层配置项、元数据本地备份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或使用pipelinelua-resty-memcached不原生支持对于高频批量写入场景考虑改用Redis或拆分请求。七、监控体系7.1 Memcached关键指标指标命令健康阈值说明命中率stats→ get_hits/get_cmds80%60%需排查Key设计或容量连接数stats→ curr_connections max_connections×80%接近上限需扩容或调pool内存使用stats→ bytes/limit_maxbytes90%90%触发LRU淘汰驱逐率stats→ evictions00表示内存不足线程繁忙stats→ busy_threads threads×50%过高需增加-t或扩容7.2 Nginx侧指标指标采集方式告警阈值MC操作P99延迟access_log histogram0.5ms连接池使用率lua_shared_dict stats80%MC错误率error.log聚合0.1%L1 HIT率shared_dict hits/misses热点接口30%7.3 Grafana面板建议缓存漏斗Request → L1 HIT → MC HIT → BackendMemcached命中率趋势突降关联发布或流量变化连接池水位峰值是否触及pool_size上限驱逐率曲线非零即告警内存规划失误的信号八、常见踩坑速查表现象根因解决方案set成功但get返回nilKey含特殊字符或超长校验Key长度≤250过滤非法字符连接超时频发pool_size过小或MC -c不足增大两端配置检查网络命中率持续低迷TTL过短或Key设计不合理分析get_misses日志优化Key内存未满但频繁evictionSlab分配不均大/小对象混存分离不同大小对象的MC实例incr返回not foundKey未初始化incr前先set初始值见4.1代码二进制数据损坏误用JSON解码区分文本/二进制按需解码Lua代码修改不生效lua_code_cache未开启确认lua_code_cache on;多节点数据不一致期望Memcached提供一致性MC是无状态缓存一致性靠应用层重启后大量MISS未做预热部署前执行预热脚本stats命令被拒绝未开启统计或防火墙拦截检查-u参数和网络策略九、结语感谢您的阅读如果你有任何疑问或想要分享的经验请在评论区留言交流