Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/SKILL-INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@

引擎特定的现场材料按引擎分组,每个引擎一个子目录:

- [`cases/mariadb/`](cases/mariadb/) — 15 个 mariadb addon 案例
- [`cases/mariadb/`](cases/mariadb/) — 16 个 mariadb addon 案例
- [`cases/valkey/`](cases/valkey/) — 2 个 valkey 案例
- [`cases/oracle/`](cases/oracle/) — 7 个 oracle 案例
- [`cases/oceanbase/`](cases/oceanbase/) — 3 个 oceanbase 案例
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# MariaDB reconfigure SET GLOBAL × ConfigMap sync race 案例

> **Audience**: addon dev / test 在 reconfigure / Reconfiguring OpsRequest 场景,特别是涉及 SET GLOBAL + restart-prone 拓扑
> **Status**: stable (root cause closed, alpha.84 chart fix landed)
> **Applies to**: MariaDB addon semisync 拓扑
> **Applies to KB version**: KB 1.2.0-alpha.1(+ 5 fixes + V(1) 诊断 + kbagent pullPolicy fix)
> **Affected by version skew**: no — 由 addon 自身写法决定,跨 KB 版本一致

通用方法论见 [`../../troubleshoot/addon-reconfigure-set-global-persist-race-guide.md`](../../troubleshoot/addon-reconfigure-set-global-persist-race-guide.md);本案例只保留 MariaDB-specific 现场材料。

## MariaDB 特有的根因链

1. semisync 拓扑没有声明自己的 `ParametersDefinition`(chart 漏写)→ KB Configure controller 不知道 `slow_query_log` / `long_query_time` 是 dynamic → 默认走 rolling restart 路径
2. rolling restart 在 semisync 拓扑里表现成 switchover / promote → 新主 mariadbd 进程级 restart
3. chart 的 `reconfigureAction` 只跑 `SET GLOBAL`(运行时生效),不落任何持久化文件
4. mariadbd restart 时机点上 `/etc/mysql/conf.d/my.cnf` 还没收到 kubelet 的 ConfigMap volume 同步(典型几秒到约 60s 延迟)→ mariadbd 读旧 my.cnf → 加载 chart 默认值
5. SET GLOBAL 应用的运行时状态被 restart 抹掉,最终运行时 = chart 默认

## 现场证据(alpha.83 clean R1)

| 字段 | pod0(原 primary → 当前 secondary) | pod1(原 secondary → 当前 primary) |
|---|---|---|
| OpsRequest phase | Succeed | Succeed |
| ComponentParameter | Finished, succeedCount=2 / expectedCount=2, revision=4 | 同 |
| kbagent action stdout | reconfigure success | reconfigure success @ 20:42:11 |
| K8s container restartCount | 0 | 0 |
| K8s container startedAt | 未变 | 未变 |
| mariadbd PID | 未变 | **20:42:33 新 PID**(进程级 restart) |
| mariadb error log shutdown | 无 | `Normal shutdown` 20:42:29 |
| /etc/mysql/conf.d/my.cnf 内容 | slow_query_log=ON | slow_query_log=ON(**但更新时间晚于 mariadbd restart**) |
| `SHOW GLOBAL VARIABLES slow_query_log` | ON ✓ | **OFF** ✗ |
| `SHOW GLOBAL VARIABLES long_query_time` | 3 ✓ | **10** ✗ |

## 时间线(pod1 的窗口)

```
T+0 reconfigure action runs SET GLOBAL slow_query_log=ON on both pods
T+10s ConfigMap update commits to etcd
T+15s KB triggers switchover/promote (PD 缺失 → 默认 rolling restart 路径)
T+15s pod1 mariadbd receives SIGTERM, normal shutdown
T+18s pod1 mariadbd new process starts
reads /etc/mysql/conf.d/my.cnf — STILL OLD (kubelet not synced yet)
loads slow_query_log=OFF (chart default)
T+45s kubelet ConfigMap volume sync completes
/etc/mysql/conf.d/my.cnf now has slow_query_log=ON
but mariadbd is already in-memory running with OFF
T+60s test checks SHOW GLOBAL VARIABLES → OFF ✗
```

