系统挂了不可怕,可怕的是没人知道怎么把它救回来

举报
Echo_Wish 发表于 2026/09/16 13:49:06 2026/09/16
【摘要】 系统挂了不可怕,可怕的是没人知道怎么把它救回来

系统挂了不可怕,可怕的是没人知道怎么把它救回来

凌晨两点,监控突然开始疯狂报警。

CPU 100%、接口 502、数据库连接池耗尽、消息堆积……值班同学第一反应往往是:先把服务重启了再说。

重启之后,指标真的恢复了。

于是大家松了一口气。

但问题来了:为什么它会挂?为什么重启能恢复?下次还会不会挂?如果数据库也挂了呢?如果只有一台机器异常呢?

这其实就是一个很容易被忽略的问题:

系统不仅要“能运行”,还得“出了问题自己能恢复,而且尽量别把故障扩散出去”。

这就是今天想聊的——可恢复性工程(Recoverability Engineering)


一、真正成熟的系统,不是“永不故障”

我一直觉得,很多系统设计存在一个误区:

“我们要保证系统不能出故障。”

这句话听起来很正确,但现实里基本做不到。

机器会坏。

网络会抖。

数据库会慢。

磁盘会满。

第三方接口会超时。

甚至一行代码写错,都可能让整个服务直接趴下。

所以真正靠谱的目标应该换一下:

不是让系统永远不出故障,而是让系统出了故障之后,能够快速识别、快速隔离、自动恢复。

这三个词非常重要:

识别 → 隔离 → 恢复

如果一个系统只能报警:

数据库挂了
↓
监控报警
↓
运维收到短信
↓
登录服务器
↓
查看日志
↓
判断问题
↓
执行命令
↓
重启服务
↓
观察指标

这叫“有人会救”。

而如果能够做到:

异常
 ↓
自动检测
 ↓
判断故障类型
 ↓
隔离异常实例
 ↓
自动恢复
 ↓
验证恢复结果
 ↓
失败则升级人工

这才开始接近真正的可恢复系统


二、自动重启,不等于自动恢复

很多公司都有这样的脚本:

if ! curl -f http://127.0.0.1:8080/health; then
    systemctl restart my-service
fi

简单、粗暴,而且确实有用。

但我要泼一盆冷水:

这只是自动重启,不是真正意义上的自动恢复。

因为它只回答了一个问题:

服务挂了,要不要重启?

但真正的故障恢复至少应该回答五个问题:

  1. 发生了什么?
  2. 影响范围多大?
  3. 应该隔离谁?
  4. 应该恢复什么?
  5. 恢复之后真的正常了吗?

比如一个服务有 10 台机器:

Server01  正常
Server02  正常
Server03  正常
Server04  正常
Server05  CPU 100%
Server06  正常
Server07  正常
Server08  正常
Server09  正常
Server10  正常

这时候最蠢的操作是什么?

把整个服务重启。

因为真正有问题的是 Server05。

正确的方式应该是:

Server05 异常
     ↓
从负载均衡摘除
     ↓
停止接收新请求
     ↓
尝试恢复
     ↓
健康检查
     ↓
恢复成功
     ↓
重新加入集群

如果恢复失败:

Server05
   ↓
恢复失败
   ↓
保持隔离
   ↓
告警
   ↓
人工处理

这就是一个非常典型的故障隔离 + 自动恢复模式


三、第一个设计模式:先隔离,再恢复

这是我特别推荐的一条原则:

不要一边让故障节点继续接流量,一边尝试修复它。

想象一下:

用户请求
    ↓
Load Balancer
 ↓    ↓    ↓
A     B     C
      ↑
    已故障

如果 B 已经出现:

连接池耗尽
线程池耗尽
CPU 100%
内存异常
响应时间 20 秒

你还继续把请求发给 B,那就是:

一台机器生病,拉着所有用户一起陪葬。

所以第一步应该是:

