sharding-jdbc 分库连接数优化 | 京东物流技术团队

[复制链接]
作者: 冬致夏陌 | 时间: 2024-4-25 00:01:27 |
0 50

2003

主题

2003

帖子

6009

积分

研究生

Rank: 9Rank: 9Rank: 9

积分
6009
发表于 2024-4-25 00:01:27| 显示全部楼层 |阅读模式
一。背景:
配运平台组的快递订单履约中心 (cp-eofc) 及物流平台履约中心 (jdl-uep-ofc) 系统都使用了 ShardingSphere 生态的 sharding-jdbc 作为分库分表中间件,整个集群采用只分库不分表的设计,共 16 个 MYSQL 实例,每个实例有 32 个库,集群共 512 个库.
当每增加一台客户端主机,一个 MYSQl 实例最少要增加 32 个连接 (通常都会使用连接池,根据配置的最大连接数,这个连接数可能会放大 5~10 倍). 并且通常一个系统都会分为 web,provider,worker 等多个应用,这些应用共用一套数据源。随着应用机器数的增加,MYSQL 实例的连接数会很快达到上限,这就对系统的扩容造成了阻碍,无法横向的增加机器数,只能纵向的提高机器的配置来应对流量的增长.
作为京东物流的核心系统,业务增长迅速,系统所承接的流量也是逐渐增加,所以急需解决这个制约系统扩展的瓶颈点.
二。分库分表的相关概念介绍
2.1 为什么要分库分表
2.1.1 分库
随着业务的发展,单库中的数据量不断增加,数据库的 QPS 会越来越高,对数据库的读写耗时也会相应的增长,这时单库的读写性能必然会成为系统的瓶颈点。这时可以通过将单个数据库拆分为多个数据库的方法,来分担数据库的压力,提升性能。同时多个数据库分布在不同的机器上也提高了数据库的可用性.
2.1.2 分表
随着单表数据量的增加,对于数据的查询和更新,即使在数据库底层有一定的优化,但是随着量变必定会引起质变,导致性能急剧下降。这时可以通过分表的方法,将单表数据按一定规则水平拆分到多个表中,减小单表的数据量,提升系统性能.
2.2 sharding-jdbc 简介
  ShardingSphere
  是一套开源的分布式数据库中间件解决方案组成的生态圈,它由 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar(计划中)这 3 款相互独立的产品组成。他们均提供标准化的数据分片、分布式事务和数据库治理功能,可适用于如 Java 同构、异构语言、容器、云原生等各种多样化的应用场景。
  Sharding-JDBC
  定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。
  适用于任何基于 Java 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC。基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP 等。
  支持任意实现 JDBC 规范的数据库。目前支持 MySQL,Oracle,SQLServer 和 PostgreSQL。
我们先看下 ShardingSphere 官网给出的基于 Spring 命名空间的规则配置示例:






























配置总结:
1. 需要配置多个数据源 ds0,ds1;
2. 分片策略中配置分片键 (sharding-column) 和分片表达式 (algorithm-expression) 需符合 groovy 语法;
3. 在 sharding 数据源中 sharding:table-rule 标签中配置逻辑表名 (logic-table), 库分片策略 (database-strategy-ref) 和表分片策略 (table-strategy-ref),actual-data-node 属性由数据源名 + 表名组成,以小数点分隔,用于广播表;
三。问题分析与解决方案
3.1 问题分析
正如文章开头提到的目前我们的 MYSQL 集群架构如下,16 个 MYSQL 实例,每个实例有 32 个库,集群共 512 个库。当客户端主机启动后与 MYSQL_0 实例中的 32 个库连接,分别会建立 32 个数据源,连接池配置的最大连接数为 5, 也就是说极端情况下一个客户端与一个 MYSQL 实例最多会建立 32*5=160 个连接数。对于物流的一些核心系统在大促时扩容上百台是很常见的,所以很快单个实例的最大连接数就会触达上限.
目前客户端连接连接数据库集群形式如图所示:

3.2 可行方案
我们的目标就是降低单个 MYSQL 实例的连接数,其中我们共探讨了几种方案如下:
3.2.1 单实例不分库只分表
这样一个客户端与单个数据库实例只需通过一个连接池连接,大大降低了连接数。但这种方案改变了现有的分片规则,需要新建一套数据库集群,根据新规则同步历史数据和增量数据,还有新旧数据验证,但难度和风险最高的还是线上切换过程,可能会造成数据不一致,且一旦出问题回滚方案也会非常复杂.
3.2.2 使用支持弹性扩展的数据库
使用京东的 jed,tidb 等支持弹性扩展的数据库,将数据同步到新库中,这类数据库的优势是开发人员只需关注业务,不需要再去处理数据库连接这些底层细节.
3.2.3 使用 sharding-proxy
Sharding-Proxy 的定位是透明化的数据库代理,我们可以在服务器上部署一套 Sharding-Proxy, 客户端只需连接 proxy 服务,再由 proxy 服务器连接 MYSQL 集群,这样 MYSQL 集群的连接数只与 proxy 服务器的数量有关,与客户端解耦.
3.2.4 通过改造 sharding-jdbc
理论上我们只要获取数据库实例上某个库的连接,我们就可以通过 "库名。表名" 的方式访问这台实例上其他库中的数据 (当然前提是用户要拥有要访问库的权限), 我们是否可以通过改造 sharding-jdbc 来实现这种访问方式?
以上几种方案,3.2.1 和 3.2.2 都需要新建数据库,同步历史和增量数据,还涉及线上切换数据源,3.2.3 需要部署一套 proxy 服务,并且为了高可用必定要以集群方式部署,这三种方案工作量和风险都较高,我们基于成本最小原则,最终选择改造 sharding-jdbc 的方案.
3.3 探究 sharding-jdbc
3.3.1 工作流程
sharding-jdbc 的工作流程可以分为以下步骤:

      
  • sql 解析 - 词法解析和语法解析;  
  • sql 路由 - 根据解析上下文匹配数据库和表的分片策略,并生成路由路径;  
  • sql 改写 - 将逻辑 SQL 改写为在真实数据库中可以正确执行的 SQL;  
  • sql 执行 - 使用多线程并发执行 sql;  
  • 结果归并 - 将从各个数据节点获取的多数据结果集,组合成为一个结果集并正确的返回至请求客户端;
