Redis 第四章 分布式缓存Redis

  • 目录
  • 缓存的主要形式
  • Redis主要历史版本
  • 数据结构
  • Strings
  • Lists:lists of strings
  • Sets
  • Hashes
  • 使用pipeline批量操作
  • 消息发布订阅
  • 影响redis性能的因素
  • Redis分区实现方式
  • Redis分布式解决方案
  • Redis分布式解决方案-客户端节点定位算法
  • 本地缓存Slot对应的节点连接信息
  • Redis主从复制
  • Redis部署结构与故障转移
  • Redis单机部署
  • 不支持自动故障转移
  • 扩容能力极为有限
  • 高并发瓶颈
  • Redis-Cluster
  • Redis-cluster特点
  • Redis-Cluster 实现基础-分片
  • Hash slot
  • Redis-Cluster 请求路由方式
  • Redis-Cluster 节点通信原理-Gossip 算法
  • Gossip 简介
  • Gossip背景
  • Gossip特点
  • Gossip在Redis-Cluster中如何作用
  • 节点通信基础
  • Gossip消息种类
  • Redis-Cluster 节点通信-成本与效率的权衡
  • 如何保证消息传播的效率?
  • 规模效应-无法忽略的成本问题
  • Redis-Cluster 故障转移
  • 故障检测
  • 选举
  • 故障转移
  • Redis-Cluster 扩容
  • 其它分布式 Redis 方案
  • 预备知识
  • 基于客户端的分片
  • 基于代理的分片
  • Codis 简介
  • Codis 特点
  • 类Codis 架构——proxy + Redis-server
  • 三主三从实现



目录

缓存的主要形式:后端系统本地缓存(包括堆外,堆内;或者从应用程序的角度来说,包括一级缓存,二级缓存;或者从操作系统组成上来说,可以分为L1 Cache, L2 Cache,L3Cache(一般在主板上), Memory(内存))、集中式缓存,以及分布式缓存常见形式及其区别。

Redis基本介绍:主要包括:redis的由来、性能参数、业界使用情况及其版本变化情况
Redis分区:这一块内容在第三章有所讲解,但这里角度更高。redis的水平拆分,如何水平拆分,如何负载均衡,如何寻址
Redis分布式解决方案:redis的分布式解决方案
Redis主从复制:Redis主从复制原理
Redis故障转移:故障发生时的failover和failback方案
Codis分布式解决方案:业界常用的codis解决方案介绍

缓存的主要形式

缓存主要表现为:本地缓存,集中式缓存,分布式缓存。他们之前的适用场景如下表所示。

本地缓存

集中式缓存

分布式缓存

数据存储在本地,适用于单点部署的应用、访问链路固定的集群,适用于查询多而更新少或无更新的数据缓存场景。

数据存储在缓存服务器端,服务器端使用单点或主从模式部署,适用于写少读多的场景。

数据存储在缓存服务器端,服务器端使用集群的方式进行水平拆分,从而达到读写负载均衡的效果。

堆内存,堆外内存,主要中间件有ehcache,guava cache

Memcached,Redis

Redis Cluster,Twemproxy,Codis

Redis主要历史版本

redis实现二级索引的原理 redis多级目录_客户端

数据结构

Redis数据类型:https://redis.io/topics/data-types

Strings

Redis中String类型是二进制安全的,意味着String类型能存储任何类型的数据,比如一个JPEG图片或者一个序列化的Ruby对象。Redis Cluster中不支持MGET/MSET。

GET key
MGET key [key ...]
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
MSET key value [key value ...]

SETEX key seconds value 
SETNX key value

INCR key
INCRBY key increment
DECR key
DECRBY key decrement

APPEND/STRLEN/GETRANGE/SETRANGE/ INCRBYFLOAT

Lists:lists of strings

根据String元素的插入顺序的集合就是Lists。

添加 LPUSH/RPUSH key value [value ...]
List存在时添加 LPUSHX/RPUSHX key value

删除 LPOP/RPOP key

查询范围 LRANGE key start stop
获取子集 LTRIM key start stop

Sets

无顺序的String类型的集合。

添加 SADD key member [member ...]
删除 SPOP key [count]

移动 SMOVE source destination member

求差集 SDIFF key [key ...]
求差集并保存 SDIFFSTORE destination_key [key ...]

求交集 SINTER key [key ...]
求交集并保存 SINTERSTORE destination_key [key ...]

求并集 SUNION key [key ...]
求并集并保存 SUNIONSTORE destination_key [key ...]

Hashes

maps between string fields and string values。

Sorted Sets:
Bitmaps and HyperLogLogs:based on the String base type
Stream:models a log data structure in a more abstract way. https://redis.io/topics/streams-intro

HASHES的操作
添加 HSET key field value
添加多个 HMSET key field value [field value ...]
查询 HGET key field
查询多个 HMGET key field [field ...]
查询所有 HGETALL key

有序集合的操作
添加 ZADD key [NX|XX] [CH] [INCR] score member [score member ...]
根据SCORE范围查询 ZRANGE key start stop [WITHSCORES]
查询SCORE ZRANK key member

