Cloud/remote storage & tiering: configurable multipart upload/download concurrency (#11319)

* pb: add multipart concurrency fields to RemoteConf and tier move requests

RemoteConf gains upload_concurrency/download_concurrency (0 = client
default); VolumeTierMoveDatToRemote/FromRemote requests gain a
concurrency field (0 = backend default).

* remote storage: honor RemoteConf upload/download concurrency in s3 and azure clients

s3 client: ReadFile passes conf download_concurrency to the downloader,
WriteFile uses upload_concurrency for the uploader; previously
hard-coded 1 upload / 5 download parts. 0 keeps defaults. Same for
azure client.

* storage: plumb concurrency through backend interface and tier upload/download

BackendStorage.CopyFile/DownloadFile take a concurrency hint (<=0 =
backend configured default); s3 backend reads
upload_concurrency/download_concurrency from scaffold config with
parseConcurrency fallback, rclone updated to the new signature. Tier
move gRPC handlers forward the request concurrency to the backend.

* shell: -upload_concurrency/-download_concurrency for remote.configure, -concurrent for volume.tier

remote.configure exposes upload/download concurrency persisted into
RemoteConf; volume.tier move/evict commands forward -concurrent to the
tier move requests. Documented in master-cloud.toml scaffold.

* test: cover concurrency propagation in remote tier integration test

* remote.configure: merge existing config on partial update

Load the stored RemoteConf before saving so a partial update (e.g. only
-upload_concurrency) preserves credentials, endpoints, and type instead
of replacing them with new-config defaults. Only treat a confirmed
ErrNotFound as a new configuration; propagate all other load errors so a
transient filer failure does not overwrite stored settings.

On a type transition, reset backend-specific fields to the destination
type's new-config defaults rather than inheriting the old backend's
empty values. Bound configured concurrency to a sane maximum.

* remote storage: honor configured download concurrency in S3 and Azure

ReadFileWithConcurrency now resolves a zero request override against the
client's configured download_concurrency (new downloadConcurrency()
helpers), so the remote-mount/cache read path honors
RemoteConf.DownloadConcurrency instead of the hard-coded default.

Azure also clamps the resolved value to math.MaxUint16 regardless of
whether the fallback was used, preventing uint16 wraparound when a
configured value exceeds 65535.

* shell: rename -concurrent to -concurrency and validate tier transfer bounds

Rename the -concurrent flag to -concurrency across volume.tier.upload,
volume.tier.download, and volume.tier.compact to match the proto field and
RemoteConf field names. Add validateTierConcurrency to reject values that
would wrap int32 or exceed a 1024 cap before constructing the request.

* server: clamp tier move concurrency in gRPC handlers

Add clampTierConcurrency to both VolumeTierMoveDatToRemote and
VolumeTierMoveDatFromRemote handlers so a direct gRPC caller cannot spawn
an unbounded number of network workers.

* trim verbose comments added with concurrency feature

Remove redundant doc comments on the backend interface, rclone backend,
s3_backend parseConcurrency, and test helpers that restated the obvious.

* remote.configure: apply type defaults before re-parse so explicit flags win

applyTypeDefaults ran after the second flag parse, overwriting explicit
destination flags (e.g. -s3.region=eu-west-1) with new-config defaults.
Move the type-transition default reset before the re-parse so user-supplied
flags override the destination defaults.

* remote.configure: only treat explicit -type as a type transition

The first parse defaults -type to s3, so a concurrency-only update on an
existing non-S3 config captured requestedType=s3 and wrongly triggered a
type transition, resetting the stored backend to S3. Use fs.Visit to
detect whether -type was explicitly supplied; an omitted -type keeps the
stored backend.

---------

