Field notes · 3 节点实验室集群 + 5 节点云集群

真机上跑一遍
把「文档这么写」变成「我量到是这样」

在这之前,这个作品一直被同一个短板压着:所有结论都是从官方文档与状态机推导出来的,没有在真机上量过。 文档可以写错、可以滞后,推导可以自洽却与实现不符 —— 这一页就是去把这件事补掉。

我们在三台实体机上装了一套 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 分钟。

这一页现在是第二轮。第一轮采到的现场,被集群自己收走了 —— 那不是故障,是它的常规调度。 我们把数据重灌、证据重采了一遍,并顺手把这件事本身也记了下来。

3 台真机 · 11 个组件全部 Up v8.5.7 8 核 / 32,768 MB 万兆内网 单盘 200 GB → /data + /log 云集群 09-25 · 5 台 · 3 → 5 TiKV 14 条实测 + 3 处工具纠偏 重采 + 补采 + 云集群 · 09-19 / 09-20 / 09-25

这一页为什么有第二轮

第一轮的现场,被集群自己收走了

第一轮采集到的分裂结果很干净: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_cluster_status{type="region_count"},store=1 / store=4 / store=5 三个序列同步 09:38:01 65 09:40:01 74 ← 第一轮 SPLIT 之后 10:38:01 17 ← 1 小时到了,PD 开始合并 10:39:01 16 10:40:01 6 ← 合并完成:只剩 6 个 13:05:01 15 ← 这一轮重灌之前的现场

这张曲线是事后从 Prometheus 里取的,不是我们当时的截图 —— 也就是说,第一轮那批数被 PD 自己的监控管道独立记录了下来。 这是这次最值钱的一条副产品:现场值会消失,但另一条管道上的时序不会。 同一段曲线里还留着重启演练的痕迹:09:42:01 停掉一台 TiKV 的那一刻,三台的 Leader 数分别是 35 / 36 / 3,09:43:01 又回到 27 一侧 —— 那次停机演练我们没有留下监控截图,是这条曲线替我们留的。

于是这一轮做了两件事:① 把 lab 库的数据重灌、把分裂重做一遍; ② 本页所有数据层数字,全部换成第二轮(13:06)的读数,并逐条标出采集时刻。 宿主机层那几条(挂载参数、磁盘、工具纠偏)是部署时刻采集的,机器状态已经变了,不可复跑,所以单独标注。

证据:probe/r2-history.txt(Prometheus 时序原文)、probe/r2-round2.txt(第二轮 R2-0 / R2-1)

先交代清楚:这是什么集群

免得被当成生产环境

三台机器从零装起,没有复用任何既有数据。集群用的是 TiUP 官方部署方式,拓扑为三节点混部 (每台同时跑 PD / TiKV / TiDB 或监控角色)。下面这些值都取自 dev/probe/ 下的采集原文。

项实测值证据
集群名 / 版本tidb-lab · v8.5.7probe/verify-cluster.txt
节点数与组件3 台机器,共 11 个组件全部 Upprobe/verify-cluster.txt
组件构成PD ×3 · TiKV ×3 · TiDB ×2 · Prometheus / Grafana / Alertmanagerprobe/verify-cluster.txt
版本一致性SQL 侧查 cluster_info:8 个核心组件版本全部为 8.5.7probe/verify-cluster.txt · probe/r2-round2.txt
SQL 侧产品版本8.0.11-TiDB-v8.5.7(协议版本号 · 产品版本号)probe/r2-round2.txt
单机硬件8 核 / 内存 32,768 MB / 网卡 10000 MBprobe/tiup-diagnose.txt
数据与日志盘仅用一块 200 GB 盘做成 LVM:/data 158 GB + /log 30 GBprobe/disk.txt
部署用户tidb(非 root 运行数据库进程)probe/verify-cluster.txt
本轮采集时刻2026-09-19 13:06:34probe/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
Cluster type: tidb Cluster name: tidb-lab Cluster version: v8.5.7 Deploy user: tidb Total nodes: 11

说明一件事:这里写的是 3 台机器,而集群报的是 11 个组件 —— 两个数都对。 每台机器上跑着多个角色,所以「机器数」与「组件数」不是一个量;本页凡是提到节点数,都标明是哪一个。

再说一件事:重灌数据不等于重建集群。上面第 10、11 两行就是证据 —— PD 的 raft_bootstrap_time 仍然是早上 09:37:04,一次都没变过; 变的只是 TiKV 进程在 12:45:20 重启过一次。所以这一轮的「重采」不是换了一套集群, 而是在同一套集群上把数据与读数刷新了一遍。

14 条实测

