高性能MySQL架构

2,538 阅读10分钟

关注公众号:xy的技术圈

在前面的文章里,分享了MySQL索引的原理及使用技巧、MySQL查询语句的优化等方面的知识。这些都是针对单个库的性能优化。在微服务和容器、云的时代,应用层可以很方便地水平扩展,用于支撑更大的并发量。

大多数开发人员都知道,数据库是性能上比较大的一个瓶颈。所以如何架构和设计数据库,使它能够支持高并发,就变得非常重要。本文主要介绍MySQL的一些高性能架构方面的知识点。比如主从、读写分离、分库分表等。

虽然MySQL原生并不是原生的分布式数据库,需要客户端或者数据库中间件配合来使用集群。但学习了解它们的原理也是非常重要的,因为同样的原理也可以用于其它厂商的关系型数据库。对自己理解分布式架构也有很大的帮助。

下面以一个小应用的规模发展来一步步介绍这个应用的MySQL架构演变。

单体应用和单体数据库

最开始,应用比较小,访问量比较小。事实上绝大多数应用的访问量都是一开始比较小,后来再慢慢扩展增加的。

这个时候所有的请求都到一个应用,然后到同一个数据库:

单体

在这个阶段,如果并发量缓慢增加,可以暂时通过增加硬件性能来解决。比如更高性能的CPU、更多的内存等。

分布式应用和读写分离

随着业务的扩展,系统的并发量越来越大,原有的单体应用和单体数据的架构已经不能很好地支撑现在的并发量了。这个时候需要把应用横向扩展成分布式的应用。但他们最终都是要持久化到数据库的,所以数据库也需要作出一定的调整来支撑当前并发量。

正常的业务流程,对数据库的操作无非是两种情况,读和写。从前面介绍的文章可以了解到,如果建立索引可以显著提高读的性能,但会影响写的性能。而在大多数互联网项目中,读操作一般是遵循写操作的。由二八定律来假设,写操作大约占20%,读操作大约占80%。这个时候如果把作用的读操作和写操作全部都请求到同一个数据库,数据库的压力是比较大的。所以我们可以对数据库做读写分离架构:

主从同步

可以根据自己的需求,配置多个从数据库。

这个时候需要做主从同步,即把主库的数据,同步到从库上。在MySQL中,这个过程叫做“复制”。

MySQL复制原理

MySQL是基于二进制日志bin log来进行复制的。Master服务器将数据的改变写进二进制日志中,主库发送更新事件到从库。

收到事件后,Slave会开启一个I/O线程。I/O线程会在Master上开启一个普通连接,然后Master开启一个dump线程来配合做复制操作。

I/O线程读取Master上面的bin log到Slave结点的中继日志relay log里。然后Slave会开启一个SQL线程,用于从中继日志读取事件,并重放其中的事件而更新slave的数据,使其与master中的数据一致。只要该线程与I/O线程保持一致,中继日志通常会位于操作系统的缓存中,所以中继日志的开销很小。

MySQL支持以下三种复制方式:

  • 基于语句的复制:优点是实现简单,日志数据量也比较小,占用带宽少。缺点是可能有些数据可能会出问题,比如主从的时区不同、存储过程和触发器不同等。
  • 基于行的复制:优点是对任何数据都能正常工作,缺点是二进制日志可能会比较大。
  • 混合模式:默认采用基于语句的复制,如果发现基于语句无法精确复制时,就采用基于行的复制。

MySQL的复制支持很多种模式,包括“主-主复制”等,但实际使用时还是建议使用一主多从的模式,双主架构可能会有ID冲突或事务等问题。

需要注意的是从数据库仍然会有写压力的。主从架构只能分担读压力,但不能分担写压力。因为所有的写操作最终都要同步到所有的从节点,所以如果写操作过多,不仅会影响主库的性能,也会影响从库的性能。

那如果主库不能支撑所有的写操作了怎么办呢?这个时候就要使用分库分表了。

分库分表

前面提到如果写操作成为性能瓶颈,那读写分离并不能很好的解决这个问题,可以用分库分表来做。分库分表在一定程度上可以解决写操作和读操作的性能瓶颈,但也会为写操作和读操作带来更多的复杂性。

数据库分布式核心内容无非就是数据切分(Sharding),以及切分后对数据的定位、整合。数据切分就是将数据分散存储到多个数据库中,使得单一数据库中的数据量变小,通过扩充主机的数量缓解单一数据库的性能问题,从而达到提升数据库操作性能的目的。

