一套分阶段启动工具在基础设施就绪检查中反复超时。SSH 可连接,Docker 能响应,容器也显示 RUNNING,但 ActiveMQ 的 61616 与两个 Kafka broker 的 9092 未开放。排查最终落到了两个不同层面:ActiveMQ 的 KahaDB 恢复位置问题,以及 Kafka 启动失败与 ZooKeeper 从节点视图不一致。
**案例结论:**ActiveMQ 通过保留 journal、重建索引恢复;两个 Kafka broker 在仲裁就绪后重新启动,并修复了一个 ZooKeeper 从节点的旧数据视图。本文区分实测事实、合理推断和未确认事项,不将"启动成功"写成"所有历史消息均已证明无损"。
发布说明:系统名、容器名、IP、目录与日志位置标识均已替换为示例值;版本、端口、依赖关系、问题机制和验证计数保留。文中的命令是脱敏后的操作模板,执行前需替换路径并核对实际部署。
01|背景、目标与约束
启动器按"网络与数据库 → 缓存及基础中间件 → 消息队列 → 业务应用"的顺序执行。某个必需组件未就绪时,该阶段中断,业务启动门禁拒绝继续。这一设计避免了把依赖未就绪的业务容器一并拉起,但也意味着:日志停在 ActiveMQ,并不能证明排在它后面的组件正常。
| 环境与边界 | 本次案例 |
|---|---|
| 运行环境 | Linux + Docker 20.10.22;Windows 启动器通过 OpenSSH 远程执行命令 |
| 组件版本 | ActiveMQ 5.14.5、Kafka 2.0.0、ZooKeeper 服务端 3.4.12、Java 8 |
| 恢复目标 | 恢复真实中间件进程与协议服务;保留消息数据和回退材料;确认集群元数据一致 |
| 操作约束 | 优先只读检查;ActiveMQ 先备份与副本验证;Kafka 不重置消费位点,不清理消息分区;保留 ZooKeeper 健康多数派 |
| 明确不包含 | 未执行业务应用全链路验收;未证明事故前后逐条业务消息完整性;未修改启动器源代码 |
02|系统架构与故障边界

图 1 · 运维检查链、容器进程与中间件依赖边界
本案例有两种"容器仍运行、服务已退出"的形态。ActiveMQ 使用后台启动再跟随日志的命令;Kafka 组合容器则同时运行 ZooKeeper、Kafka 和占位进程。对这两种容器,Docker Running 只能说明容器内仍有进程,不能替代端口、协议或集群检查。
/opt/mq/activemq/bin/activemq start && tailf /opt/mq/log.log
这里 activemq start 将 broker 放到后台。Java 后续失败,不会让已经运行的 tailf 自动退出。Kafka 容器中,ZooKeeper 的 Java 存活也不代表 kafka.Kafka 存活。因此检查"有没有 Java"同样不够精确。
03|数据模型、关联关系与一致性

