2026-03 需求03线上故障复盘与发布经验(Redis误切只读导致50001)

一、事件概述

  • 事件类型:线上可用性故障(认证签发链路失败)

  • 影响接口POST /api/v1/auth/exchange

  • 表象错误:后端返回 50001 issue_token_failed,随后业务接口连带出现 40105

  • 核心根因:Redis 运行时被切换为从库且只读,认证签发流程写会话失败

  • 处理状态:已止血、已修复代码可观测性、已完成热修复包


二、现象与影响范围

1)业务现象

  • 客户端登录交换令牌失败:/api/v1/auth/exchange 返回 50001

  • 由于未拿到有效 access token,后续受保护接口(如 GET /api/v1/couple/relationPOST /api/v1/couple/invites)出现 40105

  • 客户端即使传了 huawei_credential,仍会失败(因为失败点在凭证校验之后)

2)影响范围

  • 所有走 auth/exchange 的用户均受影响

  • 功能上表现为“无法登录/无法获取新会话”


三、关键证据链(本次定位的决定性信息)

1)应用访问日志

  • 同一来源 IP 多次触发:POST /api/v1/auth/exchange -> error_code=50001

  • 同时段紧随其后出现 40105(令牌缺失或失效导致)

2)Redis 复制状态(容器内)

role:slave
master_host:47.236.128.72
master_port:60149
slave_read_only:1

3)Redis 配置查询

CONFIG GET replica-read-only -> yes
CONFIG GET replicaof -> 47.236.128.72 60149

4)Redis 运行日志

大量出现:

MASTER <-> REPLICA sync started

Timeout connecting to the MASTER...

Reconnecting to MASTER 47.236.128.72:60149 after failure

结论:Redis 已被配置为副本并持续尝试连接外部 master,处于只读态,写入会话必然失败。


四、根因分析(Why)

1)直接根因

认证签发流程中,后端会先写 Redis 会话再签发 token:

  1. 生成 session_id + refresh_token 原始值

  1. 写入 Redis 会话键:auth:session:{session_id}

  1. 再签发 access token

当 Redis 处于只读副本时,第 2 步失败,从而返回 50001(历史版本日志中被笼统归类为 issue_token_failed)。

2)深层原因

  • Redis 运行时存在 replicaof 指向外部地址 47.236.128.72:60149

  • 导致实例切为 role:slavereplica-read-only=yes

  • 由于服务日志粒度不足(仅输出笼统 reason),定位耗时增加

3)为什么“之前正常,后来突然异常”

  • 这类问题通常由运行期命令/外部操作触发,而非应用代码逻辑变更本身

  • 若配置文件无 replicaof,而运行态出现 replicaof,可判定为运行时被改写(误操作/脚本/外部访问)


五、现场处置(How)

1)止血动作

docker exec -it <redis_container> redis-cli REPLICAOF NO ONE

2)止血验证

docker exec -it <redis_container> redis-cli INFO replication
# 期望:role:master

docker exec -it <redis_container> redis-cli SET __auth_probe 1 EX 60
# 期望:OK

3)恢复后状态

  • Redis 回到 role:master

  • 写入探针成功

  • 认证链路恢复可用


六、本次代码修复(已完成)

1)错误可观测性增强(auth/exchange)

  • 旧行为:统一 50001 issue_token_failed,难定位新行为

  • Redis 会话存储不可用 -> 50002,消息包含 session store unavailable: <raw_error>

  • 其他签发异常 -> 50001,消息包含 issue token failed: <raw_error>

  • 审计日志新增 raw_error 字段,保留系统原始错误

2)生产配置安全校验增强

  • 新增规则:prod 环境下 redis.password 不能为空,否则启动失败

3)生产配置更新

  • prod Redis 密码已改为目标值(后续建议再次轮换并走密钥管理)


七、发布产物与校验信息

  • 发布包:backend/dist/lover-api-linux-amd64-20260309-hotfix.tar.gz

  • SHA256:fca8b75e0cf44d7727f686e67b07a75746d23d62c444c9219fcae5219c2b50d4

  • 包内包含:

lover-api

configs/dev.yaml

configs/prod.yaml

migrations/0001~0005


八、systemd 配置经验(本次踩坑重点)

1)现象

  • 已写入 Environment="REDIS_PASSWORD=...",但进程里仍为空

2)根因

  • 同名环境变量在多个 drop-in 中重复定义,后者覆盖前者

  • 实际出现:一处为 REDIS_PASSWORD=password_redis@!#另一处为 REDIS_PASSWORD=

  • 最终进程环境为 REDIS_PASSWORD=

3)规则

  • 同名变量只保留一处定义,避免冲突

  • /proc/<PID>/environ 校验运行时结果,不依赖 status 页面展示


九、防复发清单(必须执行)

A. Redis 侧

  1. 配置强密码/ACL

  1. 禁止公网直连 6379(仅白名单内网访问)

  1. redis.conf 移除 replicaof/slaveof(若非主从架构)

  1. 考虑禁用危险命令:CONFIGREPLICAOFSLAVEOF(按运维策略)

  1. 重启后复检:INFO replicationrole:master
    CONFIG GET replicaof 为空

B. 应用侧

  1. 认证失败必须携带可定位错误(禁止笼统 reason)

  1. 增加告警:/auth/exchange50001/50002 异常率

  1. 增加 Redis 可写性巡检(探针写入 + 告警)

C. 发布侧

  1. 发布前校验二进制格式(ELF)

  1. 发布后固定冒烟:healthauth/exchange、关键业务链路

  1. 记录 request_id 对应审计与系统日志,形成可追踪证据


十、建议的长期优化

  1. 将 Redis/PG/Auth 等密钥迁移到专用密钥管理,不在会话中明文传递

  1. 在应用启动阶段增加“Redis 可写性自检”,失败则拒绝启动

  1. 将认证错误码进一步标准化:明确区分依赖故障、签名故障、输入故障

  1. 将此次复盘纳入发布 checklist 和 on-call SOP