mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-10 16:27:47 +02:00
* volume: recover .idx rows overwritten by tiered deletes A delete on a read-only volume backed by a remote tier used to write its tombstone row at .idx offset 0 rather than appending it, so each delete overwrote one more row at the front and lost the Put rows indexing the first needles in .dat. Those needles 404 even though .dat still holds them, and rebuilding .idx with weed fix means stopping the server and pulling the whole .dat back from the tier. The damage has a fingerprint -- .idx opening with a run of offset-0 tombstones, which a healthy .idx never does -- and .idx and .dat grow in lockstep, so the lost rows indexed exactly the first N .dat records. Detect it at load and re-derive them from a header-only walk over the head of .dat, cheap even against a remote tier, appending only the keys the .idx no longer names. * rust volume: mirror the .idx head tombstone recovery Port the Go detection and repair: an .idx opening with a run of offset-0 tombstones lost the Put rows indexing the first needles in .dat, so re-derive them at load from a header-only walk over the head of .dat and append the keys the .idx no longer names. * volume: put recovered .idx rows back in front instead of appending Appending left the offset-0 tombstone run at the head, so every later load re-walked .idx to the tail to notice the volume was already recovered, and the rows for the head of .dat sat past the .dat-tail row -- costing CheckVolumeDataIntegrity its O(1) path and breaking the ascending append order BinarySearchByAppendAtNs assumes. Rewrite .idx as the recovered rows followed by its current contents, through a temp file and a rename. .idx is back in .dat append order, so a later load stops after reading one row. * volume: keep the .idx mode when the repair replaces it The recovery renames a fresh temp file over .idx, so a fixed 0644 (Go) or whatever the umask allows (Rust) would silently widen an index an operator had locked down. Carry the mode off the file being replaced.
237 lines
7.1 KiB
Go
237 lines
7.1 KiB
Go
package storage
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/backend"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/idx"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/needle_map"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/types"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
// errStopIdxWalk ends an idx.WalkIndexFile early once the answer is known.
|
|
var errStopIdxWalk = errors.New("stop idx walk")
|
|
|
|
// repairIdxHeadTombstones restores the .idx rows that deletes on a tiered
|
|
// read-only volume used to overwrite.
|
|
//
|
|
// The sorted-file needle map opened .idx read-write but never seeded its write
|
|
// position, so a delete wrote its (key, offset 0, tombstone) row at .idx offset
|
|
// 0 and advanced one row at a time instead of appending. Every delete therefore
|
|
// replaced one more row at the front of .idx, and the rows it replaced -- the
|
|
// Put rows indexing the first needles in .dat -- were lost. Reads for those
|
|
// needles return not-found even though .dat still holds them intact.
|
|
//
|
|
// The damage leaves a fingerprint: .idx begins with a run of offset-0
|
|
// tombstones. A healthy .idx never does. Its first row is the Put for the first
|
|
// needle in .dat, and an offset-0 tombstone -- a delete against a tiered volume,
|
|
// which appends no .dat record and so has no extent to point at -- can only ever
|
|
// land at the tail.
|
|
//
|
|
// .idx and .dat grow in lockstep, so the clobbered rows indexed exactly the
|
|
// first N records of .dat. Re-deriving them is a header-only walk over the head
|
|
// of .dat, which stays cheap even when .dat is served from a remote tier.
|
|
//
|
|
// The recovered rows go back in front, where the rows they replace used to sit,
|
|
// and every existing row keeps its relative order behind them. That restores
|
|
// .idx to .dat append order, which several readers lean on: the fingerprint is
|
|
// gone so a later load stops after one row, the last row is the .dat-tail needle
|
|
// again so CheckVolumeDataIntegrity keeps its O(1) path, and
|
|
// BinarySearchByAppendAtNs sees ascending append timestamps.
|
|
func (v *Volume) repairIdxHeadTombstones() (restored int, err error) {
|
|
if v.DataBackend == nil {
|
|
return 0, nil
|
|
}
|
|
version := v.Version()
|
|
if !needle.IsSupportedVersion(version) {
|
|
return 0, nil
|
|
}
|
|
firstNeedleOffset := int64(v.SuperBlock.BlockSize())
|
|
idxFileName := v.FileName(".idx")
|
|
|
|
clobbered, err := idxHeadTombstoneCount(idxFileName)
|
|
if err != nil || clobbered == 0 {
|
|
return 0, err
|
|
}
|
|
|
|
glog.V(0).Infof("volume %d: %s starts with %d offset-0 tombstones, recovering the .idx rows they overwrote from %s",
|
|
v.Id, idxFileName, clobbered, v.FileName(".dat"))
|
|
|
|
lost, order, err := scanDatHead(v.DataBackend, version, firstNeedleOffset, clobbered)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if err = dropIndexedKeys(idxFileName, lost); err != nil {
|
|
return 0, err
|
|
}
|
|
if len(lost) == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
var rows []byte
|
|
for _, key := range order {
|
|
nv, ok := lost[key]
|
|
if !ok {
|
|
continue
|
|
}
|
|
rows = append(rows, nv.ToBytes()...)
|
|
restored++
|
|
}
|
|
return restored, prependIdxRows(idxFileName, rows)
|
|
}
|
|
|
|
// idxHeadTombstoneCount reports how many rows at the front of .idx are offset-0
|
|
// tombstones. A healthy .idx answers 0 on its first row.
|
|
func idxHeadTombstoneCount(idxFileName string) (clobbered int, err error) {
|
|
idxFile, err := os.Open(idxFileName)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return 0, nil
|
|
}
|
|
return 0, err
|
|
}
|
|
defer idxFile.Close()
|
|
|
|
err = idx.WalkIndexFile(idxFile, 0, func(_ types.NeedleId, offset types.Offset, size types.Size) error {
|
|
if !offset.IsZero() || !size.IsTombstone() {
|
|
return errStopIdxWalk
|
|
}
|
|
clobbered++
|
|
return nil
|
|
})
|
|
if errors.Is(err, errStopIdxWalk) {
|
|
err = nil
|
|
}
|
|
return clobbered, err
|
|
}
|
|
|
|
// scanDatHead reads the headers of the first limit records of .dat and returns
|
|
// the needles they hold, in .dat order. A record with an invalid size is a
|
|
// delete marker, so the key it names drops out of the result rather than being
|
|
// resurrected.
|
|
func scanDatHead(datBackend backend.BackendStorageFile, version needle.Version, firstNeedleOffset int64, limit int) (map[types.NeedleId]needle_map.NeedleValue, []types.NeedleId, error) {
|
|
scanner := &datHeadScanner{
|
|
limit: limit,
|
|
found: make(map[types.NeedleId]needle_map.NeedleValue),
|
|
}
|
|
if err := ScanVolumeFileFrom(version, datBackend, firstNeedleOffset, scanner); err != nil {
|
|
return nil, nil, fmt.Errorf("scan head of %s: %w", datBackend.Name(), err)
|
|
}
|
|
return scanner.found, scanner.order, nil
|
|
}
|
|
|
|
type datHeadScanner struct {
|
|
limit int
|
|
visited int
|
|
found map[types.NeedleId]needle_map.NeedleValue
|
|
order []types.NeedleId
|
|
}
|
|
|
|
func (s *datHeadScanner) VisitSuperBlock(super_block.SuperBlock) error { return nil }
|
|
|
|
func (s *datHeadScanner) ReadNeedleBody() bool { return false }
|
|
|
|
func (s *datHeadScanner) VisitNeedle(n *needle.Needle, offset int64, _, _ []byte) error {
|
|
if n.Size.IsValid() {
|
|
if _, seen := s.found[n.Id]; !seen {
|
|
s.order = append(s.order, n.Id)
|
|
}
|
|
s.found[n.Id] = needle_map.NeedleValue{Key: n.Id, Offset: types.ToOffset(offset), Size: n.Size}
|
|
} else {
|
|
delete(s.found, n.Id)
|
|
}
|
|
s.visited++
|
|
if s.visited >= s.limit {
|
|
return io.EOF
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// dropIndexedKeys removes every candidate the .idx already names, whether by a
|
|
// Put row or a tombstone. What is left is only what the clobbered rows held.
|
|
func dropIndexedKeys(idxFileName string, candidates map[types.NeedleId]needle_map.NeedleValue) error {
|
|
if len(candidates) == 0 {
|
|
return nil
|
|
}
|
|
idxFile, err := os.Open(idxFileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer idxFile.Close()
|
|
|
|
err = idx.WalkIndexFile(idxFile, 0, func(key types.NeedleId, _ types.Offset, _ types.Size) error {
|
|
delete(candidates, key)
|
|
if len(candidates) == 0 {
|
|
return errStopIdxWalk
|
|
}
|
|
return nil
|
|
})
|
|
if errors.Is(err, errStopIdxWalk) {
|
|
err = nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// prependIdxRows rewrites .idx as rows followed by its current contents,
|
|
// through a temp file and a rename so a crash mid-write never leaves a partial
|
|
// index at the live name.
|
|
func prependIdxRows(idxFileName string, rows []byte) error {
|
|
src, err := os.Open(idxFileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer src.Close()
|
|
|
|
srcStat, err := src.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tmpFileName := idxFileName + ".tmp"
|
|
dst, err := os.OpenFile(tmpFileName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcStat.Mode().Perm())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
committed := false
|
|
defer func() {
|
|
dst.Close()
|
|
if !committed {
|
|
os.Remove(tmpFileName)
|
|
}
|
|
}()
|
|
|
|
// The rename replaces .idx with this file, so it has to carry the mode the
|
|
// index already had -- O_CREATE alone leaves it at the umask's mercy.
|
|
if err = dst.Chmod(srcStat.Mode().Perm()); err != nil {
|
|
return err
|
|
}
|
|
if _, err = dst.Write(rows); err != nil {
|
|
return err
|
|
}
|
|
if _, err = io.Copy(dst, src); err != nil {
|
|
return err
|
|
}
|
|
if err = dst.Sync(); err != nil {
|
|
return err
|
|
}
|
|
if err = dst.Close(); err != nil {
|
|
return err
|
|
}
|
|
if err = os.Rename(tmpFileName, idxFileName); err != nil {
|
|
return err
|
|
}
|
|
if err = util.FsyncDir(filepath.Dir(idxFileName)); err != nil {
|
|
return err
|
|
}
|
|
committed = true
|
|
return nil
|
|
}
|