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的结构抽象出来,并且增加了部分结构。
转载于:https://blog.51cto.com/sofar/1299877