图 2 · KahaDB 文件关联、恢复位置与数据流
本次没有修改业务关系数据库,也没有业务表、商户或门店 ID 映射证据。KahaDB 是文件型消息存储,不能为了凑 ERD 而虚构业务表。这里用存储实体关联与 ID 映射说明恢复依赖。
| 对象 | 职责与关联 | 本次处理 |
|---|---|---|
| Destination / Message | 队列或主题组织消息;Message ID、内部序号与存储位置之间存在映射。发送、确认、事务记录不是简单的一消息一文件关系。 | 读取管理统计;未消费业务队列 |
| db-N.log | journal 文件;一个文件包含多条持久化操作记录,Location 使用文件 ID 与偏移定位。 | 不手工删除或截断 journal;由 broker 正常回放 |
| db.data | 页文件中的索引和恢复元数据,包括位置映射与恢复进度。 | 备份后移出旧文件,触发 journal 重放 |
| db.redo / db.free | 页文件恢复及空闲页相关辅助文件;需与对应页文件状态一起理解。 | 与旧 db.data 一并保留移出,避免混用新旧索引状态 |
| ZooKeeper /brokers/ids/N | broker 注册节点;数据包含地址,ephemeralOwner 标识拥有该临时节点的会话。 | 比较三个节点的数据与 owner;未手工删除注册节点 |
| ZooKeeper /controller | 当前 Kafka controller 相关元数据及其会话归属。 | 与 broker 注册视图一起进行一致性校验 |
KahaDB 将操作写入 journal,并维护索引、确认及事务等恢复状态;这些步骤不应被理解为跨所有配置都完全同步的单次写入。故障恢复需要同时解释有效日志、索引进度和未完成事务。备份时必须停止写入,否则分别复制 journal 与索引可能得到跨时刻的文件组合。官方 KahaDB 说明与5.14.5 恢复实现可供对照。
04|排查证据与处置时间线
| 阶段 | 观测与动作 | 结论 |
|---|---|---|
| 发现 | MySQL 曾端口超时,后续已通过;ActiveMQ 持续超时;两个 Kafka broker 未就绪。 | 不同组件必须分别验证,不能用一次重试概括全部问题。 |
| ActiveMQ 定位 | Java 已退出,tailf 存活;应用日志报告 KahaDB journal 恢复失败。 | 超时是表象,进程退出才是直接原因。 |
| ActiveMQ 备份与试验 | 约 7 GB 数据,442 个文件通过 SHA-256 校验;同镜像、禁用网络的副本重建索引。 | 副本回放 3,154,246 条操作,耗时约 74.5 秒。 |
| ActiveMQ 恢复 | 原索引移出后启动原服务,执行相同回放。 | 约 74.3 秒完成;临时队列收发、端口和工具检查通过。 |
| Kafka 恢复 | 确认 ZooKeeper 仲裁健康,仅启动 A/B 的 Kafka broker。 | 两者分别注册 broker 1、2 并监听 9092。 |
| 深层一致性检查 | C 从节点 zxid 与 A/B 相同,但返回旧 broker 数据及旧 owner。 | 角色和 zxid 检查不足以排除数据视图偏离。 |
| ZooKeeper 修复 | 备份 C 状态,移出其旧快照与事务日志,保留 myid,从 leader 获取新 SNAP。 | 三节点 broker/controller 读取视图一致。 |
| 最终验证 | 3 broker、47 Topic、96 分区;离线与副本不足分区均为 0。 | 中间件就绪检查通过;业务全链路验收仍是独立步骤。 |
以上顺序来自本次实际操作记录;备份和命令等待时间包含磁盘写入及连接等待,不能把 74 秒回放耗时当作整个故障停机时间。
05|ActiveMQ:Symptoms → Investigation → Root Cause
Symptoms|容器 RUNNING,但 61616 一直不通
容器已运行,保留:mq-broker
等待健康检查:mq-broker / ActiveMQ 监听端口
错误:健康检查超时:mq-broker / ActiveMQ 监听端口
Docker 标准输出只有启动脚本、Java 路径和 pidfile 创建记录,其中还包含历史启动日志。pidfile 创建仅证明启动脚本走到过某一步,不能证明 broker 此刻仍运行。
Investigation|从容器状态追到应用文件日志
docker top mq-broker -eo pid,ppid,comm
docker exec mq-broker sh -c 'tail -n 120 /opt/mq/activemq/data/activemq.log'
# 脱敏后的关键应用日志
Recovering from the journal @12001:18000000
Failed to start Apache ActiveMQ
java.io.IOException: Failed to recover data at position:12001:18000000
Apache ActiveMQ 5.14.5 is shutdown
进程表只有 shell 与 tailf,没有 broker Java。宿主机内存与磁盘空间当时充足,容器未显示 OOMKilled;这些证据不能排除所有存储问题,但不支持把当前故障直接归因于内存不足或磁盘满。按日志位置读取记录头时,抽样的 5 字节为全零。
Root Cause|旧恢复状态与有效日志遍历边界不一致
可以直接确认的是:沿既有恢复位置启动会失败;移出旧索引后,broker 能从 journal 重新回放并正常启动。这个对照支持"既有索引/恢复状态与可遍历日志不一致"的判断。全零记录头只是定位线索,也可能位于预分配或未完成记录区域,不能据此认定整个 journal 文件损坏。
**根因边界:**没有足够证据确认哪次异常退出、虚拟机快照或底层写入导致了这种状态;也没有证据证明磁盘硬件故障。对"恢复为何失败"和"这种状态最初如何产生"必须分别下结论。
06|ActiveMQ:Resolution → Verification → Lessons Learned

