twemproxy源码分析

  twemproxy是twitter开源的redis/memcached 代理,数据分片提供取模,一致性哈希等手段,维护和后端server的长连接,自动踢除server,恢复server,提供专门的状态监控端口供外部工具获取状态监控信息。代码写的比较漂亮,学习了一些Nginx的东西,比如每个请求的处理分为多个阶段,IO模型方面,采用单线程收发包,基于epoll事件驱动模型。文档中提到的Zero Copy技术,通过将消息指针在3个队列之间流转实现比较巧妙。本文主要分析twemproxy的核心部分,即一个请求的从接收到最后发送响应给客户端的流程。

一、大体流程

   twemproxy后端支持多个server pool,为每个server pool分配一个监听端口用于接收客户端的连接。客户端和proxy建立连接记作client_conn,发起请求,proxy读取数据,放入req_msg中,设置msg的owner为client_conn,proxy根据策略从server pool中选取一个server并且建立连接记作server_conn,然后转发req_forward,将req_msg指针放入client_conn的output队列中,同时放入server_conn的input队列,然后触发server_conn的写事件,server_conn的写回调函数会从input队列中取出req_msg发送给对应的后端server,发送完成后将req_msg放入server_conn的output队列,当req_msg的响应rsp_msg回来后,调用rsp_filter(用于判断消息是否为空,是否消息可以不用回复等)和rsp_forward,将req_msg从server_conn的output队列中取出,建立req_msg和rsp_msg的对应关系(通过msg的peer字段),通过req_msg的owner找到client_conn,然后启动client_conn的写事件,client_conn的写回调函数从client_conn的output队列中取出req_msg,然后通过peer字段拿到对应的rsp_msg,将其发出去。至此,一次请求从被proxy接收到最后响应给client结束。

   可以看出,整个流程,req_msg内容只有一份,req_msg指针在三个队列中的顺序是:

        1. req_msg => client_conn.outputq

        2. req_msg => server_conn.inputq

        3. server_conn.inputq => req_msg

        4. req_msg => server_conn.outputq

        5. server_conn.outputq => req_msg

        6. client_conn.outputq => req_msg

   总体来说,proxy既需要接收客户端的连接,也需要维护和后端server的长连接,根据从客户端收到的req根据特定策略选择后端一台server进行转发,同一个客户端的连接上的不同的请求可能会转发到后端不同的server。

   

  

二、基础数据结构:

  后端的每个redis server对应一个server结构:

struct server {
      uint32_t           idx;           /* server index */
      struct server_pool *owner;        /* owner pool */      // 每个server属于一个server 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 */  // 下面队列中conn的个数
      struct conn_tqh    s_conn_q;      /* server connection q */ // proxy和这个redis server之间维护的连接队列  
  
      int64_t            next_retry;    /* next retry time in usec */ // proxy有踢除后端server机制,当proxy给某台server转发请求出错次数达到server_failure_limit次,则next_retry微妙内不会请求该server。可配。
      uint32_t           failure_count; /* # consecutive failures */
  };  

  twemproxy中使用一堆宏来定义队列等数据结构,如上面struct conn_tqh,nc_connection.h中有定义TAILQ_HEAD(conn_tqh, conn),TAILQ_HEAD宏定义如下:

 /*   
   * Tail queue declarations. 
   */  
  #define TAILQ_HEAD(name, type)                                          \                                                                                                                                  
  struct name {                                                           \
      struct type *tqh_first; /* first element */                         \
      struct type **tqh_last; /* addr of last next element */             \
      TRACEBUF                                                            \
  }

可以看出结构体是通过宏来定义的,非常恶心,看代码ctags 找不到。conn_tqh是一个队列头部结构体,嵌入到一个server中,链表中每个元素是一个conn结构体,内嵌入一个TAILQ_ENTRY,用于将conn串入server的队列中。宏定义如下:

 #define TAILQ_ENTRY(type)                                               \
  struct {                                                                \
      struct type *tqe_next;  /* next element */                          \
      struct type **tqe_prev; /* address of previous next element */      \
      TRACEBUF                                                            \
  } 

各个field的关系如下图所示:

 

看一个很重要的结构conn,可以表示client和proxy之间的connection,也可以表示proxy和redis server之间的connection:

struct conn {
      TAILQ_ENTRY(conn)  conn_tqe;      /* link in server_pool / server / free q */ // 队列entry字段,用于和其他的conn串起来
      void               *owner;        /* connection owner - server_pool / server */ // 每个连接属于一个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 */  //从名字看出,和conn_tqh类似,这里也是一个消息队列,从连接读入的数据会组织成msg,push到这个消息队列,msg由mbuf队列组成用来存储具体的数据。
      struct msg_tqh     omsg_q;        /* outstanding request Q */ // 需要往这个连接中写的msg push到这个消息队列
      struct msg         *rmsg;         /* current message being rcvd */ //从连接上读到的数据往rmsg指向的msg里面填
      struct msg         *smsg;         /* current message being sent */ //当前正在写的msg指针
  
      conn_recv_t        recv;          /* recv (read) handler */ //读事件触发时回调
      conn_recv_next_t   recv_next;     /* recv next message handler */ //实际读数据之前,调这个函数来得到当前正在使用的msg
      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 */ 实际写数据之前,定位当前要写的msg
      conn_send_done_t   send_done;     /* write done handler */ //发送完一个msg则回调一次
      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 */  //这四个队列用于存放msg的指针,和Zero Copy密切相关,后续详述
      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? */ //连接属于proxy和client之间时,client为1,连接属于proxy和后端server之间时,client为0
      unsigned           proxy:1;       /* proxy? */ // listen fd封装在conn中时,proxy置1,响应的recv回调函数accept连接
      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? */ //后端server是redis还是memcached
  };

 二、

不管是proxy accept了Client的连接从而分配一个conn结构,还是proxy主动和后端server建立连接从而分配一个conn结构,都调用conn_get()函数,如下:

struct conn *conn_get(void *owner, bool client, bool redis)                                                                                                                                                             
{
      struct conn *conn;
  
      conn = _conn_get();
      if (conn == NULL) {
          return NULL;
      }
  
      /* connection either handles redis or memcache messages */
      conn->redis = redis ? 1 : 0;
  
      conn->client = client ? 1 : 0;
  
      if (conn->client) {
          /*
           * client receives a request, possibly parsing it, and sends a
           * response downstream.
           */
          conn->recv = msg_recv;  // 从conn读数据
          conn->recv_next = req_recv_next; // 在真正从conn读数据之前,需要分配一个req_msg,用于承载读进来的数据
          conn->recv_done = req_recv_done; //每次读完一个完整的消req_msg被调用
  
          conn->send = msg_send; // 将从server收到的响应rsp_msg发给客户端
          conn->send_next = rsp_send_next; // 每次发送rsp_msg之前需要首先确定从哪个开始发
          conn->send_done = rsp_send_done; // 每次发送完成一个rsp_msg给客户端,调一次
  
          conn->close = client_close; //用于proxy断开和client的半连接
          conn->active = client_active;
  
          conn->ref = client_ref; //获取conn后将conn丢进客户端连接队列
          conn->unref = client_unref;
  
          conn->enqueue_inq = NULL;
          conn->dequeue_inq = NULL;
          conn->enqueue_outq = req_client_enqueue_omsgq; // proxy每次接收到一个client发过来的req_msg,将req_msg入conn的output 队列
          conn->dequeue_outq = req_client_dequeue_omsgq; // 给客户端发送完rsp_msg后将其对应的req_msg从conn的output队列中删除
      } else {
          /*
           * server receives a response, possibly parsing it, and sends a
           * request upstream.
           */
          conn->recv = msg_recv; 
          conn->recv_next = rsp_recv_next; //从后端server接收数据之前需要先得到一个rsp_msg,用于承载读到的数据
          conn->recv_done = rsp_recv_done; // 每次读完一个完整的rsp_msg,则回调
          conn->send = msg_send; // 将req_msg往后端server发
          conn->send_next = req_send_next; // 确定发哪个req_msg
          conn->send_done = req_send_done; // 每转发完一个即回调
  
          conn->close = server_close;
          conn->active = server_active;
  
          conn->ref = server_ref;
          conn->unref = server_unref;
  
          conn->enqueue_inq = req_server_enqueue_imsgq; // proxy将需要转发的req_msg放入对应后端server连接的input队列
          conn->dequeue_inq = req_server_dequeue_imsgq; //proxy从input队列中取出req_msg发送给后端server完成后,需要将req_msg从这个后端连接的input队列中删除
          conn->enqueue_outq = req_server_enqueue_omsgq; // 继上一步,需要将req_msg放入到后端连接的output队列
          conn->dequeue_outq = req_server_dequeue_omsgq; // 收到后端server的rsp_msg后,将rsp_msg对应的req_msg从连接的output队列删除
      }
  