def handle_instance_failure(instance):
    remove_from_load_balancer(instance)
    stop_new_requests(instance)

    if recover(instance):
        health_check(instance)

        if is_healthy(instance):
            add_to_load_balancer(instance)
    else:
        alert_operator(instance)

这里最关键的不是 recover()

而是:

remove_from_load_balancer(instance)

隔离永远应该发生在恢复之前。


四、为什么“故障隔离”比“自动恢复”还重要?

因为生产环境里最危险的故障,往往不是单点故障。

而是:

一个小故障逐渐演变成全局故障。

比如:

一个实例响应变慢
       ↓
请求大量堆积
       ↓
线程池耗尽
       ↓
调用方超时
       ↓
调用方开始重试
       ↓
请求数量继续增加
       ↓
数据库连接数暴涨
       ↓
数据库变慢
       ↓
更多服务超时
       ↓
全链路雪崩

最开始可能只是:

一台服务器响应慢。

最后却变成:

整个系统不可用。

所以故障隔离的本质就是:

控制爆炸半径。

我很喜欢这个词。

不是要求系统完全没有爆炸,而是:

炸一台,别把十台都炸了。


五、第二个设计模式:熔断器

微服务里非常经典的设计就是 Circuit Breaker,也就是熔断器。

假设服务 A 调用服务 B:

A → B

正常情况下没问题。

但是 B 挂了:

A → B
    ×

如果 A 还不断请求:

A → B ×
A → B ×
A → B ×
A → B ×
A → B ×
...

很快 A 自己也会被拖死。

所以可以设计成:

正常
 ↓
失败率升高
 ↓
熔断
 ↓
停止调用
 ↓
等待恢复
 ↓
半开
 ↓
尝试少量请求
 ↓
成功 → 关闭熔断
失败 → 继续熔断

一个简单 Python 示例:

import time


class CircuitBreaker:
    def __init__(self, threshold=5, recovery_time=30):
        self.threshold = threshold
        self.recovery_time = recovery_time
        self.fail_count = 0
        self.opened_at = None

    def allow_request(self):
        if self.opened_at is None:
            return True

        if time.time() - self.opened_at > self.recovery_time:
            return True

        return False

    def record_success(self):
        self.fail_count = 0
        self.opened_at = None

    def record_failure(self):
        self.fail_count += 1

        if self.fail_count >= self.threshold:
            self.opened_at = time.time()

真正生产环境当然不会这么简单。

通常还会考虑:

  • 时间窗口
  • 错误率
  • 慢请求比例
  • 半开状态
  • 并发限制
  • fallback
  • 最大恢复次数

但思想其实非常简单:

别让一个坏掉的依赖把自己一起拖死。


六、第三个设计模式:Bulkhead——舱壁隔离

这个设计模式特别值得运维人员理解。

Bulkhead 原本是船舶上的“舱壁”。

一艘船进水了,如果所有舱室连在一起:

进水
↓↓↓↓↓↓
整个船

船很快就沉了。

如果有多个独立舱室:

┌────┬────┬────┬────┐
│ A  │ B  │ C  │ D  │
│正常│进水│正常│正常│
└────┴────┴────┴────┘

B 舱进水:

只牺牲 B。

系统架构也是一样。

比如一个电商系统:

商品服务
订单服务
支付服务
推荐服务
消息服务

千万不要让所有服务:

共享一个连接池
共享一个线程池
共享一个资源池

否则推荐服务出了问题:

推荐服务疯狂请求
        ↓
占满线程
        ↓
订单服务拿不到线程
        ↓
支付服务也受影响

这就叫资源互相污染

可以改成:

订单线程池:100
支付线程池:50
推荐线程池:30
消息线程池:30

推荐服务炸了:

推荐线程池:100%
订单线程池:正常
支付线程池:正常

这就是舱壁隔离。

资源隔离,本质上也是故障隔离。


七、第四个设计模式:重试一定要有“刹车”

