在这之前,这个作品一直被同一个短板压着:所有结论都是从官方文档与状态机推导出来的,没有在真机上量过。 文档可以写错、可以滞后,推导可以自洽却与实现不符 —— 这一页就是去把这件事补掉。
我们在三台实体机上装了一套 v8.5.7 集群,把作品里讲的机制逐条在真机上跑了一遍,并把 每一次采集的原始输出留成了文件。下面每一格都能点回那份原始日志 —— 如果一个数字在日志里找不到,那就是我们编的。
09-25 又往前推了一步:五台云服务器(南京 · 8 核 32G · Rocky Linux 9.8)上新建了一套同样的
v8.5.7 集群,把扩容(3 → 5 TiKV)与完整的 Offline → Tombstone 下线跑到兑现,
连同跨 Region 事务与 FOR UPDATE 锁范围两组实验,以及最后的 TSO 分配开销(实测 14)——
此前清单里「没跑过」的四条断言,这一批全部补完。第二批证据留在 dev/cloud-evidence/,其中采样器以 2 秒间隔盯了全程 44 分钟。
这一页现在是第二轮。第一轮采到的现场,被集群自己收走了 —— 那不是故障,是它的常规调度。 我们把数据重灌、证据重采了一遍,并顺手把这件事本身也记了下来。
第一轮采集到的分裂结果很干净:Region 总数从 66 涨到 74,lab.split_demo 变成 8 个 Region。
一个多小时后我们回到同一个接口想复核一遍,那里只剩 6 个 Region。
数据一条没丢。lab.split_demo 的 5,000 行仍然在那儿。丢的是 Region 的边界 ——
PD 的 split-merge-interval 默认是 1 小时,足够小又足够空的 Region 会被它自己合并回去。
分裂和合并都是 PD 的常规调度,合并不是故障。这一点也是量出来才敢说的。
这张曲线是事后从 Prometheus 里取的,不是我们当时的截图 —— 也就是说,第一轮那批数被 PD 自己的监控管道独立记录了下来。 这是这次最值钱的一条副产品:现场值会消失,但另一条管道上的时序不会。 同一段曲线里还留着重启演练的痕迹:09:42:01 停掉一台 TiKV 的那一刻,三台的 Leader 数分别是 35 / 36 / 3,09:43:01 又回到 27 一侧 —— 那次停机演练我们没有留下监控截图,是这条曲线替我们留的。
于是这一轮做了两件事:① 把 lab 库的数据重灌、把分裂重做一遍;
② 本页所有数据层数字,全部换成第二轮(13:06)的读数,并逐条标出采集时刻。
宿主机层那几条(挂载参数、磁盘、工具纠偏)是部署时刻采集的,机器状态已经变了,不可复跑,所以单独标注。
三台机器从零装起,没有复用任何既有数据。集群用的是 TiUP 官方部署方式,拓扑为三节点混部
(每台同时跑 PD / TiKV / TiDB 或监控角色)。下面这些值都取自 dev/probe/ 下的采集原文。
| 项 | 实测值 | 证据 |
|---|---|---|
| 集群名 / 版本 | tidb-lab · v8.5.7 | probe/verify-cluster.txt |
| 节点数与组件 | 3 台机器,共 11 个组件全部 Up | probe/verify-cluster.txt |
| 组件构成 | PD ×3 · TiKV ×3 · TiDB ×2 · Prometheus / Grafana / Alertmanager | probe/verify-cluster.txt |
| 版本一致性 | SQL 侧查 cluster_info:8 个核心组件版本全部为 8.5.7 | probe/verify-cluster.txt · probe/r2-round2.txt |
| SQL 侧产品版本 | 8.0.11-TiDB-v8.5.7(协议版本号 · 产品版本号) | probe/r2-round2.txt |
| 单机硬件 | 8 核 / 内存 32,768 MB / 网卡 10000 MB | probe/tiup-diagnose.txt |
| 数据与日志盘 | 仅用一块 200 GB 盘做成 LVM:/data 158 GB + /log 30 GB | probe/disk.txt |
| 部署用户 | tidb(非 root 运行数据库进程) | probe/verify-cluster.txt |
| 本轮采集时刻 | 2026-09-19 13:06:34 | probe/r2-round2.txt |
| PD 集群引导时刻 | 2026-09-19T09:37:04(与部署当天完全一致) | probe/r2-round2.txt |
| TiKV 进程启动时刻 | 2026-09-19T12:45:20(采集时 uptime 21 分钟) | probe/r2-round2.txt |
说明一件事:这里写的是 3 台机器,而集群报的是 11 个组件 —— 两个数都对。 每台机器上跑着多个角色,所以「机器数」与「组件数」不是一个量;本页凡是提到节点数,都标明是哪一个。
再说一件事:重灌数据不等于重建集群。上面第 10、11 两行就是证据 ——
PD 的 raft_bootstrap_time 仍然是早上 09:37:04,一次都没变过;
变的只是 TiKV 进程在 12:45:20 重启过一次。所以这一轮的「重采」不是换了一套集群,
而是在同一套集群上把数据与读数刷新了一遍。
PEERS 列是 1278, 1279, 1280 —— 三个 peer,不多不少。更硬的一条在 PD 的统计里:每个 store 上的 副本数都是 142,而同一时刻的 Region 总数也是 142 —— 每个 Region 在三台机器上各有一份副本。这不是图示,是从 PD 的接口读出来的。lab.split_demo 写 5,000 行,执行 SPLIT TABLE … REGIONS 8,返回 7,随后查这张表的 Region 列表 —— 确实是 8 个 Region;再建一张 30,000 行的 lab.bulk,REGIONS 60 返回 59。两张表分完,同一时刻 PD 报的 Region 总数从 74 涨到 142。PEERS 依然是 3 个(例如 region 1277 → 1278, 1279, 1280)—— 分裂不降副本数。这条作品里讲了,真机上也对得上。| 时刻 | store 1(.112) | store 5(.110) | store 4(.114) |
|---|---|---|---|
| 停机前 | Up · 50 leaders | Up · 50 leaders | Up · 42 leaders |
| 停机中 | Up · 70 leaders | Up · 72 leaders | Disconnected · 0 leaders |
| 恢复后 | Up · 51 leaders | Up · 50 leaders | Up · 41 leaders |
Disconnected 的降级窗口里继续往 lab.demo 写入,写入成功返回,行数从 6 涨到 7。三副本下剩两个可用副本 = 2/3 多数派。这一次我们仍然没有再去杀第二个副本,所以「再杀一个就阻塞」这半句,本次真机上还是没有验,仍然停留在推导。Disconnected 回到 Up,Leader 从 0 涨到 41;但没有回到停机前的 42。store 5 从停机中的 72 回落到 50,恰好等于它停机前的值;store 1 停在 51。三个数是 51 / 50 / 41,与停机前的 50 / 50 / 42 一个都没对齐。leader_count 是调度过程中的瞬时读数,不能拿来当「均衡已完成」的判据。要看的是「有没有持续往均衡方向走」,不是「某一刻是否相等」。这一条是第一轮没意识到的:第一轮刚好在恢复后读到了与停机前一致的 27 / 27 / 20,于是很自然地写成了「回到与停机前一致」—— 那是运气,不是规律。第二轮把同一件事又做了一遍,才把它区分出来。Offline → Tombstone 下线状态机。这里原先写着「缩容下限保护没有在真机上跑」—— 那句话在 09-20 补采后不再成立:保护已经实跑,且比我们猜的更前置(见实测 09);而完整的 Offline → Tombstone 状态机在 3 节点集群上被同一道保护拦在入口、走不进去,这条边界在实测 09 里写清楚了。tikv.toml;这一轮直接把 TiKV 进程自己上报的运行时配置读出来(/config 接口),并同时向 PD 核对。两处都对得上:共享 block cache 容量 4GiB、Region 分裂阈值 256MiB、raftstore 容量 120GiB(PD 侧三台的 capacity 也都是 120GiB)。换一个提问对象,答案一样 —— 这比只读一遍配置文件可信得多。nodelalloc 属于部署前的硬性检查项。/etc/fstab 不等于挂载生效 —— 必须回读内核实际挂载参数。三台机器的 /proc/mounts 实测:/data 为 rw,noatime,nodelalloc,data=ordered,/log 为 rw,noatime,data=ordered;并另外做了写入测试(/data、/log 均可写)。@@tidb_txn_mode = pessimistic、@@transaction_isolation = REPEATABLE-READ。BEGIN OPTIMISTIC 读同一行、各自加一。先提交的成功,后提交的被拒绝,拿到原生错误码 9007 与原文 —— 而不是悄悄把对方覆盖掉。冲突发生在这个版本上是在 提交阶段报出来的。UPDATE 这一步就被阻塞,一直等到前一个事务提交才继续 —— 本次实测等了 3.006s。也就是说,默认模式把冲突从「提交时报错」提前到了「加锁时排队」。store delete 4,PD 在入口直接拒绝,命令没有生效。随后一直到 18:35:01 的观察窗(多次采样,末次为 [T+420s]):store 4 状态恒为 Up、store check offline 恒为空、operators 恒为空 —— 没有任何进行中的调度操作,命令确实没有产生任何影响。Offline → 因没有可迁入的目标而滞留下来。实测是:命令被拒绝,压根没进状态机 —— 保护发生在入口,比预设的更前置。前面三处纠偏是「工具撒谎」,这一条是我们自己的预期撒谎:所以它也被记进这一页,而不是悄悄修掉。store cancel-delete 4 想撤销,回执是 store 4 is not offline —— 连撤销都无从谈起,反证它从未下线。另一条边界由此确认:完整走一遍 Offline → Tombstone 需要 ≥ 4 台 TiKV(留出迁入目标),本集群规模所限走不进去,那个状态机在当时只有文档支撑。Offline → Tombstone 完整走通,而且走了两条路(TiUP 原生 / pd-ctl 手动) —— 见下面的实测 11。
09-20 清点边界时,「没跑过」的还剩四条:跨 Region 多行事务、SELECT … FOR UPDATE 锁范围、TSO 分配开销,
以及完整的 Offline → Tombstone 下线状态机 —— 最后一条在 3 节点集群上被缩容下限保护拦在入口(见实测 09),
要至少 4 台 TiKV 才走得进去。
09-25,五台云服务器到位(南京 · 8 核 32G · Rocky Linux 9.8 · 数据盘 200 GB)。同一套 v8.5.7 从 3 个 TiKV 扩到 5 个,
四条被逐个走进真机 —— 最后一条 TSO 分配开销用「本底扣除法」三段实验收尾(实测 14)。全程有一台采样器以 2 秒间隔盯着 PD 的 store 列表:
从 12:01:30 到 12:46:01,共 1334 个采样点,落在 dev/cloud-evidence/samples.jsonl。
先把边界钉在这里:这五台服务器在当天实验结束后就释放了 —— 本节证据是一次性快照,无法复跑;
按 dev/cloud-exp-*.sh 可以重做实验,但重做的不是这份数据。
数字之外,留一段画面:这是那台云集群自己的 TiDB Dashboard —— 从登录页进到「概况」, 集群信息(实例清单与版本)、吞吐与延迟曲线、CPU / 内存 / 磁盘 IO 面板、各组件健康状态, 全部是当时界面上的原生呈现。不是模拟器重放,也不是动画。
录屏与本节同一次实验(内网 10.206.16.x);服务器当天已释放 —— 一次性,无法重录。
Up → Offline → Tombstone 的流水线 —— 先把 Region 全部搬走,最后退掉身份。09-20 那条「3 节点走不进去」的边界,这一批补上。The component `tikv` will become tombstone, maybe exists in several minutes or hours, after that you can use the prune command to clean it。采样器随后记录:12:13:57 转 Offline(时剩 71 个 Region),接着每 2 秒降一批 —— 71 → 62 → 55 → 45 → 38 → 27 → 22 → 13 → 5 → 2 → 0(12:14:17),20 秒搬空,之后从默认 store 列表消失。约半小时后(12:40:53)执行 tiup cluster prune,日志里出现 + [ Serial ] - RemoveTomestoneNodesInPD 与 Destroy success,完成后 Total nodes: 12 —— prune 能清掉它,反证它确实进过 Tombstone。pd-ctl store delete 1058 手动下线:12:44:02 转 Offline(31 个 Region),12 秒归零,12:44:18 是它在默认列表里的最后一次出现;25 秒后 —— 12:44:43.766,采样器在 ?state=2 查询里第一次读到 "tombstones": [1058]。12:45:04 又跑了一次 prune,但六分钟后(12:51:03)的终态快照里它原样还在:手动路径不在 TiUP 的账本里,清不掉 —— 释放前的最后一份 display 里,那行 Tombstone 因此保留。/stores 不返回 Tombstone,要加 ?state=2 才看得见。想观察下线,先得知道该问哪个接口 —— 「先怀疑仪器」的又一例。SHOW TABLE REGIONS 的原始输出里解析出三行的归属 —— id=1000 → region 290 [t_114_, t_114_r_7813)、id=200000 → region 390、id=300000 → region 442。三个主键落在三个互不相邻的 Region 上。ERROR 1205 (HY000) at line 1: Lock wait timeout exceeded; try restarting transaction。回查第一步的结果:id=300000 仍是初始值 xxxxxxxxxx。第一步虽然跑完了,事务整体回滚,没有留下半笔提交。SELECT … FOR UPDATE 的锁行为提示:点查只锁目标行,相邻行不受影响;范围条件会锁住范围内已存在的行。官方文档未展开 FOR UPDATE 的具体加锁范围(见 10 号病理页 B3 边界),本组结论以实测为准。FOR UPDATE(持锁中),在 information_schema.data_lock_waits 里直接读出等待链:锁键 7480000000000000725F7280000000000003E8,其 KEY_INFO 里写着 "handle_value":"1000" —— 锁的对象就是那一行。行为验证:B 改 id=1000 → 等到超时拿 1205;C 改左邻 id=999、D 改右邻 id=1001 → 分别 9 毫秒、8 毫秒返回,没被挡。id BETWEEN 1000 AND 1005 做 FOR UPDATE:B 改范围内的 id=1003 → 1205;C 改范围外的 id=2000 → 9 毫秒通过。start_ts 与 commit_ts 两次时间戳都从 PD 获取;TSO 是全局逻辑时钟、按批次分配以摊还开销。此前这一条只有官方文档与推导支撑,本批补上。pd_client_request_handle_tso_batch_size 直方图与服务端计数器(Prometheus 15 秒采样粒度)。| 读数时刻 | 来源 | Region 总数 | 当时在做什么 |
|---|---|---|---|
| 09:40:01 | Prometheus 时序 | 74 | 第一轮 SPLIT 之后 |
| 10:40:01 | Prometheus 时序 | 6 | 1 小时后被 PD 合并完成 |
| 13:05:01 | Prometheus 时序 | 15 | 重灌之前 |
| 13:06:34 | PD HTTP 接口 | 74 | 重灌之前的基线读数 |
| 13:06(分裂后) | PD HTTP 接口 | 142 | 两张表重分裂之后 |
| 13:17:19 | PD HTTP 接口 | 145 | 收尾复核 |
其中有一处我们没能解释清楚,也不打算编:
13:05:01 的 Prometheus 读数是 15,13:06:34 的 PD 接口读数是 74,
两者只差 89 秒,却来自两条不同的管道 —— 一条是抓取周期上的上报值,一条是按需计算值。
我们没有做到能证明「这 59 个 Region 是在哪一刻出现的」,所以这里只记读数,不给因果。
它换来的是这一页的一条硬规矩:凡写现场数字,必须同时写清「哪个仪器、哪一刻」。 只写数字不写来源,读者就无法判断它属于哪个瞬间 —— 这正是这个作品一开始就在反对的事。 而这四个数字彼此都不矛盾:它们只是四个不同瞬间的照片。
如果这次实测只告诉我「集群跑起来了」,那它的价值很有限。 真正值得记下来的是:同一个动作,官方检查工具给出的结论与实机状态三次不一致, 而三次里对的都是实机、错的是工具。
这恰好是这个作品一开头就在讲的那句话 —— 把机制画对只是及格线,难的是凭什么相信它是对的。 工具也是被相信的对象之一,它同样会错。
Warn: invalid signature for file root.json: not enough signatures (2) for threshold 3 in root.jsonError: no trusted root in the local manifestsroot.json 是通的(HTTP 200,7,275 字节),只是本地 manifests 目录里躺着过期的 3.root.json / 4.root.json。os-version Fail CentOS Linux 7 (Core) 7.9.2009 not supported, use version 9 or higheractive (running),端口 20160 在监听,二进制版本号 8.5.7,动态库检查 ldd: no missing libs,日志里正在正常做副本变更与快照。Down,而其余组件正常 Up。start 于 09:37:02 发起、状态查询于 09:37:12 执行,而 TiKV 进程是在 09:37:03 才起来的 —— 也就是说,查询只比进程就绪晚了 9 秒。这 9 秒里 TiKV 正在做「注册 + 组副本 + 传快照」,上报给 PD 的健康状态还没落地,界面上就先显示成不可用。Up。「查询时刻」本身会污染结论,这是这次最容易被忽略的一处。cloud-evidence/ 里的 1334 个采样点与全部原始输出无法再复跑 ——
按 dev/cloud-exp-*.sh 可以重做同一组实验,但重做的不是同一份数据。
这一条与宿主机层那几条的区别是:连「机器状态」本身都不存在了。
FOR UPDATE 锁范围(实测 12 / 13)都在真机上撞过;
演示台 02 这一块,「缩容下限保护」(实测 09)与完整的 Offline → Tombstone 状态机(实测 11,两条路径)也都走通了;
最后一条 TSO 分配开销(实测 14)用「本底扣除法」三段实验补上了批合并与时间戳对账 ——
量到的是机制(批分配真的发生),不是性能:单次分配耗时的分布仍不出结论,本作品仍然不出任何性能结论。
原始采集输出全部留在仓库里,不经改写。要自己核对,直接打开对应文件即可。
| 证据文件 | 里面是什么 | 本页哪几条用到 |
|---|---|---|
| probe/r2-round2.txt | 第二轮主证据:重灌与重分裂全过程、三个时刻的 store 原始 JSON、降级期间写入、TiKV 进程自报的运行时配置 | 身份卡 · 实测 01–06 |
| probe/r2-history.txt | Prometheus 时序(另一条独立管道):Region 总数与各 store Leader 数的逐分钟变化点 | 第二轮由来 · 判据 R1 / R4 |
| probe/r2-txn.txt | 真机跑的写写冲突实验原文:乐观 9007 报错原文 / 悲观等锁 3.006s / 被拒后重试 | 实测 08 |
| probe/r1-offline.txt | 缩容下限保护实跑原文(09-20 补采):下线命令被 PD 入口拒绝的原始报错、观察窗内 store 恒 Up / offline 与 operators 恒空、撤销回执 | 实测 09 |
| probe/r2-now.txt | 收尾时再读一次的现场(三个只读接口的原始响应 + 当场算出的摘要) | “没查清的事”里的第 6 行读数 |
| probe/verify-cluster.txt | 集群状态总览、SQL 连通性、cluster_info 版本一致性、Region 详情 | 身份卡 |
| probe/tiup-diagnose.txt | 签名问题诊断、清单缓存文件、清缓存后逐项检查结果 | 身份卡 · 纠偏 01 · 02 |
| probe/cluster-check.txt | 签名报错原文 | 纠偏 01 |
| probe/tikv-diag.txt | systemd 状态、端口监听、进程、二进制版本与动态库 | 纠偏 02 · 03 |
| probe/verify-mounts.txt | 三台机器的 /proc/mounts、fstab、写测试、LVM 列表 | 实测 07 |
| probe/disk.txt | 单盘 → VG/LV 创建过程与挂载结果 | 身份卡 |
| probe/start.txt | 启动后即查的状态(TiKV 显示 Down 的那一次) | 纠偏 03 |
| cloud-evidence/samples.jsonl | 云集群采样器:2 秒粒度盯 PD store 列表 44 分钟,1334 条采样 —— 798 与 1058 的完整生命周期、1058 的 Tombstone 首现 | 实测 10 · 11 |
| cloud-evidence/b2b3-txn.txt | 跨 Region 事务与 FOR UPDATE 锁范围的全过程:region 归属解析、1205 原文、data_lock_waits 视图、命令耗时对照 | 实测 12 · 13 |
| cloud-evidence/tso-b4-20260925-144942.txt | TSO 三段实验(本底扣除法):空闲 / 串行 / 并发三窗的批直方图与时间戳对账,脚本 stdout 全文 | 实测 14 |
| cloud-evidence/final-state.txt | 释放前的终态快照:display(含 Tombstone 行)、store 列表、tombstone ids、数据完整性与版本 | 实测 11 |
| cloud-evidence/scalein-console.txt | TiUP 原生下线全程输出(含「will become tombstone」提示原文) | 实测 11 |
| cloud-evidence/scaleout-console.txt | 3 → 5 扩容全程输出(Scaled cluster … out successfully) | 实测 10 |
| cloud-evidence/rescaleout-console.txt | 1058 重新上线全程输出 | 实测 11 |
| cloud-evidence/events.jsonl | 实验操作时间线(20 条人工记录) | 云集群一节 |
| _cloud_prune.txt | prune 原文(RemoveTomestoneNodesInPD / Destroy success)与完成后 display 读数 | 实测 11 |
另外两份文件本身就是第一轮的历史,保留在仓库里但不被本页引用:
probe/demo.txt 与 probe/failover.txt —— 它们记录的正是被 PD 合并掉的那批现场。
把它们删掉会更整齐,但那样这一页就只剩结论、没有来历了。
这一页的每一个数字,都由一个脚本对着上述原始日志做机器比对 —— 页面写的值如果在日志里找不到,构建就会失败。它不是自我评价,是一道会拦人的关。
核验实录管的是「数字有没有官方出处」—— 每一格都能点开官方原文与适用版本。 本页管的是「机制在真机上是不是这样」—— 每一格都能回到原始日志。 自助核验管的是「你自己怎么验一遍」—— 从点到重出。 现场直连管的是「现在这一刻是什么样」—— 它是一台仪器,不存任何实测值,打开就去读活集群。
四页叠起来才是完整的可信链:出处 → 实测 → 可复跑 → 现场。任何一环缺失,可信度都会掉一个等级。
dev/probe/ 与 dev/cloud-evidence/,未经改写;自建集群条目可在同一套拓扑上复跑核对,宿主机层条目为部署时刻快照、云集群条目为释放前的一次性快照,均不可复跑。