Twitter,世界最大的Redis集群之一部署在Twitter用于为用户提供时间轴数据。Twitter Open Source部门提供了Twemproxy(nutracker)。
Twemproxy是一个快速的单线程代理程序,支持Memcached ASCII协议和更新的Redis协议: 它全部用C写成,使用Apache 2.0 License授权。项目在Linux上可以工作,而在OSX上无法编译,因为它依赖了epoll API. Twemproxy 通过引入一个代理层,可以将其后端的多台 Redis 或 Memcached 实例进行统一管理与分配,使应用程序只需要在 Twemproxy 上进行操作,而不用关心后面具体有多少个真实的 Redis 或 Memcached 存储。
由于数据量大,以及考虑到redis的集群和高可用等需求,在项目中采用nutcracker作为redis和memcache的前端proxy。nutcracker的配置文件相当简洁,只有10来个参数。下面是一个典型的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 redis-for-test: listen: 0.0.0.0:11212 #表示监听的IP和端口 redis: true #true表示作为redis代理,false表示作为memcache代理 hash: fnv1a_64 #指定具体的hash函数 distribution: ketama #具体的hash算法 timeout: 400 #超时时间(毫秒) auto_eject_hosts: true #是否在结点无法响应的时候临时摘除结点 server_retry_timeout: 30000 #重试的时间(毫秒) server_failure_limit: 2 #结点故障多少次就算摘除掉 preconnect: true #在进程启动的时候,是否需要预连接到所有的server,默认值false servers: #下面表示所有的Redis节点(IP:端口号:权重) - 127.0.0.1:6379:1 test-1 - 127.0.0.1:6380:1 test-2
巧合的事情发生了,前端时间由于机房故障,后端的redis节点中有两台机器挂掉了。理论上这时nutcracker应该会发挥左右。然而实际情况是报警报疯了。难道nutcracker的高可用是随便骗骗人的?应该不至于吧。接下来我在测试机上做了如下测试:
开两个redis节点,端口分别为6379和6380。开一个nutcracker,配置如上。然后写一个python脚本来做数据写入,脚本代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import redisimport timeif __name__ == '__main__' : r = redis.Redis(host='10.240.129.196' , port=22221 ) for i in range (0 , 10000000 ): try : if r.set ('key' + str (i), 'value' + str (i)): print 'set key:' , i, 'success' else : print '*** set key:' , i, 'failed ***' time.sleep(0.5 ) except Exception as e: print 'except:' , e.message
每隔0.5s写入一个KV。同时监控nutcracker日志(启动nutcracker时可以用-o来指定日志文件)。正常运行一会后,kill掉6379的redis节点,这是会发现脚本返回Connection refused错误,接着正常写入,过了会又会出现Connection refused,每隔一会就会出现两次。nutcracker的日志如下:
1 2 3 4 5 6 7 8 9 10 11 12 [Thu Aug 27 16:57:31 2015] nc_core.c:207 close s 7 '127.0.0.1:6379' on event 001D eof 0 done 0 rb 0 sb 0: Connection refused [Thu Aug 27 16:57:31 2015] nc_core.c:207 close s 7 '127.0.0.1:6379' on event 001D eof 0 done 0 rb 0 sb 0: Connection refused [Thu Aug 27 16:58:01 2015] nc_core.c:207 close s 7 '127.0.0.1:6379' on event 001D eof 0 done 0 rb 0 sb 0: Connection refused [Thu Aug 27 16:58:01 2015] nc_core.c:207 close s 7 '127.0.0.1:6379' on event 001D eof 0 done 0 rb 0 sb 0: Connection refused [Thu Aug 27 16:58:32 2015] nc_core.c:207 close s 7 '127.0.0.1:6379' on event 001D eof 0 done 0 rb 0 sb 0: Connection refused [Thu Aug 27 16:58:32 2015] nc_core.c:207 close s 7 '127.0.0.1:6379' on event 001D eof 0 done 0 rb 0 sb 0: Connection refused [Thu Aug 27 16:59:02 2015] nc_core.c:207 close s 7 '127.0.0.1:6379' on event 001D eof 0 done 0 rb 0 sb 0: Connection refused [Thu Aug 27 16:59:02 2015] nc_core.c:207 close s 7 '127.0.0.1:6379' on event 001D eof 0 done 0 rb 0 sb 0: Connection refused [Thu Aug 27 16:59:32 2015] nc_core.c:207 close s 7 '127.0.0.1:6379' on event 001D eof 0 done 0 rb 0 sb 0: Connection refused [Thu Aug 27 16:59:32 2015] nc_core.c:207 close s 7 '127.0.0.1:6379' on event 001D eof 0 done 0 rb 0 sb 0: Connection refused [Thu Aug 27 17:00:02 2015] nc_core.c:207 close s 7 '127.0.0.1:6379' on event 001D eof 0 done 0 rb 0 sb 0: Connection refused [Thu Aug 27 17:00:02 2015] nc_core.c:207 close s 7 '127.0.0.1:6379' on event 001D eof 0 done 0 rb 0 sb 0: Connection refused
仔细观察发现,每大约间隔30s就会出现两次链接失败。对比我们nutcracker的配置,是不是有点明白了?原来是对nutcracker的配置有误解。上面的参数中server_retry_timeout和server_failure_limit是指“每次”失败后的下一次重试时间和重试次数。也就是说,当后端有一个redis节点挂了,nutcracker马上发现以后,回去重试。重试两次(server_failure_limit)失败以后就等30秒(server_retry_timeout)再重试,这个过程一直维持。而每次重试都是用真实得数据包发过去,失败后,数据也就丢失了。所以这个配置下每个失败的节点每隔30s就会有两次数据丢失。
nutcracker的这个做法是为了当挂掉的节点又重新爬起来时能恢复,但是不太明白为何它要用真实的数据去做重连尝试,而不是由nutcracker自己发一个心跳包到失败节点做测试,这样起码不会产生数据丢失的问题。