第 2 章 Mycat 前世今生
2.1 序章
如果我有一个 32 核心的服务器,我就可以实现 1 个亿的数据分片,我有 32 核心的服务器么?没有,所以我至今无法实现 1 个亿的数据分片。——Mycat’s Plan
上面这句话是 Mycat 1.0 快要完成时候的一段感言,而当发展到 Mycat 1.3 的时候,我们又有了一个新的Plan:
如果我们有 10 台物理机,我们就可以实现 1000 亿的数据分片,我们有 10 台物理机么?没有,所以,Mycat 至今没有机会验证 1000 亿大数据的支撑能力——Mycat’s Plan 2.0“每一个成功的男人背后都有一个女人”。自然 Mycat 也逃脱不了这个法则。Mycat 背后是阿里曾经开源的知名产品——Cobar。Cobar 的核心功能和优势是 MySQL 数据库分片,此产品曾经广为流传,据说最早的发起者对 Mysql 很精通,后来从阿里跳槽了,阿里随后开源的 Cobar,并维持到 2013 年年初,然后,就没有然后了。
Cobar 的思路和实现路径的确不错。基于 Java 开发的,实现了 MySQL 公开的二进制传输协议,巧妙地将自己伪装成一个 MySQL Server,目前市面上绝大多数 MySQL 客户端工具和应用都能兼容。比自己实现一个新的数据库协议要明智的多,因为生态环境在哪里摆着。
Cobar 使用起来也非常方便。由于是基于 Java 语言开发的,下载下来解压,安装 JDK,然后配置几个不是很复杂的配置文件,猛击鼠标,就能启动 Cobar。因此这个开源产品赢得了很多 Java 粉丝以及 PHP 用户的追捧。
当然,笨人(Leader us)也跟着进入,并且在某个大型云项目中——“苦海无边”的煎着熬,良久。
爱情就像是见鬼。只有撞见了,你才会明白爱情是怎么回事。TA 是如此神秘,欲语还羞。情窦初开的你又玩命将 TA 的优点放大,使自己成为一只迷途的羔羊。每个用过 Cobar 的人就像谈过一段一波三折、荡气回肠的爱情,令你肝肠寸断。就像围城:里面的人已经出不来了,还有更多的人拼命想挤进去。
仅以此文,献给哪些努力在 IT 界寻求未来的精英和小白们,还有更多被无视的,正准备转行的同仁,同在江湖混,不容易啊,面试时候就装装糊涂,放人家一马,说不定,以后又是一个 Made in China 的乔布斯啊。
如果我有一个 32 核心的服务器,我就可以实现 1 个亿的数据分片,我有 32 核心的服务器么?没有,所以我至今无法实现 1 个亿的数据分片。——Mycat ‘s Plan
2.1.1 曾经的 TA
曾经的 TA,长发飘飘,肤若凝脂,国色天香,长袖善舞,所以,一笑倾城。
那已成传说,一如您年少时的坚持:“书中自有黄金屋…” Cobar 曾是多少 IT 骚年心中的那个 TA,有关 Cobar 的这段美好的描述(不能说是广告)俘虏了众多程序猿躁动纯真的心:
Cobar 是阿里巴巴研发的关系型数据的分布式处理系统,该产品成功替代了原先基于 Oracle 的数据存储方案,目前已经接管了 3000 个 MySQL 数据库的 schema,平均每天处理近 50 亿次的 SQL 执行请求。
50 亿有多大?99%的普通人类看到这个数字,已经不能呼吸。当然,我指的是RMB。99%的程序猿除了对工资比较敏感,其实对数字通常并不感冒。上面这个简单的数字描述,已立刻让我们程序型的大脑短路。恨不得立刻百度 Cobar,立刻 Download,立刻熬夜研究。做个简单的推算,50 亿次请求转换为每个 schema 每秒的数据访问请求即 TPS,于是我们得到一个让自己不能相信的数字:20TPS,每秒不到 20 个访问。
Cobar 最重要的特性是分库分表。Cobar 可以让你把一个 MySQL 的 Table 放到 10 个甚至 100 个位于不同物理机上的 MySQL 服务器上去存储,而在用户看来是一张表(逻辑表)。这样功能很有价值。比如:我们有 1亿的订单,则可以划分为 10 个分片,存储到 2-10 个物理机上。每个 MySQL 服务器的压力减少,而系统的响应时间则不会增加。看上去很完美的功能,而且潜意识里,执行这句 SQL:
100%的人都会认为:会返回 1 条数据,但事实上,Cobar 会返回 N 条数据,N=分片个数。接下来我们继续执行 SQL:
你会发现奇怪的乱序现象,而且结果还随机,这是因为,Cobar 只是简单的把上述 SQL 发给了后端 N 个分片对应的 MySQL 服务器去执行,然后把结果集直接输出….
再继续看看,我们常用的 Limit 分页的结果…可以么?答案是:不可以。
这个问题可以在客户端程序里做些工作来解决。所以随后出现了 Cobar Client。据我所知,很多 Cobar 的使用者也都是自行开发了类似 Cobar Client 的工具来解决此类问题。从实际应用效果来说,一方面,客户端编程方式解决,困难度很高,Bug 率也居高不下;另一方面,对于 DBA 和运维来说,增加了困难度。
当你发现这个问题的严重性,再回头看看 Cobar 的官方文档,你怅然若失,四顾茫然。
接下来,本文将隐藏在 Cobar 代码中那些不为人知的秘密逐一披漏,你洞悉了这些秘密,就会明白 Mycat 为什么会横空出世。
2.1.2 Cobar 的十一个秘密
2.1.1.1 第一个秘密:Cobra 会假死?
是的,很多人遇到这个问题。如何来验证这点呢?可以做个简单的小实验,假如你的分片表中配置有表company,则打开 mysql 终端,执行下面的 SQL:
代码语言:javascript复制select sleep(500) from company;
此 SQL 会执行等待 500 秒,你再努力以最快的速度打开 N 个 mysql 终端,都执行相同的 SQL,确保 N>当前 Cobra 的执行线程数:
代码语言:javascript复制show @@threadpool
的所有 Processor1-E 的线程池的线程数量总和,然后你再执行任何简单的 SQL,或者试图新建立连接,都会无法响应,此时
代码语言:javascript复制show @@threadpool
里面看到 TASK_QUEUE_SIZE 已经在积压中。
不可能吧,据说 Cobra 是 NIO 的非阻塞的,怎么可能阻塞!别激动,去看看代码,Cobra 前端是 NIO 的,而后端跟 Mysql 的交互,是阻塞模式,其 NIO 代码只给出了框架,还未来得及实现。真相永远在代码里,所以,为了发现真相,还是转行去做码农吧!貌似码农也像之前的技术工人,越来越稀罕了。
2.1.1.2 第二个秘密:高可用的陷阱?
每一个秘密的背后,总是隐藏着更大的秘密。Cobra 假死的的秘密背后,还隐藏着一个更为“强大”的秘密,那就是假死以后,Cobra 的频繁主从切换问题。我们看看 Cobra 的一个很好的优点——“高可用性”的实现机制,
下图解释了 Cobra 如何实现高可用性: 分片节点 dn2_M1 配置了两个 dataSource,并且配置了心跳检测(heartbeat)语句,在这种配置下,每个dataNode 会定期对当前正在使用的 dataSource 执行心跳检测,默认是第一个,频率是 10 秒钟一次,当心跳检测失败以后,会自动切换到第二个 dataSource 上进行读写,假如 Cobra 发生了假死,则在假死的 1 分钟内,Cobra 会自动切换到第二个节点上,因为假死的缘故,第二个节点的心跳检测也超时。于是,1 分钟内 Cobra 频繁来回切换,懂得 MySQL 主从复制机制的人都知道,在两个节点上都执行写操作意味着什么?——可能数据一致性被破坏,谁也不知道那个机器上的数据是最新的。
还有什么情况下,会导致心跳检测失败呢?这是一个不得不说的秘密:当后端数据库达到最大连接后,会对新建连接全部拒绝,此时,Cobar 的心跳检测所建立的新连接也会被拒绝,于是,心跳检测失败,于是,一切都悄悄的发生了。
幸好,大多数同学都没有配置高可用性,或者还不了解此特性,因此,这个秘密,一直在安全的沉睡。
2.1.1.3 第三个秘密:看上去很美的自动切换
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,真正实现了无故障无隐患的高可用性。
2.1.1.4 第四个秘密:只实现了一半的 NIO
NIO 技术用作 JAVA 服务器编程的技术标准,已经是不容置疑的业界常规做法,若一个 Java 程序员,没听说过 NIO,都不好意思说自己是 Java 人。所以 Cobar 采用 NIO 技术并不意外,但意外的是,只用了一半。
Cobar 本质上是一个“数据库路由器”,客户端连接到 Cobar,发生 SQL 语句,Cobar 再将 SQL 语句通过后端与 MySQL 的通讯接口 Socket 发出去,然后将结果返回给客户端的 Socket 中。下面给出了 SQL 执行过程简要逻辑:
代码语言:javascript复制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,有一个重要提升。
2.1.1.5 第五个秘密:阻塞、又见阻塞
Cobar 本质上类似一个交换机,将后端 Mysql 的返回结果数据经过加工后再写入前端连接并返回,于是前后端连接都存在一个“写队列”用作缓冲,后端返回的数据发到前端连接 FrontConnection 的写队列中排队等待被发送,而通常情况下,后端写入的的速度要大于前端消费的速度,在跨分片查询的情况下,这个现象更为明显,于是写线程就在这里又一次被阻塞。
解决办法有两个,增大每个前端连接的“写队列”长度,减少阻塞出现的情况,但此办法只是将问题抛给了使用者,要是使用者能够知道这个写队列的默认值小了,然后根据情况进行手动尝试调整也行,但 Cobar 的代码中并没有把这个问题暴露出来,比如写一个告警日志,队列满了,建议增大队列数。于是绝大多数情况下,大家就默默的排队阻塞,无人知晓。
MyCAT 解决此问题的方式则更加人性化,首先将原先数组模式的固定长度的队列改为链表模式,无限制,并且并发性更好,此外,为了让用户知道是否队列过长了(一般是因为 SQL 结果集返回太多,比如 1 万条记录),当超过指定阀值(可配)后,会产生一个告警日志。
代码语言:javascript复制1024
2.1.1.6 第六个秘密:又爱又恨的 SQL 批处理模式
正如一枚硬币的正反面无法分离,一块磁石怎样切割都有南北极,爱情中也一样,爱与恨总是纠缠着,无法理顺,而 Cobar 的 SQL 批处理模式,也恰好是这样一个令人又爱又恨的个性。
通常的 SQL 批处理,是将一批 SQL 作为一个处理单元,一次性提交给数据库,数据库顺序处理完以后,再返回处理结果,这个特性对于数据批量插入来说,性能提升很大,因此也被普遍应用。JDBC 的代码通常如下:
代码语言:javascript复制String sql = "insert into travelrecord (id,user_id,traveldate,fee,days) values(?,?,?,?,?)";
ps = con.prepareStatement(sql);
for (Map 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 的批处理模式的实现,则有几个地方是与传统不同的:
- 提交到 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 语句,因此,整个过程是与通常的事务模式完全一致。
2.1.1.7 第七个秘密:庭院深深锁清秋
说起死锁,貌似我们大家都只停留在很久远的回忆中,只在教科书里看到过,也看到过关于死锁产生的原因以及破解方法,只有 DBA 可能会偶尔碰到数据库死锁的问题。但很多用了 Cobar 的同学后来经常发现一个奇怪的问题,SQL 很久没有应答,百思不得其解,无奈之下找 DBA 排查后发现竟然有数据库死锁现象,而且比较频繁发生。要搞明白为什么 Cobar 增加了数据库死锁的概率,只能从源码分析,当一个 SQL 需要拆分为多条 SQL 去到多个分片上执行的时候,这个执行过程是并发执行的,即 N 个 SQL 同时在 N 个分片上执行,这个过程抽象为教科书里的事务模型,就变成一个线程需要锁定 N 个资源并执行操作以后,才结束事务。当这 N 个资源的锁定顺序是随机的情况下,那么就很容易产生死锁现象,而恰好 Cobar 并没有保证 N 个资源的锁定顺序,于是我们再次荣幸“中奖”。
2.1.1.8 第八个秘密:出乎意料的连接池
数据库连接池,可能是仅次于线程池的我们所最依赖的“资源池”,其重要性不言而喻,业界也因此而诞生了多个知名的开源数据库连接池。我们知道,对于一个 MySQL Server 来说,最大连接通常是 1000-3000 之间,这些连接对于通常的应用足够了,通常每个应用一个 Database 独占连接,因此足够用了,而到了 Cobar 的分表分库这里,就出现了问题,因为 Cobar 对后端 MySQL 的连接池管理是基于分片——Database 来实现的,而不是整个 MySQL 的连接池共享,以一个分片数为 100 的表为例,假如 50 个分片在 Server1 上,就意味着 Server1上的数据库连接被切分为 50 个连接池,每个池是 20 个左右的连接,这些连接池并不能互通,于是,在分片表的情况下,我们的并发能力被严重削弱。明明其他水池的水都是满的,你却只能守着空池子等待。。。
2.1.1.9 第九个秘密:无奈的热装载
Cobar 有一个优点,配置文件热装载,不用重启系统而热装载配置文件,但这里存在几个问题,其中一个问题是很多人不满的,即每次重载都把后端数据库重新断连一次,导致业务中断,而很多时候,大家改配置仅仅是为了修改分片表的定义,规则,增加分片表或者分片定义,而不会改变数据库的配置信息,这个问题由来已久,但却不太好修复。
2.1.1.10 第十个秘密:不支持读写分离
不支持读写分离,可能熟悉相关中间件的同学第一反应就是惊讶,因为一个 MySQL Proxy 最基本的功能就是提供读写分离能力,以提升系统的查询吞吐量和查询性能。但的确 Cobar 不支持读写分离,而且根据 Cobar 的配置文件,要实现读写分离,还很麻烦。可能有些人认为,因为无法保证读写分离的时延,因此无法确定是否能查到之前写入的数据,因此读写分离并不重要,但实际上,Mycat 的用户里,几乎没有不使用读写分离功能的,后来还有志愿者增加了强制查询语句走主库(写库)的功能,以解决刚才那个问题。
2.1.1.11 第十一个秘密:不可控的主从切换
Cobar 提供了 MySQL 主从切换能力,这个功能很实用也很方便,但你无法控制它的切换开启或关闭,有时候我们不想它自动切换,因为到目前为止,还没有什么好的方法来确认 MySQL 写节点宕机的时候,备节点是否已经100%完成数据同步,因此存在数据不一致的风险,如何更可靠的确定是否能安全切换,这个问题比较复杂,Mycat 也一直在努力完善这个特性。