mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-16 19:40:43 +02:00
* log_buffer: share one window snapshot across all subscriber reads Every in-memory read handed each subscriber a private pooled copy of the window it wanted, so N subscribers reading the same data cost N copies of up to 8MB each -- and slow consumers (grpc send backpressure) held those copies live for their whole iteration. With hundreds of mount subscribers that multiplied into gigabytes of live heap on the filer. Share the bytes instead of copying per reader: - Sealed windows get a lazily created GC-owned snapshot, made once by the first reader and handed out zero-copy to the rest. The snapshot travels with its window when SealBuffer shifts slots, so recycling the sealed array never invalidates it. - The current window keeps a shared snapshot of its append-only prefix buf[:pos], extended on demand; each byte is copied once per window (writer-rate-bound) instead of once per reader. At seal a fully extended prefix becomes the sealed window's snapshot. ReadFromBuffer now reports whether the returned buffer is a pooled copy (flush path) or a shared view that must not be released; the read loops only recycle pooled buffers. With 200 subscribers consuming at grpc pace over sealed and current windows, peak live heap drops from 5.2GB to 178MB. * log_buffer: clear released read buffer so a panic cannot double-free it The read loops release the previous iteration's pooled buffer and then call ReadFromBuffer. If that call panicked before reassigning bytesBuf, the deferred cleanup would put the same buffer into the pool a second time, letting two future readers share one backing array. Nil the pointer at the release site so the defer sees nothing to free.
269 lines
8.4 KiB
Go
269 lines
8.4 KiB
Go
package log_buffer
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
)
|
|
|
|
func addEntries(tb testing.TB, lb *LogBuffer, n int, payloadSize int, tag string) {
|
|
payload := make([]byte, payloadSize)
|
|
for i := 0; i < n; i++ {
|
|
key := []byte(fmt.Sprintf("/%s/%d", tag, i))
|
|
if err := lb.AddDataToBuffer(key, payload, time.Now().UnixNano()); err != nil {
|
|
tb.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sealed-window reads must hand every reader the same shared snapshot rather
|
|
// than a fresh copy each.
|
|
func TestSealedReadsShareOneSnapshot(t *testing.T) {
|
|
lb := NewLogBuffer("test", time.Hour, func(*LogBuffer, time.Time, time.Time, []byte, int64, int64) {}, nil, nil)
|
|
defer lb.ShutdownLogBuffer()
|
|
|
|
addEntries(t, lb, 10, 1024, "a")
|
|
lb.ForceFlush() // seal the window
|
|
addEntries(t, lb, 1, 1024, "b")
|
|
|
|
pos := NewMessagePosition(1, -2) // far in the past => sealed window hit
|
|
buf1, _, pooled1, err1 := lb.ReadFromBuffer(pos)
|
|
buf2, _, pooled2, err2 := lb.ReadFromBuffer(pos)
|
|
if err1 != nil || err2 != nil {
|
|
t.Fatalf("read errors: %v %v", err1, err2)
|
|
}
|
|
if buf1 == nil || buf2 == nil {
|
|
t.Fatal("expected sealed data in memory")
|
|
}
|
|
if pooled1 || pooled2 {
|
|
t.Fatalf("sealed reads should not be pooled copies: %v %v", pooled1, pooled2)
|
|
}
|
|
b1, b2 := buf1.Bytes(), buf2.Bytes()
|
|
if len(b1) == 0 || len(b1) != len(b2) {
|
|
t.Fatalf("unexpected lengths %d %d", len(b1), len(b2))
|
|
}
|
|
if &b1[0] != &b2[0] {
|
|
t.Fatal("sealed reads made independent copies; expected one shared snapshot")
|
|
}
|
|
}
|
|
|
|
// A shared snapshot must stay intact after its window rotates out and the
|
|
// backing array is recycled and overwritten by new writes.
|
|
func TestSharedSnapshotSurvivesRecycle(t *testing.T) {
|
|
lb := NewLogBuffer("test", time.Hour, func(*LogBuffer, time.Time, time.Time, []byte, int64, int64) {}, nil, nil)
|
|
defer lb.ShutdownLogBuffer()
|
|
|
|
addEntries(t, lb, 5, 2048, "keep")
|
|
lb.ForceFlush()
|
|
|
|
pos := NewMessagePosition(1, -2)
|
|
buf, _, pooled, err := lb.ReadFromBuffer(pos)
|
|
if err != nil || buf == nil || pooled {
|
|
t.Fatalf("expected shared sealed read, got buf=%v pooled=%v err=%v", buf != nil, pooled, err)
|
|
}
|
|
before := append([]byte(nil), buf.Bytes()...)
|
|
|
|
// Rotate the sealed window all the way out so its array is recycled and
|
|
// overwritten with different content.
|
|
for i := 0; i < PreviousBufferCount+1; i++ {
|
|
addEntries(t, lb, 5, 2048, "overwrite")
|
|
lb.ForceFlush()
|
|
}
|
|
addEntries(t, lb, 5, 2048, "overwrite")
|
|
|
|
if string(before) != string(buf.Bytes()) {
|
|
t.Fatal("shared snapshot content changed after its window was recycled")
|
|
}
|
|
}
|
|
|
|
// Race test: concurrent lagging readers against a writer that keeps sealing
|
|
// and recycling windows. Every delivered entry must unmarshal cleanly with the
|
|
// expected payload — corruption here means a reader saw recycled bytes.
|
|
func TestSharedSnapshotConcurrentIntegrity(t *testing.T) {
|
|
lb := NewLogBuffer("test", time.Hour, func(*LogBuffer, time.Time, time.Time, []byte, int64, int64) {}, nil, nil)
|
|
defer lb.ShutdownLogBuffer()
|
|
|
|
const payloadByte = 0x5A
|
|
payload := make([]byte, 8*1024)
|
|
for i := range payload {
|
|
payload[i] = payloadByte
|
|
}
|
|
|
|
var stop sync.WaitGroup
|
|
done := make(chan struct{})
|
|
stop.Add(1)
|
|
go func() { // writer: fill and seal aggressively so arrays recycle under the readers
|
|
defer stop.Done()
|
|
for i := 0; ; i++ {
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
}
|
|
if err := lb.AddDataToBuffer([]byte(fmt.Sprintf("/k/%d", i)), payload, time.Now().UnixNano()); err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
if i%200 == 0 {
|
|
lb.ForceFlush()
|
|
}
|
|
}
|
|
}()
|
|
|
|
var readers sync.WaitGroup
|
|
errCh := make(chan error, 16)
|
|
for r := 0; r < 8; r++ {
|
|
readers.Add(1)
|
|
go func(r int) {
|
|
defer readers.Done()
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
start := NewMessagePosition(time.Now().Add(-500*time.Millisecond).UnixNano(), -2)
|
|
lb.LoopProcessLogData(fmt.Sprintf("r%d", r), start, 0,
|
|
func() bool { return time.Now().Before(deadline) },
|
|
func(le *filer_pb.LogEntry) (bool, error) {
|
|
if len(le.Data) != len(payload) {
|
|
err := fmt.Errorf("payload length %d, want %d", len(le.Data), len(payload))
|
|
select {
|
|
case errCh <- err:
|
|
default:
|
|
}
|
|
return true, err
|
|
}
|
|
for _, b := range le.Data {
|
|
if b != payloadByte {
|
|
err := fmt.Errorf("corrupted payload byte %x", b)
|
|
select {
|
|
case errCh <- err:
|
|
default:
|
|
}
|
|
return true, err
|
|
}
|
|
}
|
|
return false, nil
|
|
})
|
|
}
|
|
}(r)
|
|
}
|
|
readers.Wait()
|
|
close(done)
|
|
stop.Wait()
|
|
|
|
select {
|
|
case err := <-errCh:
|
|
t.Fatal(err)
|
|
default:
|
|
}
|
|
}
|
|
|
|
// Current-window reads must share the prefix snapshot: two behind-readers get
|
|
// views of the same backing array, and the view content must stay intact while
|
|
// the writer keeps appending and eventually seals and recycles the window.
|
|
func TestCurrentWindowPrefixSharing(t *testing.T) {
|
|
lb := NewLogBuffer("test", time.Hour, func(*LogBuffer, time.Time, time.Time, []byte, int64, int64) {}, nil, nil)
|
|
defer lb.ShutdownLogBuffer()
|
|
|
|
addEntries(t, lb, 8, 1024, "cur")
|
|
|
|
pos := NewMessagePosition(1, -2) // behind => full current-window read
|
|
buf1, _, pooled1, err1 := lb.ReadFromBuffer(pos)
|
|
buf2, _, pooled2, err2 := lb.ReadFromBuffer(pos)
|
|
if err1 != nil || err2 != nil || buf1 == nil || buf2 == nil {
|
|
t.Fatalf("reads: %v %v %v %v", buf1 != nil, err1, buf2 != nil, err2)
|
|
}
|
|
if pooled1 || pooled2 {
|
|
t.Fatalf("current-window behind-reads should be shared, got pooled %v %v", pooled1, pooled2)
|
|
}
|
|
b1, b2 := buf1.Bytes(), buf2.Bytes()
|
|
if len(b1) == 0 || len(b1) != len(b2) || &b1[0] != &b2[0] {
|
|
t.Fatalf("expected one shared prefix snapshot, lens %d %d", len(b1), len(b2))
|
|
}
|
|
before := append([]byte(nil), b1...)
|
|
|
|
// Keep appending: the prefix must be extended, not reallocated or mutated.
|
|
addEntries(t, lb, 8, 1024, "more")
|
|
buf3, _, _, err3 := lb.ReadFromBuffer(pos)
|
|
if err3 != nil || buf3 == nil {
|
|
t.Fatalf("read3: %v %v", buf3 != nil, err3)
|
|
}
|
|
if len(buf3.Bytes()) <= len(before) {
|
|
t.Fatalf("extended read %d should exceed first read %d", len(buf3.Bytes()), len(before))
|
|
}
|
|
if string(buf3.Bytes()[:len(before)]) != string(before) {
|
|
t.Fatal("prefix changed when the snapshot was extended")
|
|
}
|
|
|
|
// Seal + rotate the window fully out; earlier views must stay intact.
|
|
for i := 0; i < PreviousBufferCount+1; i++ {
|
|
lb.ForceFlush()
|
|
addEntries(t, lb, 4, 1024, "rotate")
|
|
}
|
|
if string(b1) != string(before) {
|
|
t.Fatal("shared current-window view changed after seal and recycle")
|
|
}
|
|
}
|
|
|
|
// A sealed window whose prefix snapshot was fully extended must reuse it
|
|
// rather than re-copying on the first sealed read.
|
|
func TestSealHandsOffCompleteSnapshot(t *testing.T) {
|
|
lb := NewLogBuffer("test", time.Hour, func(*LogBuffer, time.Time, time.Time, []byte, int64, int64) {}, nil, nil)
|
|
defer lb.ShutdownLogBuffer()
|
|
|
|
addEntries(t, lb, 6, 512, "h")
|
|
pos := NewMessagePosition(1, -2)
|
|
buf1, _, _, err := lb.ReadFromBuffer(pos) // extends prefix snapshot to full window
|
|
if err != nil || buf1 == nil {
|
|
t.Fatalf("read1: %v %v", buf1 != nil, err)
|
|
}
|
|
lb.ForceFlush()
|
|
addEntries(t, lb, 1, 512, "next")
|
|
|
|
buf2, _, pooled, err := lb.ReadFromBuffer(pos) // sealed read of the same window
|
|
if err != nil || buf2 == nil || pooled {
|
|
t.Fatalf("read2: buf=%v pooled=%v err=%v", buf2 != nil, pooled, err)
|
|
}
|
|
b1, b2 := buf1.Bytes(), buf2.Bytes()
|
|
if len(b1) == 0 || len(b1) > len(b2) || &b1[0] != &b2[0] {
|
|
t.Fatalf("sealed read should reuse the handed-off snapshot (lens %d %d)", len(b1), len(b2))
|
|
}
|
|
}
|
|
|
|
// Sanity: sealed-read content must byte-match what a proto round-trip expects.
|
|
func TestSharedSnapshotContentMatches(t *testing.T) {
|
|
lb := NewLogBuffer("test", time.Hour, func(*LogBuffer, time.Time, time.Time, []byte, int64, int64) {}, nil, nil)
|
|
defer lb.ShutdownLogBuffer()
|
|
|
|
addEntries(t, lb, 3, 512, "x")
|
|
lb.ForceFlush()
|
|
|
|
buf, _, _, err := lb.ReadFromBuffer(NewMessagePosition(1, -2))
|
|
if err != nil || buf == nil {
|
|
t.Fatalf("read: buf=%v err=%v", buf != nil, err)
|
|
}
|
|
data := buf.Bytes()
|
|
count := 0
|
|
for pos := 0; pos+4 < len(data); {
|
|
size, _, err := readTs(data, pos)
|
|
if err != nil {
|
|
t.Fatalf("entry %d: %v", count, err)
|
|
}
|
|
var le filer_pb.LogEntry
|
|
if err := proto.Unmarshal(data[pos+4:pos+4+size], &le); err != nil {
|
|
t.Fatalf("entry %d unmarshal: %v", count, err)
|
|
}
|
|
if want := fmt.Sprintf("/x/%d", count); string(le.Key) != want {
|
|
t.Fatalf("entry %d key %q, want %q", count, le.Key, want)
|
|
}
|
|
pos += 4 + size
|
|
count++
|
|
}
|
|
if count != 3 {
|
|
t.Fatalf("read %d entries, want 3", count)
|
|
}
|
|
}
|