作品里讲的机制,逐条在真机上量一遍
实测 01三副本是真的存在,而且真的分布在三台机器上第二轮 · 13:06
作品断言
演示台 01:一次写入在 3 个副本之间达成一致,3 副本只能容忍 1 个副本故障。
实机实测
任取一个 Region(id 1277),它的 PEERS 列是 1278, 1279, 1280 —— 三个 peer,不多不少。更硬的一条在 PD 的统计里:每个 store 上的 副本数都是 142,而同一时刻的 Region 总数也是 142 —— 每个 Region 在三台机器上各有一份副本。这不是图示,是从 PD 的接口读出来的。
与第一轮的关系
第一轮量到的是「store 各 66 个副本、PEERS = 21, 179, 271」;第二轮重灌后总量变了,但结论一模一样:副本数恒等于 Region 总数,三份,分布在三台。
REGION_ID LEADER_ID LEADER_STORE_ID PEERS 1277 1278 1 1278, 1279, 1280 … # PD 统计:每个 store 上的副本数(三个 store 相同) "store_peer_count": { "1": 142, "4": 142, "5": 142 }, # PD 统计:每个 store 上的 region 数(三台相同) "region_count": 142,
证据:probe/r2-round2.txt(R2-2 §2.2 Region 列表 / R2-3 stores 原始 JSON)
实测 02Region 分裂真的会发生,而且分裂后仍然是 3 副本第二轮 · 13:06
作品断言
演示台 02:数据长大了 Region 会自己分家,分裂不是故障,是长大。
实机实测
这一轮是从零重做的:先建 lab.split_demo 写 5,000 行,执行 SPLIT TABLE … REGIONS 8,返回 7,随后查这张表的 Region 列表 —— 确实是 8 个 Region;再建一张 30,000 行的 lab.bulk,REGIONS 60 返回 59。两张表分完,同一时刻 PD 报的 Region 总数从 74 涨到 142。
关键细节
每个新 Region 的 PEERS 依然是 3 个(例如 region 1277 → 1278, 1279, 1280)—— 分裂不降副本数。这条作品里讲了,真机上也对得上。
--- 2.1 split_demo: 5000 rows + SPLIT 8 --- 5000 7 1 REGION_ID START_KEY END_KEY PEERS 1277 t_124_ t_124_r_12500 1278, 1279, 1280 1281 t_124_r_12500 t_124_r_25000 1282, 1283, 1284 …(共 8 行) --- 2.3 bulk: 30000 rows + SPLIT 60 --- 30000 59 1 # 两张表分完后,同一时刻 PD 的统计 "count": 142, "empty_count": 140,
证据:probe/r2-round2.txt(R2-2 §2.1–2.4)
实测 03停掉一台 TiKV,Leader 真的会自己搬走第二轮 · 13:06
作品断言
演示台 02:节点下线时系统会自救,把 Leader 迁到存活节点上。
实机实测
停掉 192.168.3.114 的 TiKV(store 4),同一个 store 集合在三个时刻的 Leader 数变化如下 —— 下线的 store 4 从 42 掉到 0,另两台同步涨到 70 / 72。
顺带量到
停机前 store 4 只有 42 个 Leader,而另两台各 50。原因不是它弱,而是它 13:05 刚重启过、调度还没收完 —— 这本身就是「均衡是一个持续过程」的证据,见实测 05。
时刻store 1(.112)store 5(.110)store 4(.114)
停机前Up · 50 leadersUp · 50 leadersUp · 42 leaders
停机中Up · 70 leadersUp · 72 leadersDisconnected · 0 leaders
恢复后Up · 51 leadersUp · 50 leadersUp · 41 leaders
# R2-3 停机前:三个 store 的 leader_count "1": 50, # store 1(.112) "4": 42, # store 4(.114) "5": 50, # store 5(.110) # R2-4 §4.3 停机中 "state_name": "Disconnected", # store 4 "leader_count": 0, # store 4 "leader_count": 72, # store 5 "leader_count": 70, # store 1 # R2-4 §4.6 恢复后 "leader_count": 51, # store 1 "leader_count": 41, # store 4
证据:probe/r2-round2.txt(R2-3 停机前 / R2-4 §4.3 停机中 / §4.6 恢复后)
实测 04少了一个副本,写入照样成功 —— 多数派还在第二轮 · 13:06
作品断言
演示台 01:写入照常成功 —— 因为多数派还在。再杀一个,写入才会阻塞。
实机实测
在 store 4 处于 Disconnected 的降级窗口里继续往 lab.demo 写入,写入成功返回,行数从 6 涨到 7。三副本下剩两个可用副本 = 2/3 多数派。这一次我们仍然没有再去杀第二个副本,所以「再杀一个就阻塞」这半句,本次真机上还是没有验,仍然停留在推导。
--- 4.4 write during degradation --- 7 # 降级期间写入后的行数:写入成功
证据:probe/r2-round2.txt(R2-4 §4.4)
实测 05「恢复后回到均衡」是一个过程,不是一个瞬间第二轮 · 13:06
作品断言
演示台 02:下线是可控过程,恢复后集群会回到稳定分布。
实机实测
重新启动 store 4,它从 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 里写清楚了。
证据:probe/r2-round2.txt(R2-3 停机前 / R2-4 §4.6 恢复后)
实测 06写进拓扑的容量参数,真的落到了进程里第二轮 · 13:06
为什么查这条
混部集群最容易犯的错是「配置写了、进程没吃到」—— 文件改了但服务没重启、参数名写错、被默认值覆盖,都能做出一个看起来配好了的假象。
这一轮换了查法
第一轮是去读各节点的 tikv.toml;这一轮直接把 TiKV 进程自己上报的运行时配置读出来(/config 接口),并同时向 PD 核对。两处都对得上:共享 block cache 容量 4GiB、Region 分裂阈值 256MiB、raftstore 容量 120GiB(PD 侧三台的 capacity 也都是 120GiB)。换一个提问对象,答案一样 —— 这比只读一遍配置文件可信得多。
# PD 侧:三个 store 的容量(R2-3) "capacity": "120GiB", "available": "118.5GiB", # TiKV 进程自报的运行时配置(R2-5,从 /config 过滤摘录) "capacity":"4GiB" # storage.block-cache "capacity":"120GiB" # raftstore "region-split-size":"256MiB"
证据:probe/r2-round2.txt(R2-3 stores 原始 JSON / R2-5 进程自报配置)
实测 07「必检项」的挂载参数,是量出来的不是配出来的部署时刻 · 不可复跑
作品断言
TiKV 对数据盘文件系统有明确要求,nodelalloc 属于部署前的硬性检查项。
实机实测
写 /etc/fstab 不等于挂载生效 —— 必须回读内核实际挂载参数。三台机器的 /proc/mounts 实测:/data 为 rw,noatime,nodelalloc,data=ordered,/log 为 rw,noatime,data=ordered;并另外做了写入测试(/data、/log 均可写)。
为什么标「不可复跑」
这一条是部署当天采的。要复跑就得重新挂载文件系统,那会动到一台正在运行的集群 —— 所以它只作为部署时刻的事实存在,与上面几条数据层的可复跑程度不是一个等级。
# 三台机器 /proc/mounts 的实测行(原样) /dev/mapper/tidbvg-lv_data /data ext4 rw,noatime,nodelalloc,data=ordered 0 0 /dev/mapper/tidbvg-lv_log /log ext4 rw,noatime,data=ordered 0 0 --- nodelalloc present on /data? --- YES /data has nodelalloc
证据:probe/verify-mounts.txt(三台 /proc/mounts + fstab + 写测试)
实测 08写写冲突:被拒绝的是事务,不是数据第二轮 · 13:09 新增
为什么要补这条
演示台 03 讲的是两阶段提交,但「两个事务改同一行会怎样」这件事,第一轮只在状态机上推过,没在真机上撞过。这一轮补上 —— 这是本页第一次出现事务层的实测。
前置
这套集群的默认值是 @@tidb_txn_mode = pessimistic、@@transaction_isolation = REPEATABLE-READ。
实验一 · 乐观模式
两个会话都用 BEGIN OPTIMISTIC 读同一行、各自加一。先提交的成功,后提交的被拒绝,拿到原生错误码 9007 与原文 —— 而不是悄悄把对方覆盖掉。冲突发生在这个版本上是在 提交阶段报出来的。
实验二 · 悲观模式(默认)
后到的会话在 UPDATE 这一步就被阻塞,一直等到前一个事务提交才继续 —— 本次实测等了 3.006s。也就是说,默认模式把冲突从「提交时报错」提前到了「加锁时排队」。
实验三 · 冲突 ≠ 丢更新
把被拒绝的那个事务重试一次:值从 1 → 2 → 3,两次加一都生效,一次都没丢。被拒绝的是事务,不是数据。这一条比「冲突会报错」更重要 —— 前者是机制,后者是后果。
@@tidb_txn_mode = pessimistic @@transaction_isolation = REPEATABLE-READ # 实验一:乐观模式的写写冲突(原文摘录,超长部分截断) (9007, 'Write conflict, txnStartTS=469183914540531717, conflictStartTS=…, key={tableID=135, tableName=lab.txn_demo, handle=1}, reason=Optimistic [try again later]') # 实验二:悲观模式等锁 B: UPDATE 在 A 提交后成功返回 waited=3.006s # 实验三:被拒后重试一次,两行最终值 ((1, 3), (2, 1))
证据:probe/r2-txn.txt(R2-TXN 实验一 / 二 / 三)
实测 09缩容下限保护:不是「下线后滞留」,是「入口直接拒绝」补采 · 2026-09-20 18:28
作品断言
演示台 02:缩容必须先等 Region 迁走再下线节点;三副本集群不允许缩到无法维持副本数的规模(官方口径:总 TiKV 实例数总是要大于等于设置的副本数)。
实机实测
对三节点 3 副本集群(每台一个 TiKV)执行 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(留出迁入目标),本集群规模所限走不进去,那个状态机在当时只有文档支撑。
后续(09-25 更新)
这条边界当天就被走穿了:云集群扩到 5 个 TiKV 之后,Offline → Tombstone 完整走通,而且走了两条路(TiUP 原生 / pd-ctl 手动) —— 见下面的实测 11。
# R1-2 ISSUE: store delete 4 · 2026-09-20 18:28:00 —— 原文 Failed to delete store 4: [400] "[PD:core:ErrStoresNotEnough]can not remove store 4 since the number of up stores would be 2 while need 3" # R1-3 · 观察窗末次采样 [T+420s]:store 4 仍 Up,offline 仍空,operators 仍空 "state_name": "Up" "count": 0 # store check offline:空 [] # operators:空 # R1-4 · 事后尝试撤销 —— 原文 store 4 is not offline
证据:probe/r1-offline.txt(R1-2 拒绝原文 / R1-3 观察窗 / R1-4 撤销回执)

