架构图 机器说明 Java代码
10.75.201.67:keepalived + twemproxy10.75.201.66:keepalived + twemproxy初始化时,VIP绑定在10.75.201.6710.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./blog/2014/05/24/redis-cluster/) 一个更完美的方案是: https://blog./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代码
vrrp_script chk_nutcraker { script ' 两台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: 1timeout: 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: 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 则客户端取不到之前写入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这样的参数 部署说明就到这里 |
|