概述
Twemproxy中的IO复用考虑了跨平台的情况,针对不同平台采用不同的IO复用机制,比如Linux下使用epoll、FreeBSD使用kqueue等,在event目录下都有实现,所有的IO复用机制对外实现了统一的接口(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);
这样,方便不同平台的使用者无需考虑各种IO复用机制之间的不同。
在讲解网络通信、事件处理流程之前,需要了解NC配置文件的格式,以及其中的配置项,便于后续的讲解,简单的配置格式如下所示:
beta:
listen: 127.0.0.1:22122
hash: fnv1a_64
hash_tag: "{}"
distribution: ketama
auto_eject_hosts: false
timeout: 400
redis: true
servers:
- 127.0.0.1:6380:1 server1
- 127.0.0.1:6381:1 server2
- 127.0.0.1:6382:1 server3
- 127.0.0.1:6383:1 server4
gamma:
listen: 127.0.0.1:22123
hash: fnv1a_64
distribution: ketama
timeout: 400
backlog: 1024
preconnect: true
auto_eject_hosts: true
client_connections: 10000
server_connections: 10000
server_retry_timeout: 2000
server_failure_limit: 3
servers:
- 127.0.0.1:11212:1
- 127.0.0.1:11213:1
其中的参数意义大部分看字面意思都能知道了
Listen:监听的IP:Port
Hash:对命令中的key进行hash
Distribution:对命令分发的负载均衡的算法
Timeout:等待回复的超时时间(单位:毫秒)
Redis:标识后端服务器是redis还是memcached
Backlog:socket API listen的参数,即等待接收连接的队列的最大长度
Preconnect:是否预先(即启动后就)跟后端服务器(redis/memcached)建立连接
Auto_eject_hosts:对于无响应的后端服务器是否自动剔除
Client_connections:最大客户端连接数
Server_connections:最大服务端连接数
Server_retry_timeout:重试超时时间(单位:毫秒)
Server_failure_limit:最大重试次数
Servers:后端服务器信息,三个值分别是:IP:Port:Weight(权重)
数据结构
(1) 连接的管理——连接池(free_connq)
为了维护客户端和proxy的连接,以及proxy和server之间的连接,NC设计了一个双向链表(TAILQ)来管理每个client或server的连接队列,并间接实现了lru功能。
conn是twemproxy一个非常重要的结构,客户端到twemproxy的连接、twemproxy到后端server的连接,以及 proxy本身监听的tcp端口都可以抽象为一个conn。同时对于一个conn来说,也有不同的种类,例如:
proxy本身监听的端口所在的tcp套接字,就属于“proxy”;
客户端到proxy的连接conn就属于一 个”client“, 即proxy监听来自客户端的连接请求,执行过accept调用返回的文件描述符,就是client连接;
proxy到后端redis/memcached server的连接就属于”server”。
client:这个元素标识这个conn是一个client还是一个server。
proxy:这个元素标识这个conn是否是一个proxy。
owner:这个元素标识这个conn的属主。
纵观twemproxy, 他里边的conn有三种,client, server, proxy。当这个conn是一个proxy或者client时,则它此时的owner就是server_pool; 而当这个conn是一个server时,则它此时的owner就是server,
proxy类型的连接算不得一个真正的连接,它只是在监听来自客户端的连接,当有客户端连接到来时,经过三次握手之后,就建立了一个client类型的连接,proxy继续执行监听。
结构体struct conn表示一个连接(nc_connection.h),
struct conn {
TAILQ_ENTRY(conn) conn_tqe; /* link in server_pool / server / free q */
void *owner; /* connection owner - server_pool / server */
int sd; /* socket descriptor */
int family; /* socket address family */
socklen_t addrlen; /* socket length */
struct sockaddr *addr; /* socket address (ref in server or server_pool) */
struct msg_tqh imsg_q; /* incoming request Q */
struct msg_tqh omsg_q; /* outstanding request Q */
struct msg *rmsg; /* current message being rcvd */
struct msg *smsg; /* current message being sent */
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_post_connect_t post_connect; /* post connect handler */
conn_swallow_msg_t swallow_msg; /* react on messages to be swallowed */
conn_ref_t ref; /* connection reference handler */
conn_unref_t unref; /* connection unreference handler */
conn_msgq_t enqueue_inq; /* connection inq msg enqueue handler */
conn_msgq_t dequeue_inq; /* connection inq msg dequeue handler */
conn_msgq_t enqueue_outq; /* connection outq msg enqueue handler */
conn_msgq_t dequeue_outq; /* connection outq msg dequeue handler */
size_t recv_bytes; /* received (read) bytes */
size_t send_bytes; /* sent (written) bytes */
uint32_t events; /* connection io events */
err_t err; /* connection errno */
unsigned recv_active:1; /* recv active? */
unsigned recv_ready:1; /* recv ready? */
unsigned send_active:1; /* send active? */
unsigned send_ready:1; /* send ready? */
unsigned client:1; /* client? or server? */
unsigned proxy:1; /* proxy? */
unsigned connecting:1; /* connecting? */
unsigned connected:1; /* connected? */
unsigned eof:1; /* eof? aka passive close? */
unsigned done:1; /* done? aka close? */
unsigned redis:1; /* redis? */
unsigned authenticated:1; /* authenticated? */
};
其中主要包括:
l 因为连接是一个双向尾队列,需要每个conn保存其前(tqe_pre)后(tqe_next)的元素,就是TAILQ_ENTRY conn_tqe;
l 跟socket套接字相关的,addr/port/family
l 发送/接收请求包;
l 各种处理回调函数;
l 统计相关,接收/发送字节;
l 关注的事件events;
l 各种开关和状态;
每次需要建立新的连接(包括与client端和server端),都从连接池中取一个空闲连接。
(2) 服务端
运行上下文struct context *ctx定义中包含一个变量:
struct array pool; /*server_pool[] */
即一个ctx包含一个server_pool的数组,包含多个server_pool,而一个server_pool顾名思义,是一个server池(数组),包含多个server。
一个server_pool对应于配置信息中的一个块,比如上面的配置信息中的beta和gamma分别是一个server_pool;server对应于server_pool里的server段,比如上面的beta有四个server。
server_pool和server的关系截取源码的描述大体如下(nc_server.h):
/*
* server_pool is a collection of servers and their continuum. Each
* server_pool is the owner of a single proxy connection and one or
* more client connections. server_pool itself is owned by the current
* context.
*
* Each server is the owner of one or more server connections. server
* itself is owned by the server_pool.
*
* +-------------+
* | |<---------------------+
* | |<------------+ |
* | | +-------+--+-----+----+--------------+
* | pool 0 |+--->| | | |
* | | | server 0 | server 1 | ... ... |
* | | | | | |--+
* | | +----------+----------+--------------+ |
* +-------------+ //
* | |
* | |
* | |
* | pool 1 |
* | |
* | |
* | |
* +-------------+
* | |
* | |
* . .
* . ... .
* . .
* | |
* | |
* +-------------+
* |
* |
* //
*/
二者定义如下(nc_server.h):
struct server {
uint32_t idx; /* server index */
struct server_pool *owner; /* owner pool */
struct string pname; /* hostname:port:weight (ref in conf_server) */
struct string name; /* hostname:port or [name] (ref in conf_server) */
struct string addrstr; /* hostname (ref in conf_server) */
uint16_t port; /* port */
uint32_t weight; /* weight */
struct sockinfo info; /* server socket info */
uint32_t ns_conn_q; /* # server connection */
struct conn_tqh s_conn_q; /* server connection q */
int64_t next_retry; /* next retry time in usec */
uint32_t failure_count; /* # consecutive failures */
};
struct server_pool {
uint32_t idx; /* pool index */
struct context *ctx; /* owner context */
struct conn *p_conn; /* proxy connection (listener) */
uint32_t nc_conn_q; /* # client connection */
struct conn_tqh c_conn_q; /* client connection q */
struct array server; /* server[] */
uint32_t ncontinuum; /* # continuum points */
uint32_t nserver_continuum; /* # servers - live and dead on continuum (const) */
struct continuum *continuum; /* continuum */
uint32_t nlive_server; /* # live server */
int64_t next_rebuild; /* next distribution rebuild time in usec */
struct string name; /* pool name (ref in conf_pool) */
struct string addrstr; /* pool address - hostname:port (ref in conf_pool) */
uint16_t port; /* port */
struct sockinfo info; /* listen socket info */
mode_t perm; /* socket permission */
int dist_type; /* distribution type (dist_type_t) */
int key_hash_type; /* key hash type (hash_type_t) */
hash_t key_hash; /* key hasher */
struct string hash_tag; /* key hash tag (ref in conf_pool) */
int timeout; /* timeout in msec */
int backlog; /* listen backlog */
int redis_db; /* redis database to connect to */
uint32_t client_connections; /* maximum # client connection */
uint32_t server_connections; /* maximum # server connection */
int64_t server_retry_timeout; /* server retry timeout in usec */
uint32_t server_failure_limit; /* server failure limit */
struct string redis_auth; /* redis_auth password (matches requirepass on redis)
*/
unsigned require_auth; /* require_auth? */
unsigned auto_eject_hosts:1; /* auto_eject_hosts? */
unsigned preconnect:1; /* preconnect? */
unsigned redis:1; /* redis? */
unsigned tcpkeepalive:1; /* tcpkeepalive? */
};
server_pool中保存了到客户端的连接,server中保存了到服务端的连接,连接的存储都是使用了struct conn_tqh结构,底层使用双向链表(TAILQ)做存储介质。
在初始化时,会利用配置文件中的servers信息构造成一个个server变量,然后存入ctx中的server_pool数组中:
/* initialize server pool fromconfiguration */
status =server_pool_init(&ctx->pool, &ctx->cf->pool, ctx);
然后将该server_pool的owner设置为ctx:
/*set ctx as the server pool owner */
status= array_each(server_pool, server_pool_each_set_owner, ctx);
然后计算ctx的server_pool最大可建立的server连接数
/* compute max server connections */
ctx->max_nsconn = 0;
status = array_each(server_pool,server_pool_each_calc_connections, ctx);
配置文件的参数中有个配置参数:server_connections,记录server_pool中的每个server可以建立的server端连接数的最大值(该server_pool中的所有server公用这个值,有相同的server_connections)。一个server_pool可以建立的server端连接数 = server_connections * ((ctx->server_pool).size())+ 1,”1”代表一个server_pool有一个用于监听来自客户端连接的监听套接字,代码如下(nc_server.c):
static rstatus_t
server_pool_each_calc_connections(void *elem, void *data)
{
struct server_pool *sp = elem;
struct context *ctx = data;
ctx->max_nsconn += sp->server_connections * array_n(&sp->server);
ctx->max_nsconn += 1; /* pool listening socket */
return NC_OK;
}
所以,整个ctx可以建立的server端连接数就是所有server_pool的server端连接数的加和。
然后更新ctx->server_pool中的每一个server,采用什么方式分发,取决于配置文件中的” distribution”参数配置,有三种:KETAMA / MODULA/ RANDOM,具体每一种的逻辑是怎样的,可查看每一种的实现文件hashkit/nc_ketama.c、hashkit/nc_modula.c、hashkit/nc_random.c。
如果配置信息中的preconnect值为true,则在初始化时将建立与ctx->server_pool中的每一个server_pool中的每一个server的连接。
首先调用server_conn,从连接池中获取一个空闲连接,其次,server_connect中建立与server的TCP连接;最后,将该连接对应的conn结构体传入event(event_add_conn),conn结构体中含有该连接的套接字描述符。
(3) 客户端
客户端也有最大连接数,客户端的最大连接数是在服务端最大连接数的基础上计算出来的,即客户端最大连接数=系统允许进程最大的打开文件数-服务端最大连接数-保留的文件描述符数(nc_core.c)。
status = getrlimit(RLIMIT_NOFILE, &limit);
if (status < 0) {
log_error("getrlimit failed: %s", strerror(errno));
return NC_ERROR;
}
ctx->max_nfd = (uint32_t)limit.rlim_cur;
ctx->max_ncconn = ctx->max_nfd - ctx->max_nsconn - RESERVED_FDS;
(4) 请求包
(5) 应答包
请求处理流程
NC没有使用libevent,而是自己实现的网络通信库,采用单线程+非阻塞I/O+I/O多路复用实现的Reactor模式,将事件与发生该事件的连接(conn)联系在一起,
归纳起来,有五类事件:
(1).proxy监听客户端连接;
一个server_pool对应一个proxy,用于监听客户端发来的连接请求(注意:是连接请求,不是数据请求)。在初始化时,要初始化Proxy,主要工作就是对ctx->server_pool中的每一个server_pool从连接池中获取空闲连接(conn),并加入evb中,建立监听:
status =event_add_conn(ctx->evb, p);
status =event_del_out(ctx->evb, p);
注:event_add_conn增加新的连接监控
该conn的处理回调函数有别于数据请求的回调处理函数,比如:
conn->recv= proxy_recv;
conn->close= proxy_close;
conn->ref = proxy_ref;
conn->unref= proxy_unref;
(2).客户端接收请求;
(3).服务器端发送请求;
(4).服务器端接收应答;
(5).客户端发送应答;
每个NC都对应一个instance结构体,该结构体中包含该实例的event_base实例,event_base中注册的所有连接上的所有事件的回调函数都是core_core(nc_core.c),然后根据发生事件的连接(在初始化时,初始了各类函数指针)找到对应的函数指针,并调用。
处理过程如下:
每个client和server连接都各有一个in_q和一个out_q,为便于区分,分别起名字为c_inq/c_outq和s_inq/s_outq,首先请求到达c_inq,触发client<=>proxy(nc)连接上的recv函数,client端接收后,经过parse、filter等操作,请求从c_inq一方面放到c_outq(如果该请求需要应答的话),另一方面放到选择的某个server的s_inq中,同时修改该server对应连接的事件,增加event_out事件,
过程大致如下:
core_core->core_recv->msg_recv->req_recv_next->req_recv_done->req_filter->req_forward
等下一个event_loop运行时,这就触发了该server连接的send_out事件,该事件会调用事先初始化的send函数,这个函数会把s_inq中的请求逐个发送(msg_send_chain)给后端的服务器(Memcached/Redis)处理,
过程大致如下:
core_core->core_send->msg_send->req_send_next->req_send_done
处理完成后,返回应答给server,server将应答传递给client,client将应答发送给客户端。
过程不再列举,大概涉及以下几个函数:
core_core->core_recv->rsp_recv_next->rsp_recv_done->rsp_filter->rsp_forward
core_core->core_send->msg_send->rsp_send_next->rsp_send_done
源码里有一张图清楚地描述了整个过程(nc_message.c):
* Note that in the above discussion, the terminology send is used
* synonymously with write or OUT event. Similarly recv is used synonymously
* with read or IN event
*
* Client+ Proxy Server+
* (nutcracker)
* .
* msg_recv {read event} . msg_recv {read event}
* + . +
* | . |
* \ . /
* req_recv_next . rsp_recv_next
* + . +
* | . | Rsp
* req_recv_done . rsp_recv_done <===
* + . +
* | . |
* Req \ . /
* ===> req_filter* . *rsp_filter
* + . +
* | . |
* \ . /
* req_forward-// (a) . (c) \\-rsp_forward
* .
* .
* msg_send {write event} . msg_send {write event}
* + . +
* | . |
* Rsp' \ . / Req'
* <=== rsp_send_next . req_send_next ===>
* + . +
* | . |
* \ . /
* rsp_send_done-// (d) . (b) //-req_send_done
*
*
* (a) -> (b) -> (c) -> (d) is the normal flow of transaction consisting
* of a single request response, where (a) and (b) handle request from
* client, while (c) and (d) handle the corresponding response from the
* server.
分布式策略
twemproxy支持3种策略:
ketama:一致性hash的实现
modula:通过强hash取模来对应服务器
radom:随机分配服务器连接
zero-copy的实现
(未完待续)