Mycat:开源分布式数据库中间件详解

Mycat是一款开源的分布式数据库中间件,支持分库分表、读写分离等功能,提升数据库性能。

原文标题:分布式数据库中间件Mycat介绍

原文作者:牧羊人的方向

冷月清谈:

Mycat是一个开源的分布式数据库中间件,功能类似数据库代理。它不存储数据,而是将用户请求路由到后端数据库,例如MySQL、Oracle等。Mycat的核心功能是分库分表,通过将大表拆分成小表分布在不同的服务器上,实现负载均衡。

Mycat支持垂直和水平两种切分模式。垂直切分是按不同表拆分到不同数据库,水平切分是按同一表内数据的逻辑关系拆分到多台数据库。这两种模式组合出四种分库分表方式:水平分库、水平分表、垂直分库、垂直分表。

Mycat的工作原理是拦截SQL语句,进行分片解析、路由分析、读写分离分析等,再将SQL发送到对应的后端数据库,最后将结果返回给客户端。

Mycat的应用场景包括读写分离、分库分表、多租户应用、报表系统以及范围查询分片数据等,但不适用于无分片条件的SQL、跨三个以上分片Join的SQL以及分片表深分页。

Mycat架构包含通信协议、路由解析、结果集处理、数据库连接、监控等模块。其线程模型包含Timer、TimerExecutor、NIOConnect、Server、Manager、NIOReactor、BusinessExecutor等线程。

Mycat通过schema.xml和rule.xml文件进行配置。schema.xml定义数据库节点与Mycat中虚拟数据库和表的关联关系以及分表策略。rule.xml定义具体分表策略和算法实现类。常用的分片算法有取模、范围分片和一致性Hash。

Mycat也支持读写分离,通过在schema.xml中配置writeHost和readHost实现。Mycat提供多种负载均衡策略和主从切换机制。

怜星夜思:

1、Mycat 和其他数据库中间件,比如 ShardingSphere、ProxySQL 相比,有什么优缺点?选择的时候应该考虑哪些因素?
2、文章中提到了 Mycat 的分片规则,实际应用中,如何选择合适的分片规则和算法?有哪些最佳实践可以分享?
3、Mycat 的读写分离是如何实现的?在配置读写分离时需要注意哪些问题?

原文内容

从Cobar到Mycat,从闭源到开源,作为一个开源的分布式数据库中间件,Mycat已经被众多开源项目使用。本文简要介绍下Mycat的特性、基本架构以及分库分表和读写分离的配置。

1、Mycat基本介绍

Mycat是一个开源的分布式数据库中间件,前端用户将其看作是一个数据库代理,使用mysql客户端和命令行工具进行访问,后端使用mysql协议或者JDBC协议与主流的数据库服务器如MySQL、Oracle、DB2、PostgreSQL等进行通信。Mycat本身并不存储数据,数据的存储是由后端的存储引擎实现的,因此数据的可靠性以及事务等都由后端数据库保证。

Mycat作为数据库中间件,其核心功能是分表分库,将一个大表根据切分策略分为不同的小表分布在不同的数据存储分片上,所有的数据分片构成完整的数据库存储。

1.1 分库分表原理
数据切分指的是将数据根据某种规则分散存放在多个数据库服务器上,以达到负载分散的效果。数据的切分(sharding)根据切分规则的类型,可以分为垂直切分和水平切分两种模式:
  • 垂直切分是按照不同的表切分到不同的数据库中,适用于业务系统之间耦合度低、业务逻辑清晰的系统

  • 水平切分是根据表中数据的逻辑关系,将同一个表中的数据按照某种条件拆分到多台数据库上,对应用来说更为复杂

分库分表根据垂直和水平切分分为四种:水平分库、水平分表、垂直分库、垂直分表

1.2 Mycat实现原理
Mycat主要原理是通过对SQL的拦截,然后经过一定规则的分片解析、路由分析、读写分离分析、缓存分析等,然后将SQL发给后端真实的数据块,并将返回的结果做适当处理返回给客户端。如图,表t1分为3个分片,这三个分片分布在两台mysql数据库服务器上,这里的分片字段为city分片规则为字符串枚举方式。

当Mycat收到一个SQL时,会先解析这个SQL,查找涉及到的表,然后看此表的定义,如果有分片规则,则获取到SQL里分片字段的值,并匹配分片函数,得到该SQL对应的分片列表,然后将SQL发往这些分片去执行,最后收集和处理所有分片返回的结果数据,并输出到客户端。

