京东架构师这样讲述:分布式数据库系统演进之路

关于数据库的使用,在京东有几个趋势,早期在京东主要用SqlServer及Oracle也有少许采用MySQL,随着业务发展技术积累及使用成本等因素,不少业务都开始使用MySQL,包括早期使用SqlServer及Oracle的不少核心业务也都渐渐的开始迁移到MySQL,单机的MySQL每每没法支撑这类业务,须要考分布式的解决方案,另外本来使用MySQL的业务随着数据量及访问量的增长也会遇到瓶颈最终也会考虑采用分布式解决的方案,整个京东发展趋势如图1所示。前端

图1 业务使用数据库演变趋势

分布式的数据库解决方案有不少种,在各个互联网公司使用得也是很是的广泛,本质上就是将数据拆开存储在多个节点上从而缓解单节点的压力,业务层面也能够根据业务特色自行进行拆分,如图2所示,假设有一张user表,以ID为拆分键,假设拆分红两份,最简单的就是奇数ID的数据落到一个存储节点上,偶数ID的数据落到另一个存储节点上,实际部署示意图如图3所示。sql

除了业务层面作拆分,也能够考虑采用较为通用的一些解决方案,主要分为两类,一类是客户端解决方案,这种方案是在业务应用中引入特定的客户端包,经过该客户端包完成数据的拆分查询及结果汇总等操做,这种方案对业务有必定侵入性,随着业务应用实例部署的数量比较大,数据库端可能会面临链接数压力比较大的问题,另外版本升级也比较困难,优势是链路较短,从应用实例直接到数据库。数据库

图2 数据拆分示意图

另外一类是中间件的解决方案,这种方案是提供兼容数据库传输协议及语法规范的代理,业务在链接中间件的时候能够直接使用传统的JDBC等客户端,从而大大减轻了业务开发层面的负担,弊端是中间件的开发难度会比客户端方案稍微高一点,另外网络传输链路上多走了一段,理论上对性能略有影响,实际使用环境中这些系统都是在机房内网访问,这种网络上的影响彻底能够忽略不计。后端

图3 系统部署示意图

根据上述分析,为了更好的支撑京东大量的大规模数据量的业务,咱们开发了一套兼容MySQL协议的分布式数据库的中间件解决方案,咱们称之为JProxy,这套方案通过了屡次的演变最终完成并支撑了京东全集团的去Oracle/Sqlserver任务。网络

JProxy第一个版本如图4所示,每一个JProxy都会有一个配置文件,咱们会在配置文件中配置相应业务的库表拆分信息及路由信息, JProxy接收到SQL之后会对SQL进行解析再根据路由信息决定SQL是否须要重写及该发往哪些节点,等各节点结果返回之后再将结果汇总按照MySQL传输协议返回给应用。架构

结合上文的例子,当用户查询user这张表时假设SQL语句是select * from user where id = 1 or id = 2,当收到这条SQL之后,JProxy会将SQL拆分为select * from user where id=1 及select * from user where id = 2, 再分别把这两条sql语句发日后端的节点上,最后将两个节点上获取到的两条记录一并返回给应用。并发

这种方案在业务库表比较少的时候是可行的,随着业务的发展库表的数量可能会不断增长,尤为是针对去Oracle的业务在切换数据库的时候多是一次切换几张表,下一次再切换另外几张表,这就要求常常修改配置文件。另外JProxy在部署的时候至少须要部署两份甚至多份,如图5所示,此时面临一个问题是如何保证全部的配置文件在不断修改的过程当中是彻底一致的。在早期运维过程当中,咱们靠人工修改完一份配置文件,再将相应的配置文件拷贝给其余的JProxy,确保JProxy配置文件内容一致,这个过程心智负担较重且容易出错。运维

图4 版本一

图5 配置文件

在以后的版本中咱们引入了JManager模块,这个模块负责的工做是管理配置文件中的路由元信息,如图6所示。JProxy的路由元信息都是经过JManager来统一获取,咱们只须要经过JManager往元数据库里添加修改路由元数据,操做完成之后通知各个JProxy动态加载路由信息就能够保证每一个JProxy的路由信息是彻底一致的,从而解决维护路由元信息一致性的痛点。分布式

