Nginx外置缓存-error_page

📅 2026/7/31 2:37:36
Nginx外置缓存-error_page
一、引言当缓存成为故障源error_page是最后防线在《Nginx外置缓存》系列前文中我们构建了以OpenResty Redis为核心的分布式缓存体系。这套架构在正常情况下表现优异但生产环境的残酷现实是外置缓存本身可能成为最大的单点故障。Redis集群主从切换期间数百毫秒的写入阻塞导致Nginx大量504缓存服务OOM重启所有请求瞬间穿透到源站引发雪崩Lua脚本bug导致Redis连接池耗尽正常业务被缓存模块拖垮网络分区使Redis不可达error_log中刷屏connection refused用户看到默认500白页缓存返回了损坏的数据如截断的JSON客户端解析失败却无从恢复。这些场景的共同点是缓存层的异常没有被优雅地捕获和转化。默认的Nginx错误处理机制对Lua层面的异常几乎无感知而开发者往往只关注“缓存命中时的性能”忽略了“缓存失效时的生存能力”。error_page指令在传统Nginx中用于自定义错误页面但在外置缓存架构中它的角色远不止于此——它是缓存故障与服务可用性之间的隔离阀是降级策略的执行入口是用户体验的最后一道保障。本文将重新定义error_page在外置缓存场景下的用法从基础语法到高级降级模式给出生产级容错架构。二、error_page在外置缓存中的三重角色2.1 角色重定义传统角色外置缓存中的新角色说明自定义错误页面降级触发器将缓存错误转化为可处理的内部跳转静态内容展示动态降级路由根据错误类型选择不同的回退路径用户友好提示可观测性探针通过不同状态码区分缓存故障类型核心认知在外置缓存架构中error_page不再是“给用户看的页面”而是“给系统用的控制流”。它拦截的是基础设施层的异常输出的是架构层的决策。2.2 为什么不能只在Lua里try-catch很多开发者认为在Lua代码中用pcall包裹Redis操作就够了。但这有三个盲区Nginx原生错误无法捕获proxy_pass超时、upstream连接失败等发生在C层面Lua的pcall看不到子请求错误不传播ngx.location.capture返回的错误状态码不会自动触发外层Lua的异常处理统一降级策略难维护每个location都写一套try-catch降级逻辑代码膨胀且不一致。error_page提供了声明式的、跨层级的错误处理机制与Lua的命令式异常处理互补而非替代。三、基础配置缓存错误的精准捕获3.1 区分缓存相关错误码server { listen 80; # 缓存专用错误映射 # Redis连接失败/超时 → 590 (自定义) error_page 590 cache_fallback_backend; # 缓存数据损坏/解析失败 → 591 error_page 591 cache_fallback_stale; # 源站也失败 → 592 error_page 592 cache_static_degraded; # 标准错误兜底 error_page 500 502 503 504 /50x.html; location /api/ { content_by_lua_block { -- Lua缓存逻辑见下文 } } # 降级处理器 location cache_fallback_backend { internal; add_header X-Cache-Degraded redis-unavailable always; proxy_pass http://backend; proxy_connect_timeout 3s; # ⭐ 降级时缩短超时 proxy_read_timeout 5s; } location cache_fallback_stale { internal; add_header X-Cache-Degraded stale-data always; # 尝试读取本地备份或返回预设默认值 content_by_lua_block { local fallback ngx.shared.local_cache:get(fallback: .. ngx.var.uri) if fallback then ngx.header[X-Cache] STALE-HIT ngx.say(fallback) else ngx.status 503 ngx.say({error:service temporarily unavailable}) end } } location cache_static_degraded { internal; add_header X-Cache-Degraded full-degradation always; default_type application/json; return 503 {code:503,msg:service degraded,retry_after:30}; } }3.2 自定义错误码的意义Nginx标准错误码无法区分“Redis挂了”和“源站挂了”。自定义590/591/592让监控和降级策略可以精确识别故障层级自定义码含义降级动作告警级别590Redis不可用跳过缓存直连源站P2591缓存数据异常返回过期数据或默认值P3592源站缓存均失败返回静态兜底响应P1⚠️注意自定义错误码仅在Nginx内部流转不会暴露给客户端。最终对外返回的仍是标准HTTP状态码200/503等。四、Lua层与error_page的桥接4.1 在Lua中触发error_pageLua代码不能直接“调用”error_page但可以通过设置特定状态码来间接触发-- cache_handler.lua 增强版 local redis require resty.redis local _M {} function _M.get(key) local red, err get_redis() if not red then -- ⭐ 设置自定义状态码触发error_page 590 ngx.status 590 ngx.log(ngx.WARN, redis connect failed, triggering fallback: , err) return nil, REDIS_UNAVAILABLE end local res, err red:get(key) release_redis(red) if not res or res ngx.null then return nil, MISS end -- 验证数据完整性 local data, decode_err cjson.decode(res) if not data then -- ⭐ 数据损坏触发error_page 591 ngx.status 591 ngx.log(ngx.ERR, cache data corrupted for key: , key, err: , decode_err) return nil, DATA_CORRUPTED end return data, nil end return _M4.2 content_by_lua_block中的错误处理范式content_by_lua_block { local key build_cache_key() -- 尝试缓存 local data, err cache_handler.get(key) if err REDIS_UNAVAILABLE or err DATA_CORRUPTED then -- ⭐ 关键exit让Nginx接管error_page处理 -- 此时ngx.status已被设为590/591 return ngx.exit(ngx.status) end if data then ngx.header[X-Cache] HIT ngx.say(cjson.encode(data)) return end -- MISS回源并回填缓存 local res ngx.location.capture(/internal/backend) if res.status ~ 200 then -- 源站失败但缓存也不可用 → 触发592 ngx.status 592 return ngx.exit(592) end -- 正常回填... cache_handler.set(key, cjson.decode(res.body), 300) ngx.say(res.body) }4.3 三个关键陷阱① ngx.exit必须在header发送前调用一旦调用了ngx.say()或ngx.print()响应头已发送ngx.exit无法改变状态码error_page也不会触发。所有错误判断必须在首次输出之前完成。② error_page的号语法决定状态码传递# ❌ 保留原始错误码590客户端看到非标准状态码 error_page 590 cache_fallback_backend; # ✅ 使用号重写为200客户端看到正常响应 error_page 590 200 cache_fallback_backend; # ✅ 使用号重写为503客户端看到标准服务不可用 error_page 592 503 cache_static_degraded;③ internal标记防止外部访问所有降级location必须加internal;否则攻击者可直接请求/cache_fallback_backend绕过缓存和安全检查。五、高级降级模式5.1 多级降级瀑布Redis可用 → 返回缓存数据 ↓ (590) 源站可用 → 返回实时数据跳过缓存 ↓ (592) 本地stale可用 → 返回过期数据 ↓ (stale miss) 静态兜底 → 返回预设JSON/HTML ↓ (兜底失败) 标准503 → 最小化错误响应实现要点每一级降级处理器内部仍可触发下一级error_page形成链式容错。5.2 基于Header的条件降级# 某些接口不允许降级如支付、鉴权 map $uri $allow_degradation { ~^/api/payment/ 0; ~^/api/auth/ 0; default 1; } server { # 仅允许降级的接口走error_page if ($allow_degradation 0) { set $no_fallback 1; } location /api/ { content_by_lua_block { if ngx.var.no_fallback 1 then -- 禁用降级直接透传错误 cache_handler.strict_mode(true) end -- ...正常逻辑 } } }5.3 降级响应的缓存降级响应本身也可以被短暂缓存避免每次请求都执行降级逻辑location cache_fallback_backend { internal; proxy_pass http://backend; # ⭐ 降级响应缓存5秒减轻源站压力 proxy_cache fallback_cache; proxy_cache_valid 200 5s; proxy_cache_key fallback:$request_uri; proxy_cache_use_stale error timeout; }注意降级缓存的TTL必须极短≤10s否则会在源站恢复后仍返回旧数据造成二次不一致。六、可观测性让降级可见6.1 降级事件日志格式log_format degradation $remote_addr [$time_local] $request $status $body_bytes_sent degrade_type$http_x_cache_degraded original_status$upstream_status response_time$request_time; # 仅在降级时记录 map $http_x_cache_degraded $log_degradation { 0; default 1; } access_log /var/log/nginx/degradation.log degradation if$log_degradation;6.2 Prometheus指标暴露-- 在降级处理器中递增计数器 content_by_lua_block { local metrics ngx.shared.metrics metrics:incr(cache_degradation_total, 1) metrics:incr(cache_degradation_ .. ngx.var.http_x_cache_degraded, 1) -- ...正常降级逻辑 }Grafana面板应包含降级率趋势线按类型分色降级持续时间分布降级期间的源站QPS变化降级恢复时间从首次降级到完全恢复6.3 告警规则- alert: CacheDegradationHigh expr: rate(cache_degradation_total[5m]) 100 for: 1m labels: severity: critical annotations: summary: 缓存降级率超过100/s持续1分钟 - alert: FullDegradationActive expr: cache_degradation_full_degradation 0 for: 30s labels: severity: critical annotations: summary: 全量降级已激活缓存和源站均不可用七、生产安全检查清单检查项状态说明所有降级location标记internal☐防止外部直接访问error_page使用号重写状态码☐避免暴露自定义码给客户端Lua中ngx.exit在输出前调用☐否则error_page不生效降级超时比正常超时短☐避免降级本身成为瓶颈降级响应有明确标识Header☐X-Cache-Degraded便于调试和监控关键接口禁用降级☐支付/鉴权等不接受脏数据降级日志独立采集☐不影响正常access_log性能降级指标接入告警☐降级是事故不是常态定期演练降级流程☐模拟Redis故障验证链路兜底响应经过测试☐确保JSON格式正确、客户端可解析八、常见踩坑速查表现象根因解决方案error_page未触发Lua已发送header后才set status错误判断前置首次输出前exit客户端收到590状态码error_page缺少号重写改为error_page 590 200 handler降级location被外部访问缺少internal指令添加internal;降级后源站被打垮降级响应未做短时缓存添加proxy_cache 5s降级日志缺失log_format未匹配自定义Header检查map条件和变量名循环降级降级处理器内部又触发相同error_page限制降级深度或使用不同错误码Lua pcall吞掉错误pcall成功但返回nil未检查显式判断返回值并设置ngx.status降级响应格式错误未设置default_type添加default_type application/json监控看不到降级事件指标未在降级处理器中递增在每个handler中添加计数逻辑支付接口返回降级数据未按URI排除降级map配置no_fallback标记九、结语感谢您的阅读如果你有任何疑问或想要分享的经验请在评论区留言交流