news 2026/10/1 14:21:28

云厂商技术支持部门的原话:元数据库删了,我们也恢复不了[特殊字符]

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
云厂商技术支持部门的原话:元数据库删了,我们也恢复不了[特殊字符]

「无恃其不来,恃吾有以待也。」——《孙子兵法·九变》

托管 Airflow 的全部历史,活在一个你拿不到连接串的数据库里。

一次 MWAA 环境升级之后,整个 Airflow 元数据库归零:DAG 运行历史、任务实例记录、审计日志、Variables、Connections,全部回到出厂状态。我开了 support case 问能不能恢复,拿到的回复是这样一段:

Unfortunately, AWS does not retain accessible snapshots or backups of deleted/replaced MWAA metadata databases. Once the old environment was destroyed via the Terraform replacement, the metadata is unrecoverable. AWS Support does not have direct access to the managed metadata database and cannot perform restores on behalf of customers.

翻译过来:AWS 不保留已删除或被替换的 MWAA 元数据库的可访问快照;旧环境一旦被 Terraform 的替换动作销毁,元数据不可恢复;AWS Support 没有这个托管数据库的直接访问权限,也无法代客户执行恢复。

这段话里没有一个词是关于「这一次」的。它描述的是一个长期成立的事实:在这个服务模型里,你的 DAG 运行历史没有任何 AWS 侧的兜底副本。唯一可能存在的副本,是你自己写代码存下来的那一份。

于是问题就从"这次是谁的错"变成了一个更难的问题:在一个连不上数据库的托管服务里,"有备份"这件事到底由谁、在哪一层、用什么代码来兜底。

如果你读到这里心里想的是"我们有备份 DAG"——先对着它跑这一条:

grep-rn"is_paused = true"dags/ plugins/2>/dev/null

命中了的话,这个脚本每跑一次,就把整个环境的 DAG 暂停一次。它不是备份工具,是迁移工具。你把它挂上定时调度的那天,你其实给自己排了一个周期性停机。

根因两句话能说完(第一节)。真正花时间的是后面:官方给的两套导出代码为什么不能混用、四个只有读源码才知道的坑、以及生产上到底该怎么排。

看完这篇,你应该能拿走三件事:

  • 托管服务托管的是可用性,没托管你历史数据的持久性。这两件事在 MWAA 上的分界线,正好落在你自己的dags/目录里。
  • AWS 官方给了你两套元数据导出代码,行为完全不同:一套会暂停全部 DAG,一套不会。grep is_paused一秒分辨。用错那套,你的「每日备份」就是每日停机。
  • 备份文件的保质期由 Airflow 版本决定。task_instance在 2.10 多了executor列,log多了try_number列——跨版本恢复不是COPY,是列对齐工程。

下面是按教学顺序排的,不是按我踩坑的顺序。真实的排查是来回折返的,你不必跟着折返一遍。

(文中环境名做了脱敏,etl-sit-251/etl-sit-2112对应真实的旧/新环境名,版本号、时间戳、报错数量都是真实的。)


一、事故只有两行 diff,但它关掉的是一扇门

一次 MWAA 升级,Terraform PR 改了两行:

-airflow_version = "2.10.3" +airflow_version = "2.11.2" -name = "etl-sit-251" +name = "etl-sit-2112"

251是 2.5.1 的缩写,2112是 2.11.2 的缩写——把当前 Airflow 版本焊进环境名,这个命名习惯在这个仓库里跑了好几年,看上去只是一次可读性改进。

apply 完成,控制台AVAILABLE,LastUpdate: SUCCESS,依赖安装日志干净,调度器心跳正常。十分钟后 DAG 列表开始变红,一类是权限角色不存在,另一类查下来精确是195个:

airflow.exceptions.AirflowException: The access_control mapping for DAG 'X' includes a role named 'finance-developers', but that role does not exist
KeyError: ''

角色不存在,Variable 取出来是空字符串。两个症状,一个原因:这个环境的元数据库是全新的、空的。CloudTrail 里给出了终审证据——不是一次UpdateEnvironment,而是一前一后两次调用:

10:58:46 DeleteEnvironment etl-sit-251 11:26:12 CreateEnvironment etl-sit-2112

