origin: http://blog.codingnow.com/2012/02/ring_buffer.html

这是一篇命题作文,源于今天在微薄上的一系列讨(好吧,也可以说是吵架)。其实方案没有太多好坏,就看你信不信这样做能好一些或坏一些。那么,整理成 blog 写出,也就是供大家开拓思路了。

我理解的需求来源于网络服务提供程序的一个普遍场景:一个服务器程序可能会收到多个客户端的网络数据流,在每个数据流上实际上有多个独立的数据包,只有一个数据包接收完整了才能做进一步的处理。如果在一个网络连接上数据包并不完整,就需要暂时缓存住尚未接收完的数据包。

问题是:如何管理这些缓冲区比较简洁明了,且性能高效。

其实这个有许多解决方案,比如为每个网络连接开一个单独的固定长度的 buffer 。或是用 memory pool 等改善内存使用率以及动态内存分配释放,等等。今天在微薄上吵架也正是在于这些方案细节上,到底好与不好,性能到底如何。既然单开一篇 blog 了,我不像再谈任何有争议的细节,仅仅说说,用 Ring Buffer 如何解决这个问题。


具体点说,我倾向于用 C 语言来做这种偏底层、业务简单的模块。为了减低工作量,可以使用一些成熟的库,比如 libev 等。

类似的库多半提供的是一种回调机制的框架,设置好对应的 IO 异步请求的 callback 函数,然后启动框架的主循环,每个 socket (或别的句柄)可读写时,回调注册好的函数。

把这件事情做的干净漂亮的关键点之一在于数据缓冲区的管理。

拿到需求后,我们应该适当估算我们的程序需要解决多大的数据吞吐量。比如,我们可以假设,一个逻辑完整的数据包在 TCP 连接上,可能最长大约会经过一两秒时间,通过 1 到 10 个包发送过来。整个系统每秒会处理 100M 字节(千兆网)的数据流,那么大约在 10 秒内,处理的数据量大约就在 1G 。

根据对实际业务的估算,这个值可能不到 1G ,128M 就够,也可能多达几个 G 。没关系。我们只是估算出,大致在这个范围内,一个独立的逻辑包一定存在于整个数据流的截段中。我指的数据流是服务程序从网卡上读到的所有数据。

就以 1G 为例,那么这个服务程序只需要开单个这么大的 buffer 就足够了,不必再有任何的动态内存管理。

我们把所有的数据,不论它来至哪个 TCP 连接,都以循环队列的方式,无差别的循序置入这个 buffer。放置的时候,以每次 IO 可读时可以读入的最大字节长度为限。一旦放不下,就折返到 buffer 头部。

buffer 里大概的数据结构是这样的:

[数据长度 连接号 下一块的位置 数据] [数据长度 连接号 下一块的位置 数据] [数据长度 连接号 下一块的位置 数据] ...

另外,内存里开一张 hash 表,记下连接号到数据块的映射关系。如果不想用 hash 表的话,也可以在 buffer 中直接记下连接对象在内存中的地址。

每当一个连接可读的时候,无论读到多少字节,都向这个 buffer 后面追加。并且用链表将其和历史上曾经读过的数据连起来。

同时,可以分析一个逻辑包是否完成。如果没有完成,则继续下面的工作。完成了的话,则利用已有的链表,将分离的数据块拼合在一块连续的内存上。之后,如何处理这个逻辑包,就不在是这个层次上的工作了。

对于处理掉的数据块,可以做一个记号表示废弃即可。我的做法是对数据长度段取反,这样 buffer 在循环使用时,可以判断出下面的内存空间是否可以安全使用。

处理完一个逻辑包后,有可能最后一块数据被切分出去。我的做法时调整这个块,前半块标记为废弃数据块,后半块为待处理数据块。

理论上,如果你的估算没有错且留有余量的话,每次新到来的数据包都能在 buffer 中找到储存它们的空间。因为根据估算,消费速度是要大于生产速度的。不然整个系统都跑不下去。