      conn->ref(conn, owner);
  
      log_debug(LOG_VVERB, "get conn %p client %d", conn, conn->client);
  
      return conn;
  }

 如果该连接是proxy和后端server建立的,则client为false,否则为true,如果后端server是redis,则redis为true,如果为memcached,则为false,连接上的多个读写回调函数根据传入的标记不同而不同。

三、 请求具体处理流程

 从前面可以看出,当proxy和Client之间有数据可读时,会调用msg_recv(),如下:

rstatus_t
  msg_recv(struct context *ctx, struct conn *conn)                                                                                                                                                           
  {    
      rstatus_t status;       
      struct msg *msg;        
  
      ASSERT(conn->recv_active);
  
      conn->recv_ready = 1;   
      do {
          msg = conn->recv_next(ctx, conn, true); //req_recv_next()
          if (msg == NULL) {  
              return NC_OK;
          }
  
          status = msg_recv_chain(ctx, conn, msg);
          if (status != NC_OK) {
              return status;
          }
      } while (conn->recv_ready);    
  
      return NC_OK;
  }

代码很短,其实就是反复的做两件事:req_recv_next和msg_recv_chain

req_recv_next获取当前用于接收数据的msg,设置到conn->rmsg中,并且返回rmsg,然后传给msg_recv_chain,每次重新接收一个完整的请求时,rmsg为空,如果某次读取只读取了请求的一部分,则rmsg不为空,下次读取时数据继续追加到上一次的msg中。

重点看msg_recv_chain()做的事情:

1. 从conn->rmsg中拿出最后一个mbuf(一个msg的数据实际上存在mbuf中,一个msg可以包含多个mbuf,同样通过队列组织),下一次读最多读最后一个mbuf的剩余空间大小,如果最后一个mbuf满了,分配一个新的,插入到rmsg的mbuf队列尾部

2. 循环从conn读数据,如果读取到的数据小于参入的buf大小,设置conn->recv_ready为0,表示后续没有数据要读了,外部的while(conn->recv_ready)循环退出。同样,如果读操作返回0表示客户端主动断开连接,将conn的eof标记置位,同时recv_ready也清0

3. 调用msg_parse()解析rmsg数据,如果成功解析到一条完整的命令,则继续调用msg_parsed(msg),由于msg由多个mbuf组成,并且TCP是流式协议,所以一次读可能接收到了多条完整的命令,甚至是部分命令。这时,msg_parsed(msg)会将后面这些多余的数据拷贝到一个新的mbuf中,并且产生一个新的msg,作为conn的rmsg。由于一次读可能会读到多条命令,这就是为什么msg_recv_chain()中有下面这个循环:

for (;;) {
          status = msg_parse(ctx, conn, msg); //每次解析一条命令                                                                                                                                                     
          if (status != NC_OK) {
               return status;
           }
  
          /* get next message to parse */
          nmsg = conn->recv_next(ctx, conn, false);
          if (nmsg == NULL || nmsg == msg) {
             /* no more data to parse */
             break;
          }
          msg = nmsg;
      }

 4. 同时,每次调msg_parse(ctx,conn,msg)解析出一条新的命令后,都会回调req_recv_done()方法。这个方法对请求进行过滤(req_filter)和转发(req_forward)给后端server。

 5. msg_recv_chain()完成。

回到msg_recv,根据conn->recv_ready来判断是否连接中还有未读数据,有则继续读,parse。。

 

下面看请求转发给后端函数 req_forward():

 1. 将解析出来的msg(以后将从客户端发过来的msg叫做req_msg)push进conn的output队列

 2. 根据key和策略从server pool中选择一个server,并且获得和server的连接,记作server_conn,将req_msg push到server_conn的输入队列,起server_conn的写事件

至此,req_msg从client接收到解析到转发结束。下面看发消息给后端server,从后端server接收响应,将响应回复给客户端的过程

 