图6 版本二

在提到分布式数据库解决方案时必定会考虑的一个问题是扩容问题,扩容有两种方式,一种咱们称之为re-sharding方案,简单的说就是一片拆两片,两片拆为四片,如图7所示,本来只有一个MySQL实例一个shard,以后拆分红shard1和shard2两个分片,以后再添加新的MySQL实例,将shard1拆分红shard11和shard12两个分片,将shard2拆分红shard21和shard22两个分片放到另外新加的MySQL实例上,这种扩容方式是最理想的,但具体实现的时候会略微麻烦一点,咱们短时间以内选择了另外一种偏保守一点在合理预估前提下足以支撑业务发展的扩容模式,咱们称之为pre-sharding方案,这种方案是预先拆分在必定时期内足够用的分片数,在前期数据量较少时这些分片能够放在一个或少许的几个MySQL实例上,等后期数据量增大之后能够往集群中加新的MySQL实例,将本来的分片迁移到新添加的MySQL实例上,如图8所示,咱们在一开始就拆分红了shard一、shard二、shard三、shard4四个分片,这四个分片最初是在一个MySQL实例上,数据量增大之后咱们能够添加新的MySQL实例,将shard3和shard4迁移新的MySQL实例上,整个集群分片数没有发生变化可是容量已经变成了原来的两倍。性能

图7 re-sharding方案

图8 pre-sharding方案

Pre-sharding方案至关于经过迁移完成达到扩容的目的,分片位置的变更涉及到数据的迁移验证及路由元数据的变动等一系列变更,因此咱们引入了JTransfer系统,如图9所示。JTransfer能够作到在线无缝迁移,迁移扩容时只需提交一条迁移计划,指定将某个分片从哪一个源实例迁移到哪一个目标实例,能够指定在什么时候开始迁移任务,等到了时间点系统会自动开始作迁移。整个迁移过程当中涉及到迁移基础全量数据和迁移过程当中业务访问产生的增量数据,一开始会将基础全量数据从源实例中dump出来到目标实例恢复,确认数据正确之后开始追赶增量数据,当增量数据追赶到必定程度系统预估能够快速追赶结束时,咱们会作一个短暂的锁定操做,从而确保将最后的增量所有追赶完成,这个锁定时间也是在提交迁移任务时能够指定的一个参数,好比最多只能锁定20s,若是由于此时访问量忽然增大等缘由最终剩余的增量没能在20s内追赶完成,整个迁移任务将会放弃,确保对线上访问影响达到最小。迁移完成以后会将路由元信息进行修改,同时将路由元信息推送给全部的JProxy,最后再解除锁定,访问将根据路由打到分片所在的新位置。

图9 版本三

系统在生产环境中使用的时候,除了考虑以上的介绍之外还须要考虑不少部署及运维的事情,首先要考虑的就是系统如何活下来,须要考虑系统的自我保护能力,要确保系统的稳定性,要作到性能可以知足业务需求。

在JProxy内部咱们采用了基于事件驱动的网络IO模型同时考虑到多核等特色,将整个系统的性能发挥到极致,在压测时JProxy表现出来的性能随着MySQL实例的增长几乎是呈现线性增加的趋势,并且整个过程当中JProxy所在机器毫无压力。

保证性能还不够,还须要考虑控制链接数、控制系统内存等,链接数主要是控制链接的数量这个比较好理解,控制内存主要是指控制系统在使用过程当中对内存的需求量,好比在作数据抽数时候,sql语句是相似select * from table这种的全量查询,此时后端全部的MySQL数据会经过多条链接并发地往中间件发送数据,从中间件到应用只有一条链接,若是不对内存进行控制就会形成中间件OOM,在具体实现的时候咱们经过将数据压在TCP栈中来控制中间件先后端链接的网络流速从而很好的保证了整个系统的内存是在可控范围内。

