twemproxy(又称为nutcracker)是一个轻量级的RedisMemcached代理,主要用来减少对后端缓存服务器的连接数。由Twitter开源出来的缓存服务器集群管理工具,主要用来弥补RedisMemcached对集群(cluster)管理指出的不足。


antirez(Redis作者)写过一篇对twemproxy的介绍http://antirez.com/news/44,他认为twemproxy是目前Redis 分片管理的最好方案,虽然antirezRedis cluster正在实现并且对其给予厚望,但是我从现有的cluster实现上还是认为cluster除了增加Redis复杂度,对于集群的管理没有twemproxy来的轻量和有效。


谈到集群管理不得不又说到数据的分片管理,为了满足数据的日益增长和扩展性,数据存储系统一般都需要进行一定的分片,如传统的MySQL进行横向分表和纵向分表,然后应用程序访问正确的位置就需要找的正确的表。这时候,这个数据定向工作一般有三个位置可以放,数据存储系统本身支持,服务器端和客户端中间建代理支持或者客户端支持。Redis Cluster就是典型的试图在数据存储系统上支持分片,而twemproxy就是试图在服务器端和客户端中间建代理支持,Memcached的客户端对分片的支持就是客户端层面的。

在三种方案中,客户端方案我认为欠妥,因为这样每个客户端需要维护一定的服务器信息,但是如果动态的增加或减少节点就需要重写配置各个客户端。而在服务器端增加集群管理有利于使用者,减少使用者需要了解的东西,整合集群管理使得性能比其他方案都要更高,但是缺点是其会严重增加代码复杂度,导致服务器端代码爆炸。而采用中间层代理的方式我认为是最优雅和有效的,在不改动服务器端程序的情况下,twemproxy使得集群管理更简单,去除不支持的操作和合并,同时更可以支持多个后端服务,大大减少连接数等等,但是缺点也是显而易见的,它不能更有效的利用集群的优势,如多键运算和范围查找操作等等,这都是需要服务器端程序本身支持。

最后,如果就Redis而言,我认为最好的方式是在Redis的基础上做一个代理计算层,也就是所有的操作通过这个代理计算层进行对Redis集群的操作,也就是一个Master-Slave结构的Redis集群,因为Redis作为一个后端服务,本身连接数不宜过多,通过多Slave备份Master达到的效果比无核心节点我认为更好。

回到Twemproxy,它主要通过事件驱动模型来达到高并发,每收到一个请求,通过解析请求,发送请求到后端服务,再等待回应,发送回请求方。主要涉及到三个重要的结构:server,connection, message

server

struct server {
    uint32_t           idx;           /* server index */
    struct server_pool *owner;        /* owner pool */
                                                                                               
    struct string      pname;         /* name:port:weight (ref in conf_server) */
    struct string      name;          /* name (ref in conf_server) */
    uint16_t           port;          /* port */
    uint32_t           weight;        /* weight */
    int                family;        /* socket family */
    socklen_t          addrlen;       /* socket length */
    struct sockaddr    *addr;         /* socket address (ref in conf_server) */
                                                                                               
    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 */
};


每个server其实就是一个后端的缓存服务程序,Twemproxy可以预先连接每个server或者不,根据接收到的请求具体分析出key,然后根据key来选择适当的server,这里的选择过程采用一致性哈希算法,具体算法可以根据配置文件选择。

connection

connection在Twemproxy中非常重要,它分为三种类型的connection:proxy,client和server,也就是监听的socket,客户端连接的socket和连接后端的socket,其中proxy类型的工作比较简单,就是接受到请求,然后产生一个client connection或者server connection。

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_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? */
};


可以用面向对象的方法来考虑这个结构,struct conn就是个基类,而proxy,client和server是派生类,通过recv_done, recv_next, recv和send_done, send_next, send来达到多态的效果。

当客户端建立与Twemproxy的连接或者Twemproxy建立与后端服务的连接后,会生成一个conn结构,或为client或为server类。

message

struct msg {
    TAILQ_ENTRY(msg)     c_tqe;           /* link in client q */
    TAILQ_ENTRY(msg)     s_tqe;           /* link in server q */
    TAILQ_ENTRY(msg)     m_tqe;           /* link in send q / free q */
                                                          
    uint64_t             id;              /* message id */
    struct msg           *peer;           /* message peer */
    struct conn          *owner;          /* message owner - client | server */
                                                          