1.3 Mycat应用场景
Mycat从2013年发展到现在Mycat 1.6版本,再到Mycat2,有以下典型的使用场景:
  • 读写分离配置,支持Mycat读写分离、主从切换;

  • 分表分库,对于超过1000万的表进行分片,最大支持1000亿的单表分片;

  • 多租户应用,每个应用一个库,但应用程序只连接Mycat,从而不改造程序本身,实现多租户化

  • 报表系统,借助于Mycat的分表能力,实现多数据源的分布式查询,处理大规模报表的统计

  • 范围查询分片数据,业务访问呈现点查/范围规律而且可以根据分片键分散访问压力

Mycat不适合无分片条件的SQL、跨分片join超过3个会带来性能问题、分片表分片表不建议使用深分页即limit 10000,1。

2、Mycat架构介绍
2.1 MySQL总体架构

Mycat整体架构上由以下模块组成:通信协议、路由解析、结果集处理、数据库连接、监控等模块

1)通信协议模块

通信协议模块承担底层的收发数据、线程回调处理工作,主要采用Reactor、proactor模式来提高效率。目前Mycat通信模块默认釆用Reactor模式,在协议层采用MySQL协议。

2)路由解析模块

路由解析模块负责对传入的SQL语句进行语法解析,解析从MySQL协议中解析出来并进入该模块的SQL语句的条件、语句类型、携带的关键字等,对符合要求的SQL语句进行相关优化,最后根据这些路由计算单元进行路由计算。

3)结果集处理模块

结果集处理模块负责对跨分片的结果进行汇聚、排序、截取等。由于数据存储在不同的数据库中,所以对跨分片的数据需要进行汇聚。

4)数据库连接模块

数据库连接模块负责创建、管理、维护后端的连接池。为了诚少每次建立数据库连接的开销,数据库使用连接池机制对连接生命周期进行管理

5)监控管理模块

监控管理模块负责对Mycat中的连接、内存等资源进行监控和管理。监控主要是通过管理命令实时地展现一些监控数据,例如连接数、缓存命中数等;管理则主要通过轮询事件来检测和释放不使用的资源。

6)SQL执行模块

SQL执行模块负责从连接池中获取相应的目标连接,对目标连接进行信息同步后,再根据路由解析的结果,把SQL语句分发到相应的节点执行。

2.2 Mycat线程模型

Mycat主要线程如下:
  1. Timer:Timer单线程仅仅负责调度,任务的具体动作交给timerExecutor。

  2. TimerExecutor线程池:默认大小N=2,任务通过 timer 单线程和 timerExecutor 线程池共同完成

  3. NIOConnect 主动连接事件分离器:负责作为客户端连接 MySQL 的主动连接事件。

  4. Server被动连接事件分离器:负责作为服务端接收来自业务系统的连接事件

  5. Manager被动连接事件分离器:负责作为服务端接收来自管理系统的连接事件

  6. NIOReactor读写事件分离器:默认个数N=processor size,通道建立连接后处理NIO读写事件。由于写是采用通道空闲时其它线程直接写,只有写繁忙时才会注册写事件,再由NIOReactor分发。所以NIOReactor主要处理读操作。

  7. BusinessExecutor线程池:默认大小N=processor size,所有的NIOReactor把读出的数据交给BusinessExecutor做下一步的业务操作。

3、Mycat配置应用
3.1 基于Mycat的分库分表实现

Mycat通过定义表的分片规则来实现分片,分片规则中会定义分片字段和分片算法,分片算法包括hash、取模和范围分区等。每个表可以捆绑一个分片规则,每个分片规则指定一个分片字段并绑定一个函数,来实现动态分片算法 。

  1. Schema:逻辑库,与MySQL中的Database(数据库)对应,一个逻辑库中定义了所包括的Table。

  2. Table:逻辑表,即物理数据库中存储的某一张表,与传统数据库不同,这里的表需要声明其所存储的逻辑数据节点DataNode。在此可以指定表的分片规则。

  3. DataNode:MyCAT的逻辑数据节点,是存放table的具体物理节点,也称之为分片节点,通过DataSource来关联到后端某个具体数据库上

  4. Database:定义某个物理库的访问地址,用于捆绑到Datanode上

  5. 分片规则:前面讲了数据切分,一个大表被分成若干个分片表,就需要一定的规则,这样按照某种业务规则把数据分到某个分片的规则就是分片规则,数据切分选择合适的分片规则非常重要,将极大的避免后续数据处理的难度

Mycat中配置文件主要有schema.xml和rule.xml:schema.xml指定的是各个数据库节点与Mycat中虚拟数据库和表的关联关系,并且指定了当前表的分表策略;在rule.xml中则指定了具体的分表策略及其所使用的算法实现类。Mycat中常用分片算法包括取模、范围分片和一致性hash算法,以下分别介绍三种算法配置。

