架构图 

keepalived+twemproxy部署redis集群高可用_Java

机器说明  


Java代码   收藏代码

10.75.201.67:keepalived + twemproxy  

10.75.201.66:keepalived + twemproxy  

初始化时,VIP绑定在10.75.201.67  

10.75.201.26:ClusterA(redis master A + redis slave A1)  

10.75.201.68:ClusterB(redis master B + redis slave B1)  



如果机器充足的话,redis master A与redis slave A1部署在两台机器上(redis master B + redis slave B1也一样)  

为实验方便,目前redis master与redis slave是在同一机器上,通过不同的端口来启动  


安装目录  

Java代码   收藏代码

/home/redis:  

|-- nutcracker  

|   |-- conf  

|   |-- sbin  

|-- redis  

|   |-- bin  

|   |-- conf  

|   `-- logs  


版本  

redis-2.8.19  

nutcracker-0.4.0  

keepalived-1.2.12  


各框架作用:  

1.keepalived提供VIP漂移,避免twemproxy的单点故障  

2. twemproxy作为redis代理,可以提供一致性哈希;当它代理的某个Cluster挂掉了,它会把该Cluster移除,并把原本属于该Cluster的读写请求按哈希算法重新分派给另外的Cluster  

3.ClusterA,ClusterB,ClusterC各有一主一从。可以横向扩展,增加ClusterD、ClusterE等  


说明:  

上述方案有个瑕疵:  

当ClusterX中的redis master挂掉后,整个ClusterX就被twemproxy移除了(即使redis slave还正常)。可以通过keepalived或sentinel来使得slave可以在master挂掉时升级为master并绑定VIP。但这样意义不大,配置相对复杂(使用sentinel的例子见http://blog.youyo.info/blog/2014/05/24/redis-cluster/)  

一个更完美的方案是:  

https://blog.recurly.com/2014/05/clustering-redis-maximize-uptime-scale  

使用了keepalived+twemproxy+smitty+sentinel  

sentinel可以使得redis slave升级为master,而smitty可以监测到该变化并更新twemproxy的配置文件  

但到smitty的github上看,smitty还不能应用到生产环境:  

keepalived+twemproxy部署redis集群高可用_客户端_02




配置  


keepalived+twemproxy  


vim /etc/keepalived/keepalived.conf  

Java代码   收藏代码

vrrp_script chk_nutcraker {  

                script "</dev/tcp/127.0.0.1/63790" #监测nutcraker是否正常  

                interval 2  

}  

vrrp_instance VI_2 {  

        state BACKUP        #both BACKUP  

        interface eth1  

        virtual_router_id 12  

        priority 101    #101 on master, 100 on backup  

        nopreempt       #both nopreempt  

        track_script {  

                chk_nutcraker  

        }  

        virtual_ipaddress {  

             10.75.201.3  

        }  

}

两台keepalived都配置为BACKUP + nopreempt,表示不抢占,避免VIP不必要的漂移;为了使得初始时VIP绑定在10.75.201.67上,配置10.75.201.67的优先级为101,10.75.201.66为100  


vim  /home/redis/nutcracker/conf/nutcracker.yml  

Java代码   收藏代码

nutcrakerB:  

  listen: 0.0.0.0:63790    #nutcraker在端口63790启动。keepalived应该监控该端口  

hash: one_at_a_time  

  hash_tag: "{}"  

  distribution: modula  

  auto_eject_hosts: true  

  redis: true  

  server_retry_timeout: 2000  

  server_failure_limit: 1  

timeout: 400  

  servers:  

   - 10.75.201.26:6379:1    #这里只需要写Cluster中redis master的IP和端口  

   - 10.75.201.68:6379:1    #同上

说明:  

hash: one_at_a_time  

hash_tag: "{}"  

distribution: modula  

这三行配置在测试时可采用,可以准确地知道数据将会保存在哪台机器:  

distribution: modula表示根据key值的hash值取模,根据取模的结果选择对应的服务器  

hash_tag: "{}"表示计算hash值时,只取key中包含在{}里面的那部分来计算  

one_at_a_time计算hash值的,java版本的实现:  

Java代码   收藏代码

private static int oneAtATime (String k) {  

        int hash = 0;  

        try {  

            for (byte bt : k.getBytes("utf-8")) {  

                hash += (bt & 0xFF);  

                hash += (hash << 10);  

                hash ^= (hash >>> 6);  

            }  

            hash += (hash << 3);  

            hash ^= (hash >>> 11);  

            hash += (hash << 15);  

        } catch (Exception e) {  

            e.printStackTrace();  

        }  

        return hash;  

    }


测试可得:  

oneAtATime("a") % 2得到0  

oneAtATime("b") % 2得到1  

因此,zzz{a}xxx=yyy这样的键值对会保存在10.75.201.26,而xxx{b}yyy=zzz则保存在10.75.201.68  


生产环境可采用:  

hash: fnv1a_64  

distribution: ketama  



Redis Cluster  


目录结构:  

Java代码   收藏代码

|--/home/redis/redis  

   |--bin  

   |--6379  

      |--redis.conf  

      |--redis.pid  

   |--63791  

      |--redis.conf  

      |--redis.pid


6379为redis master,63791为redis slave  

需要修改redis.conf中对应的配置:  


vim /home/redis/redis/6379/redis.conf  

daemonize yes  

pidfile /home/redis/redis/6379/redis.pid  

port 6379  


在63791/redis.conf中还要配置:  

slaveof 127.0.0.1 6379  


启动  


1.启动redis  

在10.75.201.26和10.75.201.68上启动:  

redis-server /home/redis/redis/6379/redis.conf  

redis-server /home/redis/redis/63791/redis.conf  

2.启动twemproxy+keepalived  

先启动10.75.201.67:  

nutcracker -d -c /home/redis/nutcracker/conf/nutcracker.yml  

service keepalived start  

再启动10.75.201.66,重复上述操作  


测试验证  


1.正常情况下  

查看10.75.201.26的redis,6379为master,63791为slave  

查看10.75.201.68的redis,6379为master,63791为slave  


客户端连接并写入:  


redis-cli -h 10.75.201.3 -p 63790  

10.75.201.3:63790> set {a}1 a1  

10.75.201.3:63790> set {b}1 b1  

则{a}1=a1写到10.75.201.26,{b}1=b1写入10.75.201.68  


在10.75.201.26上(:6379以及:63791):  

get {a}1得到a1,get {b}1得到nil  


在10.75.201.68上(:6379以及:63791)  

get {a}1得到nil,get {b}1得到b1  


2.把10.75.201.67上的twemproxy或keepalived进程kill掉  

则VIP转移到10.75.201.66:  

在10.75.201.66上执行ip add | grep eth1,输出:  

eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UNKNOWN qlen 1000  

   inet 10.75.201.66/24 brd 10.75.201.255 scope global eth1  

   inet 10.75.201.3/32 scope global eth1  

此时客户端仍可连接redis-cli -h 10.75.201.3 -p 63790并进行读写,与正常情况下没什么区别  


3.把10.75.201.26的redis master进程kill掉:  

lsof -i:6379  
kill -9 <pid>

则客户端取不到之前写入ClusterA的数据了:  

10.75.201.3:63790> get {a}1  
(nil)

但ClusterA上的数据还在ClusterA-redis-slave上:  

10.75.201.26:63791> get {a}1  
"a1"


注意客户端有可能:  

10.75.201.3:63790> get {a}1  
(error) ERR Connection refused  
10.75.201.3:63790> get {a}1  
(nil)

第一次表明没有连接上,第二次表明连接上了但查询不到数据  

这时需要注意客户端的重连和失败次数设置,官方文档说:  


To ensure that requests always succeed in the face of server ejections (auto_eject_hosts: is enabled), some form of retry must be implemented at the client layer since nutcracker itself does not retry a request. This client-side retry count must be greater than server_failure_limit: value, which ensures that the original request has a chance to make it to a live server.  


因此代码里可以这样写:  

Java代码   收藏代码

int retryTimes = 2;  

boolean done = false;  

while (!done && retryTimes > 0) {  

    try {  

              bean.getRedisTemplate().opsForHash().put("{a}4", "a4".hashCode(),"a4");  

              done = true;  

          } catch (Exception e) {  

              e.printStackTrace();  

          } finally {  

              retryTimes--;  

          }  

}

代码略显丑陋,不知为什么RedisTemplate没有类似retryTimes这样的参数