作者: Kassadar



引言

在确保高可用性和优化负载均衡方面,监测后端服务的健康状况是关键步骤。HAProxy,作为一款被广泛使用的开源负载均衡器,提供了一个强大的健康检查框架,以确保服务的持续可用性。这个框架包括多种检查手段,从简单的TCP连接检查到复杂的应用层协议检查。对于采用TiDB作为后端数据库服务的系统,选择合适的健康检查配置是至关重要的,它可以确保只有健康的节点才会接收流量。在本文中,我们将探讨不同的健康检查配置,并解释为什么使用 option external-check 来执行自定义健康检查脚本,是确保 TiDB 服务稳定和可靠的最佳选择。



先上结论

在 HAProxy 的配置中,可以通过不同的检查选项来监控后端服务器的健康状况。以下是四种常见的健康检查机制的简述:



缺省的 TCP 连接检测

HAProxy 默认采用尝试建立 TCP 连接的方法来进行健康检查,这种方式能够确认端口的开放状态。然而,它的局限性在于无法检测到服务进程是否真正能够处理请求。即使服务进程被挂起(例如收到了 kill -SIGSTOP 命令),TCP 端口可能仍然在监听状态,从而导致健康检查误判服务仍然可用。



MySQL 协议检测

当启用 option mysql-check 后,HAProxy 执行更为精细的健康检查,因为它实际上模拟了客户端的协议级别交互。然而,仍然要注意,即使这种方式可以检测 MySQL 服务是否能够接受连接,但它不会执行实际的登录操作或查询,因此它可能无法捕获到数据库内部的一些问题,不足以覆盖 TiDB 这种计算存储分离的分布式数据库的复杂性,例如无法捕捉计算层与存储层间潜在的通信问题。



HTTP 检测

通过 option httpchk,HAProxy 利用 HTTP 请求来进行服务健康检查。服务返回的 HTTP 状态码在 200 到 399 之间通常表示服务运行正常。但目前缺乏全链路检查的 API,无法完整地评估像 TiDB 这样的长服务链路状况,完整涵盖 TiDB、PD 和 TiKV 之间的通信链路。



外部脚本检测

option external-check 使得 HAProxy 能够通过执行自定义的外部脚本来进行复杂的健康检查。这种方法的灵活性最高,可以针对具体的服务需求设计检查逻辑。例如,在检测 TiDB 时,脚本可以执行实际的 SQL 查询以测试服务的响应能力。然而,这种方法要求脚本配置正确,具备适当的执行权限,并且依赖的数据库结构(例如 health_check 表)以及查询必须是正确的,以确保服务状态的准确性,并避免错误的健康报告。

综合来看,配置 HAProxy 执行自定义外部脚本检查(使用 option external-check)是进行健康检查的首选方案。此方法不仅能够验证 TiDB 的网络层连通性,还能通过执行具体的数据库查询或交互来确保 TiDB 以及与之关联的服务能够正常处理请求。这种定制化的健康监测手段能够提供更深入的服务状态检查,有效避免不健康的 TiDB 实例处理客户端请求,从而显著提升整个服务的稳定性和可靠性。下面是配置参考:

global
    log /dev/log local0  # 指示 HAProxy 将日志消息发送到系统日志设施(syslog),使用 local0 设施。
    log /dev/log local1 notice  # 将通知级别以上的日志消息发送到使用 local1 设施的系统日志。
    maxconn 4096  # 设置 HAProxy 能够处理的最大并发连接数。
    user haproxy  # 指定运行 HAProxy 服务的用户为 'haproxy'。
    group haproxy  # 指定运行 HAProxy 服务的组为 'haproxy'。
    daemon  # 指示 HAProxy 作为守护进程在后台运行。
    nbproc 8  # 设置要启动的工作进程数。通常设置为 CPU 核心数。
    external-check  # 启用外部健康检查功能。
    #insecure-fork-wanted  # 允许在没有 'haproxy' 用户权限的情况下运行外部检查脚本。