## 修法(alpha.84,4 处 chart 改动)

1. 补 `mariadb-semisync-pd` 到 `paramsdef.yaml` — `componentDef: ^mariadb-semisync-`、`templateName: mariadb-semisync-config`、dynamic/static 复用同一份 `mariadb-config-effect-scope.yaml`
2. `mariadb-semisync.tpl` 末尾追加 `!includedir /var/lib/mysql/runtime-overrides.d/`
3. `cmpd-semisync.yaml` init-syncer 加 `mkdir -p {{ .Values.dataMountPath }}/runtime-overrides.d`
4. `mariadb.config.reconfigureAction` helper:SET GLOBAL 成功后写 `${OVERRIDES_DIR}/<param>.cnf`(per-param file,temp + atomic rename,写入失败 WARN-only 不 fail action)

L1 修法(补 PD)让常规路径不再 restart;L2 修法(持久化)兜底 OOMKill / 节点驱逐 / 手动维护等不可控 restart 路径。两层都做才闭环。

## 边界

- 适用拓扑:semisync(有 promote/switchover 路径 + SET GLOBAL reconfigureAction)
- replication / standalone 拓扑也写了 SET GLOBAL reconfigureAction,但 restart 路径较少;如果用户手动 `kill -TERM mariadbd` 也会丢值;建议同样落 L2 持久化(已含在 alpha.84 chart helper 修法里)
- alpha.85 / alpha.88 后续版本是同一修法路径的 chart-immutability 迭代,本质上不变

## 现场证据归档

- 失败样本 evidence tar sha:`9502a88ff4a89c17af87649f83c9662bab1b51a699c77e3541a3dd53115fec67`
- summary md sha:`4fa0f12f602bf5f3d19377987f83f1a27e92f9d4bdf0588bf99e32422f8e2af2`
- 修法 chart 版本:MariaDB addon `1.1.1-alpha.84+`(实际 land 版本含 alpha.85 + alpha.88 修复层)

## 相关文档

- 通用方法论:[`../../troubleshoot/addon-reconfigure-set-global-persist-race-guide.md`](../../troubleshoot/addon-reconfigure-set-global-persist-race-guide.md)
- 同主题 MariaDB 案例:[`cm1-dynamic-reload-case.md`](cm1-dynamic-reload-case.md)(reloadAction 未落 live PD)+ [`cm4-bounded-window-helper-semantic-bug-case.md`](cm4-bounded-window-helper-semantic-bug-case.md)(CM4 bounded window)
- 设计入口:[`../../design/addon-reconfigure-guide.md`](../../design/addon-reconfigure-guide.md)
- evidence-discipline:[`../../test/addon-evidence-discipline-guide.md`](../../test/addon-evidence-discipline-guide.md)

## 一句话总结

