分布式 MySQL 集群方案的探索与思考

阅读数:19065 2016 年 5 月 11 日 17:59

本文整理自 ArchSummit 微信大讲堂张成远线上群分享内容。

背景

数据库作为一个非常基础的系统,任何一家互联网公司都会使用,数据库产品也很多,有 Oracle、SQL Server 、MySQL、PostgeSQL、MariaDB 等,像 SQLServer/Oracle 这类数据库在初期可以帮业务搞定很多棘手的事情,我们可以花更多的精力在业务本身的发展上,但众所周知也得交不少钱。

涉及到钱的事情在公司发展壮大以后总是会回来重新审视这个事情的,在京东早期发展的过程中确实有一些业务的数据就是直接存在 oracle 或者 sqlserver 中。

后来随着业务的发展以及数据量访问量的不断增加及成本等方面的考虑,从长远考虑需要把这些业务用免费的 MySQL 来存,但单机的 MySQL 往往无法直接抗住这些业务,自然而然的我们就需要考虑引入分布式的 MySQL 解决方案帮助业务去 SQLServer/Oracle 以及支撑未来的发展。

方案选型对比及京东实现方案

说到分布式 MySQL 的解决方案一般来说解决方案主要就两种,客户端的方案或者中间代理的方案,如下图所示。

分布式MySQL集群方案的探索与思考

这两种方案各有各的优缺点:客户端的方案是指会给业务提供一个专门的客户端的包,这种方案在实现上会更容易一点,如果公司需要快速出一个相对通用的解决方案,客户端的方案可以优先考虑。

客户端方案需要为不同的语言提供不同的客户端的包,这点有所局限。客户端方案只需要走一段网络,理论上性能会更好一点。

客户端方案对业务有侵入,有一些系统部署及实现方面的可能可以控制得更好,但对业务本身不友好,客户端包升级等方面比较麻烦。

中间代理的方案是指采用一个兼容 MySQL 协议的代理的方式,业务可以使用任何语言的 MySQL 客户端的包,对业务本身无侵入的,这种方案相对来说是最友好的。

中间代理方案开发难度上来说门槛会更高一点,需要考虑前后端的东西,尤其是与 MySQL 端交互时自己解析协议的情况下会更复杂一些。中间代理方案多走一段 TCP,对性能理论上会有一些影响。

上述两种方案有一个非常重要的因素没有提及,在实际生产环境中面临一个非常现实的问题是 MySQL 能支持的连接数是有限的。以 MySQL5.5 来说假设一个 MySQL 实例配置 1000 个连接,业务应用实例部署了 100 个,每个应用实例的数据库连接池配置 20 个,采用客户端方案这个 MySQL 实例都没法正常工作了。

大多数情况下并不是每个应用实例的每条连接都是活跃的,中间代理的方案可以很好的解决这个问题,应用实例可以有很多连接打到代理上,代理只需要维护较少的与 MySQL 的连接即可满足需求,代理与 MySQL 之间的连接会被业务打过来的访问重复使用。

另外关于多走一次 TCP 对性能的影响,从我们的实际经验来看其实可以忽略不计,业务实例一多优先遇到的是 MySQL 连接数的问题,从这个角度来说中间代理的方案会更优。

我们采用的就是中

(点击放大图像)

分布式MySQL集群方案的探索与思考

间代理的方案,京东的分布式 MySQL 方案由很多部分组成,有 JManager、 JProxy、 JTransfer、JMonitor、JConsole、MySQL,在实际部署的时候还涉及到 LVS 以及域名系统等。

JManager 是中心管理节点,这个节点负责统一管理系统的元信息,元信息包括路由信息、权限管理信息、资源相关的信息等。

JProxy 就是一个兼容 MySQL 协议的代理,负责把客户端发送过来的 SQL 按照路由规则发送到相应的数据库节点上,再把返回的结果进行合并并返回给客户端。JProxy 在启动的时候会先去 JManager 中拉取相关的元信息,并在自己的内存中维护一份,平时使用的时候只用自身内存维护的这一份就可以了。

JProxy 的内部实现原理如图所示。

分布式MySQL集群方案的探索与思考

