redis 3.0 集群 之 拓扑结构和数据访问

| 分类 redis  | 标签 集群 

1 redis 3.0集群功能特点:

  • 高性能:线性扩展到1000节点的能力;集群中没有代理,使用异步复制, 无数据合并操作(熟悉dynamo对这个就会有了解)
  • 可接受范围内的写安全:系统尽最大努力(但无法保证一定做到)保存住由连接到集群中多数节点的客户端发出的写操作。在一定条件下, 被确认接到到的写操作会丢失, 但概率比较低。 如果写操作由连接到集群中少数节点的客户端发出的,丢失的概率加大。
  • 高可用:如果redis集群发生分裂,在多数master节点可达,且不可达的master节点至少有一个slave可达的情况下,整个集群可用。 通过副本迁移,一个不再被任何slave复制数据的master会从正常的master接受副本。

2 拓扑结构和数据访问

不同于我们使用的twenproxy的树状结构, redis集群是一个网状结构,盗图如下:

图片

如图,redis集群中节点之间两两相连。 集群中的每个节点都开两个TCP端口,一个端口对外提供服务, 另外一个端口用于集群间节点通信。客户端可以访问集群中的任意一个节点获取数据, 如果访问的节点没有目标数据,会通过错误信息将客户端引导到正确的节点上。

2.1 集群的建立####

redis 集群文档中提到使用redis 3.0自带工具建立集群,然而测试机器上没有ruby环境,我也懒得装。所以忘掉这些ruby脚本吧, 我们使用redis 3.0集群相关的命令完成同样的工作,步骤如下:

  • 建立几个独立的redis实例,端口分布依次从7000到7005, 在redis.conf中开启集群模式
  • 通过redis-cli连接到其中一个实例,比如redis-cli -p 7000,执行cluster meet 127.0.0.1 7002; cluster meet 127.0.0.1 7004, 这就建立了一个三节点的集群
  • 执行cluster info命令,我们看到cluster_size为3, cluster_known_nodes为3, 但cluser_state的状态是failed,cluster_slots_assigned为0, 说明集群还处于不可用状态
  • 使用cluster addslots 命令将0-16383这个16384个slots分给三个节点(cluster addslots后面只能带一个具体的hash slot,因此要写个脚 本批量执行), 再在任意一个节点上执行cluster info,可发现cluster_state的状态是ok, cluster_slots_assigned为16384, cluster_slots_ok为3,集群处于可用状态

这几个简单的命令隐藏了大量的细节, 至少可以问以下两个问题:

Q1:我们只在端口为7000的节点上执行了两条命令,为何在另外两个节点上也能看到整个集群的信息? A1:在节点A上执行cluster meet B-host B-port,即告知A节点B节点加入集群了(当然A和B都开启了cluster模式)。集群中还有别的节点C,D, E,F等等, A节点会把B节点加入集群这个信息告知其它节点,比如,A告知C,D,然后C,D后分别告知E,F,最后,集群中的所有节点都知道B加入了。 A,C,D,E, F等等也都和B建立了连接,相互通信。

Q2:为什么只有集群中的节点覆盖了从0-16383的所有slots以后,集群才处于可用状态? A2:redis集群具有自动数据分片功能,基本原理是: 将数据分布到集群中不同节点上。数据分布的依据是: 将所有的key映射到[0, 16383]这个区间,每个节点负责其中的一个子区间, 任意两个节点负责的区间不能重合。访问某个key时,先计算这个key应该被映射到哪个节点负责的区间,然后去那个节点上访问数据。如果计算得到的区间不在集群中的任何节点上,那岂不是这个key就不能存放到集群了? 所以,集群可用的一个必要条件是: 集群中的节点覆盖了从0-16383的所有slots。

2.2 数据分布模型和数据访问

2.2.1 数据分布和访问模型

redis集群的key空间被分为16384个slot, 每个key都属于唯一的一个slot,每个slot通常只能存在于唯一master节点, 所以集群最多有16384个master节点(通常建议集群大小不超过1000个节点)。 集群中每个master节点会负责16384个slots的一部分,当集群未进行slots归属的再分配时(将slot从一个节点移到另外一个节点), 集群处于稳定状态;在此状态下,一个slot一定只会存在于一个master节点上,但也可能存在于该master的多个slave节点上, 用来扩展读性能。

redis集群中没有代理,客户端可尝试连接到集群中的任意节点去访问数据。如果key不存在,被访问的节点会返回错误信息给客户端,引导客户端将请求发往正确的节点。 重定向信息分为MOVED和ASK两种,下一节会展开。

2.2.2 请求的重定向

