极客时间已完结课程限时免费阅读

08 | 数据库优化方案(一):查询请求增加时,如何做主从分离?

08 | 数据库优化方案(一):查询请求增加时,如何做主从分离?-极客时间

08 | 数据库优化方案(一):查询请求增加时,如何做主从分离?

讲述:唐扬

时长14:39大小13.42M

你好,我是唐扬。
上节课,我们用池化技术解决了数据库连接复用的问题,这时,你的垂直电商系统虽然整体架构上没有变化,但是和数据库交互的过程有了变化,在你的 Web 工程和数据库之间增加了数据库连接池,减少了频繁创建连接的成本,从上节课的测试来看性能上可以提升 80%。现在的架构图如下所示:
此时,你的数据库还是单机部署,依据一些云厂商的 Benchmark 的结果,在 4 核 8G 的机器上运行 MySQL 5.7 时,大概可以支撑 500 的 TPS 和 10000 的 QPS。这时,运营负责人说正在准备双十一活动,并且公司层面会继续投入资金在全渠道进行推广,这无疑会引发查询量骤然增加的问题。那么今天,我们就一起来看看当查询请求增加时,应该如何做主从分离来解决问题。

主从读写分离

其实,大部分系统的访问模型是读多写少,读写请求量的差距可能达到几个数量级。
这很好理解,刷朋友圈的请求量肯定比发朋友圈的量大,淘宝上一个商品的浏览量也肯定远大于它的下单量。因此,我们优先考虑数据库如何抵抗更高的查询请求,那么首先你需要把读写流量区分开,因为这样才方便针对读流量做单独的扩展,这就是我们所说的主从读写分离。
它其实是个流量分离的问题,就好比道路交通管制一样,一个四车道的大马路划出三个车道给领导外宾通过,另外一个车道给我们使用,优先保证领导先行,就是这个道理。
这个方法本身是一种常规的做法,即使在一个大的项目中,它也是一个应对数据库突发读流量的有效方法。
我目前的项目中就曾出现过前端流量突增导致从库负载过高的问题,DBA 兄弟会优先做一个从库扩容上去,这样对数据库的读流量就会落入到多个从库上,从库的负载就降了下来,然后研发同学再考虑使用什么样的方案将流量挡在数据库层之上。

主从读写的两个技术关键点

一般来说在主从读写分离机制中,我们将一个数据库的数据拷贝为一份或者多份,并且写入到其它的数据库服务器中,原始的数据库我们称为主库,主要负责数据的写入,拷贝的目标数据库称为从库,主要负责支持数据查询。可以看到,主从读写分离有两个技术上的关键点:
1. 一个是数据的拷贝,我们称为主从复制;
2. 在主从分离的情况下,我们如何屏蔽主从分离带来的访问数据库方式的变化,让开发同学像是在使用单一数据库一样。
接下来,我们分别来看一看。

1. 主从复制

