概述

“每一个成功的男人背后都有一个女人”。Mycat 也逃脱不了这个法则。Mycat 背后是阿里曾经开源的知名产品——Cobar。Cobar 的核心功能和优势是 MySQL 数据库分片,此产品曾经广为流传,据说最早的发起者对 Mysql 很精通,后来从阿里跳槽了,阿里随后开源的 Cobar,并维持到 2013 年年初,然后,就没有然后了。

今天介绍的Cobar是阿里巴巴开源( 官方github )的一个对应用保持透明的MySQL数据库分布式处理中间件。 下面介绍一下关于Cobar的11个秘密...

阿里数据库账号登录(不为人知的阿里巴巴开源MySQL数据库中间件--Cobar的11个秘密)(1)


Cobar功能

阿里数据库账号登录(不为人知的阿里巴巴开源MySQL数据库中间件--Cobar的11个秘密)(2)

Cobar 最重要的特性是分库分表。Cobar 可以让你把一个 MySQL 的 Table 放到 10 个甚至 100 个位于不同物理机上的 MySQL 服务器上去存储,而在用户看来是一张表(逻辑表)。这样功能很有价值。


Cobar 第一个秘密: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 代码只给出了框架,还未来得及实现。真相永远在代码里,所以, 为了发现真相,还是转行去做码农吧!貌似码农也像之前的技术工人,越来越稀罕了。


Cobar 第二个秘密:高可用的陷阱?

每一个秘密的背后,总是隐藏着更大的秘密。Cobra 假死的的秘密背后,还隐藏着一个更为“强大”的秘密, 那就是假死以后,Cobra 的频繁主从切换问题。我们看看 Cobra 的一个很好的优点——“高可用性”的实现机制:

阿里数据库账号登录(不为人知的阿里巴巴开源MySQL数据库中间件--Cobar的11个秘密)(3)

分片节点 dn2_M1 配置了两个 dataSource,并且配置了心跳检测(heartbeat)语句,在这种配置下,每个dataNode 会定期对当前正在使用的 dataSource 执行心跳检测,默认是第一个,频率是 10 秒钟一次,当心跳检测失败以后,会自动切换到第二个 dataSource 上进行读写,假如 Cobra 发生了假死,则在假死的 1 分钟内,Cobra 会自动切换到第二个节点上,因为假死的缘故,第二个节点的心跳检测也超时。于是,1 分钟内 Cobra 频繁来回切换,懂得 MySQL 主从复制机制的人都知道,在两个节点上都执行写操作意味着什么?——可能数据一致性被破坏,谁也不知道那个机器上的数据是最新的。

还有什么情况下,会导致心跳检测失败呢?这是一个不得不说的秘密:当后端数据库达到最大连接后,会对新建连接全部拒绝,此时,Cobar 的心跳检测所建立的新连接也会被拒绝,于是,心跳检测失败。


Cobar 第三个秘密:看上去很美的自动切换

Cobar 很诱人的一个特性是高可用性,高可用性的原理是数据节点 DataNode 配置引用两个 DataSource,并做心跳检测,当第一个 DataSource 心跳检测失败后,Cobar 自动切换到第二个节点,当第二个节点失败以后, 又自动切换回第一个节点,一切看起来很美,无人值守,几乎没有宕机时间。

在真实的生产环境中,我们通常会用至少两个 Cobar 实例组成负载均衡,前端用硬件或者 HAProxy 这样的负载均衡组件,防止单点故障,这样一来,即使某个 Cobar 实例死了,还有另外一台接手,某个 Mysql 节点死了, 切换到备节点继续,至此,一切看起来依然很美,喝着咖啡,听着音乐,领导视察,你微笑着点头——No problem,Everything is OK!直到有一天,某个 Cobar 实例果然如你所愿的死了,不管是假死还是真死,你按照早已做好的应急方案,优雅的做了一个不是很艰难的决定——重启那个故障节点,然后继续喝着咖啡,听着音乐, 轻松写好故障处理报告发给领导,然后又度过了美好的一天。

你忽然被深夜一个电话给惊醒,你来不及发火,因为你的直觉告诉你,这个问题很严重,大量的订单数据发生错误很可能是昨天重启 cobar 导致的数据库发生奇怪的问题。你努力排查了几个小时,终于发现,主备两个库都在同时写数据,主备同步失败,你根本不知道那个库是最新数据,紧急情况下,你做了一个很英明的决定,停 止昨天故障的那个 cobar 实例,然后你花了 3 个通宵,解决了数据问题。

那么,怎么避免这个陷阱?目前只有一个办法,节点切换以后,尽快找个合适的时间,全部集群都同时重启, 避免隐患。为何是重启而不是用节点切换的命令去切换?想象一下 32 个分片的数据库,要多少次切换?

MyCAT 怎么解决这个问题的?很简单,节点切换以后,记录一个 properties 文件( conf 目录下),重启的时候,读取里面的节点 index,真正实现了无故障无隐患的高可用性。


Cobar 第四个秘密:只实现了一半的 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 第五个秘密:阻塞、又见阻塞

Cobar 本质上类似一个交换机,将后端 Mysql 的返回结果数据经过加工后再写入前端连接并返回,于是前后端连接都存在一个“写队列”用作缓冲,后端返回的数据发到前端连接 FrontConnection 的写队列中排队等待被发送,而通常情况下,后端写入的的速度要大于前端消费的速度,在跨分片查询的情况下,这个现象更为明显, 于是写线程就在这里又一次被阻塞。

解决办法有两个,增大每个前端连接的“写队列”长度,减少阻塞出现的情况,但此办法只是将问题抛给了 使用者,要是使用者能够知道这个写队列的默认值小了,然后根据情况进行手动尝试调整也行,但 Cobar 的代码中并没有把这个问题暴露出来,比如写一个告警日志,队列满了,建议增大队列数。于是绝大多数情况下,大家就默默的排队阻塞,无人知晓。