一个redis客户端可以将查询请求发送到集群中的任意节点,包括slave节点,去访问数据。当一个节点收到客户端请求, 该节点首先分析请求中的key是否属于同一个slot,如果是,则会查询这个slot位于集群的哪个节点;若该slot位于当前接受请求的节点,则请求被立即处理;否则,接受请求的节点会查询其内部维护的slot=>node映射表,然后回复给客户端一个MOVED错误,如下:

GET 
-MOVED 3999 127.0.0.1:6381

这个错误包括了key所属的slot,以及这个slot所在实例的ip和port。 收到这个错误后,客户端需要将请求重新发送到实例127.0.0.1:6381。注意,如果客户端在重新发送请求之前,集群配置发生变化,slot 3999迁移到了其它节点上,127.0.0.1:6381会返回MOVED错误;如果客户端首次发送请求访问的节点的路由信息过期了,127.0.0.1:6381也会返回MOVED错误,因为127.0.0.1:6381上本来就不存在slot 3999。

如果请求中涉及的key不属于同一个slot,该请求被拒绝。 另外, 一个客户端必须能够处理-ASK重定向错误,否则就不是一个完整的redis集群客户端。关于-ASK重定向错误,后面会展开。

2.2.3 路由的缓存和更新

实现上,并不要求redis集群客户端记住 slot => nodes的映射,客户端可以随机访问任意集群节点,只要能正确处理重定向错误即可,但可以遇见,这种客户端效率不高。redis集群客户端最好能够记住slot的配置信息,但不要求实时更新该信息, 因为访问到了错误的节点也没问题,最多就是一次重定向,而重定向会触发客户端更新(slot => nodes)的映射信息。 通常在两种情况下, 客户端需要从集群获取完整的 slot => nodes 映射表:

  • 客户端初始化时
  • 收到MOVED重定错误时

注意: 收到MOVED错误时,仅仅更新错误中包含的slot的映射信息是不够的,这不是一种高效的做法, 因为, 通常slot的再分配往往涉及到多个slot而不是单独一个slot。 注意当集群稳定时,最终所有的客户端都会获得一份slot=>nodes的映射信息。

2.2.3 多key命令的执行

redis集群实现了单机版redis的所有单key命令。 对于涉及多key的命令,如集合的交,并操作,只要命令中的所有key位于同一个节点上,也能使用。另外,redis集群实现了一个叫做hash tag机制,用来将一些key强制映射到同一个slot中,从而存储在同一节点上,这在一定程度上可以解决执行多key命令的问题。然而,在手动执行数据重新分片时,涉及多key的操作会在一段时间内不可用(数据搬迁以slot为单位,逐key搬迁,因此在搬迁过程中,属于同一个slot的key可能出现在不同实例上,不可用的时间等于搬迁一个slot的时间);而单key命令不会收到搬迁的影响。redis集群不并不支持多数据库,只有database 0,不支持select命令。