我先以 MySQL 为例介绍一下主从复制。
MySQL 的主从复制是依赖于 binlog 的,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上二进制日志文件。主从复制就是将 binlog 中的数据从主库传输到从库上,一般这个过程是异步的,即主库上的操作不会等待 binlog 同步的完成。
主从复制的过程是这样的:首先从库在连接到主节点时会创建一个 IO 线程,用以请求主库更新的 binlog,并且把接收到的 binlog 信息写入一个叫做 relay log 的日志文件中,而主库也会创建一个 log dump 线程来发送 binlog 给从库;同时,从库还会创建一个 SQL 线程读取 relay log 中的内容,并且在从库中做回放,最终实现主从的一致性。这是一种比较常见的主从复制方式。
在这个方案中,使用独立的 log dump 线程是一种异步的方式,可以避免对主库的主体更新流程产生影响,而从库在接收到信息后并不是写入从库的存储中,是写入一个 relay log,是避免写入从库实际存储会比较耗时,最终造成从库和主库延迟变长。
你会发现,基于性能的考虑,主库的写入流程并没有等待主从同步完成就会返回结果,那么在极端的情况下,比如说主库上 binlog 还没有来得及刷新到磁盘上就出现了磁盘损坏或者机器掉电,就会导致 binlog 的丢失,最终造成主从数据的不一致。不过,这种情况出现的概率很低,对于互联网的项目来说是可以容忍的。
做了主从复制之后,我们就可以在写入时只写主库,在读数据时只读从库,这样即使写请求会锁表或者锁记录,也不会影响到读请求的执行。同时呢,在读流量比较大的情况下,我们可以部署多个从库共同承担读流量,这就是所说的“一主多从”部署方式,在你的垂直电商项目中就可以通过这种方式来抵御较高的并发读流量。另外,从库也可以当成一个备库来使用,以避免主库故障导致数据丢失。
那么你可能会说,是不是我无限制地增加从库的数量就可以抵抗大量的并发呢?实际上并不是的。因为随着从库数量增加,从库连接上来的 IO 线程比较多,主库也需要创建同样多的 log dump 线程来处理复制的请求,对于主库资源消耗比较高,同时受限于主库的网络带宽,所以在实际使用中,一般一个主库最多挂 3~5 个从库。
当然,主从复制也有一些缺陷,除了带来了部署上的复杂度,还有就是会带来一定的主从同步的延迟,这种延迟有时候会对业务产生一定的影响,我举个例子你就明白了。
在发微博的过程中会有些同步的操作,像是更新数据库的操作,也有一些异步的操作,比如说将微博的信息同步给审核系统,所以我们在更新完主库之后,会将微博的 ID 写入消息队列,再由队列处理机依据 ID 在从库中获取微博信息再发送给审核系统。此时如果主从数据库存在延迟,会导致在从库中获取不到微博信息,整个流程会出现异常。
这个问题解决的思路有很多,核心思想就是尽量不去从库中查询信息,纯粹以上面的例子来说,我就有三种解决方案:
第一种方案是数据的冗余。你可以在发送消息队列时不仅仅发送微博 ID,而是发送队列处理机需要的所有微博信息,借此避免从数据库中重新查询数据。
第二种方案是使用缓存。我可以在同步写数据库的同时,也把微博的数据写入到 Memcached 缓存里面,这样队列处理机在获取微博信息的时候会优先查询缓存,这样也可以保证数据的一致性。
最后一种方案是查询主库。我可以在队列处理机中不查询从库而改为查询主库。不过,这种方式使用起来要慎重,要明确查询的量级不会很大,是在主库的可承受范围之内,否则会对主库造成比较大的压力。
我会优先考虑第一种方案,因为这种方式足够简单,不过可能造成单条消息比较大,从而增加了消息发送的带宽和时间。
缓存的方案比较适合新增数据的场景,在更新数据的场景下,先更新缓存可能会造成数据的不一致,比方说两个线程同时更新数据,线程 A 把缓存中的数据更新为 1,此时另一个线程 B 把缓存中的数据更新为 2,然后线程 B 又更新数据库中的数据为 2,此时线程 A 更新数据库中的数据为 1,这样数据库中的值(1)和缓存中的值(2)就不一致了。
最后,若非万不得已的情况下,我不会使用第三种方案。原因是这种方案要提供一个查询主库的接口,在团队开发的过程中,你很难保证其他同学不会滥用这个方法,而一旦主库承担了大量的读请求导致崩溃,那么对于整体系统的影响是极大的。
所以对这三种方案来说,你要有所取舍,根据实际项目情况做好选择。
另外,主从同步的延迟,是我们排查问题时很容易忽略的一个问题。有时候我们遇到从数据库中获取不到信息的诡异问题时,会纠结于代码中是否有一些逻辑会把之前写入的内容删除,但是你又会发现,过了一段时间再去查询时又可以读到数据了,这基本上就是主从延迟在作怪。所以,一般我们会把从库落后的时间作为一个重点的数据库指标做监控和报警,正常的时间是在毫秒级别,一旦落后的时间达到了秒级别就需要告警了。

2. 如何访问数据库