云集群 · 09-25

五台云服务器上,把「没跑过」补成了实测

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 可以重做实验,但重做的不是这份数据。

现场录屏 · 云集群 Dashboard 巡览(登录 → 概览)

数字之外,留一段画面:这是那台云集群自己的 TiDB Dashboard —— 从登录页进到「概况」, 集群信息(实例清单与版本)、吞吐与延迟曲线、CPU / 内存 / 磁盘 IO 面板、各组件健康状态, 全部是当时界面上的原生呈现。不是模拟器重放,也不是动画。

录屏与本节同一次实验(内网 10.206.16.x);服务器当天已释放 —— 一次性,无法重录。

实测 10扩容之后,新节点是一格一格接收 Region 的云集群 · 09-25
作品断言
演示台 02 / loop 模拟器 scale-out:扩容引入新 TiKV 后,PD 把 Region 逐步调度过去 —— 再平衡是以分钟计的持续过程,不是瞬间完成。
实机实测
从 3 个 TiKV 扩到 5 个(新加 tidb-04、tidb-05)。采样器对新 store 798 的完整记录:12:03:14 首次可见(Down)→ 12:03:24 转 Up,随后开始接收 Region —— 起步先抖了一下(1 → 0 → 2),从 12:03:32 起每 2 秒 +2 个稳定爬升:2 → 4 → 6 → … → 46(12:04:16);继续攀到 63(12:05:00)后放缓,在 12:07:58 到达 74。从 Up 到 74,历时 4 分 34 秒。
关键细节
74 不是过路值:从 12:07:58 到 12:13:55,每 2 秒一次共 179 个采样点,全部是 74,一次都没变。也就是说,「稳定」与「均衡」不是一回事 —— 迁到 74 之后它稳住了,而不是继续向理论均分值推进。(随后这个 store 被主动下线,曲线就此打断,见实测 11。)
# samples.jsonl · 798 的关键帧(每次采样一行,节选字段) {"ts": "2026-09-25T12:03:14.233", "stores": [ … {"id": 798, "addr": "10.206.16.14:20160", "state_name": "Down", "region_count": 0, "leader_count": 0, "capacity": "0B", …} … ]} {"ts": "2026-09-25T12:03:24.253", "stores": [ … "state_name": "Up", "region_count": 0, "leader_count": 0, "capacity": "120GiB", "available": "118.5GiB", …} … ]} {"ts": "2026-09-25T12:07:58.751", "stores": [ … "state_name": "Up", "region_count": 74, "leader_count": 34, "capacity": "120GiB", "available": "118.4GiB", …} … ]} {"ts": "2026-09-25T12:13:09.317", "stores": [ … "state_name": "Up", "region_count": 74, "leader_count": 34, …} … ]} # 12:13:57 起转为 Offline 并逐格搬空,见实测 11
证据:cloud-evidence/samples.jsonl(798 的 332 个采样点;全文件 1334 点)
实测 11完整的 Offline → Tombstone:走了,而且走了两条路云集群 · 09-25
作品断言
演示台 02 / loop 模拟器 scale-in:节点下线不是「删除」,而是一条 Up → Offline → Tombstone 的流水线 —— 先把 Region 全部搬走,最后退掉身份。09-20 那条「3 节点走不进去」的边界,这一批补上。
路径 ① · TiUP 原生下线
对 798 执行 scale-in,TiUP 当场提示: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 手动下线
把同一台机器重新 scale-out 为 store 1058(12:43:16 首次可见 Down,12:43:26 转 Up),再用 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 因此保留。
数据无损
两条路径走完,最后一份快照里 100 万行数据一个不少,payload 表的 129 个 Region 也都在。
工具视角的注脚
798 的「消失」与 1058 的「出现」,是同一种状态的两张面孔:PD 默认的 /stores 不返回 Tombstone,要加 ?state=2 才看得见。想观察下线,先得知道该问哪个接口 —— 「先怀疑仪器」的又一例。
# scalein-console.txt(TiUP 原生提示,原文) The component `tikv` will become tombstone, maybe exists in several minutes or hours, after that you can use the prune command to clean it # 798 的 Offline 起点与终点(samples.jsonl) {"ts": "2026-09-25T12:13:57.402", "stores": [ … {"id": 798, "addr": "10.206.16.14:20160", "state_name": "Offline", "region_count": 71, "leader_count": 32, …} … ]} {"ts": "2026-09-25T12:14:17.438", "stores": [ … {"id": 798, "addr": "10.206.16.14:20160", "state_name": "Offline", "region_count": 0, "leader_count": 0, …} … ]} # 1058 首次以 Tombstone 现身(samples.jsonl,?state=2) {"ts": "2026-09-25T12:44:43.766", "stores": [ … {"id": 799, "addr": "10.206.16.4:20160", …} … ], "tombstones": [1058]} # _cloud_prune.txt(prune 原文 + 完成后 display 读数) + [ Serial ] - RemoveTomestoneNodesInPD Destroy success Total nodes: 12 # final-state.txt(释放前最后一份快照,12:51:03) 10.206.16.14:20160 tikv 10.206.16.14 20160/20180 linux/x86_64 Tombstone /data/tidb-data/tikv-20160 /data/tidb-deploy/tikv-20160 | 1000000 | | 129 |
证据:cloud-evidence/scalein-console.txt · cloud-evidence/samples.jsonl · cloud-evidence/final-state.txt · _cloud_prune.txt
实测 12跨 Region 的两行事务:要么一起生效,要么一起消失云集群 · 09-25
作品断言
演示台 03:两阶段提交保证跨 Region 多行事务的原子性 —— 中途任何一步失败,整体回滚,不存在「前一半生效了」的中间态。
前置 · 先量「真的跨 Region」
不假设,先查:从 SHOW TABLE REGIONS 的原始输出里解析出三行的归属 —— id=1000 → region 290 [t_114_, t_114_r_7813)、id=200000 → region 390、id=300000 → region 442。三个主键落在三个互不相邻的 Region 上。
正例 · 一起生效
一个事务里依次改 id=1000、id=200000 再提交 —— 两行同时可见(CROSS-A / CROSS-B)。
反例 · 一起消失(这条更硬)
会话 A 先锁住 id=1000(持锁 25 秒);会话 B 开事务,第一步改 id=300000 已经执行成功,第二步改 id=1000 撞上锁 —— 等满后拿到原生报错 ERROR 1205 (HY000) at line 1: Lock wait timeout exceeded; try restarting transaction。回查第一步的结果:id=300000 仍是初始值 xxxxxxxxxx。第一步虽然跑完了,事务整体回滚,没有留下半笔提交。
--- 0. region 归属(从 SHOW TABLE REGIONS 解析)--- id=1000 -> region 290 [ t_114_ , t_114_r_7813 ) id=200000 -> region 390 [ t_114_r_195301 , t_114_r_203113 ) id=300000 -> region 442 [ t_114_r_296857 , t_114_r_304669 ) --- B2-1. 提交后(两行同时可见)--- | 1000 | CROSS-A | | 200000 | CROSS-B | --- B2-2. 第二步撞锁,整体回滚 --- ERROR 1205 (HY000) at line 1: Lock wait timeout exceeded; try restarting transaction | 300000 | xxxxxxxxxx | # 仍是初始值:无部分提交
证据:cloud-evidence/b2b3-txn.txt(B2-1 正例 / B2-2 反例全文)
实测 13FOR UPDATE 的锁范围:点查只锁一行,范围查才锁一片云集群 · 09-25
作品断言
SELECT … FOR UPDATE 的锁行为提示:点查只锁目标行,相邻行不受影响;范围条件会锁住范围内已存在的行。官方文档未展开 FOR UPDATE 的具体加锁范围(见 10 号病理页 B3 边界),本组结论以实测为准。
点查 · 锁的落点看得见
A 对 id=1000 做 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 毫秒返回,没被挡。
范围 · 锁一片但不越界
A 对 id BETWEEN 1000 AND 1005 做 FOR UPDATE:B 改范围内的 id=1003 → 1205;C 改范围外的 id=2000 → 9 毫秒通过。
口径说明
那几个毫秒数是命令的端到端耗时(含连接建立),不是性能指标;它在这里只承担一件事:把「被锁挡住(一直等到超时)」和「没被挡住(立刻返回)」对照出来。
--- B3-1. 锁等待视图里的落点(原文节选)--- KEY: 7480000000000000725F7280000000000003E8 KEY_INFO: {"db_name":"sbtest","table_name":"payload","handle_type":"int","handle_value":"1000","db_id":112,"table_id":114} --- 邻行与范围外的对照(命令耗时)--- real 0m0.009s # id=999(左邻行)没被阻塞 real 0m0.008s # id=1001(右邻行)没被阻塞 real 0m0.009s # id=2000(范围外)没被阻塞
证据:cloud-evidence/b2b3-txn.txt(B3-1 点查 / B3-2 范围全文)
实测 14TSO 不是「每次一个」:并发把小事务的时间戳合进一个批次云集群 · 09-25
作品断言
病理页「未覆盖清单」B4 / 演示台事务流程图:事务的 start_ts 与 commit_ts 两次时间戳都从 PD 获取;TSO 是全局逻辑时钟、按批次分配以摊还开销。此前这一条只有官方文档与推导支撑,本批补上。
怎么量的
「本底扣除法」三段实验:先空转 64 秒标定集群本底(10.57 批次/s —— 部署后恒定存在、无法归因到具体组件,如实记为集群本底);再以 1 并发串行跑 60 秒小事务;最后 16 并发跑 60 秒。读数取自 TiDB 侧的 pd_client_request_handle_tso_batch_size 直方图与服务端计数器(Prometheus 15 秒采样粒度)。
批合并看见了
串行窗平均批 1.010(≈ 1:每个时间戳一次请求);并发窗平均批 1.092(> 1:单次 RPC 带上多个时间戳)。bucket 直方图更直接:并发窗 batch≥2 的批次 +2525 个,串行窗只有 batch≥2 的批次 +45 个 —— 相差 56 倍。合并的动因是并发,这就是「批分配摊还开销」的机制证据。
时间戳对账
串行窗 1135 个事务 → 净时间戳 +1978(理论 2×1135=2270,87%);并发窗 17966 个事务 → 净时间戳 +34974(理论 2×17966=35932,97%)。两处时间戳都经 PD 分配的机制成立;精确百分比受本底扣除与采样粒度影响,如实标注。
口径说明
PD 服务端处理请求数随负载增长(+1475 → +4146 → +31928,均值微秒级),与官方「TSO 处理是轻量内存操作」的口径一致。本实验只测机制 —— 公网 RTT 与虚机环境不产生任何性能结论,本页也不写。此前记录里「没跑过」的断言,至此清零。
--- 阶段 B:空闲本底 60s(集群无外部负载)--- [B 本底] 窗口 64s · 事务 0(理论时间戳 0) batch_count +678(批次) batch_sum +713(时间戳) 平均批 1.052 本底速率: 10.57 批次/s · 11.11 时间戳/s(后续扣除用) --- 阶段 S:串行小事务(1 并发 × 60s,全速 BEGIN/UPDATE/COMMIT)--- [S 串行] 窗口 64s · 事务 1135(理论时间戳 2270) bucket: batch=1 的批次 +2593 · batch≥2 的批次 +45 · (≤2: +2631, ≤4: +2638) 扣除本底后净增量: 时间戳 +1978(理论 2270)· 批次 +1959 · 平均批 1.010 --- 阶段 P:并发小事务(16 并发 × 60s,同语句)--- [P 并发] 窗口 65s · 事务 17966(理论时间戳 35932) bucket: batch=1 的批次 +30186 · batch≥2 的批次 +2525 · (≤2: +32342, ≤4: +32697) 扣除本底后净增量: 时间戳 +34974(理论 35932)· 批次 +32029 · 平均批 1.092 --- 机制对账(只写机制,不写性能)--- 1) 时间戳对账:S 段 1135 txn → 净时间戳 +1978(理论 2×1135=2270,87%);P 段 17966 txn → 净时间戳 +34974(理论 2×17966=35932,97%)…… 4) PD 服务端:处理请求数随负载增长(本底 +1475 → S 段 +4146 → P 段 +31928),处理耗时均值微秒级(5.9/5.7/3.2 µs)……
证据:cloud-evidence/tso-b4-20260925-144942.txt(脚本 stdout 全文;实验脚本 dev/tso-b4-experiment.py)

