背景
数据库作为一个非常基础的系统,任何一家互联网公司都会使用,数据库产品也很多,有Oracle、SQL Server 、MySQL、PostgeSQL、MariaDB等,像SQLServer/Oracle 这类数据库在初期可以帮业务搞定很多棘手的事情,我们可以花更多的精力在业务本身的发展上,但众所周知也得交不少钱。
涉及到钱的事情在公司发展壮大以后总是会回来重新审视这个事情的,在京东早期发展的过程中确实有一些业务的数据就是直接存在oracle或者sqlserver中。
后来随着业务的发展以及数据量访问量的不断增加及成本等方面的考虑,从长远考虑需要把这些业务用免费的MySQL来存,但单机的MySQL往往无法直接抗住这些业务,自然而然的我们就需要考虑引入分布式的MySQL解决方案帮助业务去SQLServer/Oracle以及支撑未来的发展。
方案选型对比及京东实现方案
说到分布式MySQL的解决方案一般来说解决方案主要就两种,客户端的方案或者中间代理的方案,如下图所示。
相关厂商内容
用Kafka Streams搭建实时的广告消费系统
OpenResty十年开源的历程和思考
阿里巴巴Blink流计算平台介绍与实践
Apache Kafka的过去,现在,和未来
阿里妈妈国际广告算法大赛,等的就是你!
相关赞助商
这两种方案各有各的优缺点:客户端的方案是指会给业务提供一个专门的客户端的包,这种方案在实现上会更容易一点,如果公司需要快速出一个相对通用的解决方案,客户端的方案可以优先考虑。
客户端方案需要为不同的语言提供不同的客户端的包,这点有所局限。客户端方案只需要走一段网络,理论上性能会更好一点。
客户端方案对业务有侵入,有一些系统部署及实现方面的可能可以控制得更好,但对业务本身不友好,客户端包升级等方面比较麻烦。
中间代理的方案是指采用一个兼容MySQL协议的代理的方式,业务可以使用任何语言的MySQL客户端的包,对业务本身无侵入的,这种方案相对来说是最友好的。
中间代理方案开发难度上来说门槛会更高一点,需要考虑前后端的东西,尤其是与MySQL端交互时自己解析协议的情况下会更复杂一些。中间代理方案多走一段TCP,对性能理论上会有一些影响。
上述两种方案有一个非常重要的因素没有提及,在实际生产环境中面临一个非常现实的问题是MySQL能支持的连接数是有限的。以MySQL5.5来说假设一个MySQL实例配置1000个连接,业务应用实例部署了100个,每个应用实例的数据库连接池配置20个,采用客户端方案这个MySQL实例都没法正常工作了。
大多数情况下并不是每个应用实例的每条连接都是活跃的,中间代理的方案可以很好的解决这个问题,应用实例可以有很多连接打到代理上,代理只需要维护较少的与MySQL的连接即可满足需求,代理与MySQL之间的连接会被业务打过来的访问重复使用。
另外关于多走一次TCP对性能的影响,从我们的实际经验来看其实可以忽略不计,业务实例一多优先遇到的是MySQL连接数的问题,从这个角度来说中间代理的方案会更优。
我们采用的就是中
(点击放大图像)
间代理的方案,京东的分布式MySQL方案由很多部分组成,有JManager、 JProxy、 JTransfer、JMonitor、JConsole、MySQL,在实际部署的时候还涉及到LVS以及域名系统等。
JManager是中心管理节点,这个节点负责统一管理系统的元信息,元信息包括路由信息、权限管理信息、资源相关的信息等。
JProxy就是一个兼容MySQL协议的代理,负责把客户端发送过来的SQL按照路由规则发送到相应的数据库节点上,再把返回的结果进行合并并返回给客户端。JProxy在启动的时候会先去JManager中拉取相关的元信息,并在自己的内存中维护一份,平时使用的时候只用自身内存维护的这一份就可以了。
JProxy的内部实现原理如图所示。
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上,我们只需要根据事件是读事件或写事件调用相应的回调函数即可。这种模型的特点是系统几乎没有太多的上下文切换,而且性能很高。
基于事件驱动的网络模型的好处是性能很高,但问题也很明显,编写时复杂度非常高,一条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出了问题我们的做法是将整个连接涉及到的资源都关闭,把该次查询涉及到的前后端资源清理干净,这样就不会影响到其他客户端的访问。正常来说不应该出现这种情况,所以也需要完善的日志信息以及监控信息。