我们已经使用主从复制的技术将数据复制到了多个节点,也实现了数据库读写的分离,这时,对于数据库的使用方式发生了变化。以前只需要使用一个数据库地址就好了,现在需要使用一个主库地址和多个从库地址,并且需要区分写入操作和查询操作,如果结合下一节课中要讲解的内容“分库分表”,复杂度会提升更多。为了降低实现的复杂度,业界涌现了很多数据库中间件来解决数据库的访问问题,这些中间件可以分为两类。
第一类以淘宝的 TDDL( Taobao Distributed Data Layer)为代表,以代码形式内嵌运行在应用程序内部。你可以把它看成是一种数据源的代理,它的配置管理着多个数据源,每个数据源对应一个数据库,可能是主库,可能是从库。当有一个数据库请求时,中间件将 SQL 语句发给某一个指定的数据源来处理,然后将处理结果返回。
这一类中间件的优点是简单易用,没有多余的部署成本,因为它是植入到应用程序内部,与应用程序一同运行的,所以比较适合运维能力较弱的小团队使用;缺点是缺乏多语言的支持,目前业界这一类的主流方案除了 TDDL,还有早期的网易 DDB,它们都是 Java 语言开发的,无法支持其他的语言。另外,版本升级也依赖使用方更新,比较困难。
另一类是单独部署的代理层方案,这一类方案代表比较多,如早期阿里巴巴开源的 Cobar,基于 Cobar 开发出来的 Mycat,360 开源的 Atlas,美团开源的基于 Atlas 开发的 DBProxy 等等。
这一类中间件部署在独立的服务器上,业务代码如同在使用单一数据库一样使用它,实际上它内部管理着很多的数据源,当有数据库请求时,它会对 SQL 语句做必要的改写,然后发往指定的数据源。
它一般使用标准的 MySQL 通信协议,所以可以很好地支持多语言。由于它是独立部署的,所以也比较方便进行维护升级,比较适合有一定运维能力的大中型团队使用。它的缺陷是所有的 SQL 语句都需要跨两次网络:从应用到代理层和从代理层到数据源,所以在性能上会有一些损耗。
这些中间件,对你而言,可能并不陌生,但是我想让你注意到是,在使用任何中间件的时候一定要保证对于中间件有足够深入的了解,否则一旦出了问题没法快速地解决就悲剧了。
我之前的一个项目中,一直使用自研的一个组件来实现分库分表,后来发现这套组件有一定几率会产生对数据库多余的连接,于是团队讨论后决定替换成 Sharding-JDBC。原本以为是一次简单的组件切换,结果上线后发现两个问题:一是因为使用姿势不对,会偶发地出现分库分表不生效导致扫描所有库表的情况,二是偶发地出现查询延时达到秒级别。由于对 Sharding-JDBC 没有足够了解,这两个问题我们都没有很快解决,后来不得已只能切回原来的组件,在找到问题之后再进行切换。

课程小结

本节课,我带你了解了查询量增加时,我们如何通过主从分离和一主多从部署抵抗增加的数据库流量的,你除了掌握主从复制的技术之外,还需要了解主从分离会带来什么问题以及它们的解决办法。这里我想让你明确的要点主要有:
1. 主从读写分离以及部署一主多从可以解决突发的数据库读流量,是一种数据库横向扩展的方法;
2. 读写分离后,主从的延迟是一个关键的监控指标,可能会造成写入数据之后立刻读的时候读取不到的情况;
3. 业界有很多的方案可以屏蔽主从分离之后数据库访问的细节,让开发人员像是访问单一数据库一样,包括有像 TDDL、Sharding-JDBC 这样的嵌入应用内部的方案,也有像 Mycat 这样的独立部署的代理方案。
其实,我们可以把主从复制引申为存储节点之间互相复制存储数据的技术,它可以实现数据的冗余,以达到备份和提升横向扩展能力的作用。在使用主从复制这个技术点时,你一般会考虑两个问题:
1. 主从的一致性和写入性能的权衡,如果你要保证所有从节点都写入成功,那么写入性能一定会受影响;如果你只写入主节点就返回成功,那么从节点就有可能出现数据同步失败的情况,从而造成主从不一致,而在互联网的项目中,我们一般会优先考虑性能而不是数据的强一致性。
2. 主从的延迟问题,很多诡异的读取不到数据的问题都可能会和它有关,如果你遇到这类问题不妨先看看主从延迟的数据。
我们采用的很多组件都会使用到这个技术,比如,Redis 也是通过主从复制实现读写分离;Elasticsearch 中存储的索引分片也可以被复制到多个节点中;写入到 HDFS 中文件也会被复制到多个 DataNode 中。只是不同的组件对于复制的一致性、延迟要求不同,采用的方案也不同。但是这种设计的思想是通用的,是你需要了解的,这样你在学习其他存储组件的时候就能够触类旁通了。

