2018.6.14
讲解Memcached如何从客户端读取命令,并且解析命令,然后处理命令并且向客户端回应消息。其中,Memcached是通过sendmsg函数向客户端发送数据的,这里我们具体分析Memcached回应消息的技术细节。
右边对应了响应客户端需要准备好发送的数据结构,值得注意的是每次发送的是一个msghrd的结构体指向的缓存空间。这是一种多缓冲技术,也就是和之前我们说的发送缓冲区是不一样的概念。
传统的发送缓冲区,就是一个缓冲区,一次性将缓冲区的数据都发送出去,一般需要预设置一段大的buff,这样的效率很低,有可能缓冲区也会满,导致数据的丢失。所以unix提出了一种新的iovec多缓冲区的技术,具体如下:
I/O vector,与readv和wirtev操作相关的结构体。readv和writev函数用于在一次函数调用中读、写多个非连续缓冲区。有时也将这两个函数称为散布读(scatter read)和聚集写(gather write)。
顾名思义,就是可以指定多个缓冲区进行一次性发送,这样的好处是,解放了缓冲区预设的限制,同时减少了系统的调用。
和以前方法比的优缺点:
通常的情况下,程序可能会在多个地方产生不同的buffer,如 nginx,第一个phase里都可能会产生buffer,放进一个chain里,
如果对每个buffer调用一次send,系统调用的个数将直接等于buffer的个数,对于多buffer的情况会很糟。
可能大家会想到重新分配一个大的buffer, 再把数据全部填充进去,这样其实只用了一次系统调用了。又或者在一开始就预先分配一块足够大的内存。
这两种情况是能满足要求,不过都不足取,前一种会浪费内存,后一种方法对phase的独立性有影响。
那么在memcached中是如何利用这种多缓冲技术的呢?
Memcached消息回应源码分析
数据结构
我们继续看一下conn这个结构。conn结构我们上一期说过,主要是存储单个客户端的连接详情信息。每一个客户端连接到Memcached都会有这么一个数据结构。
1. typedef struct conn conn;
2. struct conn {
3. //....
4. /* data for the mwrite state */
5. //iov主要存储iov的数据结构
6. //iov数据结构会在conn_new中初始化,初始化的时候,系统会分配400个iovec的结构,最高水位600个
7. struct iovec *iov;
8. //iov的长度
9. int iovsize; /* number of elements allocated in iov[] */
10. //iovused 这个主要记录iov使用了多少
11. int iovused; /* number of elements used in iov[] */
12.
13. //msglist主要存储msghdr的列表数据结构
14. //msglist数据结构在conn_new中初始化的时候,系统会分配10个结构
15. struct msghdr *msglist;
16. //msglist的长度,初始化为10个,最高水位100,不够用的时候会realloc,每次扩容都会扩容一倍
17. int msgsize; /* number of elements allocated in msglist[] */
18. //msglist已经使用的长度
19. int msgused; /* number of elements used in msglist[] */
20. //这个参数主要帮助记录那些msglist已经发送过了,哪些没有发送过。
21. int msgcurr; /* element in msglist[] being transmitted now */
22. int msgbytes; /* number of bytes in current msg */
23. }
我们可以看一下conn_new这个方法,这个方法应该在第一章节的时候讲到过。这边主要看一下iov和msglist两个参数初始化的过程。
1. conn *conn_new(const int sfd, enum conn_states init_state,
2. const int event_flags, const int read_buffer_size,
3. enum network_transport transport, struct event_base *base) {
4. //...
5. c->rbuf = c->wbuf = 0;
6. c->ilist = 0;
7. c->suffixlist = 0;
8. c->iov = 0;
9. c->msglist = 0;
10. c->hdrbuf = 0;
11.
12. c->rsize = read_buffer_size;
13. c->wsize = DATA_BUFFER_SIZE;
14. c->isize = ITEM_LIST_INITIAL;
15. c->suffixsize = SUFFIX_LIST_INITIAL;
16. c->iovsize = IOV_LIST_INITIAL; //初始化400
17. c->msgsize = MSG_LIST_INITIAL; //初始化10
18. c->hdrsize = 0;
19.
20. c->rbuf = (char *) malloc((size_t) c->rsize);
21. c->wbuf = (char *) malloc((size_t) c->wsize);
22. c->ilist = (item **) malloc(sizeof(item *) * c->isize);
23. c->suffixlist = (char **) malloc(sizeof(char *) * c->suffixsize);
24. c->iov = (struct iovec *) malloc(sizeof(struct iovec) * c->iovsize); //初始化iov
25. c->msglist = (struct msghdr *) malloc(
26. sizeof(struct msghdr) * c->msgsize); //初始化msglist
27. //...
28. }
数据结构关系图(iov和msglist之间的关系):
从图上看,memcached每次发送给客户端都是一个msglist中的msghdr元素,这个元素中包含了不同缓冲区的数据,例如c->iov中的数据1 2 3 4,注意这里1 2 3 4不是连续的数据,指向各个数据所在位置的指针。由于一个命令可以得到多个返回数据,所以一个命令可能携带有多个iov,所有一个命令的全部返回结果可能包含多个msghdr。
memcached发送客户端的消息响应的流程如下:
思考与分析:
这里我们可以学习到的是,memcached利用的多缓冲技术,其实发送缓冲区的数据很简单,但是对于这种多数据的存取,高并发的存取,使用传统的send不停的调用,或者是内存的拷贝显然是会影响效率,memcached采用的这种技术很值得学习。