转载请注明出处: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