根因到这里就结束了:MWAA 的UpdateEnvironment接口里,Name只出现在 URL 路径/environments/{Name}上,从不出现在请求体里。改名这个动作在 API 层面不存在,所以 Terraform 只能用「删掉旧的、建一个新的」来实现你声明的状态。CloudFormation 文档里那句Update requires: Replacement说的就是这件事。

但真正把这件事从「运维失误」变成「架构缺口」的,是 AWS Support 回复里的另一段话。

Support 在确认根因之后,给出的就是开头那段不留余地的答复。它的信息量比根因大得多——它说的不是「你这次没了」,而是:

在这个服务模型里,你的 DAG 运行历史、审计日志、Variables、Connections,没有任何一个 AWS 侧的兜底副本。唯一可能存在的副本,是你自己写代码存下来的那一份。

如果你的合规、SLA 统计、审计追溯依赖这些数据——那这份副本不是「运维的 nice to have」,它是你系统的一部分,而且必须写在dags/里。

你看到的信号它真正回答的问题它没有回答的问题
AVAILABLE环境现在是否健康是不是原来那个环境
LastUpdate: SUCCESS本次操作是否完成这是更新还是替换
依赖安装日志干净新环境构建成功旧环境的状态有没有跟过来
「AWS 是托管服务」谁负责跑谁负责记得

托管服务托管的是「它活着」,没托管「它记得」。


二、为什么备份 Airflow 元数据是件反常的事

先把反常之处摆清楚,否则后面所有方案看起来都像过度设计。

一个普通的 Postgres 备份长这样:控制台开自动快照、pg_dump拉一份、PITR 定个保留期,全程不碰应用代码。

MWAA 的元数据库上,这三条路一条都没有:没有 RDS 控制台条目,没有 endpoint,没有连接串,没有 PITR 按钮。官方文档写得很直白——从已有的 MWAA 环境出发,没有对元数据库的直接访问。

那唯一的入口是什么?是你自己的 DAG 进程里那个 session:

fromairflowimportsettingsfromsqlalchemyimporttext session=settings.Session()result=session.execute(text("select dag_id, run_id, state from dag_run"))

也就是说,能访问这个数据库的唯一身份,是"一个正在这个环境里运行的 Airflow task"。这一条直接决定了后面所有设计的形状:备份必须是一个 DAG,恢复必须是一个 DAG,演练必须是一个 DAG,而这些 DAG 的生命周期又绑在那个随时可能被替换掉的环境上。

这就是我后来管它叫**「托管边界的静默一半」**的东西:托管服务把「跑得起来」做成了平滑的抽象——你不需要知道 Aurora 在哪、schema 怎么迁移、心跳谁在看;但它把「记得住」留在了抽象之外,而且不会在同一个界面上告诉你这条缝在哪。责任分界线从来不画在控制台上,它画在 API 参考里那一行没人读的字段列表里。

天花板定律:一个托管资源的恢复能力上限,等于它交到你手上的最低层访问接口。MWAA 交给你的是settings.Session()——所以你的备份能力上限,就是你愿意写多少 SQL。


三、AWS 给了你两套代码,其中一套会让你每天停机一次

这是整篇文章里最实用的一段。

搜「MWAA metadata backup」,你会撞到两份 AWS 官方出品的代码,长得很像,都在aws-samples下,都叫「导出元数据」。它们的行为根本不同。

3.1 第一套:mwaa_export_data.py(start-stop 用例里的那个)

这是官方迁移路径的实现。打开源码,第一个任务就说明了它的性格:

# pause all active dags to have consistent and reliable copy of dag history exportsdefpause_dags():session=settings.Session()session.execute(text(f"update dag set is_paused = true where dag_id != '{dag_id}';"))session.commit()session.close()

它不是「顺手暂停一下」。DAG 依赖链是:

back_up_activedags >> pause_dags >> [export_data, export_active_dags, export_variable, export_connection] >> clean_up >> [activate_dags_on_failure, notify_success]

注意activate_dags_on_failure的trigger_rule="one_failed"——只有失败时才恢复暂停状态。成功路径上没有 unpause。因为设计意图就是这样:导完了你就该去新环境了,旧环境本来就该停着。官方文档在这件事上是坦白的:「在导出与导入过程中,所有其他 DAG 都处于暂停状态。」

还有一个更容易被忽略的动作——为了记住「哪些 DAG 本来是开着的」,它往被备份的那个库里建表:

defback_up_activedags():session=settings.Session()session.execute(text(f"drop table if exists active_dags;"))session.execute(text(f"create table active_dags as select dag_id from dag where not is_paused and is_active;"))

drop table if exists+create table as。一个写生产元数据库的备份脚本。对一次性迁移来说这是合理的工程折中;对一个每天跑一遍的备份任务来说,这是在生产库里反复建删表。

3.2 第二套:mwaa-dr(PyPI 包,真正的备份工具)

同样是 AWS 出品(aws-samples/mwaa-disaster-recovery),mwaa-dr把导出/导入包装成了一个 DAG factory。它的 backup DAG 里,setup这一步是这样的:

defsetup_backup(self,**context):print("Executing the backup workflow setup ...")ifself.storage_type==S3:print("No local file system setup necessary!")return# ...(只在 LOCAL_FS 模式下建目录)

没有 pause。一行都没有。它的 DAG 结构是setup >> export_tables >> teardown,纯读 + 写 S3。而且它显式支持被定时:

defschedule(self)->str:returnVariable.get("DR_BACKUP_SCHEDULE",default_var=None)

默认None(只能手动触发),你设一个DR_BACKUP_SCHEDULE变量,它就变成周期任务。这才是一个能挂上调度的东西。

普通人的看法:两份都是 AWS 官方的元数据导出脚本,随便挑一个,功能差不多。

资深工程师的洞察:它们回答的是两个不同的问题。一个回答「我要把历史搬到另一个环境去」,另一个回答「我要在不打扰任何人的情况下留一份副本」。前者可以接受停机,因为停机本来就是迁移窗口的一部分;后者一旦接受停机就自我否定了——一个你不敢在业务高峰随手跑一次的备份,等于没有备份,因为真正需要它的那一刻,你也不敢跑。

3.3 一张表分清楚

维度mwaa_export_data.py(迁移)mwaa-drbackup DAG(备份)
是否暂停全部 DAG是(update dag set is_paused = true)否
成功后是否自动恢复暂停状态否(只在失败时one_failed恢复)不涉及
是否写生产元数据库是(create table active_dags)否(只读 + 写 S3)
能否挂定时调度不应该能(DR_BACKUP_SCHEDULE)
trigger表显式排除默认包含
恢复前是否要求空库不要求(COPY进空环境)要求(配套cleanupDAG)
适用场景环境改名 / 跨版本迁移 / 蓝绿切换周期性备份 / DR 演练 / 同环境回滚

判据一行:grep -n "is_paused" <你的备份脚本>。有,它是迁移工具;没有,它才可能是备份工具。


四、四个只有读源码才知道的坑

这一节是我建议你存下来的部分。官方文档不会讲这些,但它们会在你真正需要恢复的那天决定成败。

4.1variable.csv和connection.csv里是解密后的明文

导出 Variables 的代码是这样的:

query=session.query(Variable)foryinquery.all():w.writerow({k[0]:y.key,k[1]:y.get_val(),...})

Connections 那边是y.get_password()。

get_val()和get_password()都是解密后的值。因为必须解密——Fernet key 是 per-environment 的,密文搬到新环境解不开。所以这条路走通的代价是:你的备份桶从此是一个密码库,里面是纯文本 CSV,每一行是一个生产系统的凭证。

官方文档在这件事上只留了一句 Note,说如果你要迁移敏感数据「建议为 S3 桶开启默认加密」。这个措辞相对于实际风险,是轻了的。落地时至少要有:

  • 专用桶,KMS CMK(不是 SSE-S3)加密,key policy 只放行 MWAA 执行角色
  • 桶策略拒绝非 TLS、拒绝跨账号,开 Block Public Access
  • 生命周期规则短期过期(备份的价值随时间衰减,泄露风险不衰减)
  • 更好的做法是让这两张表根本不值得备份:把 Variables/Connections 搬到 Secrets Manager backend,元数据库里就没东西可泄露了。mwaa-dr为这种情况专门留了策略开关,设成DO_NOTHING就跳过这两张表的恢复

4.2active_dags.csv的 header 契约不对称

导出端把active_dags表流成 CSV 时,用的是csv.writer(...).writerows(chunk)——没有写 header 行。

导入端读它的时候:

cursor.copy_expert("COPY active_dags FROM STDIN WITH (FORMAT CSV, HEADER TRUE)",f)

