写点什么

从零到千万用户,我是如何一步步优化 MySQL 数据库的?

用户头像
冰河
关注
发布于: 2020 年 11 月 10 日
从零到千万用户,我是如何一步步优化MySQL数据库的?

写在前面


很多小伙伴留言说让我写一些工作过程中的真实案例,写些啥呢?想来想去,写一篇我在以前公司从零开始到用户超千万的数据库架构升级演变的过程吧。


本文记录了我之前初到一家创业公司,从零开始到用户超千万,系统压力暴增的情况下是如何一步步优化 MySQL 数据库的,以及数据库架构升级的演变过程。升级的过程极具技术挑战性,也从中收获不少。希望能够为小伙伴们带来实质性的帮助。


业务背景


我之前呆过一家创业工作,是做商城业务的,商城这种业务,表面上看起来涉及的业务简单,包括:用户、商品、库存、订单、购物车、支付、物流等业务。但是,细分下来,还是比较复杂的。这其中往往会牵扯到很多提升用户体验的潜在需求。例如:为用户推荐商品,这就涉及到用户的行为分析和大数据的精准推荐。如果说具体的技术的话,那肯定就包含了:用户行为日志埋点、采集、上报,大数据实时统计分析,用户画像,商品推荐等大数据技术。


公司的业务增长迅速,仅仅 2 年半不到的时间用户就从零积累到千万级别,每天的访问量几亿次,高峰 QPS 高达上万次每秒。数据的写压力来源于用户下单,支付等操作,尤其是赶上双十一大促期间,系统的写压力会成倍增长。然而,读业务的压力会远远大于写压力,据不完全统计,读业务的请求量是写业务的请求量的 50 倍左右。


接下来,我们就一起来看看数据库是如何升级的。


最初的技术选型


作为创业公司,最重要的一点是敏捷,快速实现产品,对外提供服务,于是我们选择了公有云服务,保证快速实施和可扩展性,节省了自建机房等时间。整体后台采用的是 Java 语言进行开发,数据库使用的 MySQL。整体如下图所示。


读写分离


随着业务的发展,访问量的极速增长,上述的方案很快不能满足性能需求。每次请求的响应时间越来越长,比如用户在 H5 页面上不断刷新商品,响应时间从最初的 500 毫秒增加到了 2 秒以上。业务高峰期,系统甚至出现过宕机。在这生死存亡的关键时刻,通过监控,我们发现高期峰 MySQL CPU 使用率已接近 80%,磁盘 IO 使用率接近 90%,slow query(慢查询)从每天 1 百条上升到 1 万条,而且一天比一天严重。数据库俨然已成为瓶颈,我们必须得快速做架构升级。


当 Web 应用服务出现性能瓶颈的时候,由于服务本身无状态,我们可以通过加机器的水平扩展方式来解决。 而数据库显然无法通过简单的添加机器来实现扩展,因此我们采取了 MySQL 主从同步和应用服务端读写分离的方案。


MySQL 支持主从同步,实时将主库的数据增量复制到从库,而且一个主库可以连接多个从库同步。利用此特性,我们在应用服务端对每次请求做读写判断,若是写请求,则把这次请求内的所有 DB 操作发向主库;若是读请求,则把这次请求内的所有 DB 操作发向从库,如下图所示。



实现读写分离后,数据库的压力减少了许多,CPU 使用率和 IO 使用率都降到了 5%以内,Slow Query(慢查询)也趋近于 0。主从同步、读写分离给我们主要带来如下两个好处:


  • 减轻了主库(写)压力:商城业务主要来源于读操作,做读写分离后,读压力转移到了从库,主库的压力减小了数十倍。


  • 从库(读)可水平扩展(加从库机器):因系统压力主要是读请求,而从库又可水平扩展,当从库压力太时,可直接添加从库机器,缓解读请求压力。


当然,没有一个方案是万能的。读写分离,暂时解决了 MySQL 压力问题,同时也带来了新的挑战。业务高峰期,用户提交完订单,在我的订单列表中却看不到自己提交的订单信息(典型的 read after write 问题);系统内部偶尔也会出现一些查询不到数据的异常。通过监控,我们发现,业务高峰期 MySQL 可能会出现主从复制延迟,极端情况,主从延迟高达数秒。这极大的影响了用户体验。


