memcached源码分析-----set命令处理流程

本文详细分析了memcached处理set命令的过程,包括读取命令、解析命令、执行命令和存储item等步骤。在读取命令时,介绍了worker线程如何避免因大量命令而阻塞的问题。在解析命令阶段,解释了memcached如何识别和符号化命令内容。执行命令阶段涉及item的分配和数据填充。最后,文章讨论了存储item和回应客户端的操作。
摘要由CSDN通过智能技术生成


        转载请注明出处:http://blog.csdn.net/luotuo44/article/details/44236591



        前一篇博文以get命令为例子把整个处理流程简单讲述了一遍,本篇博文将以set命令详细讲述memcached的处理流程。具体的命令为“set tt 3 0 10”,并假设当然memcached服务器没有名为tt的item。



读取命令:

        在前一篇博文的最后,conn的状态被设置为conn_new_cmd,回到了一开始的状态。如果此时conn结构体里面的buff还有其他命令,或者该客户端的socket缓冲区里面还有数据(命令),那么就会继续处理命令而不会退出drive_machine函数。处理完后,又会回到conn_new_cmd状态。

      《半同步半异步网络模型》指明了memcached是通过worker线程执行客户端的命令,并且一个worker线程要处理多个客户端的命令。如果某一个恶意的客户端发送了大量的get命令,那么worker线程将不断地重复前一篇博文讲述的处理流程。换言之,worker线程将困死在drive_machine里面不能出来。这造成的后果是导致该worker线程负责的其他客户端处于饥饿状态,因为它们的命令得不到处理(要退出drive_machine才能知道其他客户端也发送了命令,进而进行处理)。

        为了避免客户端发现饥饿现象,memcached的解决方法是:worker线程连续处理某一个客户端的命令数不能超过一个特定值。这个特定值由全局变量settings.reqs_per_event确定(默认值是20), 可以在启动memcached的时候通过命令行参数设置,具体参考《memcached启动参数详解以及关键配置的默认值》。


static void drive_machine(conn *c) {
    bool stop = false;
    int nreqs = settings.reqs_per_event;//20

    assert(c != NULL);

	//drive_machine被调用会进行状态判断,并进行一些处理。但也可能发生状态的转换
	//此时就需要一个循环,当进行状态转换时,也能处理
    while (!stop) {

        switch(c->state) {

        case conn_new_cmd:
			
            --nreqs;
            if (nreqs >= 0) {
				//如果该conn的读缓冲区没有数据,那么将状态改成conn_waiting
				//如果该conn的读缓冲区有数据,  那么将状态改成conn_pase_cmd
                reset_cmd_handler(c);
            } else {

                if (c->rbytes > 0) {
                    /* We have already read in data into the input buffer,
                       so libevent will most likely not signal read events
                       on the socket (unless more data is available. As a
                       hack we should just put in a request to write data,
                       because that should be possible ;-)
                    */
                    if (!update_event(c, EV_WRITE | EV_PERSIST)) {
                        if (settings.verbose > 0)
                            fprintf(stderr, "Couldn't update event\n");
                        conn_set_state(c, conn_closing);
                        break;
                    }
                }
                stop = true;
            }
            break;

        }
    }

    return;
}

        从上面代码可以得知,如果某个客户端的命令数过多,会被memcached强制退出drive_mahcine。如果该客户端的socket里面还有数据并且是libevent是水平触发的,那么libevent会自动触发事件,能再次进入drive_mahcine函数。但如果该客户端的命令都读进conn结构体的读缓冲区,那么就必须等到客户端再次发送命令,libevent才会触发。但客户端一直不再发送命令了呢?为了解决这个问题,memcached采用了一种很巧妙的处理方法:为这个客户端socket设置可写事件。除非客户端socket的写缓冲区已满,否则libevent都会为这个客户端触发事件。事件一触发,那么worker线程就会进入drive_machine函数处理这个客户端的命令。

 

 

        当然我们假设nreqs大于0,然后看一下reset_cmd_handler函数。该函数会判断conn的读缓冲区是否还有数据。此外,该函数还有一个重要的作用:调节conn缓冲区的大小。前一篇博文已经说到,memcached会尽可能把客户端socket里面的数据读入conn的读缓冲区,这种特性会撑大conn的读缓冲区。除了读缓冲区,用于回写数据的iovec和msghdr数组也会被撑大,这也要收缩。因为是在处理完一条命令后才进行的收缩,所以收缩不会导致数据的丢失。

        写缓冲区呢?不需要收缩写缓冲区吗,conn结构体也是有写缓冲区的啊?这是因为写缓冲区不会被撑大。从前一篇博文的回应命令可以知道,回应命令时并没有使用到写缓冲区。写缓冲区是在向客户端返回错误信息时才会用到的,而错误信息不会太大,也就不会撑大写缓冲区了。

struct conn {
    int    sfd;//该conn对应的socket fd
    sasl_conn_t *sasl_conn;
    bool authenticated;
    enum conn_states  state;//当前状态
    enum bin_substates substate;
    rel_time_t last_cmd_time;
    struct event event;//该conn对应的event
    short  ev_flags;//event当前监听的事件类型
    short  which;   /** which events were just triggered */ //触发event回调函数的原因

