33人参与 • 2026-07-31 • 缓存
在《nginx外置缓存》系列前文中,我们构建了以openresty + redis为核心的分布式缓存体系。这套架构在正常情况下表现优异,但生产环境的残酷现实是:外置缓存本身可能成为最大的单点故障。
connection refused,用户看到默认500白页;这些场景的共同点是:缓存层的异常没有被优雅地捕获和转化。默认的nginx错误处理机制对lua层面的异常几乎无感知,而开发者往往只关注“缓存命中时的性能”,忽略了“缓存失效时的生存能力”。
error_page 指令在传统nginx中用于自定义错误页面,但在外置缓存架构中,它的角色远不止于此——它是缓存故障与服务可用性之间的隔离阀,是降级策略的执行入口,是用户体验的最后一道保障。本文将重新定义 error_page 在外置缓存场景下的用法,从基础语法到高级降级模式,给出生产级容错架构。
| 传统角色 | 外置缓存中的新角色 | 说明 |
|---|---|---|
| 自定义错误页面 | 降级触发器 | 将缓存错误转化为可处理的内部跳转 |
| 静态内容展示 | 动态降级路由 | 根据错误类型选择不同的回退路径 |
| 用户友好提示 | 可观测性探针 | 通过不同状态码区分缓存故障类型 |
📌 核心认知:在外置缓存架构中,error_page 不再是“给用户看的页面”,而是“给系统用的控制流”。它拦截的是基础设施层的异常,输出的是架构层的决策。
很多开发者认为在lua代码中用 pcall 包裹redis操作就够了。但这有三个盲区:
error_page 提供了声明式的、跨层级的错误处理机制,与lua的命令式异常处理互补,而非替代。
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}';
}
}nginx标准错误码无法区分“redis挂了”和“源站挂了”。自定义590/591/592让监控和降级策略可以精确识别故障层级:
| 自定义码 | 含义 | 降级动作 | 告警级别 |
|---|---|---|---|
| 590 | redis不可用 | 跳过缓存,直连源站 | p2 |
| 591 | 缓存数据异常 | 返回过期数据或默认值 | p3 |
| 592 | 源站+缓存均失败 | 返回静态兜底响应 | p1 |
⚠️ 注意:自定义错误码仅在nginx内部流转,不会暴露给客户端。最终对外返回的仍是标准http状态码(200/503等)。
lua代码不能直接“调用”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 _mcontent_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)
}一旦调用了 ngx.say() 或 ngx.print(),响应头已发送,ngx.exit 无法改变状态码,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;
所有降级location必须加 internal;,否则攻击者可直接请求 /@cache_fallback_backend 绕过缓存和安全检查。
redis可用 → 返回缓存数据
↓ (590)
源站可用 → 返回实时数据(跳过缓存)
↓ (592)
本地stale可用 → 返回过期数据
↓ (stale miss)
静态兜底 → 返回预设json/html
↓ (兜底失败)
标准503 → 最小化错误响应实现要点:每一级降级处理器内部仍可触发下一级error_page,形成链式容错。
# 某些接口不允许降级(如支付、鉴权)
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
-- ...正常逻辑
}
}
}降级响应本身也可以被短暂缓存,避免每次请求都执行降级逻辑:
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),否则会在源站恢复后仍返回旧数据,造成二次不一致。
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;-- 在降级处理器中递增计数器
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面板应包含:
- 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标记 |
到此这篇关于nginx外置缓存error_page的实现示例的文章就介绍到这了,更多相关nginx外置缓存error_page内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论