MariaDB semisync 这次不是 OpsRequest 计数错,而是 `SET GLOBAL` 未持久化撞上进程级 restart;PD + PVC override 两层修法一起落才闭合。
1 change: 1 addition & 0 deletions docs/troubleshoot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
- [`addon-image-tag-vs-pr-merge-boundary-guide.md`](addon-image-tag-vs-pr-merge-boundary-guide.md) — release image tag 不等于 "截止 release 时间所有已合并 PR" 集合;三步 verification + 7 类陷阱
- [`addon-mariadb-grant-monitor-priv-mariadb-11-4-guide.md`](addon-mariadb-grant-monitor-priv-mariadb-11-4-guide.md) — MariaDB 11.4+ 上 `REPLICATION CLIENT` 不再覆盖 `SLAVE MONITOR`;`SHOW SLAVE STATUS` 失败时按 `SHOW GRANTS` 定位并补显式授权
- [`addon-instanceset-image-tag-alias-readiness-trap-guide.md`](addon-instanceset-image-tag-alias-readiness-trap-guide.md) — sideload 后 pod 已 Ready 但 InstanceSet 仍 NotReady 这一具体表现的诊断 + 多节点收口;同 digest 多 tag 的成因和通用修法(删 base alias / chart digest pin)参考 [`../agent-collab/addon-patch-image-build-handoff-roles-guide.md`](../agent-collab/addon-patch-image-build-handoff-roles-guide.md) 的 "Sideload + tag 别名陷阱" 一节
- [`addon-reconfigure-set-global-persist-race-guide.md`](addon-reconfigure-set-global-persist-race-guide.md) — reconfigureAction 只跑 `SET GLOBAL` / `CONFIG SET` 等运行时语句不落持久化文件时,遇到引擎进程级 restart(switchover promote / OOMKill / 节点驱逐 / KB 默认 rolling restart)会让 OpsRequest phase=Succeed 但运行时回 chart 默认;防御两层:补 ParametersDefinition + reconfigureAction 同步写 PVC 持久化 override
150 changes: 150 additions & 0 deletions docs/troubleshoot/addon-reconfigure-set-global-persist-race-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# reconfigure 只跑 runtime apply 不持久化 × restart race

> **Audience**: 任何写 addon `reconfigureAction` 的人;常用引擎 `SET GLOBAL` / `CONFIG SET` / `ALTER SYSTEM` / `redis-cli CONFIG SET` 等"运行时生效"语句
> **Status**: stable methodology
> **Applies to**: 任何 addon 的 `reconfigureAction` / `reloadAction` 只用引擎 runtime-apply 语句改参数、不在动作里持久化到引擎重启可读路径的场景
> **Applies to KB version**: KB 1.0+,与 KB 控制器版本无关;触发条件是 addon `reconfigureAction` 写法本身
> **Affected by version skew**: no — addon `reconfigureAction` 写法决定,跨 KB 版本一致
> **Engine-neutral**:引擎相关现场材料见各 case,如 [`../cases/mariadb/mariadb-reconfigure-set-global-without-persist-race-case.md`](../cases/mariadb/mariadb-reconfigure-set-global-without-persist-race-case.md)

## 问题

OpsRequest Reconfigure 报 `phase=Succeed`,但被改的参数在引擎一次进程级 restart 之后悄悄回到 chart 默认值。这一类 false-success 出现的根因是:`reconfigureAction` 只用引擎的"运行时生效"语句把变更打到 in-memory state,并没有把变更落到引擎下一次启动时还能读到的持久化路径;任何 restart 路径(switchover / OOMKill / 节点驱逐 / KB 默认 rolling restart / 引擎自身 reload)都会把 in-memory state 抹掉,回到 chart 默认。

## 先用白话理解这篇文档

OpsRequest Reconfigure 报 `phase=Succeed` 不等于 invariant 持续。如果你的 `reconfigureAction` 只用引擎的"运行时生效"语句改参数(`SET GLOBAL` / `CONFIG SET` 等),但没把变更落到引擎重启后还能读到的文件里,那么遇到下面任何一个 restart 路径都会丢值:

- 用户触发 switchover → 新主 promote 路径里可能重启引擎进程
- 节点 OOMKill → kubelet 重建 container 或拉起新进程
- 节点驱逐 / 维护
- KB 自己因不知道这个参数是 dynamic 还是 static 而默认走 rolling restart
- 引擎自身的 SIGHUP / 重载策略

这篇 guide 给你一个统一的修法框架,让你的 reconfigureAction 通用地避开这一类 race。

## 触发条件

三件事同时成立才出问题:

1. `reconfigureAction` 用引擎的"运行时生效"语句改参数,不落任何持久化文件
2. 引擎进程在 reconfigure 窗口里发生 restart(任何路径)
3. 引擎 restart 时还没读到包含新值的 config 文件(典型 ConfigMap-as-volume kubelet sync 延迟)

