Nginx本地緩存瓶頸怎么破?外置緩存方案詳解
一、引言:當(dāng)Nginx本地緩存成為架構(gòu)短板
在單體或小型集群中,Nginx的 proxy_cache 是性價(jià)比極高的緩存方案。但當(dāng)業(yè)務(wù)規(guī)??缭侥硞€(gè)臨界點(diǎn)后,本地緩存的固有缺陷會(huì)集中爆發(fā):
- 緩存孤島:10臺(tái)Nginx各自維護(hù)獨(dú)立緩存,同一資源被重復(fù)回源10次,整體命中率遠(yuǎn)低于預(yù)期;
- 擴(kuò)容即失效:新增Nginx節(jié)點(diǎn)后,所有新節(jié)點(diǎn)的緩存從零開(kāi)始,預(yù)熱期間后端壓力驟增;
- 清理不同步:內(nèi)容更新時(shí),需逐臺(tái)調(diào)用purge接口,遺漏任何一臺(tái)都會(huì)導(dǎo)致用戶看到臟數(shù)據(jù);
- 容量天花板:?jiǎn)螜C(jī)磁盤/內(nèi)存上限決定了緩存規(guī)模,無(wú)法隨業(yè)務(wù)線性擴(kuò)展;
- 故障域耦合:Nginx宕機(jī)不僅丟失連接,還丟失該節(jié)點(diǎn)全部緩存,恢復(fù)后引發(fā)回源風(fēng)暴。
這些問(wèn)題的根源在于:本地緩存將“計(jì)算”與“存儲(chǔ)”強(qiáng)綁定在了同一個(gè)進(jìn)程和物理機(jī)上。解決之道是將緩存層從Nginx中解耦,下沉到獨(dú)立的分布式緩存集群——這就是“外置緩存”的核心思想。
本文將從架構(gòu)選型、協(xié)議對(duì)接、一致性策略和生產(chǎn)調(diào)優(yōu)四個(gè)維度,構(gòu)建一套以Nginx為接入層、Redis/Memcached為存儲(chǔ)層的分布式緩存體系。
二、架構(gòu)全景:外置緩存的三種形態(tài)
2.1 形態(tài)對(duì)比
| 形態(tài) | 實(shí)現(xiàn)方式 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場(chǎng)景 |
|---|---|---|---|---|
| Nginx + Redis/MC | OpenResty/njs直連外部緩存 | 靈活、生態(tài)成熟、功能完整 | 需Lua/njs開(kāi)發(fā)能力 | API網(wǎng)關(guān)、動(dòng)態(tài)內(nèi)容緩存 |
| Nginx + SRPCache | 專用共享緩存模塊 | 高性能、協(xié)議原生支持 | 社區(qū)活躍度低、文檔少 | 純靜態(tài)內(nèi)容、CDN源站 |
| Nginx → Varnish/ATS | 前置專業(yè)緩存代理 | 功能強(qiáng)大、HTTP語(yǔ)義完整 | 多一跳延遲、運(yùn)維復(fù)雜度↑ | 大規(guī)模CDN、復(fù)雜緩存邏輯 |
選型建議:90%的業(yè)務(wù)場(chǎng)景選擇 OpenResty + Redis 組合。它兼顧了靈活性、性能和生態(tài),且團(tuán)隊(duì)學(xué)習(xí)成本可控。Varnish適合已有專業(yè)緩存團(tuán)隊(duì)的超大規(guī)模場(chǎng)景;SRPCache僅推薦給對(duì)性能有極致要求且能接受維護(hù)風(fēng)險(xiǎn)的團(tuán)隊(duì)。
2.2 本文聚焦:OpenResty + Redis架構(gòu)
客戶端 → Nginx(OpenResty) → [Redis Cluster] → 源站
↓ ↑
Lua緩存邏輯 分布式存儲(chǔ)
(get/set/stale) (一致性哈希)核心優(yōu)勢(shì):
- 緩存共享:所有Nginx節(jié)點(diǎn)讀寫同一份緩存,命中率=集群級(jí)命中率
- 彈性伸縮:Redis Cluster擴(kuò)縮容不影響Nginx,緩存自動(dòng)重平衡
- 統(tǒng)一清理:一次DEL命令全局生效,無(wú)需逐臺(tái)purge
- 持久化可選:AOF/RDB提供重啟恢復(fù)能力,避免冷啟動(dòng)風(fēng)暴
三、OpenResty + Redis生產(chǎn)級(jí)實(shí)現(xiàn)
3.1 環(huán)境準(zhǔn)備
# 安裝OpenResty(內(nèi)置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庫(kù)(若未內(nèi)置) luarocks install lua-resty-redis
3.2 核心Lua緩存模塊
創(chuàng)建 /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, -- 等待隊(duì)列
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 _M3.3 Nginx配置集成
http {
# Lua共享字典:用于本地二級(jí)緩存和鎖
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層:本地共享字典緩存(微秒級(jí))
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,防熱點(diǎn)穿透)
local_cache:set(key, cjson.encode(data), 5)
ngx.header["X-Cache"] = "REDIS-HIT"
ngx.say(cjson.encode(data))
return
end
-- 第3層:緩存擊穿防護(hù)(分布式鎖)
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(長(zhǎng)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
-- 降級(jí):直接回源(不緩存)
local res = ngx.location.capture("/internal/backend")
ngx.header["X-Cache"] = "BYPASS"
ngx.status = res.status
ngx.say(res.body)
end
end
}
}
# 內(nèi)部回源location
location /internal/backend {
internal;
proxy_pass http://backend;
proxy_set_header Host $host;
}
}
}3.4 三層緩存架構(gòu)解析
| 層級(jí) | 存儲(chǔ)位置 | TTL | 作用 | 延遲 |
|---|---|---|---|---|
| L1 | lua_shared_dict | 5s | 攔截?zé)狳c(diǎn)請(qǐng)求,避免Redis網(wǎng)絡(luò)開(kāi)銷 | <10μs |
| L2 | Redis Cluster | 5min | 集群共享緩存,保證一致性 | 0.1~0.5ms |
| L3 | 源站 | - | 數(shù)據(jù)權(quán)威來(lái)源 | 1~50ms |
設(shè)計(jì)精髓:L1用極短TTL換取零網(wǎng)絡(luò)延遲,L2用合理TTL換取集群一致性。兩者配合,既避免了Redis成為新瓶頸,又解決了本地緩存的一致性問(wèn)題。
四、關(guān)鍵生產(chǎn)調(diào)優(yōu)要點(diǎn)
4.1 Redis連接池是性能命脈
-- ? 錯(cuò)誤:每次請(qǐng)求新建連接 local red = redis:new() red:connect(...) -- 請(qǐng)求結(jié)束連接銷毀 -- ? 正確:使用set_keepalive復(fù)用連接 red:set_keepalive(10000, 100) -- 空閑10s,池大小100
每個(gè)Nginx worker維護(hù)獨(dú)立連接池。若worker數(shù)=8、pool_size=100,則最大并發(fā)Redis連接=800。務(wù)必確保Redis maxclients > Nginx workers × pool_size。
4.2 序列化格式選擇
| 格式 | 編碼速度 | 解碼速度 | 體積 | 跨語(yǔ)言 | 推薦場(chǎng)景 |
|---|---|---|---|---|---|
| JSON (cjson) | 快 | 快 | 大 | ? | 通用API響應(yīng) |
| MessagePack | 更快 | 更快 | 小 | ? | 高頻內(nèi)部通信 |
| Protobuf | 最快 | 最快 | 最小 | ? | 結(jié)構(gòu)化數(shù)據(jù)、帶寬敏感 |
| Lua table | 最快 | 最快 | 最小 | ? | 僅OpenResty內(nèi)部 |
避坑:不要用 cjson.encode 緩存包含二進(jìn)制數(shù)據(jù)的響應(yīng)體。JSON無(wú)法安全表示任意字節(jié)序列,會(huì)導(dǎo)致數(shù)據(jù)損壞。二進(jìn)制內(nèi)容應(yīng)使用Base64編碼或直接存Redis binary string。
4.3 緩存Key設(shè)計(jì)規(guī)范
-- ? 推薦:命名空間 + 版本 + 業(yè)務(wù)標(biāo)識(shí) local key = "api:v2:user:profile:" .. user_id -- ? 避免:直接使用URI local key = ngx.var.uri -- 參數(shù)順序變化導(dǎo)致重復(fù)緩存
Key設(shè)計(jì)原則:
- 可讀性:便于調(diào)試和手動(dòng)清理
- 唯一性:包含影響響應(yīng)內(nèi)容的所有變量
- 可演進(jìn)性:嵌入版本號(hào),發(fā)布時(shí)可平滑切換
- 長(zhǎng)度控制:<256字節(jié),過(guò)長(zhǎng)浪費(fèi)Redis內(nèi)存和網(wǎng)絡(luò)帶寬
4.4 故障降級(jí)策略
-- Redis不可用時(shí),降級(jí)到本地緩存或直接回源
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")
-- 跳過(guò)緩存,直接回源
-- 或嘗試讀取過(guò)期的本地緩存作為兜底
end永遠(yuǎn)不要讓緩存故障變成服務(wù)故障。外置緩存是加速手段,不是可用性依賴。
五、緩存一致性保障
5.1 主動(dòng)失效 vs 被動(dòng)過(guò)期
| 策略 | 實(shí)現(xiàn) | 一致性 | 復(fù)雜度 | 適用場(chǎng)景 |
|---|---|---|---|---|
| 短TTL | Redis SETEX 30s | 最終一致(≤30s) | 低 | 容忍短暫延遲的內(nèi)容 |
| 主動(dòng)DEL | 業(yè)務(wù)寫操作后調(diào)用cache_handler.delete | 準(zhǔn)實(shí)時(shí)一致 | 中 | 用戶數(shù)據(jù)、配置項(xiàng) |
| 版本號(hào) | Key含version,發(fā)布時(shí)遞增 | 發(fā)布級(jí)一致 | 低 | 靜態(tài)資源、API文檔 |
| 雙刪 | 先刪緩存→寫DB→延遲再刪 | 強(qiáng)一致 | 高 | 金融級(jí)數(shù)據(jù) |
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?? 嚴(yán)禁在生產(chǎn)使用KEYS命令。SCAN游標(biāo)遍歷是唯一安全的批量操作方式。
六、監(jiān)控體系
6.1 必采指標(biāo)
| 指標(biāo) | 采集方式 | 健康閾值 |
|---|---|---|
| L1 HIT率 | lua_shared_dict stats | >30%(熱點(diǎn)接口) |
| L2 HIT率 | Redis INFO stats | >60% |
| Redis P99延遲 | redis_exporter | <1ms |
| 連接池使用率 | ngx.shared.stats | <80% |
| 緩存操作錯(cuò)誤率 | error.log聚合 | <0.1% |
| Redis內(nèi)存使用率 | redis_exporter | <75% |
6.2 Grafana面板核心視圖
- 緩存漏斗圖:Request → L1 HIT → L2 HIT → Backend,直觀展示各層攔截效果
- Redis延遲熱力圖:按時(shí)間段和命令類型分布,定位慢查詢
- 連接池水位曲線:峰值是否接近pool_size上限
- 錯(cuò)誤率趨勢(shì):突增是否關(guān)聯(lián)發(fā)布或Redis故障
七、常見(jiàn)踩坑速查表
| 現(xiàn)象 | 根因 | 解決方案 |
|---|---|---|
| Redis連接耗盡 | 未使用連接池或pool_size過(guò)小 | set_keepalive + 增大pool_size |
| 緩存命中但數(shù)據(jù)錯(cuò)亂 | Key設(shè)計(jì)缺少區(qū)分變量 | 補(bǔ)全scheme/host/method/args |
| 熱點(diǎn)Key打爆單分片 | 未做本地緩存 | L1 lua_shared_dict攔截 |
| 批量刪除超時(shí) | 使用KEYS而非SCAN | 改用SCAN游標(biāo)遍歷 |
| 二進(jìn)制響應(yīng)緩存損壞 | JSON序列化二進(jìn)制數(shù)據(jù) | 改用MessagePack或Base64 |
| Redis故障時(shí)全站500 | 無(wú)降級(jí)邏輯 | try-catch包裹緩存操作 |
| 擴(kuò)容后命中率驟降 | 未預(yù)熱 | 部署前執(zhí)行預(yù)熱腳本 |
| Lua代碼修改不生效 | 未reload或未啟用code_cache | lua_code_cache on + reload |
| 內(nèi)存泄漏 | Lua閉包持有大對(duì)象引用 | 及時(shí)置nil + GC調(diào)優(yōu) |
| 鎖競(jìng)爭(zhēng)嚴(yán)重 | lock TTL過(guò)長(zhǎng)或粒度過(guò)粗 | 縮短TTL + 細(xì)化key粒度 |
八、總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Windows設(shè)置nginx開(kāi)機(jī)自啟動(dòng)的方法
這篇文章主要介紹了Windows設(shè)置nginx開(kāi)機(jī)自啟動(dòng)的方法,通過(guò)兩種方式實(shí)現(xiàn)nginx的開(kāi)機(jī)自啟動(dòng):winws和window計(jì)劃程序,每種方式給大家介紹的非常詳細(xì)需要的朋友可以參考下2022-11-11
利用Nginx的map指令實(shí)現(xiàn)頁(yè)面跳轉(zhuǎn)
每位網(wǎng)站運(yùn)營(yíng)人可能都會(huì)碰到一些情況,比如網(wǎng)站URL規(guī)則會(huì)進(jìn)行調(diào)整,需求的不斷變化也會(huì)導(dǎo)致一些舊的URL無(wú)法訪問(wèn),這個(gè)時(shí)候可以使用Nginx的 map指令匹配這些舊的URL,并跳轉(zhuǎn)到新的URL規(guī)則,而且這種方式是在Nginx層面進(jìn)行,不會(huì)對(duì)網(wǎng)站性能產(chǎn)生影響。下面來(lái)一起看看吧。2016-10-10
Nginx動(dòng)靜分離實(shí)現(xiàn)案例代碼解析
這篇文章主要介紹了Nginx動(dòng)靜分離實(shí)現(xiàn)案例代碼解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08
Nginx配置反向代理服務(wù)器實(shí)現(xiàn)在https網(wǎng)站中請(qǐng)求http資源
?Nginx反向代理?是一種將客戶端請(qǐng)求轉(zhuǎn)發(fā)到后端服務(wù)器的技術(shù),主要用于負(fù)載均衡、提高安全性和提升性能,本文給大家介紹了Nginx配置反向代理服務(wù)器實(shí)現(xiàn)在https網(wǎng)站中請(qǐng)求http資源,需要的朋友可以參考下2025-03-03
ubuntu16.04下徹底卸載nginx的相關(guān)命令
nginx是一款自由的、開(kāi)源的、高性能的HTTP服務(wù)器和反向代理服務(wù)器;這篇文章主要介紹了ubuntu16.04下徹底卸載nginx的相關(guān)命令,需要的朋友可以參考下2018-12-12

