keepalived+twemproxy部署redis集群高可用

来源:互联网 发布:苏州seo外包 编辑:程序博客网 时间:2024/05/16 01:29

转载:http://bylijinnan.iteye.com/blog/2175191

架构图 





机器说明 

Java代码  收藏代码
  1. 10.75.201.67:keepalived + twemproxy  
  2. 10.75.201.66:keepalived + twemproxy  
  3. 初始化时,VIP绑定在10.75.201.67  
  4. 10.75.201.26:ClusterA(redis master A + redis slave A1)  
  5. 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代码  收藏代码
  1. /home/redis:  
  2. |-- nutcracker  
  3. |   |-- conf  
  4. |   |-- sbin  
  5. |-- redis  
  6. |   |-- bin  
  7. |   |-- conf  
  8. |   `-- 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 

vim /etc/keepalived/keepalived.conf 
Java代码  收藏代码
  1. vrrp_script chk_nutcraker {  
  2.                 script "</dev/tcp/127.0.0.1/63790" #监测nutcraker是否正常  
  3.                 interval 2  
  4. }  
  5. vrrp_instance VI_2 {  
  6.         state BACKUP        #both BACKUP  
  7.         interface eth1  
  8.         virtual_router_id 12  
  9.         priority 101    #101 on master, 100 on backup  
  10.         nopreempt       #both nopreempt  
  11.         track_script {  
  12.                 chk_nutcraker  
  13.         }  
  14.         virtual_ipaddress {  
  15.              10.75.201.3  
  16.         }  
  17. }  

两台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代码  收藏代码
  1. nutcrakerB:  
  2.   listen: 0.0.0.0:63790    #nutcraker在端口63790启动。keepalived应该监控该端口  
  3. hash: one_at_a_time  
  4.   hash_tag: "{}"  
  5.   distribution: modula  
  6.   auto_eject_hosts: true  
  7.   redis: true  
  8.   server_retry_timeout: 2000  
  9.   server_failure_limit: 1  
  10. timeout: 400  
  11.   servers:  
  12.    - 10.75.201.26:6379:1    #这里只需要写Cluster中redis master的IP和端口  
  13.    - 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代码  收藏代码
  1. private static int oneAtATime (String k) {  
  2.         int hash = 0;  
  3.         try {  
  4.             for (byte bt : k.getBytes("utf-8")) {  
  5.                 hash += (bt & 0xFF);  
  6.                 hash += (hash << 10);  
  7.                 hash ^= (hash >>> 6);  
  8.             }  
  9.             hash += (hash << 3);  
  10.             hash ^= (hash >>> 11);  
  11.             hash += (hash << 15);  
  12.         } catch (Exception e) {  
  13.             e.printStackTrace();  
  14.         }  
  15.         return hash;  
  16.     }  

测试可得: 
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代码  收藏代码
  1. |--/home/redis/redis  
  2.    |--bin  
  3.    |--6379  
  4.       |--redis.conf  
  5.       |--redis.pid  
  6.    |--63791  
  7.       |--redis.conf  
  8.       |--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代码  收藏代码
  1. int retryTimes = 2;  
  2. boolean done = false;  
  3. while (!done && retryTimes > 0) {  
  4.     try {  
  5.               bean.getRedisTemplate().opsForHash().put("{a}4""a4".hashCode(),"a4");  
  6.               done = true;  
  7.           } catch (Exception e) {  
  8.               e.printStackTrace();  
  9.           } finally {  
  10.               retryTimes--;  
  11.           }  
  12. }  

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

0 0
原创粉丝点击