Bitmaps的操作
SETBIT key offset value
GETBIT key offset

使用pipeline批量操作

相当于本来需要多次网络请求的,组装到一起发送一次网络请求。可以:

  1. 减少RRT(Round Trip Time)消耗
  2. 较少系统调用
  3. 减少上下文切换
  4. 减少资源初始化过程

redis实现二级索引的原理 redis多级目录_客户端_02

消息发布订阅

完全是鸡肋,和常见MQ的差异:

  1. 无持久化,无订阅者时消息丢失
  2. 无法做到重试、至少消费一次等复杂功能
  3. 没有事务支持
  4. 消息协议支持有限
  5. 对消息的管理能力有限

影响redis性能的因素

  1. 网络带宽
  2. CPU性能
  3. RAM存储速度
  4. 是否有虚拟化环境
  5. 请求包大小
  6. Client连接数

通过redis官网的测试数据可以发现,redis足够强大。

Redis分区实现方式

redis官网有系统分区的指导方案

redis实现二级索引的原理 redis多级目录_Redis_03

Redis分布式解决方案

Redis分布式解决方案-客户端节点定位算法

以下是java客户端jedis中的源码。

public static int getSlot(String key) {
    if (key == null) {
        throw new JedisClusterOperationException("Slot calculation of null is impossible");
    }

    key = JedisClusterHashTagUtil.getHashTag(key); // 在集群中使用HashTag保证关联数据在同一个节点中,标记符为“{}”
    // optimization with modulo operator with power of 2 equivalent to getCRC16(key) % 16384
    return getCRC16(key) & (16384 - 1); // CRC值对16384取模的值为Slot id
}

public static String getHashTag(String key) {
    return extractHashTag(key, true);
}

private static String extractHashTag(String key, boolean returnKeyOnAbsence) {
    int s = key.indexOf("{");
    if (s > -1) {
        int e = key.indexOf("}", s + 1);
        if (e > -1 && e != s + 1) {
            return key.substring(s + 1, e); // HashTag KEY = 第一个“{”与第一个“}”之间的字符串
        }
    }
    return returnKeyOnAbsence ? key : null;
}

本地缓存Slot对应的节点连接信息

public class JedisSlotBasedConnectionHandler extends JedisClusterConnectionHandler {
    
    protected final JedisClusterInfoCache cache;

    @Override
    public Jedis getConnectionFromSlot(int slot) {
        JedisPool connectionPool = cache.getSlotPool(slot);
        if (connectionPool != null) {

            // It can't guaranteed to get valid connection because of node
            // assignment
            return connectionPool.getResource();
        } else {
            renewSlotCache(); //It's abnormal situation for cluster mode, that we have just nothing for slot, try to rediscover state
            connectionPool = cache.getSlotPool(slot);
            if (connectionPool != null) {
                return connectionPool.getResource();
            } else {
                //no choice, fallback to new connection to random node
                return getConnection();
            }
        }
    }
}

public class JedisClusterInfoCache {

  private final Map<Integer, JedisPool> slots = new HashMap<Integer, JedisPool>();
  
  public JedisPool getSlotPool(int slot) {
    r.lock();
    try {
      return slots.get(slot);
    } finally {
      r.unlock();
    }
  }
}

Redis主从复制

redis实现二级索引的原理 redis多级目录_Redis_04


当ReplicationId已失效或Master backlog buffer中缓存不足时,触发全量复制。2.8开始支持PSYNC。

正常状态下,Master以异步的方式发送Slave以下信息:

  1. 写操作信息
  2. KEY过期信息
  3. 其他在Master数据中发生的变动

主从模式可以提高 Redis 的整体运行速度,因为使用主从模式就可以实现数据的读写分离,把写操作的请求分发到主节点上,把其他的读操作请求分发到从节点上,这样就减轻了 Redis 主节点的运行压力,并且提高了 Redis 的整体运行速度。

不但如此使用主从模式还实现了 Redis 的高可用,当主服务器宕机之后,可以很迅速的把从节点提升为主节点,为 Redis 服务器的宕机恢复节省了宝贵的时间。

并且主从复制还降低了数据丢失的风险,因为数据是完整拷贝在多台服务器上的,当一个服务器磁盘坏掉之后,可以从其他服务器拿到完整的备份数据。

Redis部署结构与故障转移

Redis单机部署

单机模式就是指 Redis 主节点以单个节点的形式存在,这个主节点可读可写,上面存储数据全集。在3.0版本之前,Redis 只能支持单机模式,出于可靠性考量,通常单机模式为“1主 N 备”的结构,如下所示:

redis实现二级索引的原理 redis多级目录_数据_05

说明:即便有很多个 Redis 主节点,只要这些主节点以单机模式存在,本质上仍为单机模式。单机模式比较简单,足以支撑一般应用场景,但单机模式具有固有的局限性:(1)不支持自动故障转移,(2)扩容能力极为有限(只能Scale-Up, 垂直扩容),(3)高并发瓶颈。

不支持自动故障转移

