memcached源码分析三-libevent与命令解析

  转载请注明出处https://www.cnblogs.com/yang-zd/p/11352833.html,谢谢合作!

  前面已经分析了memcached中的slabs内存管理及缓存对象如何利用item表达并存储在slabs管理的空间中,并分析了如何实现LRU策略实现缓存对象的释放。但memcached是一个client-server结构的缓存系统,服务端需要接收客户端的指令,然后在服务端做相应的操作,比如新建缓存对象,读取缓存对象。memcached使用libevent实现这些网络通信操作,接下来将会分析memcached如何使用libevent处理网络事件。

1. threads in memcached

  通常的网络编程模型中,服务端都会有一个处于listening状态的socket,当客户端尝试与服务端建立连接时,服务端会建立一个新的socket与该客户端通信,并将该通信任务交给其它线程完成。事实上,memcached也采用了这样的模型,但它通过libevent实现这个模型。

  简单地说libevent的工作方式就是:一个event_base,一些加入event_base中的events,每一个event都有一个回调函数,event_bse不停地循环等待这些events发生,事件发生后调用对应event的回调函数。

  memcached中有2类event_base:main_base,位于主线程中,处理listening socket;normal event_base,位于工作线程中,处理accepted socket。它们的关系可以简单地以图1-1表示,

memcached源码分析三-libevent与命令解析_第1张图片

 

图1-1 memcached中的工作线程模型

(ps: 工作线程的数量可以在启动memcached时设置)

 memcached使用struct LIBEVENT_THREAD结构保存工作线程相关的信息,其定义如下,

typedef struct {
    pthread_t thread_id;        /* unique ID of this thread */
    struct event_base *base;    /* libevent handle this thread uses */
    struct event notify_event;  /* listen event for notify pipe */
    int notify_receive_fd;      /* receiving end of notify pipe */
    int notify_send_fd;         /* sending end of notify pipe */
    struct thread_stats stats;  /* Stats generated by this thread */
    struct conn_queue *new_conn_queue; /* queue of new connections to handle */
    cache_t *suffix_cache;      /* suffix cache */
#ifdef EXTSTORE
    cache_t *io_cache;          /* IO objects */
    void *storage;              /* data object for storage system */
#endif
    logger *l;                  /* logger buffer */
    void *lru_bump_buf;         /* async LRU bump buffer */
#ifdef TLS
    char   *ssl_wbuf;
#endif

} LIBEVENT_THREAD;

thread_id它保存了线程的线程id,base,保存了属于该线程的event_base结构。

  notify_event, notify_received_fd, notify_send_fd三个成员一起用于完成唤醒线程的任务,notify_event被加入到它的event_base中,监听nofity_received_fd上的读事件,当有读事件发生时,唤醒线程。外部想要唤醒该线程时,向notify_send_fd发送数据,即可引发nofity_received_fd上的读事件。这两个fd的关联通过pipe通道完成。

  另外有一个new_conn_queue成员,这个成员用来协助完成主线程的任务分发工作。当主线程接受新的连接后,建立一个struct conn_queue_item结构,并将该结构push到某个工作线程的new_conn_queue指向的队列上。worker thread被唤醒后从该队列上pop新的连接建立新的事件加入它的event_base,开始监听它的网络通信。以下是new_conn_queue的及其元素CQ_ITEM的定义

/* A connection queue. */
typedef struct conn_queue CQ;
struct conn_queue {
    CQ_ITEM *head;
    CQ_ITEM *tail;
    pthread_mutex_t lock;
};

typedef struct conn_queue_item CQ_ITEM;
struct conn_queue_item {
    int               sfd;
    enum conn_states  init_state;
    int               event_flags;
    int               read_buffer_size;
    enum network_transport     transport;
    enum conn_queue_item_modes mode;
    conn *c;
    void    *ssl;
    CQ_ITEM          *next;
};

CQ_ITEM中sfd包含了相应的socket,transport表示传输层协议类型,read_buffer_size代表读数据的buffer初始大小,init_state表示这个conn的初始状态,用于struct conn结构,conn结构是memcached中用于读写数据的一个关键数据结构,后面将进行介绍。相关的队列操作函数是cq_pop与cq_push。

  无论是listening socekt的事件回调函数还是accepted socket的事件回调函数,在memcached中都是event_handler,该函数会调用一个相当复杂的函数drive_mathine,它会根据状态进行不同的处理。如main thread中的listening socket有连接事件到来时,回调函数将调用accept函数接受连接请求,并将新建立的socket交给某个worker thread处理。worker thread中的socket的事件发生时,处理工作相对较复杂,包括接收数据,建立item,并进行存储,或者返回响应信息等。

