You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

117 lines
14 KiB
Markdown

2 years ago
# 08 | 数据库优化方案(一):查询请求增加时,如何做主从分离?
你好,我是唐扬。
上节课我们用池化技术解决了数据库连接复用的问题这时你的垂直电商系统虽然整体架构上没有变化但是和数据库交互的过程有了变化在你的Web工程和数据库之间增加了数据库连接池减少了频繁创建连接的成本从上节课的测试来看性能上可以提升80%。现在的架构图如下所示:
![](https://static001.geekbang.org/resource/image/26/90/2643e13598139d0964bfc40469bd8390.jpg)
此时你的数据库还是单机部署依据一些云厂商的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是避免写入从库实际存储会比较耗时最终造成从库和主库延迟变长。
![](https://static001.geekbang.org/resource/image/57/4d/575ef1a6dc6463e4c5a60a3752d8554d.jpg)
你会发现基于性能的考虑主库的写入流程并没有等待主从同步完成就会返回结果那么在极端的情况下比如说主库上binlog还没有来得及刷新到磁盘上就出现了磁盘损坏或者机器掉电就会导致binlog的丢失最终造成主从数据的不一致。**不过,这种情况出现的概率很低,对于互联网的项目来说是可以容忍的。**
做了主从复制之后,我们就可以在写入时只写主库,在读数据时只读从库,这样即使写请求会锁表或者锁记录,也不会影响到读请求的执行。同时呢,在读流量比较大的情况下,我们可以部署多个从库共同承担读流量,这就是所说的“一主多从”部署方式,在你的垂直电商项目中就可以通过这种方式来抵御较高的并发读流量。另外,从库也可以当成一个备库来使用,以避免主库故障导致数据丢失。
**那么你可能会说,是不是我无限制地增加从库的数量就可以抵抗大量的并发呢?**实际上并不是的。因为随着从库数量增加从库连接上来的IO线程比较多主库也需要创建同样多的log dump线程来处理复制的请求对于主库资源消耗比较高同时受限于主库的网络带宽所以在实际使用中一般一个主库最多挂35个从库。
**当然,主从复制也有一些缺陷,**除了带来了部署上的复杂度,还有就是会带来一定的主从同步的延迟,这种延迟有时候会对业务产生一定的影响,我举个例子你就明白了。
在发微博的过程中会有些同步的操作像是更新数据库的操作也有一些异步的操作比如说将微博的信息同步给审核系统所以我们在更新完主库之后会将微博的ID写入消息队列再由队列处理机依据ID在从库中获取微博信息再发送给审核系统。此时如果主从数据库存在延迟会导致在从库中获取不到微博信息整个流程会出现异常。
![](https://static001.geekbang.org/resource/image/d0/44/d06716649d3894e8c2b2bf242b1ab544.jpg)
这个问题解决的思路有很多,核心思想就是尽量不去从库中查询信息,纯粹以上面的例子来说,我就有三种解决方案:
**第一种方案是数据的冗余。**你可以在发送消息队列时不仅仅发送微博ID而是发送队列处理机需要的所有微博信息借此避免从数据库中重新查询数据。
**第二种方案是使用缓存。**我可以在同步写数据库的同时也把微博的数据写入到Memcached缓存里面这样队列处理机在获取微博信息的时候会优先查询缓存这样也可以保证数据的一致性。
**最后一种方案是查询主库。**我可以在队列处理机中不查询从库而改为查询主库。不过,这种方式使用起来要慎重,要明确查询的量级不会很大,是在主库的可承受范围之内,否则会对主库造成比较大的压力。
我会优先考虑第一种方案,因为这种方式足够简单,不过可能造成单条消息比较大,从而增加了消息发送的带宽和时间。
缓存的方案比较适合新增数据的场景在更新数据的场景下先更新缓存可能会造成数据的不一致比方说两个线程同时更新数据线程A把缓存中的数据更新为1此时另一个线程B把缓存中的数据更新为2然后线程B又更新数据库中的数据为2此时线程A更新数据库中的数据为1这样数据库中的值1和缓存中的值2就不一致了。
最后,若非万不得已的情况下,我不会使用第三种方案。原因是这种方案要提供一个查询主库的接口,在团队开发的过程中,你很难保证其他同学不会滥用这个方法,而一旦主库承担了大量的读请求导致崩溃,那么对于整体系统的影响是极大的。
所以对这三种方案来说,你要有所取舍,根据实际项目情况做好选择。
**另外,主从同步的延迟,是我们排查问题时很容易忽略的一个问题。**有时候我们遇到从数据库中获取不到信息的诡异问题时,会纠结于代码中是否有一些逻辑会把之前写入的内容删除,但是你又会发现,过了一段时间再去查询时又可以读到数据了,这基本上就是主从延迟在作怪。所以,一般我们会把从库落后的时间作为一个重点的数据库指标做监控和报警,正常的时间是在毫秒级别,一旦落后的时间达到了秒级别就需要告警了。
#### 2\. 如何访问数据库
我们已经使用主从复制的技术将数据复制到了多个节点,也实现了数据库读写的分离,这时,对于数据库的使用方式发生了变化。以前只需要使用一个数据库地址就好了,现在需要使用一个主库地址和多个从库地址,并且需要区分写入操作和查询操作,如果结合下一节课中要讲解的内容“分库分表”,复杂度会提升更多。**为了降低实现的复杂度,业界涌现了很多数据库中间件来解决数据库的访问问题,这些中间件可以分为两类。**
第一类以淘宝的TDDL Taobao Distributed Data Layer为代表以代码形式内嵌运行在应用程序内部。你可以把它看成是一种数据源的代理它的配置管理着多个数据源每个数据源对应一个数据库可能是主库可能是从库。当有一个数据库请求时中间件将SQL语句发给某一个指定的数据源来处理然后将处理结果返回。
这一类中间件的优点是简单易用没有多余的部署成本因为它是植入到应用程序内部与应用程序一同运行的所以比较适合运维能力较弱的小团队使用缺点是缺乏多语言的支持目前业界这一类的主流方案除了TDDL还有早期的网易DDB它们都是Java语言开发的无法支持其他的语言。另外版本升级也依赖使用方更新比较困难。
另一类是单独部署的代理层方案这一类方案代表比较多如早期阿里巴巴开源的Cobar基于Cobar开发出来的Mycat360开源的Atlas美团开源的基于Atlas开发的DBProxy等等。
这一类中间件部署在独立的服务器上业务代码如同在使用单一数据库一样使用它实际上它内部管理着很多的数据源当有数据库请求时它会对SQL语句做必要的改写然后发往指定的数据源。
它一般使用标准的MySQL通信协议所以可以很好地支持多语言。由于它是独立部署的所以也比较方便进行维护升级比较适合有一定运维能力的大中型团队使用。它的缺陷是所有的SQL语句都需要跨两次网络从应用到代理层和从代理层到数据源所以在性能上会有一些损耗。
![](https://static001.geekbang.org/resource/image/e7/ff/e7e9430cbcb104764529ca5e01e6b3ff.jpg)
这些中间件,对你而言,可能并不陌生,但是我想让你注意到是,**在使用任何中间件的时候一定要保证对于中间件有足够深入的了解,否则一旦出了问题没法快速地解决就悲剧了。**
**我之前的一个项目中,**一直使用自研的一个组件来实现分库分表后来发现这套组件有一定几率会产生对数据库多余的连接于是团队讨论后决定替换成Sharding-JDBC。原本以为是一次简单的组件切换结果上线后发现两个问题一是因为使用姿势不对会偶发地出现分库分表不生效导致扫描所有库表的情况二是偶发地出现查询延时达到秒级别。由于对Sharding-JDBC没有足够了解这两个问题我们都没有很快解决后来不得已只能切回原来的组件在找到问题之后再进行切换。
## 课程小结
本节课,我带你了解了查询量增加时,我们如何通过主从分离和一主多从部署抵抗增加的数据库流量的,你除了掌握主从复制的技术之外,还需要了解主从分离会带来什么问题以及它们的解决办法。这里我想让你明确的要点主要有:
1.主从读写分离以及部署一主多从可以解决突发的数据库读流量,是一种数据库横向扩展的方法;
2.读写分离后,主从的延迟是一个关键的监控指标,可能会造成写入数据之后立刻读的时候读取不到的情况;
3.业界有很多的方案可以屏蔽主从分离之后数据库访问的细节让开发人员像是访问单一数据库一样包括有像TDDL、Sharding-JDBC这样的嵌入应用内部的方案也有像Mycat这样的独立部署的代理方案。
其实,我们可以把主从复制引申为存储节点之间互相复制存储数据的技术,它可以实现数据的冗余,以达到备份和提升横向扩展能力的作用。在使用主从复制这个技术点时,你一般会考虑两个问题:
1.主从的一致性和写入性能的权衡,如果你要保证所有从节点都写入成功,那么写入性能一定会受影响;如果你只写入主节点就返回成功,那么从节点就有可能出现数据同步失败的情况,从而造成主从不一致,**而在互联网的项目中,我们一般会优先考虑性能而不是数据的强一致性。**
2.主从的延迟问题,很多诡异的读取不到数据的问题都可能会和它有关,如果你遇到这类问题不妨先看看主从延迟的数据。
我们采用的很多组件都会使用到这个技术比如Redis也是通过主从复制实现读写分离Elasticsearch中存储的索引分片也可以被复制到多个节点中写入到HDFS中文件也会被复制到多个DataNode中。只是不同的组件对于复制的一致性、延迟要求不同采用的方案也不同。**但是这种设计的思想是通用的,是你需要了解的,这样你在学习其他存储组件的时候就能够触类旁通了。**
## 一课一思
我们提到,存储节点间互相复制数据是一种常见的,提升系统可用性和性能的方式,那么你还了解哪些组件有使用这种方式呢?它们的复制方式又是如何的呢?欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。