Files
seaweedfs/weed/storage/blockvol/snapshot.go
T
Ping QiuandClaude Opus 4.6 d874e21f93 feat: Phase 5 CP5-2 -- CoW snapshots, 10 tests
Sparse delta-file snapshots with copy-on-write in the flusher.
Zero write-path overhead when no snapshot is active.

New: snapshot.go (SnapshotBitmap, SnapshotHeader, delta file I/O)
Modified: flusher.go (flushMu, CoW phase in FlushOnce, PauseAndFlush)
Modified: blockvol.go (Create/Read/Delete/Restore/ListSnapshots, recovery)
Modified: wal_writer.go (Reset for snapshot restore)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 00:53:33 -08:00

297 lines
7.8 KiB
Go

package blockvol
import (
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"time"
)
// Snapshot delta file constants.
const (
SnapMagic = "SWBS"
SnapVersion = 1
SnapHeaderSize = 4096
)
var (
ErrSnapshotNotFound = errors.New("blockvol: snapshot not found")
ErrSnapshotExists = errors.New("blockvol: snapshot already exists")
ErrSnapshotBadMagic = errors.New("blockvol: snapshot bad magic")
ErrSnapshotBadVersion = errors.New("blockvol: snapshot unsupported version")
ErrSnapshotBadParent = errors.New("blockvol: snapshot parent UUID mismatch")
ErrSnapshotRoleReject = errors.New("blockvol: snapshots only allowed on primary or standalone")
)
// SnapshotBitmap is a dense bitset tracking which blocks have been CoW'd.
// No internal locking -- callers (flusher, CreateSnapshot) serialize access.
type SnapshotBitmap struct {
data []byte
bits uint64 // total number of bits (= VolumeSize/BlockSize)
}
// NewSnapshotBitmap creates a zero-initialized bitmap for totalBlocks blocks.
func NewSnapshotBitmap(totalBlocks uint64) *SnapshotBitmap {
byteLen := (totalBlocks + 7) / 8
return &SnapshotBitmap{
data: make([]byte, byteLen),
bits: totalBlocks,
}
}
// Get returns true if the bit at position lba is set.
func (b *SnapshotBitmap) Get(lba uint64) bool {
if lba >= b.bits {
return false
}
return b.data[lba/8]&(1<<(lba%8)) != 0
}
// Set sets the bit at position lba.
func (b *SnapshotBitmap) Set(lba uint64) {
if lba >= b.bits {
return
}
b.data[lba/8] |= 1 << (lba % 8)
}
// ByteSize returns the number of bytes in the bitmap.
func (b *SnapshotBitmap) ByteSize() int {
return len(b.data)
}
// WriteTo writes the bitmap data to w at the given offset.
func (b *SnapshotBitmap) WriteTo(w io.WriterAt, offset int64) error {
_, err := w.WriteAt(b.data, offset)
return err
}
// ReadFrom reads bitmap data from r at the given offset.
func (b *SnapshotBitmap) ReadFrom(r io.ReaderAt, offset int64) error {
_, err := r.ReadAt(b.data, offset)
return err
}
// CountSet returns the number of set bits (CoW'd blocks).
func (b *SnapshotBitmap) CountSet() uint64 {
var count uint64
for _, v := range b.data {
count += uint64(popcount8(v))
}
return count
}
// popcount8 returns the number of set bits in a byte.
func popcount8(x byte) int {
x = x - ((x >> 1) & 0x55)
x = (x & 0x33) + ((x >> 2) & 0x33)
return int((x + (x >> 4)) & 0x0F)
}
// SnapshotHeader is the on-disk header for a snapshot delta file.
type SnapshotHeader struct {
Magic [4]byte
Version uint16
SnapshotID uint32
BaseLSN uint64
VolumeSize uint64
BlockSize uint32
BitmapSize uint64
DataOffset uint64 // = SnapHeaderSize + BitmapSize, aligned to BlockSize
CreatedAt uint64
ParentUUID [16]byte
}
// WriteTo serializes the header as a SnapHeaderSize-byte block to w.
func (h *SnapshotHeader) WriteTo(w io.Writer) (int64, error) {
buf := make([]byte, SnapHeaderSize)
endian := binary.LittleEndian
off := 0
off += copy(buf[off:], h.Magic[:])
endian.PutUint16(buf[off:], h.Version)
off += 2
endian.PutUint32(buf[off:], h.SnapshotID)
off += 4
endian.PutUint64(buf[off:], h.BaseLSN)
off += 8
endian.PutUint64(buf[off:], h.VolumeSize)
off += 8
endian.PutUint32(buf[off:], h.BlockSize)
off += 4
endian.PutUint64(buf[off:], h.BitmapSize)
off += 8
endian.PutUint64(buf[off:], h.DataOffset)
off += 8
endian.PutUint64(buf[off:], h.CreatedAt)
off += 8
copy(buf[off:], h.ParentUUID[:])
n, err := w.Write(buf)
return int64(n), err
}
// ReadSnapshotHeader reads a SnapshotHeader from r.
func ReadSnapshotHeader(r io.Reader) (*SnapshotHeader, error) {
buf := make([]byte, SnapHeaderSize)
if _, err := io.ReadFull(r, buf); err != nil {
return nil, fmt.Errorf("blockvol: read snapshot header: %w", err)
}
endian := binary.LittleEndian
h := &SnapshotHeader{}
off := 0
copy(h.Magic[:], buf[off:off+4])
off += 4
if string(h.Magic[:]) != SnapMagic {
return nil, ErrSnapshotBadMagic
}
h.Version = endian.Uint16(buf[off:])
off += 2
if h.Version != SnapVersion {
return nil, fmt.Errorf("%w: got %d, want %d", ErrSnapshotBadVersion, h.Version, SnapVersion)
}
h.SnapshotID = endian.Uint32(buf[off:])
off += 4
h.BaseLSN = endian.Uint64(buf[off:])
off += 8
h.VolumeSize = endian.Uint64(buf[off:])
off += 8
h.BlockSize = endian.Uint32(buf[off:])
off += 4
h.BitmapSize = endian.Uint64(buf[off:])
off += 8
h.DataOffset = endian.Uint64(buf[off:])
off += 8
h.CreatedAt = endian.Uint64(buf[off:])
off += 8
copy(h.ParentUUID[:], buf[off:off+16])
return h, nil
}
// activeSnapshot represents an open snapshot delta file with its in-memory bitmap.
type activeSnapshot struct {
id uint32
fd *os.File
header SnapshotHeader
bitmap *SnapshotBitmap
dataOffset uint64
dirty bool // bitmap changed since last persist
}
// Close closes the delta file.
func (s *activeSnapshot) Close() error {
return s.fd.Close()
}
// deltaFilePath returns the path for a snapshot delta file.
func deltaFilePath(volPath string, id uint32) string {
return fmt.Sprintf("%s.snap.%d", volPath, id)
}
// createDeltaFile creates a new snapshot delta file and returns an activeSnapshot.
func createDeltaFile(path string, id uint32, vol *BlockVol, baseLSN uint64) (*activeSnapshot, error) {
totalBlocks := vol.super.VolumeSize / uint64(vol.super.BlockSize)
bitmap := NewSnapshotBitmap(totalBlocks)
bitmapSize := uint64(bitmap.ByteSize())
// Align DataOffset to BlockSize.
dataOffset := uint64(SnapHeaderSize) + bitmapSize
rem := dataOffset % uint64(vol.super.BlockSize)
if rem != 0 {
dataOffset += uint64(vol.super.BlockSize) - rem
}
hdr := SnapshotHeader{
Version: SnapVersion,
SnapshotID: id,
BaseLSN: baseLSN,
VolumeSize: vol.super.VolumeSize,
BlockSize: vol.super.BlockSize,
BitmapSize: bitmapSize,
DataOffset: dataOffset,
CreatedAt: uint64(time.Now().Unix()),
ParentUUID: vol.super.UUID,
}
copy(hdr.Magic[:], SnapMagic)
fd, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_EXCL, 0644)
if err != nil {
return nil, fmt.Errorf("blockvol: create delta file: %w", err)
}
// Truncate to full size (sparse file -- only header+bitmap consume disk).
totalSize := int64(dataOffset + vol.super.VolumeSize)
if err := fd.Truncate(totalSize); err != nil {
fd.Close()
os.Remove(path)
return nil, fmt.Errorf("blockvol: truncate delta: %w", err)
}
// Write header.
if _, err := hdr.WriteTo(fd); err != nil {
fd.Close()
os.Remove(path)
return nil, fmt.Errorf("blockvol: write snapshot header: %w", err)
}
// Write zero bitmap.
if err := bitmap.WriteTo(fd, SnapHeaderSize); err != nil {
fd.Close()
os.Remove(path)
return nil, fmt.Errorf("blockvol: write snapshot bitmap: %w", err)
}
// Fsync delta file.
if err := fd.Sync(); err != nil {
fd.Close()
os.Remove(path)
return nil, fmt.Errorf("blockvol: sync delta: %w", err)
}
return &activeSnapshot{
id: id,
fd: fd,
header: hdr,
bitmap: bitmap,
dataOffset: dataOffset,
}, nil
}
// openDeltaFile opens an existing snapshot delta file, reads its header and bitmap.
func openDeltaFile(path string) (*activeSnapshot, error) {
fd, err := os.OpenFile(path, os.O_RDWR, 0644)
if err != nil {
return nil, fmt.Errorf("blockvol: open delta file: %w", err)
}
hdr, err := ReadSnapshotHeader(fd)
if err != nil {
fd.Close()
return nil, err
}
totalBlocks := hdr.VolumeSize / uint64(hdr.BlockSize)
bitmap := NewSnapshotBitmap(totalBlocks)
if err := bitmap.ReadFrom(fd, SnapHeaderSize); err != nil {
fd.Close()
return nil, fmt.Errorf("blockvol: read snapshot bitmap: %w", err)
}
return &activeSnapshot{
id: hdr.SnapshotID,
fd: fd,
header: *hdr,
bitmap: bitmap,
dataOffset: hdr.DataOffset,
}, nil
}
// SnapshotInfo contains read-only snapshot metadata for listing.
type SnapshotInfo struct {
ID uint32
BaseLSN uint64
CreatedAt time.Time
CoWBlocks uint64 // number of CoW'd blocks (bitmap.CountSet())
}