defaults
    log     global  # 引用上面定义的全局日志配置。
    mode    tcp  # 将默认模式设置为 TCP。
    option  tcplog  # 启用 TCP 连接的详细日志记录。
    timeout connect 5000ms  # 设置等待连接尝试成功的最大时间。
    timeout client  50000ms  # 设置客户端侧的最大不活动时间。
    timeout server  50000ms  # 设置服务器侧的最大不活动时间。

listen stats
    bind *:8080  # 将统计接口绑定到端口 8080 的所有可用 IP 地址。
    mode http  # 将此部分的模式设置为 HTTP,这对于提供统计页面是必要的。
    stats enable  # 启用统计页面。
    stats uri /  # 设置统计页面的 URI 端点。
    stats realm Haproxy\ Statistics  # 设置统计页面认证提示的标题。
    stats auth admin:admin  # 设置访问统计页面所需的凭据(用户名:密码)。

listen tidb
    bind 0.0.0.0:5000  # 将 TiDB 服务绑定到端口 5000 的所有可用 IP 地址。
    mode tcp  # 以 TCP 模式运行,因为 TiDB 是基于 TCP 的服务。
    balance leastconn  # 使用最少连接算法进行负载均衡。
    option external-check  # 为此后端启用外部健康检查。
    external-check command /usr/local/etc/haproxy/health_check.sh  # 指定外部健康检查的命令。
    server tidb0 172.28.159.128:4001 check  # 定义一个服务器供 HAProxy 负载均衡,启用健康检查。
    server tidb1 172.28.159.128:4000 check  # 再定义一个服务器供 HAProxy 负载均衡,同样启用健康检查。
#!/bin/bash

DB_USER="haproxy_check"
DB_PASS="your_password"
DB_NAME="haproxy_db"

# 从环境变量中获取 IP 和端口
DB_HOST="$HAPROXY_SERVER_ADDR"
DB_PORT="$HAPROXY_SERVER_PORT"

# 模拟DML操作,这里以 SELECT 为例
simulate_dml() {
    /usr/bin/mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "
        select * from health_check limit 1;
    "
    return $?
}

# 调用模拟函数
simulate_dml

# 根据上面 DML 操作的结果退出,如果成功则退出码为 0,否则为 1
if [ $? -eq 0 ]; then
    exit 0
else
    exit 1
fi



In Action



TiDB 集群拓扑

Haproxy 探活 TiDB in Action_tikv



Haproxy 中使用默认 check

如果未对健康检查进行特别设置,HAProxy 将执行默认的健康检查。这涉及到周期性地对标记为 check 的每个服务器尝试建立 TCP 连接。HAProxy 会定期向服务器的指定 IP 地址和端口发起连接请求。如果连接成功建立,即完成了 TCP 三次握手,服务器则被视为健康。反之,如果连接尝试失败,例如因连接超时、连接被拒绝或其他 TCP 错误,服务器会被认定为不健康。在一定数量的失败尝试后,HAProxy 会根据配置决定将服务器从负载均衡队列中剔除。

defaults
    log     global
    mode    tcp
    option  tcplog
    timeout connect 5000ms
    timeout client  50000ms
    timeout server  50000ms

listen stats
    bind 0.0.0.0:8080
    mode http
    stats enable
    stats uri /
    stats realm Haproxy\ Statistics
    stats auth admin:admin

listen tidb
    bind 0.0.0.0:5000
    mode tcp
    balance leastconn
    server tidb1 172.28.159.128:4000 check

Haproxy 探活 TiDB in Action_haproxy_02

使用 kill -SIGSTOP <pid> 挂起 tidb 服务进程,但不释放端口

kill -SIGSTOP <pid>

Haproxy 探活 TiDB in Action_mysql_03

此时 tiup 已经发现 tidb is down,但 haproxy 中仍然被识别为 up 状态,检测类型为 L4