## 症状("长这样的 false-success")

复合特征同时出现就大概率是这一类:

- OpsRequest `phase=Succeed`
- ComponentParameter `phase=Finished`、`currentRevision == updateRevision`、`expectedCount == succeedCount`
- addon action attestation 所有 pod 都报 success
- 但业务层 `SHOW VARIABLES` / `CONFIG GET` / `SHOW LIKE` 至少一个 pod 还是 chart 默认值
- K8s container `restartCount=0`,`containerStatuses[].state.startedAt` 没变 ← **这条非常容易误导**

## 反例(不要这样判断)

| 误判 | 反例为什么不成立 |
|---|---|
| "runner 抢答 / 测试时机不对" | bounded wait 后再次查仍是旧值 |
| "kbagent 没在所有 pod 都跑 action" | attestation 显式覆盖所有 pod 都 success |
| "controller succeedCount 算错" | 现场 pod kbagent action stdout 本来就 success;不是计数虚报 |
| "trap #4 image alias 还没清干净" | image identity 已 verify |
| "K8s container restartCount=0,所以没重启" | **容器没重启不等于引擎进程没重启**;进程级 restart 在容器视角是透明的 |

## 区分这一类的关键证据

不能停在 k8s 层。必须进入 container 看进程层:

1. **引擎 error log 的 shutdown / startup 序列**:reconfigure 窗口附近有没有 `shutdown` / `starting` / `ready` 序列?
2. **引擎主进程 PID 是否变化**:`ps -e | grep <engine>` 在 reconfigure 前后是否同一个 PID?
3. **ConfigMap-mounted my.cnf / redis.conf 等的内容 vs mtime**:reconfigure 之后该文件内容是否反映新值?mtime 是什么时候?
4. **比较 config 文件更新时间 vs engine restart 时间**:如果 engine restart 在 config 文件更新之前,就是这一类 race
5. **switchover / role 变化**:reconfigure 窗口里有没有 promote/demote 事件?哪个 pod 变化了?

如果这五条满足"engine 进程重启过 + config 文件落后于 engine restart" → race 成立。

## 修法(两个防御层都要做)

### 防御层 L1:声明清楚 dynamic / static,避免 unnecessary restart

- 给每个 topology / cmpd 写自己的 `ParametersDefinition`,不要共用一份不匹配的
- `componentDef` 字段(regex 或精确)要真匹配你的 cmpd 名字
- `templateName` 要匹配 cmpd 里 `configs[].name`,**不是** ConfigMap object 名(这是常见错位 — Galera 案例已经有这个 trap)
- `staticParameters` / `dynamicParameters` 列表完整覆盖你支持的参数

正确声明后,KB Configure controller 看到 dynamic 参数变更时不会触发 rolling restart,避免大多数 restart 路径。

### 防御层 L2:持久化 reconfigure 值

即使 PD 完美声明,仍有不可控的 restart 路径(OOMKill / 节点驱逐 / 手动维护)。reconfigureAction 必须把新值同时写到引擎重启后能读到的持久化路径。

通用模式:

```sh
# 1. 引擎运行时生效(必做)
engine_cli "SET GLOBAL <param> = <value>"

# 2. 持久化(必做,写到 PVC 等不依赖 ConfigMap sync 的路径)
OVERRIDES_DIR="/var/lib/<engine>/runtime-overrides.d"
mkdir -p "$OVERRIDES_DIR"
override_tmp="$OVERRIDES_DIR/<param>.cnf.tmp.$$"
{
echo "# Written by reconfigureAction on $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "[<engine-section>]"
echo "<param> = <value>"
} > "$override_tmp" && mv "$override_tmp" "$OVERRIDES_DIR/<param>.cnf"
```

关键点:

