Redis的主从复制
拷贝自http://blog.csdn.net/stubborn_cow/article/details/50442950
Redis复制工作原理:
1.如果设置了一个Slave,无论是第一次连接还是重新连接到Master,它都会发出一个SYNC命令;设置Slave可以是在配置文件添加slaveof 主IP 端口,然后带上配置文件启动server,还可以是启动服务后通过命令的方式设置,slaveof 主IP 端口来实现。
2.当Master收到SYNC命令后,会做两件事:
a)Master执行BGSAVE,即在后台保存数据到磁盘(rdb快照文件);是通过子进程进行的
b)Master同时将新收到的写入和修改数据集的命令存入缓冲区(非查询类);
3.当Master在后台把数据保存到快照文件完成后,Master会把这个快照文件传送给Slave,而Slave则把内存清空后,加载该文件到内存中;
4.而Master也会把此前收集到缓冲区的命令,通过Redis命令协议形式转发给Slave,Slave执行这些命令,实现和Master的同步;
5.Master/Slave此后会不断通过异步方式进行命令的同步,达到最终数据的同步一致;
6.需要注意的是Master和Slave之间一旦发生重连都会引发全量同步操作,也就是命令缓冲也会重新建立。但在2.8版本以后,也可能是部分同步操作。
部分复制。2.8开始,当Master和Slave之间的连接断开后,他们之间可以采用持续复制处理方式代替全量同步。Master端为复制流维护一个内存缓冲区(in-memory backlog),记录最近发送的复制流命令;同时,Master和Slave之间都维护一个复制偏移量(replication offset)和当前Master服务器ID(Master run id)。当网络断开,Slave尝试重连时:
a.如果MasterID相同(即仍然是断网前的Master服务器),并且从断开时到当前时刻的历史命令依然在Master的内存缓冲区中存在,则Master会将缺失的这段时间的所有命令发送给Slave执行,然后复制工作就可以继续执行了;
b.否则,依然需要全量复制操作;
Redis2.8的这个部分重同步特性会用到一个新增的PSYNC内部命令,而Redis2.8以前的旧版本只有SYNC命令,不过,只要从服务器是Redis2.8或以上版本,它就会根据主服务器的版本决定到底是使用PSYNC还是SYNC。
Redis复制机制
Slave端主要经历四个阶段:
第一阶段:与master建立连接
第二阶段:向master发起同步请求(SYNC)
第三阶段:接收master发来的RDB数据
第四阶段:载入RDB文件
下面我们就通过一张图来概述每一个阶段中,slave究竟做了什么:
Redis接收到slaveof master_host master_port 命令后并没有马上与master建立连接,而是当执行服务器例行任务serverCron,发现自己正处于REDIS_REPL_CONNECT状态,这时才真正的向master发起连接。
Master端主要做以下三件事:
1.发送RDB文件
当master接收到slave发送的sync同步命令后的执行流程如下图:
上图看似分支复杂,但我们抓住以下几点即可:
a.保存RDB文件是在一个子进程中进行的;
b.如果master已经在保存RDB文件,但是没有客户端正在等待这次BGSAVE,新添加的slave需要等到下次BGSAVE,而不能直接使用这次生成的RDB文件;
c.master会定期检查RDB文件是否保存完毕(时间事件serverCron);
接下来看一下master是如何给每一个slave发送RDB文件的:
master会为每个slave维护下列信息:
a.repldboff:当前发送RDB文件的偏移
b.repldbsize:需要发送的RDB文件大小
c.replstate:当前slave状态(等待发送数据?正在发送数据?已经发送完数据?)
RDB文件分两部分发送:
a.文件头(用来告诉slave客户端,你应该接收多少数据)
b.文件体(RDB数据)
当向某个客户端发送完RDB文件后,该客户端状态从REDIS_REPL_SEND_BULK状态变为REDIS_REPL_ONLINE。
2.发送保活命令PINGNG
3.发送变更命令
master保存RDB文件是通过一个子进程进行的,所以master依然可以处理客户端请求而不被阻塞,但是这也导致了在保存RDB文件期间,“键空间”可能发生变化,因此为了保证数据同步的一致性,master会在保存RDB文件期间,把接收到的这些可能变更数据库的“键空间”的命令保存下来,然后放到每个slave的回复列表中,当RDB文件发送完master会发送这些回复列表中的内容,并且在这之后,如果数据库发生变更,master依然会把变更的命令追加到回复列表发送给slave,这样就保证master和slave数据的一致性。由于在发送完RDB文件后,master会不定时的给slave发送”变更“命令,可能过1s,也可能过1小时,所以为了防止slave无意义等待,master需要定时发送”保活“命令PING,以此告诉slave,我还活着,不要中断连接。
至此我们已经分析了主从复制过程,总结一下大致流程:
Redis-Sentinel
拷贝自 https://segmentfault.com/a/1190000002680804
Redis-Sentinel是官方推荐的高可用(HA)解决方案,当用Redis做Master-slave的高可用方案时,加入Master宕机了,Redis本身(包括他的很多客户端)都没有实现自动进行主备切换,而Redis-Sentinel本身也是一个独立运行的进程,它能够监控多个master-slave集群,发现master宕机后能进行自动切换。
它的主要功能有以下几点:
不时地监控redis是否按照预期良好的运行;
如果发现某个redis节点运行出现状况,能够通知另外一个进程(例如它的客户端);
能够进行自动切换。当一个master节点不可用时,能够选举出master的多个slave(如果有超过一个slave的话)中的一个来作为新的master,其它的slave节点会将它所追随的master的地址改为被提升为master的slave的新地址。
sentinel支持集群
很显然,只使用单个sentinel进程来监控redis集群是不可靠的,当sentinel进程宕掉后(sentinel本身也是有单点问题,single-point-of-failure)整个集群系统将无法按照预期的方式运行。所以有必要将sentinel集群化,这样有几个好处:
即使有些sentinel进程宕掉了,依然可以进行redis集群的主备切换;
如果只有一个sentinel进程,如果这个进程运行出错,挥着是网络堵塞,那么将无法实现redis集群的主备切换;
如果有多个sentinel-redis的客户端可以随意的连接任何一个sentinel来获得关于redis集群的信息;
Sentinel版本
Sentinel当前最新的稳定版本称为Sentinel2.随着redis2.8的安装包一起发行。因为Sentinel1有很多Bug,所以强烈建议使用sentinel2。
运行Sentinel
运行sentinel有两种方式:
第一种:redis-sentinel /path/to/sentinel.conf
第二种:redis-server /path/to/sentinel.conf --sentinel
以上两种方式,都必须指定一个sentinel的配置文件sentinel.conf,如果不指定,将无法启动。sentinel默认监听26379端口,所以运行前必须确定该端口没有被别的进程占用。
Sentinel的配置
典型的配置项如下:
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallels-syncs mymaster 1
sentinel monitor resque 192.168.1.3 6380 4
sentinel down-after-milliseconds resque 10000
sentinel failover-timeout resque 180000
sentinel parallels-syncs resque 5
上面的配置项配置了两个名字分别为mymaster和resque的master,配置文件只需要配置master的信息就好了,不用配置slave的信息,因为slave能够自动检测到(master节点会有关于slave的消息)。需要注意的是,配置文件在sentinel运行期间是会被动态修改的,例如当发生主备切换时,配置文件的master会被修改为另一个slave。这样,如果重启,可以根据这个配置来恢复之前监控的redis集群状态。
sentinel monitor mymaster 127.0.0.1 6379 2
这一行代表sentinel监控的master叫做mymaster,地址为127.0.0.1:6379,行尾最后的一个2代表什么意思呢?我们知道,网络是不可靠的,有时候一个sentinel会因为网络堵塞而误以为一个master redis已经死掉了,当sentinel集群模式,解决这个问题的方法就变得很简单,只需要多个sentinel互相沟通来确认某个master是否真的死了,这个2代表,当集群中有2个sentinel认为master死了,才能真正认为该master已经不可用了。(sentinel集群中各个sentinel也有互相通信,通过gossip协议)。
除了第一行的配置,剩下有一个统一模式:
sentinel
根据option_name解释上面配置项:
down-after-milliseconds:sentinel会向master发送心跳PING来确认master是否存活,如果master在“一定时间范围”内不回应PONG或者是回复了一个错误消息,那么这个Sentinel会主管地认为这个master已经不可用了(subjectively down,简称SDOWN)。这个配置项就是配置这个时间,单位毫秒。不过需要注意的是这个时候sentinel并不会马上进行failover主备切换,这个sentinel还需要参照集群中其他的sentinel意见,当集群中超过之前配置的数量的sentinel都认为这个master不可达,那么才会客观认为这个master死了(Objectively down,简称ODOWN)
parallel-syncs:在发生failover主备切换时,这个选项用来指定最多可以有多少个slave同时对新的master进行同步,这个数字越小,完成failover时间越长,越大则意味着越多slave因为replication不可用。
其他配置项在sentinel.conf中有详细介绍。所有的配置都可以在运行命令时用命令SENTINEL SET command动态改动。
Sentinel的“仲裁会”
前面我们谈到,当一个master被sentinel集群监控时,需要制定一个参数,指定当需要判决master不可用,并且进行failover时,所需要的sentinel数量,即票数。但是当failover被触发时,failover并不会马上进行,还需要sentinel中大多数sentinel授权后才可以进行failover,若是票数比大多数还要大,则询问更多的sentinel。
配置版本号
当一个sentinel被授权后,它将会获得宕掉的master的一份最新的配置版本号,当failover执行结束以后,这个版本号将会被用于最新的配置。因为大多数sentinel都已经知道该版本号已经被要执行failover的sentinel拿走了,所以其他的sentinel都不能再去使用这个版本号。这意味着,每次failover都会附带有一个独一无二的版本号。而且sentinel集群都会遵守一个规则:如果sentinel A推荐Sentinel B去执行failover,B会等待一段时间后,自行再去对同一个master执行failover,这个等待时间是通过failover-timeout配置项配置。那么sentinel集群中sentinel不会再同一时刻并发去failover同一个master,第一个如果失败了,另外一个将会在一定时间内进行重新failover。
配置传播
一旦一个sentinel成功地对一个master进行了failover,它将会把关于master的最新配置通过广播形式通知其他sentinel,更新配置。一个failover要想被成功执行,sentinel必须能够向选为master的slave发送SLAVE OF NO ONE命令,然后能够通过INFO命令看到新master信息。
新配在集群中相互传播的方式,就是为什么我们需要当一个sentinel进行failover时必须被授权一个版本号的原因。每个sentinel使用##发布/订阅##方式持续的传播master的配置版本信息,配置传播的##发布/订阅##管道是:__sentinel__:hello。
SDOWN和ODOWN的更多细节
sentinel对于不可用有两种不同看法,一个叫做主管不可用(ODOWN),另外一个叫做客官不可用(ODOWN)。SDOWN是sentinel自己主观上检测的关于master的状态,ODOWN需要一定数量的sentinel达成一致意见才能认为一个master客观上已经宕掉,各个sentinel之间通过命令SENTINEL is_master_down_by_addr来获得其他sentinel对master的检测结果。
从sentinel的角度来看,如果发送了PING心跳后,在一定的时间内没有收到合法的回复,就达到了SDOWN的条件。这个时间通过is-master-down-after-milliseconds配置。
从SDOWN切换到ODOWN不需要任何一致性算法,只需要一个gossip协议:如果一个sentinel收到了足够多的sentinel发来消息告诉他某个master已经down掉了,SDOWN状态就会变成ODOWN。
ODOWN状态只适用master。
Sentinel之间和Slaves之间的自动发现机制
虽然sentinel集群中各个sentinel都互相连接彼此来检查对方的可用性以及互相发送消息。但是你不用在任何一个sentinel配置任何其他的sentinel的节点。因为sentinel利用了master的发布/订阅机制去自动发现其它也监控了统一master的sentinel节点。
通过向名为__sentinel__:hello的管道中发送消息来实现。
同样,你也不需要在sentinel中配置某个master的所有slave地址,sentinel会通过询问master来得到这些slave的地址。
每个sentinel通过向每个master和slave的发布/订阅频道__sentinel__:hello每秒发送一次消息,来宣布它的存在。每个sentinel也订阅了每个master和slave的频道__sentinel__:hello的内容,来发现未知的sentinel,当检测到了新的sentinel,则将其加入到自身维护的master监控列表中。每个sentinel发送的消息中也包含了其当前维护的最新的master配置,如果发现自己的配置版本低于接收到的配置版本,则会用新的配置版本更新。
网络隔离时的一致性
当某个master网络断开,重新选主,此时sentinel对与这个redis的访问会怎么样?不成功?还是拒绝?
当客户端所连接的master被隔开了,但是它仍然访问成功了,那么就会出现错误,如何避免这种错误呢?
可以配置如下:
min-slaves-to-write 1
min-slaves-max-lag 10
通过上面配置,当一个redis是master时,如果它不能向至少一个slave写数据,它将会拒绝接受客户端的写请求。由于复制是异步的,master无法向slave写数据意味着slave要么断开了连接,要么不在指定时间内向master发送同步数据请求了,第二项指定了这个时间。
Sentinel状态持久化
sentinel的状态会被持久化地写入sentinel的配置文件中,每次当收到一个新的配置时,或者新创建一个配置时,配置会被持久化到硬盘中,并带上配置的版本戳。这意味着,可以安全的停止和重启sentinel进程。
无failover时的配置纠正
即使当前没有failover正在进行,sentinel依然会使用当前的配置去设置监控的master。特别是:
根据最新配置确认为slaves的节点却声称自己是master(比如宕掉的master重新上线),这时他们会被重新配置为slave。
如果slaves连接了一个错误的master,将会被改正过来。
Slave选举与优先级
当一个sentinel准备好了要进行failover,并且收到了其他sentinel的授权,那么就需要选举出一个合适的slave来做为新的master。slave的选举主要评估slave的以下几个方面:
与master断开连接的次数
Slave的优先级
数据复制的下标
进程ID
如果一个slave与master失去联系超过10次,并且每次都超过了配置的最大失联时间(down-after-milliseconds option),并且,如果sentinel在进行failover时发现slave失联,那么这个slave就会被sentinel认为不合适用来做新的master。
更严格的定义是,如果一个slave持续断开连接的时间超过
(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state
就会被认为失去选举资格。符合上述条件的slave才会被列入master候选列表,并根据以下顺序来进行排序:
sentinel首先会根据slaves的优先级来进行排序,优先级越小的排名越靠前。
如果优先级相同,则查看复制的下标,那个从master接收的复制数最多,哪个就靠前。
如果优先级和下标都相同,就选择进程ID较小的那个。
一个redis无论是master还是slave,都必须在配置中指定一个slave优先级,master也可能称为slave,若配置为0,则永远不会选为master。
Sentinel和Redis身份验证
当一个master配置为需要密码才能连接时,客户端和slave在连接时都需要提供密码。
master通过requirepass设置自身的密码,不提供密码无法连接。slave通过masterauth来设置访问master时的密码。但是当使用了sentinel时,由于一个master可能会变成slave,一个slave也可能会变成master,所以需要同时设置上述两项。
Sentinel API
Sentinel默认运行在26379端口上。sentinel支持redis协议,所以可以使用redis-cli客户端或者其他可用的客户端来与sentinel通信。
有两种方式能够与sentinel通信:
一种是直接使用客户端向它发消息
另外一种是使用发布/订阅sentinel事件,比如说failover,或者某个redis实例运行错误,等等。
Sentinel命令
sentinel支持合法的命令如下:
PING sentinel回复PONG。
SENTINEL masters显示被监控的所有master以及他们的状态
SENTINEL master 显示指定master的信息和状态
SENTINEL slaves 显示指定master的所有slave以及他们的状态
SENTINEL get-master-addr-by-name 返回指定master的ip和端口,如果正在进行failover或者failover已经完成,将会显示被提升为master的slave的IP和端口。
SENTINEL reset 重置名字匹配该正则表达式的所有的master的状态信息,清除其前面的状态信息,以及slaves信息。
SENTINEL failover 强制sentinel执行failover,并且不需要得到其他sentinel的同意。但是failover后将最新的配置发送给其他的sentinel。
动态修改Sentinel配置
从redis2.8.4开始,sentinel提供了一组API用来添加,删除,修改master的配置。需要注意的是,如果你通过API修改了一个sentinel的配置,sentinel不会把修改的配置告诉其他sentinel。你需要自己手动地对多个sentinel发送修改配置的命令。
以下是一些修改sentinel配置的命令:
SENTINEL MONITOR 这个命令告诉sentinel去监听一个新的master
SENTINEL REMOVE 命令sentinel放弃对某个master的监听
SENTINEL SET 这个命令很像Redis的CONFIG SET命令,用来改变指定master的配置。支持多个 。例如以下实例:
SENTINEL SET objects-chche-master down-after-milliseconds 1000
只要是配置文件中存在的配置项,都可以用SENTINEL SET命令来设置。这个还可以用来设置master的属性,比如说quorum,而不需要先删除master,再重新添加master。
增加或删除sentinel
由于sentinel自动发现机制,所以添加一个sentinel到你的集群非常容易,你所需要做的只是监控到某个master上,然后新添加的sentinel就能获得其他sentinel的信息以及master所有的slave。如果你需要添加多个sentinel,建议一个接着一个添加,可以预防网络隔离带来的问题。你可以每隔30秒添加一个sentinel。最后用SENTINEL MASTER mastername来检查一下是否所有的sentinel都监控到了。
删除一个sentinel就有点复杂了,因为sentinel永远不会删除一个已经存在过的sentinel,即使它已经与组织失去联系很久了。删除一个sentinel,遵循以下步骤:
停止所需要删除的sentinel
发送一个SENTINEL RESET * 命令给所有的sentinel实例,如果你想要重置指定master上面的sentinel,只需要把*改为特定名字,注意,需要一个接着一个发,每次间隔不低于30秒。
删除旧master或者不可达slave
sentinel永远会记录好一个master的slaves,即使slave已经与组织失联好久了。这是很有用的,因为sentinel集群必须有能力把一个恢复可用的slave进行重新配置。并且,failover后,失效的master将会被标记为新的master的一个slave,这样的话,当它变得可用,就会从新的master上复制数据。
有时候你想永久的删除一个slave,你只需要发送一个SENTINEL RESET master命令给所有的sentinels,他们会更新列表里能够正确复制master的slave。
发布/订阅
客户端可以向一个sentinel发送订阅某个频道的事件的命令,当有特定的事件发生时,sentinel会通知所有订阅的客户端。需要注意的是客户端只能订阅,不能发布。订阅频道的名字与事件的名字一致。例如,频道名为sdown将会发布所有与SDOWN相关的消息给订阅者。如果想要订阅所有消息,只需要简单的使用PSUBSCRIBE *。
以下是你可以收到的消息的消息格式,如果你订阅了所有消息的话。第一个单词是频道的名字,其他是数据的格式。
instance details格式为:
如果这个redis是一个master,那么@之后的消息不会显示。
+reset-master -- 当master被重置时。
+slave -- 当检测到一个slave并添加进slave列表时。
+failover-state-reconf-slaves -- Failover状态变为reconf-slaves状态时
+failover-detected -- 当failover发生时
+slave-reconf-sent -- sentinel发送SLAVEOF命令把它重新配置时
+slave-reconf-inprog -- slave被重新配置为另外一个master的slave,但数据复制还未发生时。
+slave-reconf-done -- slave被重新配置为另外一个master的slave并且数据复制已经与master同步时。
-dup-sentinel -- 删除指定master上的冗余sentinel时(当sentinel重新启动时,可能会发生)
+sentinel -- 当master增加了一个sentinel时。
+sdown -- 进入SDOWN状态时。
-sdown -- 离开SDOWN状态时。
+odown -- 进入ODOWN状态时。
-odown -- 离开ODOWN状态时。
+new-epoch -- 当前配置版本更新时。
+try-failover -- 达到failover条件,正等待其他sentinel的选举。
+elected-leader -- 被选举为去执行failover的时候。
+failover-state-select-slave -- 开始要选择一个slave当选新master时。
no-good-slave -- 没有合适的slave来担当新master的时候。
selected-slave -- 找到一个合适的slave当选新的master。
failover-state-send-slaveof-noone -- 当把选择为新的master的slave的身份进行切换的时候。
failover-end-for-timeout -- failover由于超时而失败时。
failover-end -- failover成功完成时。
switch-master
-- 当master的地址发生变化时,通常是客户端最感兴趣的消息了。
+tilt -- 进入Tilt模式
-tilt -- 退出Tilt模式
TILT模式
redis sentinel非常依赖系统时间,例如他会使用系统时间来判断一个PING恢复用了多久。然而假如系统时间被修改了,或者是系统十分繁忙,或者是进程堵塞了。sentinel可能会出现运行不正常的情况。当系统的稳定性下降时,TILT模式是sentinel可以进入的一种保护模式。当进入TILT模式时,sentinel会继续监控工作,但是它不会有任何其他动作,他不会回应is-master-down-by-addr这样的命令了,因为在TILT模式下,检测失效节点的能力已经变得让人不可信任。如果恢复正常,持续30秒,sentinel就会推出TILT模式。
-BUSY状态
该功能还未实现。当一个脚本运行的时间超过配置的运行时间时,sentinel会返回一个-BUSY错误信号。如果这件事发生在触发一个failover之前,sentinel将会发送一个SCRIPT KILL命令,如果script是只读的话,就能成功执行。‘
Twemproxy
Twemproxy简介
twemproxy(又称nutcracker)是一个轻量级的Redis和Memcached代理,主要用来减少对后端缓存服务器的连接数。另一方面它也是目前Redis分片管理的最好方案,属于中间层代理进行分片管理的方案。它主要通过事件驱动模型来达到高并发,每收到一个请求,通过解析请求,发送到后端服务,再等待回应,发送回请求方。主要涉及三个重要的结构:server,connection,message。
每一个server其实就是一个后端的缓存服务程序,Twemproxy可以预先连接每个server或者不,根据接收到的请求具体分析出key,然后根据key来选择适当的server,具体算法可以根据配置文件选择。connection在在Twemproxy中非常重要,它分为三种类型的connection:proxy,client和server,也就是监听的socket,客户端连接的socket和连接后端的socket,其中proxy类型的工作比较简单,就是接收到请求,然后产生一个client connection或者是server connection。message是连接建立后的消息内容发送载体,它支持pipeline效果,多个message属于同一个conn,conn通过接收到的内容解析来发现几个不同的message。
Twemproxy有以下几个特点:
真正实现了多阶段处理多请求,采用单线程收发包,基于epoll事件驱动模型。
减少与redis的直接连接数量,自己创建并维护和后端server的长连接,保证长连接对于来自不同的client但去向同一server的复用。
支持redis pipelining request,将多个连接请求组成redis pipelining统一向redis请求。
自带一致性hash算法,能够将数据自动分片到后端多个redis实例上;支持多种hash算法,可以设置后端实例的权重,目前redis支持的hash算法有:one_at_a_time、md5、crc16、crc32、fnv1_64、fnv1a_64、fnv1_32、fnv1a_32、hsieh、murmur、jenkins。
支持设置HashTag;通过设置HashTag可以自己设定将同一类型的key映射到同一实例上去。
支持失败节点自动删除;可以设置重新连接该节点的时间,还可以设置连接多少次之后删除该节点;如果此server恢复正常,twemproxy又能识别此server,并恢复正常访问。
支持大部分的redis命令,redis客户端可以像访问正常redis实例一样访问Twemproxy。但是不支持对多个值的操作,比如取sets的子交并补等,也不支持事务操作。
高效;对连接的管理采用epoll机制,内部数据传输采用“Zero Copy”技术,以提高运行效率。
支持状态监控;可设置状态监控IP和端口,访问IP和端口可以得到一个json格式的状态信息串,可以设置监控信息刷新间隔时间。
Twemproxy的配置详解
接下来通过讲解配置文件来进一步了解他的工作机制。
listen
twemproxy的监听端口,可以以ip:port或name:port的形式来书写。
hash
可以选择的key值的hash算法:one_at_a_time、md5、crc16、crc32、fnv1_64、fnv1a_64、fnv1_32、fnv1a_32、hsieh、murmur、jenkins。
hash_tag
允许根据key的一个部分来计算key的hash值。hash_tag由两个字符组成,一个是hash_tag的开始,另外一个是hash_tag的结束,在hash_tag的开始和结束之间是将用于计算hash值的部分,计算结果将会用于选择服务器。
distribution
存在三种可选的配置:
ketama:ketama一致性hash算法,会根据服务器构造出一个hash ring,并为ring上的节点分配hash范围。ketama的优势在于单个节点添加、删除之后,会很大程度上保持整个集群缓存的key值可以被重用。
modula:modula非常简单,就是根据key值的hash值取模,根据取模的结果选择对应的服务器。
random:random是无论key值是什么,都随机的选择一个服务器作为key值操作的目标。
timeout
单位是毫秒,是连接到server的超时值,默认是永久等待。
backlog
监听TCP的backlog(连接等待队列)的长度,默认是512。
preconnect
是一个boolean值,指示twemproxy是否应该预连接pool中的server。默认为false。
redis
是一个Boolean值,用来识别到服务器的通讯协议是redis还是memcached,默认是false。
server_connections
每个server可以被打开的连接数,默认每个服务器开一个连接。
auto_eject_hosts
是一个boolean值,用于控制twemproxy是否根据server的连接状态重建群集。这个连接状态是由server_failure_limit阈值来控制。默认false。
server_retry_timeout
单位是毫秒,控制服务器连接的时间间隔,在auto_eject_host被设置为true时产生作用。默认是30000毫秒。
server_failure_limit
控制连接服务器的次数,在auto_eject_host被设置为true的时候产生作用,默认是2。
servers
一个pool中的服务器的地址、端口和权重的列表,包括一个可选的服务器名字,如果提供服务器名字,将会使用它决定server的次序。从而提供对应的一致性hash的hash ring。否则,将使用server被定义的次序。
性能测试总结
以下对几个要点进行了测试:
1)选择合适的hash算法,可以比较均衡的分配数据。
2)与直接访问redis相比效率损失只有10%左右。
3)如果同时部署多个Twemproxy,配置文件一致,则可以从任意一个读取。
4)若是auto_eject_host设置为true,后端一台Redis挂掉后,Twemproxy能够自动摘除。恢复后,Twemproxy能够自动识别、恢复并重新加入到Redis组中重新使用。但是twemproxy的每次群集重建有可能使得原来的数据hash不正确,造成访问不到key值情况,只能人工重新分配。一致性hash算法ketama所造成的错误率最低。
5)Redis挂掉后,后端数据是否丢失与Twemproxy无关。
6)如果要新增一台Redis,Twemproxy需要重新启动才能生效,则增加后数据分布与原来的redis分布无关,并且数据不会自动重新Reblance,需要人工单独实现。
对于后面三点其实一句话总结就是如果节点无效,会自动删除节点,重新分片;若新增节点必须重启服务,重新分片;不管增还是删,twemproxy都不支持对原有的数据进行hash迁移,如果项目需要保证数据完整,那么就需要人工单独实现。
Redis Cluster
设计原则和初衷
1.性能:这是Redis赖以生存的看家本领,增加集群功能后当然不能对性能产生太大影响,所以Redis采取了P2P而非Proxy、异步复制、客户端重定向等设计,而牺牲了部分一致性、实用性。
2.水平扩展:集群的最重要的能力当然是扩展,文档中称可以线性扩展到1000节点。
3.可用性:在Cluster推出之前,可用性要靠Sentinel保证。有了集群之后也自动具有了Sentinel的监控和自动Failover能力。
内部数据结构
Redis Cluster功能涉及三个核心的数据结构clusterState、clusterNode、clusterLink。这三个数据结构中最重要的属性就是:clusterState.slots、clusterState.slots_to_keys和clusterNode.slots了,它们保存了三种映射关系:
clusterState:集群状态。
nodes:所有结点
migrating_slots_to:迁出中的槽
importing_slots_from:导入中的槽
slots_to_keys:槽中包含的所有key,用于迁移slot时获得其包含的key
slots:Slot所属的结点,用于处理请求时判断key所在的Slot是否自己负责
clusterNode:结点信息
slots:结点负责的所有slot,用于发送Gossip消息通知其他结点自己负责的slot。通过位图方式保存节省空间,16384/8恰好是2048字节,所以槽总数16384不是随意定的。
clusterLink:与其他结点通信的连接。
集群连接
Redis集群是一个网状结构,每个节点都通过TCP连接跟其他每个节点连接。在一个有N个节点的集群中,每个节点都有N-1个流出的TCP连接和N-1个流入的连接,这些TCP连接会永久保持,并不是按需创建的。该TCP连接使用二进制协议进行通讯。节点之间使用Gossip协议来进行以下工作:
a)传播(propagate)关于集群的信息,以此来发现新的节点。
b)向其他节点发送PING数据包,以此来检查目标节点是否正常运作。
c)在特定事件发生时,发送集群信息。
除此之外,集群连接还用于在集群中发布或订阅信息。
节点握手
节点总是在集群连接端口接收连接,甚至会回复收到的ping包,即使发送ping包的节点是不可信的。然而如果某个节点不被认为是在集群中,那么所有你它发出的数据包都会被丢弃。只有在两种方式下,一个节点才会认为另一个节点是集群中的一部分:
当一个节点使用MEET消息介绍自己。一个meet消息跟一个PING消息完全一样,但它会强制让接收者接受发送者为集群中的一部分。只有在系统管理员使用CLUSTER MEET ip port命令时,节点才会发送MEET消息给其他节点。
一个已经被信任的节点能够通过传播gossip消息让另外一个节点被注册为集群中的一部分。也就是说A知道B,B知道C,那么B会向A发送C的gossip消息。A就会把C当做集群中一部分。这表示集群能够自动发现其他节点,但前提是有一个由系统管理员强制创建的信任关系。
集群信息
每个节点在集群中由一个独一无二的ID标识,该ID是一个十六进制表示的160位随机数,在节点第一次启动时由/dev/urandom生成。节点会将它的ID保存到配置文件,只要这个配置文件不被删除,节点就会一直沿用这个ID。一个节点可以改变它的IP和端口号,而不改变节点ID。集群可以自动识别出IP/端口号的变化,并将这一信息通过Gossip协议广播给其他节点知道。下面是每个节点的关联信息,并且节点会将这些信息发送给其他节点:
a)节点所使用的IP地址和TCP端口号。
b)节点的标志(flags)。
c)节点负责处理的哈希槽。
d)节点最新一次使用集群连接发送PING数据包(packet)的时间。
e)节点最近一次在回复中接收到PONG数据包的时间。
f)集群将该节点标记为下线的时间。
g)该节点的从节点数量。
如果该节点是从节点的话,那么它会记录主节点的节点ID。如果这是一个主节点的话,那么主节点ID这一栏的值为0000000。
失效检测
Redis集群失效检测是用来识别出大多数节点何时无法访问某一个主节点或从节点。
每个节点都有一份跟其他已知节点相关的标识列表。其中有两个标志是用于失效检测,分别是PFAIL和FAIL。PFAIL表示可能失效,这是一个非公认的失效类型。FAIL表示一个节点已经失效,而这个情况已经被大多数节点在,某段时间内确认过了。
PFAIL标识:
当一个节点在超过NODE_TIMEOUT时间后仍然无法访问某个节点(发送一个ping包已经等待了超过NODE_TIMEOUT时间,若是经过一半NODE_TIMEOUT时间还没收到回复,尝试重新连接),那么它会用PFAIL来标识这个不可达的节点。无论节点类型是什么,主节点和从节点都能标识其他节点为PFAIL。
FAIL标识:
单独一个PFAIL标识只是每个节点的一些关于其他节点的本地信息,它不是为了起作用而使用的,也不足够触发从节点的提升。要让一个节点被认为失效了,那需要让PFAIL上升为FAIL状态。前面提到过节点之间通过gossip消息来交互随机的已知节点的状态信息。最中每个节点都能收到一份其他每个节点标识。当下面条件满足时,PFAIL状态升级为FAIL:
某个节点A,标记另一个节点B为PFAIL。
节点A通过gossip字段收集到的集群中大部分主节点标识的B的状态信息。
大部分主节点标记B为PFAIL状态,或者在NODE_TIMEOUT*FAIL_REPORT_VALIDITY_MULT这个时间内是处于PFAIL状态。
如果以上条件都满足了,那么节点A会:
标记节点B为FAIL。
向所有节点发送一个FAIL消息。
FAIL消息会强制每个接收到这消息的节点把节点B标记为FAIL状态。
FAIL标识基本是单向的,一个节点能从PFAIL升级到FAIL状态,但要清除FAIL标识只有以下两种可能方法:
节点已经恢复可达,并且它是一个从节点。在这种情况下,FAIL标识可以清除掉,因为从节点并没有被故障转移。
节点已经恢复可达,而且他是一个主节点,但经过了很长时间(N*NODE_TIMEOUT)后也没有检查到任何从节点被提升了。
PFAIL->FAIL的转变使用一种弱协议(agreement):
1)节点是在一段时间内收集其他几点信息,所以即使大多数主节点要去“同意”标记某节点为FAIL,实际上这只是表明说我们在不同时间里从不同节点收集了信息,得出当前的状态不一定是稳定的。
2)当每个节点检测到FAIL节点的时候会强制集群里的其他节点把各自对该节点的记录更新为FAIL,但没有一种方式能保证这个消息能到达所有节点。
然而Redis集群失效检测有一个要求:最终所有节点都应该同意给定节点的状态是FAIL,或者小部分节点相信该节点处于FAIL状态,或者相信节点不处于FAIL状态。在这两种情况中,最后集群都会认为给定的节点只有一个状态:
第一种情况:如果大多数节点都标记了某节点为FAIL,由于链条反应,这个主节点最终会被标记为FAIL。
第二种情况:当只有小部分的主节点标记某个节点为FAIL的时候,从节点的提升并不会发生,并且每个节点都会根据上面的清除规则(在经过了一段时间>N*NODE_TIMEOUT后仍然没有从节点提升,使用一个更正式的算法来保证每个节点最终都会知道节点的提升)清除FAIL状态。
本质上来说,FAIL标识只是用来触发从节点提升算法的安全部分。理论上一个从节点会在它的主节点不可达的时候独立起作用并且启动从节点提升程序,然后等待主节点来拒绝认可该提升。PFAIL-FAIL的状态变化、弱协议、强制在集群的可达部分用最短时间传播状态变更的FAIL消息,这些东西增加的复杂性有实际的好处。由于这种机制,如果集群处于错误状态时,所有节点都会在同一时间停止接收写入操作,者从使用redis集群的应用角度来看是个很好的特性。还有非必要的选举,是从节点在无法访问主节点时发起,若该节点能被其他大多数主节点访问的话,这个选举会被拒绝掉。
集群阶段(Cluster epoch)
Redis集群使用一个类似于木筏算法(Raft algorithm)“术语”的概念。在Redis集群中这个术语叫做阶段(epoch),它是用来记录事件的版本号,所以当有多个节点提供了冲突信息的时候,另外的节点就可以通过这个状态来了解哪个是最新的。currentEpoch是一个64bit的unsigned数,集群中每个节点都在创建的时候设置了currentEpoch为0。当节点接收到来自其他的ping包和pong包的时候,如果发送者的epoch大于该节点的epoch,那么更新发送者的epoch为currentEpoch,所以最终所有节点都会支持最大的epoch。这个信息在此处是用于,当一个节点的状态发生变化的时候为了执行一些动作寻求其他节点的同意。
配置阶段(Configure epoch)
每一个主节点总是通过发送ping包和pong包向别人宣传他的configEpoch和一份表示他负责的哈希槽的位图。当一个新节点被创建时,主节点的configEpoch设为零。
从节点由于故障转移事件被提升为主节点,为了取代它那失效的主节点,会把configEpoch设置为他赢得选举时候的configEpoch值。configEpoch用于在不同节点提出不同配置信息的时候解决冲突。
从节点的选举与提升 从节点的选举与提升都是由从节点处理的,主节点会投票要提升哪个从节点。一个从节点的选举是在主节点被至少一个具有称为主节点必备条件的从节点标记为FAIL的状态的时候发起的。
当满足以下条件时,一个从节点可以发起选举:
该从节点的主节点处于FAIL状态。
这个主节点负责的哈希槽数目不为零。
从节点和主节点之间重复连接断线不超过一段给定时间,这是为了保证数据的可靠性。
一个从节点想要被推选出来,那么第一步应该是提高他的currentEpoch计数,并且向主节点们请求投票。
从节点通过广播一个FAILOVER_AUTH_REQUEST数据包给集群里每一个主节点来请求投票。然后等待回复(最多等NODE_TIMEOUT)。一旦一个主节点给这个从节点投票,会回复一个FAILOVER_AUTH_ACK,并且在NODE_TIMEOUT*2这段时间内不能再给同个主节点的其他从节点投票。在这段时间内不回复其他授权请求。从节点会忽视所有的时期(epoch)参数比currentEpoch小的回应(ACKs),这样泵避免把之前的投票算为当前投票。
一旦某个从节点收到了大多数主节点的回应,那么他就赢得了选举权。否则选举中断,在NODE_TIMEOUT*4后由另外的从节点发起选举。从节点并不是在主节点一进入FAIL状态就马上尝试发起选举,而是有一点点延迟,包含固定延迟和随机延迟。固定延迟确保我们会等到FAIL状态在集群内广播后,否则主节点会拒绝投票,还可以让从节点有时间获得新鲜数据。随机延迟是用来添加一些不确定因素以减少多个从节点在同一时间发起选举的可能性,因为若同时多个节点发起选举或许会导致没有任何节点赢得选举,要再次发起另一个选举的话会使集群在当时变得不可用。
一旦有从节点赢得选举,他就会开始用ping和pong数据包向其他节点宣布自己已经是主节点,并提升它负责的哈希槽,设置configEpoch为currentEpoch。为了加速其他节点的重新配置,该节点会广播一个pong包给集群里所有节点,其他节点检测到一个新的主节点负责处理旧的哈希槽,升级自己配置信息。旧主节点的从节点,或者是经过故障转移后重新加入的旧主节点不仅会升级配置信息,还会配置新主节点的备份。
主节点回复从节点的投票请求
主节点接收来自于从节点的投票请求,要授予投票,必须满足:
在一个给定的时段里(epoch),一个主节点只能投一次票,并且拒绝给以前时段投票:每个主节点都有一个lastVoteEpoch域,一旦认证请求数据包里的currentEpoch小于它就拒绝投票。当响应请求,则更新值。
一个主节点投票给某个从节点当且仅当该从节点的主节点被标记为FAIL。
如果认证请求里的currentEpoch小于主节点里的currentEpoch,忽视。
主节点若已经为某个失效主节点的一个从节点投票后,在经过NODE_TIMEOUT*2时间之前不会为同一个失效的主节点的另一个从节点投票。这并不是严格要求的,因为两个从节点用同个epoch来赢得选举的可能性很低,不过在实际中,系统确保正常情况当一个从节点被选举上,那么他有足够时间来通知其他节点,以避免另外一个从节点发起另一个新的选举。
主节点不会用任何方式来尝试选出最好的从节点,只要从节点的主节点处于FAIL状态并且投票主节点在这一轮中还没投票,则会积极投票。
主节点拒绝投票,则不会给任何负面回应,忽略即可。
主节点不会授予投票给那些configEpoch值比主节点哈希槽表里configEpoch更小的从节点。
键分布模型
键空间被分割为16384槽,事实上集群的最大节点数量是16384个。所有的主节点都负责16384个哈希槽中的一部分。当集群处于稳定状态时,集群没有在执行重配置操作,每个哈希槽都只由一个节点进行处理。以下是用来把键映射到哈希槽的算法:
HASH_SLOT = CRC16(key)mod 16384
CRC16能相当好的把不同的键均匀分配到16384个槽中。
在集群刚建立时,可以用工具自动分配槽,也可以自己手动分配槽。在集群运行过程中,也可以手动对槽的分布进行重新配置,若要保证数据不丢失,还要进行槽数据迁移后方可进行更换。
键哈希标签(keys hash tags)
计算哈希槽可以实现哈希标签,是确保两个键都在同一个哈希槽里的一种方式。将来也许在允许多键操作中使用的到。
为了实现哈希标签,使用另一种方式计算哈希槽:
如果键包含一个{字符,那么在{的右边就会有一个},在{和}之间会有一个或多个字符,第一个}一定是出现在第一个{之后,然后不是直接计算键的哈希,只有在第一个{和它右边第一个}之前的内容绘本用来计算哈希值。
MOVED重定向
一个Redis客户端可以自由的向集群中的任意节点发送查询,接收到的节点会分析查询,如果这个命令是集群可以执行的,那么节点会找这个键所属的哈希槽对应的节点。如果刚好这个节点就是对应这个哈希槽,那么这个查询就直接备节点处理。否则这个节点会查看它内部的哈希槽->节点ID映射,然后给客户端返回一个MOVED错误。在Redis集群中的节点并不是把命令转发到管理所给出的键值的正确节点上,而是把客户端重定向到服务一定范围内的键值的节点上,集群节点不能代理请求,所以客户端在接收到重定向错误-MOVED和-ASK的时候,将命令重定向到其他节点上。
集群在线重配置(live reconfiguration)
Redis集群支持在集群运行过程中添加(刚添加的节点是没有槽位的)或者删除(删除一组master-slave后,它所分配的槽就空缺了)节点。实际上,添加或移除节点都被抽象为同一个操作,那就是把哈希槽从一个节点移到另一个节点。
向集群中添加一个新的节点,就是把一个空节点加入到集群中并把某些哈希槽从已经存在的节点移到新节点上。
从集群中移除一个节点,就是把该节点上的哈希槽移到其他节点上。
所以实现这个的核心是把哈希槽移来移去。从实际角度看,哈希槽就是一堆键,所以Redis集群在重组碎片时做的就是把键从一个节点移到另一节点。
为了理解这是怎么工作的。我们需要介绍CLUSTER的子命令,这些子命令是用来操作Redis集群节点上的哈希槽转换表。
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节点指派或移除哈希槽。在哈希槽被指派后,节点会将这个消息通过gossip协议向整个集群传播。ADDSLOTS命令通常是用于在一个集群刚建立的时候快速给所有节点指派哈希槽。当SETSLOT子命令使用NODE形式的时候,用来给指定ID的节点指派哈希槽。除此之外哈希槽能通过两个特殊的状态来设定,MIGRATING和IMPORTING:
当一个槽被设置为MIGRATING,原来持有该哈希槽的节点仍会接收所有跟这个哈希槽有关的请求,但只有当查询的键还存在原节点时,原节点会处理请求,否则这个查询会通过一个-ASK重定向转发到迁移的目标节点。
当一个槽被设置为IMPORTING,只有在接收到ASKING命令之后节点才会接收所有查询这个哈希槽的请求。如果客户端一直没有发送ASKING命令,那么查询都会通过-MOVED重定向错误转发到真正处理这个哈希槽的节点那里。
在进行槽状态设置后,就可以先通过CLUSTER GETKEYSINSLOT slot count获得count个键,然后通过MIGRATE命令以原子方式把指定键从旧的节点移到新的节点(过程中,键在两个节点都会被锁住)。执行MIGRATE命令的节点会连接到目标节点,把序列化后的key发送过去,一旦收到OK回复后就会从它自己的数据集中删除老的key。
ASK重定向
在前面的章节中,我们简短的提到ASK重定向,为什么我们不能单纯地使用MOVED重定向呢?因为当我们使用MOVED的时候,意味着我们认为哈希槽永久的被另一个节点处理,并且希望接下来的所有查询都尝试发到这个指定节点上去。而ASK意味着我们只要下一个查询发到指定节点上去(一般客户端会记住槽点的分布情况)。这是必要的,因为下一个查询的键或许还在旧的节点上。然而我们需要强制客户端的行为,以确保客户端会在尝试A中查找后再查找B,如果客户端在发送查询前发送了ASKING命令,那么节点B只会接收被设为IMPORTING的槽的查询,也就是ASKING在客户端设置了一个一次性标识,强制一个节点可以执行一次关于带有IMPORTING状态的槽的查询。从客户端看来,ASK重定向完整语义:
如果接收到ASK重定向,那么把查询的对象调整为指定的节点。
先发送ASKING命令,再开始发送查询。
现在不要更新本地客户端映射表。
RedisCluster的在线重配置功能使得他支持动态的槽点迁移实现负载均衡。
RedisCluster处理流程
检查Key所在的Slot是否属于当前Node,1)计算crc16(key)%16384得到Slot。2)查询clusterState.slots负责的节点指针。3)与myself指针比较。
若不属于,则响应MOVED错误重定向客户端
若属于且Key存在,则直接操作,返回结果给客户端。
若Key不存在,检查该Slot是否迁出中?
若Slot迁出中,返回ASK错误重定向客户端到迁移的目的服务器上。
若Slot未迁出,检查Slot是否导入中?
若Slot导入中且有ASKING标记,则直接操作
否则响应MOVED错误重定向客户端。
总结
Redis本身支持主从复制功能,主要通过全量RDB文件复制和增量缓冲区命令同步复制数据,主从之间的误差小。
Sentinel可以实现主从集群的管理,master宕机,slave自动切换。实现sentinel集群,集群通过gossip协议来同步配置数据;涉及到节点的失效检测和从节点的提升,利用配置版本号来保证集群对节点提升的同步进行,并且更新配置信息。同时还利用了redis的发布/订阅功能来实现sentinel和slaves的自动发现和配置更新。
Twemproxy实现redis的分片代理功能,将不同的key映射到后端redis服务,将服务处理结果再返回给客户端,横向扩展redis的存储能力。支持动态摘除和添加redis节点,但是不能保证键与后端服务的映射关系保持不变,即会引起访问不到key的情况。而且twemproxy也不支持高可用性。
RedisCluster是利用额外的进程来实现集群节点之间的TCP连接来进行节点间数据交换,也是采用gossip协议进行同步配置数据;利用PFAIL和FAIL两个标识之间的弱协议很好地实现节点失效检测和从节点的提升功能;同样利用currentEpoch和configEpoch来保证集群动态配置的同步进行;它还利用槽将数据分散到不同的redis节点中,支持动态添加和删除节点,支持槽的动态配置与迁移,并且保证迁移的同时服务的可用。但是槽的动态配置功能需要人工参与,不能够实现数据在节点间的自动负载均衡。