一课一思

我们提到,存储节点间互相复制数据是一种常见的,提升系统可用性和性能的方式,那么你还了解哪些组件有使用这种方式呢?它们的复制方式又是如何的呢?欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 32

提建议

上一篇
07 | 池化技术:如何减少频繁创建数据库连接的性能损耗?
下一篇
09 | 数据库优化方案(二):写入数据量增加时,如何实现分库分表?
 写留言

精选留言(72)

  • 老男孩
    置顶
    2019-10-09
    我觉得进入演进篇以后干货越来越多了。关于理论基础大家都能泛泛的谈一谈,可具体落地实操,老师的经验和能力就体现出来了。关于读写分离,主从同步延时出现的诡异现象,我之前也遇到过。我之前项目最开始没有只是配置了2个数据源,由开发人员选择写主库,读存库。后来发现很多读操作也放到主库上了,理由就是在一些场景下会出现诡异的数据不一致。于是,使用了mycat做代理,开发是比以前方便了,但诡异问题依然存在,又换成了atlas,还是不行。就如同老师说的一样,在没有完全深入了解组件的情况下贸然使用,本来是玩组件,结果被组件玩了。没办法,只好把查询放到一个事务里边,这样代理就会去主库中执行,但这样无异于还是增加的主库的压力。老师在专栏中提供基于消息队列和缓存的方案给我很好的启发,期待后续更多干货。
    展开
    共 5 条评论
    54
  • 被过去推开
    2019-11-27
    老师你好,我以前想要给公司分库分表,后来觉得有几个问题放在我面前,就搁浅了,现在只是挂了一个从库。 最主要的问题:我们公司每天产生许多业务订单,如果以用户id进行hash计算,分发到不同的库,对前台用户订单查询有利,但后台系统页面需要查看全部订单,以倒序排列,这样子的sql会不会执行很慢,毕竟是订单分散到几个库了。老师有好的分库分表方案吗?

    作者回复: 后台系统不能直接查询分库分表的数据,可以把数据同步到单独的一个后台库中,或者同步到es里面

    共 11 条评论
    30
  • Hwan
    2019-10-29
    老师,为啥优先读写分离,然后再缓存呢,是从那方面考虑的呢

    作者回复: 从开发和维护的难度考虑。引入缓存会引入复杂度,你要考虑缓存数据一致性,穿透,防雪崩等问题,并且也多维护一类组件

    共 3 条评论
    20
  • metasearch
    2019-10-06
    总结 1 主从原理:主库通过同步binlog到从库,relaylog去读 2 从库有延迟可以通过缓存 冗余数据来解决 3 4核8g TPS 500 QPS 10000

    作者回复: 👍

    共 5 条评论
    18
  • 无形
    2019-11-01
    之前发生过的一个问题,用Redis主从同步,写入Redis的数据量太大,没加频次控制,导致每秒几十万写入,主从延迟过大,运维频频报警,在主库不挂掉的情况下,这样大量写入会不会造成数据丢失?

    作者回复: 之前遇到过 如果主从延迟很大,数据会堆积到redis主库的发送缓冲区,会导致主库oom

    共 2 条评论
    17
  • _Axios丶靜ﻩ
    2019-10-30
    老师你好数据库的qps可以使用什么工具来监控

    作者回复: open falcon

    16
  • helloworld
    2019-10-08
    给主从复制增加延迟告警的思路很好,另外,老师能具体讲解下QPS和TPS的区别?网上查询了很多资料没有权威的解释。感谢!打卡08

    作者回复: 我理解的QPS是每秒查询数,是针对读请求的 TPS是每秒执行事务数,倾向于写请求

    共 6 条评论
    16
  • 雷霹雳的爸爸
    2019-11-06
    针对老师的一课一思...我认得得东东已经被老师说的七七八八了,能很快想到的,也就是kafka了,具体原理我还真没能力说清楚,说明个人升级之路和老师普及事业都是任重而道远啊,附上复制原理一篇 https://engineering.linkedin.com/kafka/intra-cluster-replication-apache-kafka 供同好参阅,成文比较早,不知道细节上和目前的版本有什么差异,所以立个免责得flag吧 其他的问题,主要是延伸阅读,考虑到主题(其实说的是分散数据库得读请求,但是难以避免的得围绕着复制来)和篇幅(太长估计没人看,现在看评论区都有看不仔细或不看完了就发问甚至敢直接质疑得),完整得讲解MySQL复制,例如涵盖半同步和GTID基于事务得复制等等,以及这些不同方案在一致性上得细节差异,可能不太容易做得到,所以考虑到面向互联网的场景做一些课程内容裁剪也是很有必要的;只是,考虑到可能很多专栏读者,比如我,可能并非真一定是面向消费类用户的互联网场景,即便是面向海量消费类用户的互联网应用场景,也需要在一致性、可用性等方面作必要的权衡,所以适当的扩展阅读参考指引感觉也是有必要的
    展开
    10
  • 每天晒白牙
    2019-10-04
    Kafka的数据会保存到leader副本的log文件中并写入磁盘,随后follower副本会对数据进行同步
    共 3 条评论
    8
  • Adrian
    2019-12-21
    文章里说通过冗余数据来解决主从延迟的问题,我有些异议,这种方式会造成缺乏扩展性,消息承载的数据很多消息组件都会有字节大小的限制,这样的话,如果后续数据量随着业务进行了扩展,就难免不会产生问题,持续扩展能力相对较差,我的理解倒是可以通过消息重试的方式来解决这个问题,如果主从同步延迟,那就消息重试,直到数据同步过来即可,这种解决方案通常业务上也是能忍受的,当然还是要按照具体case具体分析

    作者回复: 一般使用的比较多的是种缓存 重试其实用处不大,因为你不知道获取的数据究竟是不是延迟的数据

    共 2 条评论
    8
  • xu晓晨
    2019-10-08
    平常使用的redis也是用复制的方式 来保持数据同步。redis是以2.8版本为分界。2.8版本之前用用的是性能较差的复制和命令传播。首先是从服务器发生同步操作sync,主服务器会执行bgsave生成一个全量文件的rdb文件 然后传输给从服务器。同时主服务器会把这一过程中执行的写入命令写入缓存区。从服务器会把rdb文件进行一次全量加载。加载完毕后 主服务器会把缓存区中的写入命令传给从服务器。从服务器执行命令后 两个服务器的数据就一致了。 这种方式每次如果网络出现故障 故障重连后都要进行全量数据的复制。对主服务器的压力太大 也会增加主从网络传输的资源消耗。 redis2.8版本之后优化了这一部分缺陷 增加了部分重同步功能。部分同步就是同步故障后的一部分数据 而不是全量数据。这种优化在量级非常大的情况下 提生的效率是相当客观的。
    展开
    共 1 条评论
    7
  • 三年过后
    2019-10-04
    老师讲得很好!案例说到主从的延迟时间预警,未能详细到如何通过哪个数据库中的哪个指标来判别?经验中,我记得是,在从从库中,通过监控show slave status\G命令输出的Seconds_Behind_Master参数的值来判断,是否有发生主从延时。这个参数值是通过比较sql_thread执行的event的timestamp和io_thread复制好的 event的timestamp(简写为ts)进行比较,而得到的这么一个差值。 但是,问题来了,如果复制同步主库bin_log日志的io_thread线程负载过高的话,那么,Seconds_Behind_Master这个值就一直处于0。也是无法预警的,确切地说,通过Seconds_Behind_Master这个值来判断延迟是不够准确的。 不知,还有其他更好的方式?
    展开

    作者回复: 印象中可以通过比对master和slave的bin log位置

    6
  • 张珂
    2020-01-21
    老师你好,我想讲我之前的一个面试,面试官问存储层的高可用怎么搞。 我是这么回答的:存储用最熟悉的MySQL,架起主备,主备前用keepalive + VIP漂移,可以达到秒级切换。主从复制选用半同步复制,保证数据从binlog传输到备库后再返回请求。当主库发生异常时,需要切换,但需要备库应用完本地的relay log后才能成为主库。在这之前不能写,读的话会有少量不一致性。 面试官说:半同步复制可能延时有点大,而且你还相当于“停服”了。说说改成异步复制呢? 我针对异步复制,先分析了一下当主库挂的时候,可能会丢失一些数据。这个时候可以立即切换到备库成为主库,但影响是原主库挂之前一些提交的事务都丢失了,对于业务来说可能表现成用户做了某件事但结果却没做,比如支付了订单但后来发现没有该订单,而且余额也没少。这个时候要做好产品上的公告,表明事故以及让用户重新支付等。而且原主库要做好binlog上跳过那些跟原备库(现在的主库)不一致的部分。但这个方案依然需要等待备库应用完本地的relay log完毕才能切,只要没有长事务,一般很快。 面试官又在更高机房的层次上,提出让我设计一个方案,做到一定的“可伸缩”性,。比如A机房挂了,流量切到B机房,机房可以做到一变二,二变三等,这个时候系统的实战细节等。 这个地方我实在缺乏经验,我的回答是这样的:如果启用多个机房分别服务不同的用户群,想要达到流量切换,势必需要互相同步数据,但这里有个时间窗口不一致的问题,如果A机房挂了,马上切给B,依然采用刚才的思路,没有同步过来的数据算作“没有产生过”,业务有损。这样起码可以保证数据本身的一致性。 上面的三条回答面试官有些不满意,就结束了面试。老师您能帮我看看评价一下我的回答,看是否还有更好的方案?
    展开

    作者回复: 其实我觉得对于数据库切换的回答是没有啥问题的 多机房最主要的是数据的用户延迟,一般会自建一些工具

    共 5 条评论
    5
  • longslee
    2019-10-10
    “在 4 核 8G 的机器上运 MySQL 5.7 时,大概可支撑 500 的 TPS 和 10000 的 QPS”,老师,可以介绍下,挂了几个读库后(比如3个),同样的机器配置,QPS能上升到多少?

    作者回复: 理论上是三倍,取决于从库的负载均衡是否均匀,另外这个是benchmark结果,只是给大家一个感性认识,实际项目要比这个量极小

    共 2 条评论
    5
  • 🍚🐔🐢
    2019-10-10
    主库宕机之后,binlog丢失导致的主从数据不一致是不是只能手动恢复?

    作者回复: 在我来看是的

    共 3 条评论
    5
  • 你好旅行者
    2019-10-09
    请问一下来时,通过缓存来抗住高并发的读请求和通过MySQL的读写分离来抗高并发他们之间的区别在哪里呢?分别有哪些适用的场景?

    作者回复: 优先用读写分离,扛不住了再考虑缓存

    共 6 条评论
    5
  • xu晓晨
    2019-10-10
    个人感觉中间件不是什么地方都需要高可用 得看具体的业务场景。大多的缓存场景 并发不是很高的话 可以容忍暂时穿透查库。但是一些涉及到业务逻辑的地方 就必须高可用了

    作者回复: 可用性是是否容忍故障,如果穿透不会引发故障,是可以的

    3
  • 哇哦
    2019-10-04
    主从分离的,如果主节点写入sql,后面同步到从节点,那个时候,从节点实际上即在执行写,也在支持读。那主从分离的作用是保证主节点正常写?其他从节点只是通过增加机器来分担读数据io吗

    作者回复: 是的

    共 2 条评论
    3
  • 2020-04-06
    嗯,凡是需要通过冗余数据的方式来实现数据高可用的存储系统都需要数据复制技术,基本也都是通过日志的方式来实现的,当然日志的存在形式可能会有所变化。 数据复制最难的就是,数据的一致性问题,由于物理规律的限制必然存在数据同步延迟问题,只是站在用户的角度有些是无感知有些可能是不能忍受的。 主从延迟这个现象需要多加注意,否则有些现象解释不通会觉得非常诡异,主从延迟的时间最好监控起来,在不可接受范围内进行预警。
    展开

    作者回复: 是的,主从延迟是一定要监控的

    共 2 条评论
    2
  • kamida
    2020-03-20
    老师 您提到访问主从数据库有两种方式 能讲一下他们是怎么处理数据源的动态变化的吗 比如我们新加了一个从库

    作者回复: 其实,对于新加数据源基本上都可以做到业务无感知,一般是通过在从从库上挂域名实现吧

    2