Co-authored-by: Jack Meredith <9480542+jackusm@users.noreply.github.com>
Co-authored-by: Chris Lu <chris.lu@gmail.com>
This commit is contained in:
jackusm
2026-09-14 22:09:08 -07:00
committed by GitHub
co-authored by Jack Meredith Chris Lu
parent e735c12869
commit 87332eb60b
22 changed files with 506 additions and 118 deletions
+2
View File
@@ -30,3 +30,5 @@ sleep_minutes = 17 # sleep minutes between each script execution
bucket = "volume_bucket" # an existing bucket
endpoint = "http://server2:8333"
storage_class = "STANDARD_IA"
# upload_concurrency = 5 # concurrent multipart part uploads per volume (volume.tier.upload -concurrent overrides)
# download_concurrency = 5 # concurrent multipart part downloads per volume (volume.tier.download -concurrent overrides)
+3
View File
@@ -65,6 +65,9 @@ message RemoteConf {
string contabo_secret_key = 69;
string contabo_endpoint = 70;
string contabo_region = 71;
uint32 upload_concurrency = 72; // multipart upload concurrency per file, 0 = client default (1 for S3, 16 for Azure)
uint32 download_concurrency = 73; // multipart download concurrency per read, 0 = client default (5 for S3, 16 for Azure)
}
message RemoteStorageMapping {
+20 -2
View File
@@ -71,6 +71,8 @@ type RemoteConf struct {
ContaboSecretKey string `protobuf:"bytes,69,opt,name=contabo_secret_key,json=contaboSecretKey,proto3" json:"contabo_secret_key,omitempty"`
ContaboEndpoint string `protobuf:"bytes,70,opt,name=contabo_endpoint,json=contaboEndpoint,proto3" json:"contabo_endpoint,omitempty"`
ContaboRegion string `protobuf:"bytes,71,opt,name=contabo_region,json=contaboRegion,proto3" json:"contabo_region,omitempty"`
UploadConcurrency uint32 `protobuf:"varint,72,opt,name=upload_concurrency,json=uploadConcurrency,proto3" json:"upload_concurrency,omitempty"` // multipart upload concurrency per file, 0 = client default (1 for S3, 16 for Azure)
DownloadConcurrency uint32 `protobuf:"varint,73,opt,name=download_concurrency,json=downloadConcurrency,proto3" json:"download_concurrency,omitempty"` // multipart download concurrency per read, 0 = client default (5 for S3, 16 for Azure)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -420,6 +422,20 @@ func (x *RemoteConf) GetContaboRegion() string {
return ""
}
func (x *RemoteConf) GetUploadConcurrency() uint32 {
if x != nil {
return x.UploadConcurrency
}
return 0
}
func (x *RemoteConf) GetDownloadConcurrency() uint32 {
if x != nil {
return x.DownloadConcurrency
}
return 0
}
type RemoteStorageMapping struct {
state protoimpl.MessageState `protogen:"open.v1"`
Mappings map[string]*RemoteStorageLocation `protobuf:"bytes,1,rep,name=mappings,proto3" json:"mappings,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
@@ -552,7 +568,7 @@ var File_remote_proto protoreflect.FileDescriptor
const file_remote_proto_rawDesc = "" +
"\n" +
"\fremote.proto\x12\tremote_pb\"\xea\x0e\n" +
"\fremote.proto\x12\tremote_pb\"\xcc\x0f\n" +
"\n" +
"RemoteConf\x12\x12\n" +
"\x04type\x18\x01 \x01(\tR\x04type\x12\x12\n" +
@@ -601,7 +617,9 @@ const file_remote_proto_rawDesc = "" +
"\x12contabo_access_key\x18D \x01(\tR\x10contaboAccessKey\x12,\n" +
"\x12contabo_secret_key\x18E \x01(\tR\x10contaboSecretKey\x12)\n" +
"\x10contabo_endpoint\x18F \x01(\tR\x0fcontaboEndpoint\x12%\n" +
"\x0econtabo_region\x18G \x01(\tR\rcontaboRegion\"\xff\x01\n" +
"\x0econtabo_region\x18G \x01(\tR\rcontaboRegion\x12-\n" +
"\x12upload_concurrency\x18H \x01(\rR\x11uploadConcurrency\x121\n" +
"\x14download_concurrency\x18I \x01(\rR\x13downloadConcurrency\"\xff\x01\n" +
"\x14RemoteStorageMapping\x12I\n" +
"\bmappings\x18\x01 \x03(\v2-.remote_pb.RemoteStorageMapping.MappingsEntryR\bmappings\x12=\n" +
"\x1bprimary_bucket_storage_name\x18\x02 \x01(\tR\x18primaryBucketStorageName\x1a]\n" +
+2
View File
@@ -659,6 +659,7 @@ message VolumeTierMoveDatToRemoteRequest {
string collection = 2;
string destination_backend_name = 3;
bool keep_local_dat_file = 4;
int32 concurrency = 5; // multipart upload concurrency, 0 = backend default (5 for S3)
}
message VolumeTierMoveDatToRemoteResponse {
int64 processed = 1;
@@ -669,6 +670,7 @@ message VolumeTierMoveDatFromRemoteRequest {
uint32 volume_id = 1;
string collection = 2;
bool keep_remote_dat_file = 3;
int32 concurrency = 4; // multipart download concurrency, 0 = backend default (5 for S3)
}
message VolumeTierMoveDatFromRemoteResponse {
int64 processed = 1;
+22 -4
View File
@@ -5471,6 +5471,7 @@ type VolumeTierMoveDatToRemoteRequest struct {
Collection string `protobuf:"bytes,2,opt,name=collection,proto3" json:"collection,omitempty"`
DestinationBackendName string `protobuf:"bytes,3,opt,name=destination_backend_name,json=destinationBackendName,proto3" json:"destination_backend_name,omitempty"`
KeepLocalDatFile bool `protobuf:"varint,4,opt,name=keep_local_dat_file,json=keepLocalDatFile,proto3" json:"keep_local_dat_file,omitempty"`
Concurrency int32 `protobuf:"varint,5,opt,name=concurrency,proto3" json:"concurrency,omitempty"` // multipart upload concurrency, 0 = backend default (5 for S3)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -5533,6 +5534,13 @@ func (x *VolumeTierMoveDatToRemoteRequest) GetKeepLocalDatFile() bool {
return false
}
func (x *VolumeTierMoveDatToRemoteRequest) GetConcurrency() int32 {
if x != nil {
return x.Concurrency
}
return 0
}
type VolumeTierMoveDatToRemoteResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Processed int64 `protobuf:"varint,1,opt,name=processed,proto3" json:"processed,omitempty"`
@@ -5590,6 +5598,7 @@ type VolumeTierMoveDatFromRemoteRequest struct {
VolumeId uint32 `protobuf:"varint,1,opt,name=volume_id,json=volumeId,proto3" json:"volume_id,omitempty"`
Collection string `protobuf:"bytes,2,opt,name=collection,proto3" json:"collection,omitempty"`
KeepRemoteDatFile bool `protobuf:"varint,3,opt,name=keep_remote_dat_file,json=keepRemoteDatFile,proto3" json:"keep_remote_dat_file,omitempty"`
Concurrency int32 `protobuf:"varint,4,opt,name=concurrency,proto3" json:"concurrency,omitempty"` // multipart download concurrency, 0 = backend default (5 for S3)
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -5645,6 +5654,13 @@ func (x *VolumeTierMoveDatFromRemoteRequest) GetKeepRemoteDatFile() bool {
return false
}
func (x *VolumeTierMoveDatFromRemoteRequest) GetConcurrency() int32 {
if x != nil {
return x.Concurrency
}
return 0
}
type VolumeTierMoveDatFromRemoteResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Processed int64 `protobuf:"varint,1,opt,name=processed,proto3" json:"processed,omitempty"`
@@ -7644,23 +7660,25 @@ const file_volume_server_proto_rawDesc = "" +
"\vBytesOffset\x18\x04 \x01(\rR\vBytesOffset\x12\"\n" +
"\rdat_file_size\x18\x05 \x01(\x03R\vdatFileSize\x12 \n" +
"\vDestroyTime\x18\x06 \x01(\x04R\vDestroyTime\x12\x1b\n" +
"\tread_only\x18\a \x01(\bR\breadOnly\"\xc8\x01\n" +
"\tread_only\x18\a \x01(\bR\breadOnly\"\xea\x01\n" +
" VolumeTierMoveDatToRemoteRequest\x12\x1b\n" +
"\tvolume_id\x18\x01 \x01(\rR\bvolumeId\x12\x1e\n" +
"\n" +
"collection\x18\x02 \x01(\tR\n" +
"collection\x128\n" +
"\x18destination_backend_name\x18\x03 \x01(\tR\x16destinationBackendName\x12-\n" +
"\x13keep_local_dat_file\x18\x04 \x01(\bR\x10keepLocalDatFile\"s\n" +
"\x13keep_local_dat_file\x18\x04 \x01(\bR\x10keepLocalDatFile\x12 \n" +
"\vconcurrency\x18\x05 \x01(\x05R\vconcurrency\"s\n" +
"!VolumeTierMoveDatToRemoteResponse\x12\x1c\n" +
"\tprocessed\x18\x01 \x01(\x03R\tprocessed\x120\n" +
"\x13processedPercentage\x18\x02 \x01(\x02R\x13processedPercentage\"\x92\x01\n" +
"\x13processedPercentage\x18\x02 \x01(\x02R\x13processedPercentage\"\xb4\x01\n" +
"\"VolumeTierMoveDatFromRemoteRequest\x12\x1b\n" +
"\tvolume_id\x18\x01 \x01(\rR\bvolumeId\x12\x1e\n" +
"\n" +
"collection\x18\x02 \x01(\tR\n" +
"collection\x12/\n" +
"\x14keep_remote_dat_file\x18\x03 \x01(\bR\x11keepRemoteDatFile\"u\n" +
"\x14keep_remote_dat_file\x18\x03 \x01(\bR\x11keepRemoteDatFile\x12 \n" +
"\vconcurrency\x18\x04 \x01(\x05R\vconcurrency\"u\n" +
"#VolumeTierMoveDatFromRemoteResponse\x12\x1c\n" +
"\tprocessed\x18\x01 \x01(\x03R\tprocessed\x120\n" +
"\x13processedPercentage\x18\x02 \x01(\x02R\x13processedPercentage\"\x1b\n" +
@@ -153,6 +153,20 @@ type azureRemoteStorageClient struct {
var _ = remote_storage.RemoteStorageClient(&azureRemoteStorageClient{})
var _ = remote_storage.RemoteStorageConcurrentReader(&azureRemoteStorageClient{})
func (az *azureRemoteStorageClient) uploadConcurrency() int {
if n := int(az.conf.GetUploadConcurrency()); n > 0 {
return n
}
return defaultConcurrency
}
func (az *azureRemoteStorageClient) downloadConcurrency() int {
if n := int(az.conf.GetDownloadConcurrency()); n > 0 {
return n
}
return defaultReadConcurrency
}
func (az *azureRemoteStorageClient) ListDirectory(ctx context.Context, loc *remote_pb.RemoteStorageLocation, visitFn remote_storage.VisitFunc) (err error) {
pathKey := loc.Path[1:]
if pathKey != "" && !strings.HasSuffix(pathKey, "/") {
@@ -303,7 +317,7 @@ func (az *azureRemoteStorageClient) Traverse(loc *remote_pb.RemoteStorageLocatio
}
func (az *azureRemoteStorageClient) ReadFile(loc *remote_pb.RemoteStorageLocation, offset int64, size int64) (data []byte, err error) {
return az.ReadFileWithConcurrency(loc, offset, size, defaultReadConcurrency)
return az.ReadFileWithConcurrency(loc, offset, size, 0)
}
// ReadFileWithConcurrency fetches a byte range of a blob using the Azure SDK's
@@ -322,9 +336,9 @@ func (az *azureRemoteStorageClient) ReadFileWithConcurrency(loc *remote_pb.Remot
}
if concurrency <= 0 {
concurrency = defaultReadConcurrency
} else if concurrency > math.MaxUint16 {
// DownloadBufferOptions.Concurrency is uint16; clamp to avoid wraparound.
concurrency = az.downloadConcurrency()
}
if concurrency > math.MaxUint16 {
concurrency = math.MaxUint16
}
@@ -448,7 +462,7 @@ func (az *azureRemoteStorageClient) WriteFile(loc *remote_pb.RemoteStorageLocati
_, err = blobClient.UploadStream(context.Background(), reader, &blockblob.UploadStreamOptions{
BlockSize: defaultBlockSize,
Concurrency: defaultConcurrency,
Concurrency: az.uploadConcurrency(),
HTTPHeaders: httpHeaders,
Metadata: metadata,
})
+22 -3
View File
@@ -229,6 +229,25 @@ type s3RemoteStorageClient struct {
conn s3iface.S3API
}
const (
defaultUploadConcurrency = 1
defaultReadConcurrency = 5
)
func (s *s3RemoteStorageClient) uploadConcurrency() int {
if n := int(s.conf.GetUploadConcurrency()); n > 0 {
return n
}
return defaultUploadConcurrency
}
func (s *s3RemoteStorageClient) downloadConcurrency() int {
if n := int(s.conf.GetDownloadConcurrency()); n > 0 {
return n
}
return defaultReadConcurrency
}
var _ = remote_storage.RemoteStorageClient(&s3RemoteStorageClient{})
func (s *s3RemoteStorageClient) Traverse(remote *remote_pb.RemoteStorageLocation, visitFn remote_storage.VisitFunc) (err error) {
@@ -374,12 +393,12 @@ func (s *s3RemoteStorageClient) StatFile(loc *remote_pb.RemoteStorageLocation) (
}
func (s *s3RemoteStorageClient) ReadFile(loc *remote_pb.RemoteStorageLocation, offset int64, size int64) (data []byte, err error) {
return s.ReadFileWithConcurrency(loc, offset, size, 5)
return s.ReadFileWithConcurrency(loc, offset, size, 0)
}
func (s *s3RemoteStorageClient) ReadFileWithConcurrency(loc *remote_pb.RemoteStorageLocation, offset int64, size int64, concurrency int) (data []byte, err error) {
if concurrency <= 0 {
concurrency = 5
concurrency = s.downloadConcurrency()
}
downloader := s3manager.NewDownloaderWithClient(s.conn, func(u *s3manager.Downloader) {
u.PartSize = int64(4 * 1024 * 1024)
@@ -493,7 +512,7 @@ func (s *s3RemoteStorageClient) WriteFile(loc *remote_pb.RemoteStorageLocation,
// Create an uploader with the session and custom options
uploader := s3manager.NewUploaderWithClient(s.conn, func(u *s3manager.Uploader) {
u.PartSize = partSize
u.Concurrency = 1
u.Concurrency = s.uploadConcurrency()
})
// process tagging
+1 -1
View File
@@ -70,7 +70,7 @@ func (vs *VolumeServer) VolumeTierMoveDatFromRemote(req *volume_server_pb.Volume
}
// copy the data file (DownloadFile opens, fsyncs, and closes the .dat internally)
datFileName := v.FileName(".dat")
_, err := backendStorage.DownloadFile(datFileName, storageKey, fn)
_, err := backendStorage.DownloadFile(datFileName, storageKey, fn, clampTierConcurrency(int(req.Concurrency)))
if err != nil {
return fmt.Errorf("backend %s copy file %s: %v", storageName, datFileName, err)
}
+59 -3
View File
@@ -32,6 +32,9 @@ type tierTestBackend struct {
mu sync.Mutex
deletes []string
lastUploadConcurrency int
lastDownloadConcurrency int
}
func (b *tierTestBackend) ToProperties() map[string]string { return map[string]string{"root": b.root} }
@@ -40,7 +43,10 @@ func (b *tierTestBackend) NewStorageFile(key string, tierInfo *volume_server_pb.
return &tierTestBackendFile{backend: b, key: key, tierInfo: tierInfo}
}
func (b *tierTestBackend) CopyFile(f *os.File, fn func(progressed int64, percentage float32) error) (key string, size int64, err error) {
func (b *tierTestBackend) CopyFile(f *os.File, fn func(progressed int64, percentage float32) error, concurrency int) (key string, size int64, err error) {
b.mu.Lock()
b.lastUploadConcurrency = concurrency
b.mu.Unlock()
key = fmt.Sprintf("obj-%d", time.Now().UnixNano())
dst := filepath.Join(b.root, key)
out, err := os.Create(dst)
@@ -58,7 +64,10 @@ func (b *tierTestBackend) CopyFile(f *os.File, fn func(progressed int64, percent
return key, written, nil
}
func (b *tierTestBackend) DownloadFile(fileName string, key string, fn func(progressed int64, percentage float32) error) (size int64, err error) {
func (b *tierTestBackend) DownloadFile(fileName string, key string, fn func(progressed int64, percentage float32) error, concurrency int) (size int64, err error) {
b.mu.Lock()
b.lastDownloadConcurrency = concurrency
b.mu.Unlock()
in, err := os.Open(filepath.Join(b.root, key))
if err != nil {
return 0, err
@@ -105,6 +114,12 @@ func (b *tierTestBackend) objectExists(key string) bool {
return err == nil
}
func (b *tierTestBackend) concurrencySeen() (upload, download int) {
b.mu.Lock()
defer b.mu.Unlock()
return b.lastUploadConcurrency, b.lastDownloadConcurrency
}
type tierTestBackendFile struct {
backend *tierTestBackend
key string
@@ -201,7 +216,7 @@ func tierUpVolumeOnDisk(t *testing.T, dir string, vid needle.VolumeId, b *tierTe
t.Fatalf("expected on-disk backend before tier-up, got %T", v.DataBackend)
}
datPath := v.FileName(".dat")
key, size, err := b.CopyFile(diskFile.File, nil)
key, size, err := b.CopyFile(diskFile.File, nil, 0)
if err != nil {
t.Fatalf("upload to fake backend: %v", err)
}
@@ -326,3 +341,44 @@ func TestTierMoveDatFromRemote_KeepRemote_LeavesReplicaLocal(t *testing.T) {
t.Fatalf("local .dat content mismatch: got %d bytes, want %d", len(gotDat), len(localDat))
}
}
// TestTierMoveConcurrencyPlumbing verifies the -concurrency value travels the
// whole tier-move path to CopyFile and DownloadFile.
func TestTierMoveConcurrencyPlumbing(t *testing.T) {
b := &tierTestBackend{root: t.TempDir()}
backend.BackendStorages[tierTestBackendName] = b
t.Cleanup(func() { delete(backend.BackendStorages, tierTestBackendName) })
dir := t.TempDir()
const vid = needle.VolumeId(72)
tierUpVolumeOnDisk(t, dir, vid, b)
// download path: request concurrency must reach DownloadFile
store := newTierTestStore(t, dir)
v := store.GetVolume(vid)
if v == nil {
t.Fatal("tiered volume not loaded by store")
}
vs := &VolumeServer{store: store}
if err := vs.VolumeTierMoveDatFromRemote(&volume_server_pb.VolumeTierMoveDatFromRemoteRequest{
VolumeId: uint32(vid),
Concurrency: 4,
}, &fakeTierStream{}); err != nil {
t.Fatalf("VolumeTierMoveDatFromRemote: %v", err)
}
if up, down := b.concurrencySeen(); down != 4 {
t.Fatalf("DownloadFile concurrency = %d (upload=%d), want 4", down, up)
}
// upload path: the volume is local again after the download
if err := vs.VolumeTierMoveDatToRemote(&volume_server_pb.VolumeTierMoveDatToRemoteRequest{
VolumeId: uint32(vid),
DestinationBackendName: tierTestBackendName,
Concurrency: 3,
}, &discardServerStream[volume_server_pb.VolumeTierMoveDatToRemoteResponse]{}); err != nil {
t.Fatalf("VolumeTierMoveDatToRemote: %v", err)
}
if up, down := b.concurrencySeen(); up != 3 || down != 4 {
t.Fatalf("CopyFile concurrency = %d (download=%d), want 3/4", up, down)
}
}
+2 -2
View File
@@ -52,7 +52,7 @@ func (b *tierTimestampTestBackend) NewStorageFile(key string, volumeInfo *volume
}
}
func (b *tierTimestampTestBackend) CopyFile(file *os.File, fn func(progressed int64, percentage float32) error) (key string, size int64, err error) {
func (b *tierTimestampTestBackend) CopyFile(file *os.File, fn func(progressed int64, percentage float32) error, concurrency int) (key string, size int64, err error) {
key = "remote.dat"
fileInfo, err := file.Stat()
if err != nil {
@@ -72,7 +72,7 @@ func (b *tierTimestampTestBackend) CopyFile(file *os.File, fn func(progressed in
return key, size, err
}
func (b *tierTimestampTestBackend) DownloadFile(fileName string, key string, fn func(progressed int64, percentage float32) error) (size int64, err error) {
func (b *tierTimestampTestBackend) DownloadFile(fileName string, key string, fn func(progressed int64, percentage float32) error, concurrency int) (size int64, err error) {
input, err := os.Open(filepath.Join(b.root, key))
if err != nil {
return 0, err
+15 -1
View File
@@ -10,6 +10,20 @@ import (
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
)
// clampTierConcurrency bounds a per-request transfer concurrency so a direct
// gRPC caller cannot spawn an unbounded number of network workers.
const maxTierConcurrency = 1024
func clampTierConcurrency(n int) int {
if n < 0 {
return 0
}
if n > maxTierConcurrency {
return maxTierConcurrency
}
return n
}
// VolumeTierMoveDatToRemote copy dat file to a remote tier
func (vs *VolumeServer) VolumeTierMoveDatToRemote(req *volume_server_pb.VolumeTierMoveDatToRemoteRequest, stream volume_server_pb.VolumeServer_VolumeTierMoveDatToRemoteServer) error {
if err := vs.checkGrpcAdminAuth(stream.Context()); err != nil {
@@ -73,7 +87,7 @@ func (vs *VolumeServer) VolumeTierMoveDatToRemote(req *volume_server_pb.VolumeTi
}
// copy the data file
key, size, err := backendStorage.CopyFile(diskFile.File, fn)
key, size, err := backendStorage.CopyFile(diskFile.File, fn, clampTierConcurrency(int(req.Concurrency)))
if err != nil {
return fmt.Errorf("backend %s copy file %s: %v", req.DestinationBackendName, diskFile.Name(), err)
}
+179 -62
View File
@@ -2,6 +2,7 @@ package shell
import (
"context"
"errors"
"flag"
"fmt"
"io"
@@ -45,6 +46,9 @@ func (c *commandRemoteConfigure) Help() string {
remote.configure -name=cloud7 -type=storj -storj.access_key=xxx -storj.secret_key=yyy -storj.endpoint=https://gateway.us1.storjshare.io
remote.configure -name=cloud8 -type=filebase -filebase.access_key=xxx -filebase.secret_key=yyy -filebase.endpoint=https://s3.filebase.com
# tune transfer concurrency (applies to s3-compatible and azure storage)
remote.configure -name=cloud1 -upload_concurrency=4 -download_concurrency=8
# delete one configuration
remote.configure -delete -name=cloud1
@@ -63,71 +67,11 @@ func (c *commandRemoteConfigure) Do(args []string, commandEnv *CommandEnv, write
conf := &remote_pb.RemoteConf{}
remoteConfigureCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
isDelete := remoteConfigureCommand.Bool("delete", false, "delete one remote storage by its name")
remoteConfigureCommand.StringVar(&conf.Name, "name", "", "a short name to identify the remote storage")
remoteConfigureCommand.StringVar(&conf.Type, "type", "s3", fmt.Sprintf("[%s] storage type", remote_storage.GetAllRemoteStorageNames()))
remoteConfigureCommand.StringVar(&conf.S3AccessKey, "s3.access_key", "", "s3 access key")
remoteConfigureCommand.StringVar(&conf.S3SecretKey, "s3.secret_key", "", "s3 secret key")
remoteConfigureCommand.StringVar(&conf.S3Region, "s3.region", "us-east-2", "s3 region")
remoteConfigureCommand.StringVar(&conf.S3Endpoint, "s3.endpoint", "", "endpoint for s3-compatible local object store")
remoteConfigureCommand.StringVar(&conf.S3StorageClass, "s3.storage_class", "", "s3 storage class")
remoteConfigureCommand.BoolVar(&conf.S3ForcePathStyle, "s3.force_path_style", true, "s3 force path style")
remoteConfigureCommand.BoolVar(&conf.S3V4Signature, "s3.v4_signature", false, "s3 V4 signature")
remoteConfigureCommand.BoolVar(&conf.S3SupportTagging, "s3.support_tagging", true, "s3 supportTagging")
remoteConfigureCommand.StringVar(&conf.GcsGoogleApplicationCredentials, "gcs.appCredentialsFile", "", "google cloud storage credentials file, default to use env GOOGLE_APPLICATION_CREDENTIALS")
remoteConfigureCommand.StringVar(&conf.GcsProjectId, "gcs.projectId", "", "google cloud storage project id, default to use env GOOGLE_CLOUD_PROJECT")
remoteConfigureCommand.StringVar(&conf.AzureAccountName, "azure.account_name", "", "azure account name, default to use env AZURE_STORAGE_ACCOUNT")
remoteConfigureCommand.StringVar(&conf.AzureAccountKey, "azure.account_key", "", "azure account key, default to use env AZURE_STORAGE_ACCESS_KEY. Leave empty to authenticate with Entra ID")
remoteConfigureCommand.StringVar(&conf.AzureClientId, "azure.client_id", "", "azure user-assigned identity to authenticate, when no account key is given. Workload identity also reads env AZURE_TENANT_ID and AZURE_FEDERATED_TOKEN_FILE")
remoteConfigureCommand.StringVar(&conf.AzureEndpoint, "azure.endpoint", "", "azure blob service url, for accounts outside the public cloud, e.g. https://xxx.blob.core.usgovcloudapi.net/")
remoteConfigureCommand.StringVar(&conf.BackblazeKeyId, "b2.key_id", "", "backblaze keyID")
remoteConfigureCommand.StringVar(&conf.BackblazeApplicationKey, "b2.application_key", "", "backblaze applicationKey. Note that your Master Application Key will not work with the S3 Compatible API. You must create a new key that is eligible for use. For more information: https://help.backblaze.com/hc/en-us/articles/360047425453")
remoteConfigureCommand.StringVar(&conf.BackblazeEndpoint, "b2.endpoint", "", "backblaze endpoint")
remoteConfigureCommand.StringVar(&conf.BackblazeRegion, "b2.region", "us-west-002", "backblaze region")
remoteConfigureCommand.StringVar(&conf.AliyunAccessKey, "aliyun.access_key", "", "Aliyun access key, default to use env ALICLOUD_ACCESS_KEY_ID")
remoteConfigureCommand.StringVar(&conf.AliyunSecretKey, "aliyun.secret_key", "", "Aliyun secret key, default to use env ALICLOUD_ACCESS_KEY_SECRET")
remoteConfigureCommand.StringVar(&conf.AliyunEndpoint, "aliyun.endpoint", "", "Aliyun endpoint")
remoteConfigureCommand.StringVar(&conf.AliyunRegion, "aliyun.region", "", "Aliyun region")
remoteConfigureCommand.StringVar(&conf.TencentSecretId, "tencent.secret_id", "", "Tencent Secret Id, default to use env COS_SECRETID")
remoteConfigureCommand.StringVar(&conf.TencentSecretKey, "tencent.secret_key", "", "Tencent secret key, default to use env COS_SECRETKEY")
remoteConfigureCommand.StringVar(&conf.TencentEndpoint, "tencent.endpoint", "", "Tencent endpoint")
remoteConfigureCommand.StringVar(&conf.BaiduAccessKey, "baidu.access_key", "", "Baidu access key, default to use env BDCLOUD_ACCESS_KEY")
remoteConfigureCommand.StringVar(&conf.BaiduSecretKey, "baidu.secret_key", "", "Baidu secret key, default to use env BDCLOUD_SECRET_KEY")
remoteConfigureCommand.StringVar(&conf.BaiduEndpoint, "baidu.endpoint", "", "Baidu endpoint")
remoteConfigureCommand.StringVar(&conf.BaiduRegion, "baidu.region", "", "Baidu region")
remoteConfigureCommand.StringVar(&conf.WasabiAccessKey, "wasabi.access_key", "", "Wasabi access key")
remoteConfigureCommand.StringVar(&conf.WasabiSecretKey, "wasabi.secret_key", "", "Wasabi secret key")
remoteConfigureCommand.StringVar(&conf.WasabiEndpoint, "wasabi.endpoint", "", "Wasabi endpoint, see https://wasabi.com/wp-content/themes/wasabi/docs/API_Guide/index.html#t=topics%2Fapidiff-intro.htm")
remoteConfigureCommand.StringVar(&conf.WasabiRegion, "wasabi.region", "", "Wasabi region")
remoteConfigureCommand.StringVar(&conf.FilebaseAccessKey, "filebase.access_key", "", "Filebase access key")
remoteConfigureCommand.StringVar(&conf.FilebaseSecretKey, "filebase.secret_key", "", "Filebase secret key")
remoteConfigureCommand.StringVar(&conf.FilebaseEndpoint, "filebase.endpoint", "", "Filebase endpoint, https://s3.filebase.com")
remoteConfigureCommand.StringVar(&conf.StorjAccessKey, "storj.access_key", "", "Storj access key")
remoteConfigureCommand.StringVar(&conf.StorjSecretKey, "storj.secret_key", "", "Storj secret key")
remoteConfigureCommand.StringVar(&conf.StorjEndpoint, "storj.endpoint", "", "Storj endpoint")
if err = remoteConfigureCommand.Parse(args); err != nil {
fs, isDelete, uploadConcurrency, downloadConcurrency := c.configureFlagSet(conf, false)
if err = fs.Parse(args); err != nil {
return nil
}
if conf.Type != "s3" {
// clear out the default values
conf.S3Region = ""
conf.S3ForcePathStyle = false
}
if conf.Name == "" {
return c.listExistingRemoteStorages(commandEnv, writer)
}
@@ -140,10 +84,183 @@ func (c *commandRemoteConfigure) Do(args []string, commandEnv *CommandEnv, write
return c.deleteRemoteStorage(commandEnv, writer, conf.Name)
}
// Merge with an existing configuration so a partial update preserves
// previously stored credentials and endpoints. Only treat a confirmed
// missing entry as a new configuration; propagate all other load errors
// so a transient filer failure does not overwrite stored credentials.
typeExplicit := false
fs.Visit(func(f *flag.Flag) {
if f.Name == "type" {
typeExplicit = true
}
})
requestedType := conf.Type
existing, loadErr := c.loadRemoteStorageConf(commandEnv, conf.Name)
if loadErr != nil && !errors.Is(loadErr, filer_pb.ErrNotFound) {
return fmt.Errorf("load existing configuration %s: %v", conf.Name, loadErr)
}
if existing != nil {
conf = existing
// On an explicit type transition, reset backend-specific fields to
// the destination defaults before re-parsing so explicit flags
// override. An omitted -type keeps the stored backend.
if typeExplicit && requestedType != existing.Type {
conf.Type = requestedType
c.applyTypeDefaults(conf)
}
fs, isDelete, uploadConcurrency, downloadConcurrency = c.configureFlagSet(conf, true)
if err = fs.Parse(args); err != nil {
return nil
}
}
if err = applyConcurrency(conf, *uploadConcurrency, *downloadConcurrency); err != nil {
return err
}
if conf.Type != "s3" {
conf.S3Region = ""
conf.S3ForcePathStyle = false
}
return c.saveRemoteStorage(commandEnv, writer, conf)
}
// configureFlagSet builds the remote.configure flag set bound to conf. When
// existing is true, flag defaults are taken from conf so omitted flags preserve
// prior values instead of being reset to the hard-coded new-config defaults.
func (c *commandRemoteConfigure) configureFlagSet(conf *remote_pb.RemoteConf, existing bool) (fs *flag.FlagSet, isDelete *bool, uploadConcurrency *int, downloadConcurrency *int) {
fs = flag.NewFlagSet(c.Name(), flag.ContinueOnError)
isDelete = fs.Bool("delete", false, "delete one remote storage by its name")
fs.StringVar(&conf.Name, "name", conf.Name, "a short name to identify the remote storage")
typeDefault := "s3"
if existing {
typeDefault = conf.Type
}
fs.StringVar(&conf.Type, "type", typeDefault, fmt.Sprintf("[%s] storage type", remote_storage.GetAllRemoteStorageNames()))
uploadConcurrency = fs.Int("upload_concurrency", int(conf.UploadConcurrency), "concurrent part uploads per file (0 = client default: s3 1, azure 16)")
downloadConcurrency = fs.Int("download_concurrency", int(conf.DownloadConcurrency), "concurrent part downloads per read (0 = client default: s3 5, azure 16)")
fs.StringVar(&conf.S3AccessKey, "s3.access_key", conf.S3AccessKey, "s3 access key")
fs.StringVar(&conf.S3SecretKey, "s3.secret_key", conf.S3SecretKey, "s3 secret key")
s3RegionDefault := "us-east-2"
if existing {
s3RegionDefault = conf.S3Region
}
fs.StringVar(&conf.S3Region, "s3.region", s3RegionDefault, "s3 region")
fs.StringVar(&conf.S3Endpoint, "s3.endpoint", conf.S3Endpoint, "endpoint for s3-compatible local object store")
fs.StringVar(&conf.S3StorageClass, "s3.storage_class", conf.S3StorageClass, "s3 storage class")
s3ForcePathStyleDefault := true
if existing {
s3ForcePathStyleDefault = conf.S3ForcePathStyle
}
fs.BoolVar(&conf.S3ForcePathStyle, "s3.force_path_style", s3ForcePathStyleDefault, "s3 force path style")
fs.BoolVar(&conf.S3V4Signature, "s3.v4_signature", conf.S3V4Signature, "s3 V4 signature")
s3SupportTaggingDefault := true
if existing {
s3SupportTaggingDefault = conf.S3SupportTagging
}
fs.BoolVar(&conf.S3SupportTagging, "s3.support_tagging", s3SupportTaggingDefault, "s3 supportTagging")
fs.StringVar(&conf.GcsGoogleApplicationCredentials, "gcs.appCredentialsFile", conf.GcsGoogleApplicationCredentials, "google cloud storage credentials file, default to use env GOOGLE_APPLICATION_CREDENTIALS")
fs.StringVar(&conf.GcsProjectId, "gcs.projectId", conf.GcsProjectId, "google cloud storage project id, default to use env GOOGLE_CLOUD_PROJECT")
fs.StringVar(&conf.AzureAccountName, "azure.account_name", conf.AzureAccountName, "azure account name, default to use env AZURE_STORAGE_ACCOUNT")
fs.StringVar(&conf.AzureAccountKey, "azure.account_key", conf.AzureAccountKey, "azure account key, default to use env AZURE_STORAGE_ACCESS_KEY. Leave empty to authenticate with Entra ID")
fs.StringVar(&conf.AzureClientId, "azure.client_id", conf.AzureClientId, "azure user-assigned identity to authenticate, when no account key is given. Workload identity also reads env AZURE_TENANT_ID and AZURE_FEDERATED_TOKEN_FILE")
fs.StringVar(&conf.AzureEndpoint, "azure.endpoint", conf.AzureEndpoint, "azure blob service url, for accounts outside the public cloud, e.g. https://xxx.blob.core.usgovcloudapi.net/")
fs.StringVar(&conf.BackblazeKeyId, "b2.key_id", conf.BackblazeKeyId, "backblaze keyID")
fs.StringVar(&conf.BackblazeApplicationKey, "b2.application_key", conf.BackblazeApplicationKey, "backblaze applicationKey. Note that your Master Application Key will not work with the S3 Compatible API. You must create a new key that is eligible for use. For more information: https://help.backblaze.com/hc/en-us/articles/360047425453")
fs.StringVar(&conf.BackblazeEndpoint, "b2.endpoint", conf.BackblazeEndpoint, "backblaze endpoint")
b2RegionDefault := "us-west-002"
if existing {
b2RegionDefault = conf.BackblazeRegion
}
fs.StringVar(&conf.BackblazeRegion, "b2.region", b2RegionDefault, "backblaze region")
fs.StringVar(&conf.AliyunAccessKey, "aliyun.access_key", conf.AliyunAccessKey, "Aliyun access key, default to use env ALICLOUD_ACCESS_KEY_ID")
fs.StringVar(&conf.AliyunSecretKey, "aliyun.secret_key", conf.AliyunSecretKey, "Aliyun secret key, default to use env ALICLOUD_ACCESS_KEY_SECRET")
fs.StringVar(&conf.AliyunEndpoint, "aliyun.endpoint", conf.AliyunEndpoint, "Aliyun endpoint")
fs.StringVar(&conf.AliyunRegion, "aliyun.region", conf.AliyunRegion, "Aliyun region")
fs.StringVar(&conf.TencentSecretId, "tencent.secret_id", conf.TencentSecretId, "Tencent Secret Id, default to use env COS_SECRETID")
fs.StringVar(&conf.TencentSecretKey, "tencent.secret_key", conf.TencentSecretKey, "Tencent secret key, default to use env COS_SECRETKEY")
fs.StringVar(&conf.TencentEndpoint, "tencent.endpoint", conf.TencentEndpoint, "Tencent endpoint")
fs.StringVar(&conf.BaiduAccessKey, "baidu.access_key", conf.BaiduAccessKey, "Baidu access key, default to use env BDCLOUD_ACCESS_KEY")
fs.StringVar(&conf.BaiduSecretKey, "baidu.secret_key", conf.BaiduSecretKey, "Baidu secret key, default to use env BDCLOUD_SECRET_KEY")
fs.StringVar(&conf.BaiduEndpoint, "baidu.endpoint", conf.BaiduEndpoint, "Baidu endpoint")
fs.StringVar(&conf.BaiduRegion, "baidu.region", conf.BaiduRegion, "Baidu region")
fs.StringVar(&conf.WasabiAccessKey, "wasabi.access_key", conf.WasabiAccessKey, "Wasabi access key")
fs.StringVar(&conf.WasabiSecretKey, "wasabi.secret_key", conf.WasabiSecretKey, "Wasabi secret key")
fs.StringVar(&conf.WasabiEndpoint, "wasabi.endpoint", conf.WasabiEndpoint, "Wasabi endpoint, see https://wasabi.com/wp-content/themes/wasabi/docs/API_Guide/index.html#t=topics%2Fapidiff-intro.htm")
fs.StringVar(&conf.WasabiRegion, "wasabi.region", conf.WasabiRegion, "Wasabi region")
fs.StringVar(&conf.FilebaseAccessKey, "filebase.access_key", conf.FilebaseAccessKey, "Filebase access key")
fs.StringVar(&conf.FilebaseSecretKey, "filebase.secret_key", conf.FilebaseSecretKey, "Filebase secret key")
fs.StringVar(&conf.FilebaseEndpoint, "filebase.endpoint", conf.FilebaseEndpoint, "Filebase endpoint, https://s3.filebase.com")
fs.StringVar(&conf.StorjAccessKey, "storj.access_key", conf.StorjAccessKey, "Storj access key")
fs.StringVar(&conf.StorjSecretKey, "storj.secret_key", conf.StorjSecretKey, "Storj secret key")
fs.StringVar(&conf.StorjEndpoint, "storj.endpoint", conf.StorjEndpoint, "Storj endpoint")
return
}
// maxRemoteConcurrency caps configured remote transfer concurrency so one
// configuration cannot request an unbounded number of network workers.
const maxRemoteConcurrency = 1024
// applyTypeDefaults resets backend-specific fields to their new-config defaults
// for conf.Type, used when the storage type changes on update.
func (c *commandRemoteConfigure) applyTypeDefaults(conf *remote_pb.RemoteConf) {
conf.S3Region = ""
conf.S3ForcePathStyle = false
conf.S3SupportTagging = false
conf.S3V4Signature = false
conf.BackblazeRegion = ""
if conf.Type == "s3" {
conf.S3Region = "us-east-2"
conf.S3ForcePathStyle = true
conf.S3SupportTagging = true
} else if conf.Type == "b2" {
conf.BackblazeRegion = "us-west-002"
}
}
func applyConcurrency(conf *remote_pb.RemoteConf, upload, download int) error {
if upload < 0 || upload > maxRemoteConcurrency {
return fmt.Errorf("upload_concurrency must be between 0 and %d", maxRemoteConcurrency)
}
if download < 0 || download > maxRemoteConcurrency {
return fmt.Errorf("download_concurrency must be between 0 and %d", maxRemoteConcurrency)
}
conf.UploadConcurrency = uint32(upload)
conf.DownloadConcurrency = uint32(download)
return nil
}
func (c *commandRemoteConfigure) loadRemoteStorageConf(commandEnv *CommandEnv, name string) (*remote_pb.RemoteConf, error) {
var conf *remote_pb.RemoteConf
err := commandEnv.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
content, readErr := filer.ReadInsideFiler(context.Background(), client, filer.DirectoryEtcRemote, name+filer.REMOTE_STORAGE_CONF_SUFFIX)
if readErr != nil {
return readErr
}
conf = &remote_pb.RemoteConf{}
if unmarshalErr := proto.Unmarshal(content, conf); unmarshalErr != nil {
return fmt.Errorf("unmarshal %s/%s: %v", filer.DirectoryEtcRemote, name, unmarshalErr)
}
return nil
})
return conf, err
}
func (c *commandRemoteConfigure) listExistingRemoteStorages(commandEnv *CommandEnv, writer io.Writer) error {
return filer_pb.ReadDirAllEntries(context.Background(), commandEnv, util.FullPath(filer.DirectoryEtcRemote), "", func(entry *filer_pb.Entry, isLast bool) error {
+12 -7
View File
@@ -29,8 +29,8 @@ func (c *commandVolumeTierCompact) Name() string {
func (c *commandVolumeTierCompact) Help() string {
return `compact remote volumes to reclaim space on cloud storage
volume.tier.compact [-volumeId=<volume_id>]
volume.tier.compact [-collection=""] [-garbageThreshold=0.3]
volume.tier.compact [-volumeId=<volume_id>] [-concurrency=<n>]
volume.tier.compact [-collection=""] [-garbageThreshold=0.3] [-concurrency=<n>]
e.g.:
volume.tier.compact -volumeId=7
@@ -64,10 +64,15 @@ func (c *commandVolumeTierCompact) Do(args []string, commandEnv *CommandEnv, wri
volumeId := tierCommand.Int("volumeId", 0, "the volume id")
collection := tierCommand.String("collection", "", "comma-separated collection names, wildcards, or regex patterns; empty matches the collection with no name")
garbageThreshold := tierCommand.Float64("garbageThreshold", 0.3, "compact when garbage ratio exceeds this value")
concurrency := tierCommand.Int("concurrency", 0, "multipart transfer concurrency (0 = backend default)")
if err = tierCommand.Parse(args); err != nil {
return nil
}
if err = validateTierConcurrency(*concurrency); err != nil {
return err
}
if err = commandEnv.confirmIsLocked(args); err != nil {
return
}
@@ -107,7 +112,7 @@ func (c *commandVolumeTierCompact) Do(args []string, commandEnv *CommandEnv, wri
var failedCount int
for _, rv := range remoteVolumes {
if err = doVolumeTierCompact(commandEnv, writer, rv, *garbageThreshold); err != nil {
if err = doVolumeTierCompact(commandEnv, writer, rv, *garbageThreshold, *concurrency); err != nil {
fmt.Fprintf(writer, "error compacting volume %d: %v\n", rv.vid, err)
failedCount++
}
@@ -194,7 +199,7 @@ func collectRemoteVolumesWithInfo(topoInfo *master_pb.TopologyInfo, collectionPa
return result, nil
}
func doVolumeTierCompact(commandEnv *CommandEnv, writer io.Writer, rv remoteVolumeInfo, garbageThreshold float64) error {
func doVolumeTierCompact(commandEnv *CommandEnv, writer io.Writer, rv remoteVolumeInfo, garbageThreshold float64, concurrency int) error {
grpcDialOption := commandEnv.option.GrpcDialOption
// step 1: check garbage level
@@ -214,7 +219,7 @@ func doVolumeTierCompact(commandEnv *CommandEnv, writer io.Writer, rv remoteVolu
// step 2: download .dat from remote to local
// this deletes the remote file and reloads the volume as local, then re-uploads below
fmt.Fprintf(writer, " downloading volume %d from %s to local...\n", rv.vid, rv.remoteStorageName)
err = downloadDatFromRemoteTier(grpcDialOption, writer, rv.vid, rv.collection, rv.serverAddress, false)
err = downloadDatFromRemoteTier(grpcDialOption, writer, rv.vid, rv.collection, rv.serverAddress, false, concurrency)
if err != nil {
return fmt.Errorf("download volume %d from remote: %v", rv.vid, err)
}
@@ -227,7 +232,7 @@ func doVolumeTierCompact(commandEnv *CommandEnv, writer io.Writer, rv remoteVolu
// upload the uncompacted volume back to restore cloud tier state
fmt.Fprintf(writer, " compaction failed: %v\n", err)
fmt.Fprintf(writer, " re-uploading volume %d to %s without compaction...\n", rv.vid, rv.remoteStorageName)
uploadErr := uploadDatToRemoteTier(grpcDialOption, writer, rv.vid, rv.collection, rv.serverAddress, rv.remoteStorageName, false)
uploadErr := uploadDatToRemoteTier(grpcDialOption, writer, rv.vid, rv.collection, rv.serverAddress, rv.remoteStorageName, false, concurrency)
if uploadErr != nil {
return fmt.Errorf("compaction failed (%v) and re-upload also failed (%v), volume %d remains local",
err, uploadErr, rv.vid)
@@ -238,7 +243,7 @@ func doVolumeTierCompact(commandEnv *CommandEnv, writer io.Writer, rv remoteVolu
// step 4: upload compacted volume back to remote
fmt.Fprintf(writer, " uploading compacted volume %d to %s...\n", rv.vid, rv.remoteStorageName)
err = uploadDatToRemoteTier(grpcDialOption, writer, rv.vid, rv.collection, rv.serverAddress, rv.remoteStorageName, false)
err = uploadDatToRemoteTier(grpcDialOption, writer, rv.vid, rv.collection, rv.serverAddress, rv.remoteStorageName, false, concurrency)
if err != nil {
return fmt.Errorf("upload compacted volume %d to %s: %v (volume remains local with compacted data)",
rv.vid, rv.remoteStorageName, err)
+13 -6
View File
@@ -32,7 +32,7 @@ func (c *commandVolumeTierDownload) Help() string {
return `download the dat file of a volume from a remote tier
volume.tier.download [-collection=""]
volume.tier.download [-collection=""] -volumeId=<volume_id>
volume.tier.download [-collection=""] -volumeId=<volume_id> [-concurrency=<n>]
The -collection parameter supports regular expressions for pattern matching:
- Use exact match: volume.tier.download -collection="^mybucket$"
@@ -41,6 +41,7 @@ func (c *commandVolumeTierDownload) Help() string {
e.g.:
volume.tier.download -volumeId=7
volume.tier.download -volumeId=7 -concurrency=1
This command will download the dat file of a volume from a remote tier to a volume server in local cluster.
@@ -56,10 +57,15 @@ func (c *commandVolumeTierDownload) Do(args []string, commandEnv *CommandEnv, wr
tierCommand := flag.NewFlagSet(c.Name(), flag.ContinueOnError)
volumeId := tierCommand.Int("volumeId", 0, "the volume id")
collection := tierCommand.String("collection", "", "comma-separated collection names, wildcards, or regex patterns; empty matches the collection with no name")
concurrency := tierCommand.Int("concurrency", 0, "multipart download concurrency (0 = backend default)")
if err = tierCommand.Parse(args); err != nil {
return nil
}
if err = validateTierConcurrency(*concurrency); err != nil {
return err
}
if err = commandEnv.confirmIsLocked(args); err != nil {
return
}
@@ -74,7 +80,7 @@ func (c *commandVolumeTierDownload) Do(args []string, commandEnv *CommandEnv, wr
// volumeId is provided
if vid != 0 {
return doVolumeTierDownload(commandEnv, writer, *collection, vid)
return doVolumeTierDownload(commandEnv, writer, *collection, vid, *concurrency)
}
// apply to all volumes in the collection
@@ -85,7 +91,7 @@ func (c *commandVolumeTierDownload) Do(args []string, commandEnv *CommandEnv, wr
}
fmt.Printf("tier download volumes: %v\n", volumeIds)
for _, vid := range volumeIds {
if err = doVolumeTierDownload(commandEnv, writer, *collection, vid); err != nil {
if err = doVolumeTierDownload(commandEnv, writer, *collection, vid, *concurrency); err != nil {
return err
}
}
@@ -118,7 +124,7 @@ func collectRemoteVolumes(topoInfo *master_pb.TopologyInfo, collectionPattern st
return
}
func doVolumeTierDownload(commandEnv *CommandEnv, writer io.Writer, collection string, vid needle.VolumeId) (err error) {
func doVolumeTierDownload(commandEnv *CommandEnv, writer io.Writer, collection string, vid needle.VolumeId, concurrency int) (err error) {
// find volume location
locations, found := commandEnv.MasterClient.GetLocationsClone(uint32(vid))
if !found {
@@ -131,7 +137,7 @@ func doVolumeTierDownload(commandEnv *CommandEnv, writer io.Writer, collection s
for i, loc := range locations {
keepRemote := i < len(locations)-1
// copy the .dat file from remote tier to local
err = downloadDatFromRemoteTier(commandEnv.option.GrpcDialOption, writer, needle.VolumeId(vid), collection, loc.ServerAddress(), keepRemote)
err = downloadDatFromRemoteTier(commandEnv.option.GrpcDialOption, writer, needle.VolumeId(vid), collection, loc.ServerAddress(), keepRemote, concurrency)
if err != nil {
// A replica already made local by a prior interrupted run is not a
// failure; skip it so the remaining remote replicas still download.
@@ -146,13 +152,14 @@ func doVolumeTierDownload(commandEnv *CommandEnv, writer io.Writer, collection s
return nil
}
func downloadDatFromRemoteTier(grpcDialOption grpc.DialOption, writer io.Writer, volumeId needle.VolumeId, collection string, targetVolumeServer pb.ServerAddress, keepRemote bool) error {
func downloadDatFromRemoteTier(grpcDialOption grpc.DialOption, writer io.Writer, volumeId needle.VolumeId, collection string, targetVolumeServer pb.ServerAddress, keepRemote bool, concurrency int) error {
err := operation.WithVolumeServerClient(true, targetVolumeServer, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
stream, downloadErr := volumeServerClient.VolumeTierMoveDatFromRemote(context.Background(), &volume_server_pb.VolumeTierMoveDatFromRemoteRequest{
VolumeId: uint32(volumeId),
Collection: collection,
KeepRemoteDatFile: keepRemote,
Concurrency: int32(concurrency),
})
var lastProcessed int64
+29 -6
View File
@@ -5,6 +5,7 @@ import (
"flag"
"fmt"
"io"
"math"
"time"
"github.com/seaweedfs/seaweedfs/weed/storage/types"
@@ -21,6 +22,21 @@ import (
"github.com/seaweedfs/seaweedfs/weed/wdclient"
)
// maxTierConcurrency caps per-request multipart transfer concurrency.
const maxTierConcurrency = 1024
// validateTierConcurrency rejects values that would wrap when narrowed to the
// int32 proto field or exhaust volume-server resources. 0 means backend default.
func validateTierConcurrency(n int) error {
if n < 0 || n > math.MaxInt32 {
return fmt.Errorf("concurrency must be between 0 and %d, got %d", math.MaxInt32, n)
}
if n > maxTierConcurrency {
return fmt.Errorf("concurrency must be at most %d, got %d", maxTierConcurrency, n)
}
return nil
}
func init() {
Commands = append(Commands, &commandVolumeTierUpload{})
}
@@ -36,11 +52,12 @@ func (c *commandVolumeTierUpload) Help() string {
return `upload the dat file of a volume to a remote tier
volume.tier.upload [-collection=""] [-fullPercent=95] [-quietFor=1h]
volume.tier.upload [-collection=""] -volumeId=<volume_id> -dest=<storage_backend> [-keepLocalDatFile]
volume.tier.upload [-collection=""] -volumeId=<volume_id> -dest=<storage_backend> [-keepLocalDatFile] [-concurrency=<n>]
e.g.:
volume.tier.upload -volumeId=7 -dest=s3
volume.tier.upload -volumeId=7 -dest=s3.default
volume.tier.upload -volumeId=7 -dest=s3.telegram -concurrency=1
The <storage_backend> is defined in master.toml.
For example, "s3.default" in [storage.backend.s3.default]
@@ -78,10 +95,15 @@ func (c *commandVolumeTierUpload) Do(args []string, commandEnv *CommandEnv, writ
dest := tierCommand.String("dest", "", "the target tier name")
keepLocalDatFile := tierCommand.Bool("keepLocalDatFile", false, "whether keep local dat file")
disk := tierCommand.String("disk", "", "[hdd|ssd|<tag>] hard drive or solid state drive or any tag")
concurrency := tierCommand.Int("concurrency", 0, "multipart upload concurrency (0 = backend default)")
if err = tierCommand.Parse(args); err != nil {
return nil
}
if err = validateTierConcurrency(*concurrency); err != nil {
return err
}
if err = commandEnv.confirmIsLocked(args); err != nil {
return
}
@@ -90,7 +112,7 @@ func (c *commandVolumeTierUpload) Do(args []string, commandEnv *CommandEnv, writ
// volumeId is provided
if vid != 0 {
return doVolumeTierUpload(commandEnv, writer, *collection, vid, *dest, *keepLocalDatFile)
return doVolumeTierUpload(commandEnv, writer, *collection, vid, *dest, *keepLocalDatFile, *concurrency)
}
var diskType *types.DiskType
@@ -107,7 +129,7 @@ func (c *commandVolumeTierUpload) Do(args []string, commandEnv *CommandEnv, writ
}
fmt.Printf("tier upload volumes: %v\n", volumeIds)
for _, vid := range volumeIds {
if err = doVolumeTierUpload(commandEnv, writer, *collection, vid, *dest, *keepLocalDatFile); err != nil {
if err = doVolumeTierUpload(commandEnv, writer, *collection, vid, *dest, *keepLocalDatFile, *concurrency); err != nil {
return err
}
}
@@ -115,7 +137,7 @@ func (c *commandVolumeTierUpload) Do(args []string, commandEnv *CommandEnv, writ
return nil
}
func doVolumeTierUpload(commandEnv *CommandEnv, writer io.Writer, collection string, vid needle.VolumeId, dest string, keepLocalDatFile bool) (err error) {
func doVolumeTierUpload(commandEnv *CommandEnv, writer io.Writer, collection string, vid needle.VolumeId, dest string, keepLocalDatFile bool, concurrency int) (err error) {
// find volume location
topoInfo, _, err := collectTopologyInfo(commandEnv, 0)
if err != nil {
@@ -137,7 +159,7 @@ func doVolumeTierUpload(commandEnv *CommandEnv, writer io.Writer, collection str
}
// copy the .dat file to remote tier
err = uploadDatToRemoteTier(commandEnv.option.GrpcDialOption, writer, vid, collection, existingLocations[0].ServerAddress(), dest, keepLocalDatFile)
err = uploadDatToRemoteTier(commandEnv.option.GrpcDialOption, writer, vid, collection, existingLocations[0].ServerAddress(), dest, keepLocalDatFile, concurrency)
if err != nil {
return fmt.Errorf("copy dat file for volume %d on %s to %s: %v", vid, existingLocations[0].Url, dest, err)
}
@@ -190,7 +212,7 @@ func collectVolumeTierUploadLocations(topoInfo *master_pb.TopologyInfo, vid need
return append(tiered, local...)
}
func uploadDatToRemoteTier(grpcDialOption grpc.DialOption, writer io.Writer, volumeId needle.VolumeId, collection string, sourceVolumeServer pb.ServerAddress, dest string, keepLocalDatFile bool) error {
func uploadDatToRemoteTier(grpcDialOption grpc.DialOption, writer io.Writer, volumeId needle.VolumeId, collection string, sourceVolumeServer pb.ServerAddress, dest string, keepLocalDatFile bool, concurrency int) error {
err := operation.WithVolumeServerClient(true, sourceVolumeServer, grpcDialOption, func(volumeServerClient volume_server_pb.VolumeServerClient) error {
stream, copyErr := volumeServerClient.VolumeTierMoveDatToRemote(context.Background(), &volume_server_pb.VolumeTierMoveDatToRemoteRequest{
@@ -198,6 +220,7 @@ func uploadDatToRemoteTier(grpcDialOption grpc.DialOption, writer io.Writer, vol
Collection: collection,
DestinationBackendName: dest,
KeepLocalDatFile: keepLocalDatFile,
Concurrency: int32(concurrency),
})
if stream == nil {
+3 -2
View File
@@ -25,8 +25,9 @@ type BackendStorageFile interface {
type BackendStorage interface {
ToProperties() map[string]string
NewStorageFile(key string, tierInfo *volume_server_pb.VolumeInfo) BackendStorageFile
CopyFile(f *os.File, fn func(progressed int64, percentage float32) error) (key string, size int64, err error)
DownloadFile(fileName string, key string, fn func(progressed int64, percentage float32) error) (size int64, err error)
// concurrency > 0 caps concurrent network transfers; <= 0 uses the backend default.
CopyFile(f *os.File, fn func(progressed int64, percentage float32) error, concurrency int) (key string, size int64, err error)
DownloadFile(fileName string, key string, fn func(progressed int64, percentage float32) error, concurrency int) (size int64, err error)
DeleteFile(key string) (err error)
}
@@ -103,7 +103,7 @@ func (s *RcloneBackendStorage) NewStorageFile(key string, tierInfo *volume_serve
return f
}
func (s *RcloneBackendStorage) CopyFile(f *os.File, fn func(progressed int64, percentage float32) error) (key string, size int64, err error) {
func (s *RcloneBackendStorage) CopyFile(f *os.File, fn func(progressed int64, percentage float32) error, concurrency int) (key string, size int64, err error) {
randomUuid, err := uuid.NewRandom()
if err != nil {
return key, 0, err
@@ -154,7 +154,7 @@ func uploadViaRclone(rfs fs.Fs, filename string, key string, fn func(progressed
return obj.Size(), err
}
func (s *RcloneBackendStorage) DownloadFile(filename string, key string, fn func(progressed int64, percentage float32) error) (size int64, err error) {
func (s *RcloneBackendStorage) DownloadFile(filename string, key string, fn func(progressed int64, percentage float32) error, concurrency int) (size int64, err error) {
glog.V(1).Infof("download dat file of %s from remote rclone.%s as %s", filename, s.id, key)
util.Retry("download via Rclone", func() error {
+34 -4
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"io"
"os"
"strconv"
"strings"
"time"
@@ -41,6 +42,8 @@ type S3BackendStorage struct {
endpoint string
storageClass string
forcePathStyle bool
uploadConcurrency int
downloadConcurrency int
conn s3iface.S3API
}
@@ -54,6 +57,8 @@ func newS3BackendStorage(configuration backend.StringProperties, configPrefix st
s.endpoint = configuration.GetString(configPrefix + "endpoint")
s.storageClass = configuration.GetString(configPrefix + "storage_class")
s.forcePathStyle = util.ParseBool(configuration.GetString(configPrefix+"force_path_style"), true)
s.uploadConcurrency = parseConcurrency(configuration.GetString(configPrefix+"upload_concurrency"), defaultUploadConcurrency)
s.downloadConcurrency = parseConcurrency(configuration.GetString(configPrefix+"download_concurrency"), defaultDownloadConcurrency)
if s.storageClass == "" {
s.storageClass = "STANDARD_IA"
}
@@ -73,9 +78,27 @@ func (s *S3BackendStorage) ToProperties() map[string]string {
m["endpoint"] = s.endpoint
m["storage_class"] = s.storageClass
m["force_path_style"] = util.BoolToString(s.forcePathStyle)
m["upload_concurrency"] = strconv.Itoa(s.uploadConcurrency)
m["download_concurrency"] = strconv.Itoa(s.downloadConcurrency)
return m
}
const (
defaultUploadConcurrency = 5
defaultDownloadConcurrency = 5
)
func parseConcurrency(value string, def int) int {
if value == "" {
return def
}
if n, err := strconv.Atoi(value); err == nil && n > 0 {
return n
}
glog.Warningf("invalid concurrency value %q, using default %d", value, def)
return def
}
func (s *S3BackendStorage) NewStorageFile(key string, tierInfo *volume_server_pb.VolumeInfo) backend.BackendStorageFile {
if strings.HasPrefix(key, "/") {
key = key[1:]
@@ -90,25 +113,32 @@ func (s *S3BackendStorage) NewStorageFile(key string, tierInfo *volume_server_pb
return f
}
func (s *S3BackendStorage) CopyFile(f *os.File, fn func(progressed int64, percentage float32) error) (key string, size int64, err error) {
func (s *S3BackendStorage) CopyFile(f *os.File, fn func(progressed int64, percentage float32) error, concurrency int) (key string, size int64, err error) {
randomUuid, _ := uuid.NewRandom()
key = randomUuid.String()
if concurrency <= 0 {
concurrency = s.uploadConcurrency
}
glog.V(1).Infof("copying dat file of %s to remote s3.%s as %s", f.Name(), s.id, key)
util.Retry("upload to S3", func() error {
size, err = uploadToS3(s.conn, f.Name(), s.bucket, key, s.storageClass, fn)
size, err = uploadToS3(s.conn, f.Name(), s.bucket, key, s.storageClass, fn, concurrency)
return err
})
return
}
func (s *S3BackendStorage) DownloadFile(fileName string, key string, fn func(progressed int64, percentage float32) error) (size int64, err error) {
func (s *S3BackendStorage) DownloadFile(fileName string, key string, fn func(progressed int64, percentage float32) error, concurrency int) (size int64, err error) {
if concurrency <= 0 {
concurrency = s.downloadConcurrency
}
glog.V(1).Infof("download dat file of %s from remote s3.%s as %s", fileName, s.id, key)
size, err = downloadFromS3(s.conn, fileName, s.bucket, key, fn)
size, err = downloadFromS3(s.conn, fileName, s.bucket, key, fn, concurrency)
return
}
@@ -12,6 +12,12 @@ import (
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
)
type testProperties map[string]string
func (m testProperties) GetString(key string) string {
return m[key]
}
type stubS3Client struct {
s3iface.S3API
getObject func(*s3.GetObjectInput) (*s3.GetObjectOutput, error)
@@ -101,3 +107,50 @@ func TestReadAtRejectsInvalidRequestsLocally(t *testing.T) {
})
}
}
func TestParseConcurrency(t *testing.T) {
cases := []struct {
value string
def int
expected int
}{
{"", 5, 5}, // unset -> default
{"0", 5, 5}, // explicit zero means default
{"-3", 5, 5}, // invalid -> default
{"abc", 5, 5}, // invalid -> default
{"1", 5, 1}, // override
{"64", 5, 64}, // override
}
for _, tt := range cases {
if got := parseConcurrency(tt.value, tt.def); got != tt.expected {
t.Errorf("parseConcurrency(%q, %d) = %d, want %d", tt.value, tt.def, got, tt.expected)
}
}
}
func TestS3BackendStorageConcurrencyConfigRoundTrip(t *testing.T) {
s, err := newS3BackendStorage(testProperties{}, "", "test")
if err != nil {
t.Fatal(err)
}
if s.uploadConcurrency != defaultUploadConcurrency || s.downloadConcurrency != defaultDownloadConcurrency {
t.Fatalf("defaults: upload=%d download=%d, want %d/%d",
s.uploadConcurrency, s.downloadConcurrency, defaultUploadConcurrency, defaultDownloadConcurrency)
}
s, err = newS3BackendStorage(testProperties{
"upload_concurrency": "1",
"download_concurrency": "17",
}, "", "test")
if err != nil {
t.Fatal(err)
}
if s.uploadConcurrency != 1 || s.downloadConcurrency != 17 {
t.Fatalf("configured: upload=%d download=%d, want 1/17", s.uploadConcurrency, s.downloadConcurrency)
}
props := s.ToProperties()
if props["upload_concurrency"] != "1" || props["download_concurrency"] != "17" {
t.Fatalf("ToProperties: %v", props)
}
}
@@ -14,7 +14,10 @@ import (
)
func downloadFromS3(sess s3iface.S3API, destFileName string, sourceBucket string, sourceKey string,
fn func(progressed int64, percentage float32) error) (fileSize int64, err error) {
fn func(progressed int64, percentage float32) error, concurrency int) (fileSize int64, err error) {
if concurrency <= 0 {
concurrency = defaultDownloadConcurrency
}
fileSize, err = getFileSize(sess, sourceBucket, sourceKey)
if err != nil {
@@ -31,7 +34,7 @@ func downloadFromS3(sess s3iface.S3API, destFileName string, sourceBucket string
// Create a downloader with the session and custom options
downloader := s3manager.NewDownloaderWithClient(sess, func(u *s3manager.Downloader) {
u.PartSize = int64(64 * 1024 * 1024)
u.Concurrency = 5
u.Concurrency = concurrency
})
fileWriter := &s3DownloadProgressedWriter{
+5 -2
View File
@@ -12,7 +12,10 @@ import (
"github.com/seaweedfs/seaweedfs/weed/glog"
)
func uploadToS3(sess s3iface.S3API, filename string, destBucket string, destKey string, storageClass string, fn func(progressed int64, percentage float32) error) (fileSize int64, err error) {
func uploadToS3(sess s3iface.S3API, filename string, destBucket string, destKey string, storageClass string, fn func(progressed int64, percentage float32) error, concurrency int) (fileSize int64, err error) {
if concurrency <= 0 {
concurrency = defaultUploadConcurrency
}
//open the file
f, err := os.Open(filename)
@@ -36,7 +39,7 @@ func uploadToS3(sess s3iface.S3API, filename string, destBucket string, destKey
// Create an uploader with the session and custom options
uploader := s3manager.NewUploaderWithClient(sess, func(u *s3manager.Uploader) {
u.PartSize = partSize
u.Concurrency = 5
u.Concurrency = concurrency
})
fileReader := &s3UploadProgressedReader{
+4 -4
View File
@@ -52,7 +52,7 @@ func (b *localDirBackend) NewStorageFile(key string, tierInfo *volume_server_pb.
return &localDirBackendFile{backend: b, key: key, tierInfo: tierInfo}
}
func (b *localDirBackend) CopyFile(f *os.File, fn func(progressed int64, percentage float32) error) (key string, size int64, err error) {
func (b *localDirBackend) CopyFile(f *os.File, fn func(progressed int64, percentage float32) error, concurrency int) (key string, size int64, err error) {
key = fmt.Sprintf("obj-%d-%d", time.Now().UnixNano(), os.Getpid())
dst := filepath.Join(b.root, key)
out, err := os.Create(dst)
@@ -73,7 +73,7 @@ func (b *localDirBackend) CopyFile(f *os.File, fn func(progressed int64, percent
return key, written, nil
}
func (b *localDirBackend) DownloadFile(fileName string, key string, fn func(progressed int64, percentage float32) error) (size int64, err error) {
func (b *localDirBackend) DownloadFile(fileName string, key string, fn func(progressed int64, percentage float32) error, concurrency int) (size int64, err error) {
src := filepath.Join(b.root, key)
in, err := os.Open(src)
if err != nil {
@@ -193,7 +193,7 @@ func tierUpVolumeLive(t *testing.T, dir string, vid needle.VolumeId, b *localDir
diskFile, ok := v.DataBackend.(*backend.DiskFile)
require.True(t, ok, "expected on-disk backend before tier-up")
uploadKey, size, err := b.CopyFile(diskFile.File, nil)
uploadKey, size, err := b.CopyFile(diskFile.File, nil, 0)
require.NoError(t, err)
bType, bId := backend.BackendNameToTypeId(testBackendName)
@@ -422,7 +422,7 @@ func TestRemoteTier_ECEncodeDecode_AfterDownload(t *testing.T) {
baseFileName := filepath.Join(dir, fmt.Sprintf("%d", uint32(vid)))
datPath := baseFileName + ".dat"
_, err := b.DownloadFile(datPath, key, nil)
_, err := b.DownloadFile(datPath, key, nil, 0)
require.NoError(t, err)
require.NoError(t, erasure_coding.WriteSortedFileFromIdx(baseFileName, ".ecx"))