本文主要分为 3 部分,将依次介绍:
- 基于 ShardingSphere 的分布式数据库「负载均衡架构搭建」要点
- 结合实际的「用户问题案例」,介绍引入「负载均衡」的影响
- 介绍并展示 ShardingSphere 分布式数据库在云上的「一站式解决方案」
文章目录
- ShardingSphere 负载均衡架构搭建要点
- SharidngSphere-JDBC 负载均衡方案
- SharidngSphere-Proxy 负载均衡方案
- 部署架构
- 负载均衡方案要点
- 对应用层的建议
- 执行间隔较长的场景考虑按需创建连接
- 考虑通过通过连接池管理数据库连接
- 客户端考虑启用 TCP KeepAlive
- 用户案例:负载均衡配置不合理造成连接中断的问题
- 问题描述
- 问题分析
- 抓包现象一
- 抓包现象二
- 客户端应用与 ELB 配置检查
- 问题结论
- 超时模拟实验
- 搭建负载均衡的 ShardingSphere-Proxy 集群环境
- 配置 nginx stream
- 构造 Docker compose
- 启动环境
- 模拟客户端基于同连接定时任务
- 构造客户端延迟执行 SQL
- 预期结果与客户端运行结果
- 抓包结果分析
- ShardingSphere on Cloud 一站式解决方案
ShardingSphere 负载均衡架构搭建要点
Apache ShardingSphere 是一款分布式的数据库生态系统,可以将任意数据库转换为分布式数据库,并通过数据分片、弹性伸缩、加密等能力对原有数据库进行增强。它由 ShardingSphere-JDBC 和 ShardingSphere-Proxy 这 2 款既能够独立部署,又支持混合部署配合使用的产品组成。混合部署架构如下:
SharidngSphere-JDBC 负载均衡方案
其中,ShardingSphere-JDBC 定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。ShardingSphere-JDBC 只是在应用进行数据库操作前增加了计算操作,应用进程仍然是通过数据库驱动直接连接数据库。
因此,用户无须单独考虑 ShardingSphere-JDBC 的负载均衡,只需关注其应用程序本身如何进行负载均衡即可。
SharidngSphere-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/