去年本博主发表了一篇关于Redis Cluster搭建的博文,因为当时没有找到任何官方的说明文档,所以只能根据Redis主页上的Redis cluster Specification一步步地摸索出了一种搭建方法,可以说是彻彻底底的野路子。后来自己怕误人子弟,还很自觉地在原文中对这一点作了说明。后来某位网友发来一篇博文的链接:http://no-fucking-idea.com/blog/2012/04/16/setting-up-redis-cluster/,其中就是介绍如何使用redis源码包中的redis-trib.rb工具来实现Redis Cluster的搭建,不仅实现方法异常简单而且发表时间也早了一年有余,由此可知自己的那篇博文确实是闭门造车了。最近Redis官网正式推出了支持Redis Cluster的3.0 Beta版,我在官网上也找到了关于Redis Cluster搭建的tutorial,今天按照上面的步骤尝试了一把,果真是极好的!于是乎赶紧发一篇博文,希望已经被俺前一篇博文毒害的弟兄们早日悬崖勒马,回归正道!
首先,下载支持Redis Cluster的源码包,最方便的当然就是直接下载3.0 Beta版,其链接地址为:https://github.com/antirez/redis/archive/3.0.0-beta1.tar.gz。解压安装后,对Redis进行配置,主要就是将cluster功能打开,与Cluster相关的具体配置如下所示。
Redis集群功能说明
作者:nosqlfan on 星期一, 十月 24, 2011 · 2条评论 【阅读:14,306 次】
虽然目前可以通过在客户端做hash的方法来构建Redis集群,但Redis原生的集群支持还是颇受期待。本文是对Redis集群功能官方描述文档的一个翻译,译者是@PPS萝卜同学,也感谢他的投稿分享。
介绍
这篇文档主要是为了说明正在进展中的Redis集群功能。文档主要分为两个部分,前一部分主要介绍我在非稳定分支已完成的代码,后一部分主要介绍还有哪些功能待实现。
本文档所有的说明都有可能在将来由于设计原因而进行更改,而未实现的计划比已实现的功能更有可能会被更改。
本文档包含了所有client library所需要的细节,但是client library的作者们需要提前意识到真正实现的细节在将来很有可能会有变化。
什么是Redis集群?
Redis集群是一个实现分布式并且允许单点故障的Redis高级版本。
Redis集群没有最重要或者说中心节点,这个版本最主要的一个目标是设计一个线性可伸缩(可随意增删节点?)的功能。
Redis集群为了数据的一致性可能牺牲部分允许单点故障的功能,所以当网络故障和节点发生故障时这个系统会尽力去保证数据的一致性和有效性。(这里我们认为节点故障是网络故障的一种特殊情况)
为了解决单点故障的问题,我们同时需要masters 和 slaves。 即使主节点(master)和从节点(slave)在功能上是一致的,甚至说他们部署在同一台服务器上,从节点也仅用以替代故障的主节点。 实际上应该说 如果对从节点没有read-after-write(写并立即读取数据 以免在数据同步过程中无法获取数据)的需求,那么从节点仅接受只读操作。
已实现子集
Redis集群会把所有的单一key存储在非分布式版本的Redis中。对于复合操作比如求并集求交集之类则未实现。
在将来,有可能会增加一种为“Computation Node”的新类型节点。这种节点主要用来处理在集群中multi-key的只读操作,但是对于multi-key的只读操作不会以集群传输到Computation Node节点再进行计算的方式实现。
Redis集群版本将不再像独立版本一样支持多数据库,在集群版本中只有database 0,并且SELECT命令是不可用的。
客户端与服务端在Redis集群版中的约定
在Redis集群版本中,节点有责任/义务保存数据和自身状态,这其中包括把数据(key)映射到正确的节点。所有节点都应该自动探测集群中的其他节点,并且在发现故障节点之后把故障节点的从节点更改为主节点(原文这里有“如果有需要” 可能是指需要设置或者说存在从节点)。
集群节点使用TCP bus和二进制协议进行互联并对任务进行分派。各节点使用gossip 协议发送ping packets给集群其他节点以确定其他节点是否正常工作。cluster bus也可以用来在节点间执行PUB/SUB命令。
当发现集群节点无应答的时候则会使用redirections errors -MOVED and -ASK命令并且会重定向至可用节点。理论上客户端可随意向集群中任意节点发送请求并获得重定向,也就是说客户端实际上并不用关心集群的状态。然而,客户端也可以缓存数据对应的节点这样可以免去服务端进行重定向的工作,这在一定程度上可以提高效率。
Keys分配模式
一个集群可以包含最多4096个节点(但是我们建议最多设置几百个节点)。
所有的主节点会控制4096个key空间的百分比。当集群稳定之后,也就是说不会再更改集群配置(更改配置指的增删节点),那么一个节点将只为一个hash slot服务。(但是服务节点(主节点)可以拥有多个从节点用来防止单点故障)
用来计算key属于哪个hash slot的算法如下:
HASH_SLOT = CRC16(key) mod 4096
Name: XMODEM (also known as ZMODEM or CRC-16/ACORN)
Width: 16 bit
Poly: 1021 (That is actually x^16 + x^12 + x^5 + 1)
Initialization: 0000
Reflect Input byte: False
Reflect Output CRC: False
Xor constant to output CRC: 0000
Output for "123456789": 31C3
这里我们会取CRC16后的12个字节。在我们的测试中,对于4096个slots, CRC16算法最合适。
集群节点特性
在集群中每个节点都拥有唯一的名字。节点名为16进制的160 bit随机数,当节点获取到名字后将被立即启用。节点名将被永久保存到节点设置文件中,除非系统管理员手动删除节点配置文件。
节点名是集群中每个节点的身份证明。在不更改节点ID的情况下是允许修改节点IP和地址的。cluster bus会自动通过gossip协议获取更改后的节点设置。
每个节点可获知其他节点的信息包括:
- IP 端口
- 状态
- 管理的hash slots
- cluster bus最后发送PING的时间
- 最后接收到PONG的时间
- 从节点数量
- 节点ID
无论是主节点还是从节点都可以通过CLUSTER NODES命令来获取以上信息
示例如下:
$ redis-cli cluster nodes
d1861060fe6a534d42d8a19aeb36600e18785e04 :0 myself - 0 1318428930 connected 0-1364
3886e65cc906bfd9b1f7e7bde468726a052d1dae 127.0.0.1:6380 master - 1318428930 1318428931 connected 1365-2729
d289c575dcbc4bdd2931585fd4339089e461a27d 127.0.0.1:6381 master - 1318428931 1318428931 connected 2730-4095
节点交互
所有节点总是允许接受来自cluster bus的连接请求,并且即使请求PING的节点是不可信的也会进行应答。然而,所有来自非集群节点的packets都会被忽略。
只有以下两种情况节点才会把其他节点认为是集群的一部分:
如果一个节点使用 MEET message 介绍自己。MEET message 命令是强制其他节点把自己当成是集群的一部分。只有系统管理员使用 CLUSTER MEET ip port 命令节点才会发送MEET message给其他节点。
另外一种方式就是通过集群节点间的推荐机制。例如 如果A节点知道B节点属于集群,而B知道C节点属于集群,那么B将会发送gossip信息告知A:C是属于集群的。当A获得gossip信息之后就会尝试去连接C。
这意味着,当我们以任意连接方式为集群加入一个节点,集群中所有节点都会自动对新节点建立信任连接。也就是说,集群具备自动识别所有节点的功能,但是这仅发生在当系统管理强制为新节点与集群中任意节点建立信任连接的前提下。
这个机制使得集群系统更加健壮。
当一个节点故障时,其余节点会尝试连接其他所有已知的节点已确定其他节点的健壮性。
被移动数据的重定向
Redis客户端被允许向集群中的任意节点发送命令,其中包括从节点。被访问的节点将会分析命令中所需要的数据(这里仅指请求单个key),并自己通过判断hash slot确定数据存储于哪个节点。
如果被请求节点拥有hash slot数据(这里指请求数据未被迁移过 或者 hash slot在数据迁移后被重新计算过),则会直接返回结果,否则将会返回一个 MOVED 错误。
MOVED 错误如下:
GET x
-MOVED 3999 127.0.0.1:6381
这个错误包括请求的数据所处的 hash slot(3999) 和 数据目前所属的IP:端口。客户端需要通过访问返回的IP:端口获取数据。即使在客户端请求并等待数据返回的过程中,集群配置已被更改(也就是说请求的key在配置文件中所属的节点ID已被重定向至新的IP:端口),目标节点依然会返回一个MOVED错误。
所以虽然在集群中的节点使用节点ID来确定身份,但是map依然是靠hash slot和Redis节点的IP:端口来进行配对。
客户端虽然不被要求但是应该尝试去记住hash slot 3999现在已被转移至127.0.0.1:6381。这样的话,当一个新的命令需要从hash slot 3999获取数据时就可以有更高的几率从hash slot获取到正确的目标节点。
假设集群已经足够的稳定(不增删节点),那么所有的客户端将会拥有一份hash slots对应节点的数据,这可以使整个集群更高效,因为这样每个命令都会直接定向到正确的节点,不需要通过节点寻找节点或者重新计算hash slot对应的节点。
集群不下线更新配置
Redis集群支持线上增删节点。实际上对于系统来说,增加和删除节点在本质上是一样的,因为他们都是把hash slot从一个节点迁移至另外一个节点而已。
增加节点:集群中加入一个空节点并且把hash slot从已存在的节点们移至新节点。
删除节点:集群删除一个已存在节点并且把hash slot分散到已存在的其他节点中。
所以实现这个功能的核心就是迁移slots。实际上从某种观点上来说,hash slot只不过是一堆key的合集,所以Redis集群要做的事情只是在重分片的时候把一堆key从一个实例移动到另外一个实例。
为了清楚的了解这是如何实现的,我们需要先了解一下CLUSTER用来控制slots传输的底层命令。
这些底层命令包括:
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 是用来在Redis节点上增加/删除slots。当hash slots被赋值之后他们会通过gossip协议在整个集群进行广播(例如:大喊一声 兄弟们 我现在住在X节点 有需要我的以后请到X节点来找我)。当slots被添加,ADDSLOTS 命令是用来通知集群其余所有节点最高效的方法。
SETSLOT 命令是用来给把slot注册给一个特殊的node ID(也就是说ADDSLOTS 和 DELSLOTS 对slots进行操作是不指定节点的 而SETSLOT 是会指定节点的)。另外 SETSLOT 还包含两个特殊的状态 MIGRATING 和 IMPORTING:
当一个slot是以 MIGRATING 状态进行设置,那么目标节点将在确认key存在的前提下接受这个hash slot的所有请求,否则查询会被使用 -ASK 重定向至源节点。
当一个slot是以 IMPORTING 状态进行设置,那么目标节点只接受被设置过ASKING命令的所有请求,否则查询将会通过 -MOVED错误重定向至真正的hash slot所有者。
(MIGRATING 和 IMPORTING 我自己也没太看懂 所以这里不敢保证翻译的没有问题)
当你第一次看到以上内容的时候或许会感到困惑,不过没关系,现在我们来把思路理清楚。假设我们有2个Redis节点,一个叫A,另一个叫B。现在我们希望把hash slot8 从A移动到B,那么我们执行的命令应该如下:
We send B: CLUSTER SETSLOT 8 IMPORTING A
We send A: CLUSTER SETSLOT 8 MIGRATING B
所有来自客户端对hash slot8的查询每次都会被导向至节点A,实际过程如下:
所有对A节点存在的数据查询会由A节点来处理
所有对A节点不存在的数据查询会由B节点来处理
我们会发现我们将会无法在A节点创建任何新的数据,因为会被导向B节点。为了解决这个问题,我们设计了一个叫redis-trib的特殊客户端来保证把A节点所有存在的key迁移至B节点。
我们用以下命令来处理:
CLUSTER GETKEYSINSLOT slot count
上面的命令将会返回hash slot中 count keys。对每一个key,redis-trib都会给A节点发送一个 MIGRATE 命令,这个命令会以一种原子的方式把key从A迁移到B(两个节点在迁移过程中都会被锁定)。
以下展示 MIGRATE 如何工作:
MIGRATE target_host target_port key target_database id timeout
MIGRATE 命令会先连接目标节点,并把目标key序列化后进行传输,当源节点收到OK返回值后会删除源节点上的key删除。所以从这个观点上来看,一个key只能存在A或者B而不会同时存在与A和B。
ASK 重定向
在之前的章节我们说了一下ASK重定向,为什么我们不能只是简单的使用 MOVED 重定向?因为如果使用MOVED命令则有可能会为一个key轮询集群中所有的节点,而ASK命令只询问下一个节点。
ASK是必要的因为在对于hash slot8的下一次查询命令依然是发送给A节点,我们希望客户端先尝试在A节点找数据然后在获取不到的情况下再向B节点请求数据。
然后我们真正的需求是客户端在向A节点请求数据失败后仅尝试向B节点请求数据而不再轮询。节点B将只接受带ASKING命令的IMPORTING 数据查询。
简单说,ASKING 命令给IMPORTING slot添加了一个只轮询一次的标记。
Clients implementations hints
TODO Pipelining: use MULTI/EXEC for pipelining.
TODO Persistent connections to nodes.
TODO hash slot guessing algorithm.
单点故障
节点故障侦测
故障侦测使用以下方法实现:
- 如果一个节点没有在给定时间内回复PING请求,则该一个节点会被其他节点设置 PFAIL 标志(possible failure 有可能故障)
- 如果一个节点被设置 PFAIL 标志,那么对目标节点设置 PFAIL 标志的节点会在节点之间互相进行广播通知并通知其他节点发送PING请求
- 如果有一个节点被设置 PFAIL 标志,并且其他节点也认同其为 PFAIL 状态,那么该节点会被设置为 FAIL 状态(故障)
- 一旦一个节点被设置 FAIL 标志,那么对故障节点设置 FAIL 标志的节点会通知其余所有节点
所以实际只有大多数节点认同的情况下,一个节点才会被设置为故障状态
(还在努力实现)一旦一个节点被设置为故障,那么其他任何节点收到来自故障节点的PING或者连接请求则会返回“MARK AS FAIL”从而强制故障节点把自己设置为故障
集群状态侦测(目前仅实现了一部分):每当集群配置文件发生变更,所有集群节点都会重新扫描节点列表(这可以是由更改一个hash slot 或者只是一个节点故障造成的)
每个被扫描的节点会返回以下状态中的一个:
这意味着Redis集群被设计为有能力拒绝对故障节点的查询。然而这里有一个特例,就是一个节点从被设置为 PFAIL 到被设置为 FAIL 是有时间差的,如果仅仅是被设置为 PFAIL 还是有可能对该节点尝试连接
另外,Redis集群将不再支持MULTI/EXEC批量方法
从节点推举制度(未实现)
每个主节点可以拥有0个或者多个从节点。当主节点发生故障的时候,从节点有责任/义务推举自己成为主节点。假设我们有A1,A2,A3三个节点,A1是主节点并且A2和A3为A1的从节点
如果A1发生故障并且长时间未回复PING请求,那么其他节点将会将A1标记为故障节点。当这种情况发生的时候,第一个从节点将会尝试推举自己为主节点。
定义第一个从节点非常简单。取所有从节点中节点ID最小的那个。如果第一个从节点也被标记为故障,那么就由第二个从节点推举自己,以此类推。
实际流程是:集群配置被变更(节点故障导致的),故障节点所有的从节点检测自己是否是第一个从节点。从节点在升级为主节点后会通知其他节点更改配置
保护模式(未实现)
如果部分节点由于网络原因被隔离(比如断网),则这些节点会停止判断其他节点是否正常,而会开始从节点推举或者其他操作去更改集群配置。为了防止这种情况发生,节点间一旦发现大部分节点在一段时间内被标记为 PFAIL 或者 FAIL 状态则会立即让集群启动保护模式以阻止集群采取任何行动(更改配置)。
一旦集群状态恢复正常则保护模式会被取消
主节点多数原则(未完成)
对于发生网络故障的情况,2个或者更多的分片有能力处理所有的hash slots。而这会影响集群数据的一致性,所以网络故障应该导致0或者只有1个分区可用。
为了强制此规则生效,所有符合主节点多数原则的节点应该强制不处理任何命令。
Publish/Subscribe(已实现,但是会重定义)
在一个Redis集群中,所有节点都被允许订阅其他节点或者对其他节点进行广播。集群系统会保证所有的广播通知给所有节点。
目前的实现仅仅是简单的一一进行广播,但是在某种程度上广播应该使用bloom filters或者其他算法进行优化。
另外,关于Redis Cluster的一些信息,你还可以参考本站较早的文章:《听 antirez 讲 Redis Cluster》(视频+PPT)
http://quietmadman.blog.51cto.com/3269500/1553232
这里使用的 redis 版本是 redis-3.0.0-beta8(2.9.57)来安装和配置的。
整个安装和配置过程都是参考的 redis 官方的 cluster 手册: http://redis.io/topics/cluster-tutorial
其他的一些 API 和 redis cluster 相关的开源项目有:
https://github.com/xetorthio/jedis
https://github.com/antirez/redis-rb-cluster
https://github.com/Grokzen/redis-py-cluster
在安装配置之前对 redis cluster 有了一点初步的了解:
+ 每一个 redis cluster 节点都至少需要两个 tcp 连接,一个是用于为 client 服务的监听端口(如: 6379),另一个则用于为 cluster 节点通信提供的通道(cluster bus,如:6379+10000 = 16379);
+ cluster 节点之间传输的协议为 binary protocol,主要用于 故障检测(failure detection),配置更新(configuration update),故障转移授权(failover authorization)等等;
+ Redis Cluster 并没有采用一致性 hash 算法来对 data 进行 sharding,而是采用了简单的 hash slot 机制来实现 -- 计算给定 key 的 hash slot 槽位 -- CRC16(key) % 16384 ;
+ Redis Cluster 最多能够支持 16384 个节点;
+ 为了把所有 slot 占满,当节点少的时候,需要每一个节点分配一定范围的 slots,如:
- Node A contains hash slots from 0 to 5500
- Node B contains hash slots from 5501 to 11000
- Node C contains hash slots from 11001 to 16384
当有新的 node 进来,那需要从 A,B,C 中移出一些 slots 分配给 D。
当有节点要退出(如:A),那就需要将 A 上的 slots 再分配给 B 和 C。
+ Redis Cluster 支持 master-slave 模型来实现高可用性,一个节点退出,可以再次选举出该节点的某一个 slave 节点作为新的 master 节点来服务;
下面开始安装配置:
1,创建本地测试目录:
1
|
$ mkdir -p /root/test/redis-cluster
|
2,本次测试以在同台主机上测试集群,端口从 7000~7005 的 6 个节点
1
2
|
$ cd /root/test/redis-cluster
$ mkdir 7000 7001 7002 7003 7004 7005
|
3,在每个节点目录下分别创建 redis.conf 配置文件,这里的配置文件内容都如下(端口除外):
1
2
3
4
5
|
port 7000
cluster-enabled yes
cluster-config- file nodes.conf
cluster-node-timeout 5000
appendonly yes
|
创建好后,结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
[redis-cluster]
.
├── 7000
│ └── redis.conf
├── 7001
│ └── redis.conf
├── 7002
│ └── redis.conf
├── 7003
│ └── redis.conf
├── 7004
│ └── redis.conf
└── 7005
└── redis.conf
6 directories, 6 files
|
4,启动这 6 个节点,如下示例:
1
2
|
$ cd 7000/
$ redis-server redis.conf
|
运行后在每个节点的目录下可以看到新增了 nodes.conf 配置,如 7000 节点的配置如下:
1
2
|
123ed65d59ff22370f2f09546f410d31207789f6 :0 myself,master - 0 0 0 connected
vars currentEpoch 0 lastVoteEpoch 0
|
其中 123ed65d59ff22370f2f09546f410d31207789f6 为每个节点给自己分配的 NodeID,该 ID 在之后的 cluster 环境中唯一的标识该节点实例,每一个节点也都是通过这个 ID 来表示其他的节点的。
5,查看网络监听,可以看到同时监听 700* + 10000
1
2
3
4
5
6
7
8
9
10
11
12
|
tcp 0 0 :::17003 :::* LISTEN 10614 /redis-server
tcp 0 0 :::17004 :::* LISTEN 10655 /redis-server
tcp 0 0 :::17005 :::* LISTEN 10696 /redis-server
tcp 0 0 :::7000 :::* LISTEN 10483 /redis-server
tcp 0 0 :::7001 :::* LISTEN 10549 /redis-server
tcp 0 0 :::7002 :::* LISTEN 10573 /redis-server
tcp 0 0 :::7003 :::* LISTEN 10614 /redis-server
tcp 0 0 :::7004 :::* LISTEN 10655 /redis-server
tcp 0 0 :::7005 :::* LISTEN 10696 /redis-server
tcp 0 0 :::17000 :::* LISTEN 10483 /redis-server
tcp 0 0 :::17001 :::* LISTEN 10549 /redis-server
tcp 0 0 :::17002 :::* LISTEN 10573 /redis-server
|
6,下面开始搭建集群,这里使用源码中自带的 ruby 脚本 redis-trib.rb 来创建集群,因此首先确保系统已经安装了最新版本的 ruby 环境
redis-trib.rb 除了创建集群,还可以 check,reshard 一个存在的 cluster。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
$ . /redis-trib .rb create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005
>>> Creating cluster
Connecting to node 127.0.0.1:7000: OK
Connecting to node 127.0.0.1:7001: OK
... ...
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
127.0.0.1:7000
127.0.0.1:7001
127.0.0.1:7002
Adding replica 127.0.0.1:7003 to 127.0.0.1:7000
Adding replica 127.0.0.1:7004 to 127.0.0.1:7001
Adding replica 127.0.0.1:7005 to 127.0.0.1:7002
// M 开头的为 master 节点
// S 开头的为 slave 节点,同时 replicates 后面的 NodeID 为 master 节点ID
M: 123ed65d59ff22370f2f09546f410d31207789f6 127.0.0.1:7000
slots:0-5460 (5461 slots) master
M: 82578e8ec9747e46cbb4b8cc2484c71b9b2c91f4 127.0.0.1:7001
slots:5461-10922 (5462 slots) master
M: f5bdda1518cd3826100a30f5953ed82a5861ed48 127.0.0.1:7002
slots:10923-16383 (5461 slots) master
S: 35e0f6fdadbf81a00a1d6d1843698613e653867b 127.0.0.1:7003
replicates 123ed65d59ff22370f2f09546f410d31207789f6
S: 61dfb1055760d5dcf6519e35435d60dc5b207940 127.0.0.1:7004
replicates 82578e8ec9747e46cbb4b8cc2484c71b9b2c91f4
S: bfc910f924d772fe03d9fe6a19aabd73d5730d26 127.0.0.1:7005
replicates f5bdda1518cd3826100a30f5953ed82a5861ed48
// 回复 yes 表示接受 redis-trib 的 master-slave 的配置
Can I set the above configuration? ( type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
// 等待 cluster 的 node 加入
Waiting for the cluster to join ...
// 加入后开始给各个 node 分配 slots 槽位
// 如:127.0.0.1:7000 的 master 节点分配了 0-5460 的槽位
>>> Performing Cluster Check (using node 127.0.0.1:7000)
M: 123ed65d59ff22370f2f09546f410d31207789f6 127.0.0.1:7000
slots:0-5460 (5461 slots) master
M: 82578e8ec9747e46cbb4b8cc2484c71b9b2c91f4 127.0.0.1:7001
slots:5461-10922 (5462 slots) master
M: f5bdda1518cd3826100a30f5953ed82a5861ed48 127.0.0.1:7002
slots:10923-16383 (5461 slots) master
M: 35e0f6fdadbf81a00a1d6d1843698613e653867b 127.0.0.1:7003
slots: (0 slots) master
replicates 123ed65d59ff22370f2f09546f410d31207789f6
M: 61dfb1055760d5dcf6519e35435d60dc5b207940 127.0.0.1:7004
slots: (0 slots) master
replicates 82578e8ec9747e46cbb4b8cc2484c71b9b2c91f4
M: bfc910f924d772fe03d9fe6a19aabd73d5730d26 127.0.0.1:7005
slots: (0 slots) master
replicates f5bdda1518cd3826100a30f5953ed82a5861ed48
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
[root@sasd redis-cluster]
|
下面为使用 cluster nodes 和 slots 命令查看节点以及 slot 的信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
$ redis-cli -c -p 7000
// 查看节点分布信息
127.0.0.1:7000> cluster nodes
35e0f6fdadbf81a00a1d6d1843698613e653867b 127.0.0.1:7003 slave 123ed65d59ff22370f2f09546f410d31207789f6 0 1410835785216 4 connected
61dfb1055760d5dcf6519e35435d60dc5b207940 127.0.0.1:7004 slave 82578e8ec9747e46cbb4b8cc2484c71b9b2c91f4 0 1410835784715 5 connected
82578e8ec9747e46cbb4b8cc2484c71b9b2c91f4 127.0.0.1:7001 master - 0 1410835786217 2 connected 5461-10922
123ed65d59ff22370f2f09546f410d31207789f6 127.0.0.1:7000 myself,master - 0 0 1 connected 0-5460
bfc910f924d772fe03d9fe6a19aabd73d5730d26 127.0.0.1:7005 slave f5bdda1518cd3826100a30f5953ed82a5861ed48 0 1410835786717 6 connected
f5bdda1518cd3826100a30f5953ed82a5861ed48 127.0.0.1:7002 master - 0 1410835785715 3 connected 10923-16383
127.0.0.1:7000>
// 查看 slots 分布信息
127.0.0.1:7000> cluster slots
1) 1) (integer) 5461 // slots 的其实槽位索引
2) (integer) 10922 // slots 的结束槽位索引
// 即 [5461, 10922]
3) 1) "127.0.0.1"
2) (integer) 7001 // master
4) 1) "127.0.0.1"
2) (integer) 7004 // 对应的 slave 节点
2) 1) (integer) 0
2) (integer) 5460
3) 1) "127.0.0.1"
2) (integer) 7000
4) 1) "127.0.0.1"
2) (integer) 7003
3) 1) (integer) 10923
2) (integer) 16383
3) 1) "127.0.0.1"
2) (integer) 7002
4) 1) "127.0.0.1"
2) (integer) 7005
127.0.0.1:7000>
|
本文出自 “安静的疯子” 博客,请务必保留此出处http://quietmadman.blog.51cto.com/3269500/1553232
http://blog.csdn.net/freebird_lb/article/details/7778999
Redis-2.4.15目前没有提供集群的功能,Redis作者在博客中说将在3.0中实现集群机制。目前Redis实现集群的方法主要是采用一致性哈稀分片(Shard),将不同的key分配到不同的redis server上,达到横向扩展的目的。下面来介绍一种比较常用的分布式场景:
在读写操作比较均匀且实时性要求较高,可以用下图的分布式模式:
在读操作远远多于写操作时,可以用下图的分布式模式:
对于一致性哈稀分片的算法,Jedis-2.0.0已经提供了,下面是使用示例代码(以ShardedJedisPool为例):
package com.jd.redis.client; import java.util.ArrayList; import java.util.List; import redis.clients.jedis.JedisPoolConfig; import redis.clients.jedis.JedisShardInfo; import redis.clients.jedis.ShardedJedis; import redis.clients.jedis.ShardedJedisPool; import redis.clients.util.Hashing; import redis.clients.util.Sharded; publicclass RedisShardPoolTest { static ShardedJedisPoolpool; static{ JedisPoolConfig config =new JedisPoolConfig();//Jedis池配置 config.setMaxActive(500);//最大活动的对象个数 config.setMaxIdle(1000 * 60);//对象最大空闲时间 config.setMaxWait(1000 * 10);//获取对象时最大等待时间 config.setTestOnBorrow(true); String hostA = "10.10.224.44"; int portA = 6379; String hostB = "10.10.224.48"; int portB = 6379; List jdsInfoList =new ArrayList(2); JedisShardInfo infoA = new JedisShardInfo(hostA, portA); infoA.setPassword("redis.360buy"); JedisShardInfo infoB = new JedisShardInfo(hostB, portB); infoB.setPassword("redis.360buy"); jdsInfoList.add(infoA); jdsInfoList.add(infoB); pool =new ShardedJedisPool(config, jdsInfoList, Hashing.MURMUR_HASH, Sharded.DEFAULT_KEY_TAG_PATTERN); } /** * @param args */ publicstaticvoid main(String[] args) { for(int i=0; i<100; i++){ String key = generateKey(); //key += "{aaa}"; ShardedJedis jds = null; try { jds = pool.getResource(); System.out.println(key+":"+jds.getShard(key).getClient().getHost()); System.out.println(jds.set(key,"1111111111111111111111111111111")); } catch (Exception e) { e.printStackTrace(); } finally{ pool.returnResource(jds); } } } privatestaticintindex = 1; publicstatic String generateKey(){ return String.valueOf(Thread.currentThread().getId())+"_"+(index++); } } |
从运行结果中可以看到,不同的key被分配到不同的Redis-Server上去了。
实际上,上面的集群模式还存在两个问题:
1. 扩容问题:
因为使用了一致性哈稀进行分片,那么不同的key分布到不同的Redis-Server上,当我们需要扩容时,需要增加机器到分片列表中,这时候会使得同样的key算出来落到跟原来不同的机器上,这样如果要取某一个值,会出现取不到的情况,对于这种情况,Redis的作者提出了一种名为Pre-Sharding的方式:
Pre-Sharding方法是将每一个台物理机上,运行多个不同断口的Redis实例,假如有三个物理机,每个物理机运行三个Redis实际,那么我们的分片列表中实际有9个Redis实例,当我们需要扩容时,增加一台物理机,步骤如下:
A. 在新的物理机上运行Redis-Server;
B. 该Redis-Server从属于(slaveof)分片列表中的某一Redis-Server(假设叫RedisA);
C. 等主从复制(Replication)完成后,将客户端分片列表中RedisA的IP和端口改为新物理机上Redis-Server的IP和端口;
D. 停止RedisA。
这样相当于将某一Redis-Server转移到了一台新机器上。Prd-Sharding实际上是一种在线扩容的办法,但还是很依赖Redis本身的复制功能的,如果主库快照数据文件过大,这个复制的过程也会很久,同时会给主库带来压力。所以做这个拆分的过程最好选择为业务访问低峰时段进行。
http://blog.nosqlfan.com/html/3153.html
2. 单点故障问题:
还是用到Redis主从复制的功能,两台物理主机上分别都运行有Redis-Server,其中一个Redis-Server是另一个的从库,采用双机热备技术,客户端通过虚拟IP访问主库的物理IP,当主库宕机时,切换到从库的物理IP。只是事后修复主库时,应该将之前的从库改为主库(使用命令slaveof no one),主库变为其从库(使命令slaveof IP PORT),这样才能保证修复期间新增数据的一致性。
http://www.searchdatabase.com.cn/showcontent_78941.htm
豁达是正确乐观的面对失败的系统。不需要过多的担心,需要一种去说那又怎样的能力。因此架构的设计是如此的重要。许多优秀的系统没有进一步成长的能力,我们应该做的是使用其他的系统去共同分担工作。
Redis是其中一个吸引我的系统,一个持久性的,键值对存储内存操作高性能的平台。它是一个优秀的键值对数据库。我已经在使用了。即使AWS最近宣布开始支持ElasticCache的下级缓存。但是一个无主的redis集群仍然起着重要的作用。我们需要多系统去完成工作。同时,我们能够集合多种组件在一个容错和无主的集群里共同工作么?在这片文章中我将介绍梦幻般的redis。
一致哈希
构建一个存储数据集群的关键是有一个有效的数据存储和复制机制。我希望通过一个行之有效的方法来说明建造一个数据集群,在这个过程中你可以随意添加或移除一个Redis节点,同时保证你的数据仍然存在,而不会消失。这个方法称为一致哈希。
由于它不是一个很明显的概念,我将用一点时间来解释一下。为了理解一致哈希,你可以想像有一个函数f(x),对于给定的x总是返回一个1到60(为什么是60?你会知道的,但现在请等等)之间的结果,同样对于一个唯一的x,f(x)总是返回相同的结果。这些1到60的值按顺时针排成一个环。
现在集群中的每个节点都需要一个唯一的名字。所以如果你将这个名字传递给f(''),它将返回一个1到60之间的数字(包括1和60),这个数字就是节点在环上的位置。当然它只是节点的逻辑(记录的)位置。这样,你获得一个节点,将它传给哈希函数,获得结果并将它放到环上。是不是很简单?这样每个节点都在环上有了它自己的位置。假设这里有5个Redis节点,名字分别为'a','b','c','d','e'。每个节点都传给哈希函数f(x)并且放到了环上。在这里f('a') = 21, f('b') = 49, f('c') = 11, f('d') = 40, f('e') = 57。一定记得这里位置是逻辑位置。
那么,我们为什么要将节点放在一个环上呢?将节点放到环上的目的是确定拥有哪些哈希空间。图中的节点'd'拥有的哈希空间就是f('a')到f('d')(其值为40)之间的部分,包括f('d'),即(21, 40]。也就是说节点'd'将拥有键x,如果f(x)的属于区间(21, 40】。比如键‘apple’,其值f('apple') = 35,那么键'apple'将被存在'd'节点。类似的,每个存储在集群上的键都会通过哈希函数,在环上按顺时针方向被恰当地存到最近的节点。
虽然一致哈希讲完了,但应知道,在多数情况下,这种类型的系统是伴随着高可用性而构建。为了满足数据的高可用性,需要根据一些因子进行复制, 这些因子称为复制因子。假设我们集群的复制因子为2,那么属于'd'节点的数据将会被复制到按顺时针方向与之相隔最近的'b'和'e'节点上。这就保证了如果从'd'节点获取数据失败,这些数据能够从'b'或'e'节点获取。
不仅仅是键使用一致哈希来存储,也很容易覆盖失败了的节点,并且复制因子依然完好有效。比如'd'节点失败了,'b'节点将获取'd'节点哈希空间的所有权,同时'd'节点的哈希空间能够很容易地复制到'c'节点。
坏事和好事
坏事就是目前这些讨论过的所有概念,复制(冗余),失效处理以及集群规模等,在Redis之外是不可行的。一致哈希仅仅描述了节点在哈希环上的映射以及那些哈希数据的所有权,尽管这样,它仍然是构建一个弹性可扩展系统的极好的开端。
好事就是,也有一些分立的其他工具在Redis集群上实现一致哈希,它们能提醒节点失效和新节点的加入。虽然这个功能不是一个工具的一部分,我们将看到如何用多个系统来使一个理想化的Redis集群运转起来。
Twemproxy aka Nutcracker
Twemproxy是一个开源工具,它是一个基于memcached和Redis协议的快速、轻量的代理。其本质就是,如果你有一些Redis服务器在运行,同时希望用这些服务器构建集群,你只需要将Twemproxy部署在这些服务器前端,并且让所有Redis流量都通过它。
Twemproxy除了能够代理Redis流量外,在它存储数据在Redis服务器时还能进行一致哈希。这就保证了数据被分布在基于一致哈希的多个不同Redis节点上。
但是Twemproxy并没有为Redis集群建立高可用性支持。最简单的办法是为集群中的每个节点都建立一个从(冗余)服务器,当主服务器失效时将从(冗余)服务器提升为主服务器。为Redis配置一个从服务器是非常简单的。
这种模型的缺点是非常明显的,它需要为Redis集群中的每个节点同时运行两个服务器。但是节点失效也是非常明显,并且更加危险,所以我们怎么知道这些问题并解决。
Gossip on Serf
Gossip是一个标准的机制,通过这个机制集群上的节点可以很清楚的了解成员的最新情况。这样子集群中的每个节点就很清楚集群中节点的变化,如节点的新增和节目的删除。
Serf通过实现Gossip机制提供这样的帮助。Serf是一个基于代理的机制,这个机制实现了Gossip的协议达到节点成员信息交换的目的。Serf是不断运行,除此之外,它还可以生成自定义的事件
现在拿我们的节点集群为例,如果每个节点上也有一个serf代理正在运行,那么节点与节点之间可以进行细节交换.因此,群集中的每个节点都能清楚知道其他节点的存在,也能清楚知道他们的状态。
这还并不够,为了高可靠性我们还需要让twemproxy知道何时一个节点已经失效,这样的话它就可以据此修改它的配置。像前面提到的Serf,就可以做到这一点,它是基于一些gossip触发的事件,使用自定义动作实现的。因此只要集群中的一个Redis节点因为一些原因宕掉了,另一个节点就可以发送有成员意外掉线的消息给任何给定的端点,这个端点在我们的案例中也就是twemproxy服务器。
这还不是全部
现在我们有了Redis集群,基于一致性哈希环,相应的是用twemproxy存储数据(一致性哈希),还有Serf,它用gossip协议来检测Redis集群成员失效,并且向twemproxy发送失效消息;但是我们还没有建立起理想化的Redis集群。
消息侦听器
虽然Serf可以给任何端点发送成员离线或者成员上线消息。然而twemproxy却没有侦听此类事件的机制。因此我们需要自定义一个侦听器,就像Redis-Twenproxy代理,它需要做以下这些事情。
- 侦听Serf消息
- 更新nutcraker.yml 以反映新的拓扑
- 重启twemproxy
这个消息侦听器可以是一个小型的http服务器;它在收到一批POST数据的时候,为twemproxy做以上列表中的动作。需要记住的是,这种消息应该是一个原子操作;因为当一个节点失效(或者意外离线)的时候,所有能发消息的活动节点都将发送这个失效事件消息给侦听器;但是侦听器应该只响应一次。
数据复制
在上面的“一致哈希”中,我提到了Redis集群中的复制因素。同样它也不是Twemproxy的固有特性;Twemproxy只关心使用一致哈希存储一个拷贝。所以在我们追求理想化Redis集群的过程中,我们还需要给twemproxy或者redis自己创建这种复制的能力。
为了给Twenproxy创建复制能力,需要将复制因子作为一个配置项目,并且将数据保存在集群中相邻的redis节点(根据复制因子)。由于twemproxy知道节点的位置,所以这将给twemproxy增加一个很棒的功能。
由于twemproxy只不过是代理服务器,它的简单功能就是它强大的地方,为它创建复制管理功能将使它臃肿膨胀。
Redis 主从环
在思考这其中的工作机制的时候,我忽然想到,为什么不将每个节点设置成另一个节点的副本,或者说从节点,并由此而形成一个主从环呢?
这样的话如果一个节点失效了,失效节点的数据在这个环上相邻的节点上仍然可以获得。而那个具有该数据副本的节点,将作为该节点的从节点,并提供保存数据副本的服务。这是一个既是主节点也是从节点的环。Serf仍像通常一样作为散布节点失效消息的代理。但是这一回,我们twenproxy上的的客户端,即侦听器,将不仅仅只更新twenproxy上的失效信息,还要调整redis服务器群集来适应这个变化。
在这个环中有一个明显的,同样也是技术方面的缺陷。这个明显的缺陷是,这个环会坏掉,因为从节点的从节点无法判别哪一个是它的主节点的数据,哪一个是它的主节点的主节点的数据。这样的话就会循环的传送所有的数据。
同样技术性的问题是,一旦redis将主节点的数据同步给从节点,它就会将从节点的数据擦除干净;这样就将曾经写到从节点的所有数据删除了。这种主从环显然不能实际应用,除非修改主从环的复制机制以适应我们的需求。这样的话每个从节点就不会将它的主节点的数据同步给它的从节点。要想实现这一点,必须的条件是每个节点都可以区分出自己的密钥空间,以及它的主节点的密钥空间;这样的话它就不会将主节点的数据传送给它的从节点。
那么这样一来,当一个节点失效时,就需要执行四个动作。一,将失效节点的从节点作为它的密钥空间的所有者。二,将这些密钥散布给失效节点从节点的从节点,以便进行复制。三,将失效节点的从节点作为失效节点的主节点的从节点。最后,在新的拓扑上复位twemproxy。
如何理想化?
事实上并没有这样的Redis集群,它可以具有一致性的哈希,高可靠性以及分区容错性。因此最后一幅图片描绘了一种理想化的Redis集群;但这并不是不可能的。接下来将罗列一下需要哪些条件才能使之成为一个实实在在的产品。
透明的Twenproxy
有必要部署一个Twenproxy,这会使得Hash散列中Redis各节点的位置都是透明的。每个Redis节点都可以知道自己以及其相邻节点的位置,这些信息对于节点的主从复制以及失败节点的修复是有必要的。自从Twenproxy开源之后,节点的位置信息可以被修改、以及扩展。
Redis的数据拥有权
这是比较困难的部分的,每个Redis节点都应该记录自身拥有的数据,以及哪些是主节点的数据。当前这样的隔离是不可能的。这也需要修改Redis的基础代码,这样节点才知道何时与从节点同步,什么时候不需要。
综上所述,我们的理想化的Redis集群变成现实需要修改这样的两个组件。一直以来,它们都是非常大的工业级别,使用在生产环境中。这已经值得任何人去实现这个集群了。
原文出处:http://megam.in/post/66659169136/utopian-redis-cluster
http://www.cnblogs.com/lulu/archive/2013/06/10/3130906.html
使用zookeeper 实现一致性hash。
redis服务启动时,将自己的路由信息通过临时节点方式写入zk,客户端通过zk client读取可用的路由信息。
服务端
使用python 脚本写的守护进程:https://github.com/LittlePeng/redis-manager
脚本部署在redis-server本机,定时ping redis-server
节点失效的情况:
1.服务器与ZK服务器失去连接 Session Expired ,环境网络波动造成,需要根据网络情况设置适当zookeeper的Timeout时间,避免此情况发生
2. 服务器宕机,Zookeeper server 发现zkclient ping超时,就会通知节点下线
3. redis-server 挂了,redis-manager ping 超时主动断开与zookeeper server的连接
客户端
需要zkclient监控 节点变化,及时更新路由策略
下面是C# 版本一致性hash算法:
1: class KetamaNodeLocator
2: {
3: private Dictionary<long, RedisCluster> ketamaNodes;
4: private HashAlgorithm hashAlg;
5: private int numReps = 160;
6: private long[] keys;
7:
8: public KetamaNodeLocator(List nodes)
9: {
10: ketamaNodes = new Dictionary<long, RedisCluster>();
11:
12: //对所有节点,生成nCopies个虚拟结点
13: for (int j = 0; j < nodes.Count; j++) {
14: RedisCluster node = nodes[j];
15: int numReps = node.Weight;
16:
17: //每四个虚拟结点为一组
18: for (int i = 0; i < numReps / 4; i++) {
19: byte[] digest = ComputeMd5(
20: String.Format("{0}_{1}_{2}", node.RoleName, node.RouteValue, i));
21:
22: /** Md5是一个16字节长度的数组,将16字节的数组每四个字节一组,
23: * 分别对应一个虚拟结点,这就是为什么上面把虚拟结点四个划分一组的原因*/
24: for (int h = 0; h < 4; h++) {
25:
26: long rv = ((long)(digest[3 + h * 4] & 0xFF) << 24)
27: | ((long)(digest[2 + h * 4] & 0xFF) << 16)
28: | ((long)(digest[1 + h * 4] & 0xFF) << 8)
29: | ((long)digest[0 + h * 4] & 0xFF);
30:
31: rv = rv & 0xffffffffL; /* Truncate to 32-bits */
32: ketamaNodes[rv] = node;
33: }
34: }
35: }
36:
37: keys = ketamaNodes.Keys.OrderBy(p => p).ToArray();
38: }
41: public RedisCluster GetWorkerNode(string k)
42: {
43: byte[] digest = ComputeMd5(k);
44: return GetNodeInner(Hash(digest, 0));
45: }
46:
47: RedisCluster GetNodeInner(long hash)
48: {
49: if (ketamaNodes.Count == 0)
50: return null;
51: long key = hash;
52: int near = 0;
53: int index = Array.BinarySearch(keys, hash);
54: if (index < 0) {
55: near = (~index);
56: if (near == keys.Length)
57: near = 0;
58: }
59: else {
60: near = index;
61: }
62:
63: return ketamaNodes[keys[near]];
64: }
65:
66: public static long Hash(byte[] digest, int nTime)
67: {
68: long rv = ((long)(digest[3 + nTime * 4] & 0xFF) << 24)
69: | ((long)(digest[2 + nTime * 4] & 0xFF) << 16)
70: | ((long)(digest[1 + nTime * 4] & 0xFF) << 8)
71: | ((long)digest[0 + nTime * 4] & 0xFF);
72:
73: return rv & 0xffffffffL; /* Truncate to 32-bits */
74: }
79: public static byte[] ComputeMd5(string k)
80: {
81: MD5 md5 = new MD5CryptoServiceProvider();
82:
83: byte[] keyBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(k));
84: md5.Clear();
85: return keyBytes;
86: }
87: }