图 3 · ActiveMQ 备份、副本试验、原服务恢复与验证时序
Resolution|完整备份,再在隔离副本上重建索引
原服务先停止,数据、配置、容器信息与旧索引分别留档。磁盘上的完整备份不作为试验工作目录;隔离容器使用同一镜像、独立数据副本和 --network none,避免客户端接入。为减少额外磁盘写入,测试副本使用了空间足够的内存盘,磁盘备份仍保留。
# Bash 模板:示例名称和路径,请先替换并核对
set -eu
container=mq-broker
data=/srv/example/mq/activemq/data
recovery=/srv/example/recovery/amq-$(date +%Y%m%d-%H%M%S)
mkdir -p "$recovery"
chmod 700 "$recovery"
docker stop --time 30 "$container"
docker inspect "$container" > "$recovery/container-inspect.json"
docker cp "$container:/opt/mq/activemq/conf" "$recovery/conf-backup"
cp -a "$data" "$recovery/data-backup"
(cd "$data" && find . -type f -print0 | sort -z | xargs -0 sha256sum) > "$recovery/source.sha256"
(cd "$recovery/data-backup" && sha256sum -c "$recovery/source.sha256") > "$recovery/backup-verify.txt"
停止服务是为了建立同一时刻的可恢复文件集合。442 个文件校验通过,证明本次复制与源文件一致,而非证明数据在故障前就完整。docker inspect 可能包含环境变量等敏感内容,因此备份目录限制访问,文章中不发布完整 inspect。
# 已完成副本验证后,才对停止状态的原服务执行
# 下面的 data、recovery、container 沿用前一段变量
test "$(docker inspect -f '{{.State.Running}}' "$container")" = false
mkdir "$recovery/index-before"
for file in db.data db.redo db.free; do
if [ -f "$data/kahadb/$file" ]; then
mv "$data/kahadb/$file" "$recovery/index-before/$file"
fi
done
if [ -f "$data/activemq.pid" ]; then
mv "$data/activemq.pid" "$recovery/stale.pid"
fi
docker start "$container"
这一步移出可重新构建的索引状态,保留 journal,让恢复走重新遍历路径。它不是所有 KahaDB 故障的通用修复:若回放再次报错,必须停止并定位实际日志问题。本次未启用 ignoreMissingJournalfiles,未手工清空、截断或删除消息日志;broker 启动后的正常清理行为仍由自身管理。
Verification|四层证据,而非只看容器状态
| 层次 | 实际结果 | 能证明什么 |
|---|---|---|
| 恢复日志 | 副本和原服务均回放 3,154,246 条存储操作;新恢复日志未检出 ERROR、Dropped、Failed to recover。 | 两条恢复路径得到相同操作数,未观察到新的恢复错误。 |
| 进程与端口 | Java 持续存在;宿主机连接容器 IP:61616 成功。 | 进程和 TCP 服务可用。 |
| 协议与管理 | 临时队列发送随机标识并接收校验;JMX TotalMessageCount 为 2,279,与副本一致。 | 基本 JMS 收发正常,两个恢复实例的待处理统计一致。 |
| 运维工具 | 重新调用配置中的 ActiveMQ 健康检查,返回 True。 | 恢复结果与工具实际检查路径一致。 |
**不要混淆计数:**3,154,246 是 journal 存储操作数,2,279 是恢复后的待处理消息统计;两者不是同一个指标。临时队列测试未消费业务消息,也不证明断电后的持久化耐久性、业务幂等或历史消息零丢失。
Lessons Learned|让主进程退出可见,让应用日志可见
容器主进程与 broker 生命周期应绑定;前台运行、退出码传递、明确的 broker 进程检查,以及应用文件日志采集,应一起建设。本次仅完成服务恢复,这些启动器和镜像层改造属于后续工作。
07|Kafka:Symptoms → Investigation → Root Cause
Symptoms|两个容器活着,broker 已经退出
stream-a、stream-b 中的 ZooKeeper Java 仍运行,Kafka Java 均已退出。A 的历史日志出现注册节点所属会话不匹配;B 在启动时连接 ZooKeeper 超时。
# stream-a:历史注册冲突(标识已脱敏)
Error while creating ephemeral at /brokers/ids/1
node already exists and owner does not match current session
KeeperException$NodeExistsException
# stream-b:历史依赖连接超时
ZooKeeperClientTimeoutException:
Timed out waiting for connection while in state: CONNECTING
# 当时配置
zookeeper.connection.timeout.ms=6000
Investigation|先确认进程,再比较三节点实际视图
A、B、C 的 broker.id 分别为 1、2、3,未发现这三个配置重复。ZooKeeper 当前角色为一主两从,leader 显示两个同步 follower。但从 C 读取 broker 注册数据,仍返回旧时间戳和旧 ephemeralOwner;A、B 返回当前会话。即使三节点显示相同 zxid,实际读取视图仍不一致。