一处我们没查清的事

写在这里,而不是编一个说得通的解释

同一天、同一套集群,我们在四个时刻读到四个不同的 Region 总数

读数时刻来源Region 总数当时在做什么
09:40:01Prometheus 时序74第一轮 SPLIT 之后
10:40:01Prometheus 时序61 小时后被 PD 合并完成
13:05:01Prometheus 时序15重灌之前
13:06:34PD HTTP 接口74重灌之前的基线读数
13:06(分裂后)PD HTTP 接口142两张表重分裂之后
13:17:19PD HTTP 接口145收尾复核

其中有一处我们没能解释清楚,也不打算编: 13:05:01 的 Prometheus 读数是 15,13:06:34 的 PD 接口读数是 74, 两者只差 89 秒,却来自两条不同的管道 —— 一条是抓取周期上的上报值,一条是按需计算值。 我们没有做到能证明「这 59 个 Region 是在哪一刻出现的」,所以这里只记读数,不给因果。

它换来的是这一页的一条硬规矩:凡写现场数字,必须同时写清「哪个仪器、哪一刻」。 只写数字不写来源,读者就无法判断它属于哪个瞬间 —— 这正是这个作品一开始就在反对的事。 而这四个数字彼此都不矛盾:它们只是四个不同瞬间的照片。

