Chaos网络库(二)- Buffer的设计

对于buffer的设计,chaos和其他网络库的做法稍有不同,就拿libevent-stable-1.4.13(以下所提到libevent处皆为该版本)来说,它采用一种能够自动扩张的buffer,基本策略如下:

1. 当缓冲区不够存放新数据时,它会先在内部做marshal,看看是否能够腾挪出足够的空间

2. 假如没有满足,那么对buffer进行expand, 大小为原来的两倍,扩张之后该buffer就不会缩小

另外,libevent中有两个buffer,input 和 output, 分别代表读缓冲和写缓冲,libevent对他们扩张时的策略稍有不同

对于input,libevent限定它最大的缓冲大小为4096,这对于现在的网络环境,尤其是内网环境肯定是不太够的

对于output,libevent没有限定大小

这样做在普通的稳定传输下不会有什么问题,但是假如上层将很大的一块数据块(1MB以上)放进output,或者是上层快速地将小块数据放入output,而底层的IO复用的线程由于某些情况没有来得及响应,都会导致output增长到非常大的程度,而由于buffer只能伸不能缩的性质,在之后的传输过程中内存使用率就会很低

考虑这样一个场景,一个应用在接收一个新连接之后,会首先发送一大块的数据(2MB)给对端进行初始化,而上层又没有对这2MB的数据进行分块发送,直接放进了output中,output被直接撑大到2MB(应该说起码2MB),这当然没问题,传输一样可以顺利完成,但在数据初始化完毕后,该应用持续发送的都是小包,那么这2MB多的output就被浪费了

 

而对于input,情况会稍微好些,但也存在一些问题,倘若上层不从input中读取数据而让input一直保持“满”状态,那么libevent也就不会从socket中读取数据到input中,假设我们在linux平台上使用epoll模型,对于LT模式,我们将会发生每次epoll_wait返回都会存在该socket,对于ET模式,我们需要对socket一直读到EAGAIN或者EWOULDBLOCK标志,但如果读的中途input满了,尴尬的情况同样会发生

那么如何解决这种既能满足外部无论数据多大都能放入的需求,同时又能提高buffer的内存使用率?

Chaos网络库设计了一种buffer list,可以较好的解决这个问题

解决方案 -

首先要让外部感受不到buffer“满”的状态,这将导致一旦有超出剩余空间的数据要存放,buffer就必须分配内存,但是如果buffer是连续性的,我们又很难去回收那些不用的内存来提高内存使用率,毕竟要缩小一块连续的内存,唯一的方法就是将原有数据拷贝到另一块较小的内存,然后释放原来的内存,这当然不被推荐,但假设我们将一块一块的buffer串联在一起,形成一张buffer的链表,是否能够有效地解决?

我们来看个场景 -

buffer list(以下简称BL)的初始状态为只有一块expand buffer(基本同libevent的buffer设计),上层不断地向BL中投入数据,且这时没有任何读取方从BL中取走数据,这时,BL中的那块buffer被填满了,但上层仍然在投入数据,接下来的数据我们不会再去扩张原来那块buffer,而是重新产生一个新的buffer,作为BL的的第二个节点放入链表尾,新的数据就能放入这块新的buffer中,如果新的数据非常之大,足以是单块buffer的n倍,那么BL就会产生n个buffer来存放数据。

记住,我们这样做的目的是为了既能满足任何大小的数据存入,也能保证一定的内存使用率,上面我们展示了BL如何支持“无限大”的空间,那么现在我们来看如何保证内存使用率

链表这种数据结构的增删性很强,且每个节点都是独立的,可以单个回收,这就解决了一整块连续buffer无法回收部分内存从而导致使用率低的问题

当读取方开始从BL中取走数据时,从链表头开始,第一块buffer的数据全部都取走之后,这块buffer所占用的内存就被释放了,然后读取方不停地取走数据,直到BL中最后一块buffer(注意:最后一块buffer不会被释放)

这样设计之后,我们来重新看看上面提到场景

1. 接收一个连接,发送2MB以上数据给对端进行初始化,BL中有多块buffer存在(假设每块buffer大小为8k),BL所占用总大小>=2MB

2. 网络层开始从BL中取出数据发送给对端,每当一块buffer发送完毕,就从BL中释放该buffer节点,至最后只有一块buffer

3. 连接进入传输平稳阶段,且数据包都是小包(小于单块buffer大小),连接断开之前BL所占用大小一直保持8k

好了,这个问题是解决了,但你也许会说,buffer的连续性的优势在于能够只调用一次send就将buffer上所有的数据传输到对端(更严谨的说法是拷贝到内核缓冲区),如果是BL结构的话,就有可能会调用多次send,影响整体性能

我的回答是:

BL设计解决的问题是在连接的生命周期中,有少量的大块数据传输导致的buffer增大,而绝大多部分时间是小包发送

倘若是持续在进行大数据量的传输,那么你可以动态低将单块buffer得最大上限设得尽可能大,在传输过程中即使BL有多个buffer节点,会调用多次send,但也不会成为瓶颈导致性能下降

 

数据移动的优化

当要在一块buffer中加入数据但末端剩余空间不足时,buffer内部会首先检查marshal后是否能满足(将已被读取的数据覆盖),这里有一个问题就是,假如我们的buffer很大,可能设置成了1MB,且buffer中的数据已经填满了数据,这时读取方从buffer中读取出10个字节,之后又希望向buffer中存入10个字节,但这时buffer尾段已经没有空余了,虽然头部有10个字节的空余正好可以满足,但我们需要先进行marshal操作,也就是将近1MB的数据进行内存移动,这是比较耗时的操作,而现在我们有了BL设计,我们就可以这么来做,倘若本次操作可能会造成大量(多大可以自己设定)的内存移动,那么就新增一个BL buffer节点,将数据放入,毕竟现在的内存池算法在分配这么一个BL buffer节点时,几乎只是做了一个指针运算

 

单个数据包跨buffer存放

有可能会发生这种情况,就是当我们从socket中读取数据时,一个数据包被分成两部分(或者n部分),存放于BL中的多个连续的buffer,这时候我们需要将不连续的n块buffer中的数据拼接起来,组成一个完整的包,再传递给逻辑层,chaos对于如何拼接数据的处理没有放在BL中来做,因为BL只单纯地做好它工作,换句话说它根本没法区分数据的内容,这部分工作交给上层的conn_strategy来做,这里需要一提的是,当一个数据包在单块buffer中,chaos会直接零拷贝(zero copy)给逻辑层

 

关于chaos网络库buffer的设计就写到这吧,buffer list的完整代码,大家都可以在

https://github.com/lyjdamzwf/chaos/tree/master/chaos/network/buffer_list.h

https://github.com/lyjdamzwf/chaos/tree/master/chaos/network/buffer_list.cpp

中查看

 

我的个人博客地址:www.cppthinker.com

欢迎大家交流:)

 

posted @ 2012-12-11 14:29  Zark  阅读(702)  评论(0编辑  收藏  举报