nginx

关注公众号 jb51net

关闭
首页 > 网站技巧 > 服务器 > nginx > Nginx本地缓存瓶颈外置缓存方案

Nginx本地缓存瓶颈怎么破?外置缓存方案详解

作者:難釋懷

本文深入剖析Nginx本地缓存的五大缺陷,详解基于OpenResty+Redis的分布式外置缓存架构,涵盖选型对比、Lua脚本实现、三层缓存设计、生产调优及一致性保障策略,助您突破单机缓存瓶颈,构建高可用、易扩展的缓存体系

一、引言:当Nginx本地缓存成为架构短板

在单体或小型集群中,Nginx的 proxy_cache 是性价比极高的缓存方案。但当业务规模跨越某个临界点后,本地缓存的固有缺陷会集中爆发:

这些问题的根源在于:本地缓存将“计算”与“存储”强绑定在了同一个进程和物理机上。解决之道是将缓存层从Nginx中解耦,下沉到独立的分布式缓存集群——这就是“外置缓存”的核心思想。

本文将从架构选型、协议对接、一致性策略和生产调优四个维度,构建一套以Nginx为接入层、Redis/Memcached为存储层的分布式缓存体系。

二、架构全景:外置缓存的三种形态

2.1 形态对比

形态实现方式优点缺点适用场景
Nginx + Redis/MCOpenResty/njs直连外部缓存灵活、生态成熟、功能完整需Lua/njs开发能力API网关、动态内容缓存
Nginx + SRPCache专用共享缓存模块高性能、协议原生支持社区活跃度低、文档少纯静态内容、CDN源站
Nginx → Varnish/ATS前置专业缓存代理功能强大、HTTP语义完整多一跳延迟、运维复杂度↑大规模CDN、复杂缓存逻辑

选型建议:90%的业务场景选择 OpenResty + Redis 组合。它兼顾了灵活性、性能和生态,且团队学习成本可控。Varnish适合已有专业缓存团队的超大规模场景;SRPCache仅推荐给对性能有极致要求且能接受维护风险的团队。

2.2 本文聚焦:OpenResty + Redis架构

客户端 → Nginx(OpenResty) → [Redis Cluster] → 源站
              ↓                    ↑
         Lua缓存逻辑          分布式存储
         (get/set/stale)      (一致性哈希)

核心优势:

三、OpenResty + Redis生产级实现

3.1 环境准备

# 安装OpenResty(内置LuaJIT + ngx_lua模块)
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_redis2_module --with-http_lua_module
make && make install

# 安装lua-resty-redis库(若未内置)
luarocks install lua-resty-redis

3.2 核心Lua缓存模块

创建 /usr/local/openresty/lualib/cache_handler.lua

local redis = require "resty.redis"
local cjson = require "cjson.safe"

local _M = {}

-- Redis连接池配置
local REDIS_CONF = {
    host = "redis-cluster.internal",
    port = 6379,
    pool_size = 100,        -- 每worker连接池大小
    backlog = 200,          -- 等待队列
    connect_timeout = 100,  -- ms
    read_timeout = 200,     -- ms
}

-- 获取Redis连接(带连接池)
local function get_redis()
    local red = redis:new()
    red:set_timeouts(REDIS_CONF.connect_timeout, 
                     REDIS_CONF.read_timeout, 
                     REDIS_CONF.read_timeout)
    
    local ok, err = red:connect(REDIS_CONF.host, REDIS_CONF.port)
    if not ok then
        ngx.log(ngx.ERR, "redis connect failed: ", err)
        return nil, err
    end
    
    return red, nil
end

-- 释放连接到池中
local function release_redis(red)
    local ok, err = red:set_keepalive(10000, REDIS_CONF.pool_size)
    if not ok then
        ngx.log(ngx.ERR, "redis keepalive failed: ", err)
    end
end