如果一个表过大,有两种切分方式——垂直切分和水平切分。垂直切分即把一些列切分出去,在微服务场景下,其实我们的数据库一般只为一个微服务服务,相当于已经切分到比较契合业务了。所以基本上不用考虑垂直切分。

而水平切分有两种方式:

  • 库内分表:即把一个表切分成多个表,但仍然放在同一个数据库。比如按照id范围或者按照时间范围来切分。这种方式适用于热点数据的场景,比如“朋友圈”,一般近期的新数据访问量比较大,而较久的数据访问量比较小。这样就可以基于时间范围来切分。
  • 分库分表:分库分表即把一个表切分到不同的数据库,这些数据库一般是在不同的节点上。也就形成了我们常说的“分布式数据库”。

下面介绍两种分库分表的切分方式。第一种是像库内分表一样,按照id的取值范围或者时间区间来切分。同样适用于热点数据,并且天然便于水平扩展,后期如果想对整个分片集群扩容时,只需要添加节点即可,无需对其他分片的数据进行迁移。

另一种是根据id来取模,一般是使用一致性hash算法来做,可以较好地支持水平扩展。这种方式数据分配比较均匀,不容易出现热点和并发访问的瓶颈,适用于大多数业务场景。

分库分表可以跟主从配合,每个分片都可以使用主从来缓解读压力。

分库分表带来的问题

分库分表以后,会下面的一些问题:

事务

当更新内容同时分布在不同库中,不可避免会带来跨库事务问题。可使用主流的分布式事务解决方案,比如XA协议、两阶段提交等。分布式事务能最大限度保证了数据库操作的原子性。但在提交事务时需要协调多个节点,推后了提交事务的时间点,延长了事务的执行时间。导致事务在访问共享资源时发生冲突或死锁的概率增高。随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面上水平扩展的枷锁。

对于强一致性要求不那么高的数据,可以只需要保证最终一致性即可。

join

分库分表后,join操作将变得非常麻烦。下面列举一些比较常见的解决方案:

  • 全局表:也就是系统中所有模块都可能依赖的一些表,为了避免跨库join查询,每个数据库都保存一份,这些数据通常很少会进行修改,所以也不担心一致性的问题。

  • 反范式设计:用空间换时间,数据冗余。可以避免join查询,但可能会带来数据同步的问题。比如卖家修改了名字,需要同步到订单表里吗?这个根据实际的业务场景来考虑是否使用反范式设计。

  • 数据组装:在应用层面来组装数据,第一次查询结果找到关联的id,在应用层用这个id去二次请求得到关联的数据,然后在应用层拼装数据。

  • ER分片:简单来说,就是把存在关联关系的记录放在同一个节点上,这样就能局部join,避免了跨分配join。这个时候关联表在和主表1:1或者n:1的情况下,通常按照主表的id主键进行切分。

分页、排序、和函数

这种情况下,需要先在不同的分片节点中将数据进行排序并返回,然后将不同分片返回的结果集进行汇总和再次排序,最终返回给用户。

这个事情在应用层来做比较麻烦,而MySQL数据库也不能很好的支持,所以比较适合使用插件或者数据库中间件来做这个事情。

主键冲突

在分库分表环境中,由于表中数据同时存在不同数据库中,主键值平时使用的自增长将无用武之地,某个分区数据库自生成的ID无法保证全局唯一。因此需要单独设计全局主键,以避免跨库主键冲突问题。有一些常见的主键生成策略:

  • UUID:UUID实现简单,但缺点也比较明显,由于UUID非常长,会占用大量的存储空间;另外,作为主键建立索引和基于索引进行查询时都会存在性能问题,在InnoDB下,UUID的无序性会引起数据位置频繁变动,导致分页。
  • 分布式ID生成方案:市面上有很多分布式ID的生成方案,比如Twitter的snowflake算法、美团的Leaf、百度的UidGenerator、Redis生成ID等方式。

解决方案

上面介绍了一个关系型数据库的架构演变。对于MySQL来说,原生对分布式的支持并不是很好,所以我们可能需要使用一些数据库中间件来辅助。比如MyCat、Atlas等。笔者个人对Sharding-JDBC(现已升级为Sharding-Sphere项目)比较感兴趣,后续可能会学习研究一下这个工具。

除此之外,还有一些原生即支持分布式的数据库,比如TiDB。TiDB兼容MySQL的语法,可以很方便地从现有的MySQL数据库迁移到TiDB。

另外主流的云平台也提供了数据库扩展方面的支持。比如AWS、阿里云等云数据库RDS,就可以很方便地配置主从、分库分表等。

认真写文章,用心做分享。

个人网站:yasinshaw.com

公众号:xy的技术圈