2021.07.31
Jalex Chang
Contact:
Introduction
Pebble & CockroachDB
Memory Management Approaches in Pebble
Discussions
Summary
[1] Introduction to Pebble, https://www.cockroachlabs.com/blog/pebble-rocksdb-kv-store
[2] Source code of Pebble (v21.1), https://github.com/cockroachdb/pebble/tree/crl-release-21.1
[3] Memo of memory management in Pebble, https://github.com/cockroachdb/pebble/blob/crl-release-21.1/docs/memory.md
In this tech talk, we are going to introduce the memory management approaches in Pebble, a key-value data store written in GO.
The topics will be covered in the talk:
What is Pebble?
How does Pebble manage its memory usage efficiently?
What are the pros and cons of the approach?
What are the use cases of the approach?
The topics will not be covered in the talk:
Concurrency control
The details of data formats
WAL (write-ahead logging) and SSTable (String Sorted Table)
Go's memory allocation and management mechanisms are complicated.
Such as garbage collection (GC), TCMalloc, multi-layered memory allocator, escape analysis, and etc.
Some shared topics in recent years
Garbage collection in Go (Meetup#37)
Escape analysis in Go (GopherCon TW 2020)
Go memory management mechanisms are powerful enough in most cases, especially for Web-based applications.
However, does it also meet the needs of high-performance applications, such as database systems?
To find out the answer, I start to dig out the Go database system - CockroachDB and its underlying data engine Pebble.
//source code: pebble/internal/manual/manual.go
//go:linkname throw runtime.throw
func throw(s string)
func New(n int) []byte {
if n == 0 {
return make([]byte, 0)
}
ptr := C.calloc(C.size_t(n), 1)
if ptr == nil {
throw("out of memory")
}
// Interpret the C pointer as a pointer
// to a Go array, then slice.
return (*[MaxArrayLen]byte)(unsafe.Pointer(ptr))[:n:n]
}
func Free(b []byte) {
if cap(b) != 0 {
if len(b) == 0 {
b = b[:cap(b)]
}
ptr := unsafe.Pointer(&b[0])
C.free(ptr)
}
}
Free related blocks in Block Cache when flushing MemTables
To avoid cache collision
Free related blocks in Block Cache when compacting SSTables
MemTable
Each MemTable has a single but huge C memory space.
The space is size-fixed and is allocated at the very beginning.
C memory is encapsulated in the MemTable.
Block Cache
Block Cache contains a huge amount of small C memory spaces.
Each space is size-fixed but is allocated on-demanded.
C memory can be manipulated out of the Block Cache.
Spaces may get lost if callers have not sent them back.
To avoid the memory leak in Block Cache, runtime.SetFinalizer is used in Pebble's testing and development.
The finalizer is a function associated with an object.
The finalizer is run when the object is no longer reachable => run when the object is going to be GC.
// source: pebble/internal/cache/value_invariants.go
func newValue(n int) *Value {
b := manual.New(n)
v := &Value{buf: b}
// Note: this is a no-op if invariants and tracing are disabled or race is
// enabled.
invariants.SetFinalizer(v, func(obj interface{}) {
v := obj.(*Value)
if v.buf != nil {
fmt.Fprintf(os.Stderr, "%p: cache value was not freed: refs=%d\n%s",
v, v.refs(), v.ref.traces())
os.Exit(1)
}
})
return v
}
Q1: What are the pros and cons of the manual memory management approach?
Q2: What are the use cases of the manual memory management approach?
In this tech sharing, we have introduced Pebble and its memory management approach.
To avoid high pressure on Go GC, it uses the C memory allocator to its significant memory sources: MemTable and Block Cache.
To avoid memory leaks:
Manual lifetime tracking is needed.
A finalizer is used during the testing and development.
Through the case study of Pebble, we have learned:
Although Go's memory management mechanisms are powerful, they still cannot meet the requirements of really high-performance applications
Manual memory management is a possible alternative though risky.