Redis 单机模式下,即便是“1主 N 备”结构,当主节点故障时,备节点也无法自动升主,即无法自动故障转移(Failover)。故障转移需要“哨兵”Sentinel 辅助,Sentinel 是 Redis 高可用的解决方案,由一个或者多个 Sentinel 实例组成的系统可以监视 Redis 主节点及其从节点,当检测到 Redis 主节点下线时,会根据特定的选举规则从该主节点对应的所有从节点中选举出一个“最优”的从节点升主,然后由升主的新主节点处理请求。

sentinel模式是建立在主从模式的基础上,如果只有一个Redis节点,sentinel就没有任何意义。
当master节点挂了以后,sentinel会在slave中选择一个做为master,并修改它们的配置文件,其他slave的配置文件也会被修改,比如slaveof属性会指向新的master。
当master节点重新启动后,它将不再是master而是做为slave接收新的master节点的同步数据。sentinel因为也是一个进程有挂掉的可能,所以sentinel也会启动多个形成一个sentinel集群。当主从模式配置密码时,sentinel也会同步将配置信息修改到配置文件中,不许要担心。
一个sentinel或sentinel集群可以管理多个主从Redis。
sentinel最好不要和Redis部署在同一台机器,不然Redis的服务器挂了以后,sentinel也挂了。
sentinel监控的Redis集群都会定义一个master名字,这个名字代表Redis集群的master Redis。
当使用sentinel模式的时候,客户端就不要直接连接Redis,而是连接sentinel的ip和port,由sentinel来提供具体的可提供服务的Redis实现,这样当master节点挂掉以后,sentinel就会感知并将新的master节点提供给使用者。
sentinel模式基本可以满足一般生产的需求,具备高可用性。但是当数据量过大到一台服务器存放不下的情况时,主从模式或sentinel模式就不能满足需求了,这个时候需要对存储的数据进行分片,将数据存储到多个Redis实例中,就是下面要讲的cluster。

具有 Sentinel 系统的单机模式示意图如下:

redis实现二级索引的原理 redis多级目录_数据_06

扩容能力极为有限

这一点应该很好理解,单机模式下,只有主节点能够写入数据,那么,最大数据容量就取决于主节点所在物理机的内存容量,而物理机的内存扩容(Scale-Up)能力目前仍是极为有限的。

高并发瓶颈

Redis使用单线程的IO复用模型,对于单纯的IO操作来说,单线程可以将速度优势发挥到最大,但是Redis也提供了一些简单的计算功能,比如排序、聚合等,对于这些操作,单线程模型实际会严重影响整体吞吐量,CPU计算过程中,整个IO调度都是被阻塞住的。因此,单机模式下并发支持能力很容易陷入瓶颈。

Redis-Cluster

cluster的出现是为了解决单机Redis容量有限及高并发的问题,将Redis的数据根据一定的规则分配到多台机器。对cluster的一些理解:
cluster可以说是sentinel和主从模式的结合体,通过cluster可以实现主从和master重选功能,所以如果配置两个副本三个分片的话,就需要六个Redis实例。
因为Redis的数据是根据一定规则分配到cluster的不同机器的,当数据量过大时,可以新增机器进行扩容
这种模式适合数据量巨大的缓存要求,当数据量不是很大使用sentinel即可。

单实例 Redis 虽然简单,但瓶颈明显。一是容量问题,在一些应用场景下,数据规模可达数十 G,甚至数百G,而物理机的资源却是有限的,内存无法无限扩充;二是并发性能问题,Redis 号称单实例10万并发,但也仅仅是10万并发。鉴于单机模式的局限性,历时三年,Redis-Cluster应运而生。

Redis-cluster特点

自3.0版本起,Redis官方推出了一个原生的分布式方案—— Redis-Cluster,它是一个分布式、容错的 Redis 实现。 Redis-Cluster中不存在中心节点或者代理节点, 集群主要设计目标之一是实现线性可扩展性。Redis-Cluster具有以下特点:

  1. 节点互通:所有的Redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽;
  2. 去中心化:Redis-Cluster不存在中心节点,每个节点都记录有集群的状态信息,并且通过Gossip协议,使每个节点记录的信息实现最终一致性;
  3. 客户端直连:客户端与Redis节点直连,不需要中间proxy层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可;
  4. 数据分片: Redis-Cluster的键空间被分割为 16384 个slot,这些slot被分别指派给主节点,当存储Key-Value 时,根据 CRC16(key) Mod 16384的值,决定将一个 Key-value 放到哪个 Slot 中;
  5. 多数派原则:对于集群中的任何一个节点,需要超过半数的节点检测到它失效(pFail),才会将其判定为失效(Fail);
  6. 自动Failover:当集群中某个主节点故障后(Fail),其它主节点会从故障主节点的从节点中选举一个”最佳“从节点升主,替代故障的主节点;
  7. 功能弱化:集群模式下,由于数据分布在多个节点,不支持单机模式下的集合操作,也不支持多数据库功能,集群只能使用默认的0号数据库;
  8. 集群规模:官方推荐的最大节点数量为 1000 个左右,这是因为当集群规模过大时,Gossip协议的效率会显著下降,通信成本剧增;

Redis-Cluster 实现基础-分片

