IT技术博客大学习 共学习 共进步

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

MySQLOPS 数据库与运维自动化技术分享 2012-08-14 13:57:15 浏览 2,142 次

对于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

中查看

建议继续学习

  1. Buffer和cache的区别是什么? (阅读 7,841)
  2. Linux操作系统中内存buffer和cache的区别 (阅读 6,341)
  3. 快速预热Innodb Buffer Pool的方法 (阅读 4,983)
  4. MySQL数据库InnoDB存储引擎 Buffer pool LRU List Flush策略详解 (阅读 4,923)
  5. InnoDB之Dirty Page、Redo log (阅读 4,482)
  6. MySQL数据库InnoDB存储引擎 Insert Buffer实现机制详解 (阅读 4,383)
  7. 小心grep 的buffer (阅读 4,103)
  8. grep 命令的buffer选项 (阅读 3,964)
  9. Chaos网络事件库开篇介绍(一) (阅读 3,903)
  10. HBase如何合理设置客户端Write Buffer (阅读 3,722)