0
3
3
3
博客/.../

TiDB 7.5 HAProxy 双机与 Keepalived VIP 切换测试记录

 拍脑袋小助手  发表于  2026-09-04
原创测试

前言

上一篇把两台 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.110192.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_visitcharge_detaildoctor_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()
);

TESTMZ260904873TESTBR04872 只用于本次测试,不对应医院里的真实门诊号和患者编号。写入后按门诊号查询:

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 秒,它还受 intervalfall、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:4000TiDB02:4000 将异常 TiDB Server 从后端池摘除
Keepalived 本机 HAProxy systemd 状态 根据有效优先级决定 VIP 所在节点

TiDB01 故障时,HAProxy 把它从后端池摘掉即可,VIP 不需要转移。HAProxy01 故障时,Keepalived 才需要把 VIP 移到 HAProxy02。

客户端连不上 VIP 时,可以按下面的大致顺序逐个检查:

  1. 确认 VIP 在哪台机器上。
  2. 查看两台 Keepalived 的状态和日志。
  3. 检查当前 VIP 节点是否监听 3390
  4. 检查 HAProxy 后端 TiDB Server 状态。
  5. 确认问题进入 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、交换机策略和网络分区都要纳入日常监控和故障演练。

0
3
3
3

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论