Redis 集群实现的基础是分片,即将数据集有机的分割为多个片,并将这些分片指派给多个 Redis 实例,每个实例只保存总数据集的一个子集。利用多台计算机内存和来支持更大的数据库,而避免受限于单机的内存容量;通过多核计算机集群,可有效扩展计算能力;通过多台计算机和网络适配器,允许我们扩展网络带宽。

Hash slot

基于“分片”的思想,Redis 提出了 Hash Slot。Redis-Cluster 把所有的物理节点映射到预分好的16384个 Slot,当需要在 Redis 集群中放置一个 Key-Value 时,根据 CRC16(key) Mod 16384的值,决定将一个 Key 放到哪个 Slot 中。

redis实现二级索引的原理 redis多级目录_客户端_07

Redis-Cluster 请求路由方式

客户端直连 Redis 服务,进行读写操作时,Key 对应的 Slot 可能并不在当前直连的节点上,经过“重定向”才能转发到正确的节点。如下图所示,我们直接登陆127.0.0.1:6379客户端,进行 set 操作,当 Key 对应的 Slot 不在当前节点时(如 key-test),客户端会报错并返回正确的节点的 IP 和端口;set 成功则返回OK。

redis实现二级索引的原理 redis多级目录_数据_08


以集群模式登陆127.0.0.1:6379客户端(注意命令的差别:-c 表示集群模式),则可以清楚的看到“重定向”的信息,并且客户端也发生了切换:“6379”->“6381”。

redis实现二级索引的原理 redis多级目录_客户端_09


以三节点为例,上述操作的路由查询流程示意图如下所示:

redis实现二级索引的原理 redis多级目录_客户端_10


和普通的查询路由相比,Redis-Cluster 借助客户端实现的请求路由是一种混合形式的查询路,它并非从一个 Redis 节点到另外一个 Redis,而是借助客户端转发到正确的节点。

在实际应用中,可以在客户端缓存 Slot 与 Redis 节点的映射关系,当接收到 MOVED 响应时修改缓存中的映射关系。如此,基于保存的映射关系,请求时会直接发送到正确的节点上,从而减少一次交互,提升效率。

目前,包括 Lettuce、Jedis、Redission 在内的许多 Redis Client,都已经实现了对 Redis-Cluster 的支持,关于客户端的内容,将在第五章详细介绍。

Redis-Cluster 节点通信原理-Gossip 算法

在第二章曾介绍:Redis自3.0版本起,支持Redis-Cluster,真正意义上实现了分布式。在分布式系统中,节点间的通信十分重要,是构建集群的基石。那么Redis-Cluster 中,节点间是如何通信的呢?又是如何保障一致性、可用性的呢?欲知答案,必先了解Gossip算法。

Gossip 简介

Gossip,英文释义为:n 流言蜚语,谣言; vi 传播流言,说长道短。Gossip算法源自流行病学的研究,经过不断的发展演化,作为一种分布式一致性协议而得到广泛应用,如Cassandra、Akka、Redis都有用到。

Gossip背景

Gossip算法如其名,在办公室,只要一个人八卦一下,在有限的时间内所有的人都会知道该八卦的信息,这种方式也与病毒传播类似,因此Gossip有众多的别名:“闲话算法”、“疫情传播算法”、“病毒感染算法”、“谣言传播算法”。但Gossip并不是一个新东西,之前的泛洪查找、路由算法都归属于这个范畴,不同的是Gossip给这类算法提供了明确的语义、具体实施方法及收敛性证明。

Gossip特点

Gossip算法又被称为反熵(Anti-Entropy),熵是物理学上的一个概念,代表杂乱无章,而反熵就是在杂乱无章中寻求一致,这充分说明了Gossip的特点:在一个有界网络中,每个节点都随机地与其它节点通信,经过一番杂乱无章的通信,最终所有节点的状态都会达成一致。每个节点可能知道所有其它节点,也可能仅知道几个邻居节点,只要这些节可以通过网络连通,最终它们的状态都是一致的。

要注意到的一点是,即使有的节点因宕机而重启,有新节点加入,但经过一段时间后,这些节点的状态也会与其他节点达成一致,也就是说,Gossip天然具有分布式容错的优点。

####Gossip本质

Gossip是一个带冗余的容错算法,更进一步,Gossip是一个最终一致性算法。虽然无法保证在某个时刻所有节点状态一致,但可以保证在“最终”所有节点一致,“最终”是一个现实中存在,但理论上无法证明的时间点。

因为Gossip不要求节点知道所有其它节点,因此又具有去中心化的特点,节点之间完全对等,不需要任何的中心节点。实际上Gossip可以用于众多能接受“最终一致性”的领域:失败检测、路由同步、Pub/Sub、动态负载均衡。

但Gossip的缺点也很明显,冗余通信会对网路带宽、CUP资源造成很大的负载,而这些负载又受限于通信频率,该频率又影响着算法收敛的速度,下文中,笔者将结合Redis源码详细解释。

Gossip在Redis-Cluster中如何作用