那如何监控主从同步状态?在从库机器上,执行 show slave status,查看 SecondsBehindMaster 值,代表主从同步从库落后主库的时间,单位为秒,若主从同步无延迟,这个值为 0。MySQL 主从延迟一个重要的原因之一是主从复制是单线程串行执行(高版本 MySQL 支持并行复制)。


那如何避免或解决主从延迟?我们做了如下一些优化:


  • 优化 MySQL 参数,比如增大 innodbbufferpool_size,让更多操作在 MySQL 内存中完成,减少磁盘操作。

  • 使用高性能 CPU 主机。

  • 数据库使用物理主机,避免使用虚拟云主机,提升 IO 性能。

  • 使用 SSD 磁盘,提升 IO 性能。SSD 的随机 IO 性能约是 SATA 硬盘的 10 倍甚至更高。

  • 业务代码优化,将实时性要求高的某些操作,强制使用主库做读操作。

  • 升级高版本 MySQL,支持并行主从复制。


垂直分库


读写分离很好的解决了读压力问题,每次读压力增加,可以通过加从库的方式水平扩展。但是写操作的压力随着业务爆发式的增长没有得到有效的缓解,比如用户提交订单越来越慢。通过监控 MySQL 数据库,我们发现,数据库写操作越来越慢,一次普通的 insert 操作,甚至可能会执行 1 秒以上。


另一方面,业务越来越复杂,多个应用系统使用同一个数据库,其中一个很小的非核心功能出现延迟,常常影响主库上的其它核心业务功能。这时,主库成为了性能瓶颈,我们意识到,必需得再一次做架构升级,将主库做拆分,一方面以提升性能,另一方面减少系统间的相互影响,以提升系统稳定性。这一次,我们将系统按业务进行了垂直拆分。如下图所示,将最初庞大的数据库按业务拆分成不同的业务数据库,每个系统仅访问对应业务的数据库,尽量避免或减少跨库访问。



垂直分库过程,我们也遇到不少挑战,最大的挑战是:不能跨库 join,同时需要对现有代码重构。单库时,可以简单的使用 join 关联表查询;拆库后,拆分后的数据库在不同的实例上,就不能跨库使用 join 了。


例如,通过商家名查询某个商家的所有订单,在垂直分库前,可以 join 商家和订单表做查询,也可以直接使用子查询,如下如示:


select * from tb_order where supplier_id in (select id from supplier where name=’商家名称’);
复制代码


分库后,则要重构代码,先通过商家名查询商家 id,再通过商家 id 查询订单表,如下所示:


select id from supplier where name=’商家名称’select * from tb_order where supplier_id in (supplier_ids )
复制代码


垂直分库过程中的经验教训,使我们制定了 SQL 最佳实践,其中一条便是程序中禁用或少用 join,而应该在程序中组装数据,让 SQL 更简单。一方面为以后进一步垂直拆分业务做准备,另一方面也避免了 MySQL 中 join 的性能低下的问题。


经过近十天加班加点的底层架构调整,以及业务代码重构,终于完成了数据库的垂直拆分。拆分之后,每个应用程序只访问对应的数据库,一方面将单点数据库拆分成了多个,分摊了主库写压力;另一方面,拆分后的数据库各自独立,实现了业务隔离,不再互相影响。


水平分库


读写分离,通过从库水平扩展,解决了读压力;垂直分库通过按业务拆分主库,缓存了写压力,但系统依然存在以下隐患:


  • 单表数据量越来越大。如订单表,单表记录数很快就过亿,超出 MySQL 的极限,影响读写性能。

  • 核心业务库的写压力越来越大,已不能再进一次垂直拆分,此时的系统架构中,MySQL 主库不具备水平扩展的能力。


此时,我们需要对 MySQL 进一步进行水平拆分。


水平分库面临的第一个问题是,按什么逻辑进行拆分。一种方案是按城市拆分,一个城市的所有数据在一个数据库中;另一种方案是按订单 ID 平均拆分数据。按城市拆分的优点是数据聚合度比较高,做聚合查询比较简单,实现也相对简单,缺点是数据分布不均匀,某些城市的数据量极大,产生热点,而这些热点以后可能还要被迫再次拆分。按订单 ID 拆分则正相反,优点是数据分布均匀,不会出现一个数据库数据极大或极小的情况,缺点是数据太分散,不利于做聚合查询。比如,按订单 ID 拆分后,一个商家的订单可能分布在不同的数据库中,查询一个商家的所有订单,可能需要查询多个数据库。针对这种情况,一种解决方案是将需要聚合查询的数据做冗余表,冗余的表不做拆分,同时在业务开发过程中,减少聚合查询。


