Twemproxy(nutcracker)容易误解的参数

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
# coding: utf-8

import redis
import time

if __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自己发一个心跳包到失败节点做测试,这样起码不会产生数据丢失的问题。