HEADER TRUE。PostgreSQL 会把文件第一行当表头丢掉。而文件第一行是一个真实的dag_id。

推论:迁移完成后,active_dags表里会少一个 DAG,UPDATE dag SET is_paused=false FROM active_dags就不会把它放出来——有一个本来在跑的 DAG,在新环境里会一直是 paused,而且没有任何报错。

这是我读代码得出的结论,不是我跑出来的,建议你在自己的环境里验一遍:

# 导出后:CSV 有多少行aws s3cp"s3://$BUCKET/data/active_dags.csv"-|wc-l# 导入后:新环境里 active_dags 表有多少行,差值应该是 0,不是 1

一个静默少一行的 bug,和一个抛异常的 bug,代价差了一个数量级。

4.3 CSV 的列名在说谎,而它恰好还能工作

这个最微妙,也最值得内行一笑。导出 Connections 时声明的列名是:

k=["conn_id","conn_type","host","schema","login","password","port","extra","is_encrypted","is_extra_encrypted","description"]

实际写进去的值却是错位的:

CSV 第 N 列声明的列名实际写入的值
3hostdescription
4schemahost
7portschema
8extraport
9is_encryptedextra

看到这里你会以为找到了一个数据错乱的 bug。但导入端是这样读的:

rows.append(Connection(row[0],row[1],row[2],row[3],row[4],row[5],row[6],port,row[8]))

位置参数。而Connection.__init__的签名恰好是(conn_id, conn_type, description, host, login, password, schema, port, extra)——和实际写入顺序完全对齐。所以 round-trip 是正确的。

真正的问题是:这个 CSV 文件的契约不是它的列名,而是Connection.__init__的参数顺序。列名只存在于导出脚本的源码里,而且是错的。谁要是照着列名自己写一个 importer(或者拿这个 CSV 去做审计、做 diff、导进别的系统),host 和 description 会对调,schema 和 port 会错位,而且照样不报错。

口诀:一个只有位置契约、没有 header 的数据文件,它的 schema 不在文件里,在读它的那段代码里。备份格式如果依赖「有人记得读的顺序」,它就已经开始腐烂了。

4.4trigger表:两个官方工具的意见是相反的

mwaa_export_data.py里有一段很少见的、写得很诚实的注释:

# NOTE: The trigger table is intentionally excluded from export.# Starting in Airflow 2.9.0, trigger.kwargs is Fernet-encrypted with a per-environment key.# Exporting and importing these rows into a different environment causes# cryptography.fernet.InvalidToken errors that crash the triggerer and scheduler.

而mwaa-dr的默认备份表清单里,trigger在里面:variable、connection、slot_pool、log、job、dag_run、trigger、task_instance、task_fail、xcom。

两个 AWS 官方工具对同一张表给出了相反的答案,而且两个都对——因为它们的恢复目标不同:

恢复场景Fernet keytrigger能不能带
同环境恢复(误删数据、回滚、DR 演练回原环境)相同能,kwargs解得开
跨环境恢复(改名、蓝绿、跨区 DR)不同不能,InvalidToken会把 triggerer 和 scheduler 打崩

面试拿分点:被问到「Airflow 元数据迁移有哪些表不能直接搬」,答「trigger 表」只是及格;答「trigger.kwargs从 2.9.0 起用 per-environment Fernet key 加密,跨环境导入会抛InvalidToken打崩 triggerer;而且 trigger 本身是 deferred task 的瞬时状态,新环境跑一遍就重建了,所以正确做法是排除而不是修复」——这个答案会被记住,因为它同时给出了机制、后果和取舍。

顺手记住另外几张表的性格,都能在源码里对上:

表会不会跟着走为什么
dag、dag_tag、dag_code、serialized_dag不需要scheduler 解析 S3 里的 DAG 文件时自动重建
权限/角色相关表不需要(但要重建)由 IAM 执行角色与 FAB 配置生成;那一批access_control报错就是它没跟过来
dataset/asset相关表显式排除2.9.2 起自动生成,导入会撞主键
slot_pool带,但排除default_pool新环境自带default_pool,导进去会冲突
task_instance带,但过滤掉未终结状态导出条件是state NOT IN ('running','restarting','queued','scheduled','up_for_retry','up_for_reschedule')——正在跑的任务不在备份里
导出 DAG 自己的dag_run行特殊处理否则恢复后它会像catchup=True一样重跑自己

