本文主要分为 3 部分,将依次介绍:
基于 ShardingSphere 的分布式数据库「负载均衡架构搭建」要点
结合实际的「用户问题案例」,介绍引入「负载均衡」的影响
介绍并展示 ShardingSphere 分布式数据库在云上的「一站式解决方案」
要点介绍
ShardingSphere 负载均衡架构搭建要点
Apache ShardingSphere 是一款分布式的数据库生态系统,可以将任意数据库转换为分布式数据库,并通过数据分片、弹性伸缩、加密等能力对原有数据库进行增强。它由 ShardingSphere-JDBC 和 ShardingSphere-Proxy 这 2 款既能够独立部署,又支持混合部署配合使用的产品组成。混合部署架构如下:
❝负载均衡方案:
ShardingSphere-JDBC
其中,ShardingSphere-JDBC 定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。ShardingSphere-JDBC 只是在应用进行数据库操作前增加了计算操作,应用进程仍然是通过数据库驱动直接连接数据库。因此,用户无须单独考虑 ShardingSphere-JDBC 的负载均衡,只需关注其应用程序本身如何进行负载均衡即可。
❝负载均衡方案:
ShardingSphere-Proxy
部署架构
ShardingSphere-Proxy 定位为透明化的数据库代理端,通过数据库协议,向数据库客户端提供服务。ShardingSphere-Proxy 作为一个独立部署的进程,在其上层进行负载均衡的参考架构如下:
负载均衡方案要点
社区有同学详细讨论过如何搭建 ShardingSphere-Proxy 集群,也有同学咨询过 ShardingSphere-Proxy 负载均衡后的行为和想象中不一致的问题:
如何来搭建 ShardingSphere-Proxy 集群?
参考:
https://github.com/apache/shardingsphere/discussions/12593
客户端发送的请求通过 HAProxy 后未轮询多个 ShardingSphere-Proxy 实例:
参考:
https://github.com/apache/shardingsphere/issues/20016
ShardingSphere-Proxy 集群负载均衡的要点:数据库协议本身设计是有状态的。例如连接认证状态、事务状态、预编译语句(Prepared Statement)等。
如果 ShardingSphere-Proxy 上层的负载均衡无法理解数据库协议,则只能选择四层负载均衡代理 ShardingSphere-Proxy 集群,客户端与 ShardingSphere-Proxy 的数据库连接状态由具体的 Proxy 实例维护。
由于连接本身状态维护在一个具体的 Proxy 实例中,四层负载均衡只能做到连接级别的负载均衡。对于同一个数据库连接的多个请求,无法轮询到多个 Proxy 实例执行,即无法做到请求级别的负载均衡。关于四层负载均衡与七层负载均衡的详细信息,本文不再赘述。
对应用层的建议
客户端直接连接单个 ShardingSphere-Proxy 与通过负载均衡入口连接 ShardingSphere-Proxy 集群相比,从理论上来说,在功能上没有区别。但不同负载均衡的技术实现与配置存在一些差异。例如,在直接连接 ShardingSphere-Proxy 且没有限制数据库连接会话保持最长时间的情况下,某些 ELB 产品的四层会话保持最长时间为 60 分钟,如果空闲的数据库连接被负载均衡超时关闭,但客户端又对被动的 TCP 连接关闭没有感知,可能会导致应用程序报错。因此,除了在负载均衡层面进行考虑,客户端本身也可以考虑采取一些措施来避免引入负载均衡带来的影响。
执行间隔较长的场景,考虑「按需创建连接」
例如在执行间隔 1 小时且执行时间较短的定时作业,如果创建连接单例并持续使用,数据库连接在大部分时间都处于空闲状态。如果客户端本身无法感知连接状态的变化,长时间空闲会增加连接状态的不确定性。对于执行间隔较长的场景,可以考虑按需创建连接,使用完毕后释放。
考虑通过「连接池」管理数据库连接
一般的数据库连接池都具备维护有效连接、剔除失效连接等能力,通过连接池管理数据库连接,可以减少自行维护连接的成本。
客户端考虑启用 TCP KeepAlive
一般客户端都支持配置 TCP KeepAlive,例如:
MySQL Connector/J 支持配置 autoReconnect 或 tcpKeepAlive,默认不开启;
PostgreSQL JDBC Driver 支持配置 tcpKeepAlive,默认不开启;
不过,启用 TCP KeepAlive 的方式存在限制:
客户端不一定支持配置 TCP KeepAlive 或自动重连;
客户端不打算做任何代码或配置调整;
TCP KeepAlive 依赖操作系统实现与配置;
用户案例
负载均衡配置不合理造成连接中断的问题
前段时间有用户反馈,其部署的 ShardingSphere-Proxy 集群通过上层负载均衡对外提供服务,使用过程中,发现应用与 ShardingSphere-Proxy 之间的连接稳定性存在问题。
某用户生产环境使用 3 节点 ShardingSphere-Proxy 集群,集群通过某云厂商的 ELB 对应用提供服务。
其中一个应用是执行定时作业的常驻进程,定时作业执行频率为每小时执行一次,作业逻辑中存在数据库操作。用户反馈,每次定时作业触发时,应用日志中都会出现报错:
send of 115 bytes failed with errno=104 Connection reset by peer
检查 ShardingSphere-Proxy 日志,没有任何异常信息。该问题仅在执行频率为一小时的定时作业中出现,其他应用访问 ShardingSphere-Proxy 均正常。由于作业逻辑具备重试机制,每次重试后作业执行都能成功,对原本的业务没有造成影响。
应用显示报错的原因非常明确:客户端向一个已经关闭的 TCP 连接发送数据。
因此,问题排查的目标是:明确该 TCP 连接关闭的具体原因。出于以下考虑,我们建议用户在问题复现时间点的前后几分钟内,对应用与 ShardingSphere-Proxy 两侧同时进行网络抓包:
该问题会每小时准时复现;
该问题与网络相关;
该问题不影响用户实时业务。
抓包现象一
ShardingSphere-Proxy 每 15 秒都会受到客户端发起的 TCP 连接建立请求,在完成三次握手建立连接后,客户端却立即向 Proxy 发送了 RST。MySQL 协议的连接建立是由服务端先主动发送 Greeting 给客户端,从抓包结果中看,客户端在接收到 Server Greeting 后没有任何回应就向 Proxy 发送了 RST,甚至在 Proxy 还没有发送 Server Greeting 时就发送了 RST。
但是,在应用侧抓包结果中,却没有找到符合以上行为的流量。在阅读用户所使用的 ELB 的文档发现,以上网络交互是该 ELB 的四层健康检查机制的实现方式。因此,该现象与本案例的问题无关。
抓包现象二
客户端与 ShardingSphere-Proxy 所建立的 MySQL 连接,在 TCP 连接断开阶段,客户端向 Proxy 发送了 RST。
以上抓包结果显示,客户端先主动向 ShardingSphere-Proxy 发送了 COM_QUIT 命令,即该 MySQL 连接是由客户端主动断开,包括但不限于以下可能的情况:
应用程序对 MySQL 连接的使用已完毕,正常关闭数据库连接;
应用程序与 ShardingSphere-Proxy 的数据库连接受到连接池管理,连接池对空闲超时、或超出最长生命周期的空闲连接进行释放操作。
由于连接是应用侧主动关闭,如非应用本身逻辑存在问题,理论上不影响其他业务操作。经过多轮抓包分析,在问题复现前后的几分钟内,都没有发现 ShardingSphere-Proxy 向客户端发送 RST 的情况。根据现有信息推测,客户端与 ShardingSphere-Proxy 的连接有可能在更早的时候就断开了,只是抓包时长有限,没有采集到断开的那一刻。
由于 ShardingSphere-Proxy 本身没有主动断开客户端连接的逻辑,因此考虑从客户端与 ELB 这两层去排查问题。
客户端应用与 ELB 配置检查
根据用户反馈:
应用的定时作业为每小时执行一次,应用没有使用数据库连接池,手动维护了一个数据库连接,提供给定时作业持续使用;
ELB 配置了四层会话保持,会话空闲超时时间为 40 分钟。
考虑定时作业执行的频率,我们建议用户修改 ELB 会话空闲超时大于定时作业的执行间隔时间。用户修改 ELB 超时时间为 66 分钟后,Connection reset 问题不再出现。如果问题排查过程中持续抓包,极有可能在每小时的第 40 分钟捕获到 ELB 断开 TCP 连接的流量。
问题结论
客户端报错 Connection reset by peer 根本原因:ELB 空闲超时时间小于定时任务执行间隔,客户端空闲的时间超过了 ELB 的会话保持超时时间,导致客户端与 ShardingSphere-Proxy 之间的连接被 ELB 超时断开。客户端向已经被 ELB 关闭的 TCP 连接发送数据,导致报错 Connection reset by peer。
超时模拟实验
本文进行一个简单的实验,验证客户端在负载均衡会话超时后的表现,并在实验过程中进行抓包,分析网络流量观察负载均衡的行为。
搭建负载均衡的 ShardingSphere-Proxy 集群环境
理论上任何四层负载均衡实现都能作为本文探讨的对象,因此本文使用 nginx 作为四层负载均衡技术实现。
配置 nginx stream
空闲超时设置 1 分钟,即 TCP 会话保持最多 1 分钟。
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
stream {
upstream shardingsphere {
hash $remote_addr consistent;
server proxy0:3307;
server proxy1:3307;
}
server {
listen 3306;
proxy_timeout 1m;
proxy_pass shardingsphere;
}
}
构造 Docker compose
version: "3.9"
services:
nginx:
image: nginx:1.22.0
ports:
- 3306:3306
volumes:
- /path/to/nginx.conf:/etc/nginx/nginx.conf
proxy0:
image: apache/shardingsphere-proxy:5.3.0
hostname: proxy0
ports:
- 3307
proxy1:
image: apache/shardingsphere-proxy:5.3.0
hostname: proxy1
ports:
- 3307
启动环境
$ docker compose up -d
[+] Running 4/4
⠿ Network lb_default Created 0.0s
⠿ Container lb-proxy1-1 Started 0.5s
⠿ Container lb-proxy0-1 Started 0.6s
⠿ Container lb-nginx-1 Started 0.6s
模拟客户端基于同连接定时任务,构造客户端延迟执行 SQL
此处通过 Java 和 MySQL Connector/J 访问 ShardingSphere-Proxy。
逻辑大致如下:
与 ShardingSphere-Proxy 建立连接,并向 Proxy 执行一次查询;
等待 55 秒后,再向 Proxy 执行一次查询;
等待 65 秒后,再向 Proxy 执行一次查询。
public static void main(String[] args) {
try (Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306?useSSL=false", "root", "root"); Statement statement = connection.createStatement()) {
log.info(getProxyVersion(statement));
TimeUnit.SECONDS.sleep(55);
log.info(getProxyVersion(statement));
TimeUnit.SECONDS.sleep(65);
log.info(getProxyVersion(statement));
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
private static String getProxyVersion(Statement statement) throws SQLException {
try (ResultSet resultSet = statement.executeQuery("select version()")) {
if (resultSet.next()) {
return resultSet.getString(1);
}
}
throw new UnsupportedOperationException();
}
预期结果与客户端运行结果
预期结果:
客户端与 ShardingSphere-Proxy 连接建立且第一次查询成功;
客户端第二次查询成功;
由于 nginx 空闲超时设置为 1 分钟,客户端第三次查询因 TCP 连接断开报错。
执行结果与预期符合。由于编程语言与数据库驱动的差异,报错信息表现不一致,但根本原因相同:都是 TCP 连接已断开。日志如下所示:
15:29:12.734 [main] INFO icu.wwj.hello.jdbc.ConnectToLBProxy - 5.7.22-ShardingSphere-Proxy 5.1.1
15:30:07.745 [main] INFO icu.wwj.hello.jdbc.ConnectToLBProxy - 5.7.22-ShardingSphere-Proxy 5.1.1
15:31:12.764 [main] ERROR icu.wwj.hello.jdbc.ConnectToLBProxy - Communications link failure
The last packet successfully received from the server was 65,016 milliseconds ago. The last packet sent successfully to the server was 65,024 milliseconds ago.
at com.mysql.cj.jdbc.exceptions.SQLError.createCommunicationsException(SQLError.java:174)
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:64)
at com.mysql.cj.jdbc.StatementImpl.executeQuery(StatementImpl.java:1201)
at icu.wwj.hello.jdbc.ConnectToLBProxy.getProxyVersion(ConnectToLBProxy.java:28)
at icu.wwj.hello.jdbc.ConnectToLBProxy.main(ConnectToLBProxy.java:21)
Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure
The last packet successfully received from the server was 65,016 milliseconds ago. The last packet sent successfully to the server was 65,024 milliseconds ago.
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:480)
at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:61)
at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:105)
at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:151)
at com.mysql.cj.exceptions.ExceptionFactory.createCommunicationsException(ExceptionFactory.java:167)
at com.mysql.cj.protocol.a.NativeProtocol.readMessage(NativeProtocol.java:581)
at com.mysql.cj.protocol.a.NativeProtocol.checkErrorMessage(NativeProtocol.java:761)
at com.mysql.cj.protocol.a.NativeProtocol.sendCommand(NativeProtocol.java:700)
at com.mysql.cj.protocol.a.NativeProtocol.sendQueryPacket(NativeProtocol.java:1051)
at com.mysql.cj.protocol.a.NativeProtocol.sendQueryString(NativeProtocol.java:997)
at com.mysql.cj.NativeSession.execSQL(NativeSession.java:663)
at com.mysql.cj.jdbc.StatementImpl.executeQuery(StatementImpl.java:1169)
... 2 common frames omitted
Caused by: java.io.EOFException: Can not read response from server. Expected to read 4 bytes, read 0 bytes before connection was unexpectedly lost.
at com.mysql.cj.protocol.FullReadInputStream.readFully(FullReadInputStream.java:67)
at com.mysql.cj.protocol.a.SimplePacketReader.readHeaderLocal(SimplePacketReader.java:81)
at com.mysql.cj.protocol.a.SimplePacketReader.readHeader(SimplePacketReader.java:63)
at com.mysql.cj.protocol.a.SimplePacketReader.readHeader(SimplePacketReader.java:45)
at com.mysql.cj.protocol.a.TimeTrackingPacketReader.readHeader(TimeTrackingPacketReader.java:52)
at com.mysql.cj.protocol.a.TimeTrackingPacketReader.readHeader(TimeTrackingPacketReader.java:41)
at com.mysql.cj.protocol.a.MultiPacketReader.readHeader(MultiPacketReader.java:54)
at com.mysql.cj.protocol.a.MultiPacketReader.readHeader(MultiPacketReader.java:44)
at com.mysql.cj.protocol.a.NativeProtocol.readMessage(NativeProtocol.java:575)
... 8 common frames omitted
抓包结果分析
抓包结果显示,在连接空闲超时后,nginx 同时断开了和客户端、Proxy 的 TCP 连接。但由于客户端没有任何感知,发送命令后,nginx 返回了 RST。nginx 连接空闲超时后,与 Proxy 正常完成了 TCP 断开连接的流程,后续客户端使用已断开的连接发送请求时,Proxy 是完全没有感知的。分析以下抓包结果:
编号 1~44 是客户端与 ShardingSphere-Proxy 建立 MySQL 连接的交互过程;
编号 45~50 是客户端执行第一次查询;
编号 55~60 是客户端执行第一次查询的 55 秒后,执行第二次查询;
编号 73~77 是在会话超时后,nginx 同时向客户端与 ShardingSphere-Proxy 发起 TCP 连接断开流程;
编号 78~79 是客户端执行第二次查询的 65 秒后,执行第三次查询,发生 Connection Reset。
解决方案
❝ShardingSphere on Cloud
一站式解决方案
人工部署、运维 ShardingSphere-Proxy 集群及负载均衡,难免消耗一定的人力、时间成本。对此,Apache ShardingSphere 重磅推出云上解决方案集合——ShardingSphere on Cloud。
ShardingSphere on Cloud 包括在 AWS、GCP、阿里云等云环境下面向虚机的自动化部署脚本,如 CloudFormation Stack 模板、Terraform 一键部署脚本等,在 Kubernetes 云原生环境下的 Helm Charts、Operator、自动水平扩缩容等工具,以及高可用、可观测性、安全合规、等方面的各类实践内容。ShardingSphere on Cloud 包括以下能力:
基于 Helm Charts 的 ShardingSphere-Proxy 在 Kubernetes 环境下「一键部署」
基于 Operator 的 ShardingSphere-Proxy 在 Kubernetes 环境下「一键部署&自动运维」
基于 AWS CloudFormation 的 ShardingSphere-Proxy 「快速部署」
基于 Terraform 的 AWS 环境下 ShardingSphere-Proxy 「快速部署」
本文简要展示 ShardingSphere on Cloud 的基本能力之一:使用 Helm Charts 在 Kubernetes 一键部署 ShardingSphere-Proxy 集群。
使用以下 3 行命令,即可实现以默认配置在 Kubernetes 集群内创建一个 3 节点的 ShardingSphere-Proxy 集群,并通过 Service 提供服务。
helm repo add shardingsphere https://apache.github.io/shardingsphere-on-cloud
helm repo update
helm install shardingsphere-proxy shardingsphere/apache-shardingsphere-proxy-charts -n shardingsphere
应用即可通过 svc 域名访问 ShardingSphere-Proxy 集群。
kubectl run mysql-client --image=mysql:5.7.36 --image-pull-policy=IfNotPresent -- sleep 300
kubectl exec -i -t mysql-client -- mysql -h shardingsphere-proxy-apache-shardingsphere-proxy.shardingsphere.svc.cluster.local -P3307 -uroot -proot
以上仅仅是对 ShardingSphere on Cloud 基本能力之一的展示,对于更多生产可用的高级特性,欢迎点击探索 ShardingSphere on Cloud 官方文档。
https://shardingsphere.apache.org/oncloud/current/cn/overview/