第 3 章 生命周期管理 — 优雅启动与关闭

本章导读

发版时用 kill -9 结束进程,再重新启动,进程就没有机会完成清理和持久化收尾。启动时要恢复数据、确认事务状态、准备对外服务;关闭时要处理在途请求、保存必要的数据、交接集群职责。忽略其中一步,重启就可能变成一次事故。

同样是 kill -9,Redis 可能丢失尚未持久化的数据,持久化配置会影响丢失量(详见第 4 章);MySQL 重新启动时进入 InnoDB 的崩溃恢复(crash recovery)流程;Kafka 则可能触发分区的 Leader 重选、让上游业务短暂抖动。单机实例需要恢复本地状态;Redis、MySQL 的复制或集群部署也可能触发故障转移;Kafka Broker 下线则会影响它承载的分区。

3.1 问题的本质

对有状态软件,启动的门槛不是进程存在,而是已恢复到能够处理请求的状态。Redis 要重建内存键空间,MySQL 要确定事务状态,Kafka 要恢复分区日志和节点职责。

关闭时则要确定:哪些数据必须保存,哪些工作可以留到以后处理,哪些职责要在退出前交接。图 3-1 概括了启动与关闭各自处理的状态变化;崩溃或强制退出跳过正常收尾,可能造成数据损失或把工作留到下次启动。

启动前的状态 已有持久化数据 或集群状态(若有) 可服务状态 必要状态已恢复, 可对外承接请求 正常退出 按方案完成必要收尾 有职责时交接或待接管 启动:恢复必要状态 完成恢复后才可服务 关闭:必要收尾与职责处理 刷盘、交接或后移工作依配置和部署决定 崩溃 / kill -9:跳过正常收尾,下次启动可能需要恢复
图 3-1 启动恢复与关闭收尾

3.2 Redis 的做法

Redis 运行时的键空间在内存中。有本地持久化时,重启要从 AOF 或 RDB 重建键空间;关闭时则要决定是否再生成一次 RDB,同时完成已开启的 AOF 刷盘。

3.2.1 启动四阶段

第一段:确定配置。 Redis 先设默认值,再读配置文件和命令行参数,供后续初始化使用。

第二段:建立基础设施。 Redis 创建事件循环和内存中的数据库结构,并准备网络监听。监听端口打开,还不表示已有数据恢复完成。

第三段:恢复数据。 启用 AOF 时,Redis 按 AOF 路径恢复;未启用时,才尝试加载独立 RDB。AOF 通常比定期快照保留更多近期写入,因此这一顺序关系到恢复点。图 3-2 展示了数据恢复在启动过程中的位置。

① 确定配置 设默认值 读配置文件与命令行参数 ② 基础设施就位 建立事件循环与数据库结构 准备网络监听 ③ 恢复本地数据 启用 AOF:按 AOF 路径恢复 否则:尝试加载独立 RDB ④ 进入事件循环 数据加载完成后,开始处理请求
图 3-2 Redis 启动流程

第四段:开始处理请求。 本地数据加载完成后,Redis 才进入事件循环,对外提供服务。

3.2.2 关闭:两种触发,一套流程

客户端发出 SHUTDOWN,或进程收到 SIGTERM/SIGINT,都会进入正常关闭流程。有副本时,Redis 7.0 起可在退出前短暂等待副本追平;等待超时后继续退出。随后才是本地持久化与连接收尾。

关闭触发 SHUTDOWN 命令 · SIGTERM/SIGINT ① 处理后台任务 检查并处理后台快照 / AOF 重写 ② 刷 AOF 缓冲(若 AOF 开启) 把待写入的数据刷盘 ③a SHUTDOWN SAVE 生成本次 RDB 快照 ③b SHUTDOWN NOSAVE 跳过本次 RDB 快照 ④ 断开连接并退出 释放本地资源
图 3-3 Redis 正常关闭流程

关闭时,Redis 先处理未完成的后台快照或 AOF 重写,再把已开启的 AOF 缓冲刷盘。SHUTDOWN SAVE 在退出前生成 RDB;SHUTDOWN NOSAVE 跳过本次 RDB 快照,仍会完成 AOF 刷盘。随后断开客户端并释放资源。

3.2.3 用信号触发优雅关闭

