Twemproxy源码走读(5):事件处理

概述

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的实现

(未完待续)


 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章