三处「工具撒谎」

这一页里最有价值的部分,都来自这里

如果这次实测只告诉我「集群跑起来了」,那它的价值很有限。 真正值得记下来的是:同一个动作,官方检查工具给出的结论与实机状态三次不一致, 而三次里对的都是实机、错的是工具。

这恰好是这个作品一开头就在讲的那句话 —— 把机制画对只是及格线,难的是凭什么相信它是对的。 工具也是被相信的对象之一,它同样会错。

纠偏 01官方检查工具直接报错,但集群本身没有任何问题
工具原文
Warn: invalid signature for file root.json: not enough signatures (2) for threshold 3 in root.json
Error: no trusted root in the local manifests
若照信
会立刻去怀疑镜像被污染、网络被劫持、签名校验失败 —— 然后去查网络、换镜像源、甚至重装工具,全是无效动作。
根因
不是网络、不是镜像,而是本机 TiUP 清单缓存里的陈旧文件。实测下载 root.json 是通的(HTTP 200,7,275 字节),只是本地 manifests 目录里躺着过期的 3.root.json / 4.root.json。
处置
清掉本地清单缓存后重跑,检查正常完成,各节点逐项出结果。全程没有改网络、没有换镜像。
--- 2. root.json direct fetch --- http=200 bytes=7275 t=0.176s --- 4. tiup 本地清单目录 --- -rwxrwxr-x 1 tidb tidb 7275 Sep 19 09:34 3.root.json -rwxrwxr-x 1 tidb tidb 7275 Sep 19 09:34 4.root.json
证据:probe/cluster-check.txt(报错原文)、probe/tiup-diagnose.txt(HTTP 200 + 缓存文件清单 + 清缓存后重跑成功)
纠偏 02检查项报「操作系统不支持」,而二进制在这台机器上跑得好好的
工具原文
os-version Fail CentOS Linux 7 (Core) 7.9.2009 not supported, use version 9 or higher
若照信
会得出「这台机器不能装」的结论 —— 而这是错的。三台机器上 TiKV 服务是 active (running),端口 20160 在监听,二进制版本号 8.5.7,动态库检查 ldd: no missing libs,日志里正在正常做副本变更与快照。
怎么判
一条不能简单记成「看到 Fail 就忽略」。判据是两条独立证据:① 产品文档明确该版本支持这一操作系统;② 实测二进制在本机可运行且无缺失依赖。两条都成立,才可以把这条 Fail 判定为检查规则滞后于产品支持策略,而不是环境不满足。
结论
工具是规则快照,官方文档才是支持策略的权威。两者冲突时,以文档 + 实机复现为准,并且要把冲突记下来,而不是悄悄绕过去。
--- systemctl status tikv-20160 --- Active: active (running) since Sat 2026-09-19 09:37:03 CST --- tikv binary glibc check --- Release Version: 8.5.7 Edition: Community ldd: no missing libs
证据:probe/tiup-diagnose.txt(os-version 行原文)、probe/tikv-diag.txt(systemd 状态 / 端口监听 / Release Version / ldd)
纠偏 03界面显示「挂了三台」,其实只是查得太早
工具原文
集群启动后立刻查看状态:三个 TiKV 全部显示 Down,而其余组件正常 Up。
若照信
会去翻 TiKV 日志找崩溃原因、怀疑配置写错、甚至推倒重装 —— 排查方向从一开始就是错的。
根因
这是时序假象。start 于 09:37:02 发起、状态查询于 09:37:12 执行,而 TiKV 进程是在 09:37:03 才起来的 —— 也就是说,查询只比进程就绪晚了 9 秒。这 9 秒里 TiKV 正在做「注册 + 组副本 + 传快照」,上报给 PD 的健康状态还没落地,界面上就先显示成不可用。
处置
不回滚任何东西,先做独立取证:查 systemd 状态、查端口监听、查进程、查日志,四条全部正常;稍后重查,三个 TiKV 全部转为 Up。「查询时刻」本身会污染结论,这是这次最容易被忽略的一处。
===== start 2026-09-19 09:37:02 ===== 192.168.3.110:20160 tikv … Down 192.168.3.112:20160 tikv … Down 192.168.3.114:20160 tikv … Down Total nodes: 11 ===== done 2026-09-19 09:37:12 =====
证据:probe/start.txt(启动后即查,TiKV 显示 Down)、probe/tikv-diag.txt(systemd/端口/进程/日志四项取证)、probe/verify-cluster.txt(随后重查,11 个组件全部 Up)

