4 21
RingBuffer的应用

最近团队在某些go开发的项目中, 都不同程度地出现内存暴涨, 计算量特别大的时候,内存一直不下降,且gc很频繁,但按理说GC后内存清理后,依旧不释放就有问题了, 经过pprof分析出的原因是: 写的缓冲层实现,不满足我们数据计算量非常大的场景, 毕竟并发的世界充满意想不到的事情

当然某些中间计算的结果,我们可以加入redis作为缓冲来存放, 但作为一级缓存, 我认为也没必要去加重代码的对第三方服务的依赖, 如果用原生map的时候,效率和手动去lock又会带来上面的回收频繁问题, 所以就想有没有好的算法或者好的手段来简单快速解决问题

然后看了很多技术文章, 跟我们目前情况差不多的团队,他们都用了很多基于RingBuffer的思路来解决问题.

概念

Ring Buffer在维基百科的解析是:

圆形缓冲区(circular buffer),也称作圆形队列(circular queue),循环缓冲区(cyclic buffer),环形缓冲区(ring buffer),是一种用于表示一个固定尺寸、头尾相连的缓冲区的数据结构,适合缓存数据流。

简单概况, 它的基本特点:

  • FIFO
  • 读指针
  • 写指针
  • 固定尺寸、头尾相连的结构

rb1

基本结构示意

rb2

存储操作示意

RingBUffer之所以ringbuffer采用这种数据结构,是因为它在可靠消息传递方面有很好的性能:

首先,因为它是数组,所以插入要比链表快

其次,你可以为数组预先分配内存,使得数组对象一直存在, 这就意味着不需要花大量的时间用于垃圾回收。此外,不像链表那样,需要为每一个添加到其上面的对象创造节点对象—对应的,当删除节点时,需要执行相应的内存清理操作。

实现

了解RingBuffer的原理后,我们可以自己来写一个简单的RingBuffer的Golang实现

我们先定义最基本的RingBuffer结构体

type RingBuffer struct {
    data         []byte
    size         int64
    writeCursor  int64
    writtenCount int64
}

提供初始化的方法: 由于rb的特定是大小需要预先固定,所以我们在初始化的时候,把尺寸都给预分配好

//size 必须大于0
func NewBuffer(size int64) (*RingBuffer, error) {
    if size <= 0 {
        return nil, errors.New("Size must be positive")
    }

    b := &RingBuffer{
        size: size,
        data: make([]byte, size)}

    return b, nil
}

RingBuffer最重要的功能就是要写数据, 由于ringbuffer的形态特殊, 我们必须写数据的时候,要移动我们的 "写指针", 再根据fifo的特点来进行数据填充

//写入buf到ringbuffer内部
//如果需要会覆盖旧数据(fifo)
func (b *RingBuffer) Write(buf []byte) (int, error) {

    n := len(buf)
    b.writtenCount += int64(n)

    //如果buf的大小超过容量限制,根据fifo原则
    //我们只关注最近最新的部分数据
    if int64(n) > b.size {
        buf = buf[int64(n)-b.size:]
    }

    remain := b.size - b.writeCursor
    copy(b.data[b.writeCursor:], buf)
    if int64(len(buf)) > remain {
        copy(b.data, buf[remain:])
    }

    b.writeCursor = ((b.writeCursor + int64(len(buf))) % b.size)
    return n, nil
}

关于读的方法实现, 这里就简单实现一个输出所有数据的方法, 当然我们也可以根据具体位置读取的方法实现, 这个大家可以根据自己的需要去扩展.

//读出buffer所有数据
func (b *RingBuffer) ReadAll() []byte {

    switch {
    case b.writtenCount >= b.size && b.writeCursor == 0:
        return b.data
    case b.writtenCount > b.size:
        out := make([]byte, b.size)
        copy(out, b.data[b.writeCursor:])
        copy(out[b.size-b.writeCursor:], b.data[:b.writeCursor])
        return out
    default:
        return b.data[:b.writeCursor]
    }

    return nil
}

总结

  1. ring buffer在网络编程各种语言很多场景下都有不同的应用和用法,一次性开块大内存是生产环境下的常用做法

  2. 使用 ring buffer 的优势是内存使用率很高,不会造成内存碎片,几乎没有浪费(比如传统动态内存分配需要的 cookie),业务处理的同一时间,访问的内存数据段集中。可以更好的适应不同系统,取得较高的性能。

  3. 内存的物理布局简单单一,不太容易发生内存越界、悬空指针等 bug,出了问题也容易在内存级别分析调试。做出来的系统容易保持健壮。

参考资料

云风的blog : RingBuffer的应用