现在有一个架构图如下所示:
图片
在这种情况下,咱们的数据库仍然是单机部署。根据一些云厂商的基准测试结果,使用4核8GB的机器运行MySQL 5.7时,大约可以支持每秒500个事务(TPS)和每秒10,000个查询(QPS)。然而,现在运营团队正在准备双十一活动,并且公司计划在全渠道推广中继续投资,这显然会导致查询量突然增加的问题。因此,今天我们将探讨如何通过主从分离来应对查询请求的增加。
主从读写分离
实际上,许多系统都面临着读取操作远多于写入操作的情况,读写请求的数量差距可能达到几个数量级。
这并不难理解,因为浏览朋友圈的请求肯定比发布朋友圈的请求多,而在线商店中,一个商品的浏览量肯定远远超过下单量。因此,我们首先考虑如何使数据库能够处理更多的查询请求,而为此,首先需要将读取流量和写入流量分开,这就是我们所说的主从读写分离。
实际上,这是一种流量分流的策略,可以将其比作道路交通控制,其中一个有四个车道的主干道中有三个车道供领导和外宾使用,而另一个车道供一般市民使用,以确保领导和外宾能够优先通过。这个策略是一种常规做法,即使在大型项目中,也是应对数据库突发读取流量增加的有效方法。
在我目前的项目中,我们曾经遇到前端流量突然增加导致从数据库副本负载过高的问题,数据库管理员首先扩展了从数据库,这样读取流量就分散到多个从数据库上,从而降低了从数据库的负载。接下来,开发团队再考虑如何在数据库层之上采取措施来处理这一流量。
主从读写的两个技术关键点
通常,在主从读写分离机制中,我们复制一个数据库的数据到一个或多个其他数据库服务器上,并将原始数据库称为主库,其主要任务是处理数据写入操作,而复制到的目标数据库称为从库,主要用于支持数据查询操作。主从读写分离涉及两个关键技术点:首先,数据的拷贝,这称为主从复制;其次,在主从分离的情况下,如何屏蔽应用访问数据库时的变化,以使开发人员感觉就像在使用单一数据库一样。
1. 主从复制
让我用不同的表达方式来解释MySQL的主从复制过程。MySQL的主从复制是依赖于二进制日志(binlog)的技术。二进制日志记录了MySQL上的所有数据变化,并以二进制形式保存在磁盘上的日志文件中。主从复制的目标是将主库中的binlog数据传输到从库上,通常这个过程是异步的,也就是说,主库上的操作不需要等待binlog同步完成。
主从复制的过程如下:首先,从库连接到主库时会创建一个I/O线程,用于请求主库上的binlog数据,并将接收到的binlog信息写入一个名为"relay log"的日志文件中。与此同时,主库也会创建一个"log dump"线程来将binlog数据发送给从库。此外,从库还会创建一个SQL线程,用于读取"relay log"中的内容,并在从库上进行回放,从而实现主从库的数据一致性。
这种主从复制方式的优点在于,它使用了独立的"log dump"线程,这是一种异步方式,避免了对主库的主要数据更新流程产生影响。此外,从库在接收到binlog信息后,并不会直接写入从库的实际存储中,而是将数据写入"relay log",这可以避免写入从库存储的延迟,从而减少主从库之间的数据同步延迟。
图片
主从复制是一种在性能考虑下实现的数据库同步机制,其中主库负责数据写入,而从库负责数据读取。主库的写入操作通常不会等待主从同步完成就返回结果,这意味着在极端情况下,如主库上的binlog数据还未来得及刷新到磁盘时发生磁盘损坏或机器掉电,可能会导致binlog数据丢失,进而引起主从数据不一致的问题。尽管这种情况出现的概率很低,对于互联网项目来说是可以接受的。
一旦进行了主从复制,就可以实现写请求只写主库,读请求只读从库的策略。这意味着即使写请求锁定了表或记录,也不会影响读请求的执行。此外,在读流量较大的情况下,可以部署多个从库来共同处理读流量,这被称为"一主多从"部署方式,可以用来应对高并发读取流量,比如在您的垂直电商项目中。
此外,从库还可以用作备份库,以防止主库发生故障导致数据丢失。然而,需要注意的是,增加从库的数量并不是无限制的,因为随着从库数量的增加,主库需要处理更多的IO线程和log dump线程,这可能会对主库的资源消耗和网络带宽造成影响。通常情况下,一个主库最多可以连接3到5个从库。
尽管主从复制提供了许多好处,例如负载均衡和容错性,但它也有一些缺点,包括引入部署复杂性和可能导致主从同步延迟。这种延迟有时会对某些业务产生影响,例如在微博发布中,主从同步延迟可能会影响后续异步操作的正常流程,如将微博信息发送给审核系统。
图片
这个问题解决的思路有很多,核心思想就是尽量不去从库中查询信息,纯粹以上面的例子来说,我就有三种解决方案:
- 数据冗余方案: 在将消息发送到消息队列时,不仅发送微博ID,还将队列处理机所需的完整微博信息一并发送,以避免从数据库重新查询数据。这可以确保数据一致性,但可能会增加单条消息的大小,从而增加消息传输的带宽和时间成本。
- 缓存方案: 同步写入数据库的同时,将微博数据写入Memcached缓存。这样,队列处理机在获取微博信息时首先尝试从缓存中获取,以提高查询速度和减轻数据库负担。这种方法特别适用于新增数据的场景,但在更新数据时需要小心,因为同时更新缓存和数据库可能导致数据不一致,特别是在并发更新时。
- 查询主库方案: 在队列处理机中直接查询主库而不是从库。这是一种可行的方法,但需要谨慎使用,因为如果查询量很大,可能会给主库带来过大的压力。这种方式适用于查询量较小且在主库负载可承受范围内的情况。
最终,我会倾向于选择数据冗余方案,因为它相对简单,但可能会导致消息的大小增加以及消息传输的带宽和时间成本的增加。缓存方案适用于新增数据的情况,但需要小心处理数据一致性。查询主库方案是一种可行的备选方案,但需要谨慎使用以避免对主库造成过大的压力。
主从同步延迟是一个容易被忽略的问题,特别在排查问题时。有时候,当我们遇到从数据库中无法获取信息的奇怪问题时,我们可能会首先怀疑代码中是否有一些逻辑会导致数据被删除。但随着时间的推移,我们又发现之前丢失的数据突然又能够查询到,这通常是主从同步延迟导致的问题。因此,我们通常将从库的数据落后时间作为一个关键的数据库性能指标来监控和设置警报。正常情况下,数据同步延迟应该在毫秒级别,一旦延迟达到秒级别,就需要触发警报,以及时发现和解决潜在的问题。
2. 如何访问数据库
第一类中间件,以淘宝的TDDL(Taobao Distributed Data Layer)为代表,通常以代码形式嵌入到应用程序内部运行。可以将它视为数据源的代理,它管理着多个数据源,每个数据源对应一个数据库,可以是主库也可以是从库。当应用程序发起数据库请求时,这种中间件会将SQL语句路由到特定的数据源进行处理,然后返回处理结果。
这种中间件的优点在于它的简单和易用,而且没有额外的部署成本,因为它与应用程序一起运行,适合运维能力较弱的小团队使用。然而,它的缺点在于缺乏多语言支持。目前,这类主流解决方案如TDDL和早期的网易DDB,它们都是用Java开发的,无法支持其他编程语言。另外,版本升级也需要使用方主动更新,相对较为困难。
第二类中间件方案是单独部署的代理层,代表性的有早期的阿里巴巴开源项目Cobar、基于Cobar开发的Mycat、360开源的Atlas,以及美团开源的基于Atlas开发的DBProxy等等。这些中间件独立部署在专用服务器上,业务代码可以像使用单一数据库一样与它交互,尽管实际上它内部管理着多个数据源。当应用程序发起数据库请求时,这些中间件会对SQL语句进行必要的改写,然后将其发送到指定的数据源。它们通常使用标准的MySQL通信协议,因此可以很好地支持多种编程语言。
由于这些中间件是独立部署的,因此比较容易进行维护和升级,适合具有一定运维能力的大中型团队使用。然而,它们的缺点在于所有SQL语句需要跨越两次网络:从应用到代理层,然后从代理层到数据源,因此在性能方面可能会有一些损耗。
图片
这些中间件,对你而言,可能并不陌生,但是我想让你注意到是,在使用任何中间件的时候一定要保证对于中间件有足够深入的了解,否则一旦出了问题没法快速地解决就悲剧了。
需要明确的要点主要有:
- 主从复制是一种存储节点之间相互复制数据的技术,它实现了数据冗余,用于备份和提高横向扩展性。在使用主从复制时,需要权衡一致性和写入性能。如果要确保所有从节点都成功写入,写入性能可能会受到影响。如果只写入主节点并立即返回成功,从节点可能出现数据同步失败的情况,导致主从不一致。在互联网项目中,通常更注重性能而不是强一致性。
- 主从复制的延迟问题是一个关键的监控指标,可能会导致无法立刻读取数据的问题。很多奇怪的读取不到数据的情况可能与主从延迟有关。因此,在遇到这类问题时,首先要考虑检查主从延迟的数据。
- 业界存在多种方案来屏蔽主从复制后数据库访问的细节,使开发人员能够像访问单一数据库一样使用它们。这些方案包括嵌入应用内部的解决方案(如TDDL、Sharding-JDBC)和独立部署的代理方案(如Mycat)。不同的方案适用于不同的场景和需求。
这种思想和技术在许多存储组件中都有应用,如Redis使用主从复制实现读写分离,Elasticsearch中的索引分片可以被复制到多个节点,HDFS中的文件会被复制到多个DataNode。尽管不同组件对复制的一致性和延迟要求不同,但这种设计思想是通用的,对于理解和学习其他存储组件非常有帮助。