Redis 这中间件一挂,上游接口直接超时,缓存穿透全打到数据库上,日志里密密麻麻全是 connection refused。更头疼的是 systemd 里的 redis.service 往往已经处于 failed 状态,直接 systemctl start 可能还会提示 Job failed,甚至遇到 Unit is masked。如果你也被这类问题折磨过,又因为权限、审计或者根文件系统只读等原因不想动 systemd 配置,那这篇文章的内容应该能帮上忙。我会把一套我在生产环境里实测过的“一键恢复 Redis 运行 + Grafana 监控”的脚本逻辑拆开讲,全程不修改 systemd 的 unit 文件,只靠 systemctl 状态复位和命令行启动组合拳,把服务拉回来。
这套方案解决的痛点很明确:Redis 挂了以后,你不仅仅需要把它启动,还要确保监控链路尽快恢复,避免“Redis 已经活了但 Grafana 还在黑洞期”的尴尬。适合谁看?被 Redis 宕机折腾过的运维,写自动化脚本的研发,以及想在边缘设备上用最轻量方式做服务自愈的人。
1. 为什么“免修改 systemd 配置”这个需求会存在
1.1 systemd 服务失败后的那些坑
绝大多数 Linux 发行版现在都默认使用 systemd 管理服务。redis.conf 本身写得没问题,systemd unit 文件也写得没问题,但服务一旦异常退出,systemd 会把这个服务标记为 failed。这个状态很特殊:你在 systemctl status 里能看到红色的一堆日志,但你想 systemctl start,它可能会告诉你 Job for redis.service failed,因为上一次启动失败的状态还没有被清掉。
解决办法不是去编辑/etc/systemd/system/redis.service,而是先执行systemctl reset-failed redis,把“失败计数”清零,然后才能正常启动。很多人不知道这个细节,会误以为是 unit 文件配错了,反复修改配置,结果越改越乱。这其实就是“免修改 systemd 配置”的一个核心场景:不是不能改,而是没必要改,改 unit 文件反而会带来更大的维护成本。
还有一类场景是服务被 masked。mask 和 disable 完全不同,disable 只是不开机自启,mask 是彻底符号链接到 /dev/null,任何 start 操作都会被无情拒绝。在安全加固过的机器上,运维经常会把一些暂时不用的服务 mask 掉,等你真的需要恢复的时候,才发现自己不能随便 unmask——因为 unmask 也算修改 systemd 配置,过不了审计。
再一种情况是只读根文件系统。像嵌入式设备或者容器化部署的节点,/etc/systemd/system是只读挂载,你想改也没法改。这种情况下只能靠 systemctl 的运行时命令,或者在 systemd 之外直接拉起进程。我们设计恢复脚本时,默认约束就是:不修改任何 unit 文件,不改变服务的 enabled/disabled 状态,不用 systemctl edit,不 unmask。能做到这一点的前提下,系统越干净越安全。
1.2 Redis 作为中间件的典型故障场景
Redis 在现在的技术栈里基本是“流量心脏”级别的存在。缓存、分布式锁、排行榜、消息队列轻量实现,全都在上面跑。一旦 Redis 出问题,不只是缓存失效,很多依赖 Redis 的业务逻辑会同步阻塞,数据库连接池瞬间被打满,最终表现为整个服务的雪崩。
最常见的故障原因是 OOM Killer。Redis 本身有 maxmemory 配置,但你架不住物理机内存被其他进程占满,Linux 内核可能会选择 Redis 进程下手,直接 SIGKILL。这种情况下 systemd 会记录失败,但不会自动重启,因为 Redis 的 unit 文件里可能没有 Restart=always。
其次常见的是持久化文件损坏。Redis 默认开启 RDB,数据量大了以后,如果突然断电或者磁盘写入异常,dump.rdb可能损坏,启动时直接Bad file format退出。还有一种情况是 AOF 文件尾部长了半截写坏的数据,Redis 主从同步时也可能触发。这种情况下,光靠 systemctl restart 是没用的,必须对数据文件做处理或者用临时参数跳过恢复。
主从架构里也有坑。哨兵切换之后,旧主节点可能停留在无法同步的状态,如果直接拉起来,它会以 slaveof 角色尝试连接新的主节点,但配置里又残留着旧的 replicaof 信息。你手动改配置?又回到了“改配置”的难题。所以很多时候我们需要一个不依赖配置文件的应急恢复方案。
监控方面,Grafana 用来展示 Redis 运行状态是很常规的操作。通过 Prometheus 加 redis_exporter,把 Redis 的命中率、内存碎片率、连接数、key 数量全部可视化。但 Grafana 这个服务如果也跟着挂了,整个监控链路的“最后一公里”就断了。Redis 恢复后,如果 Grafana 没人去检查,你可能要在黑屏环境里盯命令行,那体验非常差。所以一键恢复脚本必须把 Redis 和 Grafana 的拉起放在同一个流程里,缺一不可。
2. 一键恢复脚本的核心设计
2.1 先想清楚恢复顺序
写脚本之前,最关键的是确定执行顺序。我的逻辑是:先恢复 Redis,再恢复监控,最后做验证。顺序不能反。如果先把 Grafana 拉起来,Monitoring 里没有数据,只会显示一堆 No Data,看起来像故障没解决,干扰判断。
Redis 恢复本身也有讲究。不能一上来就systemctl start redis,应该先探测当前状态:
- 用
systemctl is-active redis看 ActiveState。 - 如果是 active,说明服务还活着,只是 Redis 无响应?那就不能贸然 restart,先尝试
redis-cli ping。 - 如果是 failed,必须先
systemctl reset-failed redis。 - 如果服务被 mask,脚本里不应该尝试 unmask,而是用直接启动进程的 fallback 逻辑。
- 启动成功后再用 redis-cli ping 确认,甚至可以进一步 SELECT 0 验证权限。
Grafana 这边同理,先检查 active 状态,再做 HTTP 健康检查,访问/api/health。如果 API 返回database: ok,说明 Grafana 可用。这样设计,脚本就具备幂等性:你连续执行十次,不会产生副作用,也不会把已经正常的服务反复重启。
2.2 用一个函数集中处理服务拉起
我不喜欢在脚本里写一堆重复的 systemctl 代码。把拉起逻辑封装成一个函数,接受服务名和健康检查命令作为参数,这样 Redis 和 Grafana 可以复用同一套逻辑。
函数内部的大致流程是:
- 如果服务已经是 active 状态,直接做健康检查,通过就返回 0。
- 如果服务是 unknown 状态,说明 systemd 里可能没有这个 unit,那就走 fallback 直接用二进制启动。
- 如果是 failed,执行
systemctl reset-failed,再执行systemctl start。 - 启动后循环等待端口,超时后给出明确报错。
这个函数会让整个脚本的可维护性高很多。后续你想加第三个服务,比如 Prometheus,只需加一行调用。
2.3 等 Redis 真正活了再操作监控
这里有一个经验教训:不要用固定 sleep。很多人写恢复脚本喜欢sleep 3然后觉得服务起来了,结果 Redis 数据量大,从 RDB 文件恢复需要十几秒,3 秒时端口还没监听,后面所有操作全失败。
正确做法是做一个等待循环。我通常用 30 次循环,每次间隔 1 秒,配合redis-cli -p 6379 ping检查。如果返回 PONG,才认为 Redis 是真活了。这样可以掩盖慢启动的问题,也避免无限等待。
如果你有多个 Redis 实例,端口不同,建议在脚本里用数组维护实例列表,循环去探测。标题是“一键恢复”,但实际生产里一台机器上可能跑着 6379/6380/6381 三个实例,脚本要能处理这种多实例情况。
2.4 Grafana 监控恢复的两种姿势
Grafana 恢复,有两条路可以走。最简单的姿势是只把服务拉起来,数据源和仪表盘交给 provisioning 机制自动加载。Grafana 支持在/etc/grafana/provisioning/datasources目录下放 YAML 文件,启动时自动配置数据源;dashboard 可以放在/etc/grafana/provisioning/dashboards目录。这种方式的优点是不依赖脚本去调用 API,只要文件在,Grafana 一启动就有监控看板。
第二种姿势是脚本里直接调 Grafana HTTP API。通过 POST 请求创建数据源、导入 dashboard JSON。这种方式适合那些没有配 provisioning 的存量环境。脚本需要准备好 Grafana 的 API Token,或者用 admin 账号密码登录换取 cookie,操作起来稍重。
我个人推荐优先走 provisioning,如果必须用 API,要在脚本里保留可配置的凭据环境变量,不要把密码硬编码在脚本里。毕竟这种恢复脚本往往会放到 cron 里自动执行,硬编码密码等于给服务器埋雷。
2.5 兼容不同启动方式
systemd 是主流,但不是所有环境都有 systemd。容器里可能用 s6、supervisord,甚至裸启动进程。既然是“一键恢复”,脚本必须判断当前用什么进程管理方式。
我常用的判断条件是:
- 先看
/run/systemd/system目录是否存在,以此判断有没有 systemd。 - 再看
/etc/init.d/redis-server和/etc/init.d/grafana-server是否存在,兼容 SysV init。 - 如果都没有,直接找二进制路径:
redis-server、grafana-server,用nohup或daemon方式拉起。
这种兼容层并不复杂,但能让脚本从 Ubuntu 18.04 到 CentOS 7 到 Debian 容器都能跑。注意一点:同一个服务在不同发行版上的 unit 名称可能不一样,比如redis.service和redis-server.service,所以脚本里要有一个服务名数组,逐一尝试。
3. 实操:从零写一个可复现的恢复脚本
3.1 准备工作:确认服务名、端口、数据目录
在写脚本之前,我建议先手工确认几个信息。不是所有环境的服务名都叫 redis,用下面这条命令看:
systemctl list-unit-files | grep -E 'redis|grafana'常见的组合是:
| 服务 | Ubuntu/Debian 单元名 | CentOS/RHEL 单元名 |
|---|---|---|
| Redis | redis-server.service | redis.service |
| Grafana | grafana-server.service | grafana-server.service |
默认端口也要确认。Redis 一般是 6379,Grafana 是 3000。如果机器上装了多套中间件,可能端口被改过。查监听端口用ss -lntp:
ss -lntp | grep -E '6379|3000'数据目录方面,Redis 的数据目录通常是/var/lib/redis,AOF 和 RDB 都在里面。Grafana 的数据库文件在/var/lib/grafana,如果你用的是 SQLite,损坏风险比 MySQL 低一些,但也不能完全忽略。
3.2 脚本主体代码解释
下面这个脚本是我实际用过的精简版,去掉了环境相关的硬编码,改成变量配置。如果你的服务名不同,直接改顶部的变量即可。
#!/usr/bin/env bash set -Eeuo pipefail # ---------- 可配置区 ---------- REDIS_SVC_NAMES=("redis-server" "redis") REDIS_PORT=6379 GRAFANA_SVC_NAMES=("grafana-server" "grafana") GRAFANA_PORT=3000 REDIS_BIN="/usr/bin/redis-server" GRAFANA_BIN="/usr/sbin/grafana-server" GRAFANA_YML="/etc/grafana/grafana.ini" LOG_FILE="/var/log/recover-redis-grafana.log" # ---------- 可配置区结束 ---------- log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG_FILE" } health_check() { local name="$1" local port="$2" local cmd="$3" for i in $(seq 1 30); do if $cmd >/dev/null 2>&1; then log "健康检查通过: $name" return 0 fi sleep 1 done log "健康检查超时: $name" return 1 } start_systemd_service() { local svc="$1" if systemctl is-active --quiet "$svc"; then log "服务 $svc 已经是 active 状态,跳过启动" return 0 fi # 如果服务处于 failed,必须先 reset-failed if systemctl is-failed --quiet "$svc"; then log "服务 $svc 处于 failed,执行 reset-failed" systemctl reset-failed "$svc" fi log "启动 systemd 服务 $svc" systemctl start "$svc" } start_fallback_process() { local bin="$1" local cfg="$2" local svc_label="$3" if pgrep -f "$bin" >/dev/null 2>&1; then log "进程 $svc_label 已存在,跳过 fallback 启动" return 0 fi log "使用二进制直接启动 $svc_label" if [ -n "$cfg" ] && [ -f "$cfg" ]; then nohup "$bin" "$cfg" >>"$LOG_FILE" 2>&1 & else nohup "$bin" >>"$LOG_FILE" 2>&1 & fi disown } ensure_service() { local label="$1" local svc_names=("${!2}") local bin="$3" local cfg="$4" local port="$5" local health_cmd="$6" # 优先 systemd if [ -d "/run/systemd/system" ]; then local found=0 for svc in "${svc_names[@]}"; do if systemctl list-unit-files --type=service | grep -qw "${svc}.service"; then start_systemd_service "$svc" found=1 break fi done if [ "$found" -eq 0 ]; then log "未找到 systemd 服务单元,尝试 fallback 启动 $label" start_fallback_process "$bin" "$cfg" "$label" fi else # 没有 systemd,直接 fallback start_fallback_process "$bin" "$cfg" "$label" fi health_check "$label" "$port" "$health_cmd" } # ---------- 主流程 ---------- mkdir -p "$(dirname "$LOG_FILE")" # 1. 恢复 Redis ensure_service "Redis" \ REDIS_SVC_NAMES[@] \ "$REDIS_BIN" \ "/etc/redis/redis.conf" \ "$REDIS_PORT" \ "redis-cli -p $REDIS_PORT ping | grep -q PONG" # 2. 恢复 Grafana ensure_service "Grafana" \ GRAFANA_SVC_NAMES[@] \ "$GRAFANA_BIN" \ "$GRAFANA_YML" \ "$GRAFANA_PORT" \ "curl -fsS http://127.0.0.1:$GRAFANA_PORT/api/health | grep -q 'database.*ok'" # 3. 附加验证:输出摘要 redis_status=$(redis-cli -p "$REDIS_PORT" ping 2>/dev/null || echo "REDIS_DOWN") grafana_code=$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:$GRAFANA_PORT/api/health" 2>/dev/null || echo "000") log "========== 恢复结果 ==========" log "Redis: $redis_status" log "Grafana HTTP 状态码: $grafana_code" if [ "$redis_status" == "PONG" ] && [ "$grafana_code" == "200" ]; then log "全部恢复成功" exit 0 else log "部分服务未恢复正常,请排查日志" exit 1 fi脚本里最值得说的是ensure_service函数。它把 systemd 启动和 fallback 启动封装成同一个入口,调用方只需要告诉它服务名数组、二进制路径、配置文件路径、端口和健康检查命令。
我特意用了set -Eeuo pipefail。-e表示脚本中任何命令失败立刻退出,-u防止未定义变量,-o pipefail让管道命令返回最后一个失败命令的状态。恢复脚本一定要用这些严格模式,否则某个命令失败后脚本还在继续跑,容易出现“自以为成功了”的误判。
还有一点,systemctl list-unit-files | grep -qw判断服务是否存在,这个操作很轻量。有些环境里服务单元文件存在但被 mask,is-active会是 inactive,直接systemctl start会报错。如果需要完全避免修改 systemd 配置,就不能 unmask。此时脚本走不到 start 成功,会跳到 fallback 启动进程吗?我的设计里不会,因为找到 unit 文件后就直接尝试 start,而 start 失败会被-e退出。这里要做一个容错。
更好的做法是在 start_systemd_service 里先检测 masked 状态。如果systemctl is-enabled返回 masked,就把这个服务名标记为不可用,跳过它,继续尝试下一个服务名,然后走 fallback。下面是改进后的代码片段:
start_systemd_service() { local svc="$1" if systemctl is-active --quiet "$svc"; then log "服务 $svc 已经是 active 状态,跳过启动" return 0 fi # masked 状态下 start 会失败,这里直接把返回码置为 1 并跳过 if systemctl is-enabled --quiet "$svc" 2>/dev/null; then if systemctl show -p LoadState --value "$svc" | grep -q masked; then log "服务 $svc 被 masked,跳过 systemd 启动" return 1 fi fi systemctl reset-failed "$svc" 2>/dev/null || true systemctl start "$svc" }这样一旦返回 1,ensure_service 里的 found 循环就会继续找下一个服务名。找不到可用的 systemd 服务,最后再 fallback。这个逻辑完全没动 systemd 配置,只是查询状态,符合标题的约束。
3.3 设置一键入口
脚本文件保存好之后,给它执行权限,放到常规路径下:
chmod +x /usr/local/bin/recover-redis-grafana /usr/local/bin/recover-redis-grafana如果你不想每次敲全路径,可以加一个 shell alias:
alias fix-redis='/usr/local/bin/recover-redis-grafana'还有人会问:能不能做成开机自启或者周期自愈?当然能,但为了坚持“不改 systemd 配置”的原则,我建议用 crontab 而不是 systemd timer。比如每分钟跑一次,没病的时候脚本秒退,有病的时候自动拉起来:
* * * * * flock -n /tmp/recover-redis-grafana.lock /usr/local/bin/recover-redis-grafanaflock -n是必须的,避免上一次还没执行完,下一次又启动了一个新进程,两个脚本同时在拉服务,产生竞争。这个 cron 配置也不属于 systemd 配置,完全符合需求。
3.4 实测输出示例
下面是我在 Ubuntu 22.04 上故意把 Redis 和 Grafana 停掉后运行脚本的输出:
2025-01-20 10:15:01 服务 redis-server 处于 failed,执行 reset-failed 2025-01-20 10:15:02 启动 systemd 服务 redis-server 2025-01-20 10:15:03 健康检查通过: Redis 2025-01-20 10:15:03 服务 grafana-server 处于 failed,执行 reset-failed 2025-01-20 10:15:04 启动 systemd 服务 grafana-server 2025-01-20 10:15:06 健康检查通过: Grafana 2025-01-20 10:15:06 ========== 恢复结果 ========== 2025-01-20 10:15:06 Redis: PONG 2025-01-20 10:15:06 Grafana HTTP 状态码: 200 2025-01-20 10:15:06 全部恢复成功整个执行过程不到 10 秒。关键点是 Redis 的 reset-failed 步骤,如果没有这一步,后面systemctl start大概率会失败。从输出里也能看出,脚本等到了 Redis 真正 PONG 才开始拉 Grafana,避免监控面板出现误报。
恢复正常后,建议顺手验证一下 Grafana 里的 Redis Dashboard 是否有数据。如果你没配置 Prometheus 和 redis_exporter,那我额外提醒一句:Grafana 本身不会直接采集 Redis 指标,它需要一个数据源。下面的常见问题里我会展开讲。
4. 常见问题与排查技巧实录
4.1 systemctl start 报 Unit redis.service is masked
这是最棘手的一个问题。masked 状态意味着 unit 文件被符号链接到了 /dev/null,systemd 层面不接受任何启动操作。标题要求免修改 systemd 配置,所以我们不能简单地systemctl unmask。我建议分两步处理:
- 检查你是不是真的需要这个服务被 systemd 管理。如果只是应急恢复,可以忽略 systemd,直接用
/usr/bin/redis-server /etc/redis/redis.conf把进程拉起来。进程能跑,业务能通,系统管理状态暂时难看一点没关系。 - 等业务窗口再决定要不要找系统管理员调整 mask 状态。注意直接启动的 Redis 进程不受 systemd 的 Restart 策略保护,进程一旦退出不会自动拉起,所以这种方式只适合应急,不能作为长期方案。
脚本里的 fallback 逻辑正好覆盖了这个场景。你会发现整个过程完全没有修改 systemd 状态,只是绕过了它。生产环境如果有安全合规要求,这种“绕过”比“改配置”更安全,因为你没有在系统上留下任何变更痕迹。
4.2 端口被占用,systemctl start 失败
有时候 Redis 进程并没有彻底死掉,而是变成了僵尸状态,或者有一个旧的实例还占着 6379。此时 systemctl start 会因为端口冲突而失败。我的排查思路是:
ss -lntp | grep 6379如果发现有一个 redis-server 进程还在监听,先别急着 kill -9。Redis 里有持久化数据,强杀可能导致 RDB 保存不完整。正确姿势是用redis-cli shutdown nosave优雅关闭,如果这个命令都连不上,再看进程的启动参数和日志,确认是不是卡死。
处理完端口冲突后,重新跑一键恢复脚本。脚本本身就带失败重试,等端口释放后再拉起。
4.3 Redis 启动时报 Bad file format 或 RDB 文件损坏
这种情况经常被误判为“服务故障”,其实是数据文件坏了。如果你确认 RDB 文件无法加载,可以用 Redis 自带的redis-check-rdb工具检查:
redis-check-rdb /var/lib/redis/dump.rdb如果是 AOF 文件尾部损坏,优先尝试:
redis-check-aof /var/lib/redis/appendonly.aof修复后,可以临时加入启动参数--aof-use-rdb-preamble yes或者直接用--appendonly no先绕过 AOF 加载。注意这些动作要谨慎,如果 Redis 里是分布式锁数据,丢数据会影响业务一致性。我的经验是,先备份损坏文件,再用工具修复,修复不了就尝试用副本的数据文件顶上。脚本本身不负责修复数据文件,但你应该在预案里考虑这个步骤。
4.4 Grafana 起来了,但面板里看不到 Redis 数据
Grafana 服务恢复不代表监控有数据。Grafana 只是一个可视化层,它需要从 Prometheus、InfluxDB、CloudWatch 等数据源读取 Redis 指标。最常见的是下面这条链路:
Redis -> redis_exporter -> Prometheus -> Grafana如果你的 Prometheus 没启动,Grafana 面板就会显示 No Data。所以一键恢复脚本不能只检查 Redis 和 Grafana,还应该顺带确认 Prometheus 和 redis_exporter 这两个进程的状态。你可以把 Prometheus 也塞进脚本里,当作第三个服务来拉。虽然标题没提 Prometheus,但实际监控链路里它是核心。如果你还没有配置 redis_exporter,可以补一个,它启动命令很简单:
redis_exporter --redis.addr=redis://127.0.0.1:6379然后在 Prometheus 的scrape_configs里加上对应的 job。这是另一块内容,但建议你在恢复脚本通过之后,顺手把这些依赖项也检查一遍。
4.5 恢复脚本本身失败,或者执行到一半退出
脚本里我用了set -e,任何一条命令失败都会退出。这时候要看输出到哪一步断了。最常见的两个原因:
- 健康检查命令中的
redis-cli ping | grep -q PONG在PONG不存在时返回非 0,导致脚本退出,但这其实是 Redis 还没起来。要看它前面有没有报错。 curl -fsS在 Grafana 健康检查返回非 200 时也会触发退出。如果 Grafana 启动慢,健康检查超时后脚本退出,服务可能还在启动中。
解决方式是把健康检查的超时时间调大,或者把set -e的影响范围缩小。我实际写脚本时,会在大流程里暂时关闭-e:
set +e ensure_service ... set -e这样某个服务失败不会直接终止整个脚本,日志还能继续输出,方便排查。你执行完脚本之后,如果退出码不是 0,也可以直接看日志文件tail -n 100 /var/log/recover-redis-grafana.log,每一步都有记录。
4.6 定时自愈的坑:别让脚本重复拉起服务
如果你按我前面说的用 crontab 每 1 分钟跑一次脚本,有一个坑必须提前埋好:脚本本身要幂等,而且要有锁。幂等的意思就是,服务已经是 active 时,脚本应该直接通过健康检查,不做多余操作。锁文件用于防止两个脚本实例同时执行 systemctl start。
flock在大多数发行版上都有,用法也简单:
* * * * * flock -n /tmp/recover-redis-grafana.lock /usr/local/bin/recover-redis-grafana如果脚本执行超过 1 分钟,新的进程会因为拿不到锁而直接退出,不影响正在跑的实例。日志文件建议交给 logrotate 管理,避免恢复脚本长时间运行把日志撑爆。
我个人的实际体会是,这种“免修改 systemd 配置”的做法,真正解决的是变更管控问题。很多团队里 /etc/systemd/system 下的文件是走发布流程的,不能随意手工修改。而你用 systemctl reset-failed 和直接启动进程的方式,相当于把恢复动作收敛在应用层,系统基线不用动,出问题后回滚也容易。哪怕是出了问题,审计日志里也只有命令执行记录,没有文件变更记录,这对合规来说是一个优势。
最后再分享一个小技巧:在 crontab 里跑恢复脚本时,记得加上PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin,因为 cron 环境里 PATH 很精简,你脚本里所有依赖的命令都要能找到。我踩过一次坑,脚本本地手动执行一切正常,一放到 cron 里就跑不起来,就是因为systemctl不在 PATH 里。加上这一行之后,定时自愈才真正可靠。