运维里还有一个非常经典的坑:

自动重试。

程序员经常这样写:

for i in range(10):
    try:
        call_api()
        break
    except Exception:
        pass

看起来挺稳。

但如果所有服务都这么干:

1000个请求
 ↓
第一次失败
 ↓
1000个重试
 ↓
继续失败
 ↓
1000个再次重试
 ↓
继续失败

原本 1000 个请求:

最后可能变成:

10000 个请求

这不是恢复。

这是:

拿重试把故障系统再捅几刀。

所以重试至少应该具备:

1. 次数限制

MAX_RETRIES = 3

2. 指数退避

delay = 2 ** retry_count

例如:

第1次:1秒
第2次:2秒
第3次:4秒

3. 随机抖动

避免所有客户端同时重试:

import random

delay = 2 ** retry_count + random.random()

4. 只重试“值得重试”的错误

例如:

网络超时       → 可以重试
连接断开       → 可以重试
HTTP 503       → 可以重试

参数错误       → 不要重试
权限错误       → 不要重试
业务校验失败   → 不要重试

一句话:

重试不是越多越好,重试必须有边界。


八、第五个设计模式:自动恢复必须“验证”

这个问题非常容易被忽略。

很多自动化脚本是这样的:

发现服务挂了
 ↓
systemctl restart
 ↓
脚本结束

然后监控:

服务进程:Running

于是认为:

恢复成功。

实际上:

进程活着 ≠ 服务正常

比如:

进程:Running
HTTP:500
数据库:连接失败
Redis:连接失败
MQ:连接失败
业务接口:超时

所以恢复之后一定要做恢复验证

例如:

def verify_service():
    checks = [
        check_process(),
        check_http(),
        check_database(),
        check_redis(),
        check_business_api()
    ]

    return all(checks)

甚至可以设计成分级健康检查:

L1:进程是否存在
L2:端口是否正常
L3:HTTP是否正常
L4:依赖是否正常
L5:核心业务是否正常

真正的恢复应该是:

重启
 ↓
L1
 ↓
L2
 ↓
L3
 ↓
L4
 ↓
L5
 ↓
重新接入流量

而不是:

重启
 ↓
Process Running
 ↓
宣布成功

九、自动恢复还有一个大坑:无限自愈

“自动恢复”听起来非常高级。

但如果设计不好,会出现一种很搞笑的情况:

服务启动
 ↓
5分钟后挂
 ↓
自动重启
 ↓
5分钟后挂
 ↓
自动重启
 ↓
5分钟后挂
 ↓
自动重启

然后:

监控:正常
监控:正常
监控:正常

运维人员甚至不知道系统已经处于“半死不活”状态。

所以自动恢复必须设置:

恢复预算。

例如:

MAX_RECOVERY_COUNT = 3

if recovery_count >= MAX_RECOVERY_COUNT:
    disable_auto_recovery()
    alert("自动恢复已达到上限")

更进一步,可以设置:

5分钟内重启 ≥ 3次
        ↓
认为系统持续异常
        ↓
停止自动重启
        ↓
隔离实例
        ↓
升级人工告警

这非常重要。

自动化不是让机器永远自己折腾,而是让机器知道什么时候应该把问题交给人。


十、真正成熟的恢复流程应该长这样

如果让我设计一套生产系统的自动恢复流程,我会更倾向于:

              ┌──────────────┐
              │   监控发现异常 │
              └──────┬───────┘
                     ↓
              ┌──────────────┐
              │   判断故障类型 │
              └──────┬───────┘
                     ↓
              ┌──────────────┐
              │    故障隔离    │
              └──────┬───────┘
                     ↓
              ┌──────────────┐
              │   自动恢复     │
              └──────┬───────┘
                     ↓
              ┌──────────────┐
              │    健康检查    │
              └──────┬───────┘
                成功 ↓       ↓ 失败
             ┌──────────┐   ┌──────────┐
             │ 恢复流量 │   │ 再次恢复 │
             └──────────┘   └────┬─────┘
                                  ↓
                           超过恢复阈值
                                  ↓
                           ┌──────────┐
                           │ 人工介入 │
                           └──────────┘