另外还须要考虑权限,哪些IP能够访问哪些IP不能访问都须要能够精确的控制,具体到某一张表还须要控制增删改查的权限,咱们建议业务在写SQL的时候尽可能都带有拆分字段保证SQL均可以落在某个分片上从而保证整个访问是足够的简单可控,咱们为之提供了精细的权限控制,能够作到表级别的增删改查权限,包括是否要带有拆分字段,最大程度作到对SQL的控制,保证业务在测试阶段写出不知足指望的SQL都能及时发现,大大下降后期线上运行时的风险。

除了基本的稳定性以外,在整个系统全局上还须要考虑到服务高可用方案。JProxy是无状态的,一个业务在同一个机房内部署至少两个JProxy且必须跨机架的,保证在同一个机房里JProxy是高可用的。在另外的机房会部署再部署两个JProxy,作到跨机房的高可用。除了中间件自身的高可用之外还须要保证数据库层面的高可用,全链路的高可用才是真正的高可用。数据库层面在同一个机房里会按照一主一从部署,在备用机房会再部署一个备,如图10所示。JProxy访问MySQL时经过域名访问,若是MySQL的主出异常数据库会进行相应的主从切换操做,JProxy能够访问到切换之后新的主,若是整个机房的数据库异常能够直接将数据的域名切换到备用机房,保证JProxy能够访问到备用机房的数据库。业务访问JProxy时也是经过域名访问,若是一个机房的JProxy都出现了异常,和数据库相似直接将JProxy前端的域名切换到备用机房,从而保证业务始终都能正常访问JProxy。

数据高可靠也是很是关键的点,咱们会这对数据库的数据进行按期备份,将备份数据存储到相应的存储系统中,从而保证数据库中的数据即便被删除依然是能够恢复的。

图10 部署示意图

系统在线上运行时候监控报警是极其重要的,监控能够分多个层次,如图11所示,从主机和操做系统的信息到应用系统的信息到特定系统内部特定的信息的监控等,针对操做系统及主机的监控京东有MJDOS系统能够把系统的内存/cpu/磁/网卡/机器负载等各类信息都归入监控系统,这些操做系统的基础信息对系统异常的诊断很是关键,好比由于网络丢包等引发的服务异常均可以在这个监控系统中及时找到根源。

京东还有统一的监控报警系统UMP,这个监控系统主要是给全部的应用系统服务,全部的应用系统按照必定的规则暴露接口,在UMP系统中注册之后,UMP系统就能够提供一整套监控报警服务,最基本的好比系统的存活监控以及是否有慢查询等。

除了这两个基本的监控系统之外,咱们还针对整套中间件系统开发了定制的监控系统JMonitor,之因此开发这套监控系统是由于咱们须要采集更多的定制的监控信息,在系统发生异常时可以第一时间定位问题,举个例子当业务发现TP99降低时每每伴随着有慢SQL,应用从发送SQL到收到结果这个过程当中通过了JProxy到MySQL又从MySQL通过JProxy再回到应用,这条链路上任何一个环节均可能慢,无论是哪一个阶段耗时,咱们须要将这种慢SQL的记录精细化,精细到各个阶段都花了多少时间,作到出现慢SQL时能快速准确的找到问题根源快速解决问题。

另外在配合业务去Oracle/SqlServer时,咱们不建议使用跨库的事务,可是会出现有一种状况,同一个事务里的SQL都是带有拆分字段的,每条SQL都是单节点的,同一个事务里有多条这种SQL,结果却出现这个事务是跨库的,这种事务咱们都会有详细的记录,业务方能够直接经过JMonitor找到这种事务从而更好的进一步改进。除了这个之外,在测试环境时候业务系统一开始写的SQL没有考虑太多的优化可能会出现比较多的慢SQL,这些慢SQL咱们都会统一采集在JMonitor系统上进行分析处理,帮助业务方快速迭代调整SQL语句。

图11 监控体系

业务在使用这套系统的时候 要尽可能出现避免跨库的SQL,有一个很重要的缘由是当出现跨库SQL的时候会耗费MySQL较多的链接如图12所示,一条不带拆分字段的SQL将会发送到全部的分片上,若是在一个MySQL实例上有64个分片,那一条这样的SQL就会耗费这个MySQL实例上的64个链接,这个资源消耗是很是可观的,若是能够控制SQL落在单个分片上能够大大下降MySQL实例上的链接压力。