但如果碰到例外怎么办? 比如有个客户端半个逻辑包发来以后,迟迟不发下半个包。最简单的做法是,碰到 ring buffer 回卷后,碰到那些未废弃的数据块(尚未处理掉),索引到对应的连接,直接 close 掉连接,把没有处理的数据扔掉即可。因为在互联网上,连接本来就是不稳定的。你的协议原本就要处理主动断开连接的情况。无非是根据 ring buffer 的大小和当时的负载情况,设置了一个超时而已。

有兴趣的同学,可以用这个思路实现一下几年前我提到的连接服务器 。代码量应该不大。


另,在更高层的应用上,同样可以使用类似的策略。即循环使用一个 ring buffer 。当 buffer 回转时碰到有对象占用 buffer 拦路时,杀掉对象。对于一些对象比较复杂占用的数据段不固定,对象生命期很短的应用,ring buffer 都有参考价值。例如 3d engine 中的粒子系统。对于要个别需要长期生存的对象,还可以定期复制自己,重新压入 ring buffer 的方式来延长生命期。


使用 ring buffer 的优势是内存使用率很高,不会造成内存碎片,几乎没有浪费(比如传统动态内存分配需要的 cookie)。业务处理的同一时间,访问的内存数据段集中。可以更好的适应不同系统,取得较高的性能。内存的物理布局简单单一,不太容易发生内存越界、悬空指针等 bug ,出了问题也容易在内存级别分析调试。做出来的系统容易保持健壮。


2 月 3 日 补充:

读了几位同学的反馈,发现在几个地方没讲清楚,造成许多疑惑。

一、从 ring buffer 里切分出去的数据块是可以任意合并或再切分的。数据块头上的数据块长度正是为了找到下一块的位置,可以把邻接的两个被标记(废弃)的块合成一个;为了可以任意切分,这里数值上制定的长度和实际占用的长度可能略微不等。实际占用的长度是 4 字节对齐的,而记录的长度则是真实值。比如,记录长度为 6 ,那么在推算下一块位置的时候,需要先圆整到 8 。这样做的话,可以让数据块长度至少为 4 。我们用 4 字节记录长度信息的话,就足够了。更细节的位置还有,我们需要利用连接号域做一个标记,比如空出一个不存在的连接号,表示这个块废弃且无关联连接。实际上,ring buffer 初始化时,就是把整个空间初始化为一个块,并打上可用(废弃)标记。使用的时候就从这个块中一分为二,取其一而用。

经过网友提醒, 这里还需要多设置一个单字节的 offset 域, 记录未处理的数据块头部偏移。

二、ring buffer 中切分出去的块的分配过程是顺序依次进行的,回收的次序则是随机的。初看起来这会导致使用一段时间后,buffer 里有很多空洞。但是, ring buffer 不是一个 memory pool ,并不做复杂的内存块管理工作。如果你想那样做的话,不如直接使用 malloc/free 。也不用为预测的数据量留大一个数量级的空间。ring buffer 在使用时,留足了空间和时间,当 buffer 回转,头指针继续从低地址位移动时,那些先前分配出去的数据块应该都被打上废弃标记了。我们要做的只是根据需要,把这些连续废弃块合成我们需要的程度。万一碰到中间有个别继续被占用的数据块怎么办?那就是上文中提到的,根据连接号强行回收的工作了。

云风 提交于 February 2, 2012 10:20 PM | 固定链接

COMMENTS

ring buffer里面可以放packet对象指针,这样就避免了内存拷贝。这个方案有个陷阱就是packet对象复用的时候内部buffer的realloc的问题,处理不好会消耗太多内存。微软的ADO就很痛快的踩到这个陷阱了。

Posted by: wsq | (23) July 28, 2014 01:38 PM

