业务切换后还要验证节点故障
前面的测试已经把 MySQL 数据通过 DM 迁到了 TiDB,也做了业务切换和回退。业务切到 TiDB 后,还要确认节点发生故障时会有什么影响。实际生产中在节点故障时,主要考虑的是挂号、收费和医嘱能不能继续,已有连接和事务是否受影响。这些问题都需要结合业务操作来检查。
TiDB Server 是无状态 SQL 层,PD 通常部署三个或更多的奇数节点,TiKV 默认保存三份数据副本。少数副本故障时,集群可以自动进行故障转移,但应用不一定完全无感。如果 JDBC 直连某一台 TiDB Server,该节点停止后当前连接仍会断开。TiKV Region Leader 发生变化时,TiDB 会在内部重试;恢复时间超过 backoff 阈值后,错误仍可能返回客户端。
一、测试环境
继续使用前面已经搭好的 TiDB 7.5 集群。
| 组件 | 节点 |
|---|---|
| TiDB Server | 192.168.56.101:4000、192.168.56.102:4000 |
| PD | 192.168.56.101:2379、192.168.56.102:2379、192.168.56.103:2379 |
| TiKV | 192.168.56.101:20160、192.168.56.102:20160、192.168.56.103:20160 |
| TiDB 版本 | v7.5.7 |
| 测试数据库 | his_source |
业务表继续使用前文已经迁移到 TiDB 的几张表:
outpatient_visit 门诊就诊
charge_detail 收费明细
doctor_order 医嘱
测试前先看集群状态:
tiup cluster display tidb75-lab
数据库里也查一下实际拓扑:
SELECT
TYPE,
INSTANCE,
STATUS_ADDRESS,
VERSION,
UPTIME
FROM information_schema.CLUSTER_INFO
ORDER BY TYPE, INSTANCE;
CLUSTER_INFO 可以直接看到 TiDB、PD、TiKV 的实例地址、版本和运行时间,比只看操作系统进程方便一些。
二、先准备一笔完整的模拟门诊业务
这次不单独建探针表,直接使用门诊、收费和医嘱三张业务表。
先生成一条测试就诊:
INSERT INTO his_source.outpatient_visit
(
visit_no,
patient_no,
dept_code,
visit_type,
visit_time,
visit_status,
update_time
)
VALUES
(
'TEST-MZ-0901-37',
'TEST-PAT-037',
'CARD',
'OUTPATIENT',
NOW(),
'WAITING',
NOW()
);
TEST-MZ-0901-37 是本次故障测试使用的模拟门诊号,TEST-PAT-037 是专门生成的测试患者编号,不使用真实患者姓名、身份证号、住院号或医保信息。
确认记录已经存在:
SELECT
visit_no,
patient_no,
dept_code,
visit_time,
visit_status
FROM his_source.outpatient_visit
WHERE visit_no = 'TEST-MZ-0901-37';
后面的收费和医嘱都挂在这次测试就诊下面。故障结束后除了检查数据是否写入,还要核对门诊、收费和医嘱之间的关联关系。
三、TiDB Server 停一台以后会发生什么
上一篇做业务切换时,测试程序直接连接 192.168.56.101:4000。这种方式方便查看迁移过程,不能作为正式的高可用接入设计。
先直接连接第一台 TiDB:
mysql -h 192.168.56.101 -P 4000 \
-u his_app -p his_source
保持这个会话,再从 TiUP 中控机人为停止这一台 TiDB Server:
tiup cluster stop tidb75-lab \
-N 192.168.56.101:4000
-N 后面使用的是 tiup cluster display 第一列显示的节点 ID。TiUP 官方支持通过这个参数只停止指定服务。
这条命令只模拟单个 TiDB Server 服务不可用,不包含服务器断电、网络中断或进程崩溃。原来直接连接 192.168.56.101:4000 的客户端会失去这条连接。
这时候改连第二台:
mysql -h 192.168.56.102 -P 4000 \
-u his_app -p his_source
如果集群状态正常,第二台 TiDB Server 仍可提供 SQL 服务。TiDB Server 本身不保存业务数据,多台 TiDB Server 通常通过 LVS、HAProxy、F5 等负载均衡组件提供统一入口。这个操作只能确认集群还有其他 SQL 节点可用,不能证明原有应用连接不会中断。
四、通过第二台 TiDB 做一笔收费
第一台 TiDB Server 仍然保持停止状态,通过 192.168.56.102:4000 写入一笔测试收费:
INSERT INTO his_source.charge_detail
(
charge_no,
visit_no,
item_type,
item_code,
item_name,
qty,
unit_price,
amount,
charge_time,
charge_status
)
VALUES
(
'TEST-SF-0901-06',
'TEST-MZ-0901-37',
'REG',
'TEST-REG-FEE',
'普通门诊诊查费',
1,
15.00,
15.00,
NOW(),
'PAID'
);
查询:
SELECT
charge_no,
visit_no,
item_type,
amount,
charge_status,
charge_time
FROM his_source.charge_detail
WHERE charge_no = 'TEST-SF-0901-06';
这里按 charge_no 和 visit_no 的关联检查业务数据。TiDB Server 不保存业务数据,两个 TiDB Server 访问的是同一个 TiKV 存储集群,无需按 TiDB Server 区分数据。
第一台 TiDB 恢复:
tiup cluster start tidb75-lab \
-N 192.168.56.101:4000
再从第一台查询:
SELECT
charge_no,
visit_no,
amount,
charge_status
FROM his_source.charge_detail
WHERE charge_no = 'TEST-SF-0901-06';
这一步用于确认两个 TiDB Server 入口查到的是同一条收费记录。
五、PD 测试先找到 Leader,不把地址写死
PD 和 TiDB Server 的情况不一样。PD 保存集群元信息、数据分布信息,并负责时间戳分配和调度。TiDB 官方建议 PD 部署不少于三个节点,并使用奇数节点组成高可用集群。
测试前先找当前 PD Leader:
tiup ctl:v7.5.7 pd \
-u http://192.168.56.101:2379 \
member leader show
member leader show 是 PD Control 提供的 Leader 查询命令。Leader 会动态变化,不能提前指定 192.168.56.101 一定是 PD Leader。实际返回哪一台,就停哪一台。例如查询结果为 192.168.56.102:2379,测试命令为:
tiup cluster stop tidb75-lab \
-N 192.168.56.102:2379
停止以后,从另一个在线 PD 再查 Leader:
tiup ctl:v7.5.7 pd \
-u http://192.168.56.101:2379 \
member leader show
三节点 PD 集群失去一个节点后仍满足多数派条件,但这个操作不能证明业务完全无感。Leader 重新选举需要时间,现场还要记录业务 SQL 是否报错、延迟是否有明显变化,不能只看 PD 是否选出新的 Leader。
六、PD Leader 切换期间做一次医嘱写入
PD Leader 停止以后,通过 TiDB 写一条模拟医嘱:
INSERT INTO his_source.doctor_order
(
order_no,
visit_no,
order_type,
order_code,
order_name,
start_time,
order_status,
update_time
)
VALUES
(
'TEST-YZ-0901-03',
'TEST-MZ-0901-37',
'LAB',
'TEST-LAB-CBC',
'血常规测试项目',
NOW(),
'ACTIVE',
NOW()
);
确认:
SELECT
order_no,
visit_no,
order_type,
order_code,
order_status,
start_time
FROM his_source.doctor_order
WHERE order_no = 'TEST-YZ-0901-03';
这里同样不使用真实医嘱数据,TEST-LAB-CBC 只是测试项目编码。
测试结束后恢复刚才停止的 PD:
tiup cluster start tidb75-lab \
-N 192.168.56.102:2379
实际执行时要使用前面查到的 Leader 地址,不要直接复制示例地址。
七、停 TiKV 之前,先看副本是不是健康
TiKV 默认三副本,但停节点前仍要确认各个 Region 是否有足够的健康副本。事务在数据成功写入 Raft 多数副本后才能提交,少数副本故障时可以继续提供服务。故障测试前先检查 Region 状态。
tiup ctl:v7.5.7 pd \
-u http://192.168.56.101:2379 \
region check miss-peer
再查:
tiup ctl:v7.5.7 pd \
-u http://192.168.56.101:2379 \
region check down-peer
以及:
tiup ctl:v7.5.7 pd \
-u http://192.168.56.101:2379 \
region check pending-peer
PD Control 对这几个状态的定义如下:
miss-peer:Region 缺副本;down-peer:存在已经 Down 的副本;pending-peer:存在暂时不可用的副本。
如果已经存在异常 Region,应先处理原有副本问题,不再继续停止 TiKV。tiup cluster display 显示三台 TiKV 都是 Up,也不能代替 Region 副本状态检查。
八、再看一下 TiKV 当前分布
数据库里可以直接查询 TiKV Store:
SELECT
STORE_ID,
ADDRESS,
STORE_STATE_NAME,
LEADER_COUNT,
REGION_COUNT,
LAST_HEARTBEAT_TS,
UPTIME
FROM information_schema.TIKV_STORE_STATUS
ORDER BY STORE_ID;
TIKV_STORE_STATUS 可以看到 Store 地址、Region 数量、Leader 数量和最近心跳等信息。
Region Peer 也可以查:
SELECT
STATUS,
COUNT(*) AS peer_count
FROM information_schema.TIKV_REGION_PEERS
GROUP BY STATUS
ORDER BY STATUS;
TIKV_REGION_PEERS 中的 Peer 状态包括 NORMAL、PENDING 和 DOWN。这里先记录故障前的 Store、Region 和 Peer 状态,停止 TiKV 后再作比较。
九、模拟单 TiKV 节点不可用
确认副本状态没有异常以后,选择一个 TiKV 做测试。以下以 192.168.56.103:20160 为例:
tiup cluster stop tidb75-lab \
-N 192.168.56.103:20160
这里仍然只是服务级故障模拟,与服务器断电、交换机故障或磁盘损坏不同。
停止后先看:
tiup cluster display tidb75-lab
同时持续执行查询:
SELECT
visit_no,
patient_no,
visit_status
FROM his_source.outpatient_visit
WHERE visit_no = 'TEST-MZ-0901-37';
再查收费:
SELECT
charge_no,
amount,
charge_status
FROM his_source.charge_detail
WHERE charge_no = 'TEST-SF-0901-06';
如果这些数据所在 Region 仍然有 Raft 多数副本可用,TiDB 可以继续完成读取和写入。
如果故障节点上存在 Region Leader,Leader 需要重新选举。TiDB 遇到 not leader、epoch not match、请求超时等 Region 暂时不可用情况时,会在内部进行 backoff 重试,不能预设停一台 TiKV 后业务完全无感。
官方故障排查文档说明,如果 Region 在默认 backoff 阈值内恢复,客户端可以不感知;超过阈值后,则可能返回 Region is Unavailable。文档给出的默认阈值是约 20 秒。
实际医院系统还受 JDBC、连接池和应用超时配置影响。如果应用超时时间比数据库内部重试时间短,业务侧可能先报超时。节点测试要记录 SQL 报错、延迟变化和应用超时,不能只看数据库最后是否恢复。
十、TiKV 少一个节点期间,再做一次收费状态更新
收费业务不适合为了测试反复插入同一张收费单,这里对前面的测试收费做一次状态变更。先改成待确认:
UPDATE his_source.charge_detail
SET charge_status = 'CHECKING'
WHERE charge_no = 'TEST-SF-0901-06';
确认:
SELECT
charge_no,
visit_no,
amount,
charge_status
FROM his_source.charge_detail
WHERE charge_no = 'TEST-SF-0901-06';
再恢复成:
UPDATE his_source.charge_detail
SET charge_status = 'PAID'
WHERE charge_no = 'TEST-SF-0901-06';
这组操作用于检查 TiKV 节点停止期间已有收费记录能否正常更新。
十一、TiKV 恢复以后还要检查 Region
恢复节点:
tiup cluster start tidb75-lab \
-N 192.168.56.103:20160
等待服务恢复后:
tiup cluster display tidb75-lab
然后重新做副本检查:
tiup ctl:v7.5.7 pd \
-u http://192.168.56.101:2379 \
region check miss-peer
tiup ctl:v7.5.7 pd \
-u http://192.168.56.101:2379 \
region check down-peer
tiup ctl:v7.5.7 pd \
-u http://192.168.56.101:2379 \
region check pending-peer
TiKV 进程重新显示为 Up 后,还要等 Region 副本状态恢复正常。如果生产环境发生硬件损坏,节点无法继续使用,需要按 TiKV 缩容、替换和补副本的流程处理。
十二、故障期间不能看到数据库报错就直接重做收费
故障测试还要检查业务重试逻辑。假设应用正在提交收费:
INSERT INTO charge_detail ...
如果这时发生网络异常,客户端看到数据库错误后,不能直接认定“收费肯定没有成功”,也不能立即把原 SQL 再发一次。收费系统一般使用业务流水号控制重复提交,例如收费流水号、支付流水号或结算流水号。本次测试使用的 TEST-SF-0901-06 在表里有唯一约束。
应用异常以后,先按业务流水查询:
SELECT
charge_no,
visit_no,
amount,
charge_status
FROM his_source.charge_detail
WHERE charge_no = 'TEST-SF-0901-06';
确认业务状态以后,再决定补做还是继续后面的处理。
TiDB 返回错误后,需要按错误码和具体报错内容处理,不能把所有异常都改成自动重试。
| 错误码 | v7.5 文档中的处理要点 |
|---|---|
8022 |
事务提交失败且已经回滚,应用可以安全地重新执行整个事务。 |
8028 |
与事务期间的表结构变化有关,处理方式还受元数据锁状态和 DDL 类型影响。 |
9007 |
表示写冲突,需要继续查看报错中的具体原因,不能直接套用 8022 的处理方式。 |
对于悲观事务,也不能套用乐观事务自动重试参数。tidb_disable_txn_auto_retry 和 tidb_retry_limit 只作用于乐观事务。
收费、退费、医嘱和发药业务仍要有自己的幂等控制,不能只依赖数据库重试。
十三、所有节点恢复后,再从业务关系检查一次
节点都恢复以后:
tiup cluster display tidb75-lab
数据库侧重新确认拓扑:
SELECT
TYPE,
INSTANCE,
VERSION,
UPTIME
FROM information_schema.CLUSTER_INFO
ORDER BY TYPE, INSTANCE;
然后检查这次测试使用的门诊记录:
SELECT
visit_no,
patient_no,
dept_code,
visit_status,
visit_time
FROM his_source.outpatient_visit
WHERE visit_no = 'TEST-MZ-0901-37';
收费:
SELECT
charge_no,
visit_no,
item_name,
amount,
charge_status
FROM his_source.charge_detail
WHERE visit_no = 'TEST-MZ-0901-37';
医嘱:
SELECT
order_no,
visit_no,
order_name,
order_status
FROM his_source.doctor_order
WHERE visit_no = 'TEST-MZ-0901-37';
再检查业务关联:
SELECT
v.visit_no,
COUNT(DISTINCT c.charge_no) AS charge_count,
COUNT(DISTINCT o.order_no) AS order_count
FROM his_source.outpatient_visit v
LEFT JOIN his_source.charge_detail c
ON c.visit_no = v.visit_no
LEFT JOIN his_source.doctor_order o
ON o.visit_no = v.visit_no
WHERE v.visit_no = 'TEST-MZ-0901-37'
GROUP BY v.visit_no;
这条 SQL 检查测试就诊下的收费和医嘱数量,用于确认三张业务表之间的关联记录是否完整。
十四、数据库入口还缺一层高可用
上一篇使用的连接方式是:
JDBC
↓
192.168.56.101:4000
第一台 TiDB Server 停止后,应用正在使用的连接会断开。集群里还有其他 TiDB Server,不代表应用能够自动找到新的节点。多台 TiDB Server 前面需要通过 LVS、HAProxy、F5 等负载均衡组件提供统一入口。
后续准备完善统一入口,再检查 TiDB Server 计划重启时连接池如何迁移、未完成事务如何处理,以及统一入口自身故障时应用能否重新建连。