在分布式系统中,需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据、主从属性、是否出现故障等状态信息。常见的元数据维护方式分为:集中式和无中心式。Redis-Cluster采用Gossip协议实现了无中心式。

Redis-Cluster使用Gossip主要有两方面的作用:

  1. 去中心化,以实现分布式和弹性扩展;
  2. 失败检测,以实现高可用;

节点通信基础

Redis-Cluster中的每个 Redis 实例监听两个 TCP 端口,6379(默认)用于服务客户端查询,16379(默认服务端口+10000)用于集群内部通信。集群中节点通信方式如下:

  1. 每个节点在固定周期内通过特定规则选择几个节点发送ping消息;
  2. 接收到ping消息的节点用pong消息作为响应;

集群中每个节点通过一定规则挑选要通信的节点,每个节点可能知道全部节点,也可能仅知道部分节点,只要这些节点彼此可以正常通信,最终它们会达到一致的状态。当节点故障、新节点加入、主从关系变化、槽信息变更等事件发生时,通过不断的ping/pong消息通信,经过一段时间后所有的节点都会知道集群全部节点的最新状态,从而达到集群状态同步的目的。

Gossip消息种类

Gossip协议的主要职责就是信息交换。信息交换的载体就是节点彼此发送的Gossip消息,常用的Gossip消息可分为:ping消息、pong消息、meet消息、fail消息。

  1. meet消息:用于通知新节点加入。消息发送者通知接收者加入到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换;
  2. ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其它节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。ping消息发送封装了自身节点和部分其它节点的状态数据;
  3. pong消息:当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新;
  4. fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态;

Redis-Cluster 节点通信-成本与效率的权衡

由于集群内部需要频繁地进行节点信息交换,而ping/pong消息会携带当前节点和部分其它节点的状态数据,势必会加重带宽和计算的负担。Redis集群内节点通信采用固定频率(定时任务每秒执行10次)。因此,节点每次选择需要通信的节点列表变得非常重要。通信节点选择过多虽然可以做到信息及时交换但成本过高。节点选择过少则会降低集群内所有节点彼此信息交换的频率,从而影响故障判定、新节点发现等需求的速度。因此Redis集群的Gossip协议需要兼顾信息交换实时性和成本开销。

4.1 节点间是如何交换信息的?
Redis节点启动之后,会每间隔100ms执行一次集群的周期性函数clusterCron()。在Redis源码server.c中可见:

/* Run the Redis Cluster cron. */
    run_with_period(100) {
        if (server.cluster_enabled) clusterCron();
    }

而clusterCron()中又会调用clusterSendPing()函数,该函数用于将随机选择的节点的信息加入到ping消息体中,然后发送出去。部分源码如下:

void clusterSendPing(clusterLink *link, int type) {
    unsigned char *buf;
    clusterMsg *hdr;
    int gossipcount = 0; /* Number of gossip sections added so far. */
    int wanted; /* Number of gossip sections we want to append if possible. */
    int totlen; /* Total packet length. */
    // freshnodes = 集群总节点数 - (2=当前节点+发送消息的目的节点)
    // freshnodes 的值是ping消息体中可以携带节点信息的最大值
    int freshnodes = dictSize(server.cluster->nodes)-2;
    // wanted 的值是集群节点的十分之一向下取整,并且最小等于3
    // wanted 表示ping消息体中期望携带的其它节点信息个数
    wanted = floor(dictSize(server.cluster->nodes)/10);
    if (wanted < 3) wanted = 3;
    // 因此 wanted 最多等于 freshnodes。
    if (wanted > freshnodes) wanted = freshnodes;
 
    // 计算分配消息的最大空间
    totlen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
    totlen += (sizeof(clusterMsgDataGossip)*wanted);
    // 消息的总长最少为一个消息结构的大小
    if (totlen < (int)sizeof(clusterMsg)) totlen = sizeof(clusterMsg);
    // 分配空间
    buf = zcalloc(totlen);
    hdr = (clusterMsg*) buf;
    // 设置发送PING命令的时间
    if (link->node && type == CLUSTERMSG_TYPE_PING)
        link->node->ping_sent = mstime();
    // 构建消息的头部
    clusterBuildMessageHdr(hdr,type);
    int maxiterations = wanted*3;
    // 循环体,构建消息内容
    while(freshnodes > 0 && gossipcount < wanted && maxiterations--) {
        // 随机选择一个集群节点
        dictEntry *de = dictGetRandomKey(server.cluster->nodes);
        clusterNode *this = dictGetVal(de);
        clusterMsgDataGossip *gossip;
        int j;
 
        // 1. 跳过当前节点,不选myself节点,myself代表当前节点
        if (this == myself) continue;
 
        // 2. 偏爱选择处于下线状态或疑似下线状态的节点
        if (maxiterations > wanted*2 &&
            !(this->flags & (CLUSTER_NODE_PFAIL|CLUSTER_NODE_FAIL)))
            continue;
 
        // 以下节点不能作为被选中的节点:
        /*
            1. 处于握手状态的节点
            2. 带有NOADDR标识的节点
            3. 因为不处理任何槽而断开连接的节点
        */
        if (this->flags & (CLUSTER_NODE_HANDSHAKE|CLUSTER_NODE_NOADDR) ||
            (this->link == NULL && this->numslots == 0))
        {
            freshnodes--; /* Tecnically not correct, but saves CPU. */
            continue;
        }      
    }
//(中间部分代码省略.............)
    // 发送消息
    clusterSendMessage(link,buf,totlen);
    zfree(buf);
}