Haproxy 探活 TiDB in Action_tidb_04

尝试访问该 tidb,会在交互命令行卡住,tidb 无法正常提供服务

Haproxy 探活 TiDB in Action_mysql_05

SIGSTOP 是一个信号,在Unix和类Unix操作系统中用于停止(挂起)一个进程的执行。当一个进程接收到 SIGSTOP 信号时,它的执行会立即停止,直到它收到一个 SIGCONT 信号为止。在进程被停止期间,它仍然保留在系统的进程表中,它的所有状态信息(例如内存、打开的文件描述符、程序计数器位置等)都保持不变,但是CPU不会再执行这个进程的任何指令。

可以使用 kill 命令向进程发送 SIGSTOP 信号:

kill -SIGSTOP <pid>

与 SIGSTOP 对应的是 SIGCONT 信号,它用于恢复被 SIGSTOP 信号挂起的进程。当一个进程收到 SIGCONT 信号时,它会恢复执行。恢复进程的命令如下:

kill -SIGCONT <pid>

这个机制可以用来控制进程的执行,例如,在调试时暂停和继续进程,或者是在系统管理中暂时停止服务以进行维护,而不是完全终止它。

为了解决挂起的 tidb 未被 haproxy 检测出的问题,接下来我们尝试使用 haproxy 中的 mysql-check 进行 mysql 连接。



Haproxy 中使用 option mysql-check

使用 option mysql-check 指令允许 HAProxy 执行专门针对 MySQL 服务器的健康检查。这个设置使得 HAProxy 能够定期验证 MySQL 服务器的连接能力及其运行状态。启用该选项后,HAProxy 会向 MySQL 服务器发送符合 MySQL 协议的检查请求。HAProxy 首先尝试建立一个 TCP 连接,随后按照 MySQL 协议发送一个握手包,并等待来自 MySQL 服务器的适当响应。如果服务器的响应符合预期,HAProxy 则认定服务器为健康;如果响应不符或连接建立失败,服务器被判定为不健康,暂停向该服务器发送请求。

defaults
    log     global
    mode    tcp
    option  tcplog
    timeout connect 5000ms
    timeout client  50000ms
    timeout server  50000ms

listen stats
    bind 0.0.0.0:8080
    mode http
    stats enable
    stats uri /
    stats realm Haproxy\ Statistics
    stats auth admin:admin

listen tidb
    bind 0.0.0.0:5000
    mode tcp
    balance leastconn
    option mysql-check user haproxy_check
    server tidb1 172.28.159.128:4000 check

这个检查是通过简单的 TCP 握手完成的,不进行完整的登录过程,因此不需要用户密码,不需要授予任何特殊的数据库权限。但是,需要在 MySQL 服务器上创建一个用户供 HAProxy 使用,因为它需要在握手过程中提供用户名。

CREATE USER 'haproxy_check'@'%' IDENTIFIED BY '';
FLUSH PRIVILEGES;

重启 haproxy,可以看到检测类型变为 L7,tidb 状态 up

Haproxy 探活 TiDB in Action_tikv_06

再次注入 SIGSTOP,查看 Haproxy 中 tidb 的状态,已经被识别为 down

Haproxy 探活 TiDB in Action_haproxy_07

但是 option mysql-check 是面向单机 mysql 设计的,它并不能覆盖到 TiDB 的一些特殊场景,原因是:

这个检查是通过简单的 TCP 握手完成的,不需要进行完整的登录过程,而 TiDB 是一个计算存储分离的分布式数据库

我们假设如下情况,关闭集群中的 pd 和 kv,保留 tidb 存活(真实场景中,可能是 pd 故障了,也可能是 tidb 的网络隔离了)

Haproxy 探活 TiDB in Action_tikv_08

尝试访问存活的 tidb,并进行 select 1 查询,发现可正常响应请求

Haproxy 探活 TiDB in Action_tidb_09