如有的同学说说,服务器这面最大的性能杀手是内存拷贝次数.你这个方案的问题,是没有照顾好网络数据接收层/逻辑处理层是如何利用这些内存的?按照我这面的使用方法:网络接受是为数几个线程管理的,逻辑处理都在主线程,那么,逻辑处理部分得提前把数据从Ring Buffer里拷贝出来,否则,多个线程操作同一个Ring Buffer带来的线程同步问题也很难严重.
经过多年的实践,我比较趋向于采用链表的方案来实现.每次接受的数据都是一块新分配的内存,并且,这块内存的大小是根据系统支持的最大数据接受大小/操作系统分页大小等因素确定的.因为很多网络底层在系统级别锁定内存的时候,需要按照操作系统分页大小获得物品内存地址,并锁定之,如果一个分页跨多个socket,会导致系统底层互锁降低效率.
在我的观察中,一段很小的数据,如20字节以下,几乎很少发生一次接受不全的问题,也就是说,其实很少发生一个数据包接受不全的问题.基本上都是几个完整的数据包累计在一起后,才被网络层接受.网络层接受到数据后,根据包协议分析完整数据包的长度,将完整的包保留,不完整的包拷贝到下一个接受缓冲里(很少发生).完整的包投入到已接受数据链表中,等待逻辑层一次全部处理.
逻辑层从链表里获取数据仅仅是一次LockedExchange操作(32位下利用Exchange64完成,64位下,利用自己写的Exchange128完成) ,将链表头尾给置换出来.无锁链表的节点添加利用CompareExchange完成(分别对应DCAS64,DCAS128).

Posted by: tearshark | (22) March 12, 2012 04:30 PM

有个简单的方法,内存池管理器,可以存储不同大小的内存块。。。。。比如一个链都是16K的块,另一个都是64K的块。。。。。大的数据,用大块的链,小的用小块。。。。。。

Posted by: 忠义哥 | (21) February 25, 2012 12:36 AM

通常网游业务处理性能的重要衡量指标是响应时间(它直接影响吞吐能力),这其中业务处理耗时占绝大部分比例,尤其是在有数据库访问或其他IO操作的应用中。
网络数据接收缓存机制,应该采用简单模型,并尽可能的减少内存拷贝次数就够了,拷贝数据近似FOR循环操作,多一次拷贝,复杂的优化就白费力气了,另外,更值得考虑的是,怎样让自己的设计随着硬件性能的提升表现出良好的线性关系。

Posted by: jessc | (20) February 13, 2012 11:16 PM

@老突发

如果有的网速快,数据高速发送,消费能力肯定很强能够消化的,越快越好,应该不会影响低速用户。

Posted by: punkydog | (19) February 9, 2012 01:54 PM

处理完一个逻辑包后,有可能最后一块数据被切分出去。我的做法时调整这个块,前半块标记为废弃数据块,后半块为待处理数据块。

大哥,你的意思是说, 最后一块包含了该连接上下个逻辑包的一部分,所以要隔开该块吗? 就这个看不明白什么意思。 麻烦云风大哥回答下

Posted by: zhanzenghui | (18) February 7, 2012 08:52 PM

有必要使用一大块内存缓冲所有链接的数据吗?一般来说,网游单个连接的下行流量在2KB/s左右,按照16秒的缓冲标准,就是32KB,按照每个服务进程处理4K个连接,大概是32MB。这个数量级上,似乎没必要为了提高内存利用率,而把所有数据放在一个大块里。

Posted by: Holimion | (17) February 6, 2012 10:53 AM

将分离的数据块拼合在一块连续的内存上.

抱歉再问个细节。 这个动作是在ring buff 里面做,还是把这些数据copy 到另一块内存里?

私以为会copy 走,因为这个消息应该进逻辑层。如果在ring buff 里面做
就会有移动内存的要求了,而且什么时候释放又是大问题,逻辑层不知道什么时候能用完这块内存。

如果是copy 走又有一个新问题,新开辟的内存的大小应该是和这个消息差不多大。当逻辑层用完再释放。 但是为了提高性能 这块新开内存 应该又要走上开内存池,反复使用的老路了。
不知是否如我所说呢?

Posted by: 老突发 | (16) February 6, 2012 10:10 AM

还有一个问题, 目前方案buff大小 影响了系统最长的处理时间,比如有一个高速用户,他有高速的数据传输,很可能造成不少普通的低速用户被断开。因为它高速的数据 迅速的将buff 进行了回环。(可以是攻击,也可以是大数据) 只要发生了回环,前面那些还在连接的socket 都会被断掉。

比较偏激,但是 如果是大规模用户可能会有些低速用户的,这个方案 还应该在每个buff 上加一个本消息接收的初始时间比较好,免得错杀。

Posted by: 老突发 | (15) February 6, 2012 10:02 AM

