随着业务的快速发展,做到未雨绸缪很重要,在提升关系型数据库的扩展性和高可用性方面需要提前布局,MySQL方案虽然不是万金油,却是架构演进中的一种典型方案,也是建设MySQL分布式存储平台一个很好的切入点。
本文会着重讨论迁移到MySQL架构体系的演进过程,相信大大小小的公司在不同的发展阶段都会碰到其中一些共性的问题。
我们先来简单介绍一下系统迁移的背景,在这个过程中我们不会刻意强调源数据库的一些功能性差异,相对来说是一种更通用的架构改进方式。
一、架构改造背景和演进策略
迁移前,我们做了业务梳理,整体的系统现状梳理如下表,可以发现这个业务其实可以划分为两个大类,一个是数据业务,一个是账单业务。数据业务负责事务性数据,而账单业务是状态数据的操作历史。
改造前架构如下图所示,对数据做了过滤,整体上库里面的表有上万张,虽然是多个独立的业务单元,但是状态数据和流水数据是彼此通过存储过程级联调用。
对这样一个系统做整体的改造,存在大量存储过程,在业务耦合度较高的情况下,要拆分为分布式架构是很困难的,主要体现在3个地方:
(1)研发和运维对于分布式架构的理解有限,认为改造虽然可行,但是改动量极大,基本会在做和不做之间摇摆。
(2)对于大家的常规理解来说,希望达到的效果是一种透明平移的状态,即原来的存储过程我们都无缝的平移过来,在MySQL分布式的架构下,这种方案显然是不可行的,而且如果硬着头皮做完,效果也肯定不好。
(3)对于分布式的理解,不是仅仅把业务拆开那么简单,我们心中始终要有一个平衡点,并不是所有业务都需要拆分做成分布式。分布式虽能带来好处,但是同时分布式也会带来维护的复杂成本。
所以对于架构的改进,我们为了能够落地,要在这个过程中尽可能和研发团队保持架构的同步迭代,整体上走过了如下图所示的4个阶段。
(1)功能阶段:梳理需求,对存储过程进行转移,适配MySQL方向。
(2)架构阶段:对系统架构和业务架构进行改进设计,支持分布式扩展。
(3)性能阶段:对系统压力进行增量测试和全量测试,全面优化性能问题。
(4)迁移阶段:设计数据迁移方案,完成线上环境到MySQL分布式环境的迁移。
我们主要讨论上面前3个阶段,我总结为8个架构演进策略,我们逐个来说一下。
二、功能设计阶段
策略1:功能平移
对于一个已经运行稳定的商业数据库系统,如果要把它改造为基于MySQL分布式架构,很自然会存在一种距离感,这是一种重要但不紧急的事情,而且从改进的步调来说,是很难一步到位的。所以我们在这里实行的是迭代的方案,如下图所示。
如同大家预期的那样,既然里面有大量的存储过程逻辑,我们是不是把存储过程转移到MySQL里面就可以了呢。
在没有做完这件事情之前,大家谁都不敢这么说,况且MySQL单机的性能和商业数据库相比本身存在差距,在摇摆不定中,我们还是选择既有的思维来进行存储过程转移。
在初始阶段,这部分的时间投入会略大一些,在功能和调用方式上,我们需要做到尽可能让应用层少改动或者不改动逻辑代码。
存储过程转移之后,我们的架构演进才算是走入了轨道,接下来我们要做的是系统拆分。
三、系统架构演进阶段
策略2:系统架构拆分
我们之前做业务梳理时清楚地知道:系统分为数据业务和账单业务,那么我们下一步的改造目标也很明确了。
首先的切入点是数据库的存储容量,如果一个TB级别的MySQL库,存在着上万张表,而且业务的请求极高,很明显单机存在着较大的风险,系统拆分是把原来的一个实例拆成两个,通过这种拆分就能够强行把存储过程的依赖解耦。
而拆分的核心思路是对于账单数据的写入从实时转为异步,这样对于前端的响应就会更加高效。
拆分后的架构如下图所示。
当然拆分后,新的问题出现了,账单业务的写入量按照规划是很高的,无论单机的写入性能和存储容量都难以扩展,所以我们需要想出新的解决方案。
策略3:写入水平扩展
账单数据在业务模型上属于流水型数据,不存在事务,所以我们的改进就是把账单业务的存储过程转变为insert语句,在转换之后,我们把账单数据库改造为基于中间件的分布式架构,这个过程对于应用同学来说是透明的,因为它的调用方式依然是SQL。
同时因为之前的账单数据有大量的表,数据分布参差不齐,表结构都相同,所以我们也借此机会把数据入口做了统一,根据业务模型梳理了几个固定的数据入口。
这样一来,对于应用来说,数据写入方式就更简单,更清晰了,改造后的架构如下图所示。
这个改造对于应用同学的收益是很大的,因为这个架构改造让他们直接感受到:不用修改任何逻辑和代码,数据库层就能够快速实现存储容量和性能的水平扩展。
账单的改进暂时告一段落,我们开始聚焦于数据业务,发现这部分的读请求非常高,读写比例可以达到8:1左右,我们继续架构的改进。
策略4:读写分离扩展
这部分的改进方案相对清晰,我们可以根据业务特点创建多个从库来对读请求做负载均衡。这个时候数据库业务的数据库中依然有大量的存储过程。
所以做读写分离,使用中间件来完成还是存在瓶颈,业务层有自己的中间件方案,所以读写分离的模式是通过存储过程调用查询数据。这虽然不是我们理想中的解决方案,但是它会比较有效,如下图所示。通过这种方式分流了大概50%的查询流量。
现在整体来看,业务的压力都在数据业务方向,有的同学看到这种情况可能会有疑问:为什么不直接把存储过程重构为应用层的SQL呢,在目前的情况下,具有说服力的方案是满足已有的需求,而且目前要业务配合改进还存在一定的困难和风险。我们接下来继续开始演进。
四、业务架构演进阶段
策略5:业务拆分
因为数据业务的压力现在是整个系统的瓶颈,所以一种思路就是先仔细梳理数据业务的情况,我们发现其实可以把数据业务拆分为平台业务和应用业务,平台业务更加统一,是全局的,应用业务相对来说种类会多一些。
做这个拆分对于应用层来说工作量也会少一些,而且也能够快速验证改进效果。改进后的架构如下图所示。
这个阶段的改进可以说是架构演进的一个里程碑,根据模拟测试的结果来看,数据库的QPS指标总体在9万左右,而整体的压力经过估算会是目前的20倍以上,所以毫无疑问,目前的改造是存在瓶颈的,简单来说,就是不具备真实业务的上线条件。
这个时候大家的压力都很大,要打破目前的僵局,目前可见的方案就是对于存储过程逻辑进行改造,这是不得已而为之的事情,也是整个架构改进的关键,这个阶段的改进,我们称之为事务降维。
策略6:事务降维
事务降维的过程是在经过这些阶段的演进之后,整体的业务逻辑脉络已经清晰,改动的过程竟然比想象的还要快很多,经过改进后的方案对原来的大量复杂逻辑校验做了取舍,也经过了反复迭代,最终是基于SQL的调用方案,大家在此的最大顾虑是原来使用存储过程应用层只需要一次请求,而现在的逻辑改造后需要3次请求,可能从数据流量上会带给集群很大的压力,后来经过数据验证这种顾虑消除了。改进后的架构如下图所示,目前已经是完全基于应用层的架构方式了。
在这个基础之上,我们的梳理就进入了快车道,既然改造为应用逻辑的方式已经见效,那么我们可以在梳理现有SQL逻辑的基础上来评估是否可以改造为分布式架构。
从改进后的效果来看,原来的QPS在近40万,而改造后逻辑清晰简单,在2万左右,通过这个过程也让我对架构优化有了新的理解,我们很多时候都是希望能够做得更多,但是反过来却发现能够简化也是一种优化艺术,通过这个阶段的改进之后,大家都充满了信心。
策略7:业务分布式架构改造
这个阶段的演进是我们架构改造的第二个里程碑,这个阶段的改造我们碰到了如下的问题:
- 高并发下的数据主键冲突;
- 业务表数量巨大。
我们逐个说明一下。
1)问题1:高并发下的数据主键冲突和解决方案
业务逻辑中对于数据处理是如下图所示的流程,比如id是主键,我们要修改id=100的用户属性,增加10。
① 检查记录是否存在
select value from user where id=100;
② 如果记录存在,则执行update操作
update user set id=value 10;
③ 如果记录不存在,则执行insert操作
insert into user(id,value) values(100,10)
在并发量很大的情况下,很可能线程1检测数据不存在要执行insert操作的瞬间,线程2已经完成了insert操作,这样一来就很容易抛出主键数据冲突。
对于这个问题的解决方案,我们可以充分使用MySQL的冲突检测功能,即用insert on duplicate update key语法来解决,这个方案从索引维护的角度来看,在基于主键的条件下,其实是不需要索引维护的,而类似的语法replace操作在delete insert的过程中是执行了两条DML,从索引的维护代价来看要高一些。
类似下面的形式:
Insert into acc_data(id,value,mod_date) values(100,10,now()) on duplicate key update value=value 10,mod_date=now();
这种情况不是最完美的,在少数情况下会产生数据的脏读,但是从数据生效的策略来看,我们后续可以在缓存层进行改进,所以这个问题算是基本解决了。
2)问题2:业务表数量巨大
对于业务表数量巨大的问题,在之前账单业务的架构重构中,我们已经有了借鉴的思路。所以我们可以通过配置化的方式提供几个统一的数据入口,比如原来的业务的数据表为:
app1_data,app2_data,app3_data... app500_data,
我们可以简化为一个或者少数访问入口,比如:
app_group1_data(包含app1_data,app2_data... app100_data)
app_group2_data(包含app101_data,app102_data...app200_data),
以此类推。
通过配置化的方式对于应用来说不用关心数据存储的细节,而数据的访问入口可以根据配置灵活定制。
经过类似的方式改进,我们把系统架构统一改造成了三套分布式架构,如下图所示。
在整体改进之后,我们查看现在的QPS,每个分片节点均在5000左右,基本实现了水平扩展,而且从存储容量上来看也是达到了预期的目标,到了这个阶段,整体的架构已经逐步趋于稳定,但是我们是面向业务的架构,还需要做后续的迭代。
五、性能优化阶段
策略8:业务分片逻辑改造
我们通过业务层的检测发现,部分业务处理的延时在10毫秒左右,对于一个高并发的业务来说,这种结果是不能接受的,但是我们已经是分布式架构,要进行优化可以充分利用动态配置来实现。
比如某个数据入口包含10个表数据,其中有个表的数据量过大,导致这个分片的容量过大,这种情况下我们就可以做一下中和,根据业务情况来重构数据,把容量大的表尽可能打散到不同的组中。
通过对数据量较大的表重构,修改分片的数据分布之后,每个分片节点上的文件大小从200M左右降为70M左右,数据容量也控制在100万条以内,下图是其中一个分片节点的系统负载情况,总体按照线上环境的部署情况,单台服务器的性能会控制在一个有效范围之内,整体的性能提升了15%左右,而从业务的反馈来看,读延迟优化到了1毫秒以内,写延迟优化到了4毫秒以内。
后续业务关闭了数据缓存,这样一来所有的查询和写入压力都加在了现有的集群中,从实际的效果来看QPS仅仅增加了不到15%(如下图),而在后续做读写分离时这部分的压力会完全释放。
六、架构里程碑和补充:基于分布式架构的水平扩展方案
至此,我们的分布式集群架构初步实现了业务需求,后续就是数据迁移的方案设计了,3套集群的实例部署架构如下图所示。
在这个基础上需要考虑中间件的高可用,比如现在使用中间件服务,如果其中的一个中间件服务发生异常宕机,那么业务如何保证持续访问,如果我们考虑负载均衡,加入一个代理层,比如使用HAProxy,这就势必带来另外一个问题,代理层的高可用如何保证,所以在这个架构设计中,我们需要考虑得更多是全局的设计。
我们可以考虑使用LVS keepalived的组合方案,经过测试故障转移对于应用层面来说几乎无感知,整个方案的设计如下图所示。
补充要点1:充分利用硬件性能和容量评估
根据上面的分布式存储架构演进和后端的数据监控,我们可以看到整体的集群对于CPU的使用率并不高,而对于IO的需求更大,在这种情况下,我们需要基于硬件来完善我们的IO吞吐量。
在基于SATA-SSD,PCIE-SSD等磁盘资源的测试,我们设定了基于sysbench 的IO压测基准:
- 调度策略:cfq
- 测试用例:oltp_read_write.lua
- 压测表数量:10
- 单表条目数:50000000
- 压测时长:3600s
经过对比测试,整体得到了如下表所示的表格数据。
从以上的数据我们可以分析得到如下图所示的图形。
其中,TPS:QPS大概是1:20,我们对于性能测试情况有了一个整体的认识,从成本和业务需求来看,目前SATA-SSD的资源配置能够完全满足我们的压力场景。
补充要点2:需要考虑的服务器部署架构
对于整体架构设计方案已经具备交付条件,那么线上环境的部署我们还需要设计合理的架构,这个合理主要就是两个边界:
- 满足现有的性能,能够支撑指数级的压力支撑;
- 成本合理。
在这个基础上进行了多次讨论和迭代,我们梳理了如下图所示的服务器部署架构,对于30多个实例,我们最终采用了10台物理服务器来支撑。
从机器的使用成本来说,MySQL的使用场景更偏向于PC服务,但是对于单机来说,CPU、内存、磁盘资源都会存在较大的冗余,所以我们考虑了单机多实例,交叉互备,从而提高资源使用效率,同时节省了大量的服务器资源成本。其中LVS服务可以作为通用的配置资源,故如上资源中无需重复申请。