根据上述代码,相信读者可以明确:

  1. 当前节点向另一个节点发送ping消息时,携带的其它节点的消息数量至少为3,最大等于集群节点总数-2;
  2. 为ping消息体中选择携带的其它节点的信息时,采用的是混合选择模式:随机选择+偏好性选择,这样不仅可以保证Gossip协议随机传播的原则,还可以尽量将当前节点掌握的其它节点的故障信息传播出去;

如何保证消息传播的效率?

前面已经提到,集群的周期性函数clusterCron()执行周期是100ms,为了保证传播效率,每10个周期,也就是1s,每个节点都会随机选择5个其它节点,并从中选择一个最久没有通信的节点发送ping消息,源码如下:

redis实现二级索引的原理 redis多级目录_redis实现二级索引的原理_11

当然,这样还是没法保证效率,毕竟5个节点是随机选出来的,其中最久没有通信的节点不一定是全局“最久”。因此,对哪些长时间没有 “被” 随机到的节点进行特殊照顾:每个周期(100ms)扫描本地节点列表,如果发现节点最近一次接受pong消息的时间大于cluster_node_timeout/2,则立刻发送ping消息,防止该节点信息太长时间未更新。源码如下:

redis实现二级索引的原理 redis多级目录_Redis_12

规模效应-无法忽略的成本问题

  1. 关键参数cluster_node_timeout
    从上面的分析可以看出,cluster_node_timeout参数对消息发送的节点数量影响非常大。当带宽资源紧张时,可以适当调大这个参数,如从默认15秒改为30秒来降低带宽占用率。但是,过度调大cluster_node_timeout会影响消息交换的频率从而影响故障转移、槽信息更新、新节点发现的速度。因此需要根据业务容忍度和资源消耗进行平衡。同时整个集群消息总交换量也跟节点数成正比。
  2. 消息体与集群规模
    每个ping消息的数据量体现在消息头和消息体中,其中消息头空间占用相对固定。消息体会携带一定数量的其它节点信息用于信息交换,消息体携带数据量跟集群的节点数息息相关,更大的集群每次消息通信的成本也就更高,因此对于Redis集群来说并不是越大越好。

Redis-Cluster 故障转移

在上面已经介绍过单机模式的故障转移(主节点下线后,对应从节点升主并替代原主节点继续工作的过程),单机模式下故障转移需要 Sentinel 系统的辅助,与之不同,Redis 集群模式故障转移并不需要 Sentinel系统辅助,而是通过集群内部主节点选举完成,是一个“自治”的系统,以下详细介绍。

故障检测

(1)单点视角检测

集群中的每个节点都会定期通过集群内部通信总线向集群中的其它节点发送 PING 消息,用于检测对方是否在线,如果接收 PING 消息的节点没有在规定的时间内向发送 PING 消息的节点返回 PONG 消息,那么,发送 PING 消息的节点就会将接收 PING 消息的节点标注为疑似下线状态(Probable Fail,Pfail)。

(2)检测信息传播

集群中的各个节点会通过相互发送消息的方式来交换自己掌握的集群中各个节点的状态信息,如在线,疑似下线(Pfail),下线(fail)。例如,当一个主节点 A 通过消息得知主节点 B 认为主节点 C 疑似下线时,主节点 A 会更新自己保存的集群状态信息,将从 B 获得的下线报告保存起来。

(3)基于检测信息作下线判决

如果在一个集群里,超过半数的持有slot(槽)的主节点都将某个主节点 X 报告为疑似下线,那么,主节点 X 将被标记为下线(fail),并广播出去,所有收到这条 fail 消息的节点都会立即将主节点 X 标记为 fail。至此,故障检测完成。

选举

(1)从节点拉票

基于故障检测信息的传播,集群中所有正常节点都将感知到某个主节点下线的信息,当然也包括这个下线主节点的所有从节点。当从节点发现自己的复制的主节点状态为已下线时,从节点就会向集群广播一条请求消息,请求所有收到这条消息并且具有投票权的主节点给自己投票。

(2)拉票优先级

严格的讲,从节点在发现其主节点下线时,并不是立即发起故障转移流程而进行“拉票”的,而是要等待一段时间,在未来的某个时间点才发起选举。这个时间点有如下计算表达式:其中,固定延时500ms,是为了留出时间,使主节点下线的消息能传播到集群中其他节点,这样集群中的主节点才有可能投票;随机延时是为了避免两个从节点同时开始故障转移流程;rank 表示从节点的排名,排名是指当前从节点在下线主节点的所有从节点中的排名,排名主要是根据复制数据量来定,复制数据量越多,排名越靠前,因此,具有较多复制数据量的从节点可以更早发起故障转移流程,从而更可能成为新的主节点。
mstime() + 500ms + random()%500ms + rank*1000ms
(3)主节点投票