JTransfer 是在线迁移系统,我们针对业务的数据进行拆分以后,比如某个 MySQL 实例上有 32 个库,等到业务数据量继续增大以后在这个实例上就放不下了,我们就需要往整个集群中加 MySQL 实例,将之前的 32 个库中一部分迁移到这个新增加的实例上,如何在不停业务的情况完成数据的在线迁移就是 JTransfer 这个系统来保证的。

JConsole 系统可以理解为将多个业务的中心管理节点整合起来的一个后台管理控制系统,这个系统可以与每个 JManager 交互。在具体使用的时候,业务方需要申请创建库表、拆分规则、什么权限、对哪些 IP 授权,我们会通过 JConsole 系统与 JManager 交互完成元数据的配置。

JMonitor 系统会将各个业务的 jproxy 以及 MySQL 相关的信息采集起来,整合到一起形成一个统一的监控系统,完成对系统的全面详尽的监控。

网络模型

JProxy 作为一个非常典型的代理服务,程序本身的性能非常关键,具体在实现的时候我们参考了 Nginx 的网络模型。

大家都知道 Nginx 的性能非常高,根据机器核数配置相应的 worker 数就可以,每个 worker 可以理解为围绕一个 epoll 把前后端的连接以完全基于事件驱动的方式串在一起,避免了上下文切换避免了锁等待等各种可能阻塞或者耗时的操作。

同样的网络模型也可以参考一下 Redis 的实现,redis 虽然不像 nginx 需要考虑前后端连接的处理,但 redis 的模型也是一种非常类似的经典的实现方式。

JProxy 整个网络模型如图所示,采用一个全局的 nioacceptor 以及多个 nioreactor,由 nioacceptor 统一 accept 连接,之后把连接分给某个 nioreactor。

nioreactor 可以理解为底层就是一个 epoll(java nio 实现),前后端的连接都是注册在这个 epoll 上,我们只需要根据事件是读事件或写事件调用相应的回调函数即可。这种模型的特点是系统几乎没有太多的上下文切换,而且性能很高。

分布式MySQL集群方案的探索与思考

基于事件驱动的网络模型的好处是性能很高,但问题也很明显,编写时复杂度非常高,一条 SQL 发送过来到收到结果的上下文被切成很多片段,同一时刻有来自很多不同上下文的不同的片段要处理,全程只有一个进 (线) 程来处理这些片段 (暂且假设 NIOReactor 只配置成一个),所以在实现的过程中要求把所有的细节都考虑非常周全,一旦某个片段的处理有阻塞或者耗时,整个程序都将阻塞,个人觉得这种编程方式有点反人类思维。

关于分布式事务的思考

另外关于分布式事务的支持也是一个大家可能比较感兴趣的点,基于 MySQL 的方式来做分布式数据库的时候分布式事务是不可能满足严格的分布式事务语义的。

数据库事务有 ACID 四个属性,分别是原子性、一致性、隔离性、持久性。

原子性 (Atomicity) 的意思是整个事务最终只能是要么成功要么失败,不能存在中间状态,如果发生错误了就需要回滚回去,就像这个事务从来没有执行过一样。

一致性 (Consistency) 是指系统要处于一个一致的状态,不能因为并发事务的多少影响到系统的一致性,举个典型的例子就是转帐的情况,假设有 ABC 三个帐号各有 100 元,那么不管这三个帐号之间怎么转账,整个系统总的额度是 300 元这一点是应该是不变的。其实 ACID 里的一致性更多的是应用程序需要考虑的问题,和分布式系统里的 CAP 里的一致性完全不是一个概念。

隔离性 (Isolation),本质上是解决并发执行的事务如何保证数据库状态是正确的,抽象描述叫可串行化,就是并发的事务在执行的时候效果要求达到看起来像是一个个事务串行执行的效果。有冲突的事务之间的隔离性如果保证不了会引起前面的一致性 (consistency) 也无法满足。

每个事务包含多个动作,这些动作如果按照事务本身的顺序依次执行就是所谓的串行执行,这些动作也可以重新排列,排列完以后的动作如果效果可以等价于事务串行执行的效果我们就叫做可串行化调度。

实际实现的时候往往采用的是冲突可串行化,这个条件比可串行化要求会更高一点,规定了一些读写顺序规定了一些访问冲突的情况规定了哪些情况两个事物的动作可以调换哪些是不可以的,可以理解为冲突可串行化是可串行化的充分条件。