从这两轮踩坑里得到的判据

比结论本身更值得带走
  1. 先怀疑仪器,再怀疑被测对象。 三次不一致里,系统本身都是好的,坏的是「我们用来观察它的东西」—— 缓存、检查规则、查询时刻。观察工具出错时,表现得和系统真出故障一模一样。 第二轮又添了一层:同一天同一套集群,四个时刻四个 Region 总数(74 / 6 / 15→74 / 142 / 145), 彼此不矛盾,它们只是四张不同瞬间的照片。所以引用任何现场数字,都要连来源与时刻一起引用。
  2. 「能跑通」不等于「跑对了」。 集群全绿只能说明它活着;这一页里真正有价值的部分,是去验证它活成什么样—— 副本是不是真的三份、Leader 是不是真的搬了、参数是不是真的落到了进程里、冲突是不是真的会把事务挡下来。
  3. 官方文档是支持策略的权威,检查工具只是规则快照。 两者冲突时不要二选一,也不要默默绕过 —— 用文档 + 独立实测两条证据定性,并把这个冲突记录下来。
  4. 「现场」是有保鲜期的。 默认配置下,PD 可以在 1 小时后把你留在那里当证据的 Region 合并掉 —— 而你什么错都没犯,甚至什么都没做。所以要么当场把原始输出落盘, 要么就换成看监控时序;不要指望「下次再看还在」。
  5. 多问一个对象,比多读一遍同一个对象更有用。 容量参数这一条,第一轮读的是配置文件,第二轮读的是进程自己的上报,再向 PD 交叉核对。 同一件事换个来源去问,才能把「写进去了」和「生效了」分开。