经过反复思考,我们最后决定按订单 ID 做水平分库。从架构上,将系统分为三层:


  • 应用层:即各类业务应用系统

  • 数据访问层:统一的数据访问接口,对上层应用层屏蔽读写分库、分表、缓存等技术细节。

  • 数据层:对 DB 数据进行分片,并可动态的添加 shard 分片。


水平分库的技术关键点在于数据访问层的设计,数据访问层主要包含三部分:


  • 分布式缓存

  • 数据库中间件

  • 数据异构中间件


而数据库中间件需要包含如下重要的功能:


  • ID 生成器:生成每张表的主键

  • 数据源路由:将每次 DB 操作路由到不同的分片数据源上


ID 生成器


ID 生成器是整个水平分库的核心,它决定了如何拆分数据,以及查询存储-检索数据。ID 需要跨库全局唯一,否则会引发业务层的冲突。此外,ID 必须是数字且升序,这主要是考虑到升序的 ID 能保证 MySQL 的性能(若是 UUID 等随机字符串,在高并发和大数据量情况下,性能极差)。同时,ID 生成器必须非常稳定,因为任何故障都会影响所有的数据库操作。


我们系统中 ID 生成器的设计如下所示。


  • 整个 ID 的二进制长度为 64 位

  • 前 36 位使用时间戳,以保证 ID 是升序增加

  • 中间 13 位是分库标识,用来标识当前这个 ID 对应的记录在哪个数据库中

  • 后 15 位为自增序列,以保证在同一秒内并发时,ID 不会重复。每个分片库都有一个自增序列表,生成自增序列时,从自增序列表中获取当前自增序列值,并加 1,做为当前 ID 的后 15 位

  • 下一秒时,后 15 位的自增序列再次从 1 开始。


水平分库是一个极具挑战的项目,我们整个团队也在不断的迎接挑战中快速成长。


为了适应公司业务的不断发展,除了在 MySQL 数据库上进行相应的架构升级外,我们还搭建了一套完整的大数据实时分析统计平台,在系统中对用户的行为进行实时分析。


关于如何搭建大数据实时分析统计平台,对用户的行为进行实时分析,我们后面再详细介绍。


好了,今天就到这儿吧,我是冰河,我们下期见!!


重磅福利


微信搜一搜【冰河技术】微信公众号,关注这个有深度的程序员,每天阅读超硬核技术干货,公众号内回复【PDF】有我准备的一线大厂面试资料和我原创的超硬核 PDF 技术文档,以及我为大家精心准备的多套简历模板(不断更新中),希望大家都能找到心仪的工作,学习是一条时而郁郁寡欢,时而开怀大笑的路,加油。如果你通过努力成功进入到了心仪的公司,一定不要懈怠放松,职场成长和新技术学习一样,不进则退。如果有幸我们江湖再见!


另外,我开源的各个 PDF,后续我都会持续更新和维护,感谢大家长期以来对冰河的支持!!


写在最后


如果你觉得冰河写的还不错,请微信搜索并关注「 冰河技术 」微信公众号,跟冰河学习高并发、分布式、微服务、大数据、互联网和云原生技术,「 冰河技术 」微信公众号更新了大量技术专题,每一篇技术文章干货满满!不少读者已经通过阅读「 冰河技术 」微信公众号文章,吊打面试官,成功跳槽到大厂;也有不少读者实现了技术上的飞跃,成为公司的技术骨干!如果你也想像他们一样提升自己的能力,实现技术能力的飞跃,进大厂,升职加薪,那就关注「 冰河技术 」微信公众号吧,每天更新超硬核技术干货,让你对如何提升技术能力不再迷茫!



发布于: 2020 年 11 月 10 日阅读数: 53
用户头像

冰河

关注

公众号:冰河技术 2020.05.29 加入

Mykit系列开源框架发起者、核心架构师和开发者,《海量数据处理与大数据技术实战》与《MySQL开发、优化与运维实战》作者。【冰河技术】微信公众号作者。

评论

发布
暂无评论
从零到千万用户,我是如何一步步优化MySQL数据库的?