带你从0到1搭建一个稳定高性能的Redis集群想一想,Redis是如何稳定高性能的提供服务的呢?你也可以尝试回答以下问题:我使用Redis的场景很简单。只用单机版的Redis有什么问题吗?我的Redis出现故障,数据丢失了怎么办?如何确保我的业务应用程序不受影响?为什么需要主从集群?它的优点是什么?什么是分片集群?我真的需要一个分片集群吗?...如果你已经对Redis有所了解,那么你一定听说过数据持久化、主从复制、哨兵等概念。它们之间有什么区别和联系?如果你有这样的疑惑,本文将从0到1,再从1到N,带你一步步搭建一个稳定、高性能的Redis集群。在这个过程中,你可以了解到Redis采用了哪些优化方案来实现稳定性和高性能,为什么?掌握这些原则,让你在使用Redis时游刃有余。本文干货较多,希望大家耐心阅读。从最简单的开始:StandaloneRedis首先,我们从最简单的场景开始。假设你有一个业务应用,需要引入Redis来提高应用的性能。这时候你可以选择部署一个单机版的Redis来使用,像这样:这个架构很简单,你的业务应用可以使用Redis作为缓存。使用,从MySQL中查询数据,然后写入Redis,然后业务应用从Redis中读取数据。由于Redis的数据是存放在内存中的,所以速度非常快。如果你的业务量不大,那么这样的架构模型基本可以满足你的需求。是不是很简单?随着时间的推移,你的业务量逐渐发展起来,存储在Redis中的数据也越来越多。这个时候,你的业务应用越来越依赖Redis。然而,突然有一天,你的Redis由于某种原因宕机了。这时候你所有的业务流量都会打到后端的MySQL上,这会导致你的MySQL压力急剧增加,严重的时候甚至会压垮MySQL。.这个时候你该怎么办?我猜你的解决办法肯定是快速重启Redis,让它继续提供服务。但是,由于Redis中的数据之前是在内存中的,即使现在重启Redis,之前的数据也会丢失。虽然重启后的Redis可以正常工作,但是由于Redis中没有数据,业务流量还是会打到后端MySQL上,MySQL的压力还是很大的。这是怎么办?你陷入了沉思。有什么好的方法可以解决这个问题吗?由于Redis只将数据存储在内存中,它是否也可以将这些数据的副本写入磁盘?如果采用这种方式,当Redis重启的时候,我们很快将磁盘中的数据恢复到内存中,使其能够继续正常提供服务。是的,这是一个很好的解决方案。将内存数据写入磁盘的过程就是“数据持久化”。数据持久化:准备现在,你想象中的Redis数据持久化是这样的:但是,数据持久化应该怎么做呢?我猜你能想到的最多的解决方案就是Redis每次进行写操作,除了写入内存之外,还会向磁盘写入一份,就像这样:是的,这是最简单最直接的解决方案。但是仔细想想,这个方案有个问题:客户端每次写操作都需要写内存和磁盘,而写磁盘的耗时肯定比写内存慢很多!这势必会影响Redis的性能。如何规避这个问题?我们可以这样优化:Redis通过主线程写入内存,内存写入完成后返回结果给客户端,然后Redis使用另外一个线程写入磁盘,这样主线程写入的性能影响可以避免到磁盘。这确实是个好计划。另外,我们是不是可以换个角度,想一想其他的方式来持久化数据呢?这时候就得考虑Redis的使用场景了。回想一下,我们在使用Redis的时候,一般是在什么场景下使用呢?是的,缓存。使用Redis作为缓存意味着虽然没有将全量数据存储在Redis中,但是对于不在缓存中的数据,我们的业务应用仍然可以通过查询后端数据库得到结果,但是查询后端的速度-end数据会比较慢,但对业务结果没有影响。基于这个特性,我们的Redis数据持久化也可以通过“数据快照”的方式来完成。那么什么是数据快照呢?简单的说,你可以这样理解:你把Redis想象成一个水杯,向Redis写入数据就相当于往杯子里倒水。这时候,你拿着相机拍下水杯。瞬间,照片中记录的杯子的水容量就是杯子的数据快照。也就是说,Redis的数据快照就是将某个时刻的数据记录在Redis中,然后只需要将这个数据快照写入磁盘即可。好的。它的优点是只在需要持久化的时候才将数据“一次”写入磁盘,其他时候不需要对磁盘进行操作。基于此方案,我们可以周期性的为Redis做数据快照,并将数据持久化到磁盘。其实上面说的持久化方案就是Redis的“RDB”和“AOF”:RDB:只把某个时刻的数据快照持久化到磁盘(创建一个子进程来做)AOF:每次写操作都持久化到磁盘(主线程写入内存,主线程或子线程可根据策略配置持久化数据)它们的区别除了上述还有以下特点:RDB采用二进制+数据压缩方式写入磁盘,所以文件体积小,数据恢复速度也快。AOF记录每一次写命令,数据最完整,但文件体积大,数据恢复速度慢。如果让你选择持久化方案,你可以这样选择:如果你的业务对数据丢失不敏感,使用RDB方案持久化数据如果你的业务对数据完整性要求比较高,使用AOF方案持久化数据假设你的业务对Redis的数据完整性要求比较高,而你选择了AOF方案,那么这个时候你又会遇到这些问题:AOF记录每一次写操作,随着时间的增长,AOF文件的大小会越来越大和更大。这么大的AOF文件在数据恢复的时候会变得很慢。我应该怎么办?数据完整性要求变高,数据恢复难度变大?有什么办法可以减小文件大小吗?提高恢复速度怎么样?下面继续分析AOF的特点。既然每次写操作都会记录在AOF文件中,但是同一个key可能会被修改多次,我们只保留最后一次修改的值,是不是也可以呢?没错,这就是我们经常听到的“AOFrewrite”,你也可以理解为AOF“瘦身”。我们可以定期重写AOF文件,避免文件大小的不断扩大,从而在恢复时缩短恢复时间。再想一想,有没有办法继续缩小AOF文件呢?再回顾一下前面提到的RDB和AOF的特点:RDB采用二进制+数据压缩方式存储,文件体积小。AOF记录了每一个写命令,拥有最完整的数据。能否发挥各自的优势?当然,这就是Redis的“混合持久化”。具体来说,在AOFrewrite时,Redis首先以RDB格式在AOF文件中写入一个数据快照,然后将这期间产生的每条写命令追加到AOF文件中。因为RDB是用二进制压缩写的,所以AOF文件的体积变小了。此时,当你使用AOF文件恢复数据时,恢复时间会更短!Redis4.0及以上版本只支持混合持久化。有了这样的优化,你的Redis再也不用担心实例宕机了。当发生宕机时,可以使用持久化文件快速恢复Redis中的数据。但这可以吗?仔细想想。虽然我们已经将持久化文件优化到最少,但是恢复数据还是需要时间的。在此期间,您的业务应用程序仍会受到影响。我应该怎么办?下面分析一下有没有更好的解决方案。如果实例宕机,只能通过恢复数据来解决。我们是不是可以部署多个Redis实例,然后让这些实例的数据实时同步,这样当一个实例宕机的时候,我们可以选择剩下的一个实例继续提供服务就可以了。没错,这个方案就是接下来要讲的“主从复制:多副本”。主从复制:多副本此时可以部署多个Redis实例,架构模型变成这样:这里我们称实时读写的节点为master,另一个实时同步数据的节点称为奴隶。采用多副本方案的好处是:缩短不可用时间:当master宕机时,我们可以手动将slave提升为master继续提供服务,提高读性能:让slave分担部分读请求并提高应用程序的整体性能。不仅可以节省数据恢复的时间,还可以提高性能,那么它有什么问题呢?你可以考虑一下。其实它的问题在于,当master宕机时,我们需要“手动”将slave提升为master,这个过程也是需要时间的。虽然比恢复数据快得多,但它仍然需要人工干预。一旦需要人工干预,就必须计算人的反应时间和操作时间,所以在此期间您的业务应用仍然会受到影响。如何解决这个问题呢?我们可以自动化这个切换过程吗?针对这种情况,我们需要一种“故障转移”机制,也就是我们经常听到的“哨兵”的能力。Sentinel:自动故障转移现在,我们可以引入一个“观察者”来实时监控master的健康状态。这个观察者就是“哨兵”。怎么做?哨兵每隔一段时间就询问主人是否正常。master正常回复,说明状态正常,回复超时说明异常哨兵发现异常,发起主从切换。有了这个解决方案,就不需要人工干预,一切都变得自动化了,是不是很酷?但是这里还有一个问题。如果master状态正常,但是sentinel在询问master的时候他们之间的网络有问题,那么sentinel可能会误判。如何解决这个问题呢?答案是我们可以部署多个哨兵,分布在不同的机器上,一起监控master的状态,流程就变成了这样:多个哨兵定时询问master是否正常,master回复正常,表示状态正常,回复超时表示异常。一旦某个sentinel判断master出现异常(不管是不是网络问题),就会去询问其他的sentinel。如果多个哨兵(设置一个阈值)都认为master异常,则判定master确实失效了。多个sentinel协商确定master故障后,发起master-slave切换。因此,我们使用多个哨兵相互协商来确定主人的地位。这样一来,就可以大大降低误判的概率。哨兵协商确定master异常后,还有一个问题:由哪个哨兵发起主从切换?答案是选择一个哨兵“领导者”,他会在主从之间切换。问题又来了,这个leader怎么选?想象一下现实生活中的选举是如何进行的?是的,投票。在选举哨兵领袖的时候,我们可以制定这样一个选举规则:每个哨兵要求其他哨兵互相请对方为自己投票。每个哨兵只投票给第一个请求投票的哨兵,并且只能投票一次。获得半数以上选票的哨兵被选为领导者并发起主从切换。其实这个选举过程就是我们经常听到的:分布式系统领域的“共识算法”。什么是共识算法?我们把哨兵部署在多台机器上,他们需要协同工作来完成一个任务,所以他们形成了一个“分布式系统”。在分布式系统领域,多个节点如何就一个问题达成共识的算法称为共识算法。在这种情况下,多个哨兵一起协商选出一个他们都认可的领导者,这是使用共识算法完成的。该算法还规定节点数必须为奇数,这样可以保证即使系统中某个节点出现故障,其余“一半”以上的节点处于正常状态,仍能提供正确的结果。也就是说,这个算法也兼容有节点失效的情况。分布式系统领域的共识算法有很多,比如Paxos、Raft,以及哨兵选举领导者的场景。使用Raft共识算法是因为它足够简单,易于实现。现在,我们使用多个哨兵来共同监控Redis的状态。这样就可以避免误判的问题,架构模型就变成了这样:好了,这里先总结一下。你的Redis从最简单的单机版通过数据持久化、主从多副本、sentinel集群进行了优化。你的Redis性能和稳定性越来越高了。不用担心。如果你的Redis部署在这样的架构模式下,基本上可以长期稳定运行。...随着时间的发展,您的业务量开始迎来爆发式增长。这个时候你的架构模型还能承受这么大的流量吗?一起来分析一下:稳定性:Redis故障宕机,我们有Sentinel+replica,可以自动完成主从切换写请求量增加,但是我们只有一个master实例,如果这个实例达到了怎么办瓶颈?你有没有看到,当你的写请求越来越大的时候,一个Master实例可能无法处理这么大的写流量。要完美解决这个问题,这时候就需要考虑使用“shardedclusters”。FragmentedCluster:横向扩展什么是“shardedcluster”?简单的说,一个实例无法承受写的压力,那么我们是否可以部署多个实例,然后将这些实例按照一定的规则组织起来,作为一个整体,对外提供服务,这样我们就可以解决一个实例集中写入的问题是瓶颈问题?所以,现在的架构模型就变成了这样:现在问题又来了,那么多的实例怎么组织?我们制定如下规则:每个节点存储一部分数据,所有节点数据的总和为全量数据。制定路由规则。对于不同的key,路由到一个固定的实例进行读写。分片集群基于路由规则。根据位置的不同,可以分为两类:客户端分片服务器分片客户端分片是指关键的路由规则放在客户端,具体如下:这种方案的缺点是,客户端需要维护这个路由规则,也就是你需要把路由规则写到你的业务代码中。如何避免在业务代码中耦合路由规则?你可以这样优化,把这个路由规则封装成一个模块,需要用的时候集成这个模块。这是RedisCluster采用的方案。RedisCluster内置哨兵逻辑,无需部署哨兵。当您使用RedisCluster时,您的业务应用程序需要使用配套的RedisSDK。路由规则都集成在这个SDK里面,不用自己写。让我们再看看服务器端分片。这种方案意味着路由规则不是在客户端完成的,而是在客户端和服务器之间增加了一个“中间代理层”。这个proxy就是我们经常听到的proxy。数据路由规则在代理层维护。这样就不需要关心服务器端有多少个Redis节点,只需要和这个Proxy进行交互即可。Proxy会根据路由规则将你的请求转发到对应的Redis节点。而且,当集群实例不足以支撑更大的流量请求时,还可以横向扩展,增加新的Redis实例来提升性能。这一切对于你的客户来说,都是透明的,难以察觉的。业界开源的Redis分片集群方案,如Twemproxy、Codis等均采用该方案。分片集群的数据扩容涉及到很多细节。该内容不是本文的重点,所以暂时不做详细介绍。至此,当你使用分片集群时,可以从容面对未来更大的流量压力!总结一下,总结一下我们是如何一步步搭建稳定高性能的Redis集群的。首先,我们在使用最简单的单机版Redis时,发现当Redis崩溃时,数据无法恢复,于是想到了“数据持久化”,将内存中的数据持久化到磁盘中,以便Redis在重启后可以快速从磁盘恢复数据。在进行数据持久化的时候,我们面临着如何更高效的将数据持久化到磁盘的问题。后来发现Redis提供了RDB和AOF两种方案,分别对应数据快照和实时命令记录。当我们对数据完整性要求不高时,可以选择RDB持久化方案。如果对数据完整性要求高,可以选择AOF持久化方案。但是我们也发现随着时间的推移,AOF文件的大小会越来越大。这时候我们想到的优化方案是使用AOFrewrite来减小文件体积,减小文件体积。后来发现可以结合RDB和AOF各自的优点,在AOFrewrite时使用结合两者的“混合持久化”的方式进一步减小AOF文件的大小。后来我们发现,虽然可以通过数据恢复来恢复数据,但是恢复数据是需要时间的,也就是说业务应用还是会受到影响。我们进一步优化并采用了“多副本”的方案,让多个实例实时同步。当一个实例出现故障时,可以手动提升其他实例继续提供服务。但是,这也存在问题。手动升级实例需要人工干预,人工干预也需要时间。我们开始想办法自动化这个过程,所以我们引入了一个“哨兵”集群。哨兵集群相互协商。发现故障节点并自动完成切换,大大降低了对业务应用的影响。最后,我们关注的是如何支持更大的写流量,所以我们引入了“分片集群”来解决这个问题,让多个Redis实例分担写压力。未来,面对更大的流量,我们还可以增加新的实例,横向扩展,进一步提升集群的性能。至此,我们的Redis集群已经能够长期稳定、高性能的为我们的业务提供服务。这里我画了一张思维导图,方便大家更好的理解它们之间的关系以及进化的过程。后记看到这里,我想你应该对如何搭建稳定高性能的Redis集群有了自己的看法。其实本文所讨论的优化思路都是围绕着“架构设计”的核心思想:高性能:读写分离,碎片化集群的高可用:数据持久化,多副本,自动故障转移和易扩展:拆分Shard集群,横向扩展当我们说到sentinel集群和shard集群的时候,这也涉及到“分布式系统”相关的知识:分布式共识:sentinelleader选举负载均衡:shard集群数据分片,数据路由当然,除了Redis,你可以用这个想法来思考和优化构建任何数据集群,看看他们是怎么做到的。比如你在使用MySQL的时候,你可以想想MySQL和Redis的区别是什么?MySQL是如何做到的,才能做到高性能和高可用呢?其实思路都是一样的。我们现在随处可见分布式系统和数据集群。希望通过这篇文章,大家能够了解这些软件是如何一步步进化的,在进化过程中遇到了哪些问题。为了解决这些问题,软件的设计者设计了什么样的方案,做了哪些权衡?只有理解了原理,掌握了分析和解决问题的能力,才能在以后的开发过程中,或者在学习其他优秀的软件时,快速找到“重点”,并在最短的时间内掌握。并能在实际应用中发挥其优势。其实这个思考过程也是做“架构设计”的思路。在做软件架构设计的时候,你面临的场景就是发现问题、分析问题、解决问题,一步一步演化升级你的架构,最终在性能和可靠性上取得平衡。尽管各种软件层出不穷,但架构设计的思路是不会变的。我希望你真正吸收的是这些想法,这样你才能以不变应万变。