-- 读取缓存
function _M.get(key)
    local red, err = get_redis()
    if not red then return nil, err end
    
    local res, err = red:get(key)
    release_redis(red)
    
    if not res or res == ngx.null then
        return nil, "miss"
    end
    
    return cjson.decode(res), nil
end

-- 写入缓存(带TTL)
function _M.set(key, value, ttl)
    local red, err = get_redis()
    if not red then return false, err end
    
    local encoded = cjson.encode(value)
    local ok, err = red:setex(key, ttl, encoded)
    release_redis(red)
    
    return ok ~= nil, err
end

-- 删除缓存
function _M.delete(key)
    local red, err = get_redis()
    if not red then return false, err end
    
    local ok, err = red:del(key)
    release_redis(red)
    
    return ok ~= nil, err
end

return _M

3.3 Nginx配置集成

http {
    # Lua共享字典:用于本地二级缓存和锁
    lua_shared_dict local_cache 100m;
    lua_shared_dict cache_locks 10m;
    init_by_lua_block {
        cache_handler = require "cache_handler"
    }
    server {
        listen 80;
        location /api/ {
            content_by_lua_block {
                local key = ngx.var.scheme .. ":" .. ngx.var.host .. ngx.var.uri
                -- 第1层:本地共享字典缓存(微秒级)
                local local_cache = ngx.shared.local_cache
                local val = local_cache:get(key)
                if val then
                    ngx.header["X-Cache"] = "LOCAL-HIT"
                    ngx.say(val)
                    return
                end
                -- 第2层:Redis外置缓存
                local data, err = cache_handler.get(key)
                if data then
                    -- 回填本地缓存(短TTL,防热点穿透)
                    local_cache:set(key, cjson.encode(data), 5)
                    ngx.header["X-Cache"] = "REDIS-HIT"
                    ngx.say(cjson.encode(data))
                    return
                end
                -- 第3层:缓存击穿防护(分布式锁)
                local locks = ngx.shared.cache_locks
                local lock_key = "lock:" .. key
                local elapsed, err = locks:add(lock_key, true, 3)
                if elapsed then
                    -- 获得锁,回源
                    local res = ngx.location.capture("/internal/backend")
                    if res.status == 200 then
                        local body = res.body
                        -- 写入Redis(长TTL)
                        cache_handler.set(key, cjson.decode(body), 300)
                        -- 写入本地缓存
                        local_cache:set(key, body, 5)
                        ngx.header["X-Cache"] = "MISS"
                        ngx.say(body)
                    else
                        ngx.status = res.status
                        ngx.say(res.body)
                    end
                    locks:delete(lock_key)
                else
                    -- 未获得锁,短暂等待后重试或直接回源
                    ngx.sleep(0.1)
                    local retry_data = cache_handler.get(key)
                    if retry_data then
                        ngx.header["X-Cache"] = "LOCK-WAIT-HIT"
                        ngx.say(cjson.encode(retry_data))
                    else
                        -- 降级:直接回源(不缓存)
                        local res = ngx.location.capture("/internal/backend")
                        ngx.header["X-Cache"] = "BYPASS"
                        ngx.status = res.status
                        ngx.say(res.body)
                    end
                end
            }
        }
        # 内部回源location
        location /internal/backend {
            internal;
            proxy_pass http://backend;
            proxy_set_header Host $host;
        }
    }
}

3.4 三层缓存架构解析

层级存储位置TTL作用延迟
L1lua_shared_dict5s拦截热点请求,避免Redis网络开销<10μs
L2Redis Cluster5min集群共享缓存,保证一致性0.1~0.5ms
L3源站-数据权威来源1~50ms

设计精髓:L1用极短TTL换取零网络延迟,L2用合理TTL换取集群一致性。两者配合,既避免了Redis成为新瓶颈,又解决了本地缓存的一致性问题。

四、关键生产调优要点

4.1 Redis连接池是性能命脉