如果一个主节点具有投票权(负责处理 Slot 的主节点),并且这个主节点尚未投票给其它从节点,那么这个主节点将向请求投票的从节点返回一条回应消息,表示支持该从节点升主。

(4)根据投票结果决策

在一个具有 N 个主节点投票的集群中,理论上每个参与拉票的从节点都可以收到一定数量的主节点投票,但是,在同一轮选举中,只可能有一个从节点收到的票数大于 N/2 + 1,也只有这个从节点可以升级为主节点,并代替已下线的主节点继续工作。

(5)选举失败

跟生活中的选举一样,选举可能失败——没有一个候选从节点获得超过半数的主节点投票。遇到这种情况,集群将会进入下一轮选举,直到选出新的主节点为止。

(6)选举算法

选举新的主节点的算法是基于 Raft 算法的 Leader Election 方法来实现的,本文就不展开了,在本课程后面介绍ETCD的章节会详述Raft算法。

故障转移

(1)身份切换

通过选举晋升的从节点会执行一系列的操作,清除曾经为从的信息,改头换面,成为新的主节点。

(2)接管职权

新的主节点会通过轮询所有 Slot,撤销所有对已下线主节点的 Slot 指派,消除影响,并且将这些 Slot 全部指派给自己。

(3)广而告之

升主了嘛,必须让圈子里面的都知道,新的主节点会向集群中广播一条 PONG 消息,将自己升主的信息通知到集群中所有节点。

(4)履行义务

在其位谋其政,新的主节点开始处理自己所负责 Slot 对应的请求,至此,故障转移完成。

Redis-Cluster 扩容

随着应用场景的升级,缓存可能需要扩容,扩容的方式有两种:垂直扩容(Scale Up)和水平扩容(Scale Out)。垂直扩容无需详述。实际应用场景中,采用水平扩容更多一些,根据是否增加主节点数量,水平扩容方式有两种。

(1)主节点数量不变

比如,当前有一台物理机 A,构建了一个包含3个 Redis 实例的集群;扩容时,我们新增一台物理机 B,拉起一个 Redis 实例并加入物理机 A 的集群;B 上 Redis 实例对 A 上的一个主节点进行复制,然后进行主备倒换;如此,Redis 集群还是3个主节点,只不过变成了 A2-B1 的结构,将一部分请求压力分担到了新增的节点上,同时物理容量上限也会增加,主要步骤如下:

将新增节点加入集群;
将新增节点设置为某个主节点的从,进而对其进行复制;
进行主备倒换,将新增的节点调整为主。
(2)增加主节点数量

不增加主节点数量的方式扩容比较简单,但是,从负载均衡的角度来看,并不是很好的选择。例如,如果主节点数量较少,那么单个节点所负责的 Slot 的数量必然较多,很容易出现大量 Key 的读写集中于少数节点的现象,而增加主节点的数量,可以更有效的分摊访问压力,充分利用资源。主要步骤如下:

将新增节点加入集群;
将集群中的部分 Slot 迁移至新增的节点。

其它分布式 Redis 方案

作为Redis官方出品,Redis-Cluster有很多优点,但是,当集群规模超过百级别后,Gossip协议的效率将会显著下降,通信成本越来越高。此外,Redis-Cluster模式下,16384个slot中只要有任意一个slot不可用,整个集群都将不可用,换言之,任何一个被指派slot的主节点故障,在其恢复期间,集群都是不可用的。鉴于此,Redis-Cluster并不适合超大规模商用场景,国内IT巨头基本上采用的都是自研的集群方案,如阿里云ApsaraDB for Redis/ApsaraCache,腾讯的CRS;当然,Redis-Cluster也有很成功的商用案例:亚马逊采用的就是Redis-Cluster。

TIPS

通常,Redis-Cluster的规模都控制在400个节点以内,以单节点支撑8W QPS 计算,400个节点可支持 200*8=1600W QPS(每个主节点配置一个从节点),足以支撑绝大多数应用场景。此外,Redis-Cluster使用简单,无需深度定制便可实现商用,成本较低,因此,即便它不适合超大规模商用,仍然是一个非常优秀的集群方案。

预备知识

基于客户端的分片

如下图所示,客户端与Redis节点直连,为了提高可用性,每个主节点挂一个从节点,故障倒换可由“哨兵”系统实现(其它方案也可实现)。客户端对任何一个主节点的读写操作本质上就是单机模式下的读写操作;对于一个key-value,其读写节点完全由客户端决定。比如,采用hash算法:

hash(key)%N = 目标节点编号, 其中N为Redis主节点的数量,哈希取余的方式会将不同的 key 分发到不同的Redis主节点上。

redis实现二级索引的原理 redis多级目录_Redis_13

