背景
负载均衡通常使用源地址转换(SNAT)的方式进行负载均衡。负载均衡设备会将客户端发出的请求中的源 IP 地址更改为自己的 IP 地址,然后将请求转发给后端服务器处理。后端服务器响应的数据也会经过负载均衡设备,负载均衡设备再将响应中的目标IP地址更改为客户端的 IP 地址,并将响应返回给客户端。
在 TiDB 的部署拓扑中,负载均衡设备在负载均衡过程中因为 SNAT 隐藏 MySQL 客户端,即应用的真实 IP 地址,使得 tidb-server 只能看到负载均衡设备的 IP 地址。对于 DBA 管理业务应用连接的场景造成不便。
本文探讨在各种负载均衡的场景下的 IP 地址透传原理和实现。
软件负载均衡及 IP 地址透传
软件负载均衡的主要方式及 IP 地址透传机制
目前已知有多种方案能实现软件负载均衡和客户端 IP 地址透传。
- 通过 haproxy 的 proxy 协议。
- LVS 的 TOA 功能。LVS IP 地址透传的主要机制是基于 TCP Option 实现,在 ASKTUG 中已经有文章详述。
- Tidb-loadbalance 客户端负载均衡。客户端负载均衡方式是 JDBC 直连,等同于中间件的 IP 地址透传。
从三种实现机制来看,haproxy 的兼容性最好,仅需要开启配置项。TOA 方式除参数外,还需要服务器系统上添加驱动模块。Tidb-loadbalance 方式对于Java 应用场景较适用,但是对于部分企业场景缺少支持。
Haproxy 的 Proxy 配置
生产环境请参考官方文档
https://docs.pingcap.com/zh/tidb/stable/haproxy-best-practices#%E9%85%8D%E7%BD%AE-haproxy
https://docs.pingcap.com/zh/tidb/stable/tidb-configuration-file#proxy-protocol
haproxy 测试过程
新增一个专用的测试实例并配置允许 proxy 的源 ip
- host: 10.2.103.12
ssh_port: 22
port: 6000
status_port: 16080
deploy_dir: /tidb-deploy/tidb-6000
log_dir: /tidb-deploy/tidb-6000/log
config:
proxy-protocol.networks: 10.2.103.92
arch: amd64
os: linux
Haproxy 中新增一个专用的端口对应测试实例并配置 proxy 协议
listen tidb-cluster # 配置 database 负载均衡。
bind 10.2.103.92:5000 # 浮动 IP 和 监听端口。
mode tcp # HAProxy 要使用第 4 层的传输层。
balance leastconn
server tidb-1 10.2.103.12:6000 send-proxy
注:在测试中去掉了 check 的参数配置,避免 haproxy 的服务端口探活对抓包的影响。
Proxy 的配置行为
通过在 tidb server 使用 Tcpdump 工具进行抓包,交由 Wireshark 后,可以对比看出 proxy 的规则行为。
- MySQL 客户端直连 tidb-server
在节点 12 上捕获从 10.2.103.28 到本机 6000 的数据包。
[tidb@vm10-2-103-12 ~]$ sudo tcpdump -i eth0 host 10.2.103.28 and port 6000 -w 28_range_12_6000
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
^C
14 packets captured
19 packets received by filter
0 packets dropped by kernel
节点 28 的 MySQL 客户端访问节点 12 的 6000 端口的 tidb-server 实例。
[tidb@vm10-2-103-28 ~]$ mysql -h 10.2.103.12 -P6000 -uroot -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 449
Server version: 5.7.25-TiDB-v6.5.1 TiDB Server (Apache License 2.0) Community Edition, MySQL 5.7 compatible
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show processlist;
+---------------------+------+------------------+------+---------+------+------------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+---------------------+------+------------------+------+---------+------+------------+------------------+
| 1563927747162538433 | root | 10.2.103.28:6258 | NULL | Query | 0 | autocommit | show processlist |
+---------------------+------+------------------+------+---------+------+------------+------------------+
1 row in set (0.00 sec)
数据包交互过程如下,TCP三次握手后,TiDB Server 主动发送 “Server Greeting”。
- MySQL 客户端经过 haproxy 连 tidb-server
节点 28 的 MySQL 客户端访问节点 92 的 5000 端口的负载均衡到达节点 12 的 6000 端口的 tidb-server 实例。
[tidb@vm10-2-103-28 ~]$ mysql -h 10.2.103.92 -P5000 -uroot -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 445
Server version: 5.7.25-TiDB-v6.5.1 TiDB Server (Apache License 2.0) Community Edition, MySQL 5.7 compatible
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show processlist;
+---------------------+------+-------------------+------+---------+------+------------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+---------------------+------+-------------------+------+---------+------+------------+------------------+
| 1563927747162538429 | root | 10.2.103.28:21390 | NULL | Query | 0 | autocommit | show processlist |
+---------------------+------+-------------------+------+---------+------+------------+------------------+
1 row in set (0.00 sec)
在节点 12 上捕获从 10.2.103.92 到本机 6000 的数据包。
[tidb@vm10-2-103-12 ~]$ sudo tcpdump -i eth0 host 10.2.103.92 and port 6000 -w 28_range_92_5000_12_6000
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
^C
15 packets captured
24 packets received by filter
0 packets dropped by kernel
数据包交互过程如下,TCP三次握手后和 TiDB Server 主动发送 “Server Greeting” 前,截图右下角的报文内容可以看出 92 负载均衡节点发送一个 Proxy 协议包,包含源 IP(客户端)、目标 IP(负载均衡)、源端口、目标端口,即节点 28 的 21390 端口(与 processlist 对应)访问节点 92 的 5000 端口。
Proxy 的机制实现
从上述行为可以看出,Proxy 协议与 SMTP 邮件的 XCLIENT协议和 HTTP 的 Forwarded 扩展类似,都是通过额外的元数据的定义,实现对客户端地址的透传。在 Tidb-server 的连接器中,通过对 go-proxyprotocol 的调用,及行为控制,最终实现正确地将协议中定义的地址放置在会话的上下文中。
6.5.1 版本上,增加了 fallbackable 特性支持。默认为 false 时,haproxy 所在节点不能直连 tidb-server,不能发不带 proxyheader 的请求,但是其他节点不受此限制。开启 fallbackable 特性后,haproxy 所在节点能发不带 proxyheader 的请求,这个特性的引入是通过上游更新并进行对应的修改实现的。以这个特性更新为例,可以看出 Proxy 部分的代码逻辑。
go-proxyprotocol 的上游更新 https://github.com/blacktear23/go-proxyprotocol/commit/d98ad85966a259234ba2f1eef99f33c71857480e ,可以看出
proxy_protocol.go 中包含 NewLazyListener 函数发起监听,可以接受超时时间、观测 IP、lazymode 和 fallbackable 新特性的定义,最终的解析行为由 parseHeader 及 extraceClientIPV2 完成。
TiDB 的更新 https://github.com/pingcap/tidb/pull/41393/files#diff-412615e1c1d4c6ae11a7885a11c879778bf401e0967c9c13c4b9dfd1f13ed29a ,可以看出 main.go 实现配置文件传入的定义。server.go 实现 proxy 监听及行为参数的控制,如:fallbackable 新特性的参数。conn.go 实现会话上下文的传入。
硬件负载均衡及 IP 地址透传
F5 设备的负载均衡配置
F5 的 VS 配置 irule
iRule 是 F5 的一项高级特性,基于 Tcl 编程语言,可用于自定义 F5 BIG-IP 交付控制器的行为。通过编写 iRules 代码,可以根据需要拦截流量并执行自定义操作,例如添加 Proxy 协议的请求头。
在 TCP 四层通过 proxy protocol插入,F5 的 VS 要配成七层模式,通过 irule 插入 proxy protocol 字段。
在 F5 上的 iRules 规则如下:
两条 iRule 规则意义如下:
- 当客户端连接到 F5 BIG-IP 上时,这个 iRule 将被触发。具体来说,这个 iRule 将设置一个名为 proxyheader 的变量,它将包含一个 PROXY 协议头信息,该协议头信息描述了客户端正在发起的连接。该协议头信息的格式如下:
PROXY TCP[IP::version] [IP::remote_addr] [IP::local_addr] [TCP::remote_port] [TCP::local_port]\r\n
其中,[IP::version] 将由IP地址协议版本号替代,可以是 IPv4 或 IPv6。[IP::remote_addr] 和 [IP::local_addr] 将分别由客户端和服务器的IP地址替代。[TCP::remote_port] 和 [TCP::local_port] 将分别由客户端和服务器的TCP端口号替代。最后,\r\n 表示一个回车符和一个换行符,表示 PROXY 协议头的结束。
- 当 F5 BIG-IP 与服务器建立连接时,将使用变量 $proxyheader 中存储的 PROXY 协议头信息来响应服务器。具体来说,TCP::respond 命令将使用 $proxyheader 变量中存储的 PROXY 协议头信息来向服务器发送响应,并将该协议头信息附加到响应数据中。这意味着服务器将收到一个带有 PROXY 协议头信息的响应,并且可以从协议头信息中确定发起请求的客户端的 IP 地址和端口号等详细信息。
irule 配置总结
基于以上两条 iRule 规则的组合,可以实现与 haproxy 的 proxy 协议包兼容的效果。客户端连接到 F5 BIG-IP 时添加一个 PROXY 协议头信息,以便在转发流量时可以更好地识别发起连接的客户端。F5 BIG-IP 与服务器建立连接时,使用 $proxyheader 变量中存储的 PROXY 头信息响应服务器。这将使服务器能够更好地识别从客户端发起的连接,并增加应用程序的安全性和可靠性。
深信服设备的负载均衡配置
网络拓扑
客户端 10.10.10.88 访问深信服负载均衡设备的数据库的虚拟服务 10.10.10.235,负载均衡设备往后转发数据库请求的时候会使用负载均衡设备 10.10.10.52 的 IP 去连真实的 TiDB 数据库节点 10.10.10.180。
TiDB 配置项
tidb_servers:
- host: 10.10.10.180
ssh_port: 22
port: 3306
status_port: 10080
deploy_dir: /home/tidb/tidb-deploy/tidb-4000
log_dir: /home/tidb/tidb-deploy/tidb-4000/log
config:
proxy-protocol.networks: 10.10.10.52
arch: amd64
os: linux
TiDB 监听使用 3306 端口,proxy-protocol.networks 的 IP 指定为深信服转发 SNAT 的实际服务器 IP,如果有多个 IP,需要配置完整。
本例中,只配置在测试的一个单独实例上。如果是全局配置,按实际情况配置 server_configs。
深信服配置
深信服的固定 IP 是 10.10.10.52。
TiDB 的 VS 是 10.10.10.235,监听在 3306。
深信服 iPro 和 F5 iRule 都基于 TCL 语言实现的事件驱动框架,依靠其可编程的特性来实现流量的自定义分发和快速标准化交付。TiDB 的 VS 需要配置 iPro 规则 proxyheader 实现 proxy protocol 的兼容。
event CLIENT_ACCEPT {
local proxyheader = string.format("PROXY TCP4 %s %s %d %d\r\n",
TCP.remoteaddr(), TCP.localaddr(), TCP.remoteport(), TCP.localport())
--日志调试接口,需要调试时,去掉注释即可.
--LOG.log(LOG.INFO, proxyheader)
TCP.replace(1, 1, proxyheader)
TCP.release()
}
iPro 规则 proxyheader 功能与 F5 的 irules 类似,也是在链路上发送 proxy header 数据包。
[root@localhost ~]# mysql -uroot -h 10.10.10.235 -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 413
Server version: 5.7.25-TiDB-v6.5.1 TiDB Server (Apache License 2.0) Community Edition, MySQL 5.7 compatible
Copyright (c) 2000, 2023, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show processlist;
+---------------------+------+-------------------+------+---------+------+------------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+---------------------+------+-------------------+------+---------+------+------------+------------------+
| 4816881276449456541 | root | 10.10.10.88:38840 | NULL | Query | 0 | autocommit | show processlist |
+---------------------+------+-------------------+------+---------+------+------------+------------------+
1 row in set (0.00 sec)
mysql>
MySQL 客户端登录 TiDB 后,可以看到 show processlist 上可以看出客户端的正确 IP。
在 TiDB 服务器 10.10.10.180 的服务器上抓包,可以看到与 Haproxy 类似,在 TCP 握手时,就会有 proxy header 数据包。
典型问题
现象
MySQL 客户端使用 LB 的 IP 登录遇到 ERROR 2013 (HY000): Lost connection to MySQL server at 'reading authorization packet', system error: 0。
提示。
分析:
从抓包过程中可以看到 proxy 包正常发送,但是 tidb-server 应答报错信息。
问题定位
该提示是 proxy-protocol.networks 未正确配置导致的,主要的原因是以下情况
- 深信服使用自动 SNAT 池,但是 IP 地址池未提供完整。
- proxy-protocol.networks 填写错误,如实例级别配置覆盖 servers_config,或者地址不完整。建议使用 SQL>show config where type='tidb' and name likes '%proxy%'; 的输出进行核对。
问题小结
与网络同事沟通时,要收集完整所有的 SNAT IP 地址池。
使用逗号隔开的方式写完整所有的 IP 清单,也可以使用子网地址段的方式。使用子网地址段时,建议打开 fallbackable 特性。
总结
TiDB 的客户端 IP 地址透传在主流硬件负载均衡平台上可以通过可编程的规则引擎实现与 haproxy 规则的兼容。
本篇文档使用 proxy protocol v1 版本在硬件负载平衡上进行仿真模拟。如果是 ipv6 环境,可能需要配置 proxy protocol v2,需要参考 https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt 进一步配置。