2. drive_machine

  首先,为了服务端与客户端的沟通,memcached定义了它的信息的格式协议。协议有两类,一类是text protocol,一类是binary protocol,具体可参考https://github.com/memcached/memcached/wiki/Protocols。事件回调函数中根据协议对消息进行解析,并进行响应。

  前面提到,memcached中所有的libevent网络事件的回调函数都是event_handler,而这个函数又简单地调用了drive_machine完成工作,drive_mathine根据conn_states值做不同的操作。每一个连接在memcached中都使用一个conn结构进行描述,其中包含了它的socket, event及读写数据使用到的相关buffer描述,而conn_states正是描述struct conn目前所处的状态的成员,其取值范围如下,  

/**
 * Possible states of a connection.
 */
enum conn_states {
    conn_listening,  /**< the socket which listens for connections */
    conn_new_cmd,    /**< Prepare connection for next command */
    conn_waiting,    /**< waiting for a readable socket */
    conn_read,       /**< reading in a command line */
    conn_parse_cmd,  /**< try to parse a command from the input buffer */
    conn_write,      /**< writing out a simple response */
    conn_nread,      /**< reading in a fixed number of bytes */
    conn_swallow,    /**< swallowing unnecessary bytes w/o storing */
    conn_closing,    /**< closing this connection */
    conn_mwrite,     /**< writing out many items sequentially */
    conn_closed,     /**< connection is closed */
    conn_watch,      /**< held by the logger thread as a watcher */
    conn_max_state   /**< Max state value (used for assertion) */
};

conn_listening状态下drive_machine会从该listening socket上接受客户端连接,建立新的连接,分发给工作线程。

具体的状态转移及所做的相关操作见图2-1,由于其中使用了许多struct conn的成员,在此之前先前conn的定义列出:

/**
 * The structure representing a connection into memcached.
 */
struct conn {
    int    sfd;
#ifdef TLS
    SSL    *ssl;
    char   *ssl_wbuf;
    bool ssl_enabled;
#endif
    sasl_conn_t *sasl_conn;
    bool sasl_started;
    bool authenticated;
    enum conn_states  state;
    enum bin_substates substate;
    rel_time_t last_cmd_time;
    struct event event;
    short  ev_flags;
    short  which;   /** which events were just triggered */

    char   *rbuf;   /** buffer to read commands into */
    char   *rcurr;  /** but if we parsed some already, this is where we stopped */
    int    rsize;   /** total allocated size of rbuf */
    int    rbytes;  /** how much data, starting from rcur, do we have unparsed */

    char   *wbuf;
    char   *wcurr;
    int    wsize;
    int    wbytes;
    /** which state to go into after finishing current write */
    enum conn_states  write_and_go;
    void   *write_and_free; /** free this memory after finishing writing */

    char   *ritem;  /** when we read in an item's value, it goes here */
    int    rlbytes;

    /* data for the nread state */

    /**
     * item is used to hold an item structure created after reading the command
     * line of set/add/replace commands, but before we finished reading the actual
     * data. The data is read into ITEM_data(item) to avoid extra copying.
     */

    void   *item;     /* for commands set/add/replace  */

    /* data for the swallow state */
    int    sbytes;    /* how many bytes to swallow */

    /* data for the mwrite state */
    struct iovec *iov;
    int    iovsize;   /* number of elements allocated in iov[] */
    int    iovused;   /* number of elements used in iov[] */

    struct msghdr *msglist;
    int    msgsize;   /* number of elements allocated in msglist[] */
    int    msgused;   /* number of elements used in msglist[] */
    int    msgcurr;   /* element in msglist[] being transmitted now */
    int    msgbytes;  /* number of bytes in current msg */

    item   **ilist;   /* list of items to write out */
    int    isize;
    item   **icurr;
    int    ileft;

    char   **suffixlist;
    int    suffixsize;
    char   **suffixcurr;
    int    suffixleft;
#ifdef EXTSTORE
    int io_wrapleft;
    unsigned int recache_counter;
    io_wrap *io_wraplist; /* linked list of io_wraps */
    bool io_queued; /* FIXME: debugging flag */
#endif
    enum protocol protocol;   /* which protocol this connection speaks */
    enum network_transport transport; /* what transport is used by this connection */

