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的實現

(未完待續)


 

 

 

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