- **per-param 文件**,独立 reconfigure op 不互相覆盖
- **temp + atomic rename**,被中断不会留半文件让 engine 启动失败
- **WARN-only 失败模式**:写文件失败不让 action fail(SET GLOBAL 已成功,运行时已生效;持久化是 defense-in-depth)
- **写到 PVC 等持久化卷**,不要写到 ConfigMap 挂载的只读路径(写不进去),也不要写到 emptyDir(pod 重建丢失)

### 让 engine 读到持久化文件

写完文件后还要让 engine 启动时读到。两条路径:

**A. 通过 main config 文件 include**:

```ini
# /etc/<engine>/conf.d/main.cnf ← chart 的 ConfigMap 渲染
[<engine-section>]
... chart defaults ...
!includedir /var/lib/<engine>/runtime-overrides.d/ # ← include 在末尾,override 胜出
```

注意 `!includedir` 的语义:包含目录里所有 .cnf 文件,silently 跳过不存在的目录。chart bootstrap 创建空目录以保证 directive 永远 safe。

**B. 通过 engine 启动参数 `--defaults-extra-file`**:

如果引擎不支持 include 语法,可在 cmpd container args 加 `--defaults-extra-file=<persist-path>` 或等效。

## 测试 verify gate(任何 reconfigure fix 都应该过这 4 项)

1. **PD 命中**:`kubectl get parametersdef <name>` Available;`dynamicParameters` / `staticParameters` 包含目标参数
2. **不触发 rolling restart**:reconfigure 前后所有 pod UID + startTime + restartCount + role label 完全一致
3. **OpsRequest Succeed 后所有 pod runtime 立刻反映新值**:bounded wait 不超过 5s
4. **强制 engine 进程 restart 后值仍保留**:手动 `kill -TERM` engine 进程,等被 kbagent / 容器 entrypoint 拉起,再次查 runtime 应该仍是新值(证明持久化文件被 engine 启动加载)

**N≥3 fresh 集群** 4 项全过才算 axis closed。单次过不能写 release-ready。

## 给跨 addon 团队的 4 条可复用经验

1. **任何 runtime-apply 语句都要配持久化备份**。MariaDB `SET GLOBAL`、PostgreSQL `ALTER SYSTEM`(自带持久化但用法要对)、MySQL `SET PERSIST`(自带,对)、Valkey `CONFIG SET`(**不**自带,需要 `CONFIG REWRITE` 跟上)、Redis 同
2. **PD 缺失会让 KB 默认 rolling restart**。每个 topology 上线时检查 PD componentDef regex 是否真的匹配 cmpd
3. **K8s container restartCount=0 不代表引擎进程没重启**。诊断要进 container 看进程层
4. **OpsRequest phase=Succeed 不等于业务 invariant 持续**。测试必须 bounded wait + 再次查询确认未回退

## 相关文档

- [`../cases/mariadb/mariadb-reconfigure-set-global-without-persist-race-case.md`](../cases/mariadb/mariadb-reconfigure-set-global-without-persist-race-case.md) — MariaDB 现场材料
- [`../cases/mariadb/cm1-dynamic-reload-case.md`](../cases/mariadb/cm1-dynamic-reload-case.md) — reloadAction 未落 live PD 的同主题相关案例
- [`../design/addon-reconfigure-guide.md`](../design/addon-reconfigure-guide.md) — reconfigure 设计入口
- [`../test/addon-evidence-discipline-guide.md`](../test/addon-evidence-discipline-guide.md) — "phase Succeed ≠ invariant 持续" 是 evidence-discipline Rule A 的具体形状
- [`../test/addon-bounded-eventual-convergence-guide.md`](../test/addon-bounded-eventual-convergence-guide.md) — bounded wait 而非 single-snapshot 验证

## 一句话总结

`reconfigureAction` 只改运行时状态不落持久化文件时,OpsRequest 成功也可能在一次引擎进程 restart 后回到 chart 默认;修法是声明正确 PD 并把 reconfigure 值写进引擎启动会读取的持久化路径。