[数据长度 连接号 下一块的位置 数据]
有个冲突的问题 是怎么解决的
第一个块 读取了100 个字节
连接号 1
下块地址 应该是 112 ( 块本身用了12 个字节 + 100 字节数据)

那么112 这块 在 第一次读的时候 就会写 上么? 还是空
否则 下次读的时候 112 这个块可能已经被另一个连接用掉了
而且 不知道分配多少字节给112 这个块。

所以 下个位置这个字段 在此次读完后 应该是空的,由下次读取的时候 再来找空块 再来填写。

Posted by: 老突发 | (14) February 6, 2012 09:50 AM

这和我现在写的服务器想法一样,只是ring buffer做成链表,设置检测时间,定期清理死连

Posted by: Scott | (13) February 4, 2012 05:10 PM

还有几个不明白
假如一个逻辑包的后半部分到来以后,怎么样找到上半部分的逻辑包并且把相应的域指到自己,如果是用连接号hash的话那这个key对应的value是指什么呢,是上半个逻辑包的位置吗?但是这样的话变成每次都要都要更新value.假如value是数据包头部的话那么每次插入数据还要线性遍历到尾部?

Posted by: 冷锋 | (12) February 3, 2012 03:38 PM

很简单直白的做法,很容易想明白……

Posted by: 老赵 | (11) February 3, 2012 03:35 PM

刚开始我还想,云风怎么把东西设计复杂了?看完了才明白:好简洁高效啊!

Posted by: haxixi_keli | (10) February 3, 2012 12:47 PM

从整个环境着手得出的特定解决方案,很好参考价值!!

Posted by: guaniu | (9) February 3, 2012 11:12 AM

@grep

不连续的问题,看文末的补充。

链接可以实现成双向的,也可以如你所说,在第一块保存头尾块。

Posted by: Cloud | (8) February 3, 2012 10:34 AM

建议客户端配合,如果请求被close了,重新请求

Posted by: Anonymous | (7) February 3, 2012 10:05 AM

两处不是很明白:
1)如何管理废弃的slot?“对于处理掉的数据块,可以做一个记号表示废弃即可。我的做法是对数据长度段取反,这样 buffer 在循环使用时,可以判断出下面的内存空间是否可以安全使用...", 怎么做会在buffer中留下许多不连续的“空闲slot“
2) hash的数据结构, “另外,内存里开一张 hash 表,记下连接号到数据块的映射关系。如果不想用 hash 表的话,也可以在 buffer 中直接记下连接对象在内存中的地址...”, 感觉hash表中一个连接号需要指向一个未完成包的开始和结束段

Posted by: grep | (6) February 3, 2012 08:57 AM

ps:关于服务器设计的常见问题。大牛在很多业务场景中都应该已经碰到,将你的解决办法总结下来,嘿嘿。造福我们啊

Posted by: pizi | (5) February 3, 2012 01:08 AM

看来这个问题,很多人的解决办法都不相同。能不能多总结些这种常见问题但是没有大牛吐槽的问题啊?也能供我们多学习学习。

Posted by: pizi | (4) February 3, 2012 01:06 AM

memcached 里的evbuffer 就是个ring buffer

Posted by: hoterran | (3) February 2, 2012 10:48 PM

开源得好像没有这种实现吧,memcache得内存管理借鉴得是内核得slab内存分配,你这个还是只适用于特定场景; 你这个管理貌似也不简单 还复杂些;

Posted by: landy | (2) February 2, 2012 10:37 PM

ring buffer在网络编程各种语言很多场景下都有不同的应用和用法,一次性开块大内存是生产环境下的常用做法,还有例如算好数据然后查表等等各种以空间换效率的做法。
这些经验型的做法,几乎成为某些特定场景下的“模式”了,可惜书里很少能见到