SIGTERM/SIGINT 让 Redis 有机会执行上述收尾;SIGKILL(kill -9)直接结束进程,无法等待 AOF 刷盘或 RDB 保存完成。MySQL 也通过退出信号进入正常关闭流程;Kafka 的 SIGTERM 则经 JVM Shutdown Hook 交给应用处理。

3.3 MySQL 的做法

MySQL 比 Redis 多了事务状态的约束。异常退出后重新启动时,要处理崩溃恢复;关闭时可以选择把部分清理留到以后。

3.3.1 启动链路:事务恢复是关键环节

MySQL 异常退出后重新启动,也要完成配置、存储引擎和网络准备,但决定何时能接请求的关键工作是 InnoDB 的事务恢复。它从 checkpoint(检查点)标记的位置扫描 redo log,补上已记录但尚未写回数据页的修改;redo 重放不按事务是否提交来筛选。未提交事务还要由 undo log 撤销。图 3-4 区分了接受连接前必须完成的恢复与可以继续进行的回滚。

InnoDB 崩溃恢复与后台回滚 完成必要的 redo 恢复并确定事务状态后,服务可以接受连接;尚未结束的事务回滚可以在后台继续。 崩溃恢复与后台回滚 ① 从 checkpoint LSN 扫描 redo 读取检查点之后可用的日志 ② 重放需要恢复的页修改 不按事务是否已提交筛选;已在页中的修改不重复补写 ③ 确定事务状态,完成启动所需准备 保留应提交的结果,安排撤销未提交事务 接受客户端连接 ready for connections 系统已经能够处理新的请求 后台继续未完成的回滚 使用 undo 撤销未提交事务 回滚工作可与新请求处理交错进行 锁等待
图 3-4 InnoDB 崩溃恢复与后台回滚

ready for connections 表示 InnoDB 已完成必要的 redo 恢复,可以接受连接;未完成的回滚可在后台继续。回滚事务持有的锁可能阻塞新请求,大事务会增加回滚工作量。

3.3.2 innodb_fast_shutdown:把清理工作在关机与开机之间分摊

MySQL 用 innodb_fast_shutdown 的 0、1、2 三档决定把多少清理工作放在关机前,多少留到下次启动或运行期。表 3-1 比较的是工作时机,不是一致性等级;三档都以保留已提交事务为前提。

表 3-1 innodb_fast_shutdown 三档对比

维度0 = 完全关闭1 = 快速关闭(默认)2 = 模拟崩溃
关闭时完成刷脏页,并完成 purge、修改缓冲合并等清理刷脏页,跳过完整 purge 与修改缓冲合并刷 redo,不刷数据页
留给之后下次启动需要处理的清理较少运行期继续完成未做完的清理下次启动执行崩溃恢复

默认档位 1 保留了脏页刷盘,把完整清理留到运行期,避免日常重启等待这部分工作。估计关闭时间时,除了档位,还要计入脏页和长事务等遗留工作量。

3.4 Kafka 的做法

Kafka 的消息保存在分区日志中。启动时,Broker 既要检查本地日志,也要取得集群元数据;关闭时,除了本地收尾,还可能需要交出分区 Leader 职责。

3.4.1 启动链路:服务就绪前的依赖

以 KRaft 模式为例,Broker 要知道自己负责哪些分区,检查相应的日志和索引,并追上所需的元数据。网络组件准备好,并不表示这些工作已经完成;满足恢复、授权和控制面的条件后,Broker 才处理请求。请求最终由 Handler 线程池执行,它的分工在第 5 章讨论。

异常退出后,Broker 检查追加日志的完整性,截断残缺尾部,并恢复索引及生产者等状态。消息本身保存在日志里,不需要像 InnoDB 那样再重放成另一套业务数据页。

3.4.2 关闭:普通关闭 vs 受控关闭

普通关闭会停止请求处理、断开连接并完成本地日志收尾,但不保证在途请求都能把结果返回客户端。如果 Broker 承担的 Leader 职责尚未迁移,Controller(集群控制器)要在它退出后重新选主,分区可能短暂不可用。

受控关闭(Controlled Shutdown)在退出前通知 Controller,让其他存活的同步副本接替 Leader,得到交接确认后再完成本地收尾。交接的是已有副本的角色,不是关机时复制全部消息。图 3-5 对照了退出前交接与退出后重选的区别。

下线 Broker Controller ISR 中其他存活副本 客户端可见抖动窗口 受控关闭(Controlled Shutdown) JVM Hook 接 SIGTERM ① 通知即将退出 ② 提前迁移该 Broker 的 Leader Leader 迁移到 ISR 存活副本 ③ 迁移完成回执 ④ 允许退出回执 ⑤ 本地收尾 → exit 减少分区抖动 对照:未提前交接的普通关闭 本地收尾 → exit 退出后由 Controller 发现 → 触发 Leader 重选 分区短暂不可用 = 客户端抖动 Broker Controller 存活副本 抖动窗口
图 3-5 Kafka 受控关闭与普通关闭对比

受控关闭要求 Controller 可达,且相关分区的同步副本集合(ISR)中有位于其他 Broker 的存活副本,能在期限内完成迁移。条件不满足或等待超时,Broker 就转入普通关闭。默认启用的受控路径增加退出前的协调时间,减少正常下线造成的分区抖动。

延伸:容器环境给三类软件增加的启停约束

部署到 Kubernetes(K8s)后,终止宽限期覆盖摘流(停止向实例分配新流量)、hook(终止钩子)和进程收尾;超时仍未退出,进程会被强制结束。Redis 的持久化、MySQL 的事务收尾、Kafka 的 Leader 交接所需时间不同,不能只按进程收到信号来判断关闭完成。摘流信息的传播需要时间;保存或交接则要看各自的完成信号,而不只是等待了多久。

启动也会受探针影响。如果把“数据库还在恢复”当成存活探针(liveness probe)检查失败,编排系统就可能反复重启它,恢复一次次被打断重来。配置启动探针(startup probe)后,存活探针和就绪探针(readiness probe)会等待它成功再开始执行,给启动恢复留出时间;随后由就绪探针判断实例是否具备接收流量的条件。

3.5 横向对比

表 3-2 把三家的就绪条件、关闭收尾与职责交接放在一起比较。

表 3-2 三个软件的启动与关闭

维度RedisMySQLKafka
开始处理请求的门槛按配置恢复内存键空间完成必要的 redo 恢复;未完成的回滚可在后台继续分区日志、元数据与授权等准备就绪
退出前的本地数据收尾已开启的 AOF 刷盘;是否生成本次 RDB 可选按档位刷数据页或 redo日志刷盘与本地收尾
可以省略或留到之后的工作NOSAVE 省略本次 RDB 快照完整清理可后移;档位 2 留待下次崩溃恢复普通关闭把 Leader 重选留到退出后
退出时的集群职责可等待落后副本追平;接管取决于部署方式角色接管取决于复制或集群方案,不由关闭档位决定受控关闭先协调 Leader 交接

3.6 架构启示

启示一:接受请求前,哪些状态必须就绪

进程已启动、端口已监听,都不足以说明请求能得到正确结果。反过来,要求所有后台清理完成后才接受请求,会无谓延长停服。划定就绪门槛的依据,是未完成的工作是否影响新请求的正确性。InnoDB 的必要 redo 恢复要在接受请求前完成;未完成的回滚可以留在后台,回滚事务持有的锁可能增加新请求的延迟。

启示二:退出要有可确认的完成条件

退出方案需要分别定义数据保存、在途请求处理和职责交接的完成条件,并约定等待超时后如何退出。Kafka 的受控关闭在具备可接替副本、交接按时完成时,能减少计划下线时的分区抖动;否则转入普通关闭。

启示三:快速关闭改变后续工作的安排

Redis 可以省略本次快照,MySQL 可以把清理留到运行期,Kafka 则可以把选主留到 Broker 退出后。它们缩短的是同一个关闭阶段,改变的保证和后续负担却不同。评估快速关闭的代价,需要明确省去的工作是否还要补做、由谁完成,以及会延长哪个阶段。

退出后能保住哪些数据,取决于持久化配置与退出流程。

3.7 小结

启动的门槛是必要状态已恢复。关闭时要确认必要的收尾工作已完成,并说明进程退出后,哪些集群职责还需要其他节点接手。把工作移到运行期、下次启动或其他节点,可以缩短本次关闭前的等待;方案也要说明后续工作由谁完成,以及超时会带来什么后果。