如何利用Redis扩展数据服务、实现分片及高可用?

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 今天,我们来聊聊如何扩展数据服务,如何实现分片(sharding)以及高可用(high availability)。   分布式系统不存在完美的设计,处处都体现了trade off。   因此我们在开始正文前,需要确定后续的讨论原则,仍然以分布式系统设计中的CAP原则为例。

今天,我们来聊聊如何扩展数据服务,如何实现分片(sharding)以及高可用(high availability)。

 

分布式系统不存在完美的设计,处处都体现了trade off。

 

因此我们在开始正文前,需要确定后续的讨论原则,仍然以分布式系统设计中的CAP原则为例。由于主角是Redis,那性能表现肯定是最高设计目标,之后讨论过程中的所有抉择,都会优先考虑CAP中的AP性质。

 

 

◆  ◆  ◆  ◆  ◆  

 

两个点按顺序来,先看分片。

 

何谓分片?简单来说,就是对单机Redis做水平扩展。

 

当然,做游戏的同学可能要问了,一服一个Redis,为什么需要水平扩展?这个话题我们在之前几篇文章中都有讨论,可以看下面两篇文章,这里就不再赘述。

  • https://mp.weixin.qq.com/s/Ime_GyDkAJMTird1nWRNUA

  • http://mp.weixin.qq.com/s__biz=MzIwNDU2MTI4NQ==&mid=2247483728&idx=1&sn=c2076dbc98de6fbd
    40b87236f2033925&chksm=973f0fbaa04886ac83c975b7046885f7171be8d26695d23fcab974124ce054a65d
    10caea3db5&scene=21#wechat_redirect

 

如果要实现服务级别的复用,那么数据服务的定位往往是全局服务。如此仅用单实例的Redis就难以应对多变的负载情况——毕竟Redis是单线程的。

 

从MySQL一路用过来的同学这时都会习惯性地水平拆分,Redis中也是类似的原理,将整体的数据进行切分,每一部分是一个分片(shard),不同的分片维护不同的key集合。

 

那么,分片问题的实质就是如何基于多个Redis实例设计全局统一的数据服务。同时,有一个约束条件,那就是我们无法保证强一致性。

 

也就是说,数据服务进行分片扩展的前提是,不提供跨分片事务的保障。Redis cluster也没有提供类似支持,因为分布式事务本来就跟Redis的定位是有冲突的。

 

因此,我们的分片方案有两个限制:

  • 不同分片中的数据一定是严格隔离的,比如是不同组服的数据,或者是完全不相干的数据。要想实现跨分片的数据交互,必须依赖更上层的协调机制保证,数据服务层面不做任何承诺。而且这样一来,如果想给应用层提供协调机制,只要在每个分片上部署单实例简易锁机制即可,简单明了。

  • 我们的分片方案无法在分片间做类似分布式存储系统的数据冗余机制,换言之,一份数据交叉存在多个分片中。

 

如何实现分片?

 

首先,我们要确定分片方案需要解决什么问题。

 

分片的Redis集群,实际上共同组成了一个有状态服务(stateful service)。设计有状态服务,我们通常会从两点考虑:

 

  • cluster membership,系统间各个节点,或者说各个分片的关系是怎样的。

  • work distribution,外部请求应该如何、交由哪个节点处理,或者说用户(以下都简称dbClient)的一次读或写应该去找哪个分片。

 

针对第一个问题,解决方案通常有三:

 

  • presharding,也就是Sharding静态配置。

  • gossip protocol,其实就是Redis cluster采用的方案。简单地说就是集群中每个节点会由于网络分化、节点抖动等原因而具有不同的集群全局视图。节点之间通过gossip protocol进行节点信息共享。这是业界比较流行的去中心化的方案。

  • consensus system,这种方案跟上一种正相反,是依赖外部分布式一致性设施,由其仲裁来决定集群中各节点的身份。

 

