十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Python自建自动化恢复演练框架:从设计到实战

用Python自建自动化恢复演练框架:从设计到实战 在运维圈待久了你会发现一个残酷的事实**真正让系统挂掉的往往不是故障本身而是我们从未验证过恢复预案能不能用。**备份脚本一直正常跑可真到数据损坏那天DBA才发现恢复包少了一个主从切换流程写了三十页文档演练时才发现有个节点的健康检查接口早在三个月前就被防火墙拦截了。这些事我踩过太多次所以后来痛定思痛用Python从零搭了一套自动化恢复演练框架今天就把这套东西的完整设计思路和实战细节全盘托出。这套框架解决的核心问题很简单用脚本模拟真实故障场景自动执行恢复流程并验证恢复结果是否达到预期。它适合所有维护着核心业务系统的运维工程师、SRE、DevOps从业者也适合那些刚接手生产环境、想搞清楚系统恢复底细的新人。不需要你有多深的Python功底但最好熟悉基础语法和Linux常用命令因为框架本身就是在这些基础能力之上盖起来的。1. 内容整体设计与思路拆解1.1 为什么非要用框架来做恢复演练先说个身边的真实案例。某次我们准备了一次数据库全量恢复演练计划书写得漂漂亮亮凌晨两点执行预计耗时四小时影响范围可控。结果真到演练那天光是把备份文件从远端存储拉回来就花了三个多小时——存储带宽被人半夜跑批任务占满了。等数据恢复完一核对发现日志截断点不对主从数据差了二十分钟。那次演练折腾到早上七点业务方差点投诉。复盘的时候大家发现问题不在于“会不会恢复”而在于恢复过程中的各种外部依赖根本没验证过存储带宽是否充足、备份文件是否可以读取、恢复服务器是否满足硬件要求。手工演练最大的问题是无法标准化每次都是靠老师傅临场应变换个人操作结果就完全不一样。而脚本化、框架化的恢复演练能让这种混沌的、依赖个人经验的过程变得可度量、可重复、可审计。我选择自研而不是直接用市面上的混沌工程工具是因为场景侧重点完全不同。Chaos Mesh、Litmus这类工具擅长在Kubernetes环境里做故障注入但我们的场景是传统虚拟机加物理机混合部署还有大量Shell脚本和定时任务在跑通用工具落地成本太高。用Python写一套轻量级框架既能快速适配内部环境又能和已有的监控、告警系统无缝对接这才是我们真正需要的。1.2 恢复演练和故障演练的根本差异这里必须掰开揉碎讲清楚一个概念故障演练不等于恢复演练。故障演练的核心是“注入故障”验证系统在异常情况下的表现比如杀死进程、模拟网络分区看看系统能不能自愈、告警准不准而恢复演练的核心是“执行恢复”验证的是预案中那些恢复动作——切换主机、恢复备份、重建应用——在实际环境中到底能不能跑通。用大白话解释故障演练是往系统上捅一刀看它能不能忍住疼恢复演练是系统已经倒下了看你能不能把它救回来。这两者关注的点完全不同。实际工作中我们最容易犯的错误就是把做了一堆故障演练当成恢复能力有保障结果真出事后发现恢复路径上的坑一个没绕开。所以我的框架设计原则很明确脚本只做三件事——构造恢复场景、触发恢复动作、校验恢复结果。故障注入工具能做的我不重复造轮子但恢复链路上的每一步都必须走真实的脚本、真实的命令、真实的检查。1.3 为什么选择Python作为框架语言选型的时候其实也纠结过是写Bash还是用Go。Bash写起来快但逻辑一复杂就是一坨乱麻变量作用域、字符串处理、数组操作全是坑更别提跨平台兼容性了——我们的演练环境里有CentOS 7有Ubuntu 20.04还有几台SUSE同一个sed命令在不同版本上行为可能都不一样。Go的编译部署是方便但团队里能在短时间上手写Go的人不多后期维护成本高。Python的优势在于生态全、上手快、胶水能力强。用subprocess模块可以轻松调用各种Shell命令和二进制工具用paramiko可以直接连接远程服务器执行恢复动作用jinja2可以动态生成恢复脚本模板用pandas处理恢复后的数据校验结果。而且Python的类型注解写清楚后代码的可维护性比纯Shell脚本强了一个量级。补充一点Python 3.6的版本都推荐使用f-string做字符串拼接比老式的format()可读性高很多。环境方面建议统一用虚拟环境管理依赖requirements.txt里锁定所有第三方库版本不然某天paramiko升级导致SSH行为变了你连自己怎么挂的都不知道。2. 核心模块拆解与实操要点2.1 框架的整体目录结构与职责划分框架我放在一个叫recovery_drill的项目里目录结构很清晰recovery_drill/ ├── config/ │ ├── scenarios/ │ │ ├── mysql_restore.yaml │ │ ├── redis_failover.yaml │ │ └── app_rollback.yaml │ └── environments/ │ ├── prod.yaml │ └── staging.yaml ├── core/ │ ├── executor.py │ ├── health_check.py │ ├── report.py │ └── runner.py ├── plugins/ │ ├── mysql_backup_check.py │ ├── redis_replica_check.py │ ├── file_integrity_check.py │ └── data_compare.py ├── logs/ ├── reports/ └── main.py每个目录的职责划分很关键。config/scenarios放演练场景定义用YAML写不需要改代码就能调整恢复动作config/environments放不同环境的连接信息环境差异被完全隔离在外面core是框架的核心逻辑包括执行器、健康检查、报告生成和流程编排plugins是各种针对性的校验插件每个插件负责一个具体的恢复验证项。这种分层设计的最大好处是关注点分离。业务侧的人只需要关心scenarios里的YAML怎么写不用碰Python代码框架本身的人只需要维护core和plugins不用担心业务数据怎么比对。有人可能会觉得目录复杂但等你维护半年后就会明白清晰的边界远比省那几次import更值钱。2.2 场景定义文件的配置思路场景定义是整个框架的入口我用YAML做配置就是为了可读性。下面是一个MySQL恢复演练场景的定义示例name: mysql_full_restore_drill description: 从全量备份恢复MySQL数据库并校验数据完整性 timeout: 3600 environment: staging steps: - name: 检查备份文件完整性 plugin: file_integrity_check params: backup_path: /backup/mysql/full/ min_file_size_mb: 5120 retries: 2 - name: 停止应用写入流量 command: systemctl stop app-writer ignore_error: false timeout: 60 - name: 执行数据目录清空 command: rm -rf /data/mysql/data/* ignore_error: true timeout: 120 - name: 解压备份文件并恢复数据 command: bash /scripts/restore_mysql.sh --backup{{ backup_path }} --target/data/mysql/data timeout: 1800 - name: 启动MySQL服务 command: systemctl start mysqld timeout: 120 - name: 校验恢复后的数据 plugin: mysql_data_check params: db_name: order_db check_sql: SELECT COUNT(*) FROM orders WHERE order_date DATE_SUB(NOW(), INTERVAL 7 DAY) expected_min_rows: 10000 health_checks: - type: mysql params: port: 3306 query: SELECT 1 - type: tcp_connect params: host: 127.0.0.1 port: 3306YAML里值得注意的几个细节。timeout是每个步骤的超时时间防止恢复脚本卡死导致演练无限挂起ignore_error很讲究比如清空数据目录这种危险操作如果在演练环境里路径写错导致目录不存在工具执行会报错但业务上这个报错不算致命所以标记为可忽略而停止应用写流量这个步骤必须成功因为后面要恢复数据如果还有流量在写数据一致性校验就会失败。环境信息单独放在config/environments/staging.yaml里用Jinja2模板渲染后注入到命令中。这样同一个演练场景可以无缝切换到不同的环境执行只需要改环境配置而不用动场景定义。这个设计在实战中真的省了很多事。2.3 三种执行器的设计命令执行、SSH远程执行、插件执行框架里我设计了三种执行器分别对应不同的恢复动作类型。第一种是本地命令执行器直接通过subprocess调用系统命令。这里有个很重要的细节必须设置超时时间subprocess默认不超时一个卡住的恢复脚本能把你整个演练时间拖垮。我的做法是这样import subprocess import shlex def run_command(cmd: str, timeout: int 300) - dict: proc subprocess.Popen( shlex.split(cmd), stdoutsubprocess.PIPE, stderrsubprocess.PIPE, universal_newlinesTrue, ) try: stdout, stderr proc.communicate(timeouttimeout) return { exit_code: proc.returncode, stdout: stdout, stderr: stderr, timed_out: False, } except subprocess.TimeoutExpired: proc.kill() stdout, stderr proc.communicate() return { exit_code: -1, stdout: stdout, stderr: stderr, timed_out: True, }用shlex.split而不是直接传字符串是为了避免shell注入和参数分割歧义。所有命令都不建议开启shellTrue宁可手工split出参数列表也不要图省事把命令丢给shell去解析。第二种是SSH远程执行器用于在远程机器上执行恢复操作。我用的paramiko连接超时和命令超时都要单独设置避免某台机器网络不通时把整个演练拖死。最好把SSH连接做成可重试的因为恢复演练往往涉及重启网络服务执行命令时网络闪断很常见。第三种是插件执行器用于处理那些不能单纯通过命令完成的数据校验。比如要对比恢复前后的数据库记录数、校验备份文件的内部校验和、检查某个业务表的关键字段分布是否正常。插件本质上就是一段Python代码继承框架的BasePlugin接口实现validate方法返回校验结果。这样可以把所有复杂校验逻辑隔离在每个插件文件里互不影响。2.4 健康检查模块怎么判断恢复真的成功了恢复演练最核心的问题不是“执行了恢复动作”而是“怎么证明恢复成功了”。很多团队做演练执行完恢复脚本看到进程起来了就算完事——这远远不够。进程起来不代表业务可用端口监听不代表接口响应正常接口响应正常不代表数据是对的。我设计的健康检查模块分为三个层次进程级别、服务级别、数据级别。进程级别检查恢复后的服务进程是否存在这最简单也是最基本的服务级别检查端口是否监听、HTTP接口是否返回预期状态码、数据库是否能正常执行查询数据级别检查关键业务表的记录数、最大值、最新时间戳是否满足预期或者抽样比对源端和恢复端的数据。健康检查模块的设计强调一点每次演练结束健康检查的结果必须落库。这样后面查看历史趋势时你可以很清晰地看到某个环境在几次演练中恢复时间是不是逐步缩短了某类故障的出现频率是不是变高了。这些数据才是恢复能力持续改进的基石。3. 实操过程与核心环节实现3.1 环境准备与依赖安装先说环境准备。开发环境我建议用Python 3.10以上3.10的match语法和异常处理改进对写框架代码很友好。如果服务器上只有Python 3.6那也够用但要注意dataclasses这类包在3.6里属于backport需要额外安装。依赖方面我只用了三个核心库pip install paramiko pyyaml jinja2就这三样。paramiko做SSH远程执行PyYAML解析场景配置文件jinja2做模板渲染。日志用标准库logging命令行参数用标准库argparse数据校验用纯Python或调用外部命令不需要引额外的重量级库。装好依赖后建议先跑一个最简单的连通性测试确认SSH连接、YAML解析、日志输出这些基础链路都通着再往里填业务逻辑。别一上来就把所有插件都写好然后发现问题不知道是框架的锅还是插件的锅。3.2 主流程编排器把多个步骤串起来core/runner.py是整个框架的“导演”它负责把场景定义里的步骤按顺序执行收集结果并汇总。核心就是一套有限状态机逻辑一个步骤执行后根据执行结果决定下一步是继续、重试还是终止演练。主流程的核心代码精简下来是这样的思路class DrillRunner: def run(self, scenario_path: str, env_path: str) - DrillResult: scenario self.load_scenario(scenario_path) env_config self.load_environment(env_path) context self.build_context(scenario, env_config) results [] for step in scenario.steps: result self.execute_step(step, context) results.append(result) if not result.passed and not step.ignore_error: self.handle_failure(step, result) break if result.passed and step.on_success_rules: self.apply_success_rules(step, context) health_check_result self.run_health_checks(scenario.health_checks, context) report self.generate_report(scenario, results, health_check_result) return report注意代码里的on_success_rules这是用来处理那些成功之后需要改变上下文的步骤的。比如一步恢复动作完成后后续命令需要引用恢复后的服务地址那这个地址就应该在成功之后写入context里。这一步很关键它保证了多步恢复流程之间能够动态传递信息而不是把所有参数都硬编码在配置里。另外整个演练执行必须支持“安全中止”。我实现了一个信号监听器收到SIGINT信号时会先尝试执行场景里配置的on_interrupt命令比如把停掉的服务重新启动然后再退出。虽然按钮叫“中止”但它不是甩手就跑而是尽力把环境恢复到操作前状态。这个真的是演练过才知道的痛有一回演练执行到一半发现校验数据写错了我直接CtrlC结果应用服务全停着数据库数据恢复了一半临时工位一团乱。3.3 插件开发规范以MySQL数据校验插件为例每个插件我建议都遵循统一的输入输出约定。输入接收一个PluginContext对象输出一个PluginResult对象。参考一个MySQL校验插件的骨架设计class BasePlugin: plugin_type base def __init__(self, config: dict): self.config config def validate(self, context: dict) - dict: raise NotImplementedError class MysqlDataCheckPlugin(BasePlugin): plugin_type mysql_data_check def validate(self, context: dict) - dict: db self.connect_mysql(context[db_host], context[db_port]) actual_rows db.query(self.config[check_sql]) expected_min self.config[expected_min_rows] passed actual_rows expected_min return { plugin: self.plugin_type, passed: passed, actual_rows: actual_rows, expected_min_rows: expected_min, check_sql: self.config[check_sql], }插件要和场景定义文件里的plugin字段严格对应。定义时用mysql_data_check框架注册插件时就把这个字符串和MysqlDataCheckPlugin类绑定在一起。绑定方式可以用字典映射也可以用Python插件系统动态加载但对绝大多数场景来说一个静态字典就够用了。写插件时要特别注意校验逻辑必须清晰可解释。比如说expected_min_rows为什么是10000那得是有业务依据的不能拍脑袋写。我在实际设计时所有校验阈值都会要求业务方在场景说明里注明来源否则再精细的演练也变成了数字游戏。3.4 YAML配置解析与参数渲染配置解析的坑比想象中多。第一个坑是YAML解析后的类型可能不是你想要的比如某个时间字段timeout: 120解析出来是int没问题但如果你写成timeout: 0120在某些解析方式下可能会被当作八进制或字符串处理。第二个坑是模板变量的注入时机环境配置和场景配置可能有同名变量你必须规定好优先级我这里的约定是场景变量优先环境变量其次默认值兜底。为了让配置解析这块干净又透明我写了一个独立的ConfigLoader类class ConfigLoader: def __init__(self, env_config: dict): self.env_config env_config def resolve(self, raw_value): if isinstance(raw_value, str): return Template(raw_value).render(**self.env_config) if isinstance(raw_value, list): return [self.resolve(item) for item in raw_value] if isinstance(raw_value, dict): return {k: self.resolve(v) for k, v in raw_value.items()} return raw_value这样场景配置里如果写了{ { backup_path } }就能被正确替换成环境配置里定义的具体路径。强烈建议模板变量统一使用双花括号不要为了少打字自己发明%var%这种写法因为Jinja2的语法生态成熟语法检查、变量缺失提示都能免费获得。4. 实战中的关键决策、参数计算与调优4.1 恢复演练的时间窗口怎么定恢复演练最大的矛盾在于既想验证真实的恢复能力又不能因为演练影响线上业务。所以要仔细设计流量窗口。我的做法是先在缓冲时间窗口内选择最小化影响的时间段然后让演练步骤尽量跑在流量低谷期。具体参数上我会给演练框架设置一个“最大影响时长”的硬指标。什么是最大影响时长就是从演练中第一个可能中断服务的步骤开始到最后一个恢复动作完成、服务重新可用为止这个时间段必须在一个预设值以内。比如核心支付业务可接受的停机窗口是15分钟那么恢复演练配置中的危险步骤累计执行时间就不能超过这个阈值。若超过就必须拆分成多轮演练或者优先演练低风险组件。另外演练开始前必须通知相关团队。我在框架里内置了一个通知步骤演练启动前自动往相关群发一条消息注明影响窗口和预估持续时间出现问题可以随时响应。这个习惯后来救了我很多次。4.2 超时参数从哪里来三个维度的依据前面代码里很多地方提到了timeout但不同步骤的超时时间怎么定我遵循三个维度的依据。第一个维度是历史基线。比如以前手工执行过一次全量恢复全程跑了35分钟那框架里这个步骤的超时就得设置成50到60分钟留出50%的余量。别卡在35分钟整生产环境和演练环境的磁盘IO性能可能差一到两倍。第二个维度是SLA约束。业务方如果承诺“RPO最多30分钟内”那恢复超时就必须在30分钟内完成这样配置里的超时上限就不能超过30分钟。因为这个数字不仅是技术指标更是业务承诺演练超时意味着你很可能无法在承诺时间内恢复。第三个维度是安全边界。一些危险操作比如数据目录清空操作超时就不要设置太长10到15秒就足够——如果这个操作真的要跑几十秒多半是磁盘卡住了等再久也没用早点中止早排查。4.3 故障场景分类与演练优先级排序实际规划演练场景时我建议先梳理自己系统中有哪些“备份了但没验证过恢复”的组件然后按故障影响面排序。影响面包含两个维度一是故障可能性这个组件多久出过一次问题二是故障后果出了问题最坏会怎么办。我自己有一套分级方法优先级场景类型示例演练频率P0核心数据丢失MySQL数据目录损坏每月P0核心服务不可用支付应用进程异常退出每月P1依赖组件故障Redis缓存宕机每季度P1基础设施异常磁盘空间耗尽每季度P2非核心组件故障日志采集服务中断每半年P2历史遗留问题某老版本应用环境损坏每半年定优先级之后可以引入“故障热力图”来动态调整每次正式故障发生时记录故障根因如果某个故障类型连续出现两次对应的演练场景优先级自动提升一级。这种方式比起纯粹依靠人力拍板效果更好。4.4 数据校验的参数选择校验粒度该多细数据校验是整个恢复演练的重中之重。很多团队用count(*)看个总数不差就认为恢复成功了——这远远不够。我给数据校验参数定了一个“三查”原则。第一查“边界值”检查最近时间窗口的数据是否存在。如果恢复出来的库是三天前的而业务上要求恢复目标点是故障前十分钟那这个恢复基本失败了。第二查“关键业务表”不是所有表都查挑那些直接承载核心业务逻辑的。第三查“样本一致性”随机抽样几十条记录逐字段比对源端和恢复端的值是否一致。参数设计上要做成可配置的。比如check_sql是查询模板expected_min_rows是下限sample_count是抽样数date_field判断数据新鲜度用。有了这些数据校验过程中产生的判断结果才是有意义的。我在框架里专门把这些参数写进报告每次演练后能很直观地看到校验覆盖面。5. 常见问题与排查技巧实录5.1 常见报错速查表在多个环境里实战踩坑多了我整理了一份高频问题速查表遇到类似情况可以先对号入座故障现象可能原因排查方向解决方案SSH连接超时网络策略变更或密钥过期检查目标端口连通、目标机SSH服务状态重新分发SSH密钥、添加重试逻辑YAML解析报错配置文件中存在特殊字符检查YAML语法高亮确认引号转义增加配置预校验步骤恢复命令执行很慢磁盘IO或网络带宽受限用iostat查看磁盘用iftop查看带宽错峰演练或者临时扩容带宽恢复后数据还是旧的备份文件刷新频率过低校验备份文件的实际时间戳加密强备份策略提高备份频率MySQL端口能通但连接失败恢复时配置了只读模式检查read_only参数尝试连接时手动关闭把“关闭只读”加入恢复脚本插件校验不通过但数据看着没问题校验维度过窄漏检了关键字段丰富校验SQL加入字段级别抽样比对增加数据一致性专项校验这张表是三年来在所有演练中提炼出来的几乎每一条背后都有一次真实的“惨案”。5.2 恢复过程中遇到权限问题的坑权限问题在恢复演练里最容易阴沟翻船。有次演练MySQL恢复脚本一直报错找不到数据文件排查了半天最后发现是执行恢复命令的操作系统用户跟备份文件属主不一致。备份文件是backup_user生成的但恢复脚本用root执行导致解压后的文件属主全是rootMySQL进程以mysql用户起来后根本没有目录的写权限。解决这类问题的思路恢复命令执行前先用ls -l列出关键目录和文件的属主和预期的属主做比对。这个检查可以做成一个通用步骤放在恢复动作之前防止后面执行很久了才发现权限问题导致一切重来。另外恢复脚本里执行一些命令时尽量用sudo -u mysql来切到目标用户不要全程用root压过去。5.3 校验插件结果和人工复核不一致这种情况最让人头疼一度让我怀疑框架是不是有问题。后来定位到插件校验的是“执行时刻”的状态比如某个配置在恢复完成后自动被刷新了但人工复核时那个配置已经变了。还有一次是因为时区问题插件从数据库取的时区和人工命令行取的时区不同导致对比的时间数据出现偏差。解决方案是插件校验结果里必须记录校验时间戳和查询时的时区信息同时校验逻辑本身要做幂等——同一份恢复结果执行两次校验结果必须一致。如果做不出幂等那校验逻辑就得重写。另外报告里要附上执行校验时的原始查询SQL和参数方便人工复核时复现插件的行为。5.4 演练环境的资源竞争问题多套演练环境同时跑的时候资源竞争是个大坑。有一次我们同时跑一个MySQL恢复演练和一个Redis切换演练结果两个演练都把磁盘IO拉满了恢复时间比平时长了三倍导致一个演练判定失败。排查后发现是共享宿主机上的磁盘带宽被瓜分了。在框架层面能做的是执行前检查宿主机当前的磁盘IO和内存占用当资源水位超过阈值时直接拒绝启动演练避免带着“环境异常”跑演练导致结果失真。这个预检步骤虽然简单但能帮你省掉很多“演练失败后的复盘时间”。6. 框架的运行报告设计与后续演进方向6.1 报告生成从“执行成功”到“结果可解读”每一轮演练结束除了打出日志我还会生成一份HTML或Markdown报告核心是“结论先行”。报告第一段就必须写明本次演练场景是什么整体结论是通过还是失败关键步骤哪一步耗时最长健康检查几个指标没过影响时长是否符合SLA报告里每个步骤都要附上实际执行时长和预期时长的对比图这样管理者能快速判断“这次恢复快不快、卡在哪”。记得在一份事故复盘报告中对方最关心的就是“恢复用了多久是否符合RTO”这个数一定要能一眼看到。为此我加了一个摘要区演练名称: mysql_full_restore_drill 环境: staging 开始时间: 2024-06-15 02:00:00 结束时间: 2024-06-15 02:48:32 整体结论: 通过 影响时长: 28分12秒 SLA(RTO): 30分 关键失败项: 无报告自动归档到reports/{scenario_name}/{yyyy-MM-dd_HH-mm-ss}/目录下同时生成一份JSON版本方便后续做分析和统计。有了JSON你就可以在每月复盘时把历次报告喂给pandas做一个耗时趋势图看看每个恢复步骤的耗时是越来越短还是越来越长。6.2 框架的代码级拓展方向自定义插件体系框架本身要保持小而美复杂逻辑全部放插件。我预留的插件注册方式很简单PLUGIN_REGISTRY {} def register_plugin(name): def decorator(cls): PLUGIN_REGISTRY[name] cls return cls return decorator register_plugin(mysql_data_check) class MysqlDataCheckPlugin(BasePlugin): ...新增一个恢复验证只需要三步第一在plugins目录下新建一个py文件第二用register_plugin(你的插件名)装饰类第三在场景YAML里引用这个插件名。框架的core部分几乎不用动这让我在没有大量重复代码的情况下快速扩展了几十种校验场景。6.3 演进方向从“恢复演练”到“恢复能力度量”框架搭好后我慢慢发现它最大的价值不止是执行演练而是开始积累一个“恢复能力度量系统”。每次演练都会产生一份数据什么场景、耗时多少、校验是否通过、哪个环节最薄弱。把这些数据积累下来就能绘制出一条真正的“恢复能力基线”。下一步计划做两件事。一是将演练结果接入现有的监控大屏让恢复能力指标像延迟、错误率一样成为可观测的一环二是做“自动恢复验证”就是当系统真正发生故障时自动触发和演练时完全相同的验证逻辑判断恢复动作是否真的生效。这样恢复演练框架就从一个个孤立脚本慢慢长成了恢复体系的一部分。我个人在这套框架上线运行大半年后最大的体会是恢复演练这项工作的意义不在于“演练本身成功与否”而在于它对业务连续性的兜底。框架再完善也只是工具真正关键的是让团队形成“定期验证恢复能力”的肌肉记忆。每次跑完演练我都会顺手在报告末尾加一行下次演练前要改进哪些点。就这一行字逼着大家不断把恢复预案打磨得更扎实。
返回列表