MyCAT 解决此问题的方式则更加人性化,首先将原先数组模式的固定长度的队列改为链表模式,无限制,并且并发性更好,此外,为了让用户知道是否队列过长了(一般是因为 SQL 结果集返回太多,比如 1 万条记录), 当超过指定阀值(可配)后,会产生一个告警日志。


Cobar 第六个秘密:又爱又恨的 SQL 批处理模式

正如一枚硬币的正反面无法分离,一块磁石怎样切割都有南北极,爱情中也一样,爱与恨总是纠缠着,无法理顺,而 Cobar 的 SQL 批处理模式,也恰好是这样一个令人又爱又恨的个性。

通常的 SQL 批处理,是将一批 SQL 作为一个处理单元,一次性提交给数据库,数据库顺序处理完以后,再返回处理结果,这个特性对于数据批量插入来说,性能提升很大,因此也被普遍应用。JDBC 的代码通常如下:

String sql = "insert into travelrecord (id,user_id,traveldate,fee,days) values(?,?,?,?,?)"; ps = con.prepareStatement(sql); for (Map<String, String> map : list) { ps.setLong(1, Long.parseLong(map.get("id"))); ps.setString(2, (String) map.get("user_id")); ps.setString(3, (String) map.get("traveldate")); ps.setString(4, (String) map.get("fee")); ps.setString(5, (String) map.get("days")); ps.addBatch(); } ps.executeBatch(); con.commit(); ps.clearBatch();

但 Cobar 的批处理模式的实现,则有几个地方是与传统不同的:

并发多连接同时执行,则意味着 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 语句,因此,整个过程是与通常的事务模式完全一致。


Cobar 第七个秘密:庭院深深锁清秋

说起死锁,貌似我们大家都只停留在很久远的回忆中,只在教科书里看到过,也看到过关于死锁产生的原因以及破解方法,只有 DBA 可能会偶尔碰到数据库死锁的问题。但很多用了 Cobar 的同学后来经常发现一个奇怪 的问题,SQL 很久没有应答,百思不得其解,无奈之下找 DBA 排查后发现竟然有数据库死锁现象,而且比较频繁发生。要搞明白为什么 Cobar 增加了数据库死锁的概率,只能从源码分析,当一个 SQL 需要拆分为多条 SQL 去到多个分片上执行的时候,这个执行过程是并发执行的,即 N 个 SQL 同时在 N 个分片上执行,这个过程抽象为教科书里的事务模型,就变成一个线程需要锁定 N 个资源并执行操作以后,才结束事务。当这 N 个资源的锁定顺序是随机的情况下,那么就很容易产生死锁现象,而恰好 Cobar 并没有保证 N 个资源的锁定顺序,于是我们再次荣幸“中奖”。


Cobar 第八个秘密:出乎意料的连接池

数据库连接池,可能是仅次于线程池的我们所最依赖的“资源池”,其重要性不言而喻,业界也因此而诞生了多个知名的开源数据库连接池。我们知道,对于一个 MySQL Server 来说,最大连接通常是 1000-3000 之间, 这些连接对于通常的应用足够了,通常每个应用一个 Database 独占连接,因此足够用了,而到了 Cobar 的分表分库这里,就出现了问题,因为 Cobar 对后端 MySQL 的连接池管理是基于分片——Database 来实现的,而不是整个 MySQL 的连接池共享,以一个分片数为 100 的表为例,假如 50 个分片在 Server1 上,就意味着 Server1上的数据库连接被切分为 50 个连接池,每个池是 20 个左右的连接,这些连接池并不能互通,于是,在分片表的情况下,我们的并发能力被严重削弱。明明其他水池的水都是满的,你却只能守着空池子等待。。。


Cobar 第九个秘密:无奈的热装载

Cobar 有一个优点,配置文件热装载,不用重启系统而热装载配置文件,但这里存在几个问题,其中一个问题是很多人不满的,即每次重载都把后端数据库重新断连一次,导致业务中断,而很多时候,大家改配置仅仅是为了修改分片表的定义,规则,增加分片表或者分片定义,而不会改变数据库的配置信息,这个问题由来已久, 但却不太好修复。


Cobar 第十个秘密:不支持读写分离

不支持读写分离,可能熟悉相关中间件的同学第一反应就是惊讶,因为一个 MySQL Proxy 最基本的功能就是提供读写分离能力,以提升系统的查询吞吐量和查询性能。但的确 Cobar 不支持读写分离,而且根据 Cobar 的配置文件,要实现读写分离,还很麻烦。可能有些人认为,因为无法保证读写分离的时延,因此无法确定是否能查到之前写入的数据,因此读写分离并不重要,但实际上,Mycat 的用户里,几乎没有不使用读写分离功能的, 后来还有志愿者增加了强制查询语句走主库(写库)的功能,以解决刚才那个问题。


Cobar 第十一个秘密:不可控的主从切换

Cobar 提供了 MySQL 主从切换能力,这个功能很实用也很方便,但你无法控制它的切换开启或关闭,有时候我们不想它自动切换,因为到目前为止,还没有什么好的方法来确认 MySQL 写节点宕机的时候,备节点是否已经 100%完成数据同步,因此存在数据不一致的风险,如何更可靠的确定是否能安全切换,这个问题比较复杂,Mycat 也一直在努力完善这个特性。


觉得有用的朋友多帮忙转发哦!后面会分享更多devops和DBA方面的内容,感兴趣的朋友可以关注下~

阿里数据库账号登录(不为人知的阿里巴巴开源MySQL数据库中间件--Cobar的11个秘密)(4)

,