这套实测核验不了什么

补掉一个短板,不等于没有短板

诚实的边界

上一页写「我们没接过真实集群」—— 那句话现在不成立了。但换成「我们有真机了」,也同样不等于没有边界。

证据索引

每个数字都能回到这里

原始采集输出全部留在仓库里,不经改写。要自己核对,直接打开对应文件即可。

证据文件里面是什么本页哪几条用到
probe/r2-round2.txt第二轮主证据:重灌与重分裂全过程、三个时刻的 store 原始 JSON、降级期间写入、TiKV 进程自报的运行时配置身份卡 · 实测 01–06
probe/r2-history.txtPrometheus 时序(另一条独立管道):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.txtsystemd 状态、端口监听、进程、二进制版本与动态库纠偏 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.txtTSO 三段实验(本底扣除法):空闲 / 串行 / 并发三窗的批直方图与时间戳对账,脚本 stdout 全文实测 14
cloud-evidence/final-state.txt释放前的终态快照:display(含 Tombstone 行)、store 列表、tombstone ids、数据完整性与版本实测 11
cloud-evidence/scalein-console.txtTiUP 原生下线全程输出(含「will become tombstone」提示原文)实测 11
cloud-evidence/scaleout-console.txt3 → 5 扩容全程输出(Scaled cluster … out successfully)实测 10
cloud-evidence/rescaleout-console.txt1058 重新上线全程输出实测 11
cloud-evidence/events.jsonl实验操作时间线(20 条人工记录)云集群一节
_cloud_prune.txtprune 原文(RemoveTomestoneNodesInPD / Destroy success)与完成后 display 读数实测 11