图12 链接数

跨库的分布式事务要要尽可能避免,一个是基于MySQL的分布式数据库中间件的方案没法保证严格的分布式事务语义,另外一个即便能够作到严格的分布式事务语义支持依然是要尽可能避免垮库事务的,多个跨库的分布式事务在某个分片上发生死锁将会形成其余分片上的事务也没法继续致使直接引发大面积的死锁,即便是单节点上的事务也要尽可能控制事务小一点,下降死锁发生的几率。

具体的路由策略不一样的业务能够特殊对待,以京东分拣中心为例,各个分拣中心的大小差别很大,北京上海等大城市的分拣中心数据量很大其余城市的分拣中心相对会小一点, 针对这种特色咱们会给其定制路由策略,作到将大的分拣中心的数据落在特定的性能较好的MySQL实例上,其余小的分拣中心的数据能够按照普通的拆分方式处理。

在JProxy系统层面咱们能够支持多租户模式,但考虑到去Oracle/SqlServer的业务每每都是很是重要且数据量巨大的业务,因此咱们的系统都是不一样的业务独立部署一套,在部署层面避免各个业务之间的互相影响。考虑到独立部署会形成一些资源浪费,咱们引入了容器系统,将操做系统资源经过容器的方式进行隔离,从而保证系统资源的充分利用。不少问题不必必定要在代码层面解决,代码层面解决起来比较麻烦或者不能作到百分之百把控的事情能够经过架构层面来解决,架构层面很差解决的事情能够经过部署的层面来解决,部署层面很差解决的事情能够经过产品层面来解决,解决问题的方式各式各样,须要从整个系统全局角度来综合考量,引用邓公的一句话“无论黑猫白猫,能抓老鼠的就是好猫”,一样的道理能支撑住业务发展的系统就是好的系统。

另外再简单讨论一下为何基于MySQL的分布式数据库中间件系统没法保证严格的分布式事务语义支持。所谓分布式事务语义本质上就是事务的语义,包含了ACID属性,分别是原子性、一致性、持久性、隔离性。

原子性是指一个事务要么成功要么失败,不能存在中间状态。持久性是指一个事务一旦提交成功那么要作到系统崩溃之后再恢复依然是成功的。隔离性是指各个并发事务之间是隔离的,不可见的,在数据库具体实现上可能会分不少个隔离级别。事务的一致性是指要保证系统要处于一个一致的状态,好比从A帐户转了500元到B帐户,那么从总体系统来看系统的总金额是没有发生变化的,不能出现A的帐户已经减去500元可是B帐户却没有增长500元的状况。

图13 可串行化调度

事务在数据库系统中执行的时候有一个可串行化调度的问题,假设有T一、T二、T3三个事务,那么这三个事务的执行的效果应该和三个事务串行执行效果同样,也就是最终效果效果应该是{T1/T2/T3, T1/T3/T2, T2/T1/T3, T2/T3/T1, T3/T1/T2, T3/T2/T1}集合中的一个,当涉及到分布式事务时,每一个子事务之间的调度要和全局的分布式事务的调度顺序一致才能知足可串行化调度的要求,如图13所示,T1/T2/T3的三个分布式事务,在一个库中的调度顺序是T1/T2/T3和全局的调度顺序一致,在另外一个库中的调度顺序变成了T3/T2/T1,此时站在全局的角度来看就打破了可串行化调度,可串行化调度保证了隔离性的实现,当可串行化调度被打破时天然隔离性也就随之打破,在基于MySQL的分布式中间件方案实现上,由于同一个分布式事务的各个子事务的事务ID是在各个MySQL上生成的,并无提供全局的事务ID来保证各个子事务的调度顺序和全局的分布式事务一致,致使隔离性是没法保证的,因此说当前基于MySQL的分布式事务是没法保证严格的分布式事务语义支持的。固然随着MySQL引入GR能够作到CAP理论中的强一致,再增强中间件的相关功能及定制MySQL相关功能也是有可能作到支持严格的分布式事务的。

相关文章
相关标签/搜索