Files
seaweedfs/weed/filer/filer_notify_append.go
T
1176f975b3 filer: release metadata log chunks a failed append leaves unreferenced (#12455)
* filer: stop unbounded metadata-log growth on append-only files

Two independent leaks make a filer metadata log grow far faster than the
data it describes. On a cluster whose live logs weigh a few hundred
megabytes, they produced tens of gigabytes of metadata per day.

1. Metadata events embed the whole old and new entry, chunk lists
   included. An append-only file — a log or a journal written in small
   pieces — therefore re-serializes every chunk it ever had on every
   append, so the cost is quadratic in the number of chunks. A
   2,700-chunk file emits a couple of megabytes per append. Cap the
   carried chunk list at 256 entries and set
   metadata_event_chunks_truncated so subscribers that need the content
   read the entry from the filer instead of trusting the event.
   Subscribers that only track names, attributes, or chunk deltas keep
   working unchanged.

2. appendToFile uploads the chunk before committing the filer entry.
   When the commit fails — store error, cancelled context, filer
   shutting down — the chunk is stranded: no entry references it, so no
   later delete can mark it, and neither vacuum nor fsck can see it
   because both work from the entry index. The volume server keeps
   counting those bytes as live data forever. Release the chunk when
   the commit fails.

* filer: keep chunk lists in metadata events

Mount meta caches, peer filers replaying into their own store, and the
filer.sync, filer.backup and filer.meta.backup sinks all store the event's
entry as the file. Dropping the chunk list past a count left those copies
of a large file without content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* filer: release a metadata log chunk the lookup or commit leaves unreferenced

The flush retries a failed append with a new upload, so a store outage
left one stranded chunk per retry. A failed lookup strands the chunk too.
Use DeleteUncommittedChunks, as the other write paths do: the chunk was
never part of the file, so path chunk-deletion rules do not apply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* filer: keep a metadata log chunk the failed create stored

CreateEntry inserts a new entry before its parent folders, so a parent
failure returns an error with the entry already referencing the chunk.
Release it only when the store shows no entry, or an entry without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* filer: count a metadata log piece the failed create stored as appended

The flush retry would append the same bytes after it, and a piece cut
inside a record then breaks replay of that segment. The retry takes the
update path, which does not create parent folders either, so returning
the error only duplicated the piece.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Martin Morez <martin@morez.org>
Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:19:09 -08:00

142 lines
4.8 KiB
Go

package filer
import (
"context"
"errors"
"fmt"
"os"
"slices"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/operation"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
)
func (f *Filer) appendToFile(ctx context.Context, targetFile string, data []byte) error {
assignResult, uploadResult, err2 := f.assignAndUpload(ctx, targetFile, data)
if err2 != nil {
return err2
}
// The piece is already uploaded; commit it on a detached context so an
// expired shutdown deadline does not strand the chunk.
ctx = context.WithoutCancel(ctx)
return f.commitAppendedChunk(ctx, util.FullPath(targetFile), uploadResult.ToPbFileChunk(assignResult.Fid, 0, time.Now().UnixNano()))
}
// commitAppendedChunk appends an uploaded chunk to the file, releasing the
// chunk when it cannot be committed: the flush retries with a new upload.
func (f *Filer) commitAppendedChunk(ctx context.Context, fullpath util.FullPath, chunk *filer_pb.FileChunk) error {
// find out existing entry
entry, err := f.FindEntry(ctx, fullpath)
if err == filer_pb.ErrNotFound {
entry = &Entry{
FullPath: fullpath,
Attr: Attr{
Crtime: time.Now(),
Mtime: time.Now(),
Mode: os.FileMode(0644),
Uid: OS_UID,
Gid: OS_GID,
},
}
} else if err != nil {
f.DeleteUncommittedChunks(ctx, []*filer_pb.FileChunk{chunk})
return fmt.Errorf("find %s: %v", fullpath, err)
} else {
chunk.Offset = int64(TotalSize(entry.GetChunks()))
}
// append to existing chunks
entry.Chunks = append(entry.GetChunks(), chunk)
// update the entry
if err := f.CreateEntry(ctx, entry, nil, false, false, nil, false, f.MaxFilenameLength); err != nil {
// A create can store the entry and then fail on its parent folders;
// a retry would only append the piece again.
stored, findErr := f.FindEntry(ctx, fullpath)
if findErr == nil && slices.ContainsFunc(stored.GetChunks(), func(c *filer_pb.FileChunk) bool {
return c.GetFileIdString() == chunk.GetFileIdString()
}) {
glog.V(0).Infof("append to %s landed despite: %v", fullpath, err)
return nil
}
if findErr == nil || errors.Is(findErr, filer_pb.ErrNotFound) {
f.DeleteUncommittedChunks(ctx, []*filer_pb.FileChunk{chunk})
}
return err
}
return nil
}
// resolveMetadataLogAssignDiskType returns the disk type and matched path rule for
// metadata log volume assigns. Disk type uses the rule when set, otherwise
// Filer.DefaultDiskType (from -filer.disk), mirroring resolveAssignStorageOption.
func (f *Filer) resolveMetadataLogAssignDiskType(targetFile string) (string, *filer_pb.FilerConf_PathConf) {
if f.FilerConf == nil {
return f.DefaultDiskType, &filer_pb.FilerConf_PathConf{}
}
rule := f.FilerConf.MatchStorageRule(targetFile)
return util.Nvl(rule.DiskType, f.DefaultDiskType), rule
}
// metaLogCollectionFor resolves the system metadata log's collection: the
// filer.options.metaLog.collection override, then the filer default, then the
// matched storage rule.
func (f *Filer) metaLogCollectionFor(ruleCollection string) string {
return util.Nvl(f.metaLogTargetCollection, f.metaLogCollection, ruleCollection)
}
func (f *Filer) metaLogReplicationFor(ruleReplication string) string {
return util.Nvl(f.metaLogTargetReplication, f.metaLogReplication, ruleReplication)
}
func (f *Filer) assignAndUpload(ctx context.Context, targetFile string, data []byte) (*operation.AssignResult, *operation.UploadResult, error) {
// assign a volume location
diskType, rule := f.resolveMetadataLogAssignDiskType(targetFile)
assignRequest := &operation.VolumeAssignRequest{
Count: 1,
Collection: f.metaLogCollectionFor(rule.Collection),
Replication: f.metaLogReplicationFor(rule.Replication),
DiskType: diskType,
WritableVolumeCount: rule.VolumeGrowthCount,
ExpectedDataSize: uint64(len(data)),
}
assignResult, err := operation.Assign(ctx, f.GetMaster, f.masterGrpcDialOption(), assignRequest)
if err != nil {
return nil, nil, fmt.Errorf("AssignVolume: %w", err)
}
if assignResult.Error != "" {
return nil, nil, fmt.Errorf("AssignVolume error: %v", assignResult.Error)
}
// upload data
targetUrl := "http://" + assignResult.Url + "/" + assignResult.Fid
uploadOption := &operation.UploadOption{
UploadUrl: targetUrl,
Filename: "",
Cipher: f.Cipher,
IsInputCompressed: false,
MimeType: "",
PairMap: nil,
Jwt: assignResult.Auth,
}
uploader, err := operation.NewUploader()
if err != nil {
return nil, nil, fmt.Errorf("upload data %s: %v", targetUrl, err)
}
uploadResult, err := uploader.UploadData(ctx, data, uploadOption)
if err != nil {
return nil, nil, fmt.Errorf("upload data %s: %v", targetUrl, err)
}
// println("uploaded to", targetUrl)
return assignResult, uploadResult, nil
}