前言:本人有幸接手了一个大项目的运维工作,项目前期发展非常快,其中尤以数据库的发展最为困难,目前大的单表都是在几十亿,最大的达到百亿,高峰时数据流量超1000Mbps。这里还是要首先感谢mycat社区提供的开源中间件,让我能够实现一个如此庞大的数据库。这期间我们踩了无数坑,对数据库架构进行了多次调整,mysql、redis更是优化再优化(redis集群20万qps以上,这个以后有时间再写个分享),巴不得榨干机器的所有性能。整个过程大多数时候都是困难重重,压力山大,但现在回头去看,一路收获很多。现在整理出来一方面是给自己回顾总结,重新思考,另一方面也是给大家做一个案例参考。(以下的图都是自己画的,平时画的少,所以很多图标我也搞不清楚应该用哪种形状)
我现在整理的是这个项目的mysql数据库架构的一点点演变改进过程,每一步的演变其实都会涉及很多工作,比如数据迁移,拆分,程序修改等等。
一、最初的架构:主从结构
我进项目组的时候,项目已经刚起步,用户只有几万,服务器也就10台左右,其中mysql数据库只有2台,使用了最简单的一主一从结构。在我来之前使用的memory引擎,据说有一次机房停电,两台服务器全挂,数据全丢,于是改成了innodb引擎。我觉得应该感谢这次及时的停电,因为还是在项目之初,损失还能承受,如果在后面那是不可想象的。
我来的第一步是给数据库最备份,然后是搭建了监控系统nagios和cacti,后来又增加了zabbix。有了基础的监控,后面问题才能及时发现,有性能数据我们才能找到瓶颈,知道优化方向。二、引进mycat,数据横向拆分
最初用户发展非常快,数据量只能用暴增来形容,每天的高峰期数据库经常抗不住,直接导致业务不可用。每天的业务高峰时间非常集中(这个是业务特性,我们没有办法去左右用户的行为),这种不可用的时间可能就10分钟,但对业务这足够致命了。于是我们决定对数据库进行拆分,这期间我们测试分析了几种中间件,最后选择了mycat。也是架构开始演变成这样:
因为已经是上线的业务,所以数据库的拆分即紧急重要,又需要非常小心,这不是我一个人的事情,还需要牵扯到开发的修改。这里必须要感谢下自己的老板能够支持并授权给我去做,事实上推进的过程还是踩了一些坑。这里可以跟大家分享下重点内容和我们遇到的问题:
1、数据拆分规则
这很重要,直接关系到所有数据的存储结构,程序修改,以及然后的扩展。数据拆分主要还是要按照业务逻辑来做,比如我们是按照用户来分,同一个用户的所有数据都在同一个分片上,小的配置表做全局表,这样可以避免跨库关联的问题。分片规则我们采用的是先分组再取模的方式,这样的好处是每次分片满了之后可以不用拆分老数据,新分片分布在新老多台服务器上,也可以进行压力均衡。
2、自增序列带来的问题
mycat社区和文档说明中有多种方法实现自增序列的管理,我们选择的是数据库存储过程的那种(这个可以去参考mycat文档),看起来似乎很完美,但我们还是要小心。
首先我们是在已有单库的系统中演变过来的,所以要考虑一下同样是自增序列在程序中是怎么被使用的,这个还真得看看开发是怎么写的。
举个例子:我们的分片规则主要按照用户id来分(用户表的主键,数值型,自增,所有用户相关数据表的关联外键),而用户实际使用的是账号(字符型,给用户用来登陆的,所以这个建了唯一索引)。对于用户是新用户还是老用户,我们的开发同事是这么写的:insert ...on duplicate key update ... ,直接插入这个账号到用户表中,如果已存在的直接返回来的id,如果不存在的则插入新的数据并获得新的id。单库下一点问题都没有,在分库中之后就是大问题,因为mycat的过程是先给了一个新的id,然后带着这个id去插入到数据库中,分片是按照id来分的,他很有可能是分到其他的分库上,最后插入成功。于是这个账号就有条2个用户记录。就上线了一会儿,我们清洗数据花了一个晚上。
解决的方案:用户的账号必须是要保持唯一性的,这点绝对不可能让步。于是有人说先查询一次再插入,这其实还是不行的。只要你不做互斥,就有可能产生两条相同的账号。
于是我给的方案是生成两张用户表,一张是全量表account_s,用来存放用户的账号,同时再生成一张分片的account_s_shard.程序必须先项account_s中插入,插入成功的才算新用户,并获得一个id,然后再插入一次到account_s_shard。
三、引入redis缓存
随着用户增长,并发量越来越高,即使拆分数据,依然还有很多问题
1、账号到id的转换问题
用户使用的是账号,数据库基本上关联表都用的是id,包括分库的规则也是id。
这里就需要每次用户来使用的时候就必须被转换一次,实际上每个组件程序中都至少要查询一次。
用户表已经被我们做了两张表account_s和account_s_shard,最初我们使用的是account_s_shard这个表,但是因为账号不是分片键,所以他会到每一个分库中执行一次。这个其实是非常厉害的。比如我们一个mysql实例上分10个分库,那么每一次查询在这个mysql上就会被放大10倍,总共100多个片库,就会放大100多倍。可想而知这个量是非常惊人的。
在业务低谷时,我们单个mysql实例上只抓了5分钟左右的时间,就能看到接近200万次的查询。即使他的速度很快,高峰时他占的资源还是非常大。
假设我们全部移到account_s上,但这个是单库,压力也会很大。
所以我们最后还是决定引入redis缓存,在缓存中解决这个问题。
2、流量集中,需要redis缓存进行削锋填谷
每种业务都自己的业务特征,所以我们谈架构不能离开业务场景特性。我们的业务不能使用读写分离,因为程序间数据传递时间非常短,数据一点延时据有可能出现问题。另一个特征是是业务高峰非常集中在2个时间点,一个是早上9:20到10:00,这段时间是写入的高峰,下午14:50到15:10 ,这段时间是读的高峰,高峰值会一下子拉高几倍甚至十倍。
我们当然可以按照最高的峰值进行预算扩容,但是扩容数据库是非常昂贵的,所以我们也需要通过redis进行缓存,一是写缓存,数据延后写入,另外就是读缓存,缓存热点数据。
我们现在的架构就变成了这样:
这一步的工作量还是非常大的,主要还是涉及到开发的程序,因为有部分数据在redis中进行了缓存,就意味着我们必须是redis+mysql合并才是完整的数据。读写都需要修改。redis与数据库之间的数据同步也是一个非常重要的步骤。
四、mycat集群
从上面的架构可以看到,mycat只有一台,在服务上也是非常容易成为瓶颈的。
我们发现当数据量非常的查询语句时(比如我们要计算用户的排名,一次查询会产生大量的数据,我们要避免这样的大sql,但总是很难做到完全的预防),这个时候mycat会非常容易出现挂起,这个主要还是需要占用太多的内存。于是我们需要搞一个守护进程,这样万一挂起了能自动解决。
而单一的mycat,在高并发的情况下不仅会成为瓶颈,而且单点存在也是非常危险的。
于是我们还需要搭建一套mycat的集群。于是就出现了下面这个mycat的集群结构。
一开始我们使用的是haproxy,但是haproxy很快就出现了问题,因为haproxy需要流量转发,于是haproxy就会成为瓶颈。如下图所示,我们在高峰期流量会超过千兆,直接把我们的千兆网卡打爆:
当然我们可以多用几个haproxy,但这样就不能使用vip了,对应用程序我们就需要部署不同的ip,而且这样失去了高可用方案。所以我们把他改成了LVS方式,这样就不会让代理节点(入口)的流量打爆。
也许有一天单个LVS也会被打满,那时候也许不得不多开几个入口了。
五、mysql高可用方案
架构基本上就是这样,keepalived+lvs+mycat 基本上实现了mycat这一层的高可用和负载均衡。单从架构上我们还没有说明mysql的高可用。mycat后面连接的每一个mysql实例,我们都需要建立高可用的方案。方案当然很多,我们选择的是mha方式。
每一个mysql都是一主一从(我很希望能有2从,不过受限于服务器资源,我们现在普遍的都是一主一从),然后再做一个日志服务器。为防止检测中出现误判,采用2台应用服务器作为旁路检测。
在数据库这一层,我们没有通过vip方式,这是因为vip方式需要使用相同的端口号,而我们mysql实例很多,从库很多都在一台服务器上,所以端口是没有办法统一的,所以采用vip方式不能满足我们的需求。
mha在切换的时候调用的是我们自己的python程序:修改mycat配置,然后分发到各台mycat,重启生效。
以上是整个架构改变的一个过程,这中间还涉及到无数的mysql调优,扩容,版本升级,数据迁移、拆分等等,另外也还有redis集群的调优(现在20万qps以上)。这些真是一言难尽。
所以mysql数量多了之后,我们如何去管理的问题被提上日程。后面我们再分享一下管理。
|