需求决定解决方案,我认为,对于游戏服务端以及大多数应用型后端情景,后两者的成本太高,会增加很多不确定的复杂性,因此两种方案都不是合适的选择。而且,大部分服务通常是可以在设计阶段确定每个分片的容量上限的,也不需要太复杂的机制支持。

 

但是presharding的缺点也很明显,做不到动态增容减容,而且无法高可用。不过其实只要稍加改造,就足以满足需求了。

 

不过,在谈具体的改造措施之前,我们先看之前提出的分片方案要解决的第二个问题——work distribution。

 

这个问题实际上是从另一种维度看分片,解决方案很多,但是如果从对架构的影响上来看,大概分为两种:

 

  • 一种是proxy-based,基于额外的转发代理。例子有twemproxy/Codis。

  • 一种是client sharding,也就是dbClient(每个对数据服务有需求的服务)维护sharding规则,自助式选择要去哪个Redis实例。Redis cluster本质上就属于这种,dblient侧缓存了部分sharding信息。

 

第一种方案的缺点显而易见——在整个架构中增加了额外的间接层,流程中增加了一趟round-trip。如果是像twemproxy或者Codis这种支持高可用的还好,但是github上随便一翻还能找到特别多的没法做到高可用的proxy-based方案,无缘无故多个单点,这样就完全搞不明白sharding的意义何在了。

 

第二种方案的缺点,我能想到的就是集群状态发生变化的时候没法即时通知到dbClient。

 

第一种方案,我们其实可以直接pass掉了。因为这种方案更适合私有云的情景,开发数据服务的部门有可能和业务部门相去甚远,因此需要统一的转发代理服务。但是对于一些简单的应用开发情景,数据服务逻辑服务都是一帮人写的,没什么增加额外中间层的必要。

 

那么,看起来只能选择第二种方案了。

 

将presharding与client sharding结合起来后,现在我们的成果是:数据服务是全局的,Redis可以开多个实例,不相干的数据需要到不同的分片上存取,dbClient掌握这个映射关系。

 

 

◆  ◆  ◆  ◆  ◆  

 

不过目前的方案只能算是满足了应用对数据服务的基本需求。

 

游戏行业中,大部分采用Redis的团队,一般最终会选定这个方案作为自己的数据服务。后续的扩展其实对他们来说不是不可以做,但是可能有维护上的复杂性与不确定性。

 

但是作为一名有操守的程序员,我选择继续扩展。

 

现在的这个方案存在两个问题:

 

针对第一个问题,处理方式跟proxy-based采用的处理方式没太大区别,由于目前的数据服务方案比较简单,采用一致性哈希即可。或者采用一种比较简单的两段映射,第一段是静态的固定哈希,第二段是动态的可配置map。前者通过算法,后者通过map配置维护的方式,都能最小化影响到的key集合。

 

而对于第二个问题,解决方案就是实现高可用。

 

◆  ◆  ◆  ◆  ◆  

 

如何让数据服务高可用?在讨论这个问题之前,我们首先看Redis如何实现「可用性」。

 

对于Redis来说,可用性的本质是什么?其实就是Redis实例挂掉之后可以有后备节点顶上。 

 

Redis通过两种机制支持这一点。

 

第一种机制是replication。通常的replication方案主要分为两种。

 

  • 一种是active-passive,也就是active节点先修改自身状态,然后写统一持久化log,然后passive节点读log跟进状态。

  • 另一种是active-active,写请求统一写到持久化log,然后每个active节点自动同步log进度。

 

Redis的replication方案采用的是一种一致性较弱的active-passive方案。也就是master自身维护log,将log向其他slave同步,master挂掉有可能导致部分log丢失,client写完master即可收到成功返回,是一种异步replication。

 

这个机制只能解决节点数据冗余的问题,Redis要具有可用性就还得解决Redis实例挂掉让备胎自动顶上的问题,毕竟由人肉去监控master状态再人肉切换是不现实的。 因此还需要第二种机制。

 

