Files
Joe Tsai b83f28ebfb util/bufpool: add a general purpose pool of buffers (#20867)
A naive sync.Pool that stores dynamically sized []byte
is incorrect since the Pool assumes that each stored item
carries approximately the same memory cost.

Different cost items should be stored in different pools.
The bufpool tracks buffer pools by powers of two
and provides a Buffer type that mimics the API of bytes.Buffer.

This is a performance optimization. Avoid using this unless
there is a clear performance gain for doing so.
Incorrect use can lead to widespread data corruption
(as is already the case with any incorrect sync.Pool use).

Updates tailscale/corp#21363

Signed-off-by: Joe Tsai <joetsai@digital-static.net>
2026-10-14 15:56:12 -07:00

226 lines
7.6 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package bufpool provides a pool of variable length buffers.
//
// Buffers retrieved from bufpool can either be used directly
// with an API similar to [bytes.Buffer], or can be used as a raw []byte,
// by calling the [Buffer.Bytes] method. Buffers retrieved from the pool must
// be eventually returned back to the pool. Otherwise, metrics tracking
// the total number of actively used buffers will be incorrect, and
// any limits on the maximum number of active buffers will operate incorrectly.
//
// A limit on the number of active buffers may be specified to refuse future
// attempts to acquire a buffer while running in a high load state.
// This is useful in server applications as a load shedding mechanism where
// it is better to fail certain requests instead of crashing.
//
// Example usage:
//
// b, err := Get(n) // n is minimum capacity
// if err != nil {
// ... // handle err
// }
// defer Put(b)
// ... // use b as if it were a bytes.Buffer
package bufpool
import (
"errors"
"expvar"
"fmt"
"math"
"math/bits"
"strconv"
"sync"
"sync/atomic"
"tailscale.com/metrics"
)
// ErrInsufficientCapacity indicates that [Get] and [Pool.Get] could not acquire
// a buffer because of size limits (see [SetMaxSize] or [Pool.SetMaxSize]).
var ErrInsufficientCapacity = errors.New("insufficient capacity to allocate buffer")
var global = NewPool()
// Get retrieves a [Buffer] with sufficient capacity to hold n bytes.
// If the global pool lacks sufficient capacity, it returns an error.
//
// Every buffer must be returned to back to the pool by calling [Put].
// Leaking a buffer causes metric gauges about actively used buffers
// to incorrectly increase without ever decreasing.
// If [SetMaxSize] is used, then [Get] may perpetually fail.
func Get(n int) (*Buffer, error) { return global.Get(n) }
// Put returns the buffer back to the global pool. The buffer
// (including any []byte obtained through [Buffer.Bytes] or [Buffer.Next])
// must not be used any more.
func Put(b *Buffer) { global.Put(b) }
// ExpVar reports metrics about global pool usage.
// See [Pool.ExpVar].
func ExpVar() expvar.Var { return global.ExpVar() }
// SetMaxSize specifies the maximum number of buffer bytes for active use.
//
// Since this has global effects, it is recommended that this only be set in the
// main package and at a level that's a reasonable fraction of system memory.
//
// See [Pool.SetMaxSize].
func SetMaxSize(n int64) { global.SetMaxSize(n) }
var (
minBits = 10 // 1 KiB
maxBits = 30 // 1 GiB
)
// Pool is a pool of Buffers.
type Pool struct {
//lint:ignore U1000 intentional unused field
noShallowCopy [0]sync.Mutex
maxSize atomic.Int64 // maximum total number of in-use buffer bytes
// pools is a list of sync.Pool,
// where each pool manages buffers of capacity 1<<(minBits+i).
pools []pool
gaugeUsedBytes expvar.Int // current number of buffer bytes in use
gaugeUsedBuffers expvar.Int // current number of buffers in use
counterUsedBytes expvar.Int // total number of buffer bytes ever used
counterUsedBuffers expvar.Int // total number of buffers ever requested
counterRefusedBuffers expvar.Int // total number of buffers refused due to SetMaxSize
gaugeUsedBuffersMap metrics.LabelMap
counterUsedBuffersMap metrics.LabelMap
}
type pool struct {
*sync.Pool
gaugeUsedBuffers *expvar.Int
counterUsedBuffers *expvar.Int
}
// NewPool constructs a new buffer pool.
// By default, there is no limit to the maximum bytes in active use.
// See [Pool.SetMaxSize].
func NewPool() *Pool {
p := &Pool{pools: make([]pool, maxBits-minBits+1)}
p.maxSize.Store(math.MaxInt64)
p.gaugeUsedBuffersMap.Label = "bits"
p.counterUsedBuffersMap.Label = "bits"
for i := range p.pools {
p.pools[i] = pool{
Pool: &sync.Pool{New: func() any {
return &Buffer{parent: p, buf: make([]byte, 0, 1<<(minBits+i))}
}},
gaugeUsedBuffers: p.gaugeUsedBuffersMap.Get(strconv.Itoa(minBits + i)),
counterUsedBuffers: p.counterUsedBuffersMap.Get(strconv.Itoa(minBits + i)),
}
}
return p
}
// ExpVar reports metrics about pool usage:
//
// - gauge_used_bytes: buffer bytes in active use
// - gauge_used_buffers: number of buffers in active use
// - counter_total_used_bytes: total buffer bytes ever acquired
// - counter_total_used_buffers: total number of buffers ever acquired
// - counter_total_refused_buffers: total number of buffers refused
// due to a limit on the total number of active buffer bytes
// - gauge_used_buffers_by_level: a map of the number of buffers
// in active use keyed by the log2 of the buffer size
// - counter_total_used_buffers_by_level: a map of the total number of buffers
// ever acquired keyed by the log2 of the buffer size
func (p *Pool) ExpVar() expvar.Var {
m := new(metrics.Set)
m.Set("gauge_used_bytes", &p.gaugeUsedBytes)
m.Set("gauge_used_buffers", &p.gaugeUsedBuffers)
m.Set("counter_total_used_bytes", &p.counterUsedBytes)
m.Set("counter_total_used_buffers", &p.counterUsedBuffers)
m.Set("counter_total_refused_buffers", &p.counterRefusedBuffers)
m.Set("gauge_used_buffers_by_level", &p.gaugeUsedBuffersMap)
m.Set("counter_total_used_buffers_by_level", &p.counterUsedBuffersMap)
return m
}
// SetMaxSize specifies the maximum number of buffer bytes for active use.
//
// Setting this to a lower limit does not affect active buffers,
// but prevents [Pool.Get] from successfully returning until enough buffers
// have been [Pool.Put] that reduces the total active buffers back to the limit.
//
// A negative value specifies no limit. By default, there is no limit.
func (p *Pool) SetMaxSize(n int64) {
if n < 0 {
n = math.MaxInt64
}
p.maxSize.Store(n)
}
// Get retrieves a [Buffer] with sufficient capacity to hold n bytes.
// If the pool lacks sufficient capacity, it returns an error.
//
// Every buffer must be returned to back to the pool by calling [Pool.Put].
// Leaking a buffer causes metric gauges about actively used buffers
// to incorrectly increase without ever decreasing.
// If [Pool.SetMaxSize] is used, then [Pool.Get] may perpetually fail.
func (p *Pool) Get(n int) (*Buffer, error) {
if n < 0 {
panic("negative capacity")
}
i := bits.UintSize - bits.LeadingZeros(uint(n-1)) // round up to nearest power of two
if n == 0 || i < minBits {
i = minBits
}
c := int64(1 << i)
if c < int64(n) {
panic(fmt.Sprintf("integer overflow computing capacity for %d", n))
}
p.gaugeUsedBytes.Add(+c)
if p.gaugeUsedBytes.Value() > p.maxSize.Load() {
p.gaugeUsedBytes.Add(-c)
p.counterRefusedBuffers.Add(1)
return nil, ErrInsufficientCapacity
}
p.gaugeUsedBuffers.Add(+1)
p.counterUsedBytes.Add(+c)
p.counterUsedBuffers.Add(+1)
if i > maxBits {
// Too large for any of the pre-sized pools.
// Just allocate from the Go heap.
return &Buffer{parent: p, buf: make([]byte, 0, 1<<i)}, nil
}
pp := &p.pools[i-minBits]
pp.gaugeUsedBuffers.Add(+1)
pp.counterUsedBuffers.Add(+1)
b := pp.Get().(*Buffer)
b.Reset()
return b, nil
}
// Put returns the buffer back to the pool. The buffer
// (including any []byte obtained through [Buffer.Bytes] or [Buffer.Next])
// must not be used any more.
func (p *Pool) Put(b *Buffer) {
if p != b.parent {
panic(fmt.Sprintf("mismatching parent pool: %p ≠ %p", p, b.parent))
}
c := int64(cap(b.buf))
i := bits.UintSize - bits.LeadingZeros(uint(c)) - 1 // round down to nearest power of two
p.gaugeUsedBytes.Add(-c)
p.gaugeUsedBuffers.Add(-1)
if !(minBits <= i && i <= maxBits) {
// Too large for any of the pre-sized pools.
// Just ignore the buffer and let the Go GC clean it up later.
return
}
pp := &p.pools[i-minBits]
pp.gaugeUsedBuffers.Add(-1)
pp.Put(b)
}