前言
上一篇把两台 TiDB Server 放到 HAProxy 后面,应用不再直连某一台 TiDB,而是统一访问 HAProxy 的 3390 端口。这样处理后,应用不用跟着 TiDB Server 地址变化,但当时只有一个入口:
192.168.56.110:3390
如果 192.168.56.110 停机,即使后面的 TiDB01、TiDB02 都正常,应用还是连不上数据库。TiDB 官方的 HAProxy 最佳实践也是让客户端通过浮动 IP 访问 HAProxy,再由 HAProxy 把连接分发到 TiDB Server。
这次再增加一台 HAProxy,并配置一个业务 VIP:
HAProxy01 192.168.56.110
HAProxy02 192.168.56.114
VIP 192.168.56.115
测试程序只连接 192.168.56.115:3390。主要检查 HAProxy01 的代理进程停止后 VIP 是否转移、新连接能否继续建立,以及故障发生在事务中间时怎么判断事务结果。测试继续使用前面建立的门诊、收费和医嘱表。
一、数据库入口结构
TiDB 集群没有变化,代理层增加了 HAProxy02 和 VIP。
| 组件 | 地址 |
|---|---|
| TiDB01 | 192.168.56.101:4000 |
| TiDB02 | 192.168.56.102:4000 |
| HAProxy01 | 192.168.56.110 |
| HAProxy02 | 192.168.56.114 |
| 数据库 VIP | 192.168.56.115 |
| 应用访问端口 | 3390 |
| TiDB | v7.5.7 |
| 测试库 | his_source |
连接关系如下:
HIS 测试程序
|
192.168.56.115:3390
|
当前持有 VIP 的 HAProxy
(HAProxy01 或 HAProxy02)
|
+--> TiDB01 192.168.56.101:4000
+--> TiDB02 192.168.56.102:4000
VIP 同一时间只应出现在一台 HAProxy 服务器上。正常状态下由 HAProxy01 持有;HAProxy01 不能提供服务时,由 HAProxy02 接管。Keepalived 通过 VRRP 选出当前 MASTER,并把虚拟地址配置在 MASTER 节点上。
二、两台 HAProxy 使用相同的后端配置
HAProxy01 和 HAProxy02 的 /etc/haproxy/haproxy.cfg 保持一致:
global
log 127.0.0.1 local2
daemon
maxconn 4096
defaults
log global
mode tcp
option tcplog
timeout connect 5s
timeout client 1h
timeout server 1h
listen tidb-cluster
bind 0.0.0.0:3390
mode tcp
balance leastconn
server tidb01 192.168.56.101:4000 check inter 2000 rise 2 fall 3
server tidb02 192.168.56.102:4000 check inter 2000 rise 2 fall 3
这里沿用 TiDB 官方 HAProxy 最佳实践中的 TCP 模式、leastconn 和后端健康检查。SQL 连接通常是长连接,leastconn 会把新连接优先分配给当前连接数较少的 TiDB Server。上面的配置每 2 秒检查一次 TiDB 的 4000 端口,连续 3 次失败后将后端标记为不可用,连续 2 次成功后重新加入。
先在两台代理上检查配置并启动 HAProxy:
haproxy -c -f /etc/haproxy/haproxy.cfg
systemctl enable haproxy
systemctl restart haproxy
ss -lntp | grep 3390
配置 VIP 前,分别通过两台代理的实际地址连接数据库:
mysql -h 192.168.56.110 -P 3390 \
-u his_app -p his_source
mysql -h 192.168.56.114 -P 3390 \
-u his_app -p his_source
登录后执行:
SELECT @@hostname, CONNECTION_ID();
确认两台 HAProxy 都在监听 3390,并且实际地址都能访问后端 TiDB 后,再配置 VIP。如果 HAProxy02 本身不能访问 TiDB,VIP 漂到这台机器也恢复不了数据库入口。
三、Keepalived 使用单播模式
测试环境是虚拟机。医院数据库区域通常有比较严格的交换机和安全策略,有些环境下VRRP 组播可能无直接使用,因此采用 Keepalived 的 unicast_peer 单播配置。
两台服务器的地址分别为 192.168.56.110 和 192.168.56.114,业务网卡都使用 ens192。配置前要按现场结果确认网卡名称:
ip -br addr
四、配置 HAProxy01
先准备 HAProxy 状态检查脚本 /etc/keepalived/check_haproxy.sh:
#!/bin/bash
systemctl is-active --quiet haproxy
exit $?
设置权限:
chmod 750 /etc/keepalived/check_haproxy.sh
chown root:root /etc/keepalived/check_haproxy.sh
HAProxy01 的 /etc/keepalived/keepalived.conf 如下:
global_defs {
router_id TIDB_HAPROXY_01
script_user root
enable_script_security
}
vrrp_script chk_haproxy {
script "/etc/keepalived/check_haproxy.sh"
interval 2
timeout 2
weight -30
fall 2
rise 2
}
vrrp_instance VI_TIDB {
state MASTER
interface ens192
virtual_router_id 56
priority 120
advert_int 1
unicast_src_ip 192.168.56.110
unicast_peer {
192.168.56.114
}
authentication {
auth_type PASS
auth_pass tidbvip
}
virtual_ipaddress {
192.168.56.115/24 dev ens192
}
track_script {
chk_haproxy
}
}
HAProxy01 的初始优先级为 120,高于备用节点。chk_haproxy 每 2 秒检查一次 HAProxy 服务。检查连续失败后,负权重 -30 会把有效优先级降到 90,低于 HAProxy02 的 100,HAProxy02 随后可以接管 VIP。
这个脚本只检查本机 HAProxy 的 systemd 状态。服务显示 active,不等于 3390 端口、后端 TiDB 和 SQL 访问链路都正常,后面排障时要把这几层分开。
五、配置 HAProxy02
HAProxy02 的脚本内容和权限与 HAProxy01 相同,Keepalived 配置只调整节点身份、优先级和单播地址:
global_defs {
router_id TIDB_HAPROXY_02
script_user root
enable_script_security
}
vrrp_script chk_haproxy {
script "/etc/keepalived/check_haproxy.sh"
interval 2
timeout 2
weight -30
fall 2
rise 2
}
vrrp_instance VI_TIDB {
state BACKUP
interface ens192
virtual_router_id 56
priority 100
advert_int 1
unicast_src_ip 192.168.56.114
unicast_peer {
192.168.56.110
}
authentication {
auth_type PASS
auth_pass tidbvip
}
virtual_ipaddress {
192.168.56.115/24 dev ens192
}
track_script {
chk_haproxy
}
}
两边的 virtual_router_id 56、VIP 和 advert_int 1 必须一致,节点地址和优先级各自配置。完成后检查配置:
keepalived --config-test \
--use-file=/etc/keepalived/keepalived.conf
Keepalived 不同版本的命令参数可能有差异,现场先用下面的命令确认:
keepalived --help
检查通过后启动服务:
systemctl enable keepalived
systemctl restart keepalived
六、确认 VIP 所在节点
在 HAProxy01 上执行:
ip addr show dev ens192
按当前优先级,正常状态下应看到 192.168.56.115/24。HAProxy02 上不应出现这个 VIP。也可以在两台机器上直接过滤:
ip -4 addr show dev ens192 | grep 192.168.56.115
客户端改为通过 VIP 连接:
mysql -h 192.168.56.115 \
-P 3390 \
-u his_app \
-p his_source
登录后查询当前 TiDB Server 和连接号:
SELECT
@@hostname AS tidb_server,
CONNECTION_ID() AS connection_id,
DATABASE() AS current_database;
@@hostname 返回的是后端 TiDB Server 主机名,可能是 tidb01,也可能是 tidb02,取决于 HAProxy 当时的连接分布。
七、通过 VIP 写入一笔模拟门诊数据
前面的文章已经建立了 outpatient_visit、charge_detail 和 doctor_order 三张业务表。先通过 VIP 写入一条模拟门诊记录:
INSERT INTO his_source.outpatient_visit
(
visit_no,
patient_no,
dept_code,
visit_type,
visit_time,
visit_status,
update_time
)
VALUES
(
'TESTMZ260904873',
'TESTBR04872',
'CARD',
'OUTPATIENT',
NOW(),
'WAITING',
NOW()
);
TESTMZ260904873 和 TESTBR04872 只用于本次测试,不对应医院里的真实门诊号和患者编号。写入后按门诊号查询:
SELECT
visit_no,
patient_no,
dept_code,
visit_status,
visit_time
FROM his_source.outpatient_visit
WHERE visit_no = 'TESTMZ260904873';
后面的故障切换仍使用这条记录。
八、停止 HAProxy01 的代理服务
这里不关服务器,只在 HAProxy01 上停止代理进程:
systemctl stop haproxy
Keepalived 仍然运行。在单独的终端查看日志:
journalctl -u keepalived -f
同时在两台服务器上检查 VIP:
ip -4 addr show dev ens192 | grep 192.168.56.115
按前面的配置,HAProxy01 检查失败后有效优先级会降到 90,HAProxy02 的优先级为 100,VIP 应转到 HAProxy02。切换耗时不能直接写成固定的 2 秒或 3 秒,它还受 interval、fall、VRRP advertisement、系统调度和网络邻居表更新影响。测试时可以在操作前后记录时间:
date '+%F %T.%N'
九、VIP 转移后检查已有连接和新连接
原来的 MySQL 连接路径是:
客户端
|
192.168.56.115
|
HAProxy01
|
TiDB01
HAProxy01 进程停止后,即使 VIP 已转到 HAProxy02,原来建立在 HAProxy01 上的 TCP 连接也不会自动转移,客户端原连接会断。Keepalived 负责决定 VIP 由哪台机器持有,HAProxy 也不会复制另一台代理上已经建立的 TCP 会话。
仍使用原来的 VIP 重新连接,不修改数据库地址:
mysql -h 192.168.56.115 \
-P 3390 \
-u his_app \
-p his_source
再执行:
SELECT @@hostname, CONNECTION_ID();
此时要确认新连接能经 HAProxy02 到达正常的 TiDB Server。双机入口保证的是故障后还能用同一个 IP 建立新连接,已有连接仍要由应用连接池重新建立。
十、代理切换后录入模拟收费
重新连接数据库后,先确认刚才的就诊记录:
SELECT
visit_no,
patient_no,
visit_status
FROM his_source.outpatient_visit
WHERE visit_no = 'TESTMZ260904873';
然后录入一笔模拟门诊收费:
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
(
'TESTSF2609041526',
'TESTMZ260904873',
'REG',
'REG-FEE',
'普通门诊诊查费',
1,
15.00,
15.00,
NOW(),
'PAID'
);
按收费号查询:
SELECT
charge_no,
visit_no,
item_name,
amount,
charge_status,
charge_time
FROM his_source.charge_detail
WHERE charge_no = 'TESTSF2609041526';
这笔收费通过 visit_no 与前面的就诊记录关联。故障前建立就诊记录,VIP 切换后继续录入收费,后面再核对两张表的关系。
十一、补一条模拟医嘱
再给这次门诊增加一条测试医嘱:
INSERT INTO his_source.doctor_order
(
order_no,
visit_no,
order_type,
order_code,
order_name,
start_time,
order_status,
update_time
)
VALUES
(
'TESTYZ260904231',
'TESTMZ260904873',
'LAB',
'TEST-CBC',
'血常规测试项目',
NOW(),
'ACTIVE',
NOW()
);
按医嘱号查询:
SELECT
order_no,
visit_no,
order_type,
order_name,
order_status
FROM his_source.doctor_order
WHERE order_no = 'TESTYZ260904231';
本次使用的门诊号、收费号和医嘱号如下,均为模拟数据:
门诊号 TESTMZ260904873
收费号 TESTSF2609041526
医嘱号 TESTYZ260904231
用下面的 SQL 核对三张表的关联关系:
SELECT
v.visit_no,
v.patient_no,
v.visit_status,
c.charge_no,
c.amount,
c.charge_status,
o.order_no,
o.order_status
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 = 'TESTMZ260904873';
入口从 HAProxy01 切到 HAProxy02 后,这组业务数据的关联关系不应发生变化。
十二、故障发生在事务中间时单独确认结果
上一轮测试已经停止 HAProxy01。开始事务中断测试前,先恢复并确认两台代理都能通过实际地址连接 TiDB:
systemctl start haproxy
systemctl status haproxy
ss -lntp | grep 3390
在两台机器上检查 VIP 当前在哪一台。只有确认未持有 VIP 的代理也处于可用状态,才停止当前持有 VIP 的 HAProxy;否则两台代理会同时不可用,后面的重新连接没有测试意义。
ip -4 addr show dev ens192 | grep 192.168.56.115
通过 VIP 建立连接后开启事务,更新门诊状态,但先不提交:
BEGIN;
UPDATE his_source.outpatient_visit
SET visit_status = 'IN_SERVICE',
update_time = NOW()
WHERE visit_no = 'TESTMZ260904873';
这时停止当前持有 VIP 节点上的 HAProxy。客户端连接会断,VIP 应由另一台仍正常的代理接管。重新通过 VIP 登录后查询:
SELECT
visit_no,
visit_status,
update_time
FROM his_source.outpatient_visit
WHERE visit_no = 'TESTMZ260904873';
连接异常中断后,未提交事务会回滚,所以前面的状态修改不应作为已完成事务保留下来。VIP 切换只解决重新建立连接的问题,无法接管原连接中尚未提交的事务。
十三、COMMIT 附近断连接不能直接重试
收费系统已经发出 COMMIT,但 HAProxy 恰好在结果返回客户端时故障,应用看到的只有连接断开。此时不能直接把连接错误当成收费失败,再执行一次收费 SQL,因为事务可能已经提交,只是成功结果没有返回到客户端。
本次测试使用 TESTSF2609041526 作为收费业务唯一号。发生异常后,先查询这笔流水:
SELECT
charge_no,
visit_no,
amount,
charge_status
FROM his_source.charge_detail
WHERE charge_no = 'TESTSF2609041526';
查询结果还要结合应用侧的收费流水和事务状态处理。医院收费、退费、药房发药和医嘱执行通常都有自己的业务幂等机制,HAProxy 双机不会替应用解决重复提交问题。
十四、停止 Keepalived 进程
上一轮模拟的是 HAProxy 进程停止,这一轮改为停止 Keepalived。为了让测试顺序明确,先在 HAProxy01 恢复 HAProxy,并确认两台代理的 3390 端口和实际地址都可用:
systemctl start haproxy
systemctl status haproxy
ss -lntp | grep 3390
当前配置允许高优先级节点抢占。等 VIP 回到 HAProxy01,并在两台机器上确认只有 HAProxy01 持有 192.168.56.115 后,在 HAProxy01 停止 Keepalived:
systemctl stop keepalived
HAProxy02 收不到 HAProxy01 的 VRRP advertisement 后,应接管 VIP。Keepalived 的 MASTER down timer 与 advertisement interval 和优先级有关,配置说明中的默认计算包含 3 个 advertisement interval 和 skew time。
在两台机器上检查 VIP 所在位置:
ip -4 addr show dev ens192 | grep 192.168.56.115
然后仍通过 192.168.56.115:3390 建立新连接,检查入口是否可用。
十五、节点恢复后的 VIP 回切
当前优先级是:
HAProxy01 priority 120
HAProxy02 priority 100
在 HAProxy01 重新启动 Keepalived:
systemctl start keepalived
systemctl status keepalived
默认抢占开启时,HAProxy01 恢复后会凭借更高优先级重新成为 MASTER,VIP 也会从 HAProxy02 回到 HAProxy01。Keepalived 支持用 nopreempt 禁止自动抢占,但使用该选项时,实例初始状态不能配置为 MASTER。
生产环境要不要自动回切,要按变更制度和业务时段决定。比如凌晨故障切到 HAProxy02 后,如果上午门诊已经开始,修好 HAProxy01 并不代表必须立刻把 VIP 切回来,因为回切会再次影响 HAProxy02 上已经建立的连接。有些环境恢复原主节点,有些环境会保持在备机,等业务低峰再人工回切。
保留默认抢占,用来观察 MASTER/BACKUP 的完整变化。测试结束后,再次确认两台 Keepalived 都在运行,并且 VIP 只存在于一个节点。
十六、网络分区也要单独考虑
停止 HAProxy 或 Keepalived 都是比较干净的故障,真实网络故障可能出现下面的情况:
HAProxy01 可以被应用访问
HAProxy02 也可以被应用访问
两台机器之间收不到对方的 VRRP 单播报文
两边都可能判断对端失效。Keepalived 使用 VRRP 主备机制,没有数据库集群那样的仲裁,因此两节点心跳网络还要考虑双 MASTER 和 VIP 冲突。
unicast_peer 只是把 advertisement 发给指定 peer,不能消除网络分区风险。生产网络需要确认两台代理之间的 VRRP 单播可以双向传输,防火墙没有拦截 VRRP(IP 协议号 112),VIP 网段允许 Gratuitous ARP,交换机端口安全策略也允许 IP 漂移。实验环境里能切换,到了正式网络仍要重新验证这些条件。
十七、检查客户端邻居表
VIP 转移后,客户端所在网络需要把 192.168.56.115 更新到新的 HAProxy 网卡 MAC。在故障前后分别执行:
ip neigh show 192.168.56.115
如果网络能正常处理 Gratuitous ARP,邻居表中的 MAC 会随 VIP 所在节点更新。有些虚拟化平台、交换机端口安全策略或云网络不允许普通虚拟机自由漂移 IP,这类环境不能直接使用传统的二层 Keepalived VIP。
医院现网如果已经有 F5、A10 等双机负载均衡设备,可以复用现有基础设施,没有必要为 TiDB 单独建立一套与现有网络体系不同的入口。
十八、区分两层健康检查
现在有两层检查,范围如下:
| 检查位置 | 检查对象 | 处理动作 |
|---|---|---|
| HAProxy | TiDB01:4000、TiDB02:4000 |
将异常 TiDB Server 从后端池摘除 |
| Keepalived | 本机 HAProxy systemd 状态 | 根据有效优先级决定 VIP 所在节点 |
TiDB01 故障时,HAProxy 把它从后端池摘掉即可,VIP 不需要转移。HAProxy01 故障时,Keepalived 才需要把 VIP 移到 HAProxy02。
客户端连不上 VIP 时,可以按下面的大致顺序逐个检查:
- 确认 VIP 在哪台机器上。
- 查看两台 Keepalived 的状态和日志。
- 检查当前 VIP 节点是否监听
3390。 - 检查 HAProxy 后端 TiDB Server 状态。
- 确认问题进入 TiDB 集群后,再检查 PD、TiKV 和 Region。
十九、把代理层纳入监控
TiDB 使用 Prometheus 保存监控指标,通过 Grafana 和 TiDB Dashboard 查看集群状态。HAProxy 和 Keepalived 不属于 TiDB 组件,增加代理层后,最好补充下面这些检查项:
- HAProxy 进程状态和
3390端口监听状态; - 两台 TiDB 后端在 HAProxy 中是否为 UP;
- Keepalived 进程状态和当前 MASTER;
- VIP 当前所在节点;
- Keepalived 的状态切换日志。
HAProxy 支持 stats 页面和 stats socket,TiDB 官方最佳实践也给出了统计页面配置。数据库入口异常时,只看 TiDB Grafana 里两台 TiDB Server 是否正常还不够,问题也可能出在前面的 HAProxy、Keepalived 或网络。
二十、故障演练结束后核对业务数据
两台 HAProxy 和 Keepalived 都恢复正常后,仍通过 VIP 连接:
mysql -h 192.168.56.115 \
-P 3390 \
-u his_app \
-p his_source
分别检查本次就诊、收费和医嘱:
SELECT
visit_no,
patient_no,
dept_code,
visit_status,
visit_time
FROM his_source.outpatient_visit
WHERE visit_no = 'TESTMZ260904873';
SELECT
charge_no,
visit_no,
item_name,
amount,
charge_status
FROM his_source.charge_detail
WHERE charge_no = 'TESTSF2609041526';
SELECT
order_no,
visit_no,
order_name,
order_status
FROM his_source.doctor_order
WHERE order_no = 'TESTYZ260904231';
汇总检查时,先按门诊号分别聚合收费和医嘱,再与门诊表关联,避免收费明细和医嘱都有多行时把收费金额重复累加:
SELECT
v.visit_no,
v.visit_status,
COALESCE(c.charge_count, 0) AS charge_count,
COALESCE(c.charge_amount, 0) AS charge_amount,
COALESCE(o.order_count, 0) AS order_count
FROM his_source.outpatient_visit v
LEFT JOIN
(
SELECT
visit_no,
COUNT(*) AS charge_count,
SUM(amount) AS charge_amount
FROM his_source.charge_detail
WHERE visit_no = 'TESTMZ260904873'
GROUP BY visit_no
) c ON c.visit_no = v.visit_no
LEFT JOIN
(
SELECT
visit_no,
COUNT(*) AS order_count
FROM his_source.doctor_order
WHERE visit_no = 'TESTMZ260904873'
GROUP BY visit_no
) o ON o.visit_no = v.visit_no
WHERE v.visit_no = 'TESTMZ260904873';
核对门诊记录是否存在、收费是否重复、金额是否变化,以及医嘱有没有因为代理切换多出记录。
结语
两台 HAProxy 配合 Keepalived 后,应用可以始终连接 192.168.56.115:3390。HAProxy01 或其 Keepalived 进程停止时,备用节点可以接管 VIP,新连接无需修改数据库地址。
这套方案不迁移已有 TCP 会话。代理节点故障后,原连接仍会中断;事务发生在提交边界时,应用也要根据业务唯一号和自身事务状态确认结果,不能看到连接错误就直接重试 SQL。
如果现网已经有 F5、A10 等成熟的双机负载均衡设备,可以直接复用现有入口。如果采用 HAProxy + Keepalived 时,VRRP 单播、VIP 漂移、Gratuitous ARP、交换机策略和网络分区都要纳入日常监控和故障演练。