mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-17 19:48:35 +02:00
* 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>
248 lines
6.8 KiB
Go
248 lines
6.8 KiB
Go
package s3_backend
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
|
|
"github.com/aws/aws-sdk-go/service/s3"
|
|
"github.com/aws/aws-sdk-go/service/s3/s3iface"
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/volume_server_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/backend"
|
|
)
|
|
|
|
func init() {
|
|
backend.BackendStorageFactories["s3"] = &S3BackendFactory{}
|
|
}
|
|
|
|
type S3BackendFactory struct {
|
|
}
|
|
|
|
func (factory *S3BackendFactory) StorageType() backend.StorageType {
|
|
return backend.StorageType("s3")
|
|
}
|
|
func (factory *S3BackendFactory) BuildStorage(configuration backend.StringProperties, configPrefix string, id string) (backend.BackendStorage, error) {
|
|
return newS3BackendStorage(configuration, configPrefix, id)
|
|
}
|
|
|
|
type S3BackendStorage struct {
|
|
id string
|
|
aws_access_key_id string
|
|
aws_secret_access_key string
|
|
region string
|
|
bucket string
|
|
endpoint string
|
|
storageClass string
|
|
forcePathStyle bool
|
|
uploadConcurrency int
|
|
downloadConcurrency int
|
|
conn s3iface.S3API
|
|
}
|
|
|
|
func newS3BackendStorage(configuration backend.StringProperties, configPrefix string, id string) (s *S3BackendStorage, err error) {
|
|
s = &S3BackendStorage{}
|
|
s.id = id
|
|
s.aws_access_key_id = configuration.GetString(configPrefix + "aws_access_key_id")
|
|
s.aws_secret_access_key = configuration.GetString(configPrefix + "aws_secret_access_key")
|
|
s.region = configuration.GetString(configPrefix + "region")
|
|
s.bucket = configuration.GetString(configPrefix + "bucket")
|
|
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"
|
|
}
|
|
|
|
s.conn, err = createSession(s.aws_access_key_id, s.aws_secret_access_key, s.region, s.endpoint, s.forcePathStyle)
|
|
|
|
glog.V(0).Infof("created backend storage s3.%s for region %s bucket %s", s.id, s.region, s.bucket)
|
|
return
|
|
}
|
|
|
|
func (s *S3BackendStorage) ToProperties() map[string]string {
|
|
m := make(map[string]string)
|
|
m["aws_access_key_id"] = s.aws_access_key_id
|
|
m["aws_secret_access_key"] = s.aws_secret_access_key
|
|
m["region"] = s.region
|
|
m["bucket"] = s.bucket
|
|
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:]
|
|
}
|
|
|
|
f := &S3BackendStorageFile{
|
|
backendStorage: s,
|
|
key: key,
|
|
tierInfo: tierInfo,
|
|
}
|
|
|
|
return f
|
|
}
|
|
|
|
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, concurrency)
|
|
return err
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
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, concurrency)
|
|
|
|
return
|
|
}
|
|
|
|
func (s *S3BackendStorage) DeleteFile(key string) (err error) {
|
|
|
|
glog.V(1).Infof("delete dat file %s from remote", key)
|
|
|
|
err = deleteFromS3(s.conn, s.bucket, key)
|
|
|
|
return
|
|
}
|
|
|
|
type S3BackendStorageFile struct {
|
|
backendStorage *S3BackendStorage
|
|
key string
|
|
tierInfo *volume_server_pb.VolumeInfo
|
|
}
|
|
|
|
func (s3backendStorageFile S3BackendStorageFile) ReadAt(p []byte, off int64) (n int, err error) {
|
|
if off < 0 {
|
|
return 0, fmt.Errorf("negative offset %d", off)
|
|
}
|
|
|
|
requested := len(p)
|
|
if requested == 0 {
|
|
return 0, nil
|
|
}
|
|
|
|
datSize, _, _ := s3backendStorageFile.GetStat()
|
|
|
|
if datSize > 0 && off >= datSize {
|
|
return 0, io.EOF
|
|
}
|
|
|
|
bytesRange := fmt.Sprintf("bytes=%d-%d", off, off+int64(len(p))-1)
|
|
|
|
getObjectOutput, getObjectErr := s3backendStorageFile.backendStorage.conn.GetObject(&s3.GetObjectInput{
|
|
Bucket: &s3backendStorageFile.backendStorage.bucket,
|
|
Key: &s3backendStorageFile.key,
|
|
Range: &bytesRange,
|
|
})
|
|
|
|
if getObjectErr != nil {
|
|
return 0, fmt.Errorf("bucket %s GetObject %s: %v", s3backendStorageFile.backendStorage.bucket, s3backendStorageFile.key, getObjectErr)
|
|
}
|
|
defer getObjectOutput.Body.Close()
|
|
|
|
// glog.V(3).Infof("read %s %s", s3backendStorageFile.key, bytesRange)
|
|
// glog.V(3).Infof("content range: %s, contentLength: %d", *getObjectOutput.ContentRange, *getObjectOutput.ContentLength)
|
|
|
|
var readCount int
|
|
for {
|
|
p = p[readCount:]
|
|
if len(p) == 0 {
|
|
break
|
|
}
|
|
readCount, err = getObjectOutput.Body.Read(p)
|
|
n += readCount
|
|
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
if err == io.EOF && n == requested {
|
|
err = nil
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (s3backendStorageFile S3BackendStorageFile) WriteAt(p []byte, off int64) (n int, err error) {
|
|
panic("not implemented")
|
|
}
|
|
|
|
func (s3backendStorageFile S3BackendStorageFile) Truncate(off int64) error {
|
|
panic("not implemented")
|
|
}
|
|
|
|
func (s3backendStorageFile S3BackendStorageFile) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (s3backendStorageFile S3BackendStorageFile) GetStat() (datSize int64, modTime time.Time, err error) {
|
|
|
|
files := s3backendStorageFile.tierInfo.GetFiles()
|
|
|
|
if len(files) == 0 {
|
|
err = fmt.Errorf("remote file info not found")
|
|
return
|
|
}
|
|
|
|
datSize = int64(files[0].FileSize)
|
|
modTime = time.Unix(int64(files[0].ModifiedTime), 0)
|
|
|
|
return
|
|
}
|
|
|
|
func (s3backendStorageFile S3BackendStorageFile) Name() string {
|
|
return s3backendStorageFile.key
|
|
}
|
|
|
|
func (s3backendStorageFile S3BackendStorageFile) Sync() error {
|
|
return nil
|
|
}
|