Skip to content

Commit 18b7f76

Browse files
authored
Enhancing Transfer Performance: Parallel Multipart Upload for Distributing Large Files Across the Cluster (#16) (#17)
* feat: support large file transfer in cluster mode * fix: fix the src prefix in single part transfer * fix: fix the src prefix in single part transfer 2 * fix: fix the multipart split issue * chore: using sfn to controll the part merging * feat: add network throttling detect * chore: remove the throttle detect feature and add sfn check * fix: fix the updateItem issue * doc: update the readme for giant object transfer
1 parent a1e55b4 commit 18b7f76

12 files changed

Lines changed: 631 additions & 28 deletions

File tree

Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ ENV SOURCE_TYPE Amazon_S3
1919

2020
ENV JOB_TABLE_NAME ''
2121
ENV JOB_QUEUE_NAME ''
22+
ENV SINGLE_PART_TABLE_NAME ''
2223

2324
ENV SRC_BUCKET ''
2425
ENV SRC_PREFIX ''

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,13 @@ destRegion: cn-north-1
5959

6060
jobTableName: test-table
6161
jobQueueName: test-queue
62+
63+
singlePartTableName: single-part-test-table
64+
sfnArn: sfn-arn
6265
```
6366
67+
The sfaArn is a Step Function created by [Data Transfer Hub S3 Plugin](https://github.com/awslabs/data-transfer-hub/blob/main/docs/S3_PLUGIN.md).
68+
6469
By default, this tool will try to read a `config.yaml` in the same folder, if you create the configuration file in a different folder or with a different file name, please use extra option `--config xxx.yaml` to load your config file.
6570

6671

cmd/root.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ func initConfig() {
8080

8181
viper.SetDefault("options.chunkSize", dth.DefaultChunkSize)
8282
viper.SetDefault("options.multipartThreshold", dth.DefaultMultipartThreshold)
83+
viper.SetDefault("options.giantFileThreshold", dth.DefaultGiantFileThreshold)
8384
viper.SetDefault("options.maxKeys", dth.DefaultMaxKeys)
8485
viper.SetDefault("options.messageBatchSize", dth.DefaultMessageBatchSize)
8586
viper.SetDefault("options.finderDepth", dth.DefaultFinderDepth)
@@ -108,10 +109,13 @@ func initConfig() {
108109

109110
viper.BindEnv("jobTableName", "JOB_TABLE_NAME")
110111
viper.BindEnv("jobQueueName", "JOB_QUEUE_NAME")
112+
viper.BindEnv("singlePartTableName", "SINGLE_PART_TABLE_NAME")
113+
viper.BindEnv("sfnArn", "SFN_ARN")
111114

112115
viper.BindEnv("options.maxKeys", "MAX_KEYS")
113116
viper.BindEnv("options.chunkSize", "CHUNK_SIZE")
114117
viper.BindEnv("options.multipartThreshold", "MULTIPART_THRESHOLD")
118+
viper.BindEnv("options.giantFileThreshold", "GIANT_FILE_THRESHOLD")
115119
viper.BindEnv("options.messageBatchSize", "MESSAGE_BATCH_SIZE")
116120
viper.BindEnv("options.finderDepth", "FINDER_DEPTH")
117121
viper.BindEnv("options.finderNumber", "FINDER_NUMBER")
@@ -138,6 +142,7 @@ func initConfig() {
138142
options := &dth.JobOptions{
139143
ChunkSize: viper.GetInt("options.chunkSize"),
140144
MultipartThreshold: viper.GetInt("options.multipartThreshold"),
145+
GiantFileThreshold: viper.GetInt("options.giantFileThreshold"),
141146
MaxKeys: viper.GetInt32("options.maxKeys"),
142147
MessageBatchSize: viper.GetInt("options.messageBatchSize"),
143148
FinderDepth: viper.GetInt("options.finderDepth"),
@@ -166,6 +171,8 @@ func initConfig() {
166171
DestInCurrentAccount: viper.GetBool("destInCurrentAccount"),
167172
JobTableName: viper.GetString("jobTableName"),
168173
JobQueueName: viper.GetString("jobQueueName"),
174+
SinglePartTableName: viper.GetString("singlePartTableName"),
175+
SfnArn: viper.GetString("sfnArn"),
169176
JobOptions: options,
170177
}
171178

config-example.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ destAcl: bucket-owner-full-control
1818

1919
jobTableName: test-table
2020
jobQueueName: test-queue
21+
singlePartTableName: single-part-test-table
22+
sfnArn: sfn-arn
2123

2224
options:
2325
chunkSize: 5
2426
multipartThreshold: 10
27+
giantFileThreshold: 1024
2528
maxKeys: 1000
2629
messageBatchSize: 10
2730
finderDepth: 0

dth/client.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type Client interface {
4141
// READ
4242
HeadObject(ctx context.Context, key *string) *Metadata
4343
GetObject(ctx context.Context, key *string, size, start, chunkSize int64, version string) ([]byte, error)
44+
GetObjectPart(ctx context.Context, key *string, bodyRange string) ([]byte, error)
4445
ListObjects(ctx context.Context, continuationToken, prefix *string, maxKeys int32) ([]*Object, error)
4546
ListCommonPrefixes(ctx context.Context, depth int, maxKeys int32) (prefixes []*string)
4647
ListParts(ctx context.Context, key, uploadID *string) (parts map[int]*Part)
@@ -209,6 +210,45 @@ func (c *S3Client) GetObject(ctx context.Context, key *string, size, start, chun
209210

210211
}
211212

213+
// GetObjectPart is a function to get (download) object from Amazon S3
214+
func (c *S3Client) GetObjectPart(ctx context.Context, key *string, bodyRange string) ([]byte, error) {
215+
// log.Printf("S3> Downloading %s with %d bytes start from %d\n", key, size, start)
216+
var input *s3.GetObjectInput
217+
218+
if c.isPayerRequest {
219+
input = &s3.GetObjectInput{
220+
Bucket: &c.bucket,
221+
Key: key,
222+
Range: &bodyRange,
223+
RequestPayer: types.RequestPayerRequester,
224+
}
225+
} else {
226+
input = &s3.GetObjectInput{
227+
Bucket: &c.bucket,
228+
Key: key,
229+
Range: &bodyRange,
230+
}
231+
}
232+
233+
output, err := c.client.GetObject(ctx, input, getClientCredentialsModifyFn(c.isSrcClient, SRC_CRED, DST_CRED))
234+
if err != nil {
235+
log.Printf("S3> Unable to download %s with range: %s - %s\n", *key, bodyRange, err.Error())
236+
return nil, err
237+
}
238+
239+
defer output.Body.Close()
240+
241+
// Read response body
242+
s, err := io.ReadAll(output.Body)
243+
244+
if err != nil {
245+
log.Printf("S3> Unable to read the content of %s - %s\n", *key, err.Error())
246+
return nil, err
247+
}
248+
return s, nil
249+
250+
}
251+
212252
// Internal func to call API to list objects.
213253
func (c *S3Client) listObjectFn(ctx context.Context, continuationToken, prefix, delimiter *string, maxKeys int32) (*s3.ListObjectsV2Output, error) {
214254

dth/common.go

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,13 @@ var (
4747

4848
// Object represents an object to be replicated.
4949
type Object struct {
50-
Key string `json:"key"`
51-
Size int64 `json:"size"`
52-
Sequencer string `json:"sequencer,omitempty"`
50+
Key string `json:"key"`
51+
Size int64 `json:"size"`
52+
Sequencer string `json:"sequencer,omitempty"`
53+
PartNumber int `json:"partNumber,omitempty"`
54+
TotalPartsCount int `json:"totalPartsCount,omitempty"`
55+
UploadID string `json:"uploadID,omitempty"`
56+
BodyRange string `json:"bodyRange,omitempty"`
5357
}
5458

5559
// S3Event represents a basic structure of a S3 Event Message
@@ -63,6 +67,15 @@ type S3Event struct {
6367
}
6468
}
6569

70+
// SinglePartTransferEvent represents a basic structure of a SinglePart Transfer Event Message
71+
type SinglePartTransferEvent struct {
72+
ObjectKey string `json:"objectKey"`
73+
PartNumber int `json:"partNumber"`
74+
TotalPartsCount int `json:"totalPartsCount"`
75+
UploadID string `json:"uploadID"`
76+
BodyRange string `json:"bodyRange"`
77+
}
78+
6679
// Part represents a part for multipart upload
6780
type Part struct {
6881
partNumber int
@@ -132,6 +145,20 @@ func newS3Event(str *string) (e *S3Event) {
132145
return
133146
}
134147

148+
// Helper function to create single part transfer event base on Json string
149+
func newSinglePartTransferEvent(str *string) (e *SinglePartTransferEvent) {
150+
151+
e = new(SinglePartTransferEvent)
152+
err := json.Unmarshal([]byte(*str), e)
153+
154+
if err != nil {
155+
log.Printf("Unable to convert string to single part transfer job - %s", err.Error())
156+
return nil
157+
}
158+
// log.Printf("Key: %s, Size: %d\n", m.Key, m.Size)
159+
return
160+
}
161+
135162
func getHex(str *string) int64 {
136163
i64, _ := strconv.ParseInt(*str, 16, 64)
137164
return i64
@@ -169,3 +196,21 @@ func appendPrefix(key, prefix *string) *string {
169196
newkey := fmt.Sprintf("%s%s%s", *prefix, delimiter, *key)
170197
return &newkey
171198
}
199+
200+
func calculateCompletedBytes(bodyRange string) int64 {
201+
// bodyRange format: "bytes=startByte-endByte"
202+
parts := strings.Split(bodyRange, "=")
203+
if len(parts) != 2 {
204+
return 0
205+
}
206+
207+
rangeParts := strings.Split(parts[1], "-")
208+
if len(rangeParts) != 2 {
209+
return 0
210+
}
211+
212+
startByte, _ := strconv.ParseInt(rangeParts[0], 10, 64)
213+
endByte, _ := strconv.ParseInt(rangeParts[1], 10, 64)
214+
215+
return endByte - startByte + 1
216+
}

dth/config.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,14 @@ const (
3434
// DefaultMaxKeys is the maximum number of keys returned per listing request, default is 1000
3535
DefaultMaxKeys int32 = 1000
3636

37-
// DefaultMultipartThreshold is the threshold size (in MB) to determine to use multipart upload or not.
37+
// DefaultMultipartThreshold is the threshold size (in MB) to determine to use multipart upload or not (in single worker node).
3838
// When object size is greater or equals to MultipartThreshold, multipart upload will be used.
3939
DefaultMultipartThreshold int = 10
4040

41+
// DefaultGiantFileThreshold is the threshold size (in MB) to determine to use multipart upload or not (in whole worker cluster).
42+
// When object size is greater or equals to GiantFileThreshold, the object will to split and transferred by the whole cluster.
43+
DefaultGiantFileThreshold int = 1024
44+
4145
// DefaultChunkSize is the chunk size (in MB) for each part when using multipart upload
4246
DefaultChunkSize int = 5
4347

@@ -59,16 +63,16 @@ const (
5963

6064
// JobOptions is General Job Info
6165
type JobOptions struct {
62-
ChunkSize, MultipartThreshold, MessageBatchSize, FinderDepth, FinderNumber, WorkerNumber int
63-
MaxKeys int32
64-
IncludeMetadata bool
66+
ChunkSize, MultipartThreshold, GiantFileThreshold, MessageBatchSize, FinderDepth, FinderNumber, WorkerNumber int
67+
MaxKeys int32
68+
IncludeMetadata bool
6569
}
6670

6771
// JobConfig is General Job Info
6872
type JobConfig struct {
6973
SrcType, SrcBucket, SrcPrefix, SrcPrefixList, SrcRegion, SrcEndpoint, SrcCredential string
7074
DestBucket, DestPrefix, DestRegion, DestCredential, DestStorageClass, DestAcl string
71-
JobTableName, JobQueueName string
75+
JobTableName, JobQueueName, SinglePartTableName, SfnArn string
7276
SrcInCurrentAccount, DestInCurrentAccount, SkipCompare, PayerRequest bool
7377
*JobOptions
7478
}

0 commit comments

Comments
 (0)