最近工作中需要写一个一致性哈希的代理,在网上找到了twemproxy,结合网上资料先学习一下源码。
一、Twemproxy简介
Twemproxy是memcache与redis的代理,由twitter公司开发并且目前已经开源。研究这个对于理解网络通信有很大的帮助。
亮点有以下:
1、twemproxy自己创建并维护和后端server(即reids实例)的长连接,保证长连接对于来自不同client但去向同一server的复用。
2、自动识别异常状态的server,保证之后的请求不会被转发到该异常server上。但如果此server恢复正常,twemproxy又能识别此server,并恢复正常访问。
3、提供专门的状态监控端口供外部工具获取状态监控信息。
4、真正实现了多阶段处理多请求,其IO 模型值得学习,采用单线程收发包,基于epoll事件驱动模型。
5、支持Zero Copy技术,通过将消息指针在3个队列之间流转。
二、Twemproxy 入口main函数分析
Twemproxy是以c语言编写的,其整体入口在main函数中。
grep “main”,我们可以看到在文件nc.c中的main函数。开篇我们可以看到定义了一个变量,struct instance nci;
下边我们看下这个instance的定义:
struct instance {
struct context *ctx; /* active context */
int log_level; /* log level */
char *log_filename; /* log filename */
char *conf_filename; /* configuration filename */
uint16_t stats_port; /* stats monitoring port */
int stats_interval; /* stats aggregation interval */
char *stats_addr; /* stats monitoring addr */
char hostname[NC_MAXHOSTNAMELEN]; /* hostname */
size_t mbuf_chunk_size; /* mbuf chunk size */
pid_t pid; /* process id */
char *pid_filename; /* pid filename */
unsigned pidfile:; /* pid file created? */
};
再来看一下context的定义:
struct context {
uint32_t id; /* unique context id */
struct conf *cf; /* configuration */
struct stats *stats; /* stats */ struct array pool; /* server_pool[] */
struct event_base *evb; /* event base */
int max_timeout; /* max timeout in msec */
int timeout; /* timeout in msec */ uint32_t max_nfd; /* max # files */
uint32_t max_ncconn; /* max # client connections */
uint32_t max_nsconn; /* max # server connections */
};
这个instance就相当于是一个twemproxy实例,一个twemproxy进程对应一个instance,后边整个程序的初始化很多都会用到。这里面 一个instance对应于一个context,而一个context维护一个server pool列表(也就是说一个instance可以包含多个server pool)。
下面先把整个main函数的过程贴出来,加上了一些简要的注释
int
main(int argc, char **argv)
{
rstatus_t status;
struct instance nci; /*
* 赋初始值
*/
nc_set_default_options(&nci); /*
* 从命令行读入相应参数
*/
status = nc_get_options(argc, argv, &nci);
if (status != NC_OK) {
nc_show_usage();
exit();
} if (show_version) {
log_stderr("This is nutcracker-%s" CRLF, NC_VERSION_STRING);
if (show_help) {
nc_show_usage();
} if (describe_stats) {
stats_describe();
} exit();
} /*
* 测试配置文件合法性,不必关心
*/
if (test_conf) {
if (!nc_test_conf(&nci)) {
exit();
}
exit();
} /*
* 初始化log、守护进程、信号、pidfile
*/
status = nc_pre_run(&nci);
if (status != NC_OK) {
/*
* 失败就对上面的操作进行析构清理,主要就是删除pidfile、关闭log文件描述符
*/
nc_post_run(&nci);
exit();
} nc_run(&nci); nc_post_run(&nci); exit();
}
下面我们进到各个函数里面看一下吧。
1、函数nc_set_default_options
static void
nc_set_default_options(struct instance *nci)
{
int status; nci->ctx = NULL; nci->log_level = NC_LOG_DEFAULT;
nci->log_filename = NC_LOG_PATH; nci->conf_filename = NC_CONF_PATH; nci->stats_port = NC_STATS_PORT;
nci->stats_addr = NC_STATS_ADDR;
nci->stats_interval = NC_STATS_INTERVAL; status = nc_gethostname(nci->hostname, NC_MAXHOSTNAMELEN);
if (status < ) {
log_warn("gethostname failed, ignored: %s", strerror(errno));
nc_snprintf(nci->hostname, NC_MAXHOSTNAMELEN, "unknown");
}
nci->hostname[NC_MAXHOSTNAMELEN - ] = '\0'; nci->mbuf_chunk_size = NC_MBUF_SIZE; nci->pid = (pid_t)-;
nci->pid_filename = NULL;
nci->pidfile = ;
}
这个是用来设置上述instance实例nci的各项参数的默认值。
2、函数nc_get_options
static rstatus_t
nc_get_options(int argc, char **argv, struct instance *nci)
{
int c, value; opterr = ; for (;;) {
c = getopt_long(argc, argv, short_options, long_options, NULL);
if (c == -) {
/* no more options */
break;
} switch (c) {
case 'h':
show_version = ;
show_help = ;
break; case 'V':
show_version = ;
break; case 't':
test_conf = ;
break; case 'd':
daemonize = ;
break; case 'D':
describe_stats = ;
show_version = ;
break; case 'v':
value = nc_atoi(optarg, strlen(optarg));
if (value < ) {
log_stderr("nutcracker: option -v requires a number");
return NC_ERROR;
}
nci->log_level = value;
break; case 'o':
nci->log_filename = optarg;
break; case 'c':
nci->conf_filename = optarg;
break; case 's':
value = nc_atoi(optarg, strlen(optarg));
if (value < ) {
log_stderr("nutcracker: option -s requires a number");
return NC_ERROR;
}
if (!nc_valid_port(value)) {
log_stderr("nutcracker: option -s value %d is not a valid "
"port", value);
return NC_ERROR;
} nci->stats_port = (uint16_t)value;
break; case 'i':
value = nc_atoi(optarg, strlen(optarg));
if (value < ) {
log_stderr("nutcracker: option -i requires a number");
return NC_ERROR;
} nci->stats_interval = value;
break; case 'a':
nci->stats_addr = optarg;
break; case 'p':
nci->pid_filename = optarg;
break; case 'm':
value = nc_atoi(optarg, strlen(optarg));
if (value <= ) {
log_stderr("nutcracker: option -m requires a non-zero number");
return NC_ERROR;
} if (value < NC_MBUF_MIN_SIZE || value > NC_MBUF_MAX_SIZE) {
log_stderr("nutcracker: mbuf chunk size must be between %zu and"
" %zu bytes", NC_MBUF_MIN_SIZE, NC_MBUF_MAX_SIZE);
return NC_ERROR;
} nci->mbuf_chunk_size = (size_t)value;
break; case '?':
switch (optopt) {
case 'o':
case 'c':
case 'p':
log_stderr("nutcracker: option -%c requires a file name",
optopt);
break; case 'm':
case 'v':
case 's':
case 'i':
log_stderr("nutcracker: option -%c requires a number", optopt);
break; case 'a':
log_stderr("nutcracker: option -%c requires a string", optopt);
break; default:
log_stderr("nutcracker: invalid option -- '%c'", optopt);
break;
}
return NC_ERROR; default:
log_stderr("nutcracker: invalid option -- '%c'", optopt);
return NC_ERROR; }
} return NC_OK;
}
从命令行读入相应参数,如果有则覆盖上一个函数所设置的默认参数。
3、函数nc_test_conf
如果配置了-t选项 则nc_test_conf : 用于对配置文件做检查,以确保配置文件格式的正确。里面的解析用了一坨yaml的东西,不懂……不过我们也不用关心这个。
4、函数nc_pre_run
static rstatus_t
nc_pre_run(struct instance *nci)
{
rstatus_t status; status = log_init(nci->log_level, nci->log_filename);
if (status != NC_OK) {
return status;
} if (daemonize) {
status = nc_daemonize();
if (status != NC_OK) {
return status;
}
} nci->pid = getpid(); status = signal_init();
if (status != NC_OK) {
return status;
} if (nci->pid_filename) {
status = nc_create_pidfile(nci);
if (status != NC_OK) {
return status;
}
} nc_print_run(nci); return NC_OK;
}
做了这些事情:初始化log、守护进程、信号、pidfile;其中关于守护进程的实现非常经典,后面会单独写。
5、函数nc_run
static void
nc_run(struct instance *nci)
{
rstatus_t status;
struct context *ctx; ctx = core_start(nci);
if (ctx == NULL) {
return;
} /* run rabbit run */
for (;;) {
status = core_loop(ctx);
if (status != NC_OK) {
break;
}
} core_stop(ctx);
}
nc_run开始启动proxy;这个函数完成的工作就是调用core_start创建context,然后进入死循环调用core_loop开始整个事件循环的处理,接受请求并处理。当然,core_start以及core_loop这两个函数里边还包含了大量的处理工作,包括:配置文件解析以及读取,相关组件(server_pool,conf,context)的初始化等,这个后面再单独写。
总体来说,启动流程就是这些步骤,如下图:
本文参考自:http://blog.sina.cn/dpool/blog/s/blog_4f8ea2ef0101iill.html?md=gd