注意这里有一个非常重要的思想:

自动恢复不是一个动作,而是一套闭环。


十一、再往前一步:让恢复动作本身也可以测试

这其实是我特别想强调的一点。

很多公司有:

故障恢复脚本

但是从来没真正测试过。

这就像消防系统:

灭火器挂墙上十年,第一次使用的时候才发现没压力。

所以可恢复性工程必须加入故障演练

例如人为制造:

Kill Pod
↓
观察是否摘流
↓
观察是否自动拉起
↓
观察健康检查
↓
观察是否重新加入

或者:

断开 Redis
↓
观察应用
↓
是否熔断
↓
是否降级
↓
Redis 恢复
↓
是否自动恢复

甚至可以用 Python 写一个非常简单的故障注入脚本:

import subprocess
import time


def kill_service(service):
    subprocess.run(
        ["systemctl", "stop", service],
        check=False
    )

    time.sleep(10)


def check_service(service):
    result = subprocess.run(
        ["systemctl", "is-active", service],
        capture_output=True,
        text=True
    )

    return result.stdout.strip() == "active"


def chaos_test(service):
    print(f"开始故障演练:{service}")

    kill_service(service)

    if check_service(service):
        print("自动恢复成功")
    else:
        print("自动恢复失败,需要人工介入")


chaos_test("my-service")

生产环境当然不能直接这么玩。

但这个思想非常重要:

恢复能力不是写在文档里的,是通过一次次故障演练证明出来的。


十二、运维真正应该关注的,不是“重启次数”

最后说一个我个人非常在意的观点。

很多运维指标喜欢统计:

CPU
内存
磁盘
QPS
TPS
错误率

这些当然重要。

但对于可恢复性工程,我更建议增加几个指标:

MTTR

平均恢复时间。

故障发生
    ↓
恢复成功

花了多久?


自动恢复率

自动恢复成功次数
----------------
总故障次数

这个数字越清楚,越知道自己的系统到底有多“自愈”。


故障隔离时间

故障发生
 ↓
发现
 ↓
摘流

这个时间有多长?

有时候真正致命的不是恢复慢,而是:

故障实例继续接了 10 分钟流量。


恢复失败率

如果:

100次自动恢复
20次失败

那就说明自动化机制本身还有问题。


十三、我越来越觉得:真正高级的运维,是“让人少救火”

以前我们理解运维:

系统挂了
↓
找运维
↓
运维登录
↓
敲命令
↓
恢复

后来开始自动化:

系统挂了
↓
脚本执行
↓
自动重启

再往后:

系统挂了
↓
自动识别
↓
自动隔离
↓
自动恢复
↓
自动验证
↓
自动重新接入
↓
失败才找人

这才是我理解的可恢复性工程

它不是简单地多写几个 Shell 脚本。

也不是把所有事情都交给 AI。

更不是一句:

“我们做了高可用。”

真正的可恢复性,是把系统当成一个会受伤的生命体来设计:

它可以出问题,但问题要被限制;它可以失败,但失败要可恢复;它可以恢复,但恢复必须经过验证;自动化可以处理大多数情况,但必须知道什么时候该停下来叫人。

最后送给做运维、DevOps、SRE 的朋友一句话:

高可用解决的是“别轻易挂”,可恢复性解决的是“挂了也别怕”。

而真正成熟的系统,不是从来没有事故。

而是某一天凌晨两点,某个节点真的挂了——

系统自己把它隔离了,自己恢复了,自己验证了。

第二天早上你打开监控,发现昨晚发生过一次故障。

但用户甚至没有感觉到。

我觉得,这才是自动化运维真正值得追求的样子。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。