概述:
上一节我们了解了Twemproxy主程序的启动过程,但没有深入细节。作为一个proxy服务,我们应该深入了解它的进程和事件模型。本章我们从nc_run()开始深入了解。
进程模型:
在看代码之前,我们得先大体了解Twemproxy使用的是单进程、多进程还是多线程的模型。通过grep代码,查找fork()和pthread_create()函数的调用,发现Twemproxy使用的是单进程单线程来处理请求,只是另外起了一个线程来处理统计数据。
| songqi@ubuntu:~/software/twemproxy/src$ grep fork * -r nc.c: pid = fork(); nc.c: log_error("fork() failed: %s", strerror(errno)); nc.c: pid = fork(); nc.c: log_error("fork() failed: %s", strerror(errno)); songqi@ubuntu:~/software/twemproxy/src$ grep pthread_create * -r nc_stats.c: status = pthread_create(&st->tid, NULL, stats_loop, st); |
这么看就简单多了,既然是单进程单线程,我们就不用关心进程管理进程间通信上面的问题了,可以把重点放在事件驱动模型上来。
事件模型:
了解libevent的同学可能都知道,利用libevent做事件驱动模型编程,基本步骤是:
- 调用event_init()创建event_base
- 创建event,调用event_set()设置文件描述符、文件描述符上需要监听的事件以及事件的回调函数
- 调用event_add()将创建的event加入event_base
- 调用event_dispatch()进入事件循环
Twemproxy没有使用libevent,而是自己封装了一个简单的事件驱动模型,步骤其实都差不多,相关代码在event目录下。
我们可以先看一下event/nc_event.h:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
ifndef
_NC_EVENT_H_
#define _NC_EVENT_H_
#include <nc_core.h>
#define EVENT_SIZE 1024
#define EVENT_READ 0x0000ff
#define EVENT_WRITE 0x00ff00
#define EVENT_ERR 0xff0000
typedef
int
(*event_cb_t)(void
*,
uint32_t);
typedef
void
(*event_stats_cb_t)(void
*,
void
*);
|
文件开头定义了几个宏,然后定义了两个回调函数指针,分别是event的回调函数event_cb_t和状态统计的回调函数event_stats_cb_t。紧接下来是一个条件宏定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #ifdef NC_HAVE_KQUEUE struct event_base { …… }; #elif NC_HAVE_EPOLL struct event_base { …… }; #elif NC_HAVE_EVENT_PORTS struct event_base { …… }; #else # error missing scalable I/O event notification mechanism #endif |
可以看到,Twemproxy针对不同的平台定义了不同的event_base结构(bsd下是kqueue,linux下是epoll,evport是什么我也没见过了),因为我们用的是Linux,所以只关心epoll下的代码就行了:
|
#elif NC_HAVE_EPOLL
struct
event_base
{
int ep; /*
epoll descriptor */
struct
epoll_event
*event; /*
event[] - events that were triggered */
int nevent; /*
# event */
event_cb_t
cb; /*
event callback */
};
|
epoll下的event_base定义很简单:一个epoll句柄ep,一个指向epoll_event数组的指针*event,一个event计数nevent,一个callback函数。
那么需要实现哪些函数呢?在event/nc_event.h的最底下有声明:
| struct event_base *event_base_create(int size, event_cb_t cb); void event_base_destroy(struct event_base *evb); int event_add_in(struct event_base *evb, struct conn *c); int event_del_in(struct event_base *evb, struct conn *c); int event_add_out(struct event_base *evb, struct conn *c); int event_del_out(struct event_base *evb, struct conn *c); int event_add_conn(struct event_base *evb, struct conn *c); int event_del_conn(struct event_base *evb, struct conn *c); int event_wait(struct event_base *evb, int timeout); void event_loop_stats(event_stats_cb_t cb, void *arg); |
创建:
我们先看看如何创建event_base,下面是event/nc_epoll.c当中的event_base_create()函数定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
struct
event_base
*
event_base_create(int
nevent,
event_cb_t
cb)
{
struct
event_base
*evb;
int
status,
ep;
struct
epoll_event
*event;
ASSERT(nevent
>
0);
ep
=
epoll_create(nevent);
if
(ep
<
0)
{
log_error("epoll
create of size %d failed: %s",
nevent,
strerror(errno));
return
NULL;
}
event
=
nc_calloc(nevent,
sizeof(*event));
if
(event
==
NULL)
{
status
=
close(ep);
if
(status
<
0)
{
log_error("close
e %d failed, ignored: %s",
ep,
strerror(errno));
}
return
NULL;
}
evb
=
nc_alloc(sizeof(*evb));
if
(evb
==
NULL)
{
nc_free(event);
status
=
close(ep);
if
(status
<
0)
{
log_error("close
e %d failed, ignored: %s",
ep,
strerror(errno));
}
return
NULL;
}
evb->ep
=
ep;
evb->event
=
event;
evb->nevent
=
nevent;
evb->cb
=
cb;
log_debug(LOG_INFO,
"e %d with nevent %d",
evb->ep,
evb->nevent);
return
evb;
}
|
创建event_base的过程,就是初始化前面定义的event_base结构体的内容:
- 调用epoll_create()创建epoll句柄
- 申请nevent * sizeof(struct epoll_event)大小的空间,存放epoll_event指针数组
- 申请sizeof(struct event_base),存放event_base
- 对event_base中的ep、event、nevent、cb进行赋值。
调用event_base_create的地点在nc_core.c当中:
| /* initialize event handling for client, proxy and server */ ctx->evb = event_base_create(EVENT_SIZE, &core_core); |
其中EVENT_SIZE刚才nc_event.h的代码里有定义(1024),而core_core函数则在nc_core.c当中定义。调用event_base_create()之后,我们便得到了一个可以容纳1024个event的event_base。
添加event:
创建完event_base之后,便要向event_base中添加event,需要用到event_add_*函数,我们先来看看event_add_conn()函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
int
event_add_conn(struct
event_base
*evb,
struct
conn
*c)
{
int
status;
struct
epoll_event
event;
int
ep
=
evb->ep;
ASSERT(ep
>
0);
ASSERT(c
!=
NULL);
ASSERT(c->sd
>
0);
event.events
=
(uint32_t)(EPOLLIN
|
EPOLLOUT
|
EPOLLET);
event.data.ptr
=
c;
status
=
epoll_ctl(ep,
EPOLL_CTL_ADD,
c->sd,
&event);
if
(status
<
0)
{
log_error("epoll
ctl on e %d sd %d failed: %s",
ep,
c->sd,
strerror(errno));
}
else
{
c->send_active
=
1;
c->recv_active
=
1;
}
return
status;
}
|
conn的结构非常复杂,我们这里不去细看,因为这个函数只用到了conn->sd、conn->send_active和conn->recv_active,其中sd是要注册的文件描述符,而send_active和read_active只是布尔型的标记位,用来标记当前连接是否开启了读/写。
初始化event的时候,加入了EPOLLIN和EPOLLOUT事件,并指定了边缘触发EPOLLET模式,即当文件描述符的可读/可写状态切换的时候返回,成功后设置conn的send_active和read_active为1。
event_add_in()和event_add_out()其实是修改event的,具体分别将其修改为只读或者读写,同时修改send_active或者read_active。
那么什么时候添加event呢,一般是在创建连接之后,我们检索一下event_add_conn的调用位置:
| songqi@ubuntu:~/software/twemproxy/src$ grep event_add_conn * -r event/nc_epoll.c:event_add_conn(struct event_base *evb, struct conn *c) event/nc_kqueue.c:event_add_conn(struct event_base *evb, struct conn *c) event/nc_event.h:int event_add_conn(struct event_base *evb, struct conn *c); event/nc_evport.c:event_add_conn(struct event_base *evb, struct conn *c) nc_proxy.c: status = event_add_conn(ctx->evb, p); nc_proxy.c: status = event_add_conn(ctx->evb, c); nc_server.c: status = event_add_conn(ctx->evb, conn); |
其中nc_server.c中的调用是在Twemproxy创建了与后端Memcached Server的链接之后,而nc_proxy.c中的调用则分别是在创建listen端口之后,以及accept客户端的链接之后,这符合我们的预期。接下来我们分别来看看这3处。
监听端口:
针对每一个server pool,Twemproxy会创建一个proxy,每个proxy监听一个指定的端口,监听后即需要像event_base添加事件,相关代码在nc_proxy.c的proxy_listen()函数中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
static
rstatus_t
proxy_listen(struct
context
*ctx,
struct
conn
*p)
{
……
status
=
event_add_conn(ctx->evb,
p);
if
(status
<
0)
{
log_error("event
add conn p %d on addr '%.*s' failed: %s",
p->sd,
pool->addrstr.len,
pool->addrstr.data,
strerror(errno));
return
NC_ERROR;
}
status
=
event_del_out(ctx->evb,
p);
if
(status
<
0)
{
log_error("event
del out p %d on addr '%.*s' failed: %s",
p->sd,
pool->addrstr.len,
pool->addrstr.data,
strerror(errno));
return
NC_ERROR;
}
return
NC_OK;
}
|
代码已经省略掉了不相关的部分,可以看出,proxy_listen()函数先调用event_add_conn添加一个事件,然后将其修改为只读(等待客户端连接)。
等待连接:
accept的代码在nc_proxy.c的proxy_accept()函数中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | static rstatus_t proxy_accept(struct context *ctx, struct conn *p) { …… status = event_add_conn(ctx->evb, c); if (status < 0) { log_error("event add conn from p %d failed: %s", p->sd, strerror(errno)); c->close(ctx, c); return status; } log_debug(LOG_NOTICE, "accepted c %d on p %d from '%s'", c->sd, p->sd, nc_unresolve_peer_desc(c->sd)); return NC_OK; } |
同样省略了不相关的部分,可以看出在accept到客户端的文件描述符后,也是通过event_add_conn添加事件,不过这时没有调用event_del_out(),因为我们是需要向客户端写回数据的。
连接Memcached:
连接后端Memcached的代码在nc_server.c当中的server_connect()函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
rstatus_t
server_connect(struct
context *ctx,
struct
server *server,
struct
conn *conn)
{
……
status
=
event_add_conn(ctx->evb,
conn);
if
(status
!=
NC_OK)
{
log_error("event
add conn s %d for server '%.*s' failed: %s",
conn->sd,
server->pname.len,
server->pname.data,
strerror(errno));
goto
error;
}
……
}
|
初始化连接之后,调用event_add_conn()来添加事件,然后连接Memcached,因为连接需要读写,所以没有调用event_del_out()。
从上面的分析看,添加事件的过程基本上是一致的。
事件循环:
完成了event_base的创建和event的添加之后,就进入事件循环,前一节我们讲到时间循环是在core_loop()中做的:
| rstatus_t core_loop(struct context *ctx) { int nsd; nsd = event_wait(ctx->evb, ctx->timeout); if (nsd < 0) { return nsd; } core_timeout(ctx); stats_swap(ctx->stats); return NC_OK; } |
core_loop()调用了event_wait()函数做事件循环,event_wait()的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
int
event_wait(struct
event_base *evb,
int
timeout)
{
int
ep
=
evb->ep;
struct
epoll_event *event
=
evb->event;
int
nevent
=
evb->nevent;
ASSERT(ep
>
0);
ASSERT(event
!=
NULL);
ASSERT(nevent
>
0);
for
(;;)
{
int
i,
nsd;
nsd
=
epoll_wait(ep,
event,
nevent,
timeout);
if
(nsd
>
0)
{
for
(i
=
0;
i
<
nsd;
i++)
{
struct
epoll_event *ev
=
&evb->event[i];
uint32_t
events
=
0;
log_debug(LOG_VVERB,
"epoll %04"PRIX32"
triggered on conn %p",
ev->events,
ev->data.ptr);
if
(ev->events
&
EPOLLERR)
{
events
|=
EVENT_ERR;
}
if
(ev->events
&
(EPOLLIN
|
EPOLLHUP))
{
events
|=
EVENT_READ;
}
if
(ev->events
&
EPOLLOUT)
{
events
|=
EVENT_WRITE;
}
if
(evb->cb
!=
NULL)
{
evb->cb(ev->data.ptr,
events);
}
}
return
nsd;
}
if
(nsd
==
0)
{
if
(timeout
==
-1)
{
log_error("epoll
wait on e %d with %d events and %d timeout "
"returned no events",
ep,
nevent,
timeout);
return
-1;
}
return
0;
}
if
(errno
==
EINTR)
{
continue;
}
log_error("epoll
wait on e %d with %d events failed: %s",
ep,
nevent,
strerror(errno));
return
-1;
}
NOT_REACHED();
}
|
epoll_wait()返回当前I/O就绪的句柄数nsd,同时将epoll_event放入*event指向的数组中,随后就可以根据nsd和*event去依次访问每一个句柄,调用event_base的cb函数进行处理。
事件触发:
前面我们分析event_base_create()的时候了解到,event_base_create()这个函数仅在nc_core.c当中调用,也就是说全局只有一个event_base,而这个event_base上所有的事件触发的回调函数event_base->cb都是指向调用event_base_create()函数时所设置的core_core()。那么问题来了,如何区分不同的连接(后端Memcached连接句柄,客户端连接句柄以及proxy的监听端口句柄)呢?
我们需要具体看一下core_core()这个函数的代码,这个函数非常短:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | rstatus_t core_core(void *arg, uint32_t events) { rstatus_t status; struct conn *conn = arg; struct context *ctx = conn_to_ctx(conn); log_debug(LOG_VVERB, "event %04"PRIX32" on %c %d", events, conn->client ? 'c' : (conn->proxy ? 'p' : 's'), conn->sd); conn->events = events; /* error takes precedence over read | write */ if (events & EVENT_ERR) { core_error(ctx, conn); return NC_ERROR; } /* read takes precedence over write */ if (events & EVENT_READ) { status = core_recv(ctx, conn); if (status != NC_OK || conn->done || conn->err) { core_close(ctx, conn); return NC_ERROR; } } if (events & EVENT_WRITE) { status = core_send(ctx, conn); if (status != NC_OK || conn->done || conn->err) { core_close(ctx, conn); return NC_ERROR; } } return NC_OK; } |
可以看出,针对不同的事件:EVENT_READ、EVENT_WRITE,core_core()函数只是分别去调用了core_recv()和core_send()函数,而EVENT_ERR事件则报错。
那么core_recv()和core_send()函数做了什么呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
static
rstatus_t
core_recv(struct
context
*ctx,
struct
conn
*conn)
{
rstatus_t
status;
status
=
conn->recv(ctx,
conn);
if
(status
!=
NC_OK)
{
log_debug(LOG_INFO,
"recv on %c %d failed: %s",
conn->client
?
'c'
:
(conn->proxy
?
'p'
:
's'),
conn->sd,
strerror(errno));
}
return
status;
}
static
rstatus_t
core_send(struct
context
*ctx,
struct
conn
*conn)
{
rstatus_t
status;
status
=
conn->send(ctx,
conn);
if
(status
!=
NC_OK)
{
log_debug(LOG_INFO,
"send on %c %d failed: %s",
conn->client
?
'c'
:
(conn->proxy
?
'p'
:
's'),
conn->sd,
strerror(errno));
}
return
status;
}
|
可以看出他们只是分别调用了conn->recv()和conn->send(),我们看一下conn这个结构体相关部分的定义:
| struct conn { …… conn_recv_t recv; /* recv (read) handler */ conn_recv_next_t recv_next; /* recv next message handler */ conn_recv_done_t recv_done; /* read done handler */ conn_send_t send; /* send (write) handler */ conn_send_next_t send_next; /* write next message handler */ conn_send_done_t send_done; /* write done handler */ conn_close_t close; /* close handler */ conn_active_t active; /* active? handler */ …… } |
省略掉了无关的部分,可以看到conn包含了若干handler的函数指针,这其中就包含recv和send。那么这些字段是在创建conn的时候初始化的。针对不同的conn,为他们初始化不同的handler即可。
那么,首先我们看看listen的过程,调用proxy_listen()函数的位置是在nc_proxy.c的proxy_each_init()当中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
rstatus_t
proxy_each_init(void
*elem,
void
*data)
{
rstatus_t
status;
struct
server_pool
*pool
=
elem;
struct
conn
*p;
p
=
conn_get_proxy(pool);
if
(p
==
NULL)
{
return
NC_ENOMEM;
}
status
=
proxy_listen(pool->ctx,
p);
if
(status
!=
NC_OK)
{
p->close(pool->ctx,
p);
return
status;
}
log_debug(LOG_NOTICE,
"p %d listening on '%.*s' in %s pool %"PRIu32"
'%.*s'"
"
with %"PRIu32"
servers",
p->sd,
pool->addrstr.len,
pool->addrstr.data,
pool->redis
?
"redis"
:
"memcache",
pool->idx,
pool->name.len,
pool->name.data,
array_n(&pool->server));
return
NC_OK;
}
|
在调用proxy_listen()之前,调用了conn_get_proxy()来创建并初始化conn:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | struct conn * conn_get_proxy(void *owner) { struct server_pool *pool = owner; struct conn *conn; conn = _conn_get(); if (conn == NULL) { return NULL; } conn->redis = pool->redis; conn->proxy = 1; conn->recv = proxy_recv; conn->recv_next = NULL; conn->recv_done = NULL; conn->send = NULL; conn->send_next = NULL; conn->send_done = NULL; conn->close = proxy_close; conn->active = NULL; conn->ref = proxy_ref; conn->unref = proxy_unref; conn->enqueue_inq = NULL; conn->dequeue_inq = NULL; conn->enqueue_outq = NULL; conn->dequeue_outq = NULL; conn->ref(conn, owner); log_debug(LOG_VVERB, "get conn %p proxy %d", conn, conn->proxy); return conn; } |
通过这个函数定义,我们看到为conn初始化了recv、close、ref和unref这4个handler,其中recv的handler是proxy_recv,那么也就是说,当监听句柄由不可读变为可读时,将会触发proxy_recv这个回调函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
rstatus_t
proxy_recv(struct
context
*ctx,
struct
conn
*conn)
{
rstatus_t
status;
ASSERT(conn->proxy
&&
!conn->client);
ASSERT(conn->recv_active);
conn->recv_ready
=
1;
do
{
status
=
proxy_accept(ctx,
conn);
if
(status
!=
NC_OK)
{
return
status;
}
}
while
(conn->recv_ready);
return
NC_OK;
}
|
果不其然,这个函数调用了proxy_accept():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | static rstatus_t proxy_accept(struct context *ctx, struct conn *p) { rstatus_t status; struct conn *c; int sd; ASSERT(p->proxy && !p->client); ASSERT(p->sd > 0); ASSERT(p->recv_active && p->recv_ready); for (;;) { sd = accept(p->sd, NULL, NULL); if (sd < 0) { if (errno == EINTR) { log_debug(LOG_VERB, "accept on p %d not ready - eintr", p->sd); continue; } if (errno == EAGAIN || errno == EWOULDBLOCK) { log_debug(LOG_VERB, "accept on p %d not ready - eagain", p->sd); p->recv_ready = 0; return NC_OK; } /* * FIXME: On EMFILE or ENFILE mask out IN event on the proxy; mask * it back in when some existing connection gets closed */ log_error("accept on p %d failed: %s", p->sd, strerror(errno)); return NC_ERROR; } break; } c = conn_get(p->owner, true, p->redis); if (c == NULL) { log_error("get conn for c %d from p %d failed: %s", sd, p->sd, strerror(errno)); status = close(sd); if (status < 0) { log_error("close c %d failed, ignored: %s", sd, strerror(errno)); } return NC_ENOMEM; } c->sd = sd; …… } |
代码已经滤掉了无关部分,proxy_accept在accept到客户端的句柄后,通过调用conn_get()创建并初始化conn,在conn_get()中对conn做了如下初始化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/*
* client receives a request, possibly parsing
it, and sends a
* response downstream.
*/
conn->recv
=
msg_recv;
conn->recv_next
=
req_recv_next;
conn->recv_done
=
req_recv_done;
conn->send
=
msg_send;
conn->send_next
=
rsp_send_next;
conn->send_done
=
rsp_send_done;
conn->close
=
client_close;
conn->active
=
client_active;
conn->ref
=
client_ref;
conn->unref
=
client_unref;
conn->enqueue_inq
=
NULL;
conn->dequeue_inq
=
NULL;
conn->enqueue_outq
=
req_client_enqueue_omsgq;
conn->dequeue_outq
=
req_client_dequeue_omsgq;
|
而在创建Memcached连接时,是这么初始化的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/*
* server receives a response, possibly parsing
it, and sends a
* request upstream.
*/
conn->recv
=
msg_recv;
conn->recv_next
=
rsp_recv_next;
conn->recv_done
=
rsp_recv_done;
conn->send
=
msg_send;
conn->send_next
=
req_send_next;
conn->send_done
=
req_send_done;
conn->close
=
server_close;
conn->active
=
server_active;
conn->ref
=
server_ref;
conn->unref
=
server_unref;
conn->enqueue_inq
=
req_server_enqueue_imsgq;
conn->dequeue_inq
=
req_server_dequeue_imsgq;
conn->enqueue_outq
=
req_server_enqueue_omsgq;
conn->dequeue_outq
=
req_server_dequeue_omsgq;
|
这些handler的作用,这里暂时不细究,只需要理解事件触发回调函数的过程就可以了。
小结:
通过上面的分析,我们知道了Twemproxy使用了单进程模型,并且自己封装了事件模型,这与redis是类似的。这也就意味着,Twemproxy不适合做CPU密集的操作,仅作为proxy来用是OK的,若要对Twemproxy的功能进行扩展,要特别注意这一点,以免影响Twemproxy的吞吐量。