Nginx外置缓存之nginx + memcached步骤详解

 更新时间:2026年08月03日 09:37:35   作者:難釋懷  
本文将从协议特性、模块选型、生产配置到性能调优,构建一套真正可用的Nginx + Memcached外置缓存体系,感兴趣的朋友跟随小编一起看看吧

在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 核心差异对比

维度MemcachedRedis
数据模型纯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_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.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 _M

4.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μs
L2Memcached30min集群共享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返回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参数和网络策略

九、结语

到此这篇关于Nginx外置缓存-nginx + memcached的文章就介绍到这了,更多相关Nginx外置缓存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Nginx 应用范围和使用详解

    Nginx 应用范围和使用详解

    这篇文章主要介绍了Nginx 应用范围和使用详解的相关资料,需要的朋友可以参考下
    2017-03-03
  • 网站如何通过nginx设置黑/白名单IP限制及国家城市IP访问限制

    网站如何通过nginx设置黑/白名单IP限制及国家城市IP访问限制

    如果你的服务器被攻击很厉害,而且服务器是自己练手的,不需要其他用户访问的,那么就可以配置一下nginx的白名单,下面这篇文章主要给大家介绍了关于网站如何通过nginx设置黑/白名单IP限制及国家城市IP访问限制的相关资料,需要的朋友可以参考下
    2022-07-07
  • Nginx缓存在服务端 代理和客户端的区别深入探索

    Nginx缓存在服务端 代理和客户端的区别深入探索

    这篇文章主要介绍了Nginx缓存在服务端 代理和客户端的区别深入探索,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-10-10
  • Windows下用Nginx配置https服务器及反向代理的问题

    Windows下用Nginx配置https服务器及反向代理的问题

    这篇文章主要介绍了Windows下用Nginx配置https服务器及反向代理的问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-09-09
  • Nginx 代理解决跨域问题多种情况分析

    Nginx 代理解决跨域问题多种情况分析

    这篇文章主要介绍了Nginx 代理解决跨域问题分析,通过用网站8080访问Nginx代理后的接口地址,报错分为多种情况,每种情况给大家详细分析,感兴趣的朋友一起看看吧
    2022-01-01
  • nginx如何搭建前后端分离架构

    nginx如何搭建前后端分离架构

    本文介绍了如何使用Vue-cli搭建前端开发环境,并详细讲解了nginx、webpack-devserver和Postman的使用方法,此外,还介绍了API基本设计规范,包括HTTP状态码、数据格式和接口设计
    2025-01-01
  • nginx配置https加密访问的详细教程

    nginx配置https加密访问的详细教程

    这篇文章主要介绍了nginx配置https加密访问的详细教程,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-11-11
  • Nginx安装成功后,无法访问到默认页面问题及解决

    Nginx安装成功后,无法访问到默认页面问题及解决

    这段文章主要讲述了在安装Nginx后无法访问其页面时,如何排查问题,重点在于检查防火墙设置,具体包括识别防火墙是否禁用了80端口,并提供了相应的解决步骤,适合遇到类似问题的技术人员参考
    2026-06-06
  • 详解Nginx反向代理跨域基本配置与常见误区

    详解Nginx反向代理跨域基本配置与常见误区

    这篇文章主要介绍了详解Nginx反向代理跨域基本配置与常见误区,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-08-08
  • Nginx WebSocket长连接及数据容量配置实践

    Nginx WebSocket长连接及数据容量配置实践

    Nginx通过配置HTTP代理,可以有效地处理WebSocket连接,支持长连接和大数据传输,关键配置包括设置HTTP/1.1版本、升级头部、连接头部、增加超时时间、调整最大请求体大小和临时文件大小,这些配置确保了WebSocket连接的稳定性和高效性
    2025-12-12

最新评论