持久性 (Durability),在事务完成以后所有的修改可以持久的保存在数据库中,一般会采用 WAL 的方式,会把操作提前记录到日志中来保证即使操作还没有刷到磁盘就宕机的情况下有日志可以恢复。介绍完事务的 ACID 属性以后,我们再来分析为什么基于 MySQL 无法提供严格的分布式事务语义的支持。

如果客户端发送的 SQL 只涉及到一个节点,那自然是可以保证事务的,但是如果客户端发送的 SQL 涉及到两个及以上节点的 SQL,那就无法保证事务语义了。

原因主要是两个,一是原子性无法保证,另一个是隔离性无法保证。在一个节点 commit 成功以后,在另外的节点 commit 失败了,这个事务就处在一个中间状态,此时原子性被打破。

引起的另一个问题就是隔离性,这个事务的一部分提交了,另一部分未提交,此时该事务正常是不该被读取到的,但是提交成功的部分会被其他事务读到,此时就无法保证隔离性了。

另外就算是涉及多个节点的操作都是成功的,理论上来说也是无法保证隔离性的。因为假设 A 事务的一个节点先 commit 成功,其他的节点后 commit 成功,而此时 B 事务在读取的时候可能会读取到了 A 事务最早 commit 成功的那部分内容,却没有读到后来 commit 成功的内容,此时依然无法保证隔离性。

更本质一点的原因是 MySQL 的事务都是每个实例维护自身的事务 ID,而基于 MySQL 集群的分布式方案没有一个全局的事务 ID 来标识每个 MySQL 实例上的事务以及全局事务的元信息的管理,所以无法做到严格的分布式事务语义。

但实际上绝大多数业务对这个需求未必那么强烈,因为绝大多数的业务逻辑都是可以拆分的,拆成一个个只落在一个分库里的操作在绝大多数场景下是完全可行的,而且拆分完以后也会更可控,所以这个问题在我们支撑业务的过程中也不是一个特别大的问题。

生产环境监控很关键

在实际生产环境中有很多方面都非常重要,高可用高可靠可扩展等,但是除了这些之外还有一个非常关键的是监控。

一个再健壮再牛 x 的系统都需要配备完善的监控系统,监控系统是生产环境中非常重要的一道防线,没有监控的系统就像是在裸奔,线上突发状况很多完善的监控系统可以做到第一时间发现问题及时定位以及解决问题。

物理机监控。

我们在生产环境中会对系统所在物理机进行监控,京东有一个专门的物理机监控系统,可以监控包括 CPU、内存、网卡、TCP 连接数、磁盘使用情况、机器 load 等很多基础指标,针对这些指标可以设置相应的报警阈值,当超过一定阈值时会以邮件及短信的方式报警。

存活监控

但物理机的监控对于具体的系统的来说是远远不够的,我们还需要关注很多系统本身的信息,首先要有存活监控,这是最基本的。一个系统在线上运行的时候服务本身宕掉一定要求是可以第一时间监控到的。

但除了物理机监控以外,还有一个非常关键的是存活监控。系统的一切前提是可以活着,我们在每个模块都会提供相应的 http 接口,接入公司的统一监控平台,一旦有异常统一监控平台会及时通知相应的负责人。

系统内部状态可视化监控。

除了活下来以后,如何活得更好也是很关键的,所以我们还有专门针对分布式 MySQL 集群的 JMonitor 系统,该系统会整合各个模块的内部详细状态信息,包括慢查询、用户访问情况以及数据分布情况等。

一句话一个稳定健壮的系统一定要配备相应的完善的监控系统。今天我的分享就是这些,主要就是介绍一些分布式 MySQL 的相关方案以及京东是怎么做的,讨论了一下分布式事务的问题,最后是一小部分生产实践经验,谢谢大家。

Q&A

问题 1:请介绍下分布式事务保证数据最终一致性的具体方案例子。