说一下hash tag的实现细节。在计算一个key的slot时, 当key中包含一个”{…}”模式的子字符串时,那么由“{”和“}”之间的子串计算所得的slot即是整个key的slot。为了处理多对“{”和”}”出现的情况,slot的计算还需要遵循如下规则:

  • 如果key中包含一个{字符
  • 且{字符的右边有一个}字符
  • 且在第一个出现的{字符和第一个出现的}字符之间,有至少一个字符

那么,计算第一个”{“和第一个”}”之间字符串的slot,作为整个key所属的slot。例如:

  • {user1000}.following 和 {user1000}.followers 同属一个slot
  • 对于foo{}{bar}要计算整个foo{}{bar}字符串的slot
  • 对于foozap the 计算{bar的slot作为整个key的slot
  • 对于 foo{bar}{zap} ,计算字串bar的slot作为整个key的slot
  • 如果一个key以{}开头,那么算法会计算整个key的slot,这可以保证二进制key的slot计算的正确性

2.3 数据搬迁

2.3.1 数据搬迁原理

redis集群支持在线增加和移除节点。在redis集群中,增加和移除节点被抽象为同一个操作: 将slot从一个节点移到另一个节点。这个操作不但可以用来在线增加节点,移除节点,还可以用来均衡集群节点之间的负载。实现slot搬迁的核心就是key的搬迁。本质上而言,一个slot就是一组key;所以搬迁一个slot, 就是搬迁slot中所有的key。

为了理解在线数据搬迁的工作原理,首先了解下CLUSTER的几个用于操作集群中slot信息的子命令:

  • CLUSTER ADDSLOTS slot1 [slot2] … [slotN]
  • CLUSTER DELSLOTS slot1 [slot2] … [slotN]
  • CLUSTER SETSLOT slot NODE node
  • CLUSTER SETSLOT slot MIGRATING node
  • CLUSTER SETSLOT slot IMPORTING node

前两个命令,ADDSLOTS和DELSLOTS,只是简单的把slot分配给指定的redis节点。分配slot意味着,通知一个指定的master节点,它将负责存储所有属于这个slot的key。一旦slot被分配,这个变化会通过gossip协议在整个集群中扩散,如何扩散将将在后面展开。 ADDSLOTS命令用于在新集群刚建立的时候, 将16384个slot的一部分分配给每个master节点。 DELSLOTS命令用于人工修改集群slot映射,或者在debug时使用。实际中很少使用。 SETSLOT命令用于将一个slot分配给指定ID的节点,如果使用SETSLOT NODE形式。否则,这个slot可以被设置为两种特殊的状态: MIGRATING和IMPORTING。这两种特殊的状态用于保证在搬迁slot的时候,能够正确访问slot中的key:

  • 当一个slot被设置为MIGRATING状态,这个slot所在节点会接受所有跟这个slot相关的请求,但是只有当key存在于该节点时,请求才会处理; 否则请求会通过一个-ASK错误转发到迁移的目的节点。
  • 当一个slot被设置为IMPORTING状态,这个slot所在节点会接受所有跟这个slot相关的请求,但只有客户端在发送这个请求前执行了ASKING命令,请求才会被处理; 否则请求会通过MOVED错误被转发到其它节点。

举个例子,假设一个有两个节点A和B的redis集群,我们想把slot 8从A移到B, 我们使用如下两个命令:

  • 向B发送命令: CLUSTER SETSLOT 8 IMPORTING A
  • 向A发送命令: CLUSTER SETSLOT 8 MIGRATING B

集群中除A,B外所有节点如果收到的请求涉及属于slot 8的key,这些节点会将请求重定向到节点A:

  • 如果请求涉及的key在A上,那么请求被A节点处理
  • 如果请求涉及的key不在A上,那么请求将会被B处理,因为A节点将会把query重定向到B节点

通过这种方式,我们不再在A上创建新的key。同时,使用redis-trib程序可以将A节点上属于slot 8的key从A搬迁到B, 使用如下命令:

CLUSTER GETKEYINSLOT slot count

以上命令会返回属于指定slot种的count个key。 对于返回的每个key,redis-trib将会给A节点发送一个MIGRATE命令,将指定的key从A搬迁到B。整个搬迁过程时原子性的,两个节点都被被锁定一段时间。

MIGRAGE target_host target_port key target_database id timeout

MIGRATE如何工作可参考:MIGRATE 。当搬迁过程结束,SETSLOT NODE 命令会被发送到涉及搬迁的两个节点,将slot设置为正常状态。这个命令通常也发到其它所有节点,以及时让整个集群感知这个slot的变动。

2.3.2 数据搬迁中对数据的访问

上面提到了ASK重定向,这里详细说明一下。为何在数据搬迁的时候, 访问状态为IMPORTING的slot时,使用ASK重定向, 而不用MOVED重定向? 因为MOVED意味着slot永久性的搬迁到了另外一个节点,以后所有的请求应该直接去那个节点。 而ASK是说,仅仅将下一次请求,发到那个节点。

这是因为,如果下次请求中的key如果属于slot 8,那么这个key可能还在A节点上。所以我们总是想先去A节点上尝试,让后去B上找,如果不存在于A节点上时。 在客户端,要严格限制命令执行的顺序: 必须确保在访问过A节点后才访问B节点; 如果向B节点发送涉及处于IMPORTING状态的slot的查询,只有先发送ASKING命令,B节点才会处理该查询。 而本质上,ASKING命令就是让B节点知道发送这个命令的客户端下一个请求将要访问IMPORTING slot中的key, 求放行。

从客户端的角度看,ASK重定向的完整语义是:

  • 如果客户端收到ASK重定向,只将被重定向的请求发到指定的节点; 而接下来的请求继续发到老的节点
  • 先向指定节点发送ASKING命令,再发送请求
  • 客户端不需要更新本地slot => node映射表

一旦slot 8迁移完成,A将给客户端发送一个MOVED信息,客户端可以永久性地将slot 8映射到新的节点上。如果A提早更新了映射表,也没问题。因为,它在给B发送请求之前不会发送ASKING命令,那么B会将请求通过MOVED错误重新定向到A。

迁移过程中,当一个slot正在迁移时, 涉及多个属于该slot的key的命令可能无法使用。具体而言,这些key还在同一个节点,那么命令可以执行;如果这些key不存在,或者分散在迁移的源节点和目的节点,集群会返回TRYAGAIN错误,客户端收到错误后应该稍后重试或者报错。


上一篇     下一篇