victoriaMetrics之byteBuffer

charlieroro發表於2022-04-06

victoriaMetrics之byteBuffer

VictoriaMetrics經常會處理數目龐大的指標,在處理的過程中會涉及指標的拷貝,如果在指標拷貝時都進行記憶體申請的話,其記憶體消耗和效能損耗都非常大。victoriaMetrics使用byteBuffer來複用記憶體,提升效能,其核心就是用了sync.pool。下面主要看下它是如何結合sync.pool運作的。

ByteBuffer的結構體如下,只包含一個切片:

type ByteBuffer struct {
	// B is the underlying byte slice.
	B []byte
}

ByteBufferPool的用法

為了服用ByteBuffer,victoriaMetrics用到了ByteBufferPool,與常見的sync.Pool用法相同,包含一個Get和一個Put函式。

// ByteBufferPool is a pool of ByteBuffers.
type ByteBufferPool struct {
	p sync.Pool
}

// Get obtains a ByteBuffer from bbp.
func (bbp *ByteBufferPool) Get() *ByteBuffer {
	bbv := bbp.p.Get()
	if bbv == nil {
		return &ByteBuffer{}
	}
	return bbv.(*ByteBuffer)
}

// Put puts bb into bbp.
func (bbp *ByteBufferPool) Put(bb *ByteBuffer) {
	bb.Reset()
	bbp.p.Put(bb)
}

Put函式用於將ByteBuffer返回給資源池,為了防止下次使用的時候出現無效資料,在返回給sync.Pool之前需要清空切片記憶體,其使用的Reset函式如下,bb.B = bb.B[:0]也是一種常見的清空切片內容的方式:

func (bb *ByteBuffer) Reset() {
	bb.B = bb.B[:0]
}

ByteBuffer實現了io.Writerio.ReaderFrom介面。

Writer介面實現

實現的write介面如下,比較簡單,只是簡單地將入引數據新增到byteBuffer中。在append的時候會增加切片的容量。

func (bb *ByteBuffer) Write(p []byte) (int, error) {
	bb.B = append(bb.B, p...)
	return len(p), nil
}

ReaderFrom介面實現

ReaderFrom中比較有意思的是看它是如何預分配容量,以及在容量不足的情況下,如何進行擴容。其核心思想是使用make預先申請一塊記憶體,而不是通過append來讓底層自動擴容。

  1. 首先獲取b的長度,表示切片中已有的資料長度

  2. 由於ByteBuffer可能來自ByteBufferPool.Get,因此,其切片容量可能無法滿足資料讀取的需要,此時用到了ResizeWithCopyMayOverallocateResizeWithCopyMayOverallocate確保切片的容量不小於n位元組,如果容量足夠,則返回長度為n的子切片,否則申請新的切片,並返回長度為n的子切片。roundToNearestPow2會找出最接近n的2的冪的數值,以此作為新切片的容量。

    // ResizeNoCopyMayOverallocate resizes b to minimum n bytes and returns the resized buffer (which may be newly allocated).
    //
    // If newly allocated buffer is returned then b contents isn't copied to it.
    func ResizeNoCopyMayOverallocate(b []byte, n int) []byte {
    	if n <= cap(b) {
    		return b[:n]
    	}
    	nNew := roundToNearestPow2(n)
    	bNew := make([]byte, nNew)
    	return bNew[:n]
    }
    
    // roundToNearestPow2 rounds n to the nearest power of 2
    //
    // It is expected that n > 0
    func roundToNearestPow2(n int) int {
    	pow2 := uint8(bits.Len(uint(n - 1)))
    	return 1 << pow2
    }
    
  3. 將b的長度等於容量

  4. 設定offset為b中已有的資料偏移量

  5. 獲取剩餘的容量free,如果剩餘的容量不足一半(free < offset),則將容量翻倍

  6. 將資料讀取到offset之後的儲存中,並增加偏移量

  7. Read操作返回錯誤時,將ByteBuffer中的切片長度設定為b,如果返回錯誤為EOF,則視為資料讀取完成。

// ReadFrom reads all the data from r to bb until EOF.
func (bb *ByteBuffer) ReadFrom(r io.Reader) (int64, error) {
	b := bb.B
	bLen := len(b)//1
	b = ResizeWithCopyMayOverallocate(b, 4*1024) //2
	b = b[:cap(b)]//3
	offset := bLen//4
	for {
		if free := len(b) - offset; free < offset {//5
			n := len(b)
			b = append(b, make([]byte, n)...)
		}
		n, err := r.Read(b[offset:])//6
		offset += n
		if err != nil {//7
			bb.B = b[:offset]
			if err == io.EOF {
				err = nil
			}
			return int64(offset - bLen), err//9
		}
	}
}

總結

後續可以使用該庫來滿足從io.Reader中讀取資料,而不用擔心buffer不足,藉助ByteBufferPool可以有效地複用buffer。

相關文章