首先分布式事务涉及到的一致性和 CAP 中一致性是两个概念,事务 ACID 属性中的一致性不涉及最终一致性,对于关系型数据库中事务的概念,我的理解都是强一致的(通过原子性和隔离型保证)。只有涉及到某一个节点(内容是相同的情况)多副本之间的复制问题才会涉及到弱一致性或者最终一致性 (CAP 中 C) 的问题。而分布式事务本身如果保证了原子性和隔离性,数据库层面就提供了一致性保证,其余的是应用逻辑层面保证。如果问的是数据库主从复制之间的一致性问题,这个事情本质上和事务(ACID 的 C)的一致性就没有关系了,所以这个问题本身可能有待商榷。

问题 2:分布式事务如何支持,现在可以支持多大规模的集群。

基于 Mysql 的分布式集群方案无法保证严格的分布式事务语义,但是在实际使用的时候看业务情况,如果事务之间不怎么冲突的情况下也是 ok 的,如果可以改成只涉及一个分库的情况下那就绕开分布式事务的问题了。另外支持的集群,我们其实是根据业务来划分资源的,目前整体资源不能说特别大,千台规模。

问题 3:JProxy 是否可以支持所有复杂 sql 查询,主要是夸库的关联查询,具体内部逻辑可否介绍下?

我们目前不支持夸库关联查询,从业务层面来解决。因为大表之间分库以后如果要支持跨库关联查询的话,作为一个 OLTP 系统在实际生产环境估计就没法用了。

问题 4:请介绍下 MySQL 实际应用中主从复制的方案,以及主从的数据差异会在什么程度, 谢谢!

这个其实更多的是主从之间关注的问题,一般会采用基于 mix 的模式。另外主从差异这个不同业务不一样,加上严格的监控,正常访问的情况下一般不会出现延迟,但是如果涉及到业务倒数据或者突增的访问量可能会引起延迟,所以这个不太好参考,如果有异常我们都会第一时间及时介入处理。

问题 5:长时间 SQL 不会造成堵塞吗?

主要看这条 SQL 具体是做什么的,如果是抽数据,就正常抽就可以了。如果有阻塞基本都是因为在 MySQL 端因为锁冲突等原因造成阻塞,最终可能是这个事务被 abort 掉或者最终抢到锁成功做完了这个事务。

问题 6:请介绍一下 JTransfers 的工作机制,以及实现过程中最难的部分。

迁移确实是比较刺手的一件事情,要考虑的细节很多。大体的步骤是:我们提交迁移计划,指定什么时间开始迁移,到时间点以后 JTransfer 就会自动迁移。JTransfer 一开始是 dump 源分库的数据,然后将这些数据恢复到目标实例上,但是在这个期间业务是正常访问的,需要将增量数据迁移完,所以会有追增量过程。当增量追到一定程度,我们会阻塞这个库的访问,最后将剩余的少量数据迁移完。因为最后剩余数据量不多的时候,阻塞过程其实很短暂,所以对业务影响非常小。

最难的部分是:整个迁移过程中的路由变更,要保证路由变更的过程中数据不能写花,且变更以后的路由要准确的推送到 JProxy 中,由 JManager 和多个 JProxy 之间在变更路由的时候采用类似两阶段提交的协议,从而保证路由的变更是正确的。

问题 7:可以分享一下 JProxy 的并发性能优化,以及 JProxy 中间状态的异常与恢复机制吗?谢谢!

并发性能优化我们主要是通过采用基于事件驱动的网络模型,这种方式的特点是避免上下文切换避免锁的开销,但是代价的话刚才也说了需要考虑得非常周全,把一个上下文切成很多片段,不太符合人类思维。

JProxy 中间件状态的异常与恢复机制这个我不是太理解什么含义哈,我的理解是如果 jproxy 运行过程中访问出异常了怎么处理,如果是某个连接过来的 sql 出了问题我们的做法是将整个连接涉及到的资源都关闭,把该次查询涉及到的前后端资源清理干净,这样就不会影响到其他客户端的访问。正常来说不应该出现这种情况,所以也需要完善的日志信息以及监控信息。

讲师介绍

张成远, 京东资深架构师,ArchSummit 北京专题讲师《Mariadb 原理与实现》作者,开源项目 speedy 作者。毕业于东北大学,硕士阶段研究分布式数据库相关方向,2012 年加入京东数据库系统研发团队。擅长高性能服务器开发、分布式数据库、分布式存储 / 缓存等大规模分布式系统架构。目前负责京东分布式数据库系统的架构与研发工作,主要关注云数据库及分布式数据库相关领域。

评论

发布