最后那两行值得单独说。task_instance的过滤条件意味着:你的备份语义是「已终结的历史」,不是「此刻的全量状态」。任何一次恢复之后,跨越备份时刻正在运行的那批任务需要人工重跑——这是你 RPO 之外的一笔额外账,AWS 的 DR 博客也把「中断期间的 DAG run 需要手动重跑」写成了显式步骤。


五、官方 runbook 的cd目标已经是 404

这一段是时效性最强的,也是最能说明「照着文档抄会翻车」的。

AWS 官方迁移文档到今天(2026-09-30)还这样写:

gitclone https://github.com/aws-samples/amazon-mwaa-examples.gitcdamazon-mwaa-examples/usecases/metadata-migration/{existing-version}-{new-version}/

然后让你改export_data.py里的S3_BUCKET,上传,unpause 那个叫db_export的 DAG。

自己验一下:

curl-s-o/dev/null-w"%{http_code}\n"\https://raw.githubusercontent.com/aws-samples/amazon-mwaa-examples/main/usecases/metadata-migration/2.5.1-2.10.1/export_data.py# 404

usecases/metadata-migration/目录现在只剩两个文件:.airflowignore和README.md。README 的全部技术内容是一句话:

Metadata import and export scripts are now part of the MWAA Disaster Recovery project.

脚本搬家了,文档没跟上。所以如果你在事故当天照着官方文档执行,你会卡在一个不存在的目录上——而那一刻旧环境可能还活着,导出窗口正在关闭。

更值得注意的是新家的支持矩阵。mwaa-dr当前版本 2.2.0,README badge 上的支持版本是:

MWAA 2.10.3 | 2.10.1 | 2.9.2 | 2.8.1 | 2.7.2 | 2.6.3 | 2.5.1 | 2.4.3

没有 2.11。而这次事故的目标版本正是 2.11.2。

这不是什么阴谋,看一眼 factory 的代码就明白为什么:

classDRFactory_2_10(DRFactory_2_9):deftask_instance(self,model):returnBaseTable(name="task_instance",columns=[...,"executor",# New Field...],export_filter="state NOT IN ('running', ...)",)

每个版本的 factory 都是上一个版本的子类,重写的只有列发生变化的那几张表:task_instance在 2.10 多了executor,log多了try_number。也就是说,备份文件的 schema 是和 Airflow 版本一一绑定的。一份 2.10.3 上导出的task_instance.csv,COPY进 2.11.2 的库里,要么列数不匹配直接报错,要么更糟——列数恰好对上而语义错位。

文档的半衰期短于依赖的半衰期。所以判断一条 runbook 能不能用的方式不是「它在官方站点上」,而是它引用的每个路径、每个包版本,今天还curl得到。在事故里,一条过期的 runbook 比没有 runbook 更贵,因为它消耗的是你最紧张的那几十分钟。


六、那么生产上到底怎么做

先说清楚:下面这套我们还没上线,是事故之后定下的方案,写在这里是因为设计上的取舍比代码本身更值得抄。凡是需要实测验证的地方我都标出来了。

6.1 第一步不是写备份,是缩小备份范围

最省事的备份是不需要备份的数据。先给每张表定性,再决定写多少代码:

层内容处置
L0 不用备份dag、dag_tag、dag_code、serialized_dag、dataset/assetscheduler 解析 S3 自动重建。备份它们只会增加恢复时的主键冲突
L1 不该放在这儿variable、connection搬去 Secrets Manager backend。一次改造同时解决「明文进 S3」和「备份范围过大」两个问题
L2 必须备份dag_run、task_instance、task_fail、log、job、slot_pool、xcom这七张表才是审计、SLA 统计、运行历史的真正载体,也就是这次事故真正丢掉的东西
L3 不要备份跨环境场景下的trigger;权限/角色表Fernet key 不通;权限应由 IaC 重建,而不是从 CSV 恢复——否则你的权限模型会漂移到没人能复现的状态

L1 那一行值得多说一句:把 Variables/Connections 移出元数据库不只是安全改进,它还改变了事故的形状。这次KeyError: ''之所以致命,是因为业务代码的配置活在元数据库里;如果它们活在 Secrets Manager 里,同样一次环境替换只会丢历史,不会让 DAG 直接崩。

6.2 备份 DAG(真实可跑的代码)

# dags/backup_metadata.pyfromairflowimportDAGfrommwaa_dr.v_2_10.dr_factoryimportDRFactory_2_10 factory=DRFactory_2_10(dag_id="dr_backup_metadata",path_prefix="data",storage_type="S3",)# 必须赋值给全局变量,否则 DAG 扫不到dag:DAG=factory.create_backup_dag()

配套三件事,缺一不可:

  1. 专用 S3 桶(KMS CMK 加密),MWAA 执行角色有读写权限
  2. Airflow 变量DR_BACKUP_BUCKET= 桶名(不是 ARN)
  3. Airflow 变量DR_BACKUP_SCHEDULE= 你的 RPO 对应的 cron。不设就只能手动触发——而「等我需要的时候手动跑一下」,就是这次事故的完整剧本

requirements.txt里加mwaa-dr(它依赖smart-open>=7.0.4)。

升级到 2.11 时必须自己接一棒:mwaa-dr还没有v_2_11factory,所以要么继承DRFactory_2_10重写列发生变化的表,要么先在 aws-mwaa-local-runner 上把 2.11 的 schema diff 跑出来。这一步没有捷径,也别指望文档会告诉你——它连 2.10 的脚本搬家都还没更新。

6.3 恢复演练,而不是恢复方案

mwaa-dr的 restore要求一个空库,所以它配了一个cleanupDAG。这意味着「在生产环境里试一下恢复」是个危险动作——正确的演练场是 local runner:

factory=DRFactory_2_10(dag_id="dr_restore_metadata",path_prefix="data",storage_type="LOCAL_FS",# 演练用本地文件系统,不碰 S3)dag:DAG=factory.create_restore_dag()

恢复时那两个策略变量决定了 Variables/Connections 的行为,默认是APPEND:

DR_VARIABLE_RESTORE_STRATEGY/DR_CONNECTION_RESTORE_STRATEGY行为什么时候用
DO_NOTHING完全不恢复这两张表已经用 Secrets Manager backend(推荐终态)
APPEND(默认)只补缺失的条目,不覆盖往一个已经配好的新环境里补历史
REPLACE用备份覆盖现有条目确定备份比当前环境更权威时

演练要验的不是「DAG 跑绿了」,是这四条:Variables 条数对得上、Connections 能实际连通、dag_run的行数和最近 N 天的历史对得上、UI 里 unpause 状态和事故前一致(这一条正好能顺带验证 4.2 里那个 header 推论)。

6.4 把这次的教训变成一道闸门,而不是一条口头规矩

事故复盘里最没用的产物是「下次注意」。这次真正要留下的工程产物是一条 CI 检查——任何会替换有状态资源的 plan 都不允许自动合并:

terraform plan-out=tfplan terraform show-jsontfplan|jq-e' [ .resource_changes[] | select(.change.actions == ["delete","create"] or .change.actions == ["create","delete"]) | select(.type | test("mwaa_environment|db_instance|rds_cluster|elasticache|msk_cluster")) ] | length == 0 '||{echo"有状态资源将被替换,需要人工签核 + 导出窗口";exit1;}

它不阻止你替换资源——有时候你真的需要。它只是强制「替换」这件事被一个人看见一次。这就是 senior 和 principal 的分界线:把只靠口头传承的知识,变成一个会失败的检查。

6.5 日志组:那个命名习惯的黑色幽默

顺一句,也是唯一的好消息。任务日志在 CloudWatch 的airflow-{环境名}-Task里,它和 MWAA 环境是两套独立的存储,删环境不删日志。所以这次事故里,旧环境的任务输出还在airflow-etl-sit-251-*下面,可以用 Logs Insights 捞。

但日志组名是从环境名派生的,新环境写新组,旧组不会被链接进新 UI。官方文档对此有一句加粗的 Important:改名意味着历史任务日志在新环境的 Airflow UI 里访问不到。

黑色幽默在于:如果当初不把版本号焊进环境名,日志组和元数据库两样都不会丢。那个为了「一眼看出跑的是哪个版本」而设计的命名规范,同时炸掉了两样东西。查了一下这个环境的日志组,243、251、306、一次rollback、一次临时变更标记,像地层一样排着——过去几年里,这一幕演过不止一次,只是之前没人回头找过那些历史,所以从来没人把它当事故报出来。

一个流程不报警,不代表它没有代价。有时只是还没人需要被丢掉的那部分状态。


综合:三种失败模式,各自谁兜底

把整件事收成一张表。列的是「AWS 替你做了什么」和「剩下的必须你自己做」——这条分界线,就是你要写多少代码的答案:

失败模式AWS 托管的部分留给你的部分
原地版本升级(名字不变)升级前自动快照元数据库、升级组件、db migrate、失败自动回滚(最长约 2 小时不可用)用 local runner 验 DAG 与requirements.txt兼容性;手工改过的 DAG 不会被回滚
环境替换(名字变了 / 蓝绿 / 跨区)无全部:旧环境还活着时导出、新环境导入、日志组处置、恢复后验证
区域级灾难 / 误删 / 数据损坏多 AZ 容错(单 AZ 故障自动恢复)周期性备份 + 跨区复制 + SchedulerHeartbeat 告警 + 定期演练

第一行是唯一有安全网的一行,而它的唯一条件是:别碰Name。


立刻可以做的事

  1. grep -rn "is_paused = true" dags/。如果你的「备份」脚本里有这行,它是迁移工具。现在就把它从任何定时调度上摘下来。
  2. aws s3 ls你的备份桶,随便下一个connection.csv看一眼。如果里面是明文密码,今天就换 KMS CMK + 收紧桶策略,并把「Variables/Connections 迁到 Secrets Manager」排进 backlog。
  3. curl一遍你 runbook 里引用的每个 GitHub 路径和包版本。文档搬家了不会通知你。顺手确认你的目标 Airflow 版本在mwaa-dr的支持矩阵里——如果不在,这就是升级工作量的一部分,不是升级之后再说的事。
  4. 把 §6.4 那条jq加进 CI,对准你所有有状态资源:MWAA、RDS、ElastiCache、MSK。让「替换」这件事必须被一个人看见一次。
  5. 在 local runner 上跑一次完整的 backup → cleanup → restore,然后对四件事:Variables 条数、Connections 连通性、dag_run行数、unpause 状态。没演练过的恢复方案,和没有恢复方案的区别,只在于你事故那天的心理预期。

我自己接下来要验的第一件事,是 §4.2 那个active_dags.csv少一行的推论——读代码能推出来,但我还没在真实环境里数过那两个数字。


云厂商替你托管了「它还活着」,从没答应替你托管「它还记得」。而这两件事的分界线,永远不在控制台上,在你有没有把它写成一个会跑的 DAG。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 14:20:52

深圳机场加急空运、航空货运流程与禁运货物一站式服务商

深圳机场加急空运怎么办理最快?航空货运的完整流程是什么?哪些货物属于禁运范围不能走空运?这三个问题是发货企业和个人咨询航空货运时最常见的疑问。深圳市航宇顺物流有限公司作为深耕珠三角17年的老牌空运公司&#xff0c;围绕这三个高频问题&#xff0c;为大家做一次系统…

作者头像 李华
网站建设 2026/10/1 14:19:46

策略需求文档怎么写?产品经理六段式写法与评审避坑指南

简介&#xff1a;策略产品经理基础知识系列中关于策略需求文档编写的DOCX文档&#xff0c;面向策略产品经理、产品新人及希望系统掌握需求文档写作方法的从业者。文档完整拆解了策略需求文档的核心结构——项目背景、项目目标、需求概述、需求详述、统计与监控需求五大模块&…

作者头像 李华
网站建设 2026/10/1 14:19:23

GitHub 仓库完全指南:从入门到精通的全流程操作手册

1. 引言 GitHub 是全球最大的代码托管平台&#xff0c;也是开发者协作与开源生态的核心枢纽。无论你是刚接触编程的新手&#xff0c;还是希望系统化提升协作效率的资深工程师&#xff0c;掌握 GitHub 仓库的创建、管理与操作流程都是必备技能。读完本文并动手实践&#xff0c;…

作者头像 李华
网站建设 2026/10/1 14:17:37

审查器不生效时,dsh-auto-review 先查这五个原因

dsh-auto-review 在审批链上放一个只读的第二模型&#xff1a;它读取证据并返回 { decision, reason, riskLevel } 裁决&#xff0c;默认失败关闭&#xff0c;全过程可从会话日志审计&#xff08;approval/asked → autoReview/verdict → approval/decided&#xff09;。它的行…

作者头像 李华