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

简介: 很多小伙伴留言说让我写一些工作过程中的真实案例,写些啥呢?想来想去,写一篇我在以前公司从零开始到用户超千万的数据库架构升级演变的过程吧。本文记录了我之前初到一家创业公司,从零开始到用户超千万,系统压力暴增的情况下是如何一步步优化MySQL数据库的,以及数据库架构升级的演变过程。升级的过程极具技术挑战性,也从中收获不少。希望能够为小伙伴们带来实质性的帮助。

业务背景

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

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

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

最初的技术选型

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

微信图片_20211120125155.jpg

读写分离

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

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

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

微信图片_20211120125202.jpg

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

  • 减轻了主库(写)压力:商城业务主要来源于读操作,做读写分离后,读压力转移到了从库,主库的压力减小了数十倍。
  • 从库(读)可水平扩展(加从库机器):因系统压力主要是读请求,而从库又可水平扩展,当从库压力太时,可直接添加从库机器,缓解读请求压力。

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

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

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

  • 优化MySQL参数,比如增大innodb_buffer_pool_size,让更多操作在MySQL内存中完成,减少磁盘操作。
  • 使用高性能CPU主机。
  • 数据库使用物理主机,避免使用虚拟云主机,提升IO性能。
  • 使用SSD磁盘,提升IO性能。SSD的随机IO性能约是SATA硬盘的10倍甚至更高。
  • 业务代码优化,将实时性要求高的某些操作,强制使用主库做读操作。
  • 升级高版本MySQL,支持并行主从复制。

垂直分库

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

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

微信图片_20211120125211.jpg

垂直分库过程,我们也遇到不少挑战,最大的挑战是:不能跨库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进一步进行水平拆分。

微信图片_20211120125219.jpg

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

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

  • 应用层:即各类业务应用系统
  • 数据访问层:统一的数据访问接口,对上层应用层屏蔽读写分库、分表、缓存等技术细节。
  • 数据层:对DB数据进行分片,并可动态的添加shard分片。

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

  • 分布式缓存
  • 数据库中间件
  • 数据异构中间件

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

  • ID生成器:生成每张表的主键
  • 数据源路由:将每次DB操作路由到不同的分片数据源上

ID生成器

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

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

微信图片_20211120125230.jpg

  • 整个ID的二进制长度为64位
  • 前36位使用时间戳,以保证ID是升序增加
  • 中间13位是分库标识,用来标识当前这个ID对应的记录在哪个数据库中
  • 后15位为自增序列,以保证在同一秒内并发时,ID不会重复。每个分片库都有一个自增序列表,生成自增序列时,从自增序列表中获取当前自增序列值,并加1,做为当前ID的后15位
  • 下一秒时,后15位的自增序列再次从1开始。

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

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

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

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

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。   相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情: https://www.aliyun.com/product/rds/mysql 
相关文章
|
2月前
|
存储 人工智能 NoSQL
AI大模型应用实践 八:如何通过RAG数据库实现大模型的私有化定制与优化
RAG技术通过融合外部知识库与大模型,实现知识动态更新与私有化定制,解决大模型知识固化、幻觉及数据安全难题。本文详解RAG原理、数据库选型(向量库、图库、知识图谱、混合架构)及应用场景,助力企业高效构建安全、可解释的智能系统。
|
6月前
|
关系型数据库 MySQL 数据库连接
Django数据库配置避坑指南:从初始化到生产环境的实战优化
本文介绍了Django数据库配置与初始化实战,涵盖MySQL等主流数据库的配置方法及常见问题处理。内容包括数据库连接设置、驱动安装、配置检查、数据表生成、初始数据导入导出,并提供真实项目部署场景的操作步骤与示例代码,适用于开发、测试及生产环境搭建。
274 1
|
6月前
|
SQL 缓存 关系型数据库
MySQL 慢查询是怎样优化的
本文深入解析了MySQL查询速度变慢的原因及优化策略,涵盖查询缓存、执行流程、SQL优化、执行计划分析(如EXPLAIN)、查询状态查看等内容,帮助开发者快速定位并解决慢查询问题。
266 0
|
2月前
|
SQL 存储 监控
SQL日志优化策略:提升数据库日志记录效率
通过以上方法结合起来运行调整方案, 可以显著地提升SQL环境下面向各种搜索引擎服务平台所需要满足标准条件下之数据库登记作业流程综合表现; 同时还能确保系统稳健运行并满越用户体验预期目标.
212 6
|
3月前
|
缓存 Java 应用服务中间件
Spring Boot配置优化:Tomcat+数据库+缓存+日志,全场景教程
本文详解Spring Boot十大核心配置优化技巧,涵盖Tomcat连接池、数据库连接池、Jackson时区、日志管理、缓存策略、异步线程池等关键配置,结合代码示例与通俗解释,助你轻松掌握高并发场景下的性能调优方法,适用于实际项目落地。
607 5
|
4月前
|
缓存 关系型数据库 MySQL
降低MySQL高CPU使用率的优化策略。
通过上述方法不断地迭代改进,在实际操作中需要根据具体场景做出相对合理判断。每一步改进都需谨慎评估其变动可能导致其他方面问题,在做任何变动前建议先在测试环境验证其效果后再部署到生产环境中去。
225 6
|
5月前
|
机器学习/深度学习 SQL 运维
数据库出问题还靠猜?教你一招用机器学习优化运维,稳得一批!
数据库出问题还靠猜?教你一招用机器学习优化运维,稳得一批!
175 4
|
5月前
|
存储 SQL 关系型数据库
MySQL 核心知识与索引优化全解析
本文系统梳理了 MySQL 的核心知识与索引优化策略。在基础概念部分,阐述了 char 与 varchar 在存储方式和性能上的差异,以及事务的 ACID 特性、并发事务问题及对应的隔离级别(MySQL 默认 REPEATABLE READ)。 索引基础部分,详解了 InnoDB 默认的 B+tree 索引结构(多路平衡树、叶子节点存数据、双向链表支持区间查询),区分了聚簇索引(数据与索引共存,唯一)和二级索引(数据与索引分离,多个),解释了回表查询的概念及优化方法,并分析了 B+tree 作为索引结构的优势(树高低、效率稳、支持区间查询)。 索引优化部分,列出了索引创建的六大原则
144 2
|
5月前
|
存储 SQL 关系型数据库
MySQL 动态分区管理:自动化与优化实践
本文介绍了如何利用 MySQL 的存储过程与事件调度器实现动态分区管理,自动化应对数据增长,提升查询性能与数据管理效率,并详细解析了分区创建、冲突避免及实际应用中的关键注意事项。
221 0

推荐镜像

更多