分布式数据库中间件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主要线程如下:
- Timer:Timer单线程仅仅负责调度,任务的具体动作交给timerExecutor。
- TimerExecutor线程池:默认大小N=2,任务通过 timer 单线程和 timerExecutor 线程池共同完成
- NIOConnect 主动连接事件分离器:负责作为客户端连接 MySQL 的主动连接事件。
- Server被动连接事件分离器:负责作为服务端接收来自业务系统的连接事件
- Manager被动连接事件分离器:负责作为服务端接收来自管理系统的连接事件
- NIOReactor读写事件分离器:默认个数N=processor size,通道建立连接后处理NIO读写事件。由于写是采用通道空闲时其它线程直接写,只有写繁忙时才会注册写事件,再由NIOReactor分发。所以NIOReactor主要处理读操作。
- BusinessExecutor线程池:默认大小N=processor size,所有的NIOReactor把读出的数据交给BusinessExecutor做下一步的业务操作。
3、Mycat配置应用
3.1 基于Mycat的分库分表实现
Mycat通过定义表的分片规则来实现分片,分片规则中会定义分片字段和分片算法,分片算法包括hash、取模和范围分区等。每个表可以捆绑一个分片规则,每个分片规则指定一个分片字段并绑定一个函数,来实现动态分片算法 。
- Schema:逻辑库,与MySQL中的Database(数据库)对应,一个逻辑库中定义了所包括的Table。
- Table:逻辑表,即物理数据库中存储的某一张表,与传统数据库不同,这里的表需要声明其所存储的逻辑数据节点DataNode。在此可以指定表的分片规则。
- DataNode:MyCAT的逻辑数据节点,是存放table的具体物理节点,也称之为分片节点,通过DataSource来关联到后端某个具体数据库上
- Database:定义某个物理库的访问地址,用于捆绑到Datanode上
- 分片规则:前面讲了数据切分,一个大表被分成若干个分片表,就需要一定的规则,这样按照某种业务规则把数据分到某个分片的规则就是分片规则,数据切分选择合适的分片规则非常重要,将极大的避免后续数据处理的难度
Mycat中配置文件主要有schema.xml和rule.xml:schema.xml指定的是各个数据库节点与Mycat中虚拟数据库和表的关联关系,并且指定了当前表的分表策略;在rule.xml中则指定了具体的分表策略及其所使用的算法实现类。Mycat中常用分片算法包括取模、范围分片和一致性hash算法,以下分别介绍三种算法配置。
3.1.1 取模分片规则
取模分片规则是对分片字段进行求模运算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | <?xml version="1.0"?> <!DOCTYPE mycat:schema SYSTEM "schema.dtd"> <mycat:schema xmlns:mycat="http://io.mycat/"> <!-- 指定了对外所展示的数据库名称,也就是说,客户端连接MyCat数据库时,制定的database为mydb 而当前数据库中的表的配置就是根据下面的配置而来的 --> <schema name="mydb" checkSQLschema="true" sqlMaxLimit="100"> <!-- 定义了一个t_goods表,该表的主键是id,该字段是自增长的,并且该表的数据会被分配到dn1,dn2和 dn3上,这三个指的是当前MyCat数据库所代理的真实数据库的节点名,每个节点的具体配置在下面的 配置中。这里rule属性指定了t_goods表中的数据分配到dn1,dn2和dn3上的策略,mod-long指的是 按照长整型取余的方式分配,也就是按照id对节点数目进行取余 --> <table name="t_goods" primaryKey="id" autoIncrement="true" dataNode="dn1,dn2,dn3" rule="mod-long"/> </schema> <!-- 分别指定了dn1,dn2和dn3三个节点与对应的数据库的关系,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配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?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节点 指定了当前规则所对应的字段名,也就是id,algorithm节点则指定了当前规则所使用的算法,具体的 算法对应于下面的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 按范围分片规则
按照范围分片,顾名思义,就是首先对整体数据进行范围划分,然后将各个范围区间分配到对应的数据库节点上,当用户插入数据时,根据指定字段的值,判断其属于哪个范围,然后将数据插入到该范围对应的数据库节点上。
1 2 | <!-- schema.xml --> <table name="t_company" primaryKey="id" autoIncrement="true" dataNode="dn1,dn2,dn3" rule="range-sharding-by-id-count"/> |
rules.xml配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <!-- 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指定范围和数据节点的对应关系,如下:
1 2 3 4 5 6 7 8 | <!-- 上面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算法实现分片
1 2 | <!-- schema.xml --> <table name="t_house" primaryKey="id" autoIncrement="true" dataNode="dn1,dn2,dn3" rule="sharding-by-hash"/> |
rule.xml文件如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <!-- 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实现读写分离策略。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <?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并不依托于任何一个商业公司,因此不像某些开源项目,将一些重要的特性封闭在其商业产品中,使得开源项目成了一个摆设。