第二种机制是Redis自带的能够自动化fail-over的Redis sentinel。reds sentinel实际上是一种特殊的Redis实例,其本身就是一种高可用服务——可以多开,可以自动服务发现(基于Redis内置的pub-sub支持,sentinel并没有禁用掉pub-sub的command map),可以自主leader election(基于raft算法实现,作为sentinel的一个模块),然后在发现master挂掉时由leader发起fail-over,并将掉线后再上线的master降为新master的slave。

 

Redis基于这两种机制,已经能够实现一定程度的可用性。

 


 

◆  ◆  ◆  ◆  ◆  

 

接下来,我们来看数据服务如何高可用。

 

数据服务具有可用性的本质是什么?除了能实现Redis可用性的需求——Redis实例数据冗余、故障自动切换之外,还需要将切换的消息通知到每个dbClient。

 

也就是说把最开始的图,改成下面这个样子:

 

 

每个分片都要改成主从模式。

 

如果Redis sentinel负责主从切换,拿最自然的想法就是让dbClient向sentinel请求当前节点主从连接信息。但是Redis sentinel本身也是Redis实例,数量也是动态的,Redis sentinel的连接信息不仅在配置上成了一个难题,动态更新时也会有各种问题。

 

而且,Redis sentinel本质上是整个服务端的static parts(要向dbClient提供服务),但是却依赖于Redis的启动,并不是特别优雅。另一方面,dbClient要想问Redis sentinel要到当前连接信息,只能依赖其内置的pub-sub机制。Redis的pub-sub只是一个简单的消息分发,没有消息持久化,因此需要轮询式的请求连接信息模型。

 

那么,我们是否可以以较低的成本定制一种服务,既能取代Redis sentinel,又能解决上述问题?

 