    struct rbnode        tmo_rbe;         /* entry in rbtree */
                                                          
    struct mhdr          mhdr;            /* message mbuf header */
    uint32_t             mlen;            /* message length */
                                                          
    int                  state;           /* current parser state */
    uint8_t              *pos;            /* parser position marker */
    uint8_t              *token;          /* token marker */
                                                          
    msg_parse_t          parser;          /* message parser */
    msg_parse_result_t   result;          /* message parsing result */
                                                          
    mbuf_copy_t          pre_splitcopy;   /* message pre-split copy */
    msg_post_splitcopy_t post_splitcopy;  /* message post-split copy */
    msg_coalesce_t       pre_coalesce;    /* message pre-coalesce */
    msg_coalesce_t       post_coalesce;   /* message post-coalesce */
                                                          
    msg_type_t           type;            /* message type */
                                                          
    uint8_t              *key_start;      /* key start */
    uint8_t              *key_end;        /* key end */
                                                          
    uint32_t             vlen;            /* value length (memcache) */
    uint8_t              *end;            /* end marker (memcache) */
                                                          
    uint8_t              *narg_start;     /* narg start (redis) */
    uint8_t              *narg_end;       /* narg end (redis) */
    uint32_t             narg;            /* # arguments (redis) */
    uint32_t             rnarg;           /* running # arg used by parsing fsa (redis) */
    uint32_t             rlen;            /* running length in parsing fsa (redis) */
    uint32_t             integer;         /* integer reply value (redis) */
                                                          
    struct msg           *frag_owner;     /* owner of fragment message */
    uint32_t             nfrag;           /* # fragment */
    uint64_t             frag_id;         /* id of fragmented message */
                                                          
    err_t                err;             /* errno on error? */
    unsigned             error:1;         /* error? */
    unsigned             ferror:1;        /* one or more fragments are in error? */
    unsigned             request:1;       /* request? or response? */
    unsigned             quit:1;          /* quit request? */
    unsigned             noreply:1;       /* noreply? */
    unsigned             done:1;          /* done? */
    unsigned             fdone:1;         /* all fragments are done? */
    unsigned             first_fragment:1;/* first fragment? */
    unsigned             last_fragment:1; /* last fragment? */
    unsigned             swallow:1;       /* swallow response? */
    unsigned             redis:1;         /* redis? */
};


struct msg是连接建立后的消息内容发送载体,这个复杂的msg结构很大程度是因为需要实现pipeline的效果,多个msg属于同一个conn,conn通过接收到内容解析来发现几个不同的msg。

事件驱动
static void
core_core(struct context *ctx, struct conn *conn, uint32_t events)
{
    rstatus_t status;
                                                 
    log_debug(LOG_VVERB, "event %04"PRIX32" on %c %d", events,
              conn->client ? 'c' : (conn->proxy ? 'p' : 's'), conn->sd);
                                                 
    conn->events = events;
                                                 
    /* read takes precedence over write */
    if (events & EVENT_READABLE) {
        status = core_recv(ctx, conn);
        if (status != NC_OK || conn->done || conn->err) {
            core_close(ctx, conn);
            return;
        }
    }
                                                 
    if (events & EVENT_WRITABLE) {
        status = core_send(ctx, conn);
        if (status != NC_OK || conn->done || conn->err) {
            core_close(ctx, conn);
            return;
        }
    }
}


事件驱动库会绑定每一个socket fd到一个conn,当检测到可读时,会调用conn->recv来推动,如果是proxy的fd可读,这时conn->recv的工作就是建立一个新的连接来接收消息。如果client的fd可读,其对应的conn的conn->recv的工作就是接收内容并分析。如果server的fd可读时,其对应的server conn的conn->recv的工作是接收到后端服务的回应,然后再回发给请求方。

小结

Twemproxy的架构比较清晰,对Twemproxy源码印象较深的是对logging的合理布局和错误处理的清晰,这是第一次看大公司开源出来的代码,非常重视logging和错误处理。

我的fork

由于Twitter开源的Twemproxy直接使用epoll驱动,导致其他不支持epoll的系统无法使用,因此我fork了一个版本,加入了kqueue支持,让FreeBSD和Mac os x能够成功编译运行,并且因为Twemproxy不合理的将epoll嵌入到业务逻辑中,因此,只能大改事件驱动的相关代码,将复用IO的结构抽象出来,并且增加了部分结构。

我的Twemproxy Fork版本