另外两份文件本身就是第一轮的历史,保留在仓库里但不被本页引用: probe/demo.txt 与 probe/failover.txt —— 它们记录的正是被 PD 合并掉的那批现场。 把它们删掉会更整齐,但那样这一页就只剩结论、没有来历了。

这一页的每一个数字,都由一个脚本对着上述原始日志做机器比对 —— 页面写的值如果在日志里找不到,构建就会失败。它不是自我评价,是一道会拦人的关。

这一页与另外几页的关系

核验实录管的是「数字有没有官方出处」—— 每一格都能点开官方原文与适用版本。 本页管的是「机制在真机上是不是这样」—— 每一格都能回到原始日志。 自助核验管的是「你自己怎么验一遍」—— 从点到重出。 现场直连管的是「现在这一刻是什么样」—— 它是一台仪器,不存任何实测值,打开就去读活集群。

四页叠起来才是完整的可信链:出处 → 实测 → 可复跑 → 现场。任何一环缺失,可信度都会掉一个等级。

继续走

想自己验一遍的,从这里进去
现场直连 · 对着活的集群读一次 → 自助核验包 · 五条路径 → 口径核验实录 · 官方出处 → 这套集群的部署指南(含全部脚本原文)
TiDB 内核解剖室 · 1024 TiDB 社区 AIGC 黑客松参赛作品
本页实机数据来自两套 v8.5.7 集群:三台自建机器(第一轮 2026-09-19 09:38–09:40,第二轮 09-19 13:06–13:17,含 09-20 补采)与五台云服务器(09-25,扩容 3 → 5 TiKV、下线全程与 TSO 三段实验)。
原始输出保留在 dev/probe/ 与 dev/cloud-evidence/,未经改写;自建集群条目可在同一套拓扑上复跑核对,宿主机层条目为部署时刻快照、云集群条目为释放前的一次性快照,均不可复跑。
想确认某个数字现在还是不是这样,请用现场直连去读一次。