mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-17 22:53:42 +08:00
mod
This commit is contained in:
93
极客时间专栏/后端存储实战课/海量数据篇/15 | MySQL存储海量数据的最后一招:分库分表.md
Normal file
93
极客时间专栏/后端存储实战课/海量数据篇/15 | MySQL存储海量数据的最后一招:分库分表.md
Normal file
@@ -0,0 +1,93 @@
|
||||
<audio id="audio" title="15 | MySQL存储海量数据的最后一招:分库分表" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/75/94/756366dc7668dc61c10b859f8965a994.mp3"></audio>
|
||||
|
||||
你好,我是李玥。
|
||||
|
||||
从这节课开始,我们课程将进入最后一部分“海量数据篇”,这节课也是我们最后一节主要讲MySQL的课程。解决海量数据的问题,必须要用到分布式的存储集群,因为MySQL本质上是一个单机数据库,所以很多场景下不是太适合存TB级别以上的数据。
|
||||
|
||||
但是,绝大部分的电商大厂,它的在线交易这部分的业务,比如说,订单、支付相关的系统,还是舍弃不了MySQL,原因是,**只有MySQL这类关系型数据库,才能提供金融级的事务保证**。我们之前也讲过分布式事务,那些新的分布式数据库提供的所谓的分布式事务,多少都有点儿残血,目前还达不到这些交易类系统对数据一致性的要求。
|
||||
|
||||
那既然MySQL支持不了这么大的数据量,这么高的并发,还必须要用它,怎么解决这个问题呢?还是按照我上节课跟你说的思想,**分片**,也就是拆分数据。1TB的数据,一个库撑不住,我把它拆成100个库,每个库就只有10GB的数据了,这不就可以了么?这种拆分就是所谓的MySQL分库分表。
|
||||
|
||||
不过,思路是这样没错,分库分表实践起来是非常不容易的,有很多问题需要去思考和解决。
|
||||
|
||||
## 如何规划分库分表?
|
||||
|
||||
还是拿咱们的“老熟人”订单表来举例子。首先需要思考的问题是,分库还是分表?分库呢,就是把数据拆分到不同的MySQL库中去,分表就是把数据拆分到同一个库的多张表里面。
|
||||
|
||||
在考虑到底是分库还是分表之前,我们需要先明确一个原则,**那就是能不拆就不拆,能少拆不多拆**。原因也很简单,你把数据拆分得越散,开发和维护起来就越麻烦,系统出问题的概率就越大。
|
||||
|
||||
基于这个原则我们想一下,什么情况下适合分表,什么情况下不得不分库?
|
||||
|
||||
那我们分库分表的目的是为了解决两个问题:
|
||||
|
||||
第一,是数据量太大查询慢的问题。这里面我们讲的“查询”其实主要是事务中的查询和更新操作,因为只读的查询可以通过缓存和主从分离来解决,这个我们在之前的“MySQL如何应对高并发”的两节课中都讲过。那我们上节课也讲到过,**解决查询慢,只要减少每次查询的数据总量就可以了,也就是说,分表就可以解决问题**。
|
||||
|
||||
第二,是为了应对高并发的问题。应对高并发的思想我们之前也说过,一个数据库实例撑不住,就把并发请求分散到多个实例中去,所以,**解决高并发的问题是需要分库的**。
|
||||
|
||||
简单地说,**数据量大,就分表;并发高,就分库**。
|
||||
|
||||
一般情况下,我们的方案都需要同时做分库分表,这时候分多少个库,多少张表,分别用预估的并发量和数据量来计算就可以了。
|
||||
|
||||
另外,我个人不建议你在方案中考虑二次扩容的问题,也就是考虑未来的数据量,把这次分库分表设计的容量都填满了之后,数据如何再次分裂的问题。
|
||||
|
||||
现在技术和业务变化这么快,等真正到了那个时候,业务早就变了,可能新的技术也出来了,你之前设计的二次扩容方案大概率是用不上的,所以没必要为了这个而增加方案的复杂程度。还是那句话,**越简单的设计可靠性越高**。
|
||||
|
||||
## 如何选择Sharding Key?
|
||||
|
||||
分库分表还有一个重要的问题是,选择一个合适的列或者说是属性,作为分表的依据,这个属性一般称为Sharding Key。像我们上节课讲到的归档历史订单的方法,它的Sharding Key就是订单完成时间。每次查询的时候,查询条件中必须带上这个时间,我们的程序就知道,三个月以前的数据查订单历史表,三个月内的数据查订单表,这就是一个简单的按照时间范围来分片的算法。
|
||||
|
||||
选择合适Sharding Key和分片算法非常重要,直接影响了分库分表的效果。我们首先来说如何选择Sharding Key的问题。
|
||||
|
||||
**选择这个Sharding Key最重要的参考因素是,我们的业务是如何访问数据的**。
|
||||
|
||||
比如我们把订单ID作为Sharding Key来拆分订单表,那拆分之后,如果我们按照订单ID来查订单,就需要先根据订单ID和分片算法计算出,我要查的这个订单它在哪个分片上,也就是哪个库哪张表中,然后再去那个分片执行查询就可以了。
|
||||
|
||||
但是,当我打开“我的订单”这个页面的时候,它的查询条件是用户ID,这里没有订单ID,那就没法知道我们要查的订单在哪个分片上,就没法查了。当然你要强行查的话,那就只能把所有分片都查一遍,再合并查询结果,这个就很麻烦,而且性能很差,还不能分页。
|
||||
|
||||
那要是把用户ID作为Sharding Key呢?也会面临同样的问题,使用订单ID作为查询条件来查订单的时候,就没办法找到订单在哪个分片了。这个问题的解决办法是,在生成订单ID的时候,把用户ID的后几位作为订单ID的一部分,比如说,可以规定,18位订单号中,第10-14位是用户ID的后四位,这样按订单ID查询的时候,就可以根据订单ID中的用户ID找到分片。
|
||||
|
||||
那我们系统对订单的查询方式,肯定不只是按订单ID或者按用户ID这两种啊。比如说,商家希望看到的是自己店铺的订单,还有各种和订单相关的报表。对于这些查询需求,我们一旦对订单做了分库分表,就没法解决了。那怎么办呢?
|
||||
|
||||
一般的做法是,把订单数据同步到其他的存储系统中去,在其他的存储系统里面解决问题。比如说,我们可以再构建一个以店铺ID作为Sharding Key的只读订单库,专门供商家来使用。或者,把订单数据同步到HDFS中,然后用一些大数据技术来生成订单相关的报表。
|
||||
|
||||
所以你看,一旦做了分库分表,就会极大地限制数据库的查询能力,之前很简单的查询,分库分表之后,可能就没法实现了。所以我们在之前的课程中,先讲了各种各样的方法,来缓解数据多、并发高的问题,而一直没讲分库分表。**分库分表一定是,数据量和并发大到所有招数都不好使了,我们才拿出来的最后一招。**
|
||||
|
||||
## 如何选择分片算法?
|
||||
|
||||
在上节课我给你留的思考题中,我们提到过,能不能用订单完成时间作为Sharding Key呢?比如说,我分12个分片,每个月一个分片,这样对查询的兼容要好很多,毕竟查询条件中带上时间范围,让查询只落到某一个分片上,还是比较容易的,我在查询界面上强制用户必须指定时间范围就行了。
|
||||
|
||||
这种做法有个很大的问题,比如现在是3月份,那基本上所有的查询都集中在3月份这个分片上,其他11个分片都闲着,这样不仅浪费资源,很可能你3月那个分片根本抗不住几乎全部的并发请求。这个问题就是“热点问题”。
|
||||
|
||||
也就是说,我们希望并发请求和数据能均匀地分布到每一个分片上,尽量避免出现热点。这是选择分片算法时需要考虑的一个重要的因素。一般常用的分片算法就那么几种,刚刚讲到的按照时间范围分片的方法是其中的一种。
|
||||
|
||||
基于范围来分片容易产生热点问题,不适合作为订单的分片方法,但是这种分片方法的优点也很突出,那就是对查询非常友好,基本上只要加上一个时间范围的查询条件,原来该怎么查,分片之后还可以怎么查。范围分片特别适合那种数据量非常大,但并发访问量不大的ToB系统。比如说,电信运营商的监控系统,它可能要采集所有人手机的信号质量,然后做一些分析,这个数据量非常大,但是这个系统的使用者是运营商的工作人员,并发量很少。这种情况下就很适合范围分片。
|
||||
|
||||
一般来说,订单表都采用更均匀的哈希分片算法。比如说,我们要分24个分片,选定了Sharding Key是用户ID,那我们决定某个用户的订单应该落到那个分片上的算法是,拿用户ID除以24,得到的余数就是分片号。这是最简单的取模算法,一般就可以满足大部分要求了。当然也有一些更复杂的哈希算法,像一致性哈希之类的,特殊情况下也可以使用。
|
||||
|
||||
需要注意的一点是,哈希分片算法能够分得足够均匀的前提条件是,用户ID后几位数字必须是均匀分布的。比如说,你在生成用户ID的时候,自定义了一个用户ID的规则,最后一位0是男性,1是女性,这样的用户ID哈希出来可能就没那么均匀,可能会出现热点。
|
||||
|
||||
还有一种分片的方法:查表法。查表法其实就是没有分片算法,决定某个Sharding Key落在哪个分片上,全靠人为来分配,分配的结果记录在一张表里面。每次执行查询的时候,先去表里查一下要找的数据在哪个分片中。
|
||||
|
||||
查表法的好处就是灵活,怎么分都可以,你用上面两种分片算法都没法分均匀的情况下,就可以用查表法,人为地来把数据分均匀了。查表法还有一个特好的地方是,它的分片是可以随时改变的。比如我发现某个分片已经是热点了,那我可以把这个分片再拆成几个分片,或者把这个分片的数据移到其他分片中去,然后修改一下分片映射表,就可以在线完成数据拆分了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/0f/9d/0faac5967ca1f9385d8f7eda8eedd09d.jpg" alt="">
|
||||
|
||||
但你需要注意的是,分片映射表本身的数据不能太多,否则这个表反而成为热点和性能瓶颈了。查表法相对其他两种分片算法来说,缺点是需要二次查询,实现起来更复杂,性能上也稍微慢一些。但是,分片映射表可以通过缓存来加速查询,实际性能并不会慢很多。
|
||||
|
||||
## 小结
|
||||
|
||||
对MySQL这样的单机数据库来说,分库分表是应对海量数据和高并发的最后一招,分库分表之后,将会对数据查询有非常大的限制。
|
||||
|
||||
分多少个库需要用并发量来预估,分多少表需要用数据量来预估。选择Sharding Key的时候,一定要能兼容业务最常用的查询条件,让查询尽量落在一个分片中,分片之后无法兼容的查询,可以把数据同步到其他存储中去,来解决这个问题。
|
||||
|
||||
我们常用三种分片算法,范围分片容易产生热点问题,但对查询更友好,适合适合并发量不大的场景;哈希分片比较容易把数据和查询均匀地分布到所有分片中;查表法更灵活,但性能稍差。
|
||||
|
||||
对于订单表进行分库分表,一般按照用户ID作为Sharding Key,采用哈希分片算法来均匀分布用户订单数据。为了能支持按订单号查询的需求,需要把用户ID的后几位放到订单号中去。
|
||||
|
||||
最后还需要强调一下,我们这节课讲的这些分片相关的知识,不仅仅适用于MySQL的分库分表,你在使用其他分布式数据库的时候,一样会遇到如何分片、如何选择Sharding Key和分片算法的问题,它们的原理都是一样的,所以我们讲的这些方法也都是通用的。
|
||||
|
||||
## 思考题
|
||||
|
||||
课后请你想一下,把订单表拆分之后,那些和订单有外键关联的表,该怎么处理?欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。
|
||||
114
极客时间专栏/后端存储实战课/海量数据篇/16 | 用Redis构建缓存集群的最佳实践有哪些?.md
Normal file
114
极客时间专栏/后端存储实战课/海量数据篇/16 | 用Redis构建缓存集群的最佳实践有哪些?.md
Normal file
@@ -0,0 +1,114 @@
|
||||
<audio id="audio" title="16 | 用Redis构建缓存集群的最佳实践有哪些?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/b6/56/b68d0c726750b5cea58bb69314f47b56.mp3"></audio>
|
||||
|
||||
你好,我是李玥。
|
||||
|
||||
之前连续几节课,我们都在以MySQL为例子,讲如何应对海量数据,如何应对高并发,如何实现高可用,我先带你简单复习一下。
|
||||
|
||||
- 数据量太大查询慢怎么办?存档历史数据或者分库分表,这是数据分片。
|
||||
- 并发太高扛不住怎么办?读写分离,这是增加实例数。
|
||||
- 数据库宕机怎么办?增加从节点,主节点宕机的时候用从节点顶上,这是主从复制。但是这里面要特别注意数据一致性的问题。
|
||||
|
||||
我在之前课程中,也多次提到过,**这些方法不仅仅是MySQL特有的,对于几乎所有的存储系统,都是适用的。**
|
||||
|
||||
今天这节课,我们来聊一聊,如何构建一个生产系统可用的Redis缓存集群。你将会看到,几种集群解决方案用到的思想,基本和我们上面讲的都是一样的。
|
||||
|
||||
## Redis Cluster如何解决数据量大、高可用和高并发问题?
|
||||
|
||||
Redis从3.0版本开始,提供了官方的集群支持,也就是Redis Cluser。**Redis Cluster相比于单个节点的Redis,能保存更多的数据,支持更多的并发,并且可以做到高可用,在单个节点故障的情况下,继续提供服务。**
|
||||
|
||||
为了能够保存更多的数据,和MySQL分库分表的方式类似,Redis Cluster也是通过分片的方式,把数据分布到集群的多个节点上。
|
||||
|
||||
Redis Cluster是如何来分片的呢?它引入了一个“槽(Slot)”的概念,这个槽就是哈希表中的哈希槽,槽是Redis分片的基本单位,每个槽里面包含一些Key。每个集群的槽数是固定的16384(16 * 1024)个,每个Key落在哪个槽中也是固定的,计算方法是:
|
||||
|
||||
```
|
||||
HASH_SLOT = CRC16(key) mod 16384
|
||||
|
||||
```
|
||||
|
||||
这个算法很简单,先计算Key的CRC值,然后把这个CRC之后的Key值直接除以16384,余数就是Key所在的槽。这个算法就是我们上节课讲过的哈希分片算法。
|
||||
|
||||
这些槽又是如何存放到具体的Redis节点上的呢?这个映射关系保存在集群的每个Redis节点上,集群初始化的时候,Redis会自动平均分配这16384个槽,也可以通过命令来调整。这个分槽的方法,也是我们上节课讲到过的分片算法:查表法。
|
||||
|
||||
客户端可以连接集群的任意一个节点来访问集群的数据,当客户端请求一个Key的时候,被请求的那个Redis实例先通过上面的公式,计算出这个Key在哪个槽中,然后再查询槽和节点的映射关系,找到数据所在的真正节点,如果这个节点正好是自己,那就直接执行命令返回结果。如果数据不在当前这个节点上,那就给客户端返回一个重定向的命令,告诉客户端,应该去连哪个节点上请求这个Key的数据。然后客户端会再连接正确的节点来访问。
|
||||
|
||||
解决分片问题之后,Redis Cluster就可以通过水平扩容来增加集群的存储容量,但是,每次往集群增加节点的时候,需要从集群的那些老节点中,搬运一些槽到新节点,你可以手动指定哪些槽迁移到新节点上,也可以利用官方提供的[redis-trib.rb](http://download.redis.io/redis-stable/src/redis-trib.rb)脚本来自动重新分配槽,自动迁移。
|
||||
|
||||
分片可以解决Redis保存海量数据的问题,并且客观上提升了Redis的并发能力和查询性能。但是并不能解决高可用的问题,每个节点都保存了整个集群数据的一个子集,任何一个节点宕机,都会导致这个宕机节点上的那部分数据无法访问。
|
||||
|
||||
那Redis Cluster是怎么解决高可用问题的?
|
||||
|
||||
参见上面我们讲到的方法:**增加从节点,做主从复制**。Redis Cluster支持给每个分片增加一个或多个从节点,每个从节点在连接到主节点上之后,会先给主节点发送一个SYNC命令,请求一次全量复制,也就是把主节点上全部的数据都复制到从节点上。全量复制完成之后,进入同步阶段,主节点会把刚刚全量复制期间收到的命令,以及后续收到的命令持续地转发给从节点。
|
||||
|
||||
因为Redis不支持事务,所以它的复制相比MySQL更简单,连Binlog都省了,直接就是转发客户端发来的更新数据命令来实现主从同步。如果某个分片的主节点宕机了,集群中的其他节点会在这个分片的从节点中选出一个新的节点作为主节点继续提供服务。新的主节点选举出来后,集群中的所有节点都会感知到,这样,如果客户端的请求Key落在故障分片上,就会被重定向到新的主节点上。
|
||||
|
||||
最后我们看一下,Redis Cluster是如何应对高并发的。
|
||||
|
||||
一般来说,Redis Cluster进行了分片之后,每个分片都会承接一部分并发的请求,加上Redis本身单节点的性能就非常高,所以大部分情况下不需要再像MySQL那样做读写分离来解决高并发的问题。默认情况下,集群的读写请求都是由主节点负责的,从节点只是起一个热备的作用。当然了,Redis Cluster也支持读写分离,在从节点上读取数据。
|
||||
|
||||
以上就是Redis Cluster的基本原理,你可以对照下图来加深理解。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c4/fd/c405e73a4fd797ca0cda82b383e46ffd.png" alt="">
|
||||
|
||||
你可以看到,Redis Cluster整体的架构完全就是照抄MySQL构建集群的那一套东西(当然,这些设计和方法也不是MySQL发明的),抄作业抄的就差把名字一起也抄上了。
|
||||
|
||||
具体如何搭建Redis Cluster以及相关的操作命令你可以看一下[Redis官方的这篇教程](https://redis.io/topics/cluster-tutorial)。
|
||||
|
||||
## 为什么Redis Cluster不适合超大规模集群?
|
||||
|
||||
Redis Cluster的优点是易于使用。分片、主从复制、弹性扩容这些功能都可以做到自动化,通过简单的部署就可以获得一个大容量、高可靠、高可用的Redis集群,并且对于应用来说,近乎于是透明的。
|
||||
|
||||
所以,**Redis Cluster是非常适合构建中小规模Redis集群**,这里的中小规模指的是,大概几个到几十个节点这样规模的Redis集群。
|
||||
|
||||
**但是Redis Cluster不太适合构建超大规模集群,主要原因是,它采用了去中心化的设计。**刚刚我们讲了,Redis的每个节点上,都保存了所有槽和节点的映射关系表,客户端可以访问任意一个节点,再通过重定向命令,找到数据所在的那个节点。那你有没有想过一个问题,这个映射关系表,它是如何更新的呢?比如说,集群加入了新节点,或者某个主节点宕机了,新的主节点被选举出来,这些情况下,都需要更新集群每一个节点上的映射关系表。
|
||||
|
||||
Redis Cluster采用了一种去中心化的[流言(Gossip)协议](https://en.wikipedia.org/wiki/Gossip_protocol)来传播集群配置的变化。一般涉及到协议都比较复杂,这里我们不去深究具体协议和实现算法,我大概给你讲一下这个协议原理。
|
||||
|
||||
所谓流言,就是八卦,比如说,我们上学的时候,班上谁和谁偷偷好上了,搞对象,那用不了一天,全班同学都知道了。咋知道的?张三看见了,告诉李四,李四和王小二特别好,又告诉了王小二,这样人传人,不久就传遍全班了。这个就是八卦协议的传播原理。
|
||||
|
||||
这个八卦协议它的好处是去中心化,传八卦不需要组织,吃瓜群众自发就传开了。这样部署和维护就更简单,也能避免中心节点的单点故障。八卦协议的缺点就是传播速度慢,并且是集群规模越大,传播的越慢。这个也很好理解,比如说,换成某两个特别出名的明星搞对象,即使是全国人民都很八卦,但要想让全国每一个人都知道这个消息,还是需要很长的时间。在集群规模太大的情况下,数据不同步的问题会被明显放大,还有一定的不确定性,如果出现问题很难排查。
|
||||
|
||||
## 如何用Redis构建超大规模集群?
|
||||
|
||||
Redis Cluster不太适合用于大规模集群,所以很多大厂,都选择自己去搭建Redis集群。这里面,每一家的解决方案都有自己的特色,但其实总体的架构都是大同小异的。
|
||||
|
||||
一种是基于代理的方式,在客户端和Redis节点之间,还需要增加一层代理服务。这个代理服务有三个作用。
|
||||
|
||||
第一个作用是,负责在客户端和Redis节点之间转发请求和响应。客户端只和代理服务打交道,代理收到客户端的请求之后,再转发到对应的Redis节点上,节点返回的响应再经由代理转发返回给客户端。
|
||||
|
||||
第二个作用是,负责监控集群中所有Redis节点状态,如果发现有问题节点,及时进行主从切换。
|
||||
|
||||
第三个作用就是维护集群的元数据,这个元数据主要就是集群所有节点的主从信息,以及槽和节点关系映射表。这个架构和我在《[12 | MySQL如何应对高并发(二):读写分离](https://time.geekbang.org/column/article/215330)》这节课中给你讲过的,用HAProxy+Keepalived来代理MySQL请求的架构是类似的,只是多了一个自动路由分片的功能而已。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/03/d6/03c2901db83c02cbfe90a8c9b78d04d6.jpg" alt="">
|
||||
|
||||
像开源的Redis集群方案[twemproxy](https://github.com/twitter/twemproxy)和[Codis](https://github.com/CodisLabs/codis),都是这种架构的。
|
||||
|
||||
这个架构最大的优点是对客户端透明,在客户端视角来看,整个集群和一个超大容量的单节点Redis是一样的。并且,由于分片算法是代理服务控制的,扩容也比较方便,新节点加入集群后,直接修改代理服务中的元数据就可以完成扩容。
|
||||
|
||||
不过,这个架构的缺点也很突出,增加了一层代理转发,每次数据访问的链路更长了,必然会带来一定的性能损失。而且,代理服务本身又是集群的一个单点,当然,我们可以把代理服务也做成一个集群来解决单点问题,那样集群就更复杂了。
|
||||
|
||||
另外一种方式是,不用这个代理服务,把代理服务的寻址功能前移到客户端中去。客户端在发起请求之前,先去查询元数据,就可以知道要访问的是哪个分片和哪个节点,然后直连对应的Redis节点访问数据。
|
||||
|
||||
当然,客户端不用每次都去查询元数据,因为这个元数据是不怎么变化的,客户端可以自己缓存元数据,这样访问性能基本上和单机版的Redis是一样的。如果某个分片的主节点宕机了,新的主节点被选举出来之后,更新元数据里面的信息。对集群的扩容操作也比较简单,除了迁移数据的工作必须要做以外,更新一下元数据就可以了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/dc/da/dcaced0a9ce9842ef688c9626accdcda.jpg" alt="">
|
||||
|
||||
虽然说,这个元数据服务仍然是一个单点,但是它的数据量不大,访问量也不大,相对就比较容易实现。我们可以用ZooKeeper、etcd甚至MySQL都能满足要求。这个方案应该是最适合超大规模Redis集群的方案了,在性能、弹性、高可用几方面表现都非常好,缺点是整个架构比较复杂,客户端不能通用,需要开发定制化的Redis客户端,只有规模足够大的企业才负担得起。
|
||||
|
||||
## 小结
|
||||
|
||||
今天这节课我们讲了从小到大三种构建Redis集群的方式。
|
||||
|
||||
- 小规模的集群建议使用官方的Redis Cluster,在节点数量不多的情况下,各方面表现都不错。
|
||||
- 再大一些规模的集群,可以考虑使用twemproxy或者Codis这类的基于代理的集群架构,虽然是开源方案,但是已经被很多公司在生产环境中验证过。
|
||||
- 相比于代理方案,使用定制客户端的方案性能更好,很多大厂采用的都是类似的架构。
|
||||
|
||||
还有一个小问题需要注意的是,这几种集群方案对一些类似于“KEYS”这类的多KEY命令,都没法做到百分百支持。原因很简单,数据被分片了之后,这种多KEY的命令很可能需要跨多个分片查询。当你的系统从单个Redis库升级到集群时,可能需要考虑一下这方面的兼容性问题。
|
||||
|
||||
## 思考题
|
||||
|
||||
很多存储系统之间都存在“互相抄作业”的嫌疑,其实这对于我们这些存储系统的使用者来说是好事儿,比如我们把MySQL都学透了,你再去看Redis,知道它抄了哪些作业,这部分我们就可以迅速掌握了,只要再研究一下不一样的那一小部分内容,我们就可以精通Redis了是不?
|
||||
|
||||
课后请你再去看一下HDFS,它在解决分片、复制和高可用这几方面,哪些是“抄作业”,哪些又是自己独创的。欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。
|
||||
186
极客时间专栏/后端存储实战课/海量数据篇/17 | 大厂都是怎么做MySQL to Redis同步的?.md
Normal file
186
极客时间专栏/后端存储实战课/海量数据篇/17 | 大厂都是怎么做MySQL to Redis同步的?.md
Normal file
@@ -0,0 +1,186 @@
|
||||
<audio id="audio" title="17 | 大厂都是怎么做MySQL to Redis同步的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ce/f8/ce70cc33d669d707ede0e8e862ee7ef8.mp3"></audio>
|
||||
|
||||
你好,我是李玥。
|
||||
|
||||
之前我们在《[11 | MySQL如何应对高并发(一):使用缓存保护MySQL](https://time.geekbang.org/column/article/213230)》这一节课中,讲到了Read/Write Through和Cache Aside这几种更新缓存的策略,这几种策略都存在缓存穿透的可能,如果缓存没有命中,那就穿透缓存去访问数据库获取数据。
|
||||
|
||||
一般情况下,只要我们做好缓存预热,这个缓存的命中率很高,能穿透缓存打到数据库上的请求比例就非常低,这些缓存的策略都是没问题的。但是如果说,我们的Redis缓存服务的是一个超大规模的系统,那就又不一样了。
|
||||
|
||||
今天这节课,我们来说一下,在超大规模系统中缓存会面临什么样的问题,以及应该使用什么样的策略来更新缓存。
|
||||
|
||||
## 缓存穿透:超大规模系统的不能承受之痛
|
||||
|
||||
我们上节课讲到了如何构建Redis集群,由于集群可以水平扩容,那只要集群足够大,理论上支持海量并发也不是问题。但是,因为并发请求的数量这个基数太大了,即使有很小比率的请求穿透缓存,打到数据库上请求的绝对数量仍然不小。加上大促期间的流量峰值,还是存在缓存穿透引发雪崩的风险。
|
||||
|
||||
那这个问题怎么解决呢?其实方法你也想得到,不让请求穿透缓存不就行了?反正现在存储也便宜,只要你买得起足够多的服务器,Redis集群的容量就是无限的。不如把全量的数据都放在Redis集群里面,处理读请求的时候,干脆只读Redis,不去读数据库。这样就完全没有“缓存穿透”的风险了,实际上很多大厂它就是这么干的。
|
||||
|
||||
在Redis中缓存全量的数据,又引发了一个新的问题,那就是,如何来更新缓存中的数据呢?因为我们取消了缓存穿透的机制,这种情况下,从缓存读到数据可以直接返回,如果没读到数据,那就只能返回错误了!所以,当系统更新数据库的数据之后,必须及时去更新缓存。
|
||||
|
||||
说到这儿,又绕回到那个老问题上了:怎么保证Redis中的数据和数据库中的数据同步更新?我们之前讲过用分布式事务来解决数据一致性的问题,但是这些方法都不太适合用来更新缓存,**因为分布式事务,对数据更新服务有很强的侵入性**。我们拿下单服务来说,如果为了更新缓存增加一个分布式事务,无论我们用哪种分布式事务,或多或少都会影响下单服务的性能。还有一个问题是,如果Redis本身出现故障,写入数据失败,还会导致下单失败,等于是降低了下单服务性能和可用性,这样肯定不行。
|
||||
|
||||
**对于像订单服务这类核心的业务,一个可行的方法是,我们启动一个更新订单缓存的服务,接收订单变更的MQ消息,然后更新Redis中缓存的订单数据。**因为这类核心的业务数据,使用方非常多,本来就需要发消息,增加一个消费订阅基本没什么成本,订单服务本身也不需要做任何更改。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7c/8e/7cec502808318409dbc719c0b1cbbc8e.jpg" alt="">
|
||||
|
||||
唯一需要担心的一个问题是,如果丢消息了怎么办?因为现在消息是缓存数据的唯一来源,一旦出现丢消息,缓存里缺失的那条数据永远不会被补上。所以,必须保证整个消息链条的可靠性,不过好在现在的MQ集群,比如像Kafka或者RocketMQ,它都有高可用和高可靠的保证机制,只要你正确配置好,是可以满足数据可靠性要求的。
|
||||
|
||||
像订单服务这样,本来就有现成的数据变更消息可以订阅,这样更新缓存还是一个不错的选择,因为实现起来很简单,对系统的其他模块完全没有侵入。
|
||||
|
||||
## 使用Binlog实时更新Redis缓存
|
||||
|
||||
如果我们要缓存的数据,本来没有一份数据更新的MQ消息可以订阅怎么办?很多大厂都采用的,也是更通用的解决方案是这样的。
|
||||
|
||||
数据更新服务只负责处理业务逻辑,更新MySQL,完全不用管如何去更新缓存。负责更新缓存的服务,把自己伪装成一个MySQL的从节点,从MySQL接收Binlog,解析Binlog之后,可以得到实时的数据变更信息,然后根据这个变更信息去更新Redis缓存。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/91/12/918380c0e43de2f4ef7ad5e8e9d5d212.jpg" alt="">
|
||||
|
||||
这种收Binlog更新缓存的方案,和刚刚我们讲到的,收MQ消息更新缓存的方案,其实它们的实现思路是一样的,都是异步订阅实时数据变更信息,去更新Redis。只不过,直接读取Binlog这种方式,它的通用性更强。不要求订单服务再发订单消息了,订单更新服务也不用费劲去解决“发消息失败怎么办?”这种数据一致性问题了。
|
||||
|
||||
而且,在整个缓存更新链路上,减少了一个收发MQ的环节,从MySQL更新到Redis更新的时延更短,出现故障的可能性也更低,所以很多大厂更青睐于这种方案。
|
||||
|
||||
这个方案唯一的缺点是,实现订单缓存更新服务有点儿复杂,毕竟不像收消息,拿到的直接就是订单数据,解析Binlog还是挺麻烦的。
|
||||
|
||||
有很多开源的项目就提供了订阅和解析MySQL Binlog的功能,下面我们以比较常用的开源项目[Canal](https://github.com/alibaba/canal)为例,来演示一下如何实时接收Binlog更新Redis缓存。
|
||||
|
||||
Canal模拟MySQL 主从复制的交互协议,把自己伪装成一个MySQL的从节点,向MySQL主节点发送dump请求,MySQL收到请求后,就会开始推送Binlog给Canal,Canal解析Binlog字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。下图是Canal的工作原理:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/45/e4/452211795717190e55c5b0ff2ab208e4.jpg" alt="">
|
||||
|
||||
在我们这个示例中,MySQL和Redis都运行在本地的默认端口上,MySQL的端口为3306,Redis的端口为6379。为了便于大家操作,我们还是以《[04 | 事务:账户余额总是对不上账,怎么办?](https://time.geekbang.org/column/article/206544)》这节课中的账户余额表account_balance作为演示数据。
|
||||
|
||||
首先,下载并解压Canal 最新的1.1.4版本到本地:
|
||||
|
||||
```
|
||||
wget https://github.com/alibaba/canal/releases/download/canal-1.1.4/canal.deployer-1.1.4.tar.gz
|
||||
tar zvfx canal.deployer-1.1.4.tar.gz
|
||||
|
||||
```
|
||||
|
||||
然后来配置MySQL,我们需要在MySQL的配置文件中开启Binlog,并设置Binlog的格式为ROW格式。
|
||||
|
||||
```
|
||||
[mysqld]
|
||||
log-bin=mysql-bin # 开启Binlog
|
||||
binlog-format=ROW # 设置Binlog格式为ROW
|
||||
server_id=1 # 配置一个ServerID
|
||||
|
||||
```
|
||||
|
||||
给Canal开一个专门的MySQL用户并授权,确保这个用户有复制Binlog的权限:
|
||||
|
||||
```
|
||||
CREATE USER canal IDENTIFIED BY 'canal';
|
||||
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
```
|
||||
|
||||
重启一下MySQL,确保所有的配置生效。重启后检查一下当前的Binlog文件和位置:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/01/8f/01293d0ccc372418f3e01c785e204b8f.png" alt="">
|
||||
|
||||
记录下File和Position两列的值,然后我们来配置Canal。编辑Canal的实例配置文件canal/conf/example/instance.properties,以便让Canal连接到我们的MySQL上。
|
||||
|
||||
```
|
||||
canal.instance.gtidon=false
|
||||
|
||||
|
||||
# position info
|
||||
canal.instance.master.address=127.0.0.1:3306
|
||||
canal.instance.master.journal.name=binlog.000009
|
||||
canal.instance.master.position=155
|
||||
canal.instance.master.timestamp=
|
||||
canal.instance.master.gtid=
|
||||
|
||||
|
||||
# username/password
|
||||
canal.instance.dbUsername=canal
|
||||
canal.instance.dbPassword=canal
|
||||
canal.instance.connectionCharset = UTF-8
|
||||
canal.instance.defaultDatabaseName=test
|
||||
# table regex
|
||||
canal.instance.filter.regex=.*\\..
|
||||
|
||||
```
|
||||
|
||||
这个配置文件需要配置MySQL的连接地址、库名、用户名和密码之外,还需要配置canal.instance.master.journal.name和canal.instance.master.position这两个属性,取值就是刚刚记录的File和Position两列。然后就可以启动Canal服务了:
|
||||
|
||||
```
|
||||
canal/bin/startup.sh
|
||||
|
||||
```
|
||||
|
||||
启动之后看一下日志文件canal/logs/example/example.log,如果里面没有报错,就说明启动成功并连接到我们的MySQL上了。
|
||||
|
||||
Canal服务启动后,会开启一个端口(11111)等待客户端连接,客户端连接上Canal服务之后,可以从Canal服务拉取数据,每拉取一批数据,正确写入Redis之后,给Canal服务返回处理成功的响应。如果发生客户端程序宕机或者处理失败等异常情况,Canal服务没收到处理成功的响应,下次客户端来拉取的还是同一批数据,这样就可以保证顺序并且不会丢数据。
|
||||
|
||||
接下来我们来开发账户余额缓存的更新程序,以下的代码都是用Java语言编写的:
|
||||
|
||||
```
|
||||
while (true) {
|
||||
Message message = connector.getWithoutAck(batchSize); // 获取指定数量的数据
|
||||
long batchId = message.getId();
|
||||
try {
|
||||
int size = message.getEntries().size();
|
||||
if (batchId == -1 || size == 0) {
|
||||
Thread.sleep(1000);
|
||||
} else {
|
||||
processEntries(message.getEntries(), jedis);
|
||||
}
|
||||
|
||||
|
||||
connector.ack(batchId); // 提交确认
|
||||
} catch (Throwable t) {
|
||||
connector.rollback(batchId); // 处理失败, 回滚数据
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这个程序逻辑也不复杂,程序启动并连接到Canal服务后,就不停地拉数据,如果没有数据就睡一会儿,有数据就调用processEntries方法处理更新缓存。每批数据更新成功后,就调用ack方法给Canal服务返回成功响应,如果失败抛异常就回滚。下面是processEntries方法的主要代码:
|
||||
|
||||
```
|
||||
for (CanalEntry.RowData rowData : rowChage.getRowDatasList()) {
|
||||
if (eventType == CanalEntry.EventType.DELETE) { // 删除
|
||||
jedis.del(row2Key("user_id", rowData.getBeforeColumnsList()));
|
||||
} else if (eventType == CanalEntry.EventType.INSERT) { // 插入
|
||||
jedis.set(row2Key("user_id", rowData.getAfterColumnsList()), row2Value(rowData.getAfterColumnsList()));
|
||||
} else { // 更新
|
||||
jedis.set(row2Key("user_id", rowData.getAfterColumnsList()), row2Value(rowData.getAfterColumnsList()));
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里面根据事件类型来分别处理,如果MySQL中的数据删除了,就删除Redis中对应的数据。如果是更新和插入操作,那就调用Redis的SET命令来写入数据。
|
||||
|
||||
把这个账户缓存更新服务启动后,我们来验证一下,我们在账户余额表插入一条记录:
|
||||
|
||||
```
|
||||
mysql> insert into account_balance values (888, 100, NOW(), 999);
|
||||
|
||||
```
|
||||
|
||||
然后来看一下Redis缓存:
|
||||
|
||||
```
|
||||
127.0.0.1:6379> get 888
|
||||
"{\"log_id\":\"999\",\"balance\":\"100\",\"user_id\":\"888\",\"timestamp\":\"2020-03-08 16:18:10\"}"
|
||||
|
||||
```
|
||||
|
||||
可以看到数据已经自动同步到Redis中去了。我把这个示例的完整代码放在了[GitHub](https://github.com/liyue2008/canal-to-redis-example)上供你参考。
|
||||
|
||||
## 小结
|
||||
|
||||
在处理超大规模并发的场景时,由于并发请求的数量非常大,即使少量的缓存穿透,也有可能打死数据库引发雪崩效应。对于这种情况,我们可以缓存全量数据来彻底避免缓存穿透问题。
|
||||
|
||||
对于缓存数据更新的方法,可以订阅数据更新的MQ消息来异步更新缓存,更通用的方法是,把缓存更新服务伪装成一个MySQL的从节点,订阅MySQL的Binlog,通过Binlog来更新Redis缓存。
|
||||
|
||||
需要特别注意的是,无论是用MQ还是Canal来异步更新缓存,对整个更新服务的数据可靠性和实时性要求都比较高,数据丢失或者更新慢了,都会造成Redis中的数据与MySQL中数据不同步。在把这套方案应用到生产环境中去的时候,需要考虑一旦出现不同步问题时的降级或补偿方案。
|
||||
|
||||
## 思考题
|
||||
|
||||
课后请你思考一下,如果出现缓存不同步的情况,在你负责的业务场景下,该如何降级或者补偿?欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。
|
||||
82
极客时间专栏/后端存储实战课/海量数据篇/18 | 分布式存储:你知道对象存储是如何保存图片文件的吗?.md
Normal file
82
极客时间专栏/后端存储实战课/海量数据篇/18 | 分布式存储:你知道对象存储是如何保存图片文件的吗?.md
Normal file
@@ -0,0 +1,82 @@
|
||||
<audio id="audio" title="18 | 分布式存储:你知道对象存储是如何保存图片文件的吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0f/b3/0fe9c3058098500a18c7624399b193b3.mp3"></audio>
|
||||
|
||||
你好,我是李玥。
|
||||
|
||||
我们都知道,保存像图片、音视频这类大文件,最佳的选择就是对象存储。对象存储不仅有很好的大文件读写性能,还可以通过水平扩展实现近乎无限的容量,并且可以兼顾服务高可用、数据高可靠这些特性。
|
||||
|
||||
对象存储之所以能做到这么“全能”,最主要的原因是,**对象存储是原生的分布式存储系统**。这里我们讲的“原生分布式存储系统”,是相对于MySQL、Redis这类单机存储系统来说的。虽然这些非原生的存储系统,也具备一定的集群能力,但你也能感受到,用它们构建大规模分布式集群的时候,其实是非常不容易的。
|
||||
|
||||
随着云计算的普及,很多新生代的存储系统,都是原生的分布式系统,它们一开始设计的目标之一就是分布式存储集群,比如说[Elasticsearch](https://www.elastic.co/cn/)、[Ceph](http://about:blank)和国内很多大厂推出的新一代数据库,大多都可以做到:
|
||||
|
||||
- 近乎无限的存储容量;
|
||||
- 超高的读写性能;
|
||||
- 数据高可靠:节点磁盘损毁不会丢数据;
|
||||
- 实现服务高可用:节点宕机不会影响集群对外提供服务。
|
||||
|
||||
那这些原生分布式存储是如何实现这些特性的呢?
|
||||
|
||||
实际上不用我说,你也能猜得到,这里面同样存在严重的“互相抄作业”的情况。这个也可以理解,除了存储的数据结构不一样,提供的查询服务不一样以外,这些分布式存储系统,它们面临的很多问题都是一样的,那实现方法差不多也是可以理解。
|
||||
|
||||
对象存储它的查询服务和数据结构都非常简单,是最简单的原生分布式存储系统。这节课,我们就来一起来研究一下对象存储这种最简单的原生分布式存储,通过对象存储来认识一下分布式存储系统的一些共性。掌握了这些共性之后,你再去认识和学习其他的分布式存储系统,也会感觉特别容易。
|
||||
|
||||
## 对象存储数据是如何保存大文件的?
|
||||
|
||||
对象存储对外提供的服务,其实就是一个近乎无限容量的大文件KV存储,所以对象存储和分布式文件系统之间,没有那么明确的界限。对象存储的内部,肯定有很多的存储节点,用于保存这些大文件,这个就是数据节点的集群。
|
||||
|
||||
另外,我们为了管理这些数据节点和节点中的文件,还需要一个存储系统保存集群的节点信息、文件信息和它们的映射关系。这些为了管理集群而存储的数据,叫做元数据(Metadata)。
|
||||
|
||||
元数据对于一个存储集群来说是非常重要的,所以保存元数据的存储系统必须也是一个集群。但是元数据集群存储的数据量比较少,数据的变动不是很频繁,加之客户端或者网关都会缓存一部分元数据,所以元数据集群对并发要求也不高。一般使用类似[ZooKeeper](https://zookeeper.apache.org/)或者[etcd](https://github.com/etcd-io/etcd)这类分布式存储就可以满足要求。
|
||||
|
||||
另外,存储集群为了对外提供访问服务,还需要一个网关集群,对外接收外部请求,对内访问元数据和数据节点。网关集群中的每个节点不需要保存任何数据,都是无状态的节点。有些对象存储没有网关,取而代之的是客户端,它们的功能和作用都是一样的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/92/0b/925a6309372b30f660c9b8bc198f860b.jpg" alt="">
|
||||
|
||||
那么,对象存储是如何来处理对象读写请求的呢?这里面处理读和写请求的流程是一样的,我们一起来说。网关收到对象读写请求后,首先拿着请求中的Key,去元数据集群查找这个Key在哪个数据节点上,然后再去访问对应的数据节点读写数据,最后把结果返回给客户端。
|
||||
|
||||
以上是一个比较粗略的大致流程,实际上这里面包含很多的细节,我们暂时没有展开讲。目的是让你在整体上对对象存储,以至于分布式存储系统,有一个清晰的认知。
|
||||
|
||||
上面这张图,虽然我画的是对象存储集群的结构,但是把图上的名词改一改,完全可以套用到绝大多数分布式文件系统和数据库上去,比如说HDFS。
|
||||
|
||||
## 对象是如何拆分和保存的?
|
||||
|
||||
接下来我们说一下对象存储到底是如何来保存大文件对象的。一般来说,对象存储中保存的文件都是图片、视频这类大文件。在对象存储中,每一个大文件都会被拆成多个大小相等的**块儿(Block)**,拆分的方法很简单,就是把文件从头到尾按照固定的块儿大小,切成一块儿一块儿,最后一块儿长度有可能不足一个块儿的大小,也按一块儿来处理。块儿的大小一般配置为几十KB到几个MB左右。
|
||||
|
||||
把大对象文件拆分成块儿的目的有两个:
|
||||
|
||||
1. 第一是为了提升读写性能,这些块儿可以分散到不同的数据节点上,这样就可以并行读写。
|
||||
1. 第二是把文件分成大小相等块儿,便于维护管理。
|
||||
|
||||
对象被拆成块儿之后,还是太过于碎片化了,如果直接管理这些块儿,会导致元数据的数据量会非常大,也没必要管理到这么细的粒度。所以一般都会再把块儿聚合一下,放到块儿的容器里面。这里的“容器”就是存放一组块儿的逻辑单元。容器这个名词,没有统一的叫法,比如在[ceph](https://ceph.io/)中称为Data Placement,你理解这个含义就行。容器内的块儿数大多是固定的,所以容器的大小也是固定的。
|
||||
|
||||
到这里,这个容器的概念,就比较类似于我们之前讲MySQL和Redis时提到的“分片”的概念了,都是复制、迁移数据的基本单位。每个容器都会有N个副本,这些副本的数据都是一样的。其中有一个主副本,其他是从副本,主副本负责数据读写,从副本去到主副本上去复制数据,保证主从数据一致。
|
||||
|
||||
这里面有一点儿和我们之前讲的不一样的是,对象存储一般都不记录类似MySQL的Binlog这样的日志。主从复制的时候,复制的不是日志,而是整块儿的数据。这么做有两个原因:
|
||||
|
||||
1. 第一个原因是基于性能的考虑。我们知道操作日志里面,实际上就包含着数据。在更新数据的时候,先记录操作日志,再更新存储引擎中的数据,相当于在磁盘上串行写了2次数据。对于像数据库这种,每次更新的数据都很少的存储系统,这个开销是可以接受的。但是对于对象存储来说,它每次写入的块儿很大,两次磁盘IO的开销就有些不太值得了。
|
||||
1. 第二个原因是它的存储结构简单,即使没有日志,只要按照顺序,整块儿的复制数据,仍然可以保证主从副本的数据一致性。
|
||||
|
||||
以上我们说的对象(也就是文件)、块儿和容器,都是逻辑层面的概念,数据落实到副本上,这些副本就是真正物理存在了。这些副本再被分配到数据节点上保存起来。这里的数据节点就是运行在服务器上的服务进程,负责在本地磁盘上保存副本的数据。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8d/0b/8d6616675ca90df023d1622aa1f2ef0b.jpg" alt="">
|
||||
|
||||
了解了对象是如何被拆分并存储在数据节点上之后,我们再来回顾一下数据访问的流程。当我们请求一个Key的时候,网关首先去元数据中查找这个Key的元数据。然后根据元数据中记录的对象长度,计算出对象有多少块儿。接下来的过程就可以分块儿并行处理了。对于每个块儿,还需要再去元数据中,找到它被放在哪个容器中。
|
||||
|
||||
我刚刚讲过,容器就是分片,怎么把块儿映射到容器中,这个方法就是我们在《[15 | MySQL存储海量数据的最后一招:分库分表](https://time.geekbang.org/column/article/217568)》这节课中讲到的几种分片算法。不同的系统选择实现的方式也不一样,有用哈希分片的,也有用查表法把对应关系保存在元数据中的。找到容器之后,再去元数据中查找容器的N个副本都分布在哪些数据节点上。然后,网关直接访问对应的数据节点读写数据就可以了。
|
||||
|
||||
## 小结
|
||||
|
||||
对象存储是最简单的分布式存储系统,主要由数据节点集群、元数据集群和网关集群(或者客户端)三部分构成。数据节点集群负责保存对象数据,元数据集群负责保存集群的元数据,网关集群和客户端对外提供简单的访问API,对内访问元数据和数据节点读写数据。
|
||||
|
||||
为了便于维护和管理,大的对象被拆分为若干固定大小的块儿,块儿又被封装到容器(也就分片)中,每个容器有一主N从多个副本,这些副本再被分散到集群的数据节点上保存。
|
||||
|
||||
对象存储虽然简单,但是它具备一个分布式存储系统的全部特征。所有分布式存储系统共通的一些特性,对象存储也都具备,比如说数据如何分片,如何通过多副本保证数据可靠性,如何在多个副本间复制数据,确保数据一致性等等。
|
||||
|
||||
希望你通过这节课的学习,不仅是学会对象存储,还要对比分析一下,对象存储和其他分布式存储系统,比如MySQL集群、HDFS、Elasticsearch等等这些,它们之间有什么共同的地方,差异在哪儿。想通了这些问题,你对分布式存储系统的认知,绝对会上升到一个全新的高度。然后你再去看一些之前不了解的存储系统,就非常简单了。
|
||||
|
||||
## 思考题
|
||||
|
||||
我们刚刚说到过,对象存储并不是基于日志来进行主从复制的。假设我们的对象存储是一主二从三个副本,采用半同步方式复制数据,也就是主副本和任意一个从副本更新成功后,就给客户端返回成功响应。主副本所在节点宕机之后,这两个从副本中,至少有一个副本上的数据是和宕机的主副本上一样的,我们需要找到这个副本作为新的主副本,才能保证宕机不丢数据。
|
||||
|
||||
但是没有了日志,如果这两个从副本上的数据不一样,我们如何确定哪个上面的数据是和主副本一样新呢?欢迎你在留言区与我交流讨论。
|
||||
|
||||
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。
|
||||
71
极客时间专栏/后端存储实战课/海量数据篇/19 | 跨系统实时同步数据,分布式事务是唯一的解决方案吗?.md
Normal file
71
极客时间专栏/后端存储实战课/海量数据篇/19 | 跨系统实时同步数据,分布式事务是唯一的解决方案吗?.md
Normal file
@@ -0,0 +1,71 @@
|
||||
<audio id="audio" title="19 | 跨系统实时同步数据,分布式事务是唯一的解决方案吗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e7/50/e76664d75360b92e4fecd62a9bb18c50.mp3"></audio>
|
||||
|
||||
你好,我是李玥。
|
||||
|
||||
我们在《[15 | MySQL存储海量数据的最后一招:分库分表](https://time.geekbang.org/column/article/217568)》这节课中讲过,数据量太大的时候,单个存储节点存不下,那就只能把数据分片存储。
|
||||
|
||||
数据分片之后,我们对数据的查询就没那么自由了。比如订单表如果按照用户ID作为Sharding Key来分片,那就只能按照用户维度来查询。如果我是一个商家,我想查我店铺的订单,对不起,做不到了。(当然,强行查也不是不行,在所有分片上都查一遍,再把结果聚合起来,又慢又麻烦,实际意义不大。)
|
||||
|
||||
对于这样的需求,普遍的解决办法是用空间换时间,毕竟现在存储越来越便宜。再存一份订单数据到商家订单库,然后以店铺ID作为Sharding Key分片,专门供商家查询订单。
|
||||
|
||||
另外,之前我们在《[06 | 如何用Elasticsearch构建商品搜索系统](https://time.geekbang.org/column/article/208675)》这节课也讲到过,同样一份商品数据,如果我们是按照关键字搜索,放在ES里就比放在MySQL快了几个数量级。原因是,数据组织方式、物理存储结构和查询方式,对查询性能的影响是巨大的,而且海量数据还会指数级地放大这个性能差距。
|
||||
|
||||
所以,在大厂中,对于海量数据的处理原则,都是根据业务对数据查询的需求,反过来确定选择什么数据库、如何组织数据结构、如何分片数据,这样才能达到最优的查询性能。同样一份订单数据,除了在订单库保存一份用于在线交易以外,还会在各种数据库中,以各种各样的组织方式存储,用于满足不同业务系统的查询需求。像BAT这种大厂,它的核心业务数据,存个几十上百份是非常正常的。
|
||||
|
||||
那么问题来了,如何能够做到让这么多份数据实时地保持同步呢?
|
||||
|
||||
我们之前讲过分布式事务,可以解决数据一致性的问题。比如说,你可以用本地消息表,把一份数据实时同步给另外两、三个数据库,这样还可以接受,太多的话也是不行的,并且对在线交易业务还有侵入性,所以分布式事务是解决不了这个问题的。
|
||||
|
||||
今天这节课我们就来说一下,如何把订单数据实时、准确无误地同步到这么多异构的数据中去。
|
||||
|
||||
## 使用Binlog和MQ构建实时数据同步系统
|
||||
|
||||
早期大数据刚刚兴起的时候,大多数系统还做不到异构数据库实时同步,那个时候普遍的做法是,使用ETL工具定时同步数据,在T+1时刻去同步上一个周期的数据,然后再做后续的计算和分析。定时ETL对于一些需要实时查询数据的业务需求就无能为力了。所以,这种定时同步的方式,基本上都被实时同步的方式给取代了。
|
||||
|
||||
怎么来做这么大数据量、这么多个异构数据库的实时同步呢?你还记得我在《[17 | 大厂都是怎么做MySQL to Redis同步的](https://time.geekbang.org/column/article/217593)》这节课中讲到的方法吧?利用Canal把自己伪装成一个MySQL的从库,从MySQL实时接收Binlog然后写入Redis中。把这个方法稍微改进一下,就可以用来做异构数据库的同步了。
|
||||
|
||||
为了能够支撑下游众多的数据库,从Canal出来的Binlog数据肯定不能直接去写下游那么多数据库,一是写不过来,二是对于每个下游数据库,它可能还有一些数据转换和过滤的工作要做。所以需要增加一个MQ来解耦上下游。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/df/d8/dfa37d67d87fc7c8a8de50681f8134d8.jpg" alt="">
|
||||
|
||||
Canal从MySQL收到Binlog并解析成结构化数据之后,直接写入到MQ的一个订单Binlog主题中,然后每一个需要同步订单数据的业务方,都去订阅这个MQ中的订单Binlog主题,消费解析后的Binlog数据。在每个消费者自己的同步程序中,它既可以直接入库,也可以做一些数据转换、过滤或者计算之后再入库,这样就比较灵活了。
|
||||
|
||||
## 如何保证数据同步的实时性
|
||||
|
||||
这个方法看起来不难,但是非常容易出现性能问题。有些接收Binlog消息的下游业务,对数据的实时性要求比较高,不能容忍太高的同步时延。比如说,每个电商在大促的时候,都会有一个大屏幕,实时显示现在有多少笔交易,交易额是多少。这个东西都是给老板们看的,如果说大促的时候,你让老板们半小时之后才看到数字,那估计你就得走人了。
|
||||
|
||||
大促的时候,数据量大、并发高、数据库中的数据变动频繁,同步的Binlog流量也非常大。为了保证这个同步的实时性,整个数据同步链条上的任何一个环节,它的处理速度都必须得跟得上才行。我们一步一步分析可能会出现性能瓶颈的环节。
|
||||
|
||||
源头的订单库,如果它出现繁忙,对业务的影响就不只是大屏延迟了,那就影响到用户下单了,这个问题是数据库本身要解决的,这里我们不考虑。再顺着数据流向往下看,Canal和MQ这两个环节,由于没什么业务逻辑,性能都非常好。所以,**一般容易成为性能瓶颈的就是消费MQ的同步程序**,因为这些同步程序里面一般都会有一些业务逻辑,而且如果下游的数据库写性能跟不上,表象也是这个同步程序处理性能上不来,消息积压在MQ里面。
|
||||
|
||||
那我们能不能多加一些同步程序的实例数,或者增加线程数,通过增加并发来提升处理能力呢?这个地方的并发数,还真不是随便说扩容就可以就扩容的,我来跟你讲一下为什么。
|
||||
|
||||
我们知道,MySQL主从同步Binlog,是一个单线程的同步过程。为什么是单线程?原因很简单,在从库执行Binlog的时候,必须按顺序执行,才能保证数据和主库是一样的。**为了确保数据一致性,Binlog的顺序很重要,是绝对不能乱序的。** 严格来说,对于每一个MySQL实例,整个处理链条都必须是单线程串行执行,MQ的主题也必须设置为只有1个分区(队列),这样才能保证数据同步过程中的Binlog是严格有序的,写到目标数据库的数据才能是正确的。
|
||||
|
||||
那单线程处理速度上不去,消息越积压越多,这不无解了吗?其实办法还是有的,但是必须得和业务结合起来解决。
|
||||
|
||||
还是拿订单库来说啊,其实我们并不需要对订单库所有的更新操作都严格有序地执行,比如说A和B两个订单号不同的订单,这两个订单谁先更新谁后更新并不影响数据的一致性,因为这两个订单完全没有任何关系。但是同一个订单,如果更新的Binlog执行顺序错了,那同步出来的订单数据真的就错了。
|
||||
|
||||
也就是说,我们只要保证每个订单的更新操作日志的顺序别乱就可以了。这种一致性要求称为**因果一致性(Causal Consistency)**,有因果关系的数据之间必须要严格地保证顺序,没有因果关系的数据之间的顺序是无所谓的。
|
||||
|
||||
基于这个理论基础,我们就可以并行地来进行数据同步,具体的做法是这样的。
|
||||
|
||||
首先根据下游同步程序的消费能力,计算出需要多少并发;然后设置MQ中主题的分区(队列)数量和并发数一致。因为MQ是可以保证同一分区内,消息是不会乱序的,所以我们需要把具有因果关系的Binlog都放到相同的分区中去,就可以保证同步数据的因果一致性。对应到订单库就是,相同订单号的Binlog必须发到同一个分区上。
|
||||
|
||||
这是不是和之前讲过的数据库分片有点儿像呢?那分片算法就可以拿过来复用了,比如我们可以用最简单的哈希算法,Binlog中订单号除以MQ分区总数,余数就是这条Binlog消息发往的分区号。
|
||||
|
||||
Canal自带的分区策略就支持按照指定的Key,把Binlog哈希到下游的MQ中去,具体的配置可以看一下[Canal接入MQ的文档](https://github.com/alibaba/canal/wiki/Canal-Kafka-RocketMQ-QuickStart)。
|
||||
|
||||
## 小结
|
||||
|
||||
对于海量数据,必须要按照查询方式选择数据库类型和数据的组织方式,才能达到理想的查询性能。这就需要把同一份数据,按照不同的业务需求,以不同的组织方式存放到各种异构数据库中。因为数据的来源大多都是在线交易系统的MySQL数据库,所以我们可以利用MySQL的Binlog来实现异构数据库之间的实时数据同步。
|
||||
|
||||
为了能够支撑众多下游数据库实时同步的需求,可以通过MQ解耦上下游,Binlog先发送到MQ中,下游各业务方可以消费MQ中的消息再写入各自的数据库。
|
||||
|
||||
如果下游处理能力不能满足要求,可以增加MQ中的分区数量实现并发同步,但需要结合同步的业务数据特点,把具有因果关系的数据哈希到相同分区上,才能避免因为并发乱序而出现数据同步错误的问题。
|
||||
|
||||
## 思考题
|
||||
|
||||
在我们这种数据同步架构下,如果说下游的某个同步程序或数据库出了问题,需要把Binlog回退到某个时间点然后重新同步,这个问题该怎么解决?欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。
|
||||
86
极客时间专栏/后端存储实战课/海量数据篇/20 | 如何在不停机的情况下,安全地更换数据库?.md
Normal file
86
极客时间专栏/后端存储实战课/海量数据篇/20 | 如何在不停机的情况下,安全地更换数据库?.md
Normal file
@@ -0,0 +1,86 @@
|
||||
<audio id="audio" title="20 | 如何在不停机的情况下,安全地更换数据库?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/3e/8c/3e831695f95ca084d076ad803f05c68c.mp3"></audio>
|
||||
|
||||
你好,我是李玥。
|
||||
|
||||
随着我们的系统规模逐渐增长,总会遇到需要更换数据库的问题。我们来说几种常见的情况。
|
||||
|
||||
- 对MySQL做了分库分表之后,需要从原来的单实例数据库迁移到新的数据库集群上。
|
||||
- 系统从传统部署方式向云上迁移的时候,也需要从自建的数据库迁移到云数据库上。
|
||||
- 一些在线分析类的系统,MySQL性能不够用的时候,就需要更换成一些专门的分析类数据库,比如说HBase。
|
||||
|
||||
更换数据库这个事儿,是一个非常大的技术挑战,因为我们需要保证整个迁移过程中,既不能长时间停服,也不能丢数据。
|
||||
|
||||
那么,今天这节课我们就来说一下,如何在不停机的情况下,安全地迁移数据更换数据库。
|
||||
|
||||
## 如何实现不停机更换数据库?
|
||||
|
||||
我们都知道墨菲定律:“如果事情有变坏的可能,不管这种可能性有多小,它总会发生。”放到这里呢,也就是说,我们在更换数据库的过程中,只要有一点儿可能会出问题的地方,哪怕是出现问题的概率非常小,它总会出问题。
|
||||
|
||||
实际上,无论是新版本的程序,还是新的数据库,即使我们做了严格的验证测试,做了高可用方案,刚刚上线的系统,它的稳定性总是没有那么好的,需要一个磨合的过程,才能逐步达到一个稳定的状态,这是一个客观规律。这个过程中一旦出现故障,如果不能及时恢复,造成的损失往往是我们承担不起的。
|
||||
|
||||
所以我们在设计迁移方案的时候,一定要做到,每一步都是可逆的。**要保证,每执行一个步骤后,一旦出现问题,能快速地回滚到上一个步骤**。这是很多同学在设计这种升级类技术方案的时候,容易忽略的问题。
|
||||
|
||||
接下来我们还是以订单库为例子,说一下这个迁移方案应该如何来设计。
|
||||
|
||||
首先要做的就是,把旧库的数据复制到新库中。因为旧库还在服务线上业务,所以不断会有订单数据写入旧库,我们不仅要往新库复制数据,还要保证新旧两个库的数据是实时同步的。所以,我们需要用一个同步程序来实现新旧两个数据库实时同步。
|
||||
|
||||
怎么来实现两个异构数据库之间的数据实时同步,这个方法我们上节课刚刚讲过,我们可以使用Binlog实时同步数据。如果源库不是MySQL的话,就麻烦一点儿,但也可以参考我们讲过的,复制状态机理论来实现。这一步不需要回滚,原因是,只增加了一个新库和一个同步程序,对系统的旧库和程序都没有任何改变。即使新上线的同步程序影响到了旧库,只要停掉同步程序就可以了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ba/58/ba2a44c70d4766b281107f4134fe9d58.jpg" alt="">
|
||||
|
||||
然后,我们需要改造一下订单服务,业务逻辑部分不需要变,DAO层需要做如下改造:
|
||||
|
||||
1. 支持双写新旧两个库,并且预留热切换开关,能通过开关控制三种写状态:只写旧库、只写新库和同步双写。
|
||||
1. 支持读新旧两个库,同样预留热切换开关,控制读旧库还是新库。
|
||||
|
||||
然后上线新版的订单服务,这个时候订单服务仍然是只读写旧库,不读写新库。让这个新版的订单服务需要稳定运行至少一到二周的时间,期间除了验证新版订单服务的稳定性以外,还要验证新旧两个订单库中的数据是否是一致的。这个过程中,如果新版订单服务有问题,可以立即下线新版订单服务,回滚到旧版本的订单服务。
|
||||
|
||||
稳定一段时间之后,就可以开启订单服务的双写开关了。开启双写开关的同时,需要停掉同步程序。这里面有一个问题需要注意一下,就是**这个双写的业务逻辑,一定是先写旧库,再写新库,并且以写旧库的结果为准**。
|
||||
|
||||
旧库写成功,新库写失败,返回写成功,但这个时候要记录日志,后续我们会用到这个日志来验证新库是否还有问题。旧库写失败,直接返回失败,就不写新库了。这么做的原因是,不能让新库影响到现有业务的可用性和数据准确性。上面这个过程如果出现问题,可以关闭双写,回滚到只读写旧库的状态。
|
||||
|
||||
切换到双写之后,新库与旧库的数据可能会存在不一致的情况,原因有两个:一是停止同步程序和开启双写,这两个过程很难做到无缝衔接,二是双写的策略也不保证新旧库强一致,这时候我们需要上线一个对比和补偿的程序,这个程序对比旧库最近的数据变更,然后检查新库中的数据是否一致,如果不一致,还要进行补偿。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e0/ce/e0c3864866fe1ff3408e2589669b62ce.jpg" alt="">
|
||||
|
||||
开启双写后,还需要至少稳定运行至少几周的时间,并且期间我们要不断地检查,确保不能有旧库写成功,新库写失败的情况出现。对比程序也没有发现新旧两个库的数据有不一致的情况,这个时候,我们就可以认为,新旧两个库的数据是一直保持同步的。
|
||||
|
||||
接下来就可以用类似灰度发布的方式,把读请求一点儿一点儿地切到新库上。同样,期间如果出问题的话,可以再切回旧库。全部读请求都切换到新库上之后,这个时候其实读写请求就已经都切换到新库上了,实际的切换已经完成了,但还有后续的收尾步骤。
|
||||
|
||||
再稳定一段时间之后,就可以停掉对比程序,把订单服务的写状态改为只写新库。到这里,旧库就可以下线了。注意,整个迁移过程中,只有这个步骤是不可逆的。但是,这步的主要操作就是摘掉已经不再使用的旧库,对于在用的新库并没有什么改变,实际出问题的可能性已经非常小了。
|
||||
|
||||
到这里,我们就完成了在线更换数据库的全部流程。双写版本的订单服务也就完成了它的历史使命,可以在下一次升级订单服务版本的时候,下线双写功能。
|
||||
|
||||
## 如何实现对比和补偿程序?
|
||||
|
||||
在上面的整个切换过程中,如何实现这个对比和补偿程序,是整个这个切换设计方案中的一个难点。这个对比和补偿程序的难度在于,我们要对比的是两个都在随时变换的数据库中的数据。这种情况下,我们没有类似复制状态机这样理论上严谨实际操作还很简单的方法,来实现对比和补偿。但还是可以根据业务数据的实际情况,来针对性地实现对比和补偿,经过一段时间,把新旧两个数据库的差异,逐渐收敛到一致。
|
||||
|
||||
像订单这类时效性强的数据,是比较好对比和补偿的。因为订单一旦完成之后,就几乎不会再变了,那我们的对比和补偿程序,就可以依据订单完成时间,每次只对比这个时间窗口内完成的订单。补偿的逻辑也很简单,发现不一致的情况后,直接用旧库的订单数据覆盖新库的订单数据就可以了。
|
||||
|
||||
这样,切换双写期间,少量不一致的订单数据,等到订单完成之后,会被补偿程序修正。后续只要不是双写的时候,新库频繁写入失败,就可以保证两个库的数据完全一致。
|
||||
|
||||
比较麻烦的是更一般的情况,比如像商品信息这类数据,随时都有可能会变化。如果说数据上有更新时间,那我们的对比程序可以利用这个更新时间,每次在旧库取一个更新时间窗口内的数据,去新库上找相同主键的数据进行对比,发现数据不一致,还要对比一下更新时间。如果新库数据的更新时间晚于旧库数据,那可能是对比期间数据发生了变化,这种情况暂时不要补偿,放到下个时间窗口去继续对比。另外,时间窗口的结束时间,不要选取当前时间,而是要比当前时间早一点儿,比如1分钟前,避免去对比正在写入的数据。
|
||||
|
||||
如果数据连时间戳也没有,那只能去旧库读取Binlog,获取数据变化,然后去新库对比和补偿。
|
||||
|
||||
有一点需要说明的是,上面这些方法,如果严格推敲,都不是百分之百严谨的,都不能保证在任何情况下,经过对比和补偿后,新库的数据和旧库就是完全一样的。但是,在大多数情况下,这些实践方法还是可以有效地收敛新旧两个库的数据差异,你可以酌情采用。
|
||||
|
||||
## 小结
|
||||
|
||||
设计在线切换数据库的技术方案,首先要保证安全性,确保每一个步骤一旦失败,都可以快速回滚。此外,还要确保迁移过程中不丢数据,这主要是依靠实时同步程序和对比补偿程序来实现。
|
||||
|
||||
我把这个复杂的切换过程的要点,按照顺序总结成下面这个列表,供你参考:
|
||||
|
||||
1. 上线同步程序,从旧库中复制数据到新库中,并实时保持同步;
|
||||
1. 上线双写订单服务,只读写旧库;
|
||||
1. 开启双写,同时停止同步程序;
|
||||
1. 开启对比和补偿程序,确保新旧数据库数据完全一样;
|
||||
1. 逐步切量读请求到新库上;
|
||||
1. 下线对比补偿程序,关闭双写,读写都切换到新库上;
|
||||
1. 下线旧库和订单服务的双写功能。
|
||||
|
||||
## 思考题
|
||||
|
||||
我们整个切换的方案中,只有一个步骤是不可逆的,就是由双写切换为单写新库这一步。如果说不计成本,如何修改我们的迁移方案,让这一步也能做到快速回滚?你可以思考一下这个问题,欢迎你在留言区与我交流讨论。
|
||||
|
||||
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。
|
||||
73
极客时间专栏/后端存储实战课/海量数据篇/21 | 类似“点击流”这样的海量数据应该如何存储?.md
Normal file
73
极客时间专栏/后端存储实战课/海量数据篇/21 | 类似“点击流”这样的海量数据应该如何存储?.md
Normal file
@@ -0,0 +1,73 @@
|
||||
<audio id="audio" title="21 | 类似“点击流”这样的海量数据应该如何存储?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/45/44/45df906626d9a53e9eafebf5e3880f44.mp3"></audio>
|
||||
|
||||
你好,我是李玥。
|
||||
|
||||
对于大部分互联网公司来说,数据量最大的几类数据是:点击流数据、监控数据和日志数据。这里面“点击流”指的是在App、小程序和Web页面上的埋点数据,这些埋点数据记录用户的行为,比如你打开了哪个页面,点击了哪个按钮,在哪个商品上停留了多久等等这些。
|
||||
|
||||
当然你不用太担心自己的隐私问题,记录的这些行为数据不是为了监控用户,主要目的是为了从统计上分析群体用户的行为,从而改进产品和运营。比如,某件商品看的人很多,停留时间很长,最后下单购买的人却很少,那采销人员就要考虑是不是这件商品的定价太高了。
|
||||
|
||||
除了点击流数据以外,监控和日志数据都是大家常用的,我就不再多解释了。
|
||||
|
||||
这类数据都是真正“海量”的数据,相比于订单、商品这类业务的数据,数据量要多出2~3个数量级。每天产生的数据量就可能会超过TB(1 TB = 1024 GB)级别,经过一段时间累积下来,有些数据会达到PB(1 PB = 1024 TB)级别。
|
||||
|
||||
这种量级的数据,在大数据技术出现之前,是没法保存和处理的,只能是通过抽样的方法来凑合着做分析。Hadoop等大数据技术出现以后,才使得存储和计算海量数据成为可能。
|
||||
|
||||
今天这节课,我们来说说,应该选择什么样的存储系统,来保存像“点击流”这样的海量数据。
|
||||
|
||||
## 使用Kafka存储海量原始数据
|
||||
|
||||
早期对于这类海量原始数据,都倾向于**先计算再存储**。也就是,在接收原始数据的服务中,先进行一些数据过滤、聚合等初步的计算,将数据先收敛一下,再落存储。这样可以降低存储系统的写入压力,也能节省磁盘空间。
|
||||
|
||||
这几年,随着存储设备越来越便宜,并且,数据的价值被不断地重新挖掘,更多的大厂都倾向于**先存储再计算**,直接保存海量的原始数据,再对数据进行实时或者批量计算。这种方案,除了贵以外都是优点:
|
||||
|
||||
- 不需要二次分发就可以同时给多个流和批计算任务提供数据;
|
||||
- 如果计算任务出错,可以随时回滚重新计算;
|
||||
- 如果对数据有新的分析需求,上线后直接就可以用历史数据计算出结果,而不用去等新数据。
|
||||
|
||||
但是,这种方式对保存原始数据的存储系统要求就很高了:既要有足够大的容量,能水平扩容,还要读写都足够快,跟得上数据生产的写入速度,还要给下游计算提供低延迟的读服务。什么样的存储能满足这样的要求呢?这里我给出几种常用的解决方案。
|
||||
|
||||
第一种方案是,使用Kafka来存储。有的同学会问了,Kafka不是一个消息队列么,怎么成了存储系统了?那我告诉你,**现代的消息队列,本质上就是分布式的流数据存储系统。**
|
||||
|
||||
如果你感兴趣的话,你可以仔细去研究一下Kafka,它的数据是如何存储、分片、复制的?它是如何保证高可用,如何保证数据一致性的?那你会发现它和我们之前讲过的那些分布式存储系统,并没有什么太大的区别。唯一的区别就是,它的查询语言(生产和消费消息)和存储引擎的数据结构(Commit Log)比一般的存储系统要简单很多。但也正是因为这个原因,使得Kafka的读写性能远远好于其他的存储系统。Kafka官方给自己的定位也是“分布式流数据平台”,不只是一个MQ。
|
||||
|
||||
Kafka提供“无限”的消息堆积能力,具有超高的吞吐量,可以满足我们保存原始数据的大部分要求。写入点击流数据的时候,每个原始数据采集服务作为一个生产者,把数据发给Kafka就可以了。下游的计算任务,可以作为消费者订阅消息,也可以按照时间或者位点来读取数据。并且,Kafka作为事实标准,和大部分大数据生态圈的开源软件都有非常好的兼容性和集成度,像Flink、Spark等大多计算平台都提供了直接接入Kafka的组件。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ba/8c/ba6bae1b4e59ba2000f0789886248d8c.jpg" alt="">
|
||||
|
||||
当然,Kafka也不是万能的,你可能注意到了,我刚刚讲Kafka提供“无限”的消息堆积能力,我在这个“无限”上打了个引号,这里面还是有一些限制需要注意的。Kafka也支持把数据分片,这个在Kafka中叫Partition,每个分片可以分布到不同的存储节点上。
|
||||
|
||||
写入数据的时候,可以均匀地写到这些分片上,理论上只要分片足够多,存储容量就可以是“无限”的。但是,单个分片总要落到某一个节点上,而单节点的存储容量毕竟是有限的,随着时间推移,单个分片总有写满的时候。
|
||||
|
||||
即使它支持扩容分片数量,也没办法像其他分布式存储系统那样,重新分配数据,把已有分片上的数据迁移一部分到新的分片上。所以扩容分片也解决不了已有分片写满的问题。而Kafka又不支持按照时间维度去分片,所以,**受制于单节点的存储容量,Kafka实际能存储的数据容量并不是无限的**。
|
||||
|
||||
## Kafka之外还有哪些解决方案?
|
||||
|
||||
所以,需要长时间(几个月-几年)保存的海量数据,就不适合用Kafka存储。这种情况下,只能退而求其次,使用第二种方案了。
|
||||
|
||||
第二种方案是,使用HDFS来存储。使用HDFS存储数据也很简单,就是把原始数据写成一个一个文本文件,保存到HDFS中。我们需要按照时间和业务属性来组织目录结构和文件名,以便于下游计算程序来读取,比如说:**“click/20200808/Beijing_0001.csv”**,代表2020年8月8日,从北京地区用户收集到的点击流数据,这个是当天的第一个文件。
|
||||
|
||||
对于保存海量的原始数据这个特定的场景来说,HDFS的吞吐量是远不如Kafka的。按照平均到每个节点上计算,Kafka的吞吐能力很容易达到每秒钟大几百兆,而HDFS只能达到百兆左右。这就意味着,要达到相同的吞吐能力,使用HDFS就要比使用Kafka,多用几倍的服务器数量。
|
||||
|
||||
但HDFS也有它的优势,第一个优势就是,它能提供真正无限的存储容量,如果存储空间不够了,水平扩容就可以解决。另外一个优势是,HDFS能提供比Kafka更强的数据查询能力。Kafka只能按照时间或者位点来提取数据,而HDFS配合Hive直接就可以支持用SQL对数据进行查询,虽然说查询的性能比较差,但查询能力要比Kafka强大太多了。
|
||||
|
||||
以上这两种方案因为都有各自的优势和不足,在实际生产中,都有不少的应用,你可以根据业务的情况来选择。那有没有兼顾这二者优势的方案呢?最好能做到,既有超高的吞吐能力,又能无限扩容,同时还能提供更好的查询能力,有这样的好事儿么?
|
||||
|
||||
我个人的判断是,目前还没有可用大规模于生产的,成熟的解决方案,但未来应该会有的。目前已经有一些的开源项目,都致力于解决这方面的问题,你可以关注一下。
|
||||
|
||||
一类是**分布式流数据存储**,比较活跃的项目有[Pravega](https://github.com/pravega/pravega)和Pulsar的存储引擎[Apache BookKeeper](https://github.com/apache/bookkeeper)。我所在的团队也在这个方向上持续探索中,也开源了我们的流数据存储项目[JournalKeeper](https://github.com/chubaostream/journalkeeper),也欢迎你关注和参与进来。这些分布式流数据存储系统,走的是类似Kafka这种流存储的路线,在高吞吐量的基础上,提供真正无限的扩容能力,更好的查询能力。
|
||||
|
||||
还有一类是**时序数据库(Time Series Databases)**,比较活跃的项目有[InfluxDB](https://github.com/influxdata/influxdb)和[OpenTSDB](https://github.com/OpenTSDB/opentsdb)等。这些时序数据库,不仅有非常好的读写性能,还提供很方便的查询和聚合数据的能力。但是,它们不是什么数据都可以存的,它们专注于类似监控数据这样,有时间特征并且数据内容都是数值的数据。如果你有存储海量监控数据的需求,可以关注一下这些项目。
|
||||
|
||||
## 小结
|
||||
|
||||
在互联网行业,点击流、监控和日志这几类数据,是海量数据中的海量数据。对于这类数据,一般的处理方式都是先存储再计算,计算结果保存到特定的数据库中,供业务系统查询。
|
||||
|
||||
所以,对于海量原始数据的存储系统,我们要求的是超高的写入和读取性能,和近乎无限的容量,对于数据的查询能力要求不高。生产上,可以选择Kafka或者是HDFS,Kafka的优点是读写性能更好,单节点能支持更高的吞吐量。而HDFS则能提供真正无限的存储容量,并且对查询更友好。
|
||||
|
||||
未来会有一些开源的流数据存储系统和时序数据库逐步成熟,并陆续应用到生产系统中去,你可以持续关注这些项目。
|
||||
|
||||
## 思考题
|
||||
|
||||
课后请你想一下,为什么Kafka能做到几倍于HDFS的吞吐能力,技术上的根本原因是什么?欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。
|
||||
70
极客时间专栏/后端存储实战课/海量数据篇/22 | 面对海量数据,如何才能查得更快?.md
Normal file
70
极客时间专栏/后端存储实战课/海量数据篇/22 | 面对海量数据,如何才能查得更快?.md
Normal file
@@ -0,0 +1,70 @@
|
||||
<audio id="audio" title="22 | 面对海量数据,如何才能查得更快?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6d/db/6da98294605e6f78fdfa9e7ced4ec8db.mp3"></audio>
|
||||
|
||||
你好,我是李玥。
|
||||
|
||||
我们接着上节课的话题,来继续说海量数据。上节课我们讲了,如何来保存原始数据,那我们知道,原始数据的数据量太大了,能存下来就很不容易了,这个数据是没法直接来给业务系统查询和分析的。有两个原因,一是数据量太大了,二是也没有很好的数据结构和查询能力,来支持业务系统查询。
|
||||
|
||||
所以一般的做法是,用流计算或者是批计算,把原始数据再进行一次或者多次的过滤、汇聚和计算,把计算结果落到另外一个存储系统中去,由这个存储再给业务系统提供查询支持。这里的“流计算”,指的是Flink、Storm这类的实时计算,批计算是Map-Reduce或者Spark这类的非实时计算。
|
||||
|
||||
上节课我们说过,像点击流、监控和日志这些原始数据是“海量数据中的海量数据”,这些原始数据经过过滤汇总和计算之后,大多数情况下数据量会有量级的下降,比如说从TB级别的数据量,减少到GB级别。
|
||||
|
||||
有的业务,计算后的数据非常少,比如说一些按天粒度的汇总数据,或者排行榜类的数据,用什么存储都能满足要求。那有一些业务,没法通过事先计算的方式解决全部的问题。原始数据经过计算后产生的计算结果,数据量相比原始数据会减少一些,但仍然是海量数据。并且,我们还要在这个海量数据上,提供性能可以接受的查询服务。
|
||||
|
||||
今天这节课我们就来聊一聊,面对这样的海量数据,如何才能让查询更快一些。
|
||||
|
||||
## 常用的分析类系统应该如何选择存储?
|
||||
|
||||
查询海量数据的系统,大多都是离线分析类系统,你可以简单地理解为类似于做报表的系统,也就是那些主要功能是对数据做统计分析的系统。这类系统是重度依赖于存储的。选择什么样的存储系统、使用什么样的数据结构来存储数据,直接决定了数据查询、聚合和分析的性能。
|
||||
|
||||
分析类系统对存储的需求一般是这样的:
|
||||
|
||||
1. 一般用于分析的数据量都会比在线业务大出几个数量级,这需要存储系统能保存海量数据;
|
||||
1. 能在海量的数据上做快速的聚合、分析和查询。注意这里面所说的“快速”,前提是处理GB、TB甚至PB级别的海量数据,在这么大的数据量上做分析,几十秒甚至几分钟都算很快了,和在线业务要求的毫秒级速度是不一样的;
|
||||
1. 由于数据大多数情况下都是异步写入,对于写入性能和响应时延,一般要求不高;
|
||||
1. 分析类系统不直接支撑前端业务,所以也不要求高并发。
|
||||
|
||||
然后我们看有哪些可供选择的存储产品。如果你的系统的数据量在GB量级以下,MySQL仍然是可以考虑的,因为它的查询能力足以应付大部分分析系统的业务需求。并且可以和在线业务系统合用一个数据库,不用做ETL(数据抽取),省事儿并且实时性好。这里还是要提醒你,最好给分析系统配置单独的MySQL实例,避免影响线上业务。
|
||||
|
||||
如果数据量级已经超过MySQL极限,可以选择一些列式数据库,比如:HBase、Cassandra、ClickHouse,这些产品对海量数据,都有非常好的查询性能,在正确使用的前提下,10GB量级的数据查询基本上可以做到秒级返回。高性能的代价是功能上的缩水,这些数据库对数据的组织方式都有一些限制,查询方式上也没有MySQL那么灵活。大多都需要你非常了解这些产品的脾气秉性,按照预定的姿势使用,才能达到预期的性能。
|
||||
|
||||
另外一个值得考虑的选择是Elasticsearch(ES),ES本来是一个为了搜索而生的存储产品,但是也支持结构化数据的存储和查询。由于它的数据都存储在内存中,并且也支持类似于Map-Reduce方式的分布式并行查询,所以对海量结构化数据的查询性能也非常好。
|
||||
|
||||
最重要的是,ES对数据组织方式和查询方式的限制,没有其他列式数据库那么死板。也就是说,ES的查询能力和灵活性是要强于上述这些列式数据库的。在这个级别的几个选手中,我个人强烈建议你优先考虑ES。但是ES有一个缺点,就是你需要给它准备大内存的服务器,硬件成本有点儿高。
|
||||
|
||||
数据量级超过TB级的时候,对这么大量级的数据做统计分析,无论使用什么存储系统,都快不到哪儿去。这个时候的性能瓶颈已经是磁盘IO和网络带宽了。这种情况下,实时的查询和分析肯定做不了。解决的办法都是,定期把数据聚合和计算好,然后把结果保存起来,在需要时对结果再进行二次查询。这么大量级的数据,一般都选择保存在HDFS中,配合Map-Reduce、Spark、Hive等等这些大数据生态圈产品做数据聚合和计算。
|
||||
|
||||
## 转变你的思想:根据查询来选择存储系统
|
||||
|
||||
面对海量数据,仅仅是根据数据量级来选择存储系统,是远远不够的。
|
||||
|
||||
经常有朋友会问:“我的系统,每天都产生几个GB的数据量,现在基本已经慢得查不出来了,你说我换个什么数据库能解决问题呢?”那我的回答都是,对不起,换什么数据库也解决不了你的问题。为什么这么说呢?
|
||||
|
||||
因为在过去的几十年里面,存储技术和分布式技术,在基础理论方面并没有什么本质上突破。技术发展更多的是体现在应用层面上,比如说,集群管理简单,查询更加自动化,像Map-Reduce这些。不同的存储系统之间,并没有本质的差异。它们的区别只是,存储引擎的数据结构、存储集群的构建方式,以及提供的查询能力,这些方面的差异。这些差异,使得每一种存储,在它擅长的一些领域或者场景下,会有很好的性能表现。
|
||||
|
||||
比如说,最近很火的RocksDB、LevelDB,它们的存储结构LSM-Tree,其实就是日志和跳表的组合,单从数据结构的时间复杂度上来说,和“老家伙”MySQL采用的B+树,有本质的提升吗?没有吧,时间复杂度都是O(log n)。但是,LSM-Tree在某些情况下,它利用日志有更好的写性能表现。没有哪种存储能在所有情况下,都具有明显的性能优势,所以说,**存储系统没有银弹,<strong><strong>不要指望简单**</strong>地**<strong>更换一种数据库**</strong>,就可以解决数据量大,查询慢的问题。</strong>
|
||||
|
||||
但是,在特定的场景下,通过一些优化方法,把查询性能提升几十倍甚至几百倍,这个都是有可能的。这里面有个很重要的思想就是,**根据查询来选择存储系统和数据结构**。我们前面的课程《[06 | 如何用Elasticsearch构建商品搜索系统](https://time.geekbang.org/column/article/208675)》,就是把这个思想实践得很好的一个例子。ES采用的倒排索引的数据结构,并没有比MySQL的B+树更快或者说是更先进,但是面对“全文搜索”这个查询需求,选择使用ES的倒排索引,就比使用其他的存储系统和数据结构,性能上要高出几十倍。
|
||||
|
||||
再举个例子,大家都知道,京东的物流速度是非常快的。经常是,一件挺贵的衣服,下单之后,还没来得及后悔,已经送到了。京东的物流之所以能做到这么快,有一个很重要的原因是,它有一套智能的补货系统,根据历史的物流数据,对未来的趋势做出预测,来给全国每个仓库补货。这样京东就可以做到,你下单买的商品,很大概率在离你家几公里那个京东仓库里就有货,这样自然很快就送到了。这个系统的背后,它需要分析每天几亿条物流数据,每条物流数据又细分为几段到几十段,那每天的物流数据就是几十亿的量级。
|
||||
|
||||
这份物流数据,它的用途也非常多,比如说,智能补货系统要用;调度运力的系统也要用;评价每个站点儿、每个快递小哥的时效达成情况,还要用这个数据;物流规划人员同样要用这个数据进行分析,对物流网络做持续优化。
|
||||
|
||||
那用什么样的存储系统保存这些物流数据,才能满足这些查询需求呢?显然,任何一种存储系统,都满足不了这么多种查询需求。我们需要根据每一种需求,去专门选择合适的存储系统,定义适合的数据结构,各自解决各自的问题。而不是用一种数据结构,一个数据库去解决所有的问题。
|
||||
|
||||
对于智能补货和运力调度这两个系统,它的区域性很强,那我们可以把数据按照区域(省或者地市)做分片,再汇总一份全国的跨区物流数据,这样绝大部分查询都可以落在一个分片上,查询性能就会很好。
|
||||
|
||||
对于站点儿和人的时效达成情况,这种业务的查询方式以点查询为主,那可以考虑事先在计算的时候,按照站点儿和人把数据汇总好,存放到一些分布式KV存储中,基本上可以做到毫秒级查询性能。而对于物流规划的查询需求,查询方式是多变的,可以把数据放到Hive表中,按照时间进行分片。
|
||||
|
||||
我们之前也讲到过,按照时间分片是对查询最友好的分片方式。物流规划人员可以在上面执行一些分析类的查询任务,一个查询任务即使是花上几个小时,用来验证一个新的规划算法,也是可以接受的。
|
||||
|
||||
## 小结
|
||||
|
||||
海量数据的主要用途就是支撑离线分析类业务的查询,根据数据量规模不同,由小到大可以选择:关系型数据库,列式数据库和一些大数据存储系统。对于TB量级以下的数据,如果可以接受相对比较贵的硬件成本,ES是一个不错的选择。
|
||||
|
||||
对于海量数据来说,选择存储系统没有银弹,重要的是转变思想,根据业务对数据的查询方式,反推数据应该使用什么存储系统、如何分片,以及如何组织。即使是同样一份数据,也要根据不同的查询需求,组织成不同的数据结构,存放在适合的存储系统中,才能在每一种业务中都达到理想的查询性能。
|
||||
|
||||
## 思考题
|
||||
|
||||
今天的课后思考题是这样的,我们要做一个日志系统,收集全公司所有系统的全量程序日志,给开发和运维人员提供日志的查询和分析服务,你会选择用什么存储系统来存储这些日志?原因是什么?欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。
|
||||
105
极客时间专栏/后端存储实战课/海量数据篇/23 | MySQL经常遇到的高可用、分片问题,NewSQL是如何解决的?.md
Normal file
105
极客时间专栏/后端存储实战课/海量数据篇/23 | MySQL经常遇到的高可用、分片问题,NewSQL是如何解决的?.md
Normal file
@@ -0,0 +1,105 @@
|
||||
<audio id="audio" title="23 | MySQL经常遇到的高可用、分片问题,NewSQL是如何解决的?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/57/e0/5767b04db553d7a03aaff673fe06cae0.mp3"></audio>
|
||||
|
||||
你好,我是李玥。
|
||||
|
||||
在这个系列课程中,我们讲的都是如何解决生产系统中面临的一些存储系统相关的问题。在最后两节课里面,我们来说点儿新东西,看一下存储这个技术领域,可能会有哪些值得关注的新技术。当然,技术圈每天都有很多新的技术出现,也会经常发很多论文,出现很多的开源项目,这些大多数都不太靠谱儿。
|
||||
|
||||
今天我给你要说的这个New SQL,它是我个人认为非常靠谱,甚至在未来可能会取代MySQL这样的关系型数据库的一个技术。MySQL是几乎每一个后端开发人员必须要精通的数据库,既然New SQL非常有可能在将来替代MySQL,那我们就非常有必要提前去了解一下了。
|
||||
|
||||
## 什么是New SQL?
|
||||
|
||||
什么是New SQL?这个说来话长了,还要从存储技术发展的历史来解读。我们知道,早期只有像MySQL这样的关系数据库,这种关系型数据库因为支持SQL语言,后来被叫做SQL或者Old SQL。
|
||||
|
||||
然后,出现了Redis和很多KV存储系统,性能上各种吊打MySQL,而且因为存储结构简单,所以比较容易组成分布式集群,并且能够做到水平扩展、高可靠、高可用。因为这些KV存储不支持SQL,为了以示区分,被统称为No SQL。
|
||||
|
||||
No SQL本来希望能凭借高性能和集群的优势,替代掉Old SQL。但用户是用脚投票的,这么多年实践证明,你牺牲了SQL这种强大的查询能力和ACID事务支持,用户根本不买账,直到今天,Old SQL还是生产系统中最主流的数据库。
|
||||
|
||||
这个时候,大家都开始明白了,无论你其他方面做的比Old SQL好再多,SQL和ACID是刚需,这个命你革不掉的。你不支持SQL,就不会有多少人用。所以你看,近几年很多之前不支持SQL的数据库,都开始支持SQL了,甚至于像Spark、Flink这样的流计算平台,也都开始支持SQL。当然,虽然说支持SQL,但这里面各个产品的支持程度是参差不齐的,多多少少都有一些缩水。对于ACID的支持,基本上等同于就没有。
|
||||
|
||||
这个时候,New SQL它来了!简单地说,New SQL就是兼顾了Old SQL和No SQL的优点:
|
||||
|
||||
- 完整地支持SQL和ACID,提供和Old SQL隔离级别相当的事务能力;
|
||||
- 高性能、高可靠、高可用,支持水平扩容。
|
||||
|
||||
像Google的Cloud Spanner、国产的OceanBase以及开源的[CockroachDB](https://github.com/cockroachdb/cockroach)都属于New SQL数据库。Cockroach这个英文单词是蟑螂的意思,所以一般我们都把CockroachDB俗称为小强数据库。
|
||||
|
||||
这些New SQL凭什么就能做到Old SQL和No SQL做不到的这些特性呢?那我们就以开源的CockroachDB为例子,来看一下New SQL是不是真的这么厉害。
|
||||
|
||||
## CockroachDB是如何实现数据分片和弹性扩容的?
|
||||
|
||||
首先,我们一起先来简单看一下CockroachDB的架构,从架构层面分析一下,它是不是真的像宣传的那么厉害。我们先来看一下它的架构图(图片来自于[官方文档](https://github.com/cockroachdb/cockroach/blob/master/docs/design.md)):
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/82/8c78db973e66bb62b23c8e85afe78082.jpg" alt="">
|
||||
|
||||
这是一个非常典型的分层架构,我们从上往下看。最上层是SQL层,SQL层支持和关系型数据库类似的逻辑数据结构,比如说库、表、行和列这些逻辑概念。SQL层向下调用的是一个抽象的接口层Structured Data API,实际实现这个API的是下面一层:Distributed, Monolithic KV Store,这就是一个分布式的KV存储系统。
|
||||
|
||||
我们先不深入进去看细节,从宏观层面上分析一下这个架构。你可以看到,这个架构仍然是我们之间讲过的,大部分数据库都采用的二层架构:执行器和存储引擎。它的SQL层就是执行器,下面的分布式KV存储集群就是它的存储引擎。
|
||||
|
||||
那我们知道,MySQL的存储引擎InnoDB,实际上是基于文件系统的B+树,像Hive和HBase,它们的存储引擎都是基于HDFS构建的。那CockroachDB这种,使用分布式KV存储来作为存储引擎的设计,理论上也是可行的,并没有什么特别难以逾越的技术壁垒。
|
||||
|
||||
而且,使用分布式KV存储作为存储引擎,实现高性能、高可靠、高可用,以及支持水平扩容这些特性,就不是什么难事儿了,其中很多分布式KV存储系统已经做到了,这里面使用的一些技术和方法,大多我们在之前的课程中也都讲到过。CockroachDB在实现它的存储引擎这一层,就是大量地借鉴,甚至是直接使用了已有的一些成熟技术。
|
||||
|
||||
它的分片算法采用的是范围分片,我们之前也讲到过,范围分片对查询是最友好的,可以很好地支持范围扫描这一类的操作,这样有利于它支撑上层的SQL查询。
|
||||
|
||||
它采用[Raft](https://raft.github.io/)一致性协议来实现每个分片的高可靠、高可用和强一致。这个Raft协议,它的一个理论基础,就是我们之前讲的复制状态机,并且在复制状态机的基础上,Raft实现了集群自我监控和自我选举来解决高可用的问题。Raft也是一个被广泛采用的、非常成熟的一致性协议,比如etcd也是基于Raft来实现的。
|
||||
|
||||
CockroachDB的元数据直接分布在所有的存储节点上,依靠流言协议来传播,这个流言协议,我们在《[16 | 用Redis构建缓存集群的最佳实践有哪些?](https://time.geekbang.org/column/article/217590)》这节课中也讲到过,在Redis Cluster中也是用流言协议来传播元数据变化的。
|
||||
|
||||
CockroachDB用上面这些成熟的技术解决了集群问题,在单机的存储引擎上,更是直接使用了RocksDB作为它的KV存储引擎。RocksDB也是值得大家关注的一个新的存储系统,下节课我们会专门讲RocksDB。
|
||||
|
||||
你可以看到,CockroachDB的存储引擎,也就是它的分布式KV存储集群,基本上没有什么大的创新,就是重用了已有的一些成熟的技术,这些技术在我们之前讲过的其他存储系统中,全部都见到过。我讲这些并没有贬低CockroachDB的意思,相反,站在巨人的肩膀上,才能看得更远,飞得更高,这是一种非常务实的做法。
|
||||
|
||||
## CockroachDB能提供金融级的事务隔离性么?
|
||||
|
||||
接下来我们说一下CockroachDB是怎么实现ACID的,它的ACID是不是类似于分布式事务的残血版?这是一个非常关键的问题,直接影响到它有没有可能在未来取代MySQL。
|
||||
|
||||
在说ACID之前,我们还是要简单说一下CockroachDB是怎么解析和执行SQL的。我们在《[10 | 走进黑盒:SQL是如何在数据库中执行的?](https://time.geekbang.org/column/article/213176)》这节课中讲过SQL是如何在MySQL中执行的,在CockroachDB中,这个执行的流程也是差不多的。同样是先解析SQL生成语法树,转换成逻辑执行计划,再转换为物理执行计划,优化后,执行物理执行计划返回查询结果,这样一个流程。
|
||||
|
||||
只是在CockroachDB中,物理执行计划就更加复杂了,因为它的物理执行计划面对的是一个分布式KV存储系统,在涉及到查找、聚合这类操作的时候,有可能需要涉及到多个分片(Range)。大致过程就是类似于Map-Reduce的逻辑,先查找元数据确定可能涉及到的分片,然后把物理执行计划转换成每个分片上的物理执行计划,在每个分片上去并行执行,最后,再对这些执行结果做汇总。
|
||||
|
||||
然后我们再来说CockroachDB的ACID。我们在《[04 | 事务:账户余额总是对不上账,怎么办?](https://time.geekbang.org/column/article/206544)》这节课中讲到过四种事务隔离级别,分别是RU、RC、RR和SERIALIZABLE,那CockroachDB能提供哪种隔离级别呢?答案是,以上四种都不是。
|
||||
|
||||
CockroachDB提供了另外两种隔离级别,分别是:**Snapshot Isolation (SI)** 和 **Serializable Snapshot Isolation (SSI)**,其中SSI是CockroachDB默认的隔离级别。
|
||||
|
||||
这两种隔离级别和之前提到的四种隔离级别是什么关系呢?我们通过下面这张表,和MySQL默认的隔离级别RR做一个对比。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/20/67/20e20d983ad7519e6eae11821a3f1567.jpg" alt="">
|
||||
|
||||
首先我们看SI这一行。我们之前讲到过,RR这种隔离级别,可以很好地解决脏读和不可重复读的问题,虽然可能会产生幻读,但实际上对绝大多数事务影响不大。SI不会发生脏读、不可重复读,也不会发生幻读的情况,这个隔离级别似乎比RR还要好。
|
||||
|
||||
但你要注意一下,我们这个表格比之前那个表格多了一列:写倾斜。可以看到,RR是不会出现写倾斜问题的,但是SI会有写倾斜问题。
|
||||
|
||||
什么是写倾斜?我们还是拿账户余额的例子来说明。比如说,我们的账户需要支持主副卡,主卡和副卡都分别有自己的余额,并且这个余额是可以透支的,只要满足主副卡的余额之和大于0就行了。如果我们需要给主卡支出100元,那SQL只要这样写就可以了:
|
||||
|
||||
```
|
||||
update account
|
||||
set balance = balance - 100 -- 在主卡中扣减100元
|
||||
where id = ? and
|
||||
(select balance from account where id = ?) -- 主卡余额
|
||||
+
|
||||
(select balance from account where id = ?) -- 附卡余额
|
||||
>= 100; -- 主副卡余额之和必须大于100元
|
||||
|
||||
```
|
||||
|
||||
在传统的RR隔离级别下,由于更新数据时会对记录加锁,即使更新主副卡的两个SQL分别在两个事务中并发执行,也不会出现把主副卡的余额之和扣减成负数的情况。
|
||||
|
||||
但是在SI级别下,由于它没有加锁,而是采用快照的方式来实现事务的隔离,这个时候,如果并发地去更新主副卡余额,是有可能出现把主副卡余额之和扣减为负数的情况的。这种情况称为**写倾斜**。这里顺便提一句,写倾斜是普遍的译法,我个人觉得“倾斜”这个词翻译得并不准确,实际上它表达的,就是因为没有检测读写冲突,也没有加锁,导致数据写错了。
|
||||
|
||||
SSI隔离级别在SI的基础上,加入了冲突检测的机制,通过检测读写冲突,然后回滚事务的方式来解决写倾斜的问题,当然这种方式付出的代价是降低性能,并且冲突严重的情况下,会频繁地出现事务回滚。
|
||||
|
||||
从理论上来说,CockroachDB支持的SI和SSI这两种事务隔离级别,能提供的事务隔离性,已经与传统的RC和RR隔离级别不相上下了,可以满足大多数在线交易类系统对ACID的要求。
|
||||
|
||||
## 小结
|
||||
|
||||
New SQL是新一代的分布式数据库,它具备原生分布式存储系统高性能、高可靠、高可用和弹性扩容的能力,同时还兼顾了传统关系型数据库的SQL支持。更厉害的是,它还提供了和传统关系型数据库不相上下的、真正的事务支持,具备了支撑在线交易类业务的能力。
|
||||
|
||||
CockroachDB是开源的New SQL数据库。它的存储引擎是一个分布式KV存储集群,执行器则大量借鉴了PostgreSQL的一些设计和实现,是一个集很多现有数据库和分布式存储系统技术于一身,这样的一个数据库产品。
|
||||
|
||||
从设计上来看,CockroachDB这类New SQL数据库,有非常大的潜质可以真正地取代MySQL这类传统的关系型数据库。但是我们也应该看到,目前这些New SQL数据库都还处于高速发展阶段,并没有被大规模地应用到生产系统中去。我也不建议你做小白鼠,在重要的系统上去使用它。
|
||||
|
||||
## 思考题
|
||||
|
||||
课后请你去看一下[Raft](https://raft.github.io/)一致性协议,然后简单总结一下,CockroachDB是如何利用Raft协议实现Range高可用、高可靠和强一致的。欢迎你在留言区与我讨论。
|
||||
|
||||
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。
|
||||
73
极客时间专栏/后端存储实战课/海量数据篇/24 | RocksDB:不丢数据的高性能KV存储.md
Normal file
73
极客时间专栏/后端存储实战课/海量数据篇/24 | RocksDB:不丢数据的高性能KV存储.md
Normal file
@@ -0,0 +1,73 @@
|
||||
<audio id="audio" title="24 | RocksDB:不丢数据的高性能KV存储" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5a/21/5a5315e1a88d43a49d589c55f334a121.mp3"></audio>
|
||||
|
||||
你好,我是李玥。
|
||||
|
||||
上节课我们在讲解CockroachDB的时候提到过,CockroachDB的存储引擎是一个分布式的KV存储集群,它用了一系列成熟的技术来解决集群问题,但是在集群的每个节点上,还需要一个单机的KV存储来保存数据,这个地方CockroachDB直接使用RocksDB作为它的KV存储引擎。
|
||||
|
||||
[RocksDB](https://github.com/facebook/rocksdb)是Facebook开源的一个高性能持久化KV存储。目前,你可能很少见到过哪个项目会直接使用RocksDB来保存数据,在未来,RocksDB大概率也不会像Redis那样被业务系统直接使用。那我们为什么要关注它呢?
|
||||
|
||||
因为越来越多的新生代数据库,都不约而同地选择RocksDB作为它们的存储引擎。在将来,很有可能出现什么样的情况呢?我们使用的很多不同的数据库,它们背后采用的存储引擎都是RocksDB。
|
||||
|
||||
我来给你举几个例子。我们上节课讲到的CockroachDB用到了RocksDB作为它的存储引擎。再说几个比较有名的,[MyRocks](http://myrocks.io/)这个开源项目,你看它这个名字就知道它是干什么的了。它在用RocksDB给MySQL做存储引擎,目的是取代现有的InnoDB存储引擎。并且,MySQL的亲兄弟MariaDB已经接纳了MyRocks,作为它的一个可选的存储引擎。还有大家都经常用的实时计算引擎[Flink](https://flink.apache.org/),用过的同学都知道,Flink的State就是一个KV的存储,它使用的也是RocksDB。还有包括MongoDB、Cassandra等等很多的数据库,都在开发基于RocksDB的存储引擎。
|
||||
|
||||
今天这节课,我们就一起来了解一下RocksDB这颗“未来之星”。
|
||||
|
||||
## 同样是KV存储,RocksDB有哪些不同?
|
||||
|
||||
说到KV存储,我们最熟悉的就是Redis了,接下来我们就来对比一下RocksDB和Redis这两个KV存储。
|
||||
|
||||
其实Redis和RocksDB之间没什么可比性,一个是缓存,一个是数据库存储引擎,放在一起比就像“关公战秦琼”一样。那我们把这两个KV放在一起对比,目的不是为了比谁强谁弱,而是为了让你快速了解RocksDB能力。
|
||||
|
||||
我们知道Redis是一个内存数据库,它之所以能做到非常好的性能,主要原因就是,它的数据都是保存在内存中的。从Redis官方给出的测试数据来看,它的随机读写性能大约在50万次/秒左右。而RocksDB相应的随机读写性能大约在20万次/秒左右,虽然性能还不如Redis,但是已经可以算是同一个量级的水平了。
|
||||
|
||||
这里面你需要注意到的一个重大差异是,Redis是一个内存数据库,并不是一个可靠的存储。数据写到内存中就算成功了,它并不保证安全地保存到磁盘上。而RocksDB它是一个持久化的KV存储,它需要保证每条数据都要安全地写到磁盘上,这也是很多数据库产品的基本要求。这么一比,我们就看出来RocksDB的优势了,我们知道,磁盘的读写性能和内存读写性能差着一两个数量级,读写磁盘的RocksDB,能和读写内存的Redis做到相近的性能,这就是RocksDB的价值所在了。
|
||||
|
||||
RocksDB为什么能在保证数据持久化的前提下,还能做到这么强的性能呢?我们之前反复讲到过,一个存储系统,它的读写性能主要取决于什么?取决于它的存储结构,也就是数据是如何组织的。
|
||||
|
||||
RocksDB采用了一个非常复杂的数据存储结构,并且这个存储结构采用了内存和磁盘混合存储方式,使用磁盘来保证数据的可靠存储,并且利用速度更快的内存来提升读写性能。或者说,RocksDB的存储结构本身就自带了内存缓存。
|
||||
|
||||
那我们知道,内存缓存可以很好地提升读性能,但是写入数据的时候,你是绕不过要写磁盘的。因为,要保证数据持久化,数据必须真正写到磁盘上才行。RocksDB为什么能做到这么高的写入性能?还是因为它特殊的数据结构。
|
||||
|
||||
大多数存储系统,为了能做到快速查找,都会采用树或者哈希表这样的存储结构,数据在写入的时候,必须写入到特定的位置上。比如说,我们在往B+树中写入一条数据,必须按照B+树的排序方式,写入到某个固定的节点下面。哈希表也是类似,必须要写入到特定的哈希槽中去。
|
||||
|
||||
这些数据结构会导致在写入数据的时候,不得不在磁盘上这里写一点儿,再去那里写一点儿,这样跳来跳去地写,也就是我们说的“随机写”。而RocksDB它的数据结构,可以让绝大多数写入磁盘的操作都是顺序写。那我们知道,无论是SSD还是HDD顺序写的性能都要远远好于随机写,这就是RocksDB能够做到高性能写入的根本原因。
|
||||
|
||||
那我们在《[21 | 类似“点击流”这样的海量数据应该如何存储?](https://time.geekbang.org/column/article/224162)》这节课中讲到过,Kafka也是采用顺序读写的方式,所以它的读写性能也是超级快。但是这种顺序写入的数据基本上是没法查询的,因为数据没有结构,想要查询的话,只能去遍历。RocksDB究竟使用了什么样的数据结构,在保证数据顺序写入的前提下还能兼顾很好的查询性能呢?这种数据结构就是**LSM-Tree**。
|
||||
|
||||
## LSM-Tree如何兼顾读写性能?
|
||||
|
||||
LSM-Tree的全称是:**The Log-Structured Merge-Tree**,是一种非常复杂的复合数据结构,它包含了WAL(Write Ahead Log)、跳表(SkipList)和一个分层的有序表(SSTable,Sorted String Table)。下面这张图就是LSM-Tree的结构图(图片来自于论文: [An Efficient Design and Implementation of LSM-Tree based Key-Value Store on Open-Channel SSD](http://ranger.uta.edu/~sjiang/pubs/papers/wang14-LSM-SDF.pdf))
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c0/6e/c0ba7aa330ea79a8a1dfe3a58547526e.jpg" alt="">
|
||||
|
||||
看起来非常复杂是吧?实际上它的结构比这个图更复杂。那我们尽量忽略没那么重要的细节,把它的核心原理讲清楚。首先需要注意的是,这个图上有一个横向的实线,是内存和磁盘的分界线,上面的部分是内存,下面的部分是磁盘。
|
||||
|
||||
我们先来看数据是如何写入的。当LSM-Tree收到一个写请求,比如说:PUT foo bar,把Key foo的值设置为bar。首先,这条操作命令会被写入到磁盘的WAL日志中(图中右侧的Log),这是一个顺序写磁盘的操作,性能很好。这个日志的唯一作用就是用于故障恢复,一旦系统宕机,可以从日志中把内存中还没有来得及写入磁盘的数据恢复出来。这个地方用的还是之前我们多次讲过的复制状态机理论。
|
||||
|
||||
写完日志之后,数据可靠性的问题就解决了。然后数据会被写入到内存中的MemTable中,这个MemTable就是一个按照Key组织的跳表(SkipList),跳表和平衡树有着类似的查找性能,但实现起来更简单一些。写MemTable是个内存操作,速度也非常快。数据写入到MemTable之后,就可以返回写入成功了。这里面有一点需要注意的是,**LSM-Tree在处理写入的过程中,直接就往MemTable里写,并不去查找这个Key是不是已经存在了**。
|
||||
|
||||
这个内存中MemTable不能无限地往里写,一是内存的容量毕竟有限,另外,MemTable太大了读写性能都会下降。所以,MemTable有一个固定的上限大小,一般是32M。MemTable写满之后,就被转换成Immutable MemTable,然后再创建一个空的MemTable继续写。这个Immutable MemTable,也就是只读的MemTable,它和MemTable的数据结构完全一样,唯一的区别就是不允许再写入了。
|
||||
|
||||
Immutable MemTable也不能在内存中无限地占地方,会有一个后台线程,不停地把Immutable MemTable复制到磁盘文件中,然后释放内存空间。每个Immutable MemTable对应一个磁盘文件,MemTable的数据结构跳表本身就是一个有序表,写入的文件也是一个按照Key排序的结构,这些文件就是SSTable。把MemTable写入SSTable这个写操作,因为它是把整块内存写入到整个文件中,这同样是一个顺序写操作。
|
||||
|
||||
到这里,虽然数据已经保存到磁盘上了,但还没结束,因为这些SSTable文件,虽然每个文件中的Key是有序的,但是文件之间是完全无序的,还是没法查找。这里SSTable采用了一个很巧妙的分层合并机制来解决乱序的问题。
|
||||
|
||||
SSTable被分为很多层,越往上层,文件越少,越往底层,文件越多。每一层的容量都有一个固定的上限,一般来说,下一层的容量是上一层的10倍。当某一层写满了,就会触发后台线程往下一层合并,数据合并到下一层之后,本层的SSTable文件就可以删除掉了。合并的过程也是排序的过程,除了Level 0(第0层,也就是MemTable直接dump出来的磁盘文件所在的那一层。)以外,每一层内的文件都是有序的,文件内的KV也是有序的,这样就比较便于查找了。
|
||||
|
||||
然后我们再来说LSM-Tree如何查找数据。查找的过程也是分层查找,先去内存中的MemTable和Immutable MemTable中找,然后再按照顺序依次在磁盘的每一层SSTable文件中去找,只要找到了就直接返回。这样的查找方式其实是很低效的,有可能需要多次查找内存和多个文件才能找到一个Key,但实际的效果也没那么差,因为这样一个分层的结构,它会天然形成一个非常有利于查找的情况:越是被经常读写的热数据,它在这个分层结构中就越靠上,对这样的Key查找就越快。
|
||||
|
||||
比如说,最经常读写的Key很大概率会在内存中,这样不用读写磁盘就完成了查找。即使内存中查不到,真正能穿透很多层SStable一直查到最底层的请求还是很少的。另外,在工程上还会对查找做很多的优化,比如说,在内存中缓存SSTable文件的Key,用布隆过滤器避免无谓的查找等来加速查找过程。这样综合优化下来,可以获得相对还不错的查找性能。
|
||||
|
||||
## 小结
|
||||
|
||||
RocksDB是一个高性能持久化的KV存储,被很多新生代的数据库作为存储引擎。RocksDB在保证不错的读性能的前提下,大幅地提升了写入性能,这主要得益于它的数据结构:LSM-Tree。
|
||||
|
||||
LSM-Tree通过混合内存和磁盘内的多种数据结构,将随机写转换为顺序写来提升写性能,通过异步向下合并分层SSTable文件的方式,让热数据的查找更高效,从而获得还不错的综合查找性能。
|
||||
|
||||
通过分析LSM-Tree的数据结构可以看出来,这种数据结构还是偏向于写入性能的优化,更适合在线交易类场景,因为在这类场景下,需要频繁写入数据。
|
||||
|
||||
## 思考题
|
||||
|
||||
我们刚刚讲了LSM-Tree是如何读写数据的,但是并没有提到数据是如何删除的。课后请你去看一下RocksDB或者是LevelDB相关的文档,总结一下LSM-Tree删除数据的过程,也欢迎你在留言区分享你的总结。
|
||||
|
||||
感谢你的阅读,如果你觉得今天的内容对你有帮助,也欢迎把它分享给你的朋友。
|
||||
Reference in New Issue
Block a user