回忆下前文《如何快速搭建数据服务》(https://mp.weixin.qq.com/s/Ime_GyDkAJMTird1nWRNUA)中我们解决resharding问题的思路:

 

  • 一致性哈希。

  • 采用一种比较简单的两段映射,第一段是静态的固定哈希,第二段是动态的可配置map。前者通过算法,后者通过map配置维护的方式,都能最小化影响到的key集合。

 

两种方案都可以实现动态resharding,dbClient可以动态更新:

 

  • 如果采用两段映射,那么我们可以动态下发第二段的配置数据。

  • 如果采用一致性哈希,那么我们可以动态下发分片的连接信息。

 

再梳理一下,我们要实现的服务(下文简称为watcher),至少要实现这些需求:

 

  • 要能够监控Redis的生存状态。这一点实现起来很简单,定期的PING Redis实例即可。需要的信息以及做出客观下线和主观下线的判断依据都可以直接照搬sentinel实现。

  • 要做到自主服务发现,包括其他watcher的发现与所监控的master-slave组中的新节点的发现。在实现上,前者可以基于消息队列的pub-sub功能,后者只要向Redis实例定期INFO获取信息即可。

  • 要在发现master客观下线的时候选出leader进行后续的故障转移流程。这部分实现起来算是最复杂的部分,接下来会集中讨论。

  • 选出leader之后将一个最合适的slave提升为master,然后等老的master再上线了就把它降级为新master的slave。

 

解决这些问题,watcher就兼具了扩展性、定制性,同时还提供分片数据服务的部分在线迁移机制。这样,我们的数据服务也就更加健壮,可用程度更高。

 

◆  ◆  ◆  ◆  ◆  

 

这样一来,虽然保证了Redis每个分片的master-slave组具有可用性,但是因为我们引入了新的服务,那就引入了新的不确定性——如果引入这个服务的同时还要保证数据服务具有可用性,那我们就还得保证这个服务本身是可用的。

 

说起来可能有点绕,换个说法,也就是服务A借助服务B实现了高可用,那么服务B本身也需要高可用。

 

先简单介绍一下Redis sentinel是如何做到高可用的。同时监控同一组主从的sentinel可以有多个,master挂掉的时候,这些sentinel会根据Redis自己实现的一种raft算法选举出leader,算法流程也不是特别复杂,至少比paxos简单多了。所有sentinel都是follower,判断出master客观下线的sentinel会升级成candidate同时向其他follower拉票,所有follower同一epoch内只能投给第一个向自己拉票的candidate。在具体表现中,通常一两个epoch就能保证形成多数派,选出leader。有了leader,后面再对Redis做SLAVEOF的时候就容易多了。

 


 

如果想用watcher取代sentinel,最复杂的实现细节可能就是这部分逻辑了。

 

这部分逻辑说白了就是要在分布式系统中维护一个一致状态,举个例子,可以将「谁是leader」这个概念当作一个状态量,由分布式系统中的身份相等的几个节点共同维护,既然谁都有可能修改这个变量,那究竟谁的修改才奏效呢?

 

幸好,针对这种常见的问题情景,我们有现成的基础设施抽象可以解决。


这种基础设施就是分布式系统的协调器组件(coordinator),老牌的有Zookeeper(基于对Paxos改进过的zab协议,下面都简称zk了),新一点的有etcd(这个大家都清楚,基于raft协议)。这种组件通常没有重复开发的必要,像Paxos这种算法理解起来都得老半天,实现起来的细节数量级更是难以想象。因此很多开源项目都是依赖这两者实现高可用的,比如codis一开始就是用的zk。

 

◆  ◆  ◆  ◆  ◆  

 

zk解决了什么问题?

 

以通用的应用服务需求来说,zk可以用来选leader,还可以用来维护dbClient的配置数据——dbClient直接去找zk要数据就行了。
 

zk的具体原理我就不再介绍了,有时间有精力可以研究下paxos,看看lamport的paper,没时间没精力的话搜一下看看zk实现原理的博客就行了。
 

简单介绍下如何基于zk实现leader election。zk提供了一个类似于os文件系统的目录结构,目录结构上的每个节点都有类型的概念同时可以存储一些数据。zk还提供了一次性触发的watch机制。

 

应用层要做leader election就可以基于这几点概念实现。

 

假设有某个目录节点「/election」,watcher1启动的时候在这个节点下面创建一个子节点,节点类型是临时顺序节点,也就是说这个节点会随创建者挂掉而挂掉,顺序的意思就是会在节点的名字后面加个数字后缀,唯一标识这个节点在「/election」的子节点中的id。
 

一个简单的方案是让每个watcher都watch「/election」的所有子节点,然后看自己的id是否是最小的,如果是就说明自己是leader,然后告诉应用层自己是leader,让应用层进行后续操作就行了。但是这样会产生惊群效应,因为一个子节点删除,每个watcher都会收到通知,但是至多一个watcher会从follower变为leader。

 

优化一些的方案是每个节点都关注比自己小一个排位的节点。这样如果id最小的节点挂掉之后,id次小的节点会收到通知然后了解到自己成为了leader,避免了惊群效应。

 

我在实践中发现,还有一点需要注意,临时顺序节点的临时性体现在一次session而不是一次连接的终止。

 

例如watcher1每次申请节点都叫watcher1,第一次它申请成功的节点全名假设是watcher10002(后面的是zk自动加的序列号),然后下线,watcher10002节点还会存在一段时间,如果这段时间内watcher1再上线,再尝试创建watcher1就会失败,然后之前的节点过一会儿就因为session超时而销毁,这样就相当于这个watcher1消失了。

 

解决方案有两个,可以创建节点前先显式delete一次,也可以通过其他机制保证每次创建节点的名字不同,比如guid。

 

至于配置下发,就更简单了。配置变更时直接更新节点数据,就能借助zk通知到关注的dbClient,这种事件通知机制相比于轮询请求sentinel要配置数据的机制更加优雅。

 

看下最后的架构图:

原文发布时间为:2016-12-21

本文来自云栖社区合作伙伴DBAplus

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
1月前
|
NoSQL Redis
Redis的数据淘汰策略有哪些 ?
Redis 提供了 8 种数据淘汰策略,分为淘汰易失数据和淘汰全库数据两大类。易失数据淘汰策略包括:volatile-lru、volatile-lfu、volatile-ttl 和 volatile-random;全库数据淘汰策略包括:allkeys-lru、allkeys-lfu 和 allkeys-random。此外,还有 no-eviction 策略,禁止驱逐数据,当内存不足时新写入操作会报错。
114 16
|
4天前
|
缓存 NoSQL Redis
Redis经典问题:数据并发竞争
数据并发竞争是大流量系统(如火车票系统、微博平台)中常见的问题,可能导致用户体验下降甚至系统崩溃。本文介绍了两种解决方案:1) 加写回操作加互斥锁,查询失败快速返回默认值;2) 保持多个缓存备份,减少并发竞争概率。通过实践案例展示,成功提高了系统的稳定性和性能。
|
4天前
|
缓存 监控 NoSQL
Redis经典问题:数据不一致
在使用Redis时,缓存与数据库数据不一致会导致应用异常。主要原因包括缓存更新失败、Rehash异常等。解决方案有:重试机制、缩短缓存时间、优化写入策略、建立监控报警、定期验证一致性、采用缓存分层及数据回滚恢复机制。这些措施可确保数据最终一致性,提升应用稳定性和性能。
|
1月前
|
缓存 NoSQL 关系型数据库
Redis和Mysql如何保证数据⼀致?
在项目中,为了解决Redis与Mysql的数据一致性问题,我们采用了多种策略:对于低一致性要求的数据,不做特别处理;时效性数据通过设置缓存过期时间来减少不一致风险;高一致性但时效性要求不高的数据,利用MQ异步同步确保最终一致性;而对一致性和时效性都有高要求的数据,则采用分布式事务(如Seata TCC模式)来保障。
68 14
|
1月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
43 5
|
1月前
|
存储 NoSQL 算法
Redis分片集群中数据是怎么存储和读取的 ?
Redis集群采用哈希槽分区算法,共有16384个哈希槽,每个槽分配到不同的Redis节点上。数据操作时,通过CRC16算法对key计算并取模,确定其所属的槽和对应的节点,从而实现高效的数据存取。
52 13
|
1月前
|
存储 NoSQL Redis
Redis的数据过期策略有哪些 ?
Redis 采用两种过期键删除策略:惰性删除和定期删除。惰性删除在读取键时检查是否过期并删除,对 CPU 友好但可能积压大量过期键。定期删除则定时抽样检查并删除过期键,对内存更友好。默认每秒扫描 10 次,每次检查 20 个键,若超过 25% 过期则继续检查,单次最大执行时间 25ms。两者结合使用以平衡性能和资源占用。
50 11
|
1月前
|
监控 NoSQL 测试技术
【赵渝强老师】Redis的AOF数据持久化
Redis 是内存数据库,提供数据持久化功能,支持 RDB 和 AOF 两种方式。AOF 以日志形式记录每个写操作,支持定期重写以压缩文件。默认情况下,AOF 功能关闭,需在 `redis.conf` 中启用。通过 `info` 命令可监控 AOF 状态。AOF 重写功能可有效控制文件大小,避免性能下降。
|
1月前
|
存储 监控 NoSQL
【赵渝强老师】Redis的RDB数据持久化
Redis 是内存数据库,提供数据持久化功能以防止服务器进程退出导致数据丢失。Redis 支持 RDB 和 AOF 两种持久化方式,其中 RDB 是默认的持久化方式。RDB 通过在指定时间间隔内将内存中的数据快照写入磁盘,确保数据的安全性和恢复能力。RDB 持久化机制包括创建子进程、将数据写入临时文件并替换旧文件等步骤。优点包括适合大规模数据恢复和低数据完整性要求的场景,但也有数据完整性和一致性较低及备份时占用内存的缺点。
|
2月前
|
存储 数据采集 监控
将百万数据插入到 Redis,有哪些实现方案
【10月更文挑战第15天】将百万数据插入到 Redis 是一个具有挑战性的任务,但通过合理选择实现方案和进行性能优化,可以高效地完成任务。
138 0