既然仍然可以连接到 tidb,并进行 dual 查询,haproxy 中的 tidb 状态肯定仍然是 up

Haproxy 探活 TiDB in Action_mysql_10

当运行真实业务查询时,则会返回预期的 pd server timeout 报错

Haproxy 探活 TiDB in Action_health_11

为了解决孤立的 tidb 未被 haproxy 检测出的问题,接下来我们尝试调用会触发和存储层交互的 tidb http api。



Haproxy 中使用 option httpchk

option httpchk 指令在 HAProxy 配置中用于实现 HTTP 服务的健康检查。这个选项适用于检查后端 HTTP 服务器对 HTTP 请求的响应。当在后端配置中设置了 option httpchk,HAProxy 会对每个服务器发送 HTTP 请求(如一个 GET 请求),并以服务器的响应来判定其健康状态。如果服务器返回了预期的响应,通常是 HTTP 200 至 399 的状态码,HAProxy 认为服务器运行正常;如果返回了错误的状态码或未响应,服务器将被视为不健康。在这种情况下,服务器会被临时从负载均衡池中移除,以阻止流量发送至此。

defaults
    log     global
    mode    tcp
    option  tcplog
    timeout connect 5000ms
    timeout client  50000ms
    timeout server  50000ms

listen stats
    bind 0.0.0.0:8080
    mode http
    stats enable
    stats uri /
    stats realm Haproxy\ Statistics
    stats auth admin:admin

listen tidb
    bind 0.0.0.0:5000
    mode tcp
    balance leastconn
    option httpchk GET /regions/meta
    server tidb1 172.28.159.128:4000 check port 10080

option httpchk GET /regions/meta 加上server tidb1 172.28.159.128:4000 check port 10080 会生成一个类似 curl 的访问请求:

curl http://{TiDBIP}:10080/regions/meta

该 API 负责从 TiKV 中检索元数据相关的 region信息。若 API 能成功返回数据,则意味着几个关键组件均处于健康状态:首先,TiKV 正常运行并与 PD 保持通讯,其次,PD 也在正常工作。由于此 API 属于 TiDB,其正常响应进一步表明 tidb 亦运作良好。因此,该 API 的的调用结果可以作为判定 TiKV、PD 及 TiDB 这这三大核心组件运行状态的依据。此外,由于该 API 基于 HTTP 协议,它适用于 HTTP 探测需求。

Haproxy 探活 TiDB in Action_mysql_12

重启 haproxy,发现在孤立 tidb 场景中,可正确识别到 tidb 的状态为 down

Haproxy 探活 TiDB in Action_tikv_13

但是,目前并没有一个 tidb http api 可以模拟 tidb->pd->tikv 的完整链路,由于 region cache 的存在,curl http://{TiDBIP}:10080/regions/meta 实际并不需要访问 tikv,只模拟了 tidb->pd,即如果 tidb、pd 正常,而 tikv 失联,最终检测状态仍然会是 up

Haproxy 探活 TiDB in Action_tidb_14

为了解决 tidb 暂无 health check api 的问题,接下来我们尝试模拟实际的业务请求



Haproxy 中使用 option external-check

option external-check 是 HAProxy 配置中的一个高级功能,它允许你通过执行自定义的外部命令或脚本来执行健康检查。这种方法提供了比标准的 TCP 或 HTTP 健康检查更高的灵活性,因为它可以根据应用程序的特定需求和逻辑来检测后端服务器的健康状况。

在 HAProxy 的 backend 配置部分启用 option external-check 后,HAProxy 会在进行健康检查时调用指定的外部程序。程序会对每个后端服务器进行检查,并通过其退出代码来报告服务器的健康状态。如果外部程序返回的退出状态码是 0,HAProxy 将认为该后端服务器健康;任何其他的非零退出状态都将标记该服务器为不健康。