    /* data for UDP clients */
    int    request_id; /* Incoming UDP request ID, if this is a UDP "connection" */
    struct sockaddr_in6 request_addr; /* udp: Who sent the most recent request */
    socklen_t request_addr_size;
    unsigned char *hdrbuf; /* udp packet headers */
    int    hdrsize;   /* number of headers' worth of space is allocated */

    bool   noreply;   /* True if the reply should not be sent. */
    /* current stats command */
    struct {
        char *buffer;
        size_t size;
        size_t offset;
    } stats;

    /* Binary protocol stuff */
    /* This is where the binary header goes */
    protocol_binary_request_header binary_header;
    uint64_t cas; /* the cas to return */
    short cmd; /* current command being processed */
    int opaque;
    int keylen;
    conn   *next;     /* Used for generating a list of conn structures */
    LIBEVENT_THREAD *thread; /* Pointer to the thread object serving this connection */
    int (*try_read_command)(conn *c); /* pointer for top level input parser */
    ssize_t (*read)(conn  *c, void *buf, size_t count);
    ssize_t (*sendmsg)(conn *c, struct msghdr *msg, int flags);
    ssize_t (*write)(conn *c, void *buf, size_t count);
};

memcached源码分析三-libevent与命令解析_第2张图片

图2-1 struct conn的状态转移图

  现在结合struct conn定义与状态转移图,对一个简单的客户端命令开始到客户端收到返回消息的过程进行描述,

  1. 首先,main_base中的listening socket对应的连接的conn_states为conn_listening.
  2. 当新的连接到达后,drive_machine调用accept函数接受新的连接,它(conns[id])被分发给工作线程,并处于conn_new_cmd的状态
  3. 处于conn_new_cmd状态的conns[id]检查它的成员rbytes,查看缓存rbuf中是否有数据,无数据则进入conn_waiting状态.
  4. 处于conn_waiting状态的conns[id]的成员event在event_base中等待触发,令conns[id]的下一个状态为conn_read
  5. conns[id]的读事件触发,处于conn_read状态的它从网络上读数据存储conns[id].rbuf中,并进入conn_parse_cmd状态
  6. 处于conn_parse_cmd状态的conns[id]从conns[id].buf中解析命令头,以binary protocol为例,解析出来的头部信息放入conns[id]. binary_header中,包含了key的长度,extended信息的长度,body的长度,命令等关键信息,这些工作调用conns. try_read_command函数完成。
  7. try_read_command根据解析的命令做不同的处理:信息已经足够,不需要读取更多信息,直接处理然后返回结果,如查询版本的命令、noop命令等,处理后进入conn_mwrite状态返回结果,然后再次进入conn_new_cmd状态;命令还需要读取更多信息,那么首先读取extras部分的信息,长度由conns[id].binary_header. extlen与conns[id].binary_header. keylen决定,这个长度信息放入conns[id].rlbytes中,存放数据的buf起始点记录在conns[id].ritem中,进入conn_nread状态(调用bin_read_key函数完成)。
  8. 处于conn_nread状态的conns[id],或者它的rbuf中已经有需要的rlbytes数据,或者仍然需要从网络上读取数据放入ritem起始的位置(此时ritem指向rbuf可用空间),数据足够后调用complete_nread函数进行处理。
  9. 如果前一次conn_nread读取的是extras部分的信息,complete_nread会根据命令类型进行处理:不需要数据信息,如delete命令、touch命令,处理后进入conn_mwrite状态返回结果,然后进入conn_new_cmd状态;需要继续读取数据信息,如add、set命令,首先它会根据binary_head中的body长度计算出需要的存储空间,预分配item进行存储,即conns[id].item,令conns[id].ritem指向conns[id].item的数据区,conns[id].rlbytes记录需要读取的数据长度,再次进入conn_nread状态。
  10. 再次进入conn_nread状态的conns[id]读取完数据后,调用complete_nread进行处理,将item加入到hashtable与lru的链表上,完成存储。进入conn_new_cmd状态。

conn_mwrite状态下,drive_machine会向连接的客户端发送消息(调用transmit函数),如果顺利完成则进入下一次的conn_new_cmd状态。

conn_write状态表示要发送的数据在wbuf中,需要先加入到msglist中,再进入conn_mwrite状态。