	//读缓冲区
    char   *rbuf;   /** buffer to read commands into */ 
	//有效数据的开始位置。从rbuf到rcurr之间的数据是已经处理的了,变成无效数据了
    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 */
	//ensure_iov_space函数会扩大数组长度.下面的msglist数组所使用到的
	//iovec结构体数组就是iov指针所指向的。所以当调用ensure_iov_space
	//分配新的iovec数组后,需要重新调整msglist数组元素的值。这个调整
	//也是在ensure_iov_space函数里面完成的
    struct iovec *iov;//iovec数组指针
	//数组大小
    int    iovsize;   /* number of elements allocated in iov[] */
	//已经使用的数组元素个数
    int    iovused;   /* number of elements used in iov[] */

	//因为msghdr结构体里面的iovec结构体数组长度是有限制的。所以为了能
	//传输更多的数据,只能增加msghdr结构体的个数.add_msghdr函数负责增加
    struct msghdr *msglist;//msghdr数组指针
	//数组大小
    int    msgsize;   /* number of elements allocated in msglist[] */
	//已经使用了的msghdr元素个数
    int    msgused;   /* number of elements used in msglist[] */
	//正在用sendmsg函数传输msghdr数组中的哪一个元素
    int    msgcurr;   /* element in msglist[] being transmitted now */
    //msgcurr指向的msghdr总共有多少个字节
	int    msgbytes;  /* number of bytes in current msg */

	//worker线程需要占有这个item,直至把item的数据都写回给客户端了
	//故需要一个item指针数组记录本conn占有的item
    item   **ilist;   /* list of items to write out */
    int    isize;//数组的大小
    item   **icurr;//当前使用到的item(在释放占用item时会用到)
    int    ileft;//ilist数组中有多少个item需要释放

    enum protocol protocol;   /* which protocol this connection speaks */
    enum network_transport transport; /* what transport is used by this connection */

    bool   noreply;   /* True if the reply should not be sent. */
    /* current stats command */

	...

    conn   *next;     /* Used for generating a list of conn structures */
	
    LIBEVENT_THREAD *thread;//这个conn属于哪个worker线程
};





static void reset_cmd_handler(conn *c) {
    c->cmd = -1;
    c->substate = bin_no_state;
    if(c->item != NULL) {//conn_new_cmd状态下,item为NULL
        item_remove(c->item);
        c->item = NULL;
    }
    conn_shrink(c);
    if (c->rbytes > 0) {//读缓冲区里面有数据
        conn_set_state(c, conn_parse_cmd);//接着去解析读到的数据
    } else {
        conn_set_state(c, conn_waiting);//否则等待数据的到来
    }
}



#define DATA_BUFFER_SIZE 2048

/** Initial size of list of items being returned by "get". */
#define ITEM_LIST_INITIAL 200

/** Initial size of list of CAS suffixes appended to "gets" lines. */
#define SUFFIX_LIST_INITIAL 20

/** Initial size of the sendmsg() scatter/gather array. */
#define IOV_LIST_INITIAL 400

/** Initial number of sendmsg() argument structures to allocate. */
#define MSG_LIST_INITIAL 10

/** High water marks for buffer shrinking */
#define READ_BUFFER_HIGHWAT 8192
#define ITEM_LIST_HIGHWAT 400
#define IOV_LIST_HIGHWAT 600
#define MSG_LIST_HIGHWAT 100



 //收缩到初始大小
static void conn_shrink(conn *c) {
    assert(c != NULL);

    if (IS_UDP(c->transport))
        return;

	//c->rbytes指明了当前读缓冲区有效数据的长度。当其小于DATA_BUFFER_SIZE
	//才进行读缓冲区收缩,所以不会导致客户端命令数据的丢失。
    if (c->rsize > READ_BUFFER_HIGHWAT && c->rbytes < DATA_BUFFER_SIZE) {
        char *newbuf;

        if (c->rcurr != c->rbuf)
            memmove(c->rbuf, c->rcurr, (size_t)c->rbytes);

        newbuf = (char *)realloc((void *)c->rbuf, DATA_BUFFER_SIZE);

        if (newbuf) {
            c->rbuf = newbuf;
            c->rsize = DATA_BUFFER_SIZE;
        }
        /* TODO check other branch... */
        c->rcurr = c->rbuf;
    }

    if (c->isize > ITEM_LIST_HIGHWAT) {
        item **newbuf = (item**) realloc((void *)c->ilist, ITEM_LIST_INITIAL * sizeof(c->ilist[0]));
        if (newbuf) {
            c->ilist = newbuf;
            c->isize = ITEM_LIST_INITIAL;
        }
    /* TODO check error condition? */
    }

    if (c->msgsize > MSG_LIST_HIGHWAT) {
        struct msghdr *newbuf = (struct msghdr *) realloc((void *)c->msglist, MSG_LIST_INITIAL * sizeof(c->msglist[0]));
        if (newbuf) {
            c->msglist = newbuf;
            c->msgsize = MSG_LIST_INITIAL;
        }
    /* TODO check error condition? */
    }

    if (c->iovsize > IOV_LIST_HIGHWAT) {
        struct iovec *newbuf = (struct iovec *) realloc((void *)c->iov, IOV_LIST_INITIAL * sizeof(c->iov[0]));
        if (newbuf) {
            c->iov = newbuf;
            c->iovsize = IOV_LIST_INITIAL;
        }
    /* TODO check return value */
    }
}


读取数据:

        我们假设conn的读缓冲区里面没有数据,此时conn的状态被设置为conn_waiting,等待客户端发送命令数据。如果客户端发送数据过来,libevent将检测到客户端socket变成可读,然后进入在libevent的回调函数中调用drive_machine函数,进入有限状态机。在有限状态机里面,conn的状态会被设置为conn_read。接着在conn_rea

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值