3.1.1 取模分片规则

取模分片规则是对分片字段进行求模运算。

<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<!-- 指定了对外所展示的数据库名称,也就是说,客户端连接MyCat数据库时,制定的databasemydb
而当前数据库中的表的配置就是根据下面的配置而来的 -->

<schema name="mydb" checkSQLschema="true" sqlMaxLimit="100">
<!-- 定义了一个t_goods表,该表的主键是id,该字段是自增长的,并且该表的数据会被分配到dn1,dn2
dn3上,这三个指的是当前MyCat数据库所代理的真实数据库的节点名,每个节点的具体配置在下面的
配置中。这里rule属性指定了t_goods表中的数据分配到dn1,dn2dn3上的策略,mod-long指的是
按照长整型取余的方式分配,也就是按照id对节点数目进行取余 -->

<table name="t_goods" primaryKey="id" autoIncrement="true" dataNode="dn1,dn2,dn3"
rule="mod-long"/>

</schema>

<!-- 分别指定了dn1,dn2dn3三个节点与对应的数据库的关系,dataHost对应的就是下面的数据库节点配置 -->
<dataNode name="dn1" dataHost="dhost1" database="db1"/>
<dataNode name="dn2" dataHost="dhost2" database="db2"/>
<dataNode name="dn3" dataHost="dhost3" database="db3"/>

<!-- 这里分别指定了各个数据库节点的配置 -->
<dataHost name="dhost1" maxCon="1000" minCon="10" balance="0" writeType="0"
dbType="mysql" dbDriver="native">

<heartbeat>select user()</heartbeat>
<writeHost host="hostM1" url="localhost:3306" user="root" password="password"/>
</dataHost>
a
<dataHost name="dhost2" maxCon="1000" minCon="10" balance="0" writeType="0"
dbType="mysql" dbDriver="native">

<heartbeat>select user()</heartbeat>
<writeHost host="hostM2" url="localhost:3306" user="root" password="password"/>
</dataHost>

<dataHost name="dhost3" maxCon="1000" minCon="10" balance="0" writeType="0"
dbType="mysql" dbDriver="native">

<heartbeat>select user()</heartbeat>
<writeHost host="hostM3" url="localhost:3306" user="root" password="password"/>
</dataHost>
</mycat:schema>

rules.xml配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mycat:rule SYSTEM "rule.dtd">
<mycat:rule xmlns:mycat="http://io.mycat/">
<!-- 这里的mod-long对应的就是上面schema.xml的表配置中rule属性所使用的规则名称,其columns节点
指定了当前规则所对应的字段名,也就是idalgorithm节点则指定了当前规则所使用的算法,具体的
算法对应于下面的function节点所指定的实现类-->

<tableRule name="mod-long">
<rule>
<columns>id</columns>
<algorithm>mod-long</algorithm>
</rule>
</tableRule>

<!-- 这里指定的是mod-long这个算法所使用的具体实现类,实现类需要使用全限定路径,具体的代码读者朋友
可以阅读MyCat源码,并且读者也可以查看MyCat默认为我们提供了哪些分表策略实现 -->

<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
<!-- 指定了当前所使用的数据库节点数 -->
<property name="count">3</property>
</function>
</mycat:rule>
3.1.2 按范围分片规则

按照范围分片,顾名思义,就是首先对整体数据进行范围划分,然后将各个范围区间分配到对应的数据库节点上,当用户插入数据时,根据指定字段的值,判断其属于哪个范围,然后将数据插入到该范围对应的数据库节点上。

<!-- schema.xml -->
<table name="t_company" primaryKey="id" autoIncrement="true" dataNode="dn1,dn2,dn3" rule="range-sharding-by-id-count"/>

rules.xml配置如下:

<!-- rule.xml -->
<tableRule name="range-sharding-by-id-count">
<rule>
<!-- 指定了分片字段 -->
<columns>id</columns>
<algorithm>range-id-count</algorithm>
</rule>
</tableRule>

<function name="range-id-count" class="io.mycat.route.function.AutoPartitionByLong">
<!-- 指定了范围分片的”范围-节点“的对应策略 -->
<property name="mapFile">files/tb-range-partition.txt</property>
<!-- 指定了超出范围的数据将会被分配的数据节点 -->
<property name="defaultNode">0</property>
</function>

其中mapFile指定范围和数据节点的对应关系,如下:

<!-- 上面mapFile属性指定的id-range-partition.txt文件内容,这里指定了具体的范围与数据节点的对应关系 
-->
0-10=0
11-50=1
51-100=2
101-1000=0
1001-9999=1
10000-9999999=2
3.1.3 一致性Hash分片规则

一致性Hash是基于一致性Hash算法实现分片

<!-- schema.xml -->
<table name="t_house" primaryKey="id" autoIncrement="true" dataNode="dn1,dn2,dn3" rule="sharding-by-hash"/>

rule.xml文件如下:

<!-- rule.xml -->
<tableRule name="sharding-by-murmur">
<rule>
<columns>id</columns>
<algorithm>hash</algorithm>
</rule>
</tableRule>

<!-- 下面的属性中,count指定了要分片的数据库节点数量,必须指定,否则没法分片;virtualBucketTimes指的是
一个实际的数据库节点被映射为这么多虚拟节点,默认是160倍,也就是虚拟节点数是物理节点数的160倍;
weightMapFile指定了节点的权重,没有指定权重的节点默认是1。以properties文件的格式填写,
以从0开始到count-1的整数值也就是节点索引为key,以节点权重值为值。所有权重值必须是正整数,
否则以1代替;bucketMapPath用于测试时观察各物理节点与虚拟节点的分布情况,如果指定了这个属性,
会把虚拟节点的murmur hash值与物理节点的映射按行输出到这个文件,没有默认值,如果不指定,
就不会输出任何东西-->

<function name="hash" class="io.mycat.route.function.PartitionByMurmurHash">
<property name="seed">0</property><!-- 默认是0 -->
<property name="count">3</property>
<property name="virtualBucketTimes">160</property><!-- -->
<!-- <property name="weightMapFile">weightMapFile</property> -->
<property name="bucketMapPath">
/usr/local/mycat/bucketMap.txt</property>
</function>
3.2 基于Mycat的读写分离实现
3.2.1 Mycat读写分离方案

在MySQL集群主从复制架构中,通过配置mysql可以实现读写分离。如图所示,MySQL节点开启主从复制的配置方案,并将主节点配置为Mycat的dataHost里的writeNode,从节点配置为readNode,同时Mycat内部定期对一个dataHost里的所有writeHost与readHost节点发起心跳检测.正常情况下,Mycat会将第一个writeHost作为写节点,所有的DML SQL会发送给此节点,因为Mycat开启了读写分离,则查询节点会根据读写分离的策略发往 readHost(+writeHost)执行。

3.2.2 读写分离配置

在schema.xml文件中配置读写分离,通过配置writeHost和readHost实现读写分离策略。

<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">

<schema name="test01" checkSQLschema="false" sqlMaxLimit="100" dataNode="node01">
</schema>
<dataNode name="node01" dataHost="192.168.112.10" database="test01" />
<dataHost name="192.168.112.10" maxCon="1000" minCon="10" balance="1"
writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">

<heartbeat>show slave status</heartbeat>
<!-- can have multi write hosts -->
<writeHost host="192.168.112.101" url="192.168.112.101:3306" user="root"
password="password">

<!-- can have multi read hosts -->
<readHost host="192.168.112.102" url="192.168.112.102:3306" user="root" password="password" />
<readHost host="192.168.112.103" url="192.168.112.103:3306" user="root" password="password" />
</writeHost>
</dataHost>

</mycat:schema>
  • schema:数据库设置,此数据库为逻辑数据库,name与server.xml中schema对应

  • dataNode:分片信息,也就是分库相关配置

  • dataHost:物理数据库,真正存储数据的数据库

  • balance的取值决定了负载均衡对非事务内的读操作的处理,balance属性负载均衡类型,目前的取值有4种:

    • balance="0", 不开启读写分离机制,所有读操作都发送到当前可用的writeHost上

    • balance="1",全部的readHost与 stand by writeHost参与select语句的负载均衡,简单的说,当双主双从模式(M1 ->S1 , M2->S2,并且M1与M2互为主备),正常情况下, M2,S1,S2都参与select语句的负载均衡

    • balance="2",所有读操作都随机的在writeHost、readhost上分发

    • balance="3",所有读请求随机的分发到wiriterHost对应的readhost执行,writerHost 不负担读压力,注意balance=3只在1.4及其以后版本有,1.3没有

  • writeType属性,负载均衡类型,目前的取值有 3 种:

    • writeType="0"所有写操作发送到配置的第一个writeHost,第一个挂了切到还生存的第二个writeHost,重新启动后已切换后的为准,切换记录在配置文件中:dnindex.properties

    • writeType="1"所有写操作都随机的发送到配置的writeHost

    • writeType="2"没实现

  • 主从切换(双主failover):switchType属性

    • switchType="-1":不自动切换

    • switchType="1":默认值,自动切换

    • switchType="2":基于MySQL主从同步的状态来决定是否切换。需修改heartbeat语句(即心跳语句):show slave status

    • switchType="3":基于Mysql Galera Cluster(集群多节点复制)的切换机制。需修改heartbeat语句(即心跳语句):show status like 'wsrep%'

4、总结

Mycat的诞生源自于阿里Cobar数据库中间件,这是一个完全由开源社区自主研发的分布式数据库中间件,自诞生以来深受各类开源软件项目喜爱,一直保持着社区活跃度。Mycat 1.6版本更新停滞有一段时间,现在又出现mycat2,据说是代码层完全重构了,但依旧秉持着开源的理念在贡献着,即使是在目前如火如荼发展的分布式数据库产品中,各类商业化的分布式数据库中间件层出不穷。就像mycat官网说的:

MYCAT并不依托于任何一个商业公司,因此不像某些开源项目,将一些重要的特性封闭在其商业产品中,使得开源项目成了一个摆设。

参考资料:

  1. Mycat权威指南

  2. Mycat官网,http://www.mycat.org.cn/

  3. https://www.yuque.com/ccazhw/ml3nkf?

  4. https://blog.csdn.net/liuerchong/article/details/107887804

分享一个我遇到的坑,之前一个项目用了取模分片,后来数据量增长很快,导致数据分布不均匀,不得已又进行了一次数据迁移,非常痛苦。所以选择分片规则时一定要考虑未来的扩展性。

选择分片规则和算法,首先要考虑业务场景和数据特点。比如,如果你的数据访问主要 berdasarkan 某个字段,那么就可以选择按照这个字段进行范围分片或者 Hash 分片。如果数据分布比较均匀,那么可以使用取模分片。另外,还要考虑数据的增长趋势,选择合适的算法可以避免后期数据迁移的麻烦。最佳实践方面,建议先进行充分的测试,评估不同分片规则和算法对性能的影响,然后再选择最合适的方案。

补充一下,配置读写分离时,还要注意监控主从节点的状态,如果从节点挂了,Mycat 需要能够及时切换到其他从节点或者主节点,避免影响业务。

对,楼上说的对,扩展性很重要。我补充一下,选择分片算法时,还要考虑数据访问的模式,比如如果是大量的点查,那么 Hash 分片比较合适;如果是范围查询,那么范围分片比较合适。另外,一致性 Hash 也可以考虑,它可以减少数据迁移的成本。

Mycat 的读写分离是通过配置 writeHost 和 readHost 来实现的。writeHost 指定写节点,readHost 指定读节点。Mycat 会将写操作发送到 writeHost,读操作发送到 readHost。配置的时候需要注意,要根据业务需求选择合适的负载均衡策略,比如 balance 属性。另外,还要注意主从同步的延迟,避免读到过时的数据。

个人感觉 Mycat 比较适合中小规模的项目,或者作为学习分布式数据库中间件的入门工具。ShardingSphere 功能更强大,适合大型复杂的项目。 ProxySQL 性能是强项,但对运维能力要求比较高。

是的,监控很重要。另外,如果你的业务对数据一致性要求比较高,那么建议不要使用读写分离,或者使用强一致性的读写分离方案,比如使用 Galera Cluster。

补充一点,Mycat 目前发展感觉有点放缓了,社区活跃度虽然还在,但更新频率不如以前。ShardingSphere 社区很活跃,更新迭代很快,而且文档也比较完善。ProxySQL 性能确实强,但上手难度相对较高,如果团队没有这方面的经验,维护起来可能会比较吃力。

关于 Mycat 和其他数据库中间件的比较,这个问题我之前也研究过一阵。Mycat 的优点在于配置比较简单,学习成本相对较低,而且社区很活跃,遇到问题比较容易找到解决方案。但是它的功能相对来说没有 ShardingSphere 那么全面,比如在数据加密、分片算法等方面,ShardingSphere 提供了更多的选择。至于 ProxySQL,它的性能很强,但是配置比较复杂,需要比较专业的 DBA 才能玩转。选择的时候,我觉得要根据项目的具体需求来考虑,如果项目对性能要求非常高,而且有专业的 DBA,那么 ProxySQL 是个不错的选择;如果项目需要一个功能比较全面的中间件,而且对学习成本比较敏感,那么 ShardingSphere 比较合适;如果项目规模不大,而且对配置的简易性要求比较高,那么 Mycat 是个不错的选择。