-- ❌ 错误:每次请求新建连接
local red = redis:new()
red:connect(...)
-- 请求结束连接销毁
-- ✅ 正确:使用set_keepalive复用连接
red:set_keepalive(10000, 100)  -- 空闲10s,池大小100

每个Nginx worker维护独立连接池。若worker数=8、pool_size=100,则最大并发Redis连接=800。务必确保Redis maxclients > Nginx workers × pool_size

4.2 序列化格式选择

格式编码速度解码速度体积跨语言推荐场景
JSON (cjson)通用API响应
MessagePack更快更快高频内部通信
Protobuf最快最快最小结构化数据、带宽敏感
Lua table最快最快最小仅OpenResty内部

避坑:不要用 cjson.encode 缓存包含二进制数据的响应体。JSON无法安全表示任意字节序列,会导致数据损坏。二进制内容应使用Base64编码或直接存Redis binary string。

4.3 缓存Key设计规范

-- ✅ 推荐:命名空间 + 版本 + 业务标识
local key = "api:v2:user:profile:" .. user_id

-- ❌ 避免:直接使用URI
local key = ngx.var.uri  -- 参数顺序变化导致重复缓存

Key设计原则:

4.4 故障降级策略

-- Redis不可用时,降级到本地缓存或直接回源
local data, err = cache_handler.get(key)
if err and (err == "timeout" or err == "connection refused") then
    ngx.log(ngx.WARN, "redis degraded, fallback to backend")
    -- 跳过缓存,直接回源
    -- 或尝试读取过期的本地缓存作为兜底
end

永远不要让缓存故障变成服务故障。外置缓存是加速手段,不是可用性依赖。

五、缓存一致性保障

5.1 主动失效 vs 被动过期

策略实现一致性复杂度适用场景
短TTLRedis SETEX 30s最终一致(≤30s)容忍短暂延迟的内容
主动DEL业务写操作后调用cache_handler.delete准实时一致用户数据、配置项
版本号Key含version,发布时递增发布级一致静态资源、API文档
双删先删缓存→写DB→延迟再删强一致金融级数据

5.2 批量清理方案

-- 按前缀批量删除(Redis SCAN + DEL,非KEYS)
function _M.delete_pattern(pattern)
    local red, err = get_redis()
    if not red then return false, err end
    local cursor = "0"
    repeat
        local res, err = red:scan(cursor, "MATCH", pattern, "COUNT", 100)
        if not res then break end
        cursor = res[1]
        local keys = res[2]
        if #keys > 0 then
            red:del(unpack(keys))
        end
    until cursor == "0"
    release_redis(red)
    return true, nil
end

⚠️ 严禁在生产使用KEYS命令。SCAN游标遍历是唯一安全的批量操作方式。

六、监控体系

6.1 必采指标

指标采集方式健康阈值
L1 HIT率lua_shared_dict stats>30%(热点接口)
L2 HIT率Redis INFO stats>60%
Redis P99延迟redis_exporter<1ms
连接池使用率ngx.shared.stats<80%
缓存操作错误率error.log聚合<0.1%
Redis内存使用率redis_exporter<75%

6.2 Grafana面板核心视图

七、常见踩坑速查表

现象根因解决方案
Redis连接耗尽未使用连接池或pool_size过小set_keepalive + 增大pool_size
缓存命中但数据错乱Key设计缺少区分变量补全scheme/host/method/args
热点Key打爆单分片未做本地缓存L1 lua_shared_dict拦截
批量删除超时使用KEYS而非SCAN改用SCAN游标遍历
二进制响应缓存损坏JSON序列化二进制数据改用MessagePack或Base64
Redis故障时全站500无降级逻辑try-catch包裹缓存操作
扩容后命中率骤降未预热部署前执行预热脚本
Lua代码修改不生效未reload或未启用code_cachelua_code_cache on + reload
内存泄漏Lua闭包持有大对象引用及时置nil + GC调优
锁竞争严重lock TTL过长或粒度过粗缩短TTL + 细化key粒度

八、总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文