这种外部检查机制非常适合于那些需要超出常规检查范围的场景,如执行数据库查询,检查应用程序内部状态,或者与服务器上的特定服务进行交云,以确保后端服务器不仅在网络层面上是可达的,而且其应用程序也是完全功能性的。

global
    external-check
#   insecure-fork-wanted

defaults
    log     global
    mode    tcp
    option  tcplog
    timeout connect 5000ms
    timeout client  50000ms
    timeout server  50000ms

listen stats
    bind 0.0.0.0:8080
    mode http
    stats enable
    stats uri /
    stats realm Haproxy\ Statistics
    stats auth admin:admin

listen tidb
    bind 0.0.0.0:5000
    mode tcp
    balance leastconn
    option external-check
    external-check command /usr/local/etc/haproxy/health_check.sh
    server tidb0 172.28.159.128:4001 check
    server tidb1 172.28.159.128:4000 check

我们创建一个探活的数据库账号,并配置一张用于探活的表

CREATE USER 'haproxy_check'@'%' IDENTIFIED BY 'your_password';

CREATE DATABASE IF NOT EXISTS haproxy_db;
USE haproxy_db;
CREATE TABLE IF NOT EXISTS health_check (
    id INT
);

GRANT SELECT ON haproxy_db.health_check TO 'haproxy_check'@'%';

FLUSH PRIVILEGES;

编写探活脚本去访问 haproxy_db.health_check 表

#!/bin/bash

DB_USER="haproxy_check"
DB_PASS="your_password"
DB_NAME="haproxy_db"

# 从环境变量中获取 IP 和端口
DB_HOST="$HAPROXY_SERVER_ADDR"
DB_PORT="$HAPROXY_SERVER_PORT"

# 模拟DML操作,这里以 SELECT 为例
simulate_dml() {
    /usr/bin/mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "
        select * from health_check limit 1;
    "
    return $?
}

# 调用模拟函数
simulate_dml

# 根据上面 DML 操作的结果退出,如果成功则退出码为 0,否则为 1
if [ $? -eq 0 ]; then
    exit 0
else
    exit 1
fi

$HAPROXY_SERVER_ADDR$HAPROXY_SERVER_PORT 环境变量被用来构造用于连接到 TiDB 服务器的 MySQL 命令行。这样,脚本可以对每个后端服务器进行健康检查,通过尝试访问 haproxy_db.health_check 表来确认tidb 服务器是否正常响应 MySQL 查询请求。

这个机制的工作流程如下:

  1. HAProxy 需要对名为 tidb0tidb1 的后端服务器进行健康检查。
  2. HAProxy 设置环境变量 $HAPROXY_SERVER_ADDR$HAPROXY_SERVER_PORT 分别为目标服务器的 IP 地址和端口号。
  3. HAProxy 执行指定的外部检查命令 /usr/local/etc/haproxy/health_check.sh
  4. 探活脚本使用这些环境变量,通过 MySQL 客户端尝试访问 TiDB 服务器。
  5. 如果 MySQL 查询成功,脚本返回退出码 0,表示健康检查通过;如果查询失败,脚本返回退出码 1,表示健康检查失败,HAProxy 将根据配置决定是否将该服务器标记为不健康。

需要注意的是,使用中需确保这个脚本有适当的权限被 HAProxy 进程执行,并且 MySQL 客户端工具已经安装在 HAProxy 服务器上。此外,确保 HAProxy 配置文件中的 external-check 相关设置正确无误,以便 HAProxy 可以顺利调用外部探活脚本。

重启 haproxy,发现检测类型变为 PROC,并能正确检测出 tidb 的工作状态

Haproxy 探活 TiDB in Action_mysql_15

需要注意的是,检测依赖 health_check 表,如果 health_check 表不存在,tidb 状态将变为 down。更准确的说配置的探活 SQL 不能报错,否则就会被标为 down。

Haproxy 探活 TiDB in Action_mysql_16

至此,我们得到了一个较为严谨的 haproxy 探活 tidb 的设置