mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-14 01:58:33 +02:00
* filer: decode a listed entry without building its chunk list
A readdir reads attributes and never looks at chunks, but decoding an
entry builds the whole chunk list first: four allocations per chunk, all
of it thrown away. On a directory of ordinary 4MB-chunked files that is
most of what listing costs.
DecodeAttributesOnly walks the wire format and hands everything except
the chunks to the generated unmarshaller, so new fields in filer.proto
need no attention here. The chunks are still measured, because the S3
copy and multipart paths deliberately store a zero FileSize and let the
chunk extents define the size, but nothing is allocated to do it.
The blob is only re-encoded once a chunk is actually seen, so an entry
without any -- every directory, for one -- is unmarshalled where it lies
and pays nothing for the walk.
Listings opt in through the context, the way the lazy remote paths
already do; a store that ignores it stays correct.
chunks full attrs-only allocs
0 312.8n 310.1n ~ 1 -> 1
1 686.1n 411.1n -40.07% 7 -> 1
4 1.742u 667.4n -61.69% 24 -> 1
16 5.770u 1.544u -73.25% 86 -> 1
64 25.23u 6.004u -76.20% 328 -> 1
* mount: list directories with chunk lists omitted
The two meta cache listings behind a readdir are the only callers, and
neither reads a chunk. On 200k single-chunk files one enumeration goes
from 364ms to 277ms and drops a million allocations.
The read-through listing still fetches whole entries from the filer,
which would need the request to say it wants attributes only.
* mount: give the readdir benchmark's entries a chunk
Chunkless entries made the decode look far cheaper than it is, which is
the part of a listing worth measuring.
* filer: let a listing ask for entries without their chunk lists
The read-through readdir fetches whole entries over gRPC, and for a wide
directory the chunk lists are most of what crosses the wire and most of
what the client then unmarshals. A 4MB-chunked file is 113 bytes of
entry against 46 without its chunk.
ListEntriesRequest gains omit_chunks. The size a client needs is already
in the attributes, where the store decode folded the chunk extents in,
so dropping the list costs the client nothing.
The filer still reads the entries whole. A listing is where a TTL-expired
entry gets collected and deleted, and deleting one needs its chunks to
find the data, so omitting them there would leak. Only the response is
trimmed.
The hint moves to filer_pb so one context flag serves both transports:
the gRPC request sets omit_chunks, and a listing served from the local
store skips building the chunks. Cache population is unaffected either
way, since EnsureVisited starts from its own context.
* filer: reject a chunk the full decoder would reject
The walk skipped a chunk's bytes without looking inside them, so a
FileChunk carrying a corrupt nested fid, or a string that is not valid
UTF-8, sailed past the listing decoder while every other read of the same
entry still failed. The file listed with a plausible size and then gave
EIO on open, and corruption that used to fail the listing loudly was
hidden instead.
The chunk bytes are the one part of the blob the generated unmarshaller
never sees, so the two checks it would have made are made here: a
submessage has to parse, and a proto3 string has to be valid UTF-8.
FileChunk's only submessages are FileIds of scalars, so walking them is a
complete check. A descriptor-driven test fails if FileChunk ever gains a
field of either kind that the walk does not know to check, which is the
part that keeps this honest as filer.proto grows.
Taking the scratch buffer lazily, only once a chunk is actually dropped,
also takes the pool out of the path for entries that have none. Those
were measurably slower than the full decoder before; they are now level
with it. Each chunk's length prefix is parsed once rather than twice.
chunks full attrs-only vs base
0 171.4n 176.9n ~ (p=0.670)
1 366.6n 259.4n -29.24%
4 1.034u 500.2n -51.60%
16 3.905u 1.464u -62.52%
64 13.48u 5.195u -61.46%
* filer: carry the size before dropping chunks over the wire
Dropping the chunk list assumed every store folds the chunk extents into
FileSize when it decodes. A store that keeps entries as JSON rather than
as an encoded Entry never re-derives it, so an object written with a zero
FileSize kept its real size only in the chunks, and stripping them left
the client reading the file as empty. Stamp the size into the attributes
first, which costs nothing and does not depend on how the store loaded
the entry.
* mount: test that the readdir context reaches the store decode
Everything else exercises the decoder directly, so a refactor that
stopped threading the context would have reverted the whole thing with
every test still passing.
The benchmark's chunks also carried a constant legacy FileId, which
BeforeEntrySerialization reparses over Fid on the way in, so all 200k
entries stored one byte-identical chunk rather than the varying fixture
it looked like.
350 lines
12 KiB
Go
350 lines
12 KiB
Go
package filer_pb
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
var (
|
|
OS_UID = uint32(os.Getuid())
|
|
OS_GID = uint32(os.Getgid())
|
|
)
|
|
|
|
type FilerClient interface {
|
|
WithFilerClient(streamingMode bool, fn func(SeaweedFilerClient) error) error // 15 implementation
|
|
AdjustedUrl(location *Location) string
|
|
GetDataCenter() string
|
|
}
|
|
|
|
// GetEntry returns the entry together with the log position the filer stamped
|
|
// for it and the signature of the filer that stamped it — positions are only
|
|
// comparable within one filer's clock. Both are zero against a filer that does
|
|
// not stamp them.
|
|
func GetEntry(ctx context.Context, filerClient FilerClient, fullFilePath util.FullPath) (entry *Entry, logTsNs int64, logSignature int32, err error) {
|
|
|
|
dir, name := fullFilePath.DirAndName()
|
|
|
|
err = filerClient.WithFilerClient(false, func(client SeaweedFilerClient) error {
|
|
|
|
request := &LookupDirectoryEntryRequest{
|
|
Directory: dir,
|
|
Name: name,
|
|
}
|
|
|
|
// glog.V(3).Infof("read %s request: %v", fullFilePath, request)
|
|
resp, err := LookupEntry(ctx, client, request)
|
|
if err != nil {
|
|
glog.V(3).InfofCtx(ctx, "read %s %v: %v", fullFilePath, resp, err)
|
|
return err
|
|
}
|
|
|
|
if resp.Entry == nil {
|
|
// glog.V(3).Infof("read %s entry: %v", fullFilePath, entry)
|
|
return nil
|
|
}
|
|
|
|
entry = resp.Entry
|
|
logTsNs = resp.LogTsNs
|
|
logSignature = resp.LogSignature
|
|
return nil
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
// ListSnapshotTsNsTrailerKey carries the listing snapshot in the ListEntries
|
|
// stream trailer, so empty listings can convey it without a response older
|
|
// consumers would mistake for an entry.
|
|
const ListSnapshotTsNsTrailerKey = "sw-list-snapshot-ts-ns"
|
|
|
|
type EachEntryFunction func(entry *Entry, isLast bool) error
|
|
|
|
func ReadDirAllEntries(ctx context.Context, filerClient FilerClient, fullDirPath util.FullPath, prefix string, fn EachEntryFunction) (err error) {
|
|
_, err = ReadDirAllEntriesWithSnapshot(ctx, filerClient, fullDirPath, prefix, fn)
|
|
return err
|
|
}
|
|
|
|
func ReadDirAllEntriesWithSnapshot(ctx context.Context, filerClient FilerClient, fullDirPath util.FullPath, prefix string, fn EachEntryFunction) (snapshotTsNs int64, err error) {
|
|
var counter uint32
|
|
var startFrom string
|
|
var counterFunc = func(entry *Entry, isLast bool) error {
|
|
counter++
|
|
startFrom = entry.Name
|
|
return fn(entry, isLast)
|
|
}
|
|
|
|
var paginationLimit uint32 = 10000
|
|
|
|
if snapshotTsNs, err = doListWithSnapshot(ctx, filerClient, fullDirPath, prefix, counterFunc, "", false, paginationLimit, 0); err != nil {
|
|
return snapshotTsNs, err
|
|
}
|
|
|
|
for counter == paginationLimit {
|
|
counter = 0
|
|
if _, err = doListWithSnapshot(ctx, filerClient, fullDirPath, prefix, counterFunc, startFrom, false, paginationLimit, snapshotTsNs); err != nil {
|
|
return snapshotTsNs, err
|
|
}
|
|
}
|
|
|
|
return snapshotTsNs, nil
|
|
}
|
|
|
|
func List(ctx context.Context, filerClient FilerClient, parentDirectoryPath, prefix string, fn EachEntryFunction, startFrom string, inclusive bool, limit uint32) (err error) {
|
|
return filerClient.WithFilerClient(false, func(client SeaweedFilerClient) error {
|
|
return doSeaweedList(ctx, client, util.FullPath(parentDirectoryPath), prefix, fn, startFrom, inclusive, limit)
|
|
})
|
|
}
|
|
|
|
func doListWithSnapshot(ctx context.Context, filerClient FilerClient, fullDirPath util.FullPath, prefix string, fn EachEntryFunction, startFrom string, inclusive bool, limit uint32, snapshotTsNs int64) (actualSnapshotTsNs int64, err error) {
|
|
err = filerClient.WithFilerClient(false, func(client SeaweedFilerClient) error {
|
|
actualSnapshotTsNs, err = DoSeaweedListWithSnapshot(ctx, client, fullDirPath, prefix, fn, startFrom, inclusive, limit, snapshotTsNs)
|
|
return err
|
|
})
|
|
return actualSnapshotTsNs, err
|
|
}
|
|
|
|
func SeaweedList(ctx context.Context, client SeaweedFilerClient, parentDirectoryPath, prefix string, fn EachEntryFunction, startFrom string, inclusive bool, limit uint32) (err error) {
|
|
return doSeaweedList(ctx, client, util.FullPath(parentDirectoryPath), prefix, fn, startFrom, inclusive, limit)
|
|
}
|
|
|
|
func doSeaweedList(ctx context.Context, client SeaweedFilerClient, fullDirPath util.FullPath, prefix string, fn EachEntryFunction, startFrom string, inclusive bool, limit uint32) (err error) {
|
|
_, err = DoSeaweedListWithSnapshot(ctx, client, fullDirPath, prefix, fn, startFrom, inclusive, limit, 0)
|
|
return err
|
|
}
|
|
|
|
func DoSeaweedListWithSnapshot(ctx context.Context, client SeaweedFilerClient, fullDirPath util.FullPath, prefix string, fn EachEntryFunction, startFrom string, inclusive bool, limit uint32, snapshotTsNs int64) (actualSnapshotTsNs int64, err error) {
|
|
// Redundancy limit to make it correctly judge whether it is the last file.
|
|
redLimit := limit
|
|
|
|
if limit < math.MaxInt32 && limit != 0 {
|
|
redLimit = limit + 1
|
|
}
|
|
if redLimit > math.MaxInt32 {
|
|
redLimit = math.MaxInt32
|
|
}
|
|
request := &ListEntriesRequest{
|
|
Directory: string(fullDirPath),
|
|
Prefix: prefix,
|
|
StartFromFileName: startFrom,
|
|
Limit: redLimit,
|
|
InclusiveStartFrom: inclusive,
|
|
SnapshotTsNs: snapshotTsNs,
|
|
OmitChunks: ChunksOmitted(ctx),
|
|
}
|
|
|
|
// Preserve the caller-requested snapshot so pagination uses the same
|
|
// boundary across pages. For first requests (snapshotTsNs==0) we do NOT
|
|
// synthesize a client-side timestamp — if the server returns no entries,
|
|
// we return 0 so callers like CompleteDirectoryBuild know no server
|
|
// snapshot was received and can replay all buffered events without
|
|
// clock-skew-sensitive filtering.
|
|
actualSnapshotTsNs = snapshotTsNs
|
|
|
|
glog.V(4).InfofCtx(ctx, "read directory: %v", request)
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
stream, err := client.ListEntries(ctx, request)
|
|
if err != nil {
|
|
return actualSnapshotTsNs, fmt.Errorf("list %s: %v", fullDirPath, err)
|
|
}
|
|
|
|
var prevEntry *Entry
|
|
count := 0
|
|
for {
|
|
resp, recvErr := stream.Recv()
|
|
if recvErr != nil {
|
|
if recvErr == io.EOF {
|
|
if prevEntry != nil {
|
|
if err := fn(prevEntry, true); err != nil {
|
|
return actualSnapshotTsNs, err
|
|
}
|
|
}
|
|
// An empty listing carries no in-band snapshot (a snapshot-only
|
|
// response would be read as an entry by older consumers); newer
|
|
// filers send it in the stream trailer instead.
|
|
if actualSnapshotTsNs == 0 {
|
|
if values := stream.Trailer().Get(ListSnapshotTsNsTrailerKey); len(values) > 0 {
|
|
if trailerTsNs, parseErr := strconv.ParseInt(values[0], 10, 64); parseErr == nil {
|
|
actualSnapshotTsNs = trailerTsNs
|
|
}
|
|
}
|
|
}
|
|
break
|
|
} else {
|
|
return actualSnapshotTsNs, recvErr
|
|
}
|
|
}
|
|
if resp.SnapshotTsNs != 0 {
|
|
actualSnapshotTsNs = resp.SnapshotTsNs
|
|
}
|
|
if prevEntry != nil {
|
|
if err := fn(prevEntry, false); err != nil {
|
|
return actualSnapshotTsNs, err
|
|
}
|
|
}
|
|
prevEntry = resp.Entry
|
|
count++
|
|
if count > int(limit) && limit != 0 {
|
|
prevEntry = nil
|
|
}
|
|
}
|
|
|
|
return actualSnapshotTsNs, nil
|
|
}
|
|
|
|
func Exists(ctx context.Context, filerClient FilerClient, parentDirectoryPath string, entryName string, isDirectory bool) (exists bool, err error) {
|
|
|
|
err = filerClient.WithFilerClient(false, func(client SeaweedFilerClient) error {
|
|
|
|
request := &LookupDirectoryEntryRequest{
|
|
Directory: parentDirectoryPath,
|
|
Name: entryName,
|
|
}
|
|
|
|
glog.V(4).InfofCtx(ctx, "exists entry %v/%v: %v", parentDirectoryPath, entryName, request)
|
|
resp, err := LookupEntry(ctx, client, request)
|
|
if err != nil {
|
|
if err == ErrNotFound {
|
|
exists = false
|
|
return nil
|
|
}
|
|
glog.V(0).InfofCtx(ctx, "exists entry %v: %v", request, err)
|
|
return fmt.Errorf("exists entry %s/%s: %v", parentDirectoryPath, entryName, err)
|
|
}
|
|
|
|
exists = resp.Entry.IsDirectory == isDirectory
|
|
|
|
return nil
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
func Mkdir(ctx context.Context, filerClient FilerClient, parentDirectoryPath string, dirName string, fn func(entry *Entry)) error {
|
|
return filerClient.WithFilerClient(false, func(client SeaweedFilerClient) error {
|
|
return DoMkdir(ctx, client, parentDirectoryPath, dirName, fn)
|
|
})
|
|
}
|
|
|
|
func DoMkdir(ctx context.Context, client SeaweedFilerClient, parentDirectoryPath string, dirName string, fn func(entry *Entry)) error {
|
|
entry := &Entry{
|
|
Name: dirName,
|
|
IsDirectory: true,
|
|
Attributes: &FuseAttributes{
|
|
Mtime: time.Now().Unix(),
|
|
Crtime: time.Now().Unix(),
|
|
FileMode: uint32(0777 | os.ModeDir),
|
|
Uid: OS_UID,
|
|
Gid: OS_GID,
|
|
},
|
|
}
|
|
|
|
if fn != nil {
|
|
fn(entry)
|
|
}
|
|
|
|
request := &CreateEntryRequest{
|
|
Directory: parentDirectoryPath,
|
|
Entry: entry,
|
|
}
|
|
|
|
glog.V(1).InfofCtx(ctx, "mkdir: %v", request)
|
|
if err := CreateEntry(ctx, client, request); err != nil {
|
|
glog.V(0).InfofCtx(ctx, "mkdir %v: %v", request, err)
|
|
return fmt.Errorf("mkdir %s/%s: %v", parentDirectoryPath, dirName, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func MkFile(ctx context.Context, filerClient FilerClient, parentDirectoryPath string, fileName string, chunks []*FileChunk, fn func(entry *Entry)) error {
|
|
return filerClient.WithFilerClient(false, func(client SeaweedFilerClient) error {
|
|
|
|
entry := &Entry{
|
|
Name: fileName,
|
|
IsDirectory: false,
|
|
Attributes: &FuseAttributes{
|
|
Mtime: time.Now().Unix(),
|
|
Crtime: time.Now().Unix(),
|
|
FileMode: uint32(0770),
|
|
Uid: OS_UID,
|
|
Gid: OS_GID,
|
|
},
|
|
Chunks: chunks,
|
|
}
|
|
|
|
if fn != nil {
|
|
fn(entry)
|
|
}
|
|
|
|
request := &CreateEntryRequest{
|
|
Directory: parentDirectoryPath,
|
|
Entry: entry,
|
|
}
|
|
|
|
glog.V(1).InfofCtx(ctx, "create file: %s/%s", parentDirectoryPath, fileName)
|
|
if err := CreateEntry(ctx, client, request); err != nil {
|
|
glog.V(0).InfofCtx(ctx, "create file %v:%v", request, err)
|
|
return fmt.Errorf("create file %s/%s: %v", parentDirectoryPath, fileName, err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func Remove(ctx context.Context, filerClient FilerClient, parentDirectoryPath, name string, isDeleteData, isRecursive, ignoreRecursiveErr, isFromOtherCluster bool, signatures []int32) error {
|
|
_, err := RemoveWithResponse(ctx, filerClient, parentDirectoryPath, name, isDeleteData, isRecursive, ignoreRecursiveErr, isFromOtherCluster, signatures)
|
|
return err
|
|
}
|
|
|
|
func DoRemove(ctx context.Context, client SeaweedFilerClient, parentDirectoryPath string, name string, isDeleteData bool, isRecursive bool, ignoreRecursiveErr bool, isFromOtherCluster bool, signatures []int32) error {
|
|
_, err := DoRemoveWithResponse(ctx, client, parentDirectoryPath, name, isDeleteData, isRecursive, ignoreRecursiveErr, isFromOtherCluster, signatures)
|
|
return err
|
|
}
|
|
|
|
func RemoveWithResponse(ctx context.Context, filerClient FilerClient, parentDirectoryPath, name string, isDeleteData, isRecursive, ignoreRecursiveErr, isFromOtherCluster bool, signatures []int32) (*DeleteEntryResponse, error) {
|
|
var resp *DeleteEntryResponse
|
|
err := filerClient.WithFilerClient(false, func(client SeaweedFilerClient) error {
|
|
var innerErr error
|
|
resp, innerErr = DoRemoveWithResponse(ctx, client, parentDirectoryPath, name, isDeleteData, isRecursive, ignoreRecursiveErr, isFromOtherCluster, signatures)
|
|
return innerErr
|
|
})
|
|
return resp, err
|
|
}
|
|
|
|
func DoRemoveWithResponse(ctx context.Context, client SeaweedFilerClient, parentDirectoryPath string, name string, isDeleteData bool, isRecursive bool, ignoreRecursiveErr bool, isFromOtherCluster bool, signatures []int32) (*DeleteEntryResponse, error) {
|
|
deleteEntryRequest := &DeleteEntryRequest{
|
|
Directory: parentDirectoryPath,
|
|
Name: name,
|
|
IsDeleteData: isDeleteData,
|
|
IsRecursive: isRecursive,
|
|
IgnoreRecursiveError: ignoreRecursiveErr,
|
|
IsFromOtherCluster: isFromOtherCluster,
|
|
Signatures: signatures,
|
|
}
|
|
if resp, err := client.DeleteEntry(ctx, deleteEntryRequest); err != nil {
|
|
if strings.Contains(err.Error(), ErrNotFound.Error()) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
} else {
|
|
if resp.Error != "" {
|
|
if strings.Contains(resp.Error, ErrNotFound.Error()) {
|
|
return nil, nil
|
|
}
|
|
return nil, errors.New(resp.Error)
|
|
}
|
|
return resp, nil
|
|
}
|
|
}
|