如果处理过程中出现了错误,导致连接发生混乱或者连接中断了,那么进入conn_closing,关闭连接。

memcached中向客户端返回消息使用了库函数sendmsg,因此需要返回给客户端的消息都存储在结构struct conn中的msglist中,其存储结构可以大致表示如图2-2:

memcached源码分析三-libevent与命令解析_第3张图片

图2-2 struct conn结构中msglist的结构

msglist是一个动态扩展的msghdr数组, msgused是数组中已被使用的部分的计数, msgsize是数组长度,msgcurr是目前发送的消息。

iov也是一个动态扩展的iovec数组,iovsize表示数据长度,iovused表示数组中已使用的部分的计数。

buf是存储需要发送的数据的空间,它的起始地址与长度记录在一个iovec结构中,这些buf一般是con中的wbuf, write_and_free,ilist中的item的数据区。

struct conn结构中有一个substate成员对于complete_nread处理binary protocol很重要,complete根据该值决定后续需要做的处理。substate的取值范围定义如下:

enum bin_substates {
    bin_no_state,
    bin_reading_set_header,
    bin_reading_cas_header,
    bin_read_set_value,
    bin_reading_get_key,
    bin_reading_stat,
    bin_reading_del_header,
    bin_reading_incr_header,
    bin_read_flush_exptime,
    bin_reading_sasl_auth,
    bin_reading_sasl_auth_data,
    bin_reading_touch_key,
};

这些状态是与binary protocol的命令对应的,如

set命令,substate首先会设置为bin_reading_set_header状态,表示conns[id]在第一次conn_nread状态读取了它需要的额外信息,但它还有data需要读取,需要预分配conns[id].item,后续的conn_nread状态读数据直接读入item中。然后它会设置为bin_read_set_value状态,表示第二次conn_nread读取了data数据并已存入item中,需要将该item放入hashtable与lru链表中。

delete命令,substate会设置为bin_reading_del_header状态,它仅需要一次conn_nread状态读取所需要的额外信息,然后从hashtable与lru链表中删除item,返回删除结果。

struct conn中部分其它成员的作用

ilist是一个指向item的指针的链表,存储的gets命令需要返回的item的指针,它也是一个动态扩展的数据,因为text协议的gets命令可能请求多个缓存对象。

protocol代表连接使用的协议类型,text或者binary

transport代表UDP或者TCP传输层协议

thread存储这个连接归属的工作线程的指针

suffixlist是一个char指针的动态扩展数组,存储着客户端的flags的字符串形式。

3.部分源码函数功能说明

void event_handler(const int fd, const short which, void *arg) 

网络事件的回调函数,事实上drive_machine才是核心函数,event_handler简单地调用了drive_machine。

static void drive_machine(conn *c) 

根据c->state的值进行不同的处理。

void dispatch_conn_new(int sfd, enum conn_states init_state, int event_flags,
                       int read_buffer_size, enum network_transport transport, void *ssl) 

conn_listening状态的conn在接受到新的连接后,调用该函数构造一个conn结构,并将其分发给工作线程处理。

static enum try_read_result try_read_network(conn *c)

conn_read状态的连接c调用该函数从网络上读取数据。

static int try_read_command_binary(conn *c) 

在将数据读到c.rbuf后,调用该函数对binary protocol的命令进行解析,解析结果存储c.binary_head中。

static void dispatch_bin_command(conn *c)

在try_read_command_binary中调用,根据命令执行不同的操作:命令不需要额外数据了,处理并返回结果;命令仍需要数据,调用bin_read_key设置好c.ritem、c.rlbytes及c.substate,函数返回,进入conn_nread状态。

static void complete_nread_binary(conn *c) 

binaray命令需要的extras数据已经放到了缓冲区,根据c->substate决定完成命令并返回或者继续读data部分。

static int try_read_command_ascii(conn *c) 

text协议使用的解析命令的函数,实际上调用process_command完成工作。

static void process_command(conn *c, char *command) 

根据命令行中的命令关键字进行相应的处理,如get命令调用process_get_command;add/set命令调用process_update_command;incr/decr调用process_arithmetic_command…

static enum transmit_result transmit(conn *c) 

将连接的c->msglist中的消息发送回客户端。核心操作为库函数sendmsg。

你可能感兴趣的:(memcached源码分析三-libevent与命令解析)