diff --git a/docs/SKILL-INDEX.md b/docs/SKILL-INDEX.md index 5054847..7df5f9c 100644 --- a/docs/SKILL-INDEX.md +++ b/docs/SKILL-INDEX.md @@ -48,6 +48,7 @@ - [`addon-resource-rich-cluster-parallel-test-guide.md`](addon-resource-rich-cluster-parallel-test-guide.md) — idc / idc1 / idc2 / idc4 这类资源充足集群上的并行测试调度方法:先测容量、按 suite lane 分批、逐步提升 N、namespace / evidence / cleanup 隔离、把环境压力和产品失败分开报告;KBE / KB runtime 在这些 IDC 环境中必须以 vcluster 为 SUT,host k8s 只承载 runner / helper / bootstrap;确认环境问题后通过 @Musk3 找飞书用户李国银;配套 skill `parallel-test-execution` - [`addon-multi-ns-registry-scan-preflight-guide.md`](addon-multi-ns-registry-scan-preflight-guide.md) — 多 namespace / 多 topology 并发测试或 chaos suite 启动前把测试 scope 拆成 **verified scope vs scan-only future-gate** 两层:本轮跑过的 topology 写 verified 结论;未跑但 pre-flight scan 命中 docker.io 漏点的 topology 写 future-gate precondition。具体 application 是审计 live `ComponentVersion` + `ParametersDefinition.toolsSetup.toolConfigs[].image` 的 image source 一致性。N=2 MySQL grounded(task #5 functional 多 ns 并发 + task #6 chaos vcluster pre-patch) - [`addon-soak-test-result-classification-guide.md`](addon-soak-test-result-classification-guide.md) — 长跑型测试(24h+ soak / chaos / fault-injection)出结果之后的 4-state 结果分类方法论:`invariant-break` / `product-path-failure` / `harness-race` / `external-environmental-cascade`。Q1-Q5 决策树(mermaid)+ N≥2 自验证最小证据门槛(harness-race / cascade 必须有同类 Succeed 或 baseline ±1σ 对照样本)+ ACCEPTED 判据(`invariant-break = 0 AND product-path-failure = 0`,**不是** fault total = 0)+ N=3/2 grounded 案例对照(CH30 harness-race + CH20 external-env-cascade)+ AG quorum non-sticky 行为附注。与 [`addon-test-acceptance-and-first-blocker-guide.md`](addon-test-acceptance-and-first-blocker-guide.md) 单次 fail 分层方法论互补(前者多次注入聚合维度) +- [`addon-chaos-pod-kill-vs-engine-internal-crash-guide.md`](addon-chaos-pod-kill-vs-engine-internal-crash-guide.md) — 同一引擎 "崩溃"分三种形态(K8s 层 pod-kill / 引擎 instance critical 进程崩溃 / 引擎 broker 辅助进程崩溃),三类走完全不同的恢复路径并可能触发完全不同的 failover 行为,必须分别测。文档给三轴 chaos 矩阵 + B 类下再分 instance / broker(master, worker) / listener 三子轴 + burn-in(同位置同类故障 ≥3 次) + 位置依赖性矩阵(primary vs active FSFO target vs non-target standby)+ 跨引擎适配 checklist + 反模式表;Oracle 案例见 `cases/oracle/oracle-chaos-pod-kill-vs-smon-kill-fsfo-asymmetry-case.md` ### 3. 环境 ready 前 / 环境层撞坑 @@ -166,6 +167,7 @@ - [`docs/addon-pr-reviewer-routing-guide.md`](addon-pr-reviewer-routing-guide.md) — addon 代码 PR reviewer routing 方法论:先查当前 PR touched files、初版 PR、addon 目录主要作者、最近相关功能作者,再按"最近功能维护者 → 初版 / 主要作者 → addon owner → fallback reviewer → 带证据问 Weston"选择 reviewer。包含 @Musk3 安全转发格式、Kevin 汇总表、Oracle PR #1276 / PR #764 反例,配套 callable skill `addon-pr-reviewer-routing` - [`docs/addon-github-submission-discipline-guide.md`](addon-github-submission-discipline-guide.md) — 多 agent 协作 + GitHub 公开仓库的边界纪律:(1) AI provenance trailer(`Co-Authored-By: Claude` / `🤖 Generated with` / `noreply@anthropic.com`)不外漏的硬规则与兜底命令链(heredoc + `git commit --amend -m "$(... | sed)"` strip / push 前 grep 自检);(2) 多 agent 并发推同一 PR branch 的 cascade 事故响应 playbook(force-with-lease lemma:lease 锚 last-fetched remote tip 不防 fetch 后并发 push / 双向 `git log --oneline` ritual / dropped-commit owner self-recover / single-owner-execute 收口)。5 条 doctrine(A force-with-lease / B per-commit grep / C cascade single-owner-execute / D forensic 自查 / E content-delta verify)+ §5 cross-cutting rules(forensic self-review / Doctrine E shorthand / evidence-post obligation / 递归 self-application) - [`docs/addon-soak-test-result-classification-guide.md`](addon-soak-test-result-classification-guide.md) — 长跑型测试(24h+ soak / chaos / fault-injection)出结果之后的结果分类方法论。**核心 framing**:fault total / PASS-FAIL 计数无法回答"是否 ACCEPTED",必须把每条 fault 注入按"哪一层先失败"落到 4-state schema:(1) `invariant-break`(不变量破坏 → ROLLBACK)/ (2) `product-path-failure`(产品恢复路径失败)/ (3) `harness-race`(测试工具时序竞争)/ (4) `external-environmental-cascade`(外部环境级联)。**判据**:Q1(bad_ack > 0)→ Q2(cluster 终态)→ Q3(OpsRequest Failed + N≥2 自验证)→ Q4(duration 超 mean+3σ + 外部事件关联 + 对照样本)→ product-pass(mermaid 流程图)。**N≥2 自验证最小证据门槛**:harness-race 需同类 Succeed 对照、cascade 需 baseline ±1σ 对照样本;单 sample 不能下"非产品"结论。**ACCEPTED 判据**:`invariant-break = 0 AND product-path-failure = 0`,harness-race / cascade 不阻塞但触发对应修复 ticket。grounded N=3 CH30 harness-race 对照(fault-026 vs 029/033)+ N=2 CH20 cascade negative-control(fault-028 21min outlier vs 031 95s 1σ 内)+ AG quorum non-sticky 3-transition 行为附注。与 [`addon-test-acceptance-and-first-blocker-guide.md`](addon-test-acceptance-and-first-blocker-guide.md) 单次 fail first-blocker 分层方法论形成互补对子(前者聚合维度,后者单次维度) +- [`docs/addon-chaos-pod-kill-vs-engine-internal-crash-guide.md`](addon-chaos-pod-kill-vs-engine-internal-crash-guide.md) — Addon chaos test 设计原则:同一 DB 实例的"崩溃"有三种**完全不同**的故障形态 — (A) K8s 层 pod-kill / 节点宕机;(B-instance) 引擎 critical 进程崩溃(Oracle SMON/LGWR / PG postmaster),引擎自行终止 instance;(B-broker) 仅 HA / 复制 / 监控相关进程崩溃(Oracle DMON/INSV/NSV* / MySQL semi-sync 线程),引擎悄悄重启该进程,instance / 容器不动。三类**走完全不同的恢复路径**并**可能触发完全不同的 failover 行为**,一类测了不等于测了另一类。文档给:双轴 / 三轴 chaos 矩阵、B 类下 instance vs broker(master, worker) vs listener 三子轴、burn-in 方法学(≥3 cycle / poll point 解耦 / topology rotation)、FSFO bimodal probabilism 段、位置依赖性矩阵(primary vs active FSFO target vs non-target standby)、跨引擎适配 checklist(含 listener watchdog 覆盖)、10 条反模式(含"single-shot ≠ verified" / "broker SUCCESS ≠ cluster healthy" / "不要从被杀成员 poll broker")。Oracle 19c grounded:rounds 1–16 见 `cases/oracle/oracle-chaos-pod-kill-vs-smon-kill-fsfo-asymmetry-case.md` ## 案例材料 @@ -197,6 +199,7 @@ - [`docs/cases/oracle/oracle-chart-vs-kb-schema-skew-multi-stage-case.md`](cases/oracle/oracle-chart-vs-kb-schema-skew-multi-stage-case.md) — Oracle 1.0.0-alpha.0 chart 在已发布 KB 装不上的三段反转:先误判「整代代差」、再误判「chart 字段路径错位」、最后查清是「chart 跟 KB main 上未发布 API(PR #10100 / #10109)」。同仓 `release-1.0` 分支才是答案。属 [`addon-chart-vs-kb-schema-skew-diagnosis-guide.md`](addon-chart-vs-kb-schema-skew-diagnosis-guide.md) 的工程现场补充 - [`docs/cases/oracle/oracle-12c-post-switchover-probe-cascade-kill.md`](cases/oracle/oracle-12c-post-switchover-probe-cascade-kill.md) — reconfigure_deep Run 1→3 闭环:Bug #12 (DBCA 跑期间 liveness 误杀,initialDelay=600 + 90s 重启窗口) + Bug #13 (post-switchover 慢控制面 flap readiness);3 layer fix(cmpd probe 参数 + liveness.sh 软失败 + checkDBStatus.sh best-effort dgmgrl);Run 3 全 PASS + RESTARTS=0 实证;属 [`addon-probe-timeout-and-soft-failure-guide.md`](addon-probe-timeout-and-soft-failure-guide.md) 工程现场补充 - [`docs/cases/oracle/oracle-12c-processes-cue-paramdef-range-case.md`](cases/oracle/oracle-12c-processes-cue-paramdef-range-case.md) — reconfigure_deep T22d FAIL:`processes: int & >=6` cue 太宽,10 通过 ValidatePhase → ORA-603/1092 → instance terminated → KB OpsRequest 卡 Running 25min+;fix `>=100` Run 3 验证生效(ValidatePhase reject within 10s);属 [`addon-paramdef-cue-range-validation-guide.md`](addon-paramdef-cue-range-validation-guide.md) 工程现场补充 +- [`docs/cases/oracle/oracle-chaos-pod-kill-vs-smon-kill-fsfo-asymmetry-case.md`](cases/oracle/oracle-chaos-pod-kill-vs-smon-kill-fsfo-asymmetry-case.md) — Oracle 19c grounded chaos 案例库(`o19p15v9` cluster, MaxPerformance + ASYNC + FSFO threshold 30s):rounds 1-16 完整记录三轴 chaos(A pod-kill / B-instance SMON+LGWR / B-broker DMON+INSV / B-listener tnslsnr) × 三位置(primary / active FSFO target / non-target standby)+ burn-in(round 13)+ A+B-instance concurrent race (round 8)。**关键 finding**:(1) A vs B-instance 在 primary 上行为根本不同(A 走 pod recreate 自愈, B 走 FSFO failover),(2) FSFO trigger 是 bimodal probabilism(watchdog tick 相位 vs FSFO threshold 决定 fire 还是 self-recover),(3) F39 — `runOracle.sh` watchdog blind to tnslsnr loss → listener 死后 broker SUCCESS + observer 心跳正常 + 新客户连接全部 ORA-12541 → 无 watchdog 不自愈(Sev-1,PR #1320),(4) F38 — `setup_observer.sh` 无 timeout 等 instance OPEN + hardcode `${ORACLE_SID}_0`,primary 永久不恢复时 observer 永远起不来。属 [`addon-chaos-pod-kill-vs-engine-internal-crash-guide.md`](addon-chaos-pod-kill-vs-engine-internal-crash-guide.md) 工程现场补充 ### OceanBase diff --git a/docs/addon-chaos-pod-kill-vs-engine-internal-crash-guide.md b/docs/addon-chaos-pod-kill-vs-engine-internal-crash-guide.md new file mode 100644 index 0000000..2422aaa --- /dev/null +++ b/docs/addon-chaos-pod-kill-vs-engine-internal-crash-guide.md @@ -0,0 +1,260 @@ +# Addon Chaos:Pod-Kill vs 引擎内部 Crash 双轴覆盖指南 + +> **Audience**: addon dev / test / TL,正在设计 chaos 测试矩阵或评估现有 chaos 覆盖完整性 +> **Status**: stable +> **Applies to**: any KB addon with HA/failover engine(MySQL / Oracle / PostgreSQL / Redis / MongoDB 等) +> **Applies to KB version**: any +> **Affected by version skew**: yes — 方法学(双轴 / 三轴 + 矩阵)跨版本稳定,但具体观测结论与时序受多层版本影响:engine 版本、HA 模式(MaxPerf / MaxAvail)、observer / broker config、container 内 watchdog 实现、K8s 重启路径、KB sidecar 行为 都会改变 FSFO 触发概率与恢复时长。引用具体案例时必须把这些 envelope 字段一并落定,不能直接把"我环境 N=1 观测到"当成"任何环境一样" + +## 先用白话理解这篇文档 + +很多 chaos 测试默认 "杀 pod" 就等于 "测试了节点故障"。其实远远不够。 + +同一个 DB 实例的"崩溃"有三种很不一样的形态: + +1. **K8s 层**:`kubectl delete pod --force --grace-period=0`、节点宕机、Pod evict。Pod 整个消失,K8s 重建。 +2. **引擎内部 — instance critical**:DB 关键后台进程崩溃(Oracle SMON/LGWR、PostgreSQL postmaster 等),引擎自己判断"这进程死了我整个 instance 不能继续",触发 instance 终止。Pod 还在,容器还重启了,但 DB instance 死过一次。 +3. **引擎内部 — broker / 辅助守护进程**:仅与 HA 协议、复制协议、监控相关的进程崩溃(Oracle DMON/INSV/NSV*、MySQL semi-sync 插件线程等)。引擎**自己悄悄重启该进程**,instance 不停,容器不重启,外部完全看不见。 + +这三种故障在生产里都会出现,但它们**走完全不同的恢复路径**,因此**也可能触发完全不同的 failover 行为**。一个测了不代表测了另一个。 + +本文给出一组明确的口径,让你在 chaos 矩阵里把这三类故障分别显式覆盖。 + +## 适用场景 + +当你在做下面任一件事时,本文适用: + +- 设计或扩展一个新 addon 的 chaos 测试矩阵。 +- 评估 ship-readiness 时怀疑现有 chaos 覆盖偏一边。 +- 现场观察到生产里有 failover 现象,但本地 chaos 没复现 — 排查是不是测错了轴。 +- 别的 team 跑通了某种 chaos 你跑不通(或反过来)— 检查是不是两人测的不是同一个轴。 + +## 核心边界:三类各自的恢复路径 + +### 轴 A — K8s 层 pod kill + +``` +kubectl delete pod ... --force --grace-period=0 + ↓ +Pod 立即从 API server 摘除 + ↓ +StatefulSet controller 立即 schedule 新 pod + ↓ +init container → main container 启动 → 引擎 bootstrap → 引擎主进程 ready +``` + +**特征**: +- 引擎进程被 SIGKILL(来自 kubelet 销毁容器)— 没有 PMON 清理、没有 alert log 记录"why I died" +- 网络连接对外**直接断开**(TCP RST 或 timeout)— 远端 standby / observer 看到 connection refused +- Pod 重建可能耗时数十秒到几分钟(PVC mount、image cache、bootstrap 逻辑) +- kbagent / sidecar 容器**也跟着重启** — 任何依赖 sidecar 启动期间临时做的事(如 symlink)都会重新做或丢 + +### 轴 B-instance — 引擎内部 critical 后台进程 crash(instance 终止) + +``` +kubectl exec -c -- kill -9 + ↓ +引擎内部检测到关键进程死,判断 "instance 不能继续" + ↓ +引擎自我终止 instance + 写 alert log + ↓ +容器 PID 1 watchdog 检测引擎死,exit + ↓ +kubelet restart 容器(同一 pod 内) + ↓ +新引擎 bootstrap +``` + +**特征**: +- 引擎有时间**写 alert log** 解释为什么死 — 留下证据 +- 网络连接**可能短暂保持**(listener 进程独立存活,但服务无响应)— 远端 standby / observer 看到 ORA-1034 类的"service unavailable"而不是 connection refused +- **只有 main 容器重启**,sidecar 容器(kbagent / exporter)保持原状 — sidecar 内的 ephemeral 文件保留 +- 整体恢复通常**更快**(共享 PVC 不用重 mount、网络栈不重建) +- **FSFO / failover 通常会被触发** — standby 看到的是干净的"primary 死了但 standby 还健康" + +典型例子:Oracle SMON / LGWR、PostgreSQL postmaster、MySQL InnoDB main thread。判断标准:**引擎内核把这个进程视为 instance-critical**,进程一死整个 instance 就要终止。 + +### 轴 B-broker — 引擎内部 broker / 辅助守护进程 crash(instance 不动) + +``` +kubectl exec -c -- kill -9 + ↓ +引擎内部检测到该进程死 + ↓ +引擎 spawn 一个新的同名进程(pid 变了,instance 没动) + ↓ +关联辅助进程(如 Oracle DMON 的 INSV / NSV* / RSM0 / FSFP)逐步重起 + ↓ +broker / 协议层短暂 ERROR,几十秒内回到 SUCCESS +``` + +**特征**: +- **Instance 完全不停**,main 容器**不重启**,K8s 层完全感知不到 +- 网络与读写流量**不中断** +- broker / 协议层有一个"短暂 ERROR 窗口"(典型 10-30s),期间 failover 决策可能临时不可用 +- **FSFO 不会触发** — 因为 observer 对 primary instance 的连通性检查仍然正常返回 +- 恢复时间是三类里最短的(典型 20-30 秒,全部发生在引擎内部) + +典型例子:Oracle DMON / INSV / NSV* / RSM0 / FSFP / LREG,MySQL semi-sync ACK 线程、复制协议线程。判断标准:**进程死后引擎只是 spawn 一个新的同名进程**,instance 不动。 + +**重点**:B-broker 测试的不是"引擎能不能恢复 instance"——instance 根本没倒;而是**"协议层短暂中断期间,依赖该协议的外部组件(observer、监控、自动 failover 决策器)会不会误判"**。 + +## 为什么 failover 行为会不同 + +Failover 决策的依据是 **standby / observer 怎么"看到"primary 失联**: + +- 轴 A — TCP 直接断开:standby/observer 可能立刻判定"primary 不可达",但同时也判定"我已经 out-of-sync"(因为我们用 async transport 时 standby 没收到最后一秒的 redo)。某些保护模式下,standby 会进入 SUSPEND 状态以防止数据丢失 promotion。**FSFO 可能因 standby SUSPEND 而暂停**。 +- 轴 B-instance — 引擎崩溃但 listener 还活着:standby/observer 看到 service-level 错误("instance not available")但 TCP 通路还在;standby 自己最后一次 redo apply 可能刚好接近 synced;observer 看到的是干净的"primary 死了,但 standby 还是 healthy"的状态。**FSFO 触发的几率更高**。 +- 轴 B-broker — instance 完全没倒,只有 broker 协议层短暂不可用:observer 对 primary 的连通性检查仍然正常返回;只有 broker 配置查询会瞬间返回 ERROR。**FSFO 不该触发**——这是正确的;broker 协议短暂 ERROR 不能等同于 primary 失联。 +- 轴 B-listener — instance + broker daemon 都正常,只有**对外服务监听器 (listener / TNS)** 死了。这是真正的"静默故障"轴:所有现存 TCP 长连接(broker peer-to-peer / redo transport / observer ↔ instance)继续工作,但**新连接**被拒。broker 在 observation window 里**完全报告 SUCCESS**;alert log 不写一行;watchdog(如果只 pgrep 进程监控引擎主进程)不触发。这一类故障比 B-broker 还隐蔽——B-broker 至少在重启 broker 协议时有一窗口的 ERROR;B-listener 是永久 SUCCESS-but-broken。 + +**这不是 bug 也不是 feature — 是不同故障模式天然的不同信号** — 只是 chaos test 必须把这三类故障显式测,否则你只验证了一类信号下的 failover。 + +特别要点:**"broker 协议 ERROR" ≠ "primary 不可达"**。如果你的 failover 决策器把 broker 协议查询失败也当作 primary 失联触发,那它在 B-broker 类故障下会**误触发 failover**——这是 chaos 矩阵能暴露的设计缺陷,必须显式测。 + +## 测试矩阵建议 + +最小覆盖矩阵(不分引擎通用): + +| # | 类 | 故障注入 | 期望验证 | +|---|---|---|---| +| 1 | A | primary pod kill | engine self-recovery / FSFO 行为 / 写可用性 | +| 2 | A | standby pod kill (非 failover 目标) | standby 自愈,primary 不受影响 | +| 3 | A | FSFO target pod kill | target auto-shift / 失联期间 FSFO blind window | +| 4 | A | 全部 standby 同时 kill | primary 写可用性(async/sync 模式差异) | +| 5 | A | observer (or failover decision-maker) pod kill | observer 自动 reconnect、FSFO blind window | +| 6 | **B-instance** | **primary 内部 instance-critical 进程 kill** | **engine self-restart watchdog 工作 / FSFO 触发(与 #1 对比)** | +| 7 | B-instance | standby 内部 instance-critical 进程 kill | standby self-restart,primary 不受影响 | +| 8 | **B-broker** | **primary broker / 协议层进程 kill** | **引擎内部自动重起 broker,instance 不动,FSFO 不该触发** | +| 9 | A+B-instance | observer kill + primary instance-critical 进程 kill | self-recovery vs observer-restart 谁赢的 race | +| 10 | A | rolling kill across pods | 序列失败下的稳定性 | +| 11 | **B-listener** | **任意位置 listener (tnslsnr / port-listener) 杀死** | **silent failure 检测:watchdog 是否覆盖、broker 是否察觉、新连接是否失败、有没有自愈** | + +**核心契约**: 至少 #1 (A) + #6 (B-instance) + #8 (B-broker) + #11 (B-listener) 四类都必须做,并显式比较它们的 FSFO 行为以及 watchdog 覆盖。仅做 #1 = chaos 覆盖不完整。仅做 #1 + #6 = 漏掉了"broker 短暂 ERROR ≠ primary 失联"的判定缺陷暴露面。漏 #11 = 永远不知道你的 watchdog 有没有 listener 盲区。 + +### FSFO 决策的三个独立维度 + +测试结果除"是否 fire FSFO"外,还应该分别记录: + +1. **角色变更 (role change)** — primary 是否变了。只有 #1/#6 类故障可能触发。 +2. **FSFO target shift** — active 待命 target 是否被 observer 自动切到另一个 standby。"target 失联但 primary 健康"时会触发,与 FSFO fire 互斥。 +3. **broker config 状态轨迹** — SUCCESS → ERROR → WARNING → SUCCESS 各阶段时长。短暂 ERROR 不算缺陷,长时间 ERROR 才需要分析。 + +target shift 是 #3 (A on target) 和 #6 标记为 "在 active target standby 上做"时都可能出现的中间状态,但本身**不构成 failover**。把它和 FSFO fire 分开记录,避免误报为 failover。 + +### B-instance 的位置依赖性(必测三个位置) + +同一个 kill 动作在不同位置上产生**完全不同**的 cluster 级结果,因此 "B-instance 测过了" 不等于 "B-instance 全部行为测过了"。最少需要测三个位置: + +| 位置 | FSFO fire | role change | target shift | +|---|---|---|---| +| primary | YES | YES | n/a | +| 活动 FSFO target 上的 standby | NO | NO | YES(observer 切到另一个 standby) | +| 非 target standby | NO | NO | NO | + +这三个位置的恢复路径在引擎内部是一样的(PMON 检测 → instance 终止 → watchdog → ctr restart → bootstrap),但 observer / failover 决策器对每一种情况的响应完全不同。chaos 矩阵必须分别覆盖。 + +### B-instance 在 primary 上 FSFO 触发的概率性 + +同一种 kill 在同一个 cluster 上重复跑,**FSFO 不一定每次都 fire**。是否 fire 取决于三个变量构成的赛跑: + +1. **failover decision-maker(observer / FSFO 控制器)的 threshold** — 多久判定 "primary 失联"。例如 30s。 +2. **engine self-recovery 总时间** — 从 instance terminated 到 instance OPEN 的总时长,包括 watchdog tick + ctr restart + bootstrap。 +3. **watchdog 检测 tick 的相位** — kill 发生在 watchdog tick 周期的哪个位置;最坏情况要等接近一整个 tick interval 才被发现。 + +只要 self-recovery 总时间 < threshold,FSFO 就不 fire;反过来就 fire。同一 cluster 不同次跑的差异主要来自: + +- **镜像 cache 状态**:cold image 时 ctr restart 拉镜像可能多 20-40s +- **watchdog tick 相位**:kill 时刚错过 tick → 需要等一整个 tick interval 才被检测 +- **存储 mount / PVC 重新挂载耗时**:偶发抖动 10s 级别 + +实测在 30s threshold + 30s watchdog tick 配置下,同一 SMON kill 重复 N 次会出现 bimodal 分布:约 1/3 次走 self-recovery(FSFO 不 fire),约 2/3 次走 FSFO 路径。 + +这不是"测试不稳定",而是设计层面的真实属性。结论: + +- **不要把 "kill primary 必然导致 failover" 写进运维 SOP**——这条契约不可靠 +- **如果要 "always failover" 行为**:让 self-recovery 总时间 > threshold(拉长 watchdog tick 或缩短 threshold) +- **如果要 "prefer self-recovery if fast"**:让 self-recovery 总时间 < threshold(默认 30s/30s 倾向这一侧,但是不确定) +- **burn-in 必须显式覆盖 ≥3 次 same-class kill**,否则你只看到了 bimodal 分布的一侧,把局部观察当作全局契约 + +写法上:单次 round 的 PASS/FAIL 只代表"恢复到健康状态",对 "FSFO fire / not fire" 这两个 outcome 都要独立记录,不要混入 PASS 判定。**FSFO fire 与 cluster 健康恢复是两件事**。 + +## 引擎适配 checklist + +不同引擎的进程分类不一样。设计 #6/#7/#8 时先确认这些: + +1. **哪些进程是 instance-critical(杀了导致整个 instance 终止)**:例如 Oracle 的 PMON / SMON / LGWR、PostgreSQL 的 postmaster 主进程、MySQL 的 InnoDB main thread +2. **哪些进程是 broker / 辅助守护(杀了引擎自己 spawn 一个新的,instance 不动)**:例如 Oracle 的 DMON / INSV / NSV* / RSM0 / FSFP / LREG、MySQL semi-sync 插件线程。**这一类需要专门验证 — 别把它和 #1 混到一起测** +3. **引擎自己写不写 crash 原因到 alert/error log**:决定测试结束后能不能取得"为什么死"的证据 +4. **容器 PID 1 是谁**:是 init system?是 entrypoint script?entrypoint script 有没有写 watchdog 检测引擎死? +5. **如果 PID 1 不死、引擎死了,容器会不会被 K8s 回收**:取决于 liveness probe 是否能检测到、failureThreshold 多少。watchdog(PID 1 主动 exit)比 liveness probe 快得多 +6. **failover 决策器 / observer / 监控**对 broker 协议 ERROR 的判定:会不会把"broker 配置查询失败"误当成"primary 失联"?如果会,B-broker 类故障会触发不必要的 failover +7. **observer / failover 决策器自身的恢复路径**:如果它有 setup 脚本(比如 Oracle observer 的 setup_observer.sh),脚本里有没有在等某个数据库实例 OPEN?有没有 timeout 兜底?没有的话,instance 不可恢复时 observer 永远起不来 → FSFO 永久 blind +8. **watchdog tick interval 与 FSFO threshold 之间的比例**:决定了 B-instance primary kill 是倾向 self-recovery 还是倾向 FSFO。两者接近时会出现概率性 fire;burn-in 时要显式覆盖这一现象。生产配置应该选定一侧的语义并把两者拉开 +9. **listener / port-listener / 接入层进程是否被 watchdog 覆盖**:很多 addon watchdog 只看引擎主进程(如 Oracle 的 pmon),不监控 listener。listener 死了引擎进程仍在、broker 仍报 SUCCESS、redo 仍流(现存通道),但新连接被拒——这是 silent failure。需要把 listener 列为独立监控点,要么 pgrep 监控 + auto-restart,要么作为独立的 ctr restart trigger + +## Burn-in(重复 same-class kill)方法学 + +单次 chaos round 的 PASS 不等于 "这一类故障已经验证"。对于结果带概率分布的故障类(典型是 B-instance primary kill),burn-in 才能暴露完整行为: + +1. **连续做 ≥3 次相同 kind 的 kill**,每次中间只等到 broker 报告 SUCCESS + 拓扑稳定,不做任何手工清理/reinstate +2. **每次 cycle 记录三个独立维度**:FSFO fire (Y/N) / role change / target shift —— 用同一格式 +3. **轮询点必须避开本次 kill 的目标**:从其他成员(或专门留一台不杀的成员)上跑 broker / cluster client。**这一条 burn-in 比单 round 更容易踩坑**——在 multi-cycle 里 "目标" 是变化的 +4. **拓扑会随 FSFO 旋转**:burn-in 末态可能和起点不同(primary、active target 都可能轮换到不同位置);这是预期,验收的是"自愈不破"而不是"拓扑还原" +5. **观察 bimodal 比例**:如果 N=3 次出现 1Y/2N 或 2Y/1N,说明此 cluster 在当前配置下处于 self-recovery vs FSFO 的临界线;这是有用信号 + +burn-in 还能顺带验证: + +- reinstate 路径在压力下是否退化(多次 FSFO 后 reinstate 时间是否飘移) +- 容器 restart count 是否正确累加(不重置、不丢失) +- F37 类 pod-restart-bound 资产是否 burn-in 期间被破坏(burn-in 通常只 ctr restart,不 pod restart,所以这一类 finding 不该触发) +- 任何累积型资源泄漏(FD、tmpfile、PV 残留 lock) + +burn-in cycle 之间必要的小停顿(≥30s)用来让 reinstate 完成;不要在 reinstate 中途立即起下一 cycle,否则你测的是 "broker 进入分裂态怎么办" 而不是 same-class 的概率分布。 + +## 验收口径 + +每一轮 chaos 必须留三类证据: + +1. **engine alert log**:从 kill 时间点到 recovery 完成的全段,提取关键事件("X process died" → "instance terminated" → "instance starting") +2. **broker / failover decision-maker 日志**:从 kill 时间点到 FSFO 决策完成的全段,包括 threshold 计时、target 评估、failover 执行 +3. **cluster-level state snapshot**:定时(10-30s 一次)轮询 broker / cluster config,记录 status 字符串、active target、各成员状态的轨迹 + +不取得这三类证据中的任何一类,FSFO 行为的结论都是"看着对",不是"证据上对"。 + +## 反模式 + +1. **"杀 pod 跑通了,就过了"** — 只覆盖了轴 A,FSFO 在 B-instance / B-broker 下的行为没测。 +2. **"OOM kill 容器 = 内部 crash"** — 错。OOM kill 把整个容器杀掉,是轴 A 类型;要测 B 类必须 SIGKILL 引擎进程而不打容器。 +3. **"标准故障注入工具(chaos-mesh / litmus)够用了"** — 这些工具默认主要做轴 A(pod kill / network partition / node drain)。B 类需要 `kubectl exec ... kill -9 `,没几个 chaos 框架原生支持。 +4. **"内部 crash 测试太危险,不在 prod 跑"** — chaos 本来就是在 staging / pre-prod 跑。B 类内部 crash 在 prod 里**实际上每天发生** — 只是你不知道而已(DBA 看 alert log 才能看到)。 +5. **"只测一次 B-instance 就够"** — 应该把不同 instance-critical 进程都测一遍。SMON 和 LGWR 的 fail-out 路径可能不同。 +6. **"B-instance 测过了,B-broker 不用单独测"** — 错。两类的恢复路径根本不同:B-instance 走 ctr-restart 路径,B-broker 走引擎内部 spawn 路径。前者验证 watchdog + FSFO,后者验证"协议层短暂 ERROR 不会误触发 failover"。 +7. **"B-broker instance 没倒,所以没意义"** — 错。B-broker 暴露的是 **failover 决策器的误判设计缺陷**——你能不能区分"primary 失联"和"broker 配置查询失败"。这个区分错了,B-broker 故障会被误升级为不必要的 failover。 +8. **"single-shot chaos round 通过 = FSFO 行为已验证"** — 错。FSFO 在 B-instance primary kill 下是概率性的(bimodal 分布)。单次 PASS 只代表你看到了 bimodal 的一侧,必须 ≥3 次 same-class kill 才能观察到分布的全貌。burn-in 是必要的,不是 nice-to-have。 +9. **"burn-in 期间从被杀的成员上做 broker 状态轮询"** — 错。被杀的成员上跑的 dgmgrl / broker client 在 ctr restart 期间会返回 stale cached state(甚至连续 60-90s 仍报 SUCCESS),导致测试方法学上漏检 ERROR 窗口。**永远从非本轮被杀的成员上轮询 broker 状态**。多 cycle burn-in 还要在每个 cycle 里重新选轮询点。 +10. **"broker SUCCESS = 集群健康"** — 错。broker SUCCESS 是基于现存 daemon-to-daemon TCP 通道判断的"已知健康",**不**包含"新连接能不能建立"。listener 死了之后 broker 仍然可以保持 SUCCESS 数分钟到任意长,因为它从来不主动 probe listener 是否能接受新连接。chaos 矩阵必须显式测 B-listener 类故障,不能用 broker 状态做单一健康判据。 + +## 与其他 chaos / 测试方法论的关系 + +- [`addon-controller-crash-resilience-guide.md`](addon-controller-crash-resilience-guide.md) — 覆盖**控制层** crash(KB controller 在 Ops 中段挂);本文覆盖**数据层** crash。两者是正交的,都要测。 +- [`addon-test-baseline-standard-guide.md`](addon-test-baseline-standard-guide.md) — chaos 矩阵的 baseline 标准;本文是它的 chaos 部分的轴扩充。 +- [`addon-soak-test-result-classification-guide.md`](addon-soak-test-result-classification-guide.md) — 长跑型 chaos 的结果归类;本文是单次 chaos round 设计原则。 + +## 案例附录 + +引擎相关的实测命令、日志样本、recovery 时间数据放在独立案例材料里: + +- [`cases/oracle/oracle-chaos-pod-kill-vs-smon-kill-fsfo-asymmetry-case.md`](cases/oracle/oracle-chaos-pod-kill-vs-smon-kill-fsfo-asymmetry-case.md) — Oracle 19c on KubeBlocks o19p15v9 cluster: 16 轮 chaos 矩阵实测(其中 round 13 包含 3 cycle burn-in),覆盖四类故障(A / B-instance / B-broker / B-listener)的对比 + 完整 alert log + observer log + broker poll 轨迹 + burn-in 概率分布 + position-axis matrix。其中: + - Round 1 (A 类 primary pod-kill) — FSFO 因 standby SUSPEND 抑制 + - Round 6 (B-instance SMON kill) — FSFO 触发,role 切换,自动 reinstate + - Round 7 (B-instance LGWR kill) — 验证 B-instance 行为不止 SMON + - Round 8 (A+B-instance observer+SMON 并发) — primary self-recovery 赢 race + - Round 9 (B-broker DMON kill) — 引擎内部自愈,instance 不动,FSFO 不触发 + - Round 10 (B-broker INSV kill) — 验证 B-broker 不限于 DMON,4 秒恢复,alert log 仅 2 行 + - Round 11 (B-instance on active FSFO target) — target shift 但 FSFO 不 fire;同 round 3 行为,但走 B 路径快 3 倍 + - Round 12 (B-instance on non-target standby) — 无 target shift / 无 FSFO / 无 role change,恢复 ~110s,是 B-instance 位置依赖性中的最小影响基线 + - Round 13 (B-instance primary SMON 3-cycle burn-in) — bimodal FSFO 分布直接观察:1/3 cycle self-recovery 赢 / 2/3 cycle FSFO fire;reinstate 在压力下无 degradation;burn-in 轮询点陷阱(从被杀成员上轮询 → stale broker state)暴露 + - Round 14 (B-listener silent failure: tnslsnr kill on non-target standby) — ~6 分钟 observation window 内 broker 始终 SUCCESS / 引擎 alert log 不写一行 / 没有任何自愈机制;F39 暴露 runOracle.sh watchdog 不监控 tnslsnr,需要单独 fix PR + - Round 15 (B-listener silent failure 升级到 primary 位置) — 与 round 14 同样的静默故障,但应用层等于 production outage(observer "Last Ping to Primary: 1 second ago" 同时新连接 ORA-12541);F39 升级到 Sev-1 候选 + - Round 16 (B-listener kill on active FSFO target) — 收口 position-axis matrix:与 round 14 (non-target) / 15 (primary) 同样 silent failure;得到结论"F39 是 position-independent";对比 B-instance 位置矩阵的位置-依赖差异,说明位置矩阵本身就是 chaos 设计的关键信号 diff --git a/docs/cases/oracle/oracle-chaos-pod-kill-vs-smon-kill-fsfo-asymmetry-case.md b/docs/cases/oracle/oracle-chaos-pod-kill-vs-smon-kill-fsfo-asymmetry-case.md new file mode 100644 index 0000000..80d6e7e --- /dev/null +++ b/docs/cases/oracle/oracle-chaos-pod-kill-vs-smon-kill-fsfo-asymmetry-case.md @@ -0,0 +1,430 @@ +# Oracle 19c — Pod-kill vs SMON-kill FSFO 行为不对称 案例 + +> **Audience**: Oracle addon dev / test,以及任何 HA 引擎团队想了解 chaos 注入层级差异如何改变 failover 决策 +> **Status**: stable +> **Applies to**: Oracle 19c on KubeBlocks(方法论参见 [`../../addon-chaos-pod-kill-vs-engine-internal-crash-guide.md`](../../addon-chaos-pod-kill-vs-engine-internal-crash-guide.md)) +> **Applies to KB version**: 验证于 KB 1.0.3-beta.x +> **Affected by version skew**: yes — 现象由 Oracle 19c DG broker 行为 + K8s pod 重启路径 + addon `runOracle.sh` 当前 watchdog 实现 三者共同决定。换 Oracle 版本(12c / 23ai)、换 HA 模式(MaxAvailability)、换 observer config、换 KB sidecar 行为、修 watchdog 任一项都会改变观察结果 +> +> **案例范围(scope)**:Oracle 19c / KB 1.0.3-beta.x / 一个 cluster (`o19p15v9` on idc2) / 本案例记录的 round 1–16 实测;其余 envelope(其他 Oracle 版本、其他 HA 模式、其他 KB 版本)未跑,不做断言。 + +本案例是 [`../../addon-chaos-pod-kill-vs-engine-internal-crash-guide.md`](../../addon-chaos-pod-kill-vs-engine-internal-crash-guide.md) 的工程现场补充。在同一个 Oracle 19c 三节点集群(MaxPerformance + ASYNC + FSFO threshold 30s)上跑同一组配置,仅故障注入层级不同 — 一次是 K8s 层 pod-kill,一次是 Oracle 引擎内部 SMON SIGKILL — FSFO 出现了完全相反的结果。 + +## 现场 + +- **时间**:2026-05-15 +- **集群**:`o19p15v9` on idc2 vcluster +- **拓扑**:1 primary(ORCLCDB_0 / oracle-0)+ 2 standby(ORCLCDB_1 / ORCLCDB_2)+ 1 observer +- **DG 保护模式**:MaxPerformance +- **Redo transport**:ASYNC NOAFFIRM +- **FSFO**:Enabled in Potential Data Loss Mode,threshold = 30s,AutoReinstate = TRUE +- **CommunicationTimeout**:180s +- **Active Target(pre-chaos)**:round 1 时为 ORCLCDB_1;round 6 时为 ORCLCDB_1(经过 round 3/4/5 后又重新对齐) +- **Evidence**:本地归档目录 `evidence-skyworth-oracle-19c/chaos-primary-kill-2026-05-15/` 与 `chaos-smon-kill-primary-2026-05-15/`(按 round 分目录,不进公开 repo;如需复核请联系本案例 maintainer) + +## Round 1 — K8s 层 pod-kill on primary + +### 故障注入 + +``` +kubectl delete pod -n oracle-test \ + o19p15v9-oracle-0-x-oracle-test-x-oracle-test \ + --grace-period=0 --force +``` + +T0 = `2026-05-15T03:07:11Z`。Force delete,pod 整个从 API server 摘除,所有容器(oracle / kbagent / config-manager / exporter)一起销毁。StatefulSet 立即调度新 pod。 + +### Observer 视角 + +| time | T+ | observer 日志 | +|---|---|---| +| 03:07:34Z | 23s | `Primary database cannot be reached.` | +| 03:07:35Z | 24s | `Standby is in the SUSPEND state. Fast-Start Failover suspended. Reset FSFO timer.` | +| 03:07:42Z | 31s | observer probes fail with `ORA-12537: TNS:connection closed during DB startup` | +| 03:08:07Z | 56s | primary(已重启)请求 `UNSYNC/LAGGING transition with ORCLCDB_1` | +| 03:08:16Z | 65s | primary 回到 `NOT LAGGING` 状态 | +| 03:09:48Z | 157s | broker `Configuration Status: SUCCESS`(status updated 0s) | + +### Broker 视角(关键 poll) + +``` +poll 1 (T+12s) ORCLCDB_0 = Primary(仍在 broker 记录里), status=SUCCESS(stale) +poll 2 (T+24s) ORCLCDB_0: Error ORA-1033 (ORACLE initialization or shutdown in progress), status=ERROR +poll 3 (T+37s) ORCLCDB_0: Error ORA-16525 (DG broker is not yet available), status=ERROR +... (primary 重启回来,重新接管原 role) ... +poll N (T+157s) Status=SUCCESS, ORCLCDB_0 仍是 Primary +``` + +### 结果 + +- **FSFO 没有触发** +- **没有发生角色切换** — 故障前 primary 是 ORCLCDB_0,故障后还是 ORCLCDB_0 +- **Standby ORCLCDB_1 进入 SUSPEND 状态**,observer 主动 reset 了 FSFO timer +- 整个流程是"primary self-recovery",不是"failover" + +### 为什么 FSFO 没触发 + +Observer 给出的原话是: + +> Standby is in the SUSPEND state. Fast-Start Failover suspended. + +这是 FSFO 的**数据丢失安全护栏**。在 MaxPerformance + ASYNC 模式下: + +1. Pod-kill 是 SIGKILL — primary 进程被 kubelet 直接杀,没有时间 flush 最后一秒 redo +2. 与此同时,TCP 连接也立即 RST — standby 看到 connection refused,redo transport 立即中断 +3. Standby 自己感知到"我手上的 redo 应该不完整",状态进入 SUSPEND +4. Observer 看到 standby = SUSPEND → 触发安全护栏:拒绝 failover 到一个可能丢数据的 standby +5. 在 standby SUSPEND 期间,每一轮 observer tick 都 **reset FSFO timer** — threshold 永远凑不齐 + +随后 primary pod 在 ~30-60s 内重启回来,broker 通过自我恢复路径完成 `UNSYNC/LAGGING → NOT LAGGING` 状态机切换,cluster 回到原 role 拓扑。 + +## Round 6 — 引擎内部 SMON SIGKILL on primary + +### 故障注入 + +``` +kubectl exec -n oracle-test o19p15v9-oracle-0 -c oracle -- \ + bash -c 'kill -9 $(pgrep -f ora_smon_ORCLCDB)' +``` + +T0 = `2026-05-15T04:09:28Z`。SMON PID = 113。**Pod 没被杀,只有 Oracle 引擎里的 SMON 后台进程被 SIGKILL**。oracle 容器的 PID 1 是 runOracle.sh(包含 watchdog 循环)。 + +### Alert log 视角(来自 oracle ctr 内部) + +| time | T+ | alert_ORCLCDB.log | +|---|---|---| +| 04:09:33.121Z | 5s | `PMON (ospid: ): terminating the instance due to ORA error` | +| 04:09:33.122Z | 5s | `Cause - 'Instance is being terminated due to fatal process death (pid: 24, ospid: 113, SMON)'` | +| 04:09:33.125Z | 5s | `System State dumped to trace file ORCLCDB_diag_90.trc` | +| 04:09:35.637Z | 7s | `Instance terminated by PMON, pid = 67` | +| 04:10:09.511Z | 41s | `Starting ORACLE instance (normal) (OS id: 31)` ← 新容器内的 runOracle 启动新 instance | + +> **关键**:Oracle 自己写了一行**"为什么死"**到 alert log(`Cause - 'Instance is being terminated due to fatal process death (..., SMON)'`)。Pod-kill 那一轮的 alert log 没有这种 cause 记录 — 因为整个进程组被 SIGKILL,没人有时间写 log。 + +### Observer 视角 + +``` +[W000 2026-05-15T04:09:34.912+00:00] Primary database cannot be reached. +[W000 2026-05-15T04:09:34.912+00:00] Fast-Start Failover threshold has not exceeded. Retry for the next 30 seconds +... (threshold 倒计时) ... +[W000 2026-05-15T04:10:04.996+00:00] Fast-Start Failover threshold has expired. +[W000 2026-05-15T04:10:04.996+00:00] Check if the standby is ready for failover. +[S005 2026-05-15T04:10:05.002+00:00] Fast-Start Failover started... +Initiating Fast-Start Failover to database "ORCLCDB_1"... +Performing failover NOW, please wait... +Failover succeeded, new primary is "ORCLCDB_1" +[W000 2026-05-15T04:10:37.582+00:00] Failover succeeded. Restart pinging. +[W000 2026-05-15T04:10:37.674+00:00] The standby ORCLCDB_0 needs to be reinstated +... (等 ORCLCDB_0 重启回来作为 standby) ... +[W000 2026-05-15T04:10:43.680+00:00] New primary is now ready to reinstate. +[W000 2026-05-15T04:10:43.680+00:00] Issuing REINSTATE command. +Initiating reinstatement for database "ORCLCDB_0"... +[W000 2026-05-15T04:11:19.747+00:00] The standby ORCLCDB_0 is ready to be a FSFO target +Reinstatement of database "ORCLCDB_0" succeeded +[W000 2026-05-15T04:11:42.791+00:00] Successfully reinstated database ORCLCDB_0. +``` + +### Broker 视角(关键 poll) + +``` +poll 1 (T+23s) ORCLCDB_0: Error ORA-1034 (ORACLE not available), status=ERROR + — 注意是 ORA-1034 不是 ORA-1033 / connection refused + — 因为 listener 还活着,只是 instance 不可用 +poll 2 (T+40s) ORCLCDB_0: 同上, oracle 容器 RESTARTS=1, kbagent 没动 +poll 3 (T+62s) ORCLCDB_1 = Primary ← 角色已切换 + ORCLCDB_0 = Physical standby (disabled), ORA-16661 needs reinstate + status=SUCCESS(status updated 59 seconds ago) +poll 7 (T+150s) status=SUCCESS, ORCLCDB_0 已 reinstate 成 standby +``` + +### 结果 + +- **FSFO 触发了** +- **角色发生了真正的切换**:故障前 primary 是 ORCLCDB_0,故障后 primary 是 ORCLCDB_1 +- **Auto-reinstate 自动把原 primary 拉回来当 standby**,全程无人工干预 +- 整个 cluster 从故障到完全恢复角色 + reinstate ≈ 150 秒 + +### 为什么 FSFO 触发了 + +跟 round 1 同一个 cluster、同一个 broker 配置、同一个 FSFO threshold,为什么这次能 fail over? + +1. **故障是进程级的**:oracle main process 被 watchdog 杀(PID 1 exit),但容器里的 listener、kbagent ctr、network stack 都保持活着 +2. **Standby 看到的不是 TCP RST,是 service-level 错**:broker poll 看到 ORA-1034(ORACLE not available),而不是 connection refused +3. **Standby 的 redo apply 没有被 abruptly 切断** — SMON kill 触发 PMON 主动终止 instance,PMON 有几毫秒清理 + dump 时间,最后一秒 redo 已经被 shipped/applied 或至少 standby 没收到"不完整"信号 +4. **Standby 没进 SUSPEND** — observer tick 看到 standby = healthy,threshold 倒计时正常累积 +5. T+37s(kill 后 30s + 几秒抖动)threshold expired → FSFO 触发 + +## 两轮关键差异并排 + +| 维度 | Round 1 — Pod-kill | Round 6 — SMON-kill | +|---|---|---| +| 故障注入 | `kubectl delete pod --force --grace-period=0` | `kubectl exec ... kill -9 ora_smon_*` | +| Pod 状态 | 整个 pod 被销毁重建(所有 4 个 ctr) | Pod 没动,仅 oracle ctr restart | +| Sidecar ctr 影响 | kbagent / exporter / config-manager 一起重启 | 完全不受影响 | +| Listener 状态 | 被杀掉,TCP 连接 RST | listener 进程不存在了(runOracle exit 时),但短暂窗口内 standby 看到的是 ORA-1034 | +| Alert log "why died" | **没有**(整个进程被 SIGKILL) | **有**(`Cause - 'fatal process death (..., SMON)'`) | +| Standby 状态 | **SUSPEND**(数据完整性护栏触发) | 保持 healthy(normal physical standby) | +| Observer 第一句反应 | `Standby is in the SUSPEND state. FSFO suspended. Reset FSFO timer.` | `FSFO threshold has not exceeded. Retry for the next 30 seconds` | +| FSFO threshold 累计 | 永远凑不齐(每次 reset) | 正常累计到 30s expired | +| **FSFO 触发?** | **否** | **是** | +| 角色切换? | 否(self-recovery 回原 role) | 是(ORCLCDB_0 → ORCLCDB_1) | +| Auto-reinstate? | n/a | 是(原 primary 自动回归当 standby) | +| Broker 完整恢复时间 | ~157s(无角色变) | ~150s(含角色变 + reinstate) | +| F37(kbagent ctr 无 symlink) | 触发(pod restart) | **不触发**(只 oracle ctr restart) | + +## 结论:故障层级不同 → failover 行为不同 → chaos 矩阵必须双轴覆盖 + +### 关键事实 + +**这不是 bug,也不是 feature** — 是 Oracle DG FSFO 在 MaxPerformance + ASYNC 模式下的**正确行为**。FSFO 的设计要求 standby 处于 valid + non-suspend 状态才允许 failover,这是数据丢失保护。Pod-kill 这种"全员瞬间断电"的故障模式天然让 standby 退到 SUSPEND;SMON-kill 这种"primary 内部死亡 + 网络栈短暂存活"让 standby 始终看着 healthy。 + +### 运维含义 + +| 生产场景 | 对应 chaos 轴 | FSFO 是否会救你 | +|---|---|---| +| 节点宕机、OS reboot、整 pod 被驱逐 | A(pod-kill 类) | 在 MaxPerformance + ASYNC 下,**多数情况下不会** — 需要等 pod 重启自愈,或人工 failover | +| OOM kill 一个 ora_* 后台进程(Oracle 内部断言、内存压力) | B(内部 crash 类) | **会**触发 FSFO | +| `shutdown abort` / 引擎内部死锁导致 instance 自杀 | B | **会**触发 FSFO | +| 容器 liveness probe 失败 kubelet kill 容器 | A 倾向(整 ctr 被 kill) | 同 A — 看 standby 是否被打到 SUSPEND | + +> **运维侧实操结论**:如果业务要求"primary 任何形式失联都自动 failover",MaxPerformance + ASYNC 不够。需要切到 MaxAvailability + SYNC + 合适的 NoDataLossTimeout。但代价是 primary 写可用性可能在 standby 不可达时被 hold。本 cluster 用 MaxPerformance + ASYNC 是显式选择了"保写可用性,接受单点数据丢失窗口"。 + +### Chaos 测试侧实操结论 + +**只测 pod-kill 是不够的**。如果 chaos 矩阵只跑 round 1 这种 K8s 层故障,你只验证了 ASYNC + SUSPEND-protection 路径下的 FSFO 行为 — 这条路径里 FSFO 通常不触发。你完全没验证 FSFO 自己的 threshold 累计 + failover 决策 + auto-reinstate 链路。 + +至少要再加一轮 round 6 这种引擎内部进程级 kill,才能: + +1. 验证 FSFO threshold 倒计时器是否工作(之前每次都被 SUSPEND reset 掉) +2. 验证 broker 真正 issue `Initiating Fast-Start Failover...` 的代码路径 +3. 验证 auto-reinstate(`FastStartFailoverAutoReinstate = TRUE`)是否正确把老 primary 拉回来 +4. 验证 FSFO 完整恢复时间 SLO(150s 量级 vs 仅 self-recovery 的 157s) + +## 相关 finding 联动 + +这次对照测试还顺带验证了另外几条: + +- **F37 路径只在 pod-restart 触发,不在 ctr-restart 触发**:round 1 触发 F37(kbagent ctr 没 symlink),round 6 不触发(kbagent ctr 没被重启,symlink 保留)。这是 F37 的边界条件证据。 +- **runOracle.sh PID-1 watchdog 工作正确**:round 6 中 `pgrep -f ora_pmon_${ORACLE_SID}` 30s tick 检测到 instance terminated,PID 1 exit,kubelet 立即 restart oracle ctr。从 SMON kill 到新 instance starting ≈ 41s — 比 K8s liveness probe failureThreshold 快得多。 +- **FSFO threshold 30s 设置合理**:观察到 observer 从"primary cannot be reached"到"threshold expired"刚好 30s,与配置一致;如果调到 10s 会有更快 failover 但 round 1 这种 transient outage 也会被错误触发。 + +## 案例文件清单 + +``` +evidence-skyworth-oracle-19c/ # local archive root; not in repo +├── chaos-primary-kill-2026-05-15/ # round 1 (A) +│ ├── FINDINGS.md +│ ├── during/00-kill-event.txt +│ ├── during/01-broker-poll.txt +│ ├── during/02-pod-stream.txt +│ └── post/00b-observer-full.txt +├── chaos-smon-kill-primary-2026-05-15/ # round 6 (B-instance, SMON) +│ ├── FINDINGS.md +│ ├── during/00-kill-event.txt +│ ├── during/01-broker-poll.txt +│ ├── during/02-alert-log-extract.txt +│ ├── during/03-observer-log-snapshot.txt +│ └── during/03-observer-log.txt +├── chaos-lgwr-kill-primary-2026-05-15/ # round 7 (B-instance, LGWR) +│ └── FINDINGS.md +├── chaos-observer-and-primary-smon-2026-05-15/ # round 8 (A+B-instance concurrent) +│ └── FINDINGS.md +├── chaos-dmon-kill-primary-2026-05-15/ # round 9 (B-broker master, DMON) +│ └── FINDINGS.md +├── chaos-insv-kill-primary-2026-05-15/ # round 10 (B-broker worker, INSV) +│ └── FINDINGS.md +├── chaos-smon-kill-standby-2026-05-15/ # round 11 (B-instance on active FSFO target) +│ └── FINDINGS.md +├── chaos-smon-kill-nontarget-standby-2026-05-15/ # round 12 (B-instance on non-target standby) +│ └── FINDINGS.md +├── chaos-burnin-smon-primary-2026-05-15/ # round 13 burn-in (3 consecutive SMON kills on primary) +│ ├── SUMMARY.md +│ ├── cycle1/FINDINGS.md +│ ├── cycle2/00-summary.txt + 02-poll.txt +│ └── cycle3/00-summary.txt + 02-poll.txt +├── chaos-tnslsnr-kill-nontarget-standby-2026-05-15/ # round 14 (B-listener silent failure → F39) +│ ├── 00-pre-state.txt +│ ├── 01-kill-event.txt +│ ├── 02-broker-poll.txt +│ ├── 03-diagnostics.txt +│ ├── 04-state-during-outage.txt +│ ├── 05-long-observation.txt +│ ├── 06-recovery.txt +│ └── FINDINGS.md +├── chaos-tnslsnr-kill-primary-2026-05-15/ # round 15 (B-listener silent failure on primary, F39 Sev-1) +│ ├── 00-pre-state.txt +│ ├── 01-kill-event.txt +│ ├── 02-broker-poll.txt +│ ├── 03-diagnostics.txt +│ ├── 04-recovery.txt +│ └── FINDINGS.md +├── chaos-tnslsnr-kill-active-target-2026-05-15/ # round 16 (B-listener on active FSFO target, F39 position-axis matrix close) +│ ├── setup.txt +│ ├── t0.txt +│ ├── t_recovery.txt +│ ├── 01-kill.txt +│ ├── 02-recovery.txt +│ ├── 03-broker-final.txt +│ └── FINDINGS.md +└── F38-observer-setup-no-timeout/ # latent risk discovered during round 8 + └── FINDING.md +``` + +## Round 7 / 8 / 9 关键发现摘要 + +### Round 7 — B-instance generality (LGWR kill) +LGWR 死亡的恢复路径与 round 6 SMON kill **结构上一致**:T+5s PMON 检测到、T+8s instance terminated、T+42s 新 instance starting、T+38s FSFO threshold expired、T+82s 完成 failover、T+139s broker SUCCESS。**证明 B-instance 行为不是 SMON-specific**。 +新观察:observer 在 standby reinstate 期间会因为 ORA-16657 重试 1-3 次,最终 settle。这是 normal eventual-consistency,不是 bug。 + +### Round 8 — A+B-instance 并发 race +observer pod kill 与 primary SMON kill 间隔 118ms 执行。primary self-recovery (T+19s 新 instance) 远快于 observer 重启 (T+56s observer ready)。observer 起来时 primary 已经活了 37s,**FSFO 没触发,没有 role change**。 +副产品 F38 — `setup_observer.sh` 用无 timeout 的 `while true` 等所有 instance OPEN;hardcode `${ORACLE_SID}_0` 作 primary 入口。如果 primary 永久不可恢复,observer 永远起不来,FSFO 永久 blind。已记录到 `evidence-skyworth-oracle-19c/F38-observer-setup-no-timeout/FINDING.md`。 + +### Round 9 — B-broker master (DMON kill) +DMON SIGKILL 后 6 秒,Oracle 内部 `Completed: Data Guard Broker cleanup` → `Restarting Data Guard Broker (DMON)` → 新 DMON pid=2463 起来;T+9-19s 区间 INSV / NSV2 / NSV3 / RSM0 / FSFP 逐步起来;T+27s broker SUCCESS;T+31s redo transport destinations 重新 enable。**PMON pid 不变,oracle ctr 不重启,FSFO 不触发**。 + +### Round 10 — B-broker worker (INSV kill) +INSV SIGKILL 后 4 秒内,alert log 仅两行:`Starting background process INSV` → `INSV started with pid=107, OS id=5109`。没有 broker cleanup、没有支撑进程级联、没有 broker ERROR 窗口。比 DMON kill 还要轻量得多。**说明 B-broker 类下还有一层 master vs worker 的区分**:DMON 是 broker 协调器,杀了引发全 broker re-init;INSV / NSV* / RSM0 / FSFP / LREG 是 broker 支撑进程,杀了仅触发 generic 重新 spawn。 + +但两者共同点是:**instance 不动 / FSFO 不该触发**。这就是 B-broker 类的定义性质。如果将来某次测试发现 master vs worker 在 operational 层面有显著差别,再考虑做更细的二级分类。 + +### Round 12 — B-instance on non-target standby (minimum-impact baseline) +杀 oracle-1(这时是非 target standby,round 11 把 active target shift 到了 ORCLCDB_2)的 SMON:T+16s broker ERROR;T+49s oracle ctr restart 2→3;**target 全程不变 (ORCLCDB_2),无 role change,无 FSFO**;T+110s broker SUCCESS。observer 对非 target standby 失联完全不做 target re-evaluation —— 这是正确的,因为 FSFO 只关心 primary + active target。 + +### B-instance 位置依赖性矩阵(rounds 6/7/11/12 合成) +同一个 SMON SIGKILL 在三个不同位置上产生三种完全不同的 cluster 级结果: + +| 位置 | FSFO fire | role change | target shift | primary 写 | +|---|---|---|---|---| +| primary (rounds 6, 7) | YES | YES (P → standby) | n/a | 中断 ~150s | +| active target standby (round 11) | NO | NO | YES (T → 另一 standby) | 不中断 | +| non-target standby (round 12) | NO | NO | NO | 不中断 | + +这就是为什么"测了一次 B-instance"并不意味着"测了 B-instance 的全部行为" — 每个位置要单独测。 + +### Round 11 — B-instance on active FSFO target (SMON kill on standby) +oracle-1 当时是 active FSFO target,杀其 SMON:T+10s broker ERROR;T+23s oracle ctr restart 1→2(watchdog 路径);T+43s **observer auto-shift active target 到 ORCLCDB_2**(同 round 3 pod-kill 行为,只是走 B 路径);T+84s broker SUCCESS。**primary ORCLCDB_0 全程不变,FSFO 不 fire** — 正确,"target 不可用" ≠ "primary 不可用"。 +对照 round 3 (A class 同位置 pod kill 同样 target shift):恢复时间 84s vs 4m25s。B-instance 比 A 在 standby 节点上快 3 倍 — 因为 ctr-restart 保留 PVC mount、kbagent ctr、exporter ctr 和 image cache。 +30s target-shift latency = FSFO threshold,observer 用同一个 threshold 评估 target 健康。 + +| round | 类 | FSFO 触发 | role change | target shift | broker SUCCESS 时间 | +|---|---|---|---|---|---| +| 1 | A primary | NO (SUSPEND) | NO | n/a | ~157s | +| 3 | A FSFO target | NO | NO | YES (1→2) | ~4m25s | +| 6 | B-instance primary (SMON) | YES | 0→1 | n/a | ~150s | +| 7 | B-instance primary (LGWR) | YES | 1→0 | n/a | ~139s | +| 8 | A+B-instance (observer+SMON) | NO (race) | NO | n/a | ~80s | +| 9 | B-broker master (DMON) | NO | NO | n/a | ~27s | +| 10 | B-broker worker (INSV) | NO | NO | n/a | ~4s | +| 11 | B-instance FSFO target (SMON) | NO | NO | YES (1→2) | ~84s | +| 12 | B-instance non-target standby (SMON) | NO | NO | NO | ~110s | +| 13-c1 | B-instance primary SMON burn-in 第 1 次 | **NO (self-recovery 赢)** | NO | n/a | ~75s | +| 13-c2 | B-instance primary SMON burn-in 第 2 次 | YES | 0→2 | n/a | ~150s | +| 13-c3 | B-instance primary SMON burn-in 第 3 次 | YES | 2→0 | YES (0→1) | ~121s | +| 14 | **B-listener** (tnslsnr kill on non-target standby) | **NO** | NO | NO | **永远 SUCCESS (silent failure)** | +| 15 | **B-listener** (tnslsnr kill on **primary**) | **NO** | NO | NO | **永远 SUCCESS (silent failure,但应用层 brick)** | + +### Round 13 — Burn-in 3 cycles,FSFO 概率性确诊 + +`chaos-burnin-smon-primary-2026-05-15/SUMMARY.md` + +连续 3 次同样的 SMON SIGKILL on primary,cycle 间不做任何手工 reinstate: + +| cycle | 时刻 (UTC) | pre-primary | post-primary | post-target | FSFO 触发 | +|---|---|---|---|---|---| +| 1 | 05:04:15Z | ORCLCDB_0 | ORCLCDB_0 (不变) | ORCLCDB_2 (不变) | NO | +| 2 | 05:11:39Z | ORCLCDB_0 | ORCLCDB_2 | ORCLCDB_0 | YES | +| 3 | 05:14:34Z | ORCLCDB_2 | ORCLCDB_0 | ORCLCDB_1 | YES | + +末态:primary ORCLCDB_0、active target ORCLCDB_1、ORCLCDB_2 为非 target standby —— 与 burn-in 前的拓扑结构上一致(只是 active target 从 ORCLCDB_2 旋转到了 ORCLCDB_1)。3 次 cycle 全部自愈,无人工干预。 + +**核心发现**: +- 3 次同样的 kill,1 次没 fire(self-recovery 赢),2 次 fire(FSFO 路径)—— 这是 bimodal 分布的直接观察 +- cycle 1 是**第 3 次** 观察到 self-recovery 赢 race(前两次是 round 8 observer+SMON 并发 + 隐式 ctr restart 暖镜像)。镜像 cache 暖到这种程度后 self-recovery 总时长 ~15-20s,明显低于 30s threshold +- cycle 1 → 2 → 3 之间 ctr restart 让镜像 cache 持续暖;理论上 self-recovery 应该越来越快才对,但 cycle 2 和 3 都 fire 了,说明 **watchdog tick 相位**是更主要的变量,比镜像 cache 状态更敏感 +- reinstate 时间在 3 cycle burn-in 下保持稳定(cycle 2 ~150s,cycle 3 ~121s,对照单次 round 6 ~150s / round 7 ~139s)—— 自愈机器在 stress 下无 degradation +- F37 / F34 在 burn-in 中没 regression:F34 symlink 仍在,F37 没被触发(pod 没重启) + +**Methodology bug 自我披露**: cycle 3 中,broker 轮询脚本继续从 `o19p15v9-oracle-2` 的 oracle ctr 上跑 dgmgrl,而本 cycle 杀的是 ORCLCDB_2(也在 oracle-2 pod 上)—— 被杀成员上的 dgmgrl 返回 stale cached state 长达 ~75s。这就是 burn-in 必须避开"从被杀成员轮询"的反例。已写入方法论 doc 的反模式 #9 和 burn-in 章节。 + +**生产侧操作建议**: +- 不要把"SMON kill 必然触发 failover"写进 SOP,FSFO 在当前 30s/30s 配置下是 bimodal 的 +- 想要 "always failover":把 watchdog tick 拉长到 ≥60s(让 self-recovery 总是输 race),或者把 FSFO threshold 缩短到 ≤15s +- 想要 "prefer self-recovery if fast":保持当前配置,但要接受 1/3 概率走 self-recovery、2/3 概率走 FSFO 的 bimodal 行为 + +### Round 14 — B-listener silent failure (tnslsnr kill on non-target standby) + **F39 watchdog 盲区** + +`chaos-tnslsnr-kill-nontarget-standby-2026-05-15/FINDINGS.md` + +05:29:27Z `kubectl exec o19p15v9-oracle-2 -c oracle -- bash -c 'kill -9 $(pgrep tnslsnr)'`。oracle-2 是非 target standby。 + +**~6 分钟 observation window 全程 broker 报告 SUCCESS,alert log 不写一行**。这是新的故障类(B-listener): +- tnslsnr 死后 2s 内 pgrep 找不到,但 pmon/smon/dmon 完全不变 +- broker 现存 daemon-to-daemon TCP 长连接继续工作,broker config 始终 SUCCESS +- redo 传输(rfs PID:1722)通过现存通道继续 apply +- observer 通过现存连接仍能看到 ORCLCDB_2 健康 +- 但**新连接立刻失败**: `dgmgrl /@ORCLCDB_2` 返回 `ORA-12541: TNS:no listener` +- 没有任何机制触发自愈:runOracle.sh watchdog 只 pgrep `ora_pmon`,不监控 tnslsnr + +**T+377s 手动 `lsnrctl start` 即恢复**。recovery 成本为零(无需 ctr restart)。 + +**F39 (新)**: `runOracle.sh` 的 watchdog(line 572-585)只检查 `ora_pmon_${ORACLE_SID}`。tnslsnr 死了**永远**不被发现,引擎不重启,broker 不报警。生产侧如果 listener OOM / 进程被杀,集群对客户端 "假死"。修复 PR 应该在 watchdog 里加 `pgrep -x tnslsnr` 检查,先尝试 `lsnrctl start` 自愈,失败再 exit 让 ctr restart。 + +**这一类故障的本质特征**: +- 引擎 + broker daemon 都不需要 listener 维持已建立连接 +- 只有"新连接建立"环节需要 listener +- 因此从 cluster 内部观察看不到任何异常 +- 必须用"模拟客户端尝试新连接"的探针才能检测 + +**Methodology 内化**: chaos 矩阵已加 B-listener 子类作为 #11;engine adaptation checklist 已加 listener 监控项;反模式增加"broker SUCCESS = 集群健康"的反例。F39 fix 单独 PR,不混入 PR #129(PR #129 是 chaos 文档)。 + +### Round 15 — B-listener kill on **primary** = 同 silent failure,但应用层风险等级"高" + +`chaos-tnslsnr-kill-primary-2026-05-15/FINDINGS.md` + +05:41:28Z 杀 ORCLCDB_0(primary)的 tnslsnr。轮询从 oracle-1 (ORCLCDB_1, active target) 跑,避开被杀目标。 + +**133s observation window 全程 broker SUCCESS,primary / target 不变,FSFO 不触发**。但应用层影响显著: +- T+285s 跑 `SHOW OBSERVER`:observer 报告 "Last Ping to Primary: 1 second ago" — observer 现存连接 happily ping 着,**完全不知道**新连接已经死了 +- 从 oracle-1 跑新 dgmgrl /@ORCLCDB_0 → ORA-12541: TNS:no listener +- 客户端、监控、新 sqlplus、JDBC 全部无法连接 primary +- primary alert log 在 6 分钟内零新 entry + +T+347s 手动 `lsnrctl start` → 30s 内恢复,无 ctr restart。 + +**生产风险升级**: round 14 是 standby (低影响);round 15 是 primary (**全集群应用层 outage**)。同一类故障在不同位置上严重程度差几个数量级。 + +**F39 升级到 Sev-1 候选**:listener 死 → broker SUCCESS + observer 心跳正常 + 客户写入全部 ORA-12541 → 运维只能等客户告警,分钟到小时级别"假死"。修复 PR 设计已写在 round 14 FINDINGS:watchdog 加 `pgrep -x tnslsnr` 检查,先尝试 `lsnrctl start` 自愈,失败才 exit 触发 ctr restart。 + +### Round 16 — B-listener kill on **active FSFO target standby** = position-axis matrix 收口 + +`chaos-tnslsnr-kill-active-target-2026-05-15/FINDINGS.md` + +05:59:13Z 杀 oracle-1(ORCLCDB_1,active FSFO target)的 tnslsnr。轮询从 primary oracle-0 跑,避开被杀目标。 + +**5min observation window 全程 broker SUCCESS,Active Target 停留在 ORCLCDB_1,FSFO 不触发**。同 round 14 / 15 silent failure: +- T+5s: `pgrep -x tnslsnr` 找不到,pmon/smon 全活 +- T+15s: broker `Configuration Status: SUCCESS (status updated 1 second ago)` +- T+26s: observer "Last Ping to Target: 2 seconds ago"(现存 TCP),但新 `sqlplus @ORCLCDB_1` 立即 ORA-12541 +- T+3m51s: broker 仍 SUCCESS,Active Target 仍 ORCLCDB_1,alert log 自 T0 起零新 entry(最近一条 05:17 早已不相关) +- T_recovery(T+5m20s): `lsnrctl start` → 新 tnslsnr pid 20100,broker 仍 SUCCESS,无 ctr restart + +**B-listener 位置依赖性矩阵收口**: + +| round | victim 位置 | broker SUCCESS? | FSFO fire? | target shift? | observer 视角 | 新连接到 victim | +|---|---|---|---|---|---|---| +| 14 | non-target standby (ORCLCDB_2) | YES | NO | NO | n/a | ORA-12541 | +| 15 | primary (ORCLCDB_0) | YES | NO | NO | "Last Ping to Primary: 1s ago" | ORA-12541 | +| 16 | active FSFO target (ORCLCDB_1) | YES | NO | NO | "Last Ping to Target: 2s ago" | ORA-12541 | + +**核心结论**: F39(watchdog 盲于 tnslsnr)**与位置无关** —— 三个位置上同样 silent,同样不自愈,同样依赖手动 `lsnrctl start`。这就是为什么 fix 必须从 watchdog 层(uniform 解)做,而不是任何位置专属的 fallback。 + +对照 B-instance 位置矩阵(rounds 6/11/12)—— 那里 primary vs active target vs non-target 会产生根本不同的 FSFO 行为(fire vs target-shift vs no-op)—— B-listener 的位置矩阵反而**全同**。两类故障的位置依赖性差异本身就是 chaos 测试设计的关键信号:位置-依赖的 fault class 需要 per-position 测试,位置-无关的 fault class 测一处就够但必须确认它真的位置无关。 + +**与 F39 fix PR 关系**: round 16 是 fix 提交后的最后一次 pre-fix 验证;提供同一引擎实现下三个位置一致的 baseline,PR #1320 验证时只需在一个位置上观察 watchdog 自愈即可。 + +## 与其他案例 / 方法论的关系 + +- 方法论母文档:[`../../addon-chaos-pod-kill-vs-engine-internal-crash-guide.md`](../../addon-chaos-pod-kill-vs-engine-internal-crash-guide.md) — 本案例是它的 Oracle 工程现场补充 +- 同 cluster 上 round 3/4/5 的其他 chaos 轮次:见 `evidence-skyworth-oracle-19c/chaos-{fsfo-target-kill,observer-kill,dual-standby-kill}-*` 各自 FINDINGS.md +- F34 / F35 / F36 / F37 多容器共享文件相关 finding:当前在本地 evidence 归档 `evidence-skyworth-oracle-19c/F34-*` ~ `F37-*`,等对应方法论 / 案例文档 land 后再回填本节链接(避免悬空链)