但是,hash算法有很多缺陷:

  1. 不支持动态增加节点:当业务量增加,需要增加服务器节点后,上面的计算公式变为:hash(key)%(N+1) ,那么,对于同一个key-value,增加节点前后,对应的Redis节点可能是完全不同的,可能导致大量之前存储的数据失效;为了避免解决这个问题,需要将所有数据重新计算hash值,再写入Redis 服务器。
  2. 不支持动态减少节点,原理同上。
    鉴于hash算法的不足,在实际应用中一般采用“一致性哈希”算法,在增删节点的时候,可以保证尽可能多的缓存数据不失效。关于一致性哈希算法,网上文章很多,读者可自行研读。

客户端分片的缺点:

采用客户端分片具有逻辑简单,性能高的优点,但缺点也很明显:业务逻辑与数据存储逻辑耦合,可运维性差;多业务各自使用 Redis,集群资源难以管理。

基于代理的分片

为了克服客户端分片业务逻辑与数据存储逻辑耦合的不足,可以通过 Proxy 将业务逻辑和存储的逻辑隔离。客户端发送请求到一个代理,代理解析客户端的数据,将请求转发至正确的节点,然后将结果回复给客户端。这种架构还有一个优点就是可以把 Proxy 当成一个中间件,在这个中间件上可以做很多事情,比如可以把集群和主从的兼容性做到几乎一致、可以做无缝扩减容、安全策略等。

基于代理的分片已经有很多成熟的方案,如开源的 Codis,阿里云的 ApsaraDB for Redis/ApsaraCache,腾讯的CRS 等大企业很多也是采用 Proxy+Redis-server 的架构。

基本原理如下图所示:

redis实现二级索引的原理 redis多级目录_redis实现二级索引的原理_14

基于代理分片的缺点:

没有完美的架构,由于使用了proxy,带宽和CPU基本也是double的,对资源的消耗会大很多。
7.2 Codis 架构
Codis 是一个分布式 Redis 解决方案,对于上层的应用来说,连接到 Codis Proxy 和连接原生的 Redis Server没有明显的区别 (参考6.1中的代理分片模式),客户端可以像使用单机 Redis 一样使用。

架构图如下:

redis实现二级索引的原理 redis多级目录_redis实现二级索引的原理_15

Codis 简介

从Codis的官方架构图可以看出,Codis 主要有四部分组成:

  1. Codis Proxy (codis-proxy)
  2. Codis Manager (codis-config)
  3. Codis Redis (codis-server)
  4. ZooKeeper

codis-proxy

是客户端连接的 Redis 代理服务, codis-proxy 本身实现了 Redis 协议, 表现得和一个原生的 Redis 没什么区别 (就像 Twemproxy), 对于一个业务来说, 可以部署多个 codis-proxy, codis-proxy 本身是无状态的.
codis-config

是 Codis 的管理工具, 支持包括, 添加/删除 Redis 节点, 添加/删除 Proxy 节点, 发起数据迁移等操作. codis-config 本身还自带了一个 http server, 会启动一个 dashboard, 用户可以直接在浏览器上观察 Codis 集群的运行状态.
codis-server

是 Codis 项目维护的一个 Redis 分支, 基于 2.8.13 开发, 加入了 slot 的支持和原子的数据迁移指令. Codis 上层的 codis-proxy 和 codis-config 只能和这个版本的 Redis 交互才能正常运行.
ZooKeeper

Codis 依赖 ZooKeeper 来存放数据路由表和 codis-proxy 节点的元信息, codis-config 发起的命令都会通过 ZooKeeper 同步到各个存活的 codis-proxy.

Codis 特点

Codis出现在Redis-Cluster之前,事实上,正是由于Redis官方推出Redis-Cluster太晚,IT巨头们又都是急性子,只好自力更生,搞大生产运动,才诞生了众多定制化或开源的分布式Redis方案。Codis 作为其中的代表,具有以下特点:

  1. 自动平衡
  2. 使用非常简单
  3. 图形化的面板和管理工具
  4. 支持绝大多数 Redis 命令,完全兼容twemproxy
  5. 支持 Redis 原生客户端
  6. 安全而且透明的数据移植,可根据需要轻松添加和删除节点
  7. 提供命令行接口,支持 RESTful APIs

类Codis 架构——proxy + Redis-server

在第二节中曾提及:实现Redis分布式的基础是分片。目前,主流的分片方案有三种:Redis-Cluster、客户端分片、代理分片。除了官方推出的Redis-Cluster,大多数IT公司采用的都是基于代理的分片模式,即:proxy + Redis-server,这与Codis的原理类似,因此也称为 “类Codis” 架构,其架构图如下。

redis实现二级索引的原理 redis多级目录_数据_16

架构特点

分片算法:基于代理的分片原理,将物理节点映射到slot (Codis slot数为1024,其它方案一般为16384),对key-value进行读写操作时,采用一致性hash算法或其它算法(如Redis-Cluster采用的CRC16),计算key对应的slot编号,根据slot编号转发到对应的物理节点;

分片实例之间相互独立,每组 一个master 实例和多个slave,其本质就是“1主N从”的单机模式;

路由信息存放依赖第三方存储组件,如 zookeeper 或etcd;

High Availability:Redis单机模式不支持自动故障倒换,为了保证高可用,需要类似“哨兵系统”的HA组件来支持高可用。

三主三从实现

redis实现二级索引的原理 redis多级目录_数据_17