twemproxy(又称为nutcracker)是一个轻量级的Redis和Memcached代理,主要用来减少对后端缓存服务器的连接数。由Twitter开源出来的缓存服务器集群管理工具,主要用来弥补Redis和Memcached对集群(cluster)管理指出的不足。
antirez(Redis作者)写过一篇对twemproxy的介绍http://antirez.com/news/44,他认为twemproxy是目前Redis 分片管理的最好方案,虽然antirez的Redis 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版本