显然数据库和表的分片是在 sql 路由阶段处理,所以我们以 sql 路由逻辑为入口分析下源码.
3.3.2 源码分析
ShardingStandardRoutingEngine 类中的 route 方法为计算路由的入口,返回的结果是数据库和表的分片集合:

route 方法中的核心逻辑在该类的 route0 方法中,其中 routeDataSources 方法负责 database 路由,routeTables 方法负责 table 路由,实际路由计算在 StandardShardingStrategy 的 doSharding 方法中,我们继续深入.

在 StandardShardingStrategy 类中有两个成员属性,preciseShardingAlgorithm (精准分片算法),rangeShardingAlgorithm (范围分片算法), 由于我们的 sql 都只指定分片键精准查询,使用的都是 preciseShardingAlgorithm 计算出的结果,PreciseShardingAlgorithm 是个接口,那我们就可以实现这个接口来自定义分片算法.

同时在 sharding-sphere 官网上也找到了相应的标签支持:

所以我们只需要自己实现 PreciseShardingAlgorithm 接口并配置在标签内即可实现自定义分片策略.
3.4 改造步骤
3.4.1 库分片改造
目前应用配置了 ds_0~ds_511 共 512 个数据源,我们只需配置 ds_0~ds_15 共 16 个数据源,每个数据源配置的是单个实例上的第一个库.
对于分片规则,我们可以依然使用 sharding:inline-strategy 标签,只需对 Groovy 表达式进行重写,分片键为 order_code, 之前分片算法为 (Math.abs (order_code.hashCode ()) % 512) 即用 order_code 列的哈希值对 512 取模得到 0~511, 我们只需要将结果再整除 32 即可得到 0~16, 即表达式改写为 (Math.abs (order_code.hashCode ()) % 512).intdiv (32).
改造前分库规则配置:

改造后分库规则配置:

3.4.2 表分片改造
实现 PreciseShardingAlgorithm 接口,重写表分片算法,使计算结果返回 "实际库名 + 表名" 的形式;
例如:查询 DB_31 库上 t_order 表的 user_id=35711 的数据,数据库分片算法返回的数据源为 "DB_0", 表分片算法返回 "DB_31.t_order";
自定义表分片算法:

在 xml 中定义 sharding:standard-strategy 标签,其属性 precise-algorithm-ref 配置为我们自定义的分表算法.

3.4.3 数据库连接池参数调整
改造前是一个库对应一个数据源连接池,改造后一个实例上的 32 个库共用一个数据源连接池,那么连接池的最大连接数,最小空闲连接数等参数需要相应的做调整。这个需要根据业务流量做合理的评估,当然最严谨的还是要以压测结果作为依据.
改造后客户端连接集群的形式如图:

优化前后数据库集群连接数对比:

四。小插曲
在改写库分片规则的 Groovy 表达式时,整除 32 直接在原有表达式上配置 "/32" 即 Math.abs (order_code.hashCode ()) % 512 / 32 , 在调试中发现执行 sql 会报 "no database route info" 错误信息,经过 debug 发现 sharding-jdbc 计算分片规则时会出现小数 (例如:ds_14.6857), 导致找不到数据源,这是因为 Groovy 没有提供专用的整数除法运算符,所以要用.intdiv () 方法,最终表达式改写为 (Math.abs (order_code.hashCode ()) % 512).intdiv (32).
五。总结
本文介绍了分库分表的概念及优势,以及 sharding-jdbc 分库分表中间件,探究了 sharding-jdbc 的路由规则的执行流程。当然在系统设计之初,对于数据库的分库分表,到底需不需要做?是多分库好还是多分表好?并没有一个放之四海而皆准的法则,需结合系统的特点 (例如 qps,tps, 单表数据量,磁盘规格,数据保留时间,业务增量,数据冷热方案等因素) 来决策权衡,有利有弊才需决策,有取有舍才需权衡.

  作者:京东物流 张仲良
  来源:京东云开发者社区 自猿其说 Tech


来源:
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回列表 返回顶部