起server_conn写事件后,回调函数msg_send(struct context *ctx, struct conn *conn):

  rstatus_t
  msg_send(struct context *ctx, struct conn *conn)                                                                                                                                                           
  {    
      rstatus_t status;       
      struct msg *msg;        
  
      ASSERT(conn->send_active);
  
      conn->send_ready = 1;   
      do {
          msg = conn->send_next(ctx, conn); 
          if (msg == NULL) {  
              /* nothing to send */          
              return NC_OK;   
          }
  
          status = msg_send_chain(ctx, conn, msg);
          if (status != NC_OK) {
              return status;
          }
  
      } while (conn->send_ready);

可以看出和接收函数msg_recv类似。req_send_next()从server_conn的input队列中拿出一个待发消息赋值给conn->smsg。然后调用msg_send_chain()对消息进行实际的发送。

msg_send_chain()流程如下:

1. 准备NC_IOV_MAX个iov,遍历smsg的mbuf队列,每个mbuf的数据用一个iov指向。

2. 循环调用req_send_next()不断的取出待发送的smsg,将其加入到一个局部消息队列send_msgq,同时遍历smsg的mbuf队列,每个mbuf用一个iov指向,直到NC_IOV_MAX个iov被装满,或者没有消息需要发送了,记录下装进去的消息总大小,记作nsend。

3. 循环往fd上写,返回成功写的大小nsent,遍历send_msgq中的msg,和msg中的mbuf队列,将已发送成功的mbuf置为空,并且将发送了部分msg的pos指向第一个未发送的字节。并且,如果一个msg的mbuf队列中的所有的mbuf都发送完成了,则将调用req_send_done(),将这个msg(指针)从这个连接的input队列中删除,并且放入到连接的output队列中。从这里可以看出,只有一个msg的所有的mbuf都被发送出去了才会从input队列中删除,如果只发送了部分mbuf,这些mbuf会被标记为空,下次继续发送这个msg时,会略过空的msg,实现一个msg过大或者网络阻塞导致需要多次发送才能发出一个msg的情况。

4. 至此,发送流程分析完成。

 

Redis接收到消息,处理返回,proxy接收响应,和proxy接收client的数据类似,同样调用msg_recv()

  rstatus_t
  msg_recv(struct context *ctx, struct conn *conn)                                                                                                                                                           
  {    
      rstatus_t status;       
      struct msg *msg;        
  
      ASSERT(conn->recv_active);
  
      conn->recv_ready = 1;   
      do {
          msg = conn->recv_next(ctx, conn, true); //rsp_recv_next()
          if (msg == NULL) {  
              return NC_OK;   
          }
  
          status = msg_recv_chain(ctx, conn, msg);
          if (status != NC_OK) {
              return status;
          }
      } while (conn->recv_ready);    
  
      return NC_OK;
  }

只是,在这里,conn->recv_next指向的函数是rsp_recv_next(),不是req_recv_next()。同理,接收回消息的处理函数是rsp_recv_done(),不是req_recv_done()

rsp_recv_next():

1. 如果当前conn的rmsg为空,则分配一个新的返回,否则返回当前这个rmsg

2. 将上一步返回的rmsg传给msg_recv_chain()处理。这个函数之前在proxy接收client请求时分析了,不再赘述。

收到一个完整的响应rsp_msg后,调rsp_recv_done():

1. 同样和前面类似,在这里调用rsp_filter()和rsp_forward(),而不是req_filter()和req_forward()

重点说rsp_forward():

1. 从连接的output队列中弹出第一个元素,记作req_msg,这个msg即是目前收到的这个msg(rsp_msg)相对应的请求msg

2. 将req_msg的标记done置为1,表示这个请求已完成。

3. 通过以下两个语句将这两个msg建立对应关系:

pmsg->peer = msg; //pmsg为req_msg,msg为rsp_msg
msg->peer = pmsg

4. 从pmsg的owner可以找到所属的client conn,然后启client conn的写事件。

5. 至此,proxy从后端server接收响应分析完成。

 

最后,看一下proxy将响应写给client的流程。

类似,调用msg_send():

rstatus_t
  msg_send(struct context *ctx, struct conn *conn)
  {
      rstatus_t status;
      struct msg *msg;
  
      ASSERT(conn->send_active);
  
      conn->send_ready = 1;
      do {
          msg = conn->send_next(ctx, conn); // rsp_send_next()
          if (msg == NULL) {
              /* nothing to send */
              return NC_OK;
          }
  
          status = msg_send_chain(ctx, conn, msg);
          if (status != NC_OK) {
              return status;
          }
  
      } while (conn->send_ready);
  
      return NC_OK;
  }               

同理,在这里,conn->send_next函数指针指向rsp_send_next():

1. 从client conn的output队列中拿出msg,记作req_msg,从req_msg的peer字段将相应的rsp_msg拿出来,放在conn的smsg上待发送

发出完成后,调用rsp_send_done(),主要做的事就是将req_msg从client conn的output队列中删除。

 

 

 

 

posted @ 2014-04-08 14:00  吴镝  阅读(5743)  评论(0编辑  收藏  举报