Ring Buffer 的应用相关推荐

  1. SQL Server 环形缓冲区(Ring Buffer) -- 介绍

    SQL Server 环形缓冲区(Ring Buffer) -- 介绍 以下关于Ring Buffer的介绍转载自: http://zh.wikipedia.org/wiki/%E7%92%B0%E5 ...

  2. leetcode 622. Design Circular Queue | 622. 设计循环队列(Ring Buffer)

    题目 https://leetcode.com/problems/design-circular-queue/ 题解 Ring Buffer 的实现,rear 指向新插入的位置,front 指向最旧的 ...

  3. Java 环形缓冲器(Ring Buffer)

    环形缓冲器(Ring Buffer):环形队列,这里使用数组实现,但并未用上环形功能,因为设置了队满直接出队清空队列,如果只读取部分数据,又或者想要覆盖冲写,则可以用上环形功能 package cha ...

  4. SQL Server 环形缓冲区(Ring Buffer) -- 环形缓冲在AlwaysOn的应用

    SQL Server 环形缓冲区(Ring Buffer) -- 环形缓冲在AlwaysOn的应用 可以从SQL Server环形缓冲区得到一些诊断AlwaysOn的信息,或从sys.dm_os_ri ...

  5. 解析Disruptor:写入ring buffer

    原文地址http://mechanitis.blogspot.com/2011/07/dissecting-disruptor-writing-to-ring.html 这是Disruptor end ...

  6. linux+循环buffer,说说循环缓冲区(Ring Buffer)

    关于循环缓冲区(Ring Buffer)的概念,其实来自于Linux内核(Maybe),是为解决某些特殊情况下的竞争问题提供了一种免锁的方法.这种特殊的情况就是当生产者和消费者都只有一个,而在其它情况 ...

  7. Ring Buffer (circular Buffer)环形缓冲区简介

    https://blog.csdn.net/langeldep/article/details/8888582 关于环形缓冲区的知识,请看这里 http://en.wikipedia.org/wiki ...

  8. Linux ftrace 1.1、ring buffer

    1.简介 ringbuffer是trace框架的一个基础,所有的trace原始数据都是通过ringbuffer记录的.ringbuffer的作用主要有几个: 1.存储在内存中,速度非常快,对系统性能的 ...

  9. 网卡的 Ring Buffer 详解

    网卡的 Ring Buffer 详解 1. 网卡处理数据包流程 网卡处理网络数据流程图: 图片来自参考链接1 上图中虚线步骤的解释: DMA 将 NIC 接收的数据包逐个写入 sk_buff ,一个数 ...

最新文章

  1. Transformer霸榜全景分割任务,南大、港大提出一种通用框架!
  2. 全球IT支出保持稳定增长 中国IT支出将超2.3万亿元
  3. x window的奥秘
  4. Linux--根文件系统的挂载过程分析
  5. java url后面带sessionid_Spring Mvc boot解决静态url带jsessionid问题
  6. python3.4和3.6的区别_详解Python3.6正式版新特性
  7. redis-Set集合操作SADD,SMEMBERS,scard,srem
  8. Android-- Dialog对话框的使用方法
  9. Ubuntu20.04如何卸载软件
  10. 音乐推荐系统参考资料
  11. 为何电脑系统相对通用而手机却相对定制
  12. 解决 EndNote X9 安装报错 lnstallation ended prematurely because of an error.
  13. python读取txt文件中的内容并用逗号分割_数据分析—gt;文件读写
  14. matlab设置三维图等高线,MATLAB --三维图形等高线
  15. 华为供应链的“危”与“机”
  16. 测判三极管的口诀 (挑战者)
  17. 云服务器htdocs文件夹在,htdocs文件夹
  18. “破镜”真的没办法“重圆”了吗?
  19. git由ssh改为http后,HTTP Basic: Access denied无法同步问题解决
  20. 交换机SVI配置的作用 思科/华为 网络工程

热门文章

  1. 谷歌滤镜软件叫什么_谷歌app爆红的拍照功能:你最像名画中的谁?
  2. Android中第三方SDK集成之ZXing二维码扫一扫集成指南
  3. GPS问题调试—MobileLog中有关GPS关键LOG的释义
  4. 乐城超市36计做微营销-王卫
  5. css中文字操超出固定个数显示省略... 超出隐藏
  6. Map阶段环形缓冲区详细分析
  7. 源码推荐 VVebo剥离的TableView绘制
  8. 电脑画流程图用什么软件好?这3款软件很好用
  9. ZZULIOJ-1088: 手机短号 (多实例)(Java)
  10. vue使用Echarts画柱状图