图 4 · ZooKeeper 多数派保留、从节点备份与重新同步
Root Cause|启动失败后没有自动恢复;从节点还存在旧视图
B 的直接退出原因是启动阶段连接超时;依赖就绪后它没有自行重新拉起。A 的直接退出原因是会话归属冲突;当前 C 仍残留旧注册视图,与该类问题的机制相符,但不能仅凭事后快照证明每一次历史 NodeExists 的唯一来源。ZooKeeper 从节点视图偏离多数派则是本次实测确认的独立一致性问题。
因此,单纯增加等待时间、重启整个组合容器或删除 broker 节点都不是本次首选。重启整个容器会同时重启 ZooKeeper;并行重启多个容器会削弱仲裁,而无差别删除注册节点可能影响仍在线的 broker。
08|Kafka/ZooKeeper:Resolution → Verification → Lessons Learned
Resolution|恢复 broker 与修复从节点分开执行
-
首先备份三个 broker 的配置、容器信息和启动前日志,确认健康 ZooKeeper 多数派。
-
只在 A、B 容器内启动 Kafka broker,不重启容器内的 ZooKeeper;两者重新注册,9092 开放。
-
确认 C 当前是 follower,A/B 能维持 2/3 仲裁后,只停止 C 的 ZooKeeper 进程;C 的 Kafka broker 持续运行。
-
完整备份 C 的 dataDir 与 dataLogDir 并做 SHA-256 校验;移出两个目录各自的 version-2 子目录,保留 myid 和配置。
-
重新启动 C 的 ZooKeeper,让它从健康 leader 获取 SNAP;保留旧状态目录,供后续分析和回退决策使用。
已确认 quorum 正常且目标 broker 不存在后,启动 broker
docker exec stream-a /bin/sh -c '/opt/mq/kafka/bin/kafka-server-start.sh -daemon /opt/mq/kafka/config/server.properties'
docker exec stream-b /bin/sh -c '/opt/mq/kafka/bin/kafka-server-start.sh -daemon /opt/mq/kafka/config/server.properties'C 重新同步后的关键日志(zxid 使用示例标识)
Getting a snapshot from leader 0x2a000000cc
Snapshotting: 0x2a000000cc
这里没有清空整个 ZooKeeper 集群,也没有把旧状态直接覆盖到健康 leader。仅修复一个从节点的本地状态,并保持多数派提供权威状态。快照与事务日志需要配套理解;只移走其中一类文件而保留另一类旧状态,会使恢复路径难以解释。相关机制参见ZooKeeper 管理指南。
Verification|端口之外,验证元数据与 ISR
| 验证项 | 结果 |
|---|---|
| broker 进程与注册 | A、B、C 各存在一个 kafka.Kafka 进程;注册 ID 为 1、2、3。 |
| TCP 与工具检查 | 三个 9092 均可连接;三个 Kafka 配置健康检查返回 True;ActiveMQ 检查仍为 True。 |
| ZooKeeper 读取一致性 | 分别连接三个节点,比较 /brokers/ids、各 broker 数据与 ephemeralOwner,以及 /controller 数据与 owner,3/3 一致。 |
| 仲裁状态 | leader 具有两个同步 follower,pending sync 为 0。 |
| Kafka AdminClient 只读检查 | 发现 3 broker、47 Topic(含内部 Topic)、96 分区;离线分区 0,ISR 数少于副本数的分区 0。 |
| 操作边界 | 未创建业务消息,未提交消费位点,未重置 group offset,未清理 Kafka 消息分区。 |
这次 AdminClient 验证是只读元数据检查,证明检查时的 broker、leader 和副本状态正常;它不替代生产者发送、业务消费者处理、端到端幂等或历史消息校验。Kafka 日志中的历史 LogCleaner checkpoint 告警另列为观察项,本次未手工修改消费位点或清理检查点文件。
Lessons Learned|角色、进度、数据视图都要看
ZooKeeper "leader/follower 正常"与"zxid 一样"提供的是不同维度的信息,都不能单独替代关键元数据读取校验。对于 broker 注册冲突,要同时回答:谁在运行、哪个会话持有节点、不同服务器返回的内容是否一致。
09|5 Whys:把机制与未知项分开
| 追问 | ActiveMQ 链路 | Kafka/ZooKeeper 链路 |
|---|---|---|
| 1. 为什么业务不能继续启动? | 61616 不可用,就绪门禁拒绝继续。 | 两个 9092 不可用,broker 未就绪。 |
| 2. 为什么端口不可用? | broker Java 在持久化恢复期间退出。 | broker 启动遇到连接超时或会话冲突后退出。 |
| 3. 为什么容器仍 RUNNING? | tailf 继续存活,未绑定 Java 生命周期。 | 组合容器中的 ZooKeeper 和占位进程仍存活。 |
| 4. 为什么重复点击仍不能恢复? | 容器运行被保留,既有恢复状态未改变。 | 未重启已退出的 broker;另有从节点旧视图问题。 |
| 5. 更深一层需要解决什么? | 恢复状态一致性、备份演练、主进程管理与文件日志采集。初始状态异常的触发事件仍未确认。 | 启动时序、进程级恢复和 ZooKeeper 数据视图校验。视图偏离最初如何产生仍需更早日志与存储历史。 |
10|Troubleshooting:本次验证中踩过的两个坑
坑一:journal 只读挂载,并不保证 broker 能做"只读恢复"
Symptoms: 诊断容器报 Read-only file system,并提示缺少 db.free。Investigation: 该试验只复制了部分页文件,并把旧 journal 通过只读路径链接进来。**Root Cause:**页文件辅助状态不完整;存储实现也可能以可写模式打开 journal,恢复过程并非纯读。
Resolution: 放弃此诊断路径,使用完整、可写但网络隔离的数据副本。Verification: 在完整副本上重放成功并通过收发测试。**Lessons Learned:**只读试验的报错属于试验环境限制,不能拿它当成原故障的新根因;备份与试验工作副本应分开。
坑二:JMX 本地模式可能查到的是 CLI 自己
Symptoms: 命令退出码为 0,却没有 broker 统计。Investigation: 原查询使用 --jmxlocal,没有明确连接 broker JVM。Root Cause: 命令执行成功,不等于查询对象正确。Resolution: 读取真实 broker PID,使用 --pid 查询。Verification: 输出 BrokerName 与 TotalMessageCount。**Lessons Learned:**管理命令必须验证结果字段,不能只验证退出码。
docker exec mq-broker sh -c '
pid=$(cat /opt/mq/activemq/data/activemq.pid)
/opt/mq/activemq/bin/activemq query --pid "$pid" -QBroker=localhost --view BrokerName,TotalMessageCount
'
# 预期字段:BrokerName = localhost;TotalMessageCount = 2279
11|方案取舍与回退边界
| 方案 | 本次选择与理由 |
|---|---|
| 延长健康检查超时 | 未作为恢复手段。已退出的进程不会因等待更久而恢复;应先辨别"仍在初始化"和"已退出"。 |
| 清空 KahaDB 或忽略 journal 错误 | 未采用。可能改变消息可恢复性,且本次已有保留 journal 的有效恢复路径。 |
| 重建 ActiveMQ 索引 | 采用。完整备份、同版本副本验证后执行,旧索引留档。 |
| 整组重启 Kafka 组合容器 | 未采用。ZooKeeper 与 Kafka 共容器,整组操作会扩大仲裁影响。 |
| 手工删除 broker 注册节点 | 未采用。优先核实真实进程和 owner,再修复偏离多数派的从节点。 |
| 修复单个 ZooKeeper follower | 采用。保留 A/B 仲裁,备份 C 状态,重新获取 leader 快照,逐项验证数据视图。 |
ActiveMQ 若验证失败,应先停 broker 并保留失败后的现场。只有确认没有新业务写入、并评估一致性后,才能决定恢复整套备份;不能在 journal 已变化时只把旧 db.data 覆盖回去。ZooKeeper follower 若无法重新加入,同样应保持多数派运行、隔离失败节点并保留新旧状态,不应盲目恢复旧状态后参与仲裁。
12|后续优化与完成标准
| 优先级 | 后续动作 | 完成标准 |
|---|---|---|
| P0 | 将容器生命周期与真实 broker 生命周期绑定。 | broker 异常退出可观测、可告警;不会被 tailf 或其他 JVM 掩盖。 |
| P0 | 启动器在失败时采集应用日志、目标进程和端口状态。 | 一次失败记录能解释退出原因,而非只留下"90 秒超时"。 |
| P1 | Kafka 启动前检查 ZooKeeper 仲裁与会话建立,再启动 broker。 | 依赖未就绪可有限重试;避免重启整个组合容器破坏仲裁。 |
| P1 | 增加 ZooKeeper 关键节点多视图核对与一致性告警。 | 比较注册数据、owner 和 controller;不能只比较角色或 zxid。 |
| P1 | 演练消息存储备份、恢复与业务侧对账。 | 具备可恢复备份、校验清单、恢复耗时记录和逐条消息/幂等验收方案。 |
| P2 | 观察 LogCleaner checkpoint 告警及压缩进度。 | 确认告警是否持续、压缩线程是否前进,再决定是否需要独立修复。 |
| P2 | 评估旧版本组件的升级与部署拆分。 | 在隔离环境验证协议、存储格式、复制行为和回退;本次未直接升级生产组件。 |
13|关键实现:可复用的验证与恢复片段
A. ZooKeeper 单从节点重新同步模板
下面对应本次 C 从节点的处理方式。前提是 A/B 已核实健康且足以维持仲裁,C 确认为 follower,且 dataDir、dataLogDir 已根据配置和挂载逐一核对。脚本中的两类路径是示例值,不能照搬到其他节点。
set -eu
node=stream-c
zk_data=/srv/example/stream/zk-c/data
zk_log=/srv/example/stream/zk-c/log
state=/srv/example/recovery/zk-c-$(date +%Y%m%d-%H%M%S)
mkdir -p "$state"
chmod 700 "$state"
# 必须先确认健康多数派与目标角色
for peer in stream-a stream-b; do
docker exec "$peer" /opt/mq/zookeeper/bin/zkServer.sh status 2>&1 | grep -E 'Mode: (leader|follower)'
done
docker exec "$node" /opt/mq/zookeeper/bin/zkServer.sh status 2>&1 | grep 'Mode: follower'
docker exec "$node" /opt/mq/zookeeper/bin/zkServer.sh stop
sleep 2
if docker exec "$node" sh -c "ps -ef | grep -q '[o]rg.apache.zookeeper.server.quorum.QuorumPeerMain'"; then
echo 'ZooKeeper has not stopped'; exit 1
fi
docker exec "$node" sh -c "ps -ef | grep -q '[k]afka.Kafka'"
cp -a "$zk_data" "$state/data-backup"
cp -a "$zk_log" "$state/log-backup"
(cd "$zk_data" && find . -type f -print0 | sort -z | xargs -0 sha256sum) > "$state/data.sha256"
(cd "$state/data-backup" && sha256sum -c "$state/data.sha256")
(cd "$zk_log" && find . -type f -print0 | sort -z | xargs -0 sha256sum) > "$state/log.sha256"
(cd "$state/log-backup" && sha256sum -c "$state/log.sha256")
# 保留 myid、配置与旧状态;只移出目标从节点的 version-2
mkdir "$state/old-state"
mv "$zk_data/version-2" "$state/old-state/data-version-2"
mv "$zk_log/version-2" "$state/old-state/log-version-2"
docker exec "$node" /opt/mq/zookeeper/bin/zkServer.sh start
# 接着验证 follower、同步数、实际 broker/controller 视图与 Kafka 分区状态
此脚本不会替代人工确认"哪两个节点构成可信多数派"。若停止或校验阶段失败,应停在当前步骤保留证据;若启动后无法重新加入,应保持健康多数派,按前述回退边界处理,而不是继续重置其他成员。
B. Kafka/ZooKeeper 只读验证入口
# 在能够访问集群的容器内执行;地址均为示例值
for endpoint in 10.20.0.11:2181 10.20.0.12:2181 10.20.0.13:2181; do
printf 'ls /brokers/ids\nget /brokers/ids/1\nget /brokers/ids/2\nget /brokers/ids/3\nget /controller\nquit\n' | /opt/mq/zookeeper/bin/zkCli.sh -server "$endpoint"
done
# 比较三个输出中的节点列表、注册数据、ephemeralOwner 和 controller。
/opt/mq/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server 10.20.0.11:9092
/opt/mq/kafka/bin/kafka-topics.sh --zookeeper 10.20.0.11:2181 --describe --unavailable-partitions
/opt/mq/kafka/bin/kafka-topics.sh --zookeeper 10.20.0.11:2181 --describe --under-replicated-partitions
上述 CLI 是便于复现的人工检查入口。本次最终验收使用程序同步读取三个 ZooKeeper 视图,并用 Kafka AdminClient 统计分区;不能仅以 CLI 的退出码推定"无离线分区",还需要检查实际输出。
C. ActiveMQ 临时队列收发探针
import javax.jms.*;
import org.apache.activemq.ActiveMQConnectionFactory;
public class RecoveryProbe {
public static void main(String[] args) throws Exception {
Connection c = new ActiveMQConnectionFactory(
"tcp://127.0.0.1:61616").createConnection();
try {
c.start();
Session s = c.createSession(false, Session.AUTO_ACKNOWLEDGE);
TemporaryQueue q = s.createTemporaryQueue();
MessageConsumer reader = s.createConsumer(q);
MessageProducer writer = s.createProducer(q);
String token = "probe-" + java.util.UUID.randomUUID();
writer.send(s.createTextMessage(token));
Message result = reader.receive(5000);
if (!(result instanceof TextMessage) ||
!token.equals(((TextMessage) result).getText())) {
throw new java.lang.IllegalStateException("probe failed");
}
reader.close(); writer.close(); q.delete(); s.close();
System.out.println("JMS_SEND_RECEIVE_OK");
} finally { c.close(); }
}
}
# 探针文件已放入目标容器 /tmp,使用实际 JAVA_HOME
"$JAVA_HOME/bin/javac" -cp '/opt/mq/activemq/lib/*' /tmp/RecoveryProbe.java
timeout 20 "$JAVA_HOME/bin/java" -cp '/tmp:/opt/mq/activemq/lib/*' RecoveryProbe
探针使用独立临时队列,只接收自己发送的随机标识;连接关闭后释放临时资源。它验证的是当前连接与基本 JMS 收发,不是业务队列消费或跨重启持久化验证。
14|总结与复盘结论
这次恢复的关键是把四个状态分开判断:容器是否运行、目标进程是否存活、协议服务是否可用、集群元数据是否一致。ActiveMQ 通过完整备份与隔离副本把索引重建变成可验证的恢复动作;Kafka 通过保留 ZooKeeper 多数派、单独恢复 broker、重同步偏离的 follower,避免把局部故障扩大成整组不可用。
已完成的是中间件进程、端口、基本协议和集群元数据恢复。没有证据的历史触发事件、业务端到端一致性和逐条消息完整性,保留为后续验证事项。这样的边界使复盘能迁移到类似场景,而不把一次有效操作包装成无条件通用修复。
参考资料与证据口径
- Apache ActiveMQ:KahaDB 配置与持久化说明。具体恢复判断同时对照了本案例使用版本的实现。
- Apache ActiveMQ 5.14.5:MessageDatabase 恢复实现。
- Apache ZooKeeper Administrator's Guide:快照、事务日志、仲裁和管理命令。机制参考不等同于断言本次触发了某个特定版本缺陷。
- 案例事实来自实际启动输出、进程表、应用文件日志、校验清单、JMS/JMX 验证与 Kafka AdminClient 只读检查。对外仅保留脱敏摘录,不公开原始凭据、环境变量和业务 Topic 名。