基于官方社区文档(为避免遗漏,建议您查阅原文)
写在前面
原文链接首页https://www.yuque.com/books/share/0576de75-ffc4-4c34-8586-952ae4636944
link
OLTP和OLAP
联机事务处理(OLTP)
也称为面向交易的处理系统,其基本特征是原始数据可以立即传送到计算中心进行处理,并在很短的时间内给出处理结果。
联机分析处理(OLAP)
是指通过多维的方式对数据进行分析、查询和报表,可以同数据挖掘工具、统计分析工具配合使用,增强决策分析功能。
对于两者的主要区别可以用下表来说明:
关系型数据库和NoSQL数据库
关系型数据库
,是建立在关系模型基础上的数据库,其借助于集合代数等数学概念和方法来处理数据库中的数据。主流的oracle、DB2、MS SQL Server和mysql都属于这类传统数据库。
NoSQL数据库
,全称为Not Only SQL,意思就是适用关系型数据库的时候就使用关系型数据库,不适用的时候也没有必要非使用关系型数据库不可,可以考虑使用更加合适的数据存储。
主要分为临时性键值存储(memcached、Redis)、永久性键值存储(ROMA、Redis)、面向文档的数据库(MongoDB、CouchDB)、面向列的数据库(Cassandra、HBase),每种NoSQL都有其特有的使用场景及优点。
NoSQL数据库又无法将其替代,NoSQL只能作为传统数据的补充而不能将其替代,所以规避传统数据库的缺点是目前大数据时代必须要解决的问题。如果传统数据易于扩展,可切分,就可以避免单机(单库)的性能缺陷,但是由于目前开源或者商用的传统数据库基本不支持大规模自动扩展,所以就需要借助第三方来做处理,那就是本书要讲的数据切分,
下面就来分析一下如何进行数据切分:
简单来说,就是指通过某种特定的条件,将我们存放在同一个数据库中的数据分散存放到多个数据库(主机)上面,以达到分散单台设备负载的效果
。
数据的切分(Sharding)
根据其切分规则的类型,可以分为两种切分模式。
一种是按照不同的表(或者Schema)来切分到不同的数据库(主机)之上,这种切可以称之为数据的垂直(纵向)切分;
另外一种则是根据表中的数据的逻辑关系,将同一个表中的数据按照某种条件拆分到多台数据库(主机)上面,这种切分称之为数据的水平(横向)切分。
(个人理解,更简单的理解就是垂直切分就是分库,水平就是分表。也可以结合使用)
垂直切分
的最大特点就是规则简单,实施也更为方便,尤其适合各业务之间的耦合度非常低,相互影响很小,业务逻辑非常清晰的系统
。在这种系统中,可以很容易做到将不同业务模块所使用的表分拆到不同的数据库中。根据不同的表来进行拆分,对应用程序的影响也更小,拆分规则也会比较简单清晰。
水平切分
于垂直切分相比,相对来说稍微复杂一些。因为要将同一个表中的不同数据拆分到不同的数据库中
,对于应用程序来说,拆分规则本身就较根据表名来拆分更为复杂,后期的数据维护也会更为复杂一些。
一个数据库由很多表的构成,每个表对应着不同的业务,垂直切分是指按照业务将表进行分类,分布到不同的数据库上面,这样也就将数据或者说压力分担到不同的库上面,如下图:
系统被切分成了,用户,订单交易,支付几个模块。
一个架构设计较好的应用系统,其总体功能肯定是由很多个功能模块所组成的,
而每一个功能模块所需要的数据对应到数据库中就是一个或者多个表。
而在架构设计中,各个功能模块相互之间的交互点越统一越少,系统的耦合度就越低,
系统各个模块的维护性以及扩展性也就越好。这样的系统,实现数据的垂直切分也就越容易。
但是往往系统之有些表难以做到完全的独立,存在这扩库join的情况,对于这类的表,就需要去做平衡,
是数据库让步业务,共用一个数据源,还是分成多个库,业务之间通过接口来做调用。
在系统初期,数据量比较少,或者资源有限的情况下,会选择共用数据源,但是当数据发展到了一定的规模,负载很大的情况,就需要必须去做分割。
一般来讲业务存在着复杂join的场景是难以切分的,往往业务独立的易于切分。
如何切分,切分到何种程度是考验技术架构的一个难题。
下面来分析下垂直切分的优缺点:
优点:
· 拆分后业务清晰,拆分规则明确;
· 系统之间整合或扩展容易;
· 数据维护简单。
缺点:
· 部分业务表无法join,只能通过接口方式解决,提高了系统复杂度;
· 受每种业务不同的限制存在单库性能瓶颈,不易数据扩展跟性能提高;
· 事务处理复杂。
由于垂直切分是按照业务的分类将表分散到不同的库,所以有些业务表会过于庞大,存在单库读写与存储瓶颈,所以就需要水平拆分来做解决。
相对于垂直拆分,水平拆分不是将表做分类,而是按照某个字段的某种规则来分散到多个库之中,每个表中包含一部分数据。
简单来说,我们可以将数据的水平切分理解为是按照数据行的切分,就是将表中的某些行切分到一个数据库,而另外的某些行又切分到其他的数据库中。
拆分数据就需要定义分片规则。
关系型数据库是行列的二维模型,拆分的第一原则是找到拆分维度
。
比如:从会员的角度来分析,商户订单交易类系统中查询会员某天某月某个订单,那么就需要按照会员结合日期来拆分,不同的数据按照会员ID做分组,这样所有的数据查询join都会在单库内解决;如果从商户的角度来讲,要查询某个商家某天所有的订单数,就需要按照商户ID做拆分;但是如果系统既想按会员拆分,又想按商家数据,则会有一定的困难。如何找到合适的分片规则需要综合考虑衡量。
几种典型的分片规则包括:
· 按照用户ID求模,将数据分散到不同的数据库,具有相同数据用户的数据都被分散到一个库中;
· 按照日期,将不同月甚至日的数据分散到不同的库中;
· 按照某个特定的字段求摸,或者根据特定范围段分散到不同的库中。
如图,切分原则都是根据业务找到适合的切分规则分散到不同的库,下面用用户ID求模举例:
既然数据做了拆分有优点也就优缺点。
优点:
· 拆分规则抽象好,join操作基本可以数据库做;
· 不存在单库大数据,高并发的性能瓶颈;
· 应用端改造较少;
· 提高了系统的稳定性跟负载能力。
缺点:
· 拆分规则难以抽象;
· 分片事务一致性难以解决;
· 数据多次扩展难度跟维护量极大;
· 跨库join性能较差。
前面讲了垂直切分跟水平切分的不同跟优缺点,会发现每种切分方式都有缺点,但共同的特点缺点有:
· 引入分布式事务的问题;
· 跨节点Join的问题;
· 跨节点合并排序分页问题;
· 多数据源管理问题。
针对数据源管理,目前主要有两种思路:
A. 客户端模式,在每个应用程序模块中配置管理自己需要的一个(或者多个)数据源,直接访问各个数据库,在模块内完成数据的整合;
B. 通过中间代理层来统一管理所有的数据源,后端数据库集群对前端应用程序透明;
可能90%以上的人在面对上面这两种解决思路的时候都会倾向于选择第二种,尤其是系统不断变得庞大复杂的时候。
确实,这是一个非常正确的选择,虽然短期内需要付出的成本可能会相对更大一些,但是对整个系统的扩展性来说,
是非常有帮助的。
Mycat 通过数据切分解决传统数据库的缺陷,又有了NoSQL易于扩展的优点。
通过中间代理层规避了多数据源的处理问题,对应用完全透明,同时对数据切分后存在的问题,也做了解决方案。
下面章节就分析,mycat的由来及如何进行数据切分问题。
由于数据切分后数据Join的难度在此也分享一下数据切分的经验:
第一原则:能不切分尽量不要切分。
第二原则:如果要切分一定要选择合适的切分规则,提前规划好。
第三原则:数据切分尽量通过数据冗余或表分组(Table Group)来降低跨库Join的可能。
第四原则:由于数据库中间件对数据Join实现的优劣难以把握,而且实现高性能难度极大,业务读取尽量少使用多表Join。
什么是mycat,maycat从哪里来,又是如何解决这些问题的,下一章让我们来作分析。
Mycat背后是阿里曾经开源的知名产品——Cobar。Cobar的核心功能和优势是MySQL数据库分片
文章中有寓意的话:
爱情就像是见鬼。只有撞见了,你才会明白爱情是怎么回事。TA是如此神秘,欲语还羞。
情窦初开的你又玩命将TA的优点放大,使自己成为一只迷途的羔羊。
Cobar是阿里巴巴研发的关系型数据的分布式处理系统,该产品成功替代了原先基于Oracle的数据存储方案,
目前已经接管了3000+个MySQL数据库的schema,平均每天处理近50亿次的SQL执行请求。
Cobar最重要的特性是分库分表。
Cobar可以让你把一个MySQL的Table放到10个甚至100个位于不同物理机上的MySQL服务器上去存储,而在用户看来是一张表(逻辑表)。这样功能很有价值。比如:我们有1亿的订单,则可以划分为10个分片,存储到2-10个物理机上。每个MySQL服务器的压力减少,而系统的响应时间则不会增加。看上去很完美的功能,而且潜意识里,执行这句SQL:
select count(*) from order
100%的人都会认为:会返回1条数据,但事实上,Cobar会返回N条数据,N=分片个数。
接下来我们继续执行SQL:
select count(*) from order order by order_date
你会发现奇怪的乱序现象,而且结果还随机,这是因为,
Cobar只是简单的把上述SQL发给了后端N个分片对应的MySQL服务器去执行,然后把结果集直接输出….
再继续看看,我们常用的Limit分页的结果…可以么?
"答案是:不可以。"
这个问题可以在客户端程序里做些工作来解决。所以随后出现了Cobar Client。
据我所知,很多Cobar的使用者也都是自行开发了类似Cobar Client的工具来解决此类问题。
从实际应用效果来说,一方面,客户端编程方式解决,困难度很高,Bug率也居高不下;
另一方面,对于DBA和运维来说,增加了困难度。
当你发现这个问题的严重性,再回头看看Cobar的官方文档,你怅然若失,四顾茫然。
接下来,本文将隐藏在Cobar代码中那些不为人知的秘密逐一披漏,
你洞悉了这些秘密,就会明白Mycat为什么会横空出世。
第一个秘密:Cobra会假死?
是的,很多人遇到这个问题。如何来验证这点呢?可以做个简单的小实验,
假如你的分片表中配置有表company,则打开mysql终端,执行下面的SQL:
select sleep(500) from company;
此SQL会执行等待500秒,你再努力以最快的速度打开N个mysql终端,
都执行相同的SQL,确保N>当前Cobra的执行线程数:
show @@threadpool
的所有Processor1-E的线程池的线程数量总和,然后你再执行任何简单的SQL,
或者试图新建立连接,都会无法响应,此时
show @@threadpool
里面看到TASK_QUEUE_SIZE已经在积压中。
不可能吧,据说Cobra是NIO的非阻塞的,怎么可能阻塞!别激动,去看看代码,
Cobra前端是NIO的,而后端跟Mysql的交互,是阻塞模式,
其NIO代码只给出了框架,还未来得及实现。真相永远在代码里。
第二个秘密:高可用的陷阱?
Cobra如何实现高可用性:
分片节点dn2_M1配置了两个dataSource,并且配置了心跳检测(heartbeat)语句,
在这种配置下,每个dataNode会定期对当前正在使用的dataSource执行心跳检测,
默认是第一个,频率是10秒钟一次,当心跳检测失败以后,会自动切换到第二个dataSource上进行读写,
假如Cobra发生了假死,则在假死的1分钟内,Cobra会自动切换到第二个节点上,因为假死的缘故,
第二个节点的心跳检测也超时。于是,1分钟内Cobra频繁来回切换,
懂得MySQL主从复制机制的人都知道,在两个节点上都执行写操作意味着什么?
——可能数据一致性被破坏,谁也不知道那个机器上的数据是最新的。
还有什么情况下,会导致心跳检测失败呢?这是一个不得不说的秘密:当后端数据库达到最大连接后,
会对新建连接全部拒绝,此时,Cobar的心跳检测所建立的新连接也会被拒绝,
于是,心跳检测失败,于是,一切都悄悄的发生了。
幸好,大多数同学都没有配置高可用性,或者还不了解此特性,因此,这个秘密,一直在安全的沉睡。
第三个秘密:看上去很美的自动切换
Cobar很诱人的一个特性是高可用性,高可用性的原理是数据节点DataNode配置引用两个DataSource,
并做心跳检测,当第一个DataSource心跳检测失败后,Cobar自动切换到第二个节点,
当第二个节点失败以后,又自动切换回第一个节点,一切看起来很美,无人值守,几乎没有宕机时间。
在真实的生产环境中,我们通常会用至少两个Cobar实例组成负载均衡,
前端用硬件或者HAProxy这样的负载均衡组件,防止单点故障,这样一来,即使某个Cobar实例死了,
还有另外一台接手,某个Mysql节点死了,切换到备节点继续,
至此,一切看起来依然很美,喝着咖啡,听着音乐,领导视察,你微笑着点头——No problem,Everything is OK!
直到有一天,某个Cobar实例果然如你所愿的死了,不管是假死还是真死,你按照早已做好的应急方案,
优雅的做了一个不是很艰难的决定——重启那个故障节点,
然后继续喝着咖啡,听着音乐,轻松写好故障处理报告发给领导,然后又度过了美好的一天。
你忽然被深夜一个电话给惊醒,你来不及发火,因为你的直觉告诉你,这个问题很严重,
大量的订单数据发生错误很可能是昨天重启cobar导致的数据库发生奇怪的问题。
你努力排查了几个小时,终于发现,主备两个库都在同时写数据,主备同步失败,
你根本不知道那个库是最新数据,紧急情况下,你做了一个很英明的决定,停止昨天故障的那个cobar实例,
然后你花了3个通宵,解决了数据问题。
这个陷阱的代价太高,不知道有多少同学中枪过,反正我也是躺着中枪过了。
若你还不清楚为何会产生这个陷阱,现在我来告诉你:
Cobar启动的时候,会用默认第一个Datasource进行数据读写操作;
当第一个Datasource心跳检测失败,会切换到第二个Datasource;
若有两个以上的Cobar实例做集群,当发生节点切换以后,你若重启其中任何一台Cobar,就完美调入陷阱;
那么,怎么避免这个陷阱?目前只有一个办法,节点切换以后,尽快找个合适的时间,
全部集群都同时重启,避免隐患。
为何是重启而不是用节点切换的命令去切换?想象一下32个分片的数据库,要多少次切换?
MyCAT怎么解决这个问题的?很简单,节点切换以后,记录一个properties文件( conf目录下),重启的时候,读取里面的节点index,真正实现了无故障无隐患的高可用性。
第四个秘密:只实现了一半的NIO
NIO技术用作JAVA服务器编程的技术标准,已经是不容置疑的业界常规做法,若一个Java程序员,
没听说过NIO,都不好意思说自己是Java人。所以Cobar采用NIO技术并不意外,但意外的是,只用了一半。
Cobar本质上是一个“数据库路由器”,客户端连接到Cobar,发生SQL语句,
Cobar再将SQL语句通过后端与MySQL的通讯接口Socket发出去,然后将结果返回给客户端的Socket中。
下面给出了SQL执行过程简要逻辑:
SQL->FrontConnection->Cobar->MySQLChanel->MySQL
FrontConnection 实现了NIO通讯,但MySQLChanel则是同步的IO通讯,原因很简单,指令比较复杂,
NIO实现有难度,容易有BUG。后来最新版本Cobar尝试了将后端也NIO化,
大概实现了80%的样子,但没有完成,也存在缺陷。
由于前端NIO,后端BIO,于是另一个有趣的设计产生了——两个线程池,
前端NIO部分一个线程池,后端BIO部分一个线程池。
各自相互不干扰,但这个设计的结果,导致了线程的浪费,也对性能调优带来很大的困难。
由于后端是BIO,所以,也是Cobar吞吐量无法太高、另外也是其假死的根源。
MyCAT在Cobar的基础上,完成了彻底的NIO通讯,并且合并了两个线程池,这是很大一个提升。从1.1版本开始,MyCAT则彻底用了JDK7的AIO,有一个重要提升。
第五个秘密:阻塞、又见阻塞
Cobar本质上类似一个交换机,将后端Mysql 的返回结果数据经过加工后再写入前端连接并返回,
于是前后端连接都存在一个“写队列”用作缓冲,
后端返回的数据发到前端连接FrontConnection的写队列中排队等待被发送,
而通常情况下,后端写入的的速度要大于前端消费的速度,在跨分片查询的情况下,
这个现象更为明显,于是写线程就在这里又一次被阻塞。
解决办法有两个,增大每个前端连接的“写队列”长度,减少阻塞出现的情况,
但此办法只是将问题抛给了使用者,要是使用者能够知道这个写队列的默认值小了,
然后根据情况进行手动尝试调整也行,但Cobar的代码中并没有把这个问题暴露出来,
比如写一个告警日志,队列满了,建议增大队列数。于是绝大多数情况下,大家就默默的排队阻塞,无人知晓。
MyCAT解决此问题的方式则更加人性化,首先将原先数组模式的固定长度的队列改为链表模式,无限制,并且并发性更好,此外,为了让用户知道是否队列过长了(一般是因为SQL结果集返回太多,比如1万条记录),当超过指定阀值(可配)后,会产生一个告警日志。
<system><property name="frontWriteQueueSize">1024</property></system>
第六个秘密:又爱又恨的SQL 批处理模式
正如一枚硬币的正反面无法分离,一块磁石怎样切割都有南北极,爱情中也一样,爱与恨总是纠缠着,无法理顺,
而Cobar的 SQL 批处理模式,也恰好是这样一个令人又爱又恨的个性。
通常的SQL 批处理,是将一批SQL作为一个处理单元,一次性提交给数据库,数据库顺序处理完以后,
再返回处理结果,这个特性对于数据批量插入来说,性能提升很大,因此也被普遍应用。JDBC的代码通常如下:
String sql = "insert into travelrecord (id,user_id,traveldate,fee,days) values(?,?,?,?,?)";
但Cobar的批处理模式的实现,则有几个地方是与传统不同的:
· 提交到cobar的批处理中的每一条SQL都是单独的数据库连接来执行的;
· 批处理中的SQL并发执行。
并发多连接同时执行,则意味着Batch执行速度的提升,这是让人惊喜的一个特性,
但单独的数据库连接并发执行,则又带来一个意外的副作用,即事务跨连接了,
若一部分事务提交成功,而另一部分失败,则导致脏数据问题。看到这里,你是该“爱”呢还是该“恨”?
先不用急着下结论,我们继续看看Cobar的逻辑,SQL并发执行,
其实也是依次获取独立连接并执行,因此还是有稍微的时间差,若某一条失败了,
则cobar会在会话中标记”事务失败,需要回滚“,下一个没执行的SQL就抛出异常并跳过执行,客户端就捕获到异常,
并执行rollback,回滚事务。绝大多数情况下,数据库正常运行,此刻没有宕机,因此事务还是完整保证了,
但万一恰好在某个SQL commit指令的时候宕机,于是杯具了,部分事务没有完成,数据没写入。
但这个概率有多大呢?一条insert insert 语句执行commit指令的时间假如是50毫秒,
100条同时提交,最长跨越时间是5000毫秒,即5秒中,而这个C指令的时间占据程序整个插入逻辑的时间的最多20%,
假如程序批量插入的执行时间占整个时间的20%(已经很大比例了),那就是20%×20%=4%的概率,
假如机器的可靠性是99.9%,则遇到失败的概率是0.1%×4%=十万分之四。
十万分之四,意味着99.996%的可靠性,亲,可以放心了么?
另外一个问题,即批量执行的SQL,通常都是insert的,插入成功就OK,失败的怎么办?通常会记录日志,
重新找机会再插入,因此建议主键是能日志记录的,用于判断数据是否已经插入。
最后,假如真要多个SQL使用同一个后端MYSQL连接并保持事务怎么办?就采用通常的事务模式,单条执行SQL,
这个过程中,Cobar会采用Session中上次用过的物理连接执行下一个SQL语句,因此,整个过程是与通常的事务模式完全一致。
第七个秘密:庭院深深锁清秋
说起死锁,貌似我们大家都只停留在很久远的回忆中,只在教科书里看到过,
也看到过关于死锁产生的原因以及破解方法,只有DBA可能会偶尔碰到数据库死锁的问题。
但很多用了Cobar的同学后来经常发现一个奇怪的问题,SQL很久没有应答,百思不得其解,
无奈之下找DBA排查后发现竟然有数据库死锁现象,而且比较频繁发生。
要搞明白为什么Cobar增加了数据库死锁的概率,只能从源码分析,当一个SQL需要拆分为多条SQL去到多个分片上执行的时候,这个执行过程是并发执行的,即N个SQL同时在N个分片上执行,
这个过程抽象为教科书里的事务模型,就变成一个线程需要锁定N个资源并执行操作以后,才结束事务。
当这N个资源的锁定顺序是随机的情况下,那么就很容易产生死锁现象,
而恰好Cobar并没有保证N个资源的锁定顺序,于是我们再次荣幸“中奖”。
第八个秘密:出乎意料的连接池
数据库连接池,可能是仅次于线程池的我们所最依赖的“资源池”,其重要性不言而喻,
业界也因此而诞生了多个知名的开源数据库连接池。
我们知道,对于一个MySQL Server来说,最大连接通常是1000-3000之间,这些连接对于通常的应用足够了,
通常每个应用一个Database独占连接,因此足够用了,
而到了Cobar的分表分库这里,就出现了问题,因为Cobar对后端MySQL的连接池管理是基于分片——Database来实现的,
而不是整个MySQL的连接池共享,以一个分片数为100的表为例,假如50个分片在Server1上,
就意味着Server1上的数据库连接被切分为50个连接池,每个池是20个左右的连接,
这些连接池并不能互通,于是,在分片表的情况下,我们的并发能力被严重削弱。
明明其他水池的水都是满的,你却只能守着空池子等待。。。
第九个秘密:无奈的热装载
Cobar有一个优点,配置文件热装载,不用重启系统而热装载配置文件,但这里存在几个问题,
其中一个问题是很多人不满的,即每次重载都把后端数据库重新断连一次,导致业务中断,而很多时候,
大家改配置仅仅是为了修改分片表的定义,规则,增加分片表或者分片定义,而不会改变数据库的配置信息,
这个问题由来已久,但却不太好修复。
第十个秘密:不支持读写分离
不支持读写分离,可能熟悉相关中间件的同学第一反应就是惊讶,
因为一个MySQL Proxy最基本的功能就是提供读写分离能力,以提升系统的查询吞吐量和查询性能。
但的确Cobar不支持读写分离,而且根据Cobar的配置文件,要实现读写分离,还很麻烦。
可能有些人认为,因为无法保证读写分离的时延,因此无法确定是否能查到之前写入的数据,因此读写分离并不重要,
但实际上,Mycat的用户里,几乎没有不使用读写分离功能的,后来还有志愿者增加了强制查询语句走主库(写库)的功能,以解决刚才那个问题。
第十一个秘密:不可控的主从切换
Cobar提供了MySQL主从切换能力,这个功能很实用也很方便,但你无法控制它的切换开启或关闭,
有时候我们不想它自动切换,因为到目前为止,还没有什么好的方法来确认MySQL写节点宕机的时候,
备节点是否已经100%完成数据同步,因此存在数据不一致的风险,如何更可靠的确定是否能安全切换,
这个问题比较复杂,Mycat也一直在努力完善这个特性。
以上是前世Cobar,下篇将为今生Mycat
原文链接https://www.yuque.com/books/share/0576de75-ffc4-4c34-8586-952ae4636944/vttsnv#_Toc598
link
本文说明,主要技术内容来自互联网技术大佬的分享,还有一些自我的加工(仅仅起到注释说明的作用)。如有相关疑问,请留言,将确认之后,执行侵权必删