Skip to content

Commit 11a6d37

Browse files
authored
Ports S3 Manager Upload and Download Buffering Strategies (#404)
* service/s3/s3manager: Add Strategies for Download and Upload Buffering * Add S3 Upload and Download Manager Performance Benchmarking Tools * service/s3/s3manager: Use sync.Pool for reuse of part buffers for streaming payloads
1 parent c7fa04b commit 11a6d37

34 files changed

+2593
-115
lines changed

CHANGELOG_PENDING.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ Deprecations
1313

1414
SDK Features
1515
---
16+
* `service/s3/s3manager`: Add Upload Buffer Provider ([#404](https://github.com/aws/aws-sdk-go-v2/pull/404))
17+
* Adds a new `BufferProvider` member for specifying how part data can be buffered in memory.
18+
* Windows platforms will now default to buffering 1MB per part to reduce contention when uploading files.
19+
* Non-Windows platforms will continue to employ a non-buffering behavior.
20+
* `service/s3/s3manager`: Add Download Buffer Provider ([#404](https://github.com/aws/aws-sdk-go-v2/pull/404))
21+
* Adds a new `BufferProvider` member for specifying how part data can be buffered in memory when copying from the http response body.
22+
* Windows platforms will now default to buffering 1MB per part to reduce contention when downloading files.
23+
* Non-Windows platforms will continue to employ a non-buffering behavior.
1624
* `service/dynamodb/dynamodbattribute`: New Encoder and Decoder Behavior for Empty Collections ([#401](https://github.com/aws/aws-sdk-go-v2/pull/401))
1725
* The `Encoder` and `Decoder` types have been enhanced to support the marshaling of empty structures, maps, and slices to and from their respective DynamoDB AttributeValues.
1826
* This change incorporates the behavior changes introduced via a marshal option in V1 ([#2834](https://github.com/aws/aws-sdk-go/pull/2834))
@@ -24,6 +32,9 @@ SDK Enhancements
2432
* Related to [aws/aws-sdk-go#2310](https://github.com/aws/aws-sdk-go/pull/2310)
2533
* Fixes [#251](https://github.com/aws/aws-sdk-go-v2/issues/251)
2634
* `aws/request` : Retryer is now a named field on Request. ([#393](https://github.com/aws/aws-sdk-go-v2/pull/393))
35+
* `service/s3/s3manager`: Adds `sync.Pool` to allow reuse of part buffers for streaming payloads ([#404](https://github.com/aws/aws-sdk-go-v2/pull/404))
36+
* Fixes [#402](https://github.com/aws/aws-sdk-go-v2/issues/402)
37+
* Uses the new behavior introduced in V1 [#2863](https://github.com/aws/aws-sdk-go/pull/2863) which allows the reuse of the sync.Pool across multiple Upload request that match part sizes.
2738

2839
SDK Bugs
2940
---

Makefile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ LINTIGNOREDEPS='vendor/.+\.go'
88
LINTIGNOREPKGCOMMENT='service/[^/]+/doc_custom.go:.+package comment should be of the form'
99
LINTIGNOREENDPOINTS='aws/endpoints/defaults.go:.+(method|const) .+ should be '
1010
UNIT_TEST_TAGS="example codegen awsinclude"
11+
ALL_TAGS="example codegen awsinclude integration perftest"
1112

1213
# SDK's Core and client packages that are compatable with Go 1.9+.
1314
SDK_CORE_PKGS=./aws/... ./private/... ./internal/...
@@ -56,11 +57,14 @@ cleanup-models:
5657
###################
5758
# Unit/CI Testing #
5859
###################
59-
unit: verify
60+
build:
61+
go build -o /dev/null -tags ${ALL_TAGS} ${SDK_ALL_PKGS}
62+
63+
unit: verify build
6064
@echo "go test SDK and vendor packages"
6165
@go test -tags ${UNIT_TEST_TAGS} ${SDK_ALL_PKGS}
6266

63-
unit-with-race-cover: verify
67+
unit-with-race-cover: verify build
6468
@echo "go test SDK and vendor packages"
6569
@go test -tags ${UNIT_TEST_TAGS} -race -cpu=1,2,4 ${SDK_ALL_PKGS}
6670

internal/awstesting/discard.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package awstesting
2+
3+
// DiscardAt is an io.WriteAt that discards
4+
// the requested bytes to be written
5+
type DiscardAt struct{}
6+
7+
// WriteAt discards the given []byte slice and returns len(p) bytes
8+
// as having been written at the given offset. It will never return an error.
9+
func (d DiscardAt) WriteAt(p []byte, off int64) (n int, err error) {
10+
return len(p), nil
11+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package awstesting
2+
3+
// EndlessReader is an io.Reader that will always return
4+
// that bytes have been read.
5+
type EndlessReader struct{}
6+
7+
// Read will report that it has read len(p) bytes in p.
8+
// The content in the []byte will be unmodified.
9+
// This will never return an error.
10+
func (e EndlessReader) Read(p []byte) (int, error) {
11+
return len(p), nil
12+
}

internal/awstesting/integration/integration.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"crypto/rand"
99
"fmt"
1010
"io"
11+
"io/ioutil"
1112
"os"
1213

1314
"github.com/aws/aws-sdk-go-v2/aws"
@@ -63,3 +64,36 @@ func ConfigWithDefaultRegion(region string) aws.Config {
6364

6465
return cfg
6566
}
67+
68+
// CreateFileOfSize will return an *os.File that is of size bytes
69+
func CreateFileOfSize(dir string, size int64) (*os.File, error) {
70+
file, err := ioutil.TempFile(dir, "s3Bench")
71+
if err != nil {
72+
return nil, err
73+
}
74+
75+
err = file.Truncate(size)
76+
if err != nil {
77+
file.Close()
78+
os.Remove(file.Name())
79+
return nil, err
80+
}
81+
82+
return file, nil
83+
}
84+
85+
// SizeToName returns a human-readable string for the given size bytes
86+
func SizeToName(size int) string {
87+
units := []string{"B", "KB", "MB", "GB"}
88+
i := 0
89+
for size >= 1024 {
90+
size /= 1024
91+
i++
92+
}
93+
94+
if i > len(units)-1 {
95+
i = len(units) - 1
96+
}
97+
98+
return fmt.Sprintf("%d%s", size, units[i])
99+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
## Performance Utility
2+
3+
Downloads a test file from a S3 bucket using the SDK's S3 download manager. Allows passing
4+
in custom configuration for the HTTP client and SDK's Download Manager behavior.
5+
6+
## Build
7+
### Standalone
8+
```sh
9+
go build -tags "integration perftest" -o s3DownloadManager ./awstesting/integration/performance/s3DownloadManager
10+
```
11+
### Benchmarking
12+
```sh
13+
go test -tags "integration perftest" -c -o s3DownloadManager ./awstesting/integration/performance/s3DownloadManager
14+
```
15+
16+
## Usage Example:
17+
### Standalone
18+
```sh
19+
AWS_REGION=us-west-2 AWS_PROFILE=aws-go-sdk-team-test ./s3DownloadManager \
20+
-bucket aws-sdk-go-data \
21+
-size 10485760 \
22+
-client.idle-conns 1000 \
23+
-client.idle-conns-host 300 \
24+
-client.timeout.connect=1s \
25+
-client.timeout.response-header=1s
26+
```
27+
28+
### Benchmarking
29+
```sh
30+
AWS_REGION=us-west-2 AWS_PROFILE=aws-go-sdk-team-test ./s3DownloadManager \
31+
-test.bench=. \
32+
-test.benchmem \
33+
-test.benchtime 1x \
34+
-bucket aws-sdk-go-data \
35+
-client.idle-conns 1000 \
36+
-client.idle-conns-host 300 \
37+
-client.timeout.connect=1s \
38+
-client.timeout.response-header=1s
39+
```
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// +build integration,perftest
2+
3+
package main
4+
5+
import (
6+
"net"
7+
"net/http"
8+
"time"
9+
)
10+
11+
func NewClient(cfg ClientConfig) *http.Client {
12+
tr := &http.Transport{
13+
Proxy: http.ProxyFromEnvironment,
14+
DialContext: (&net.Dialer{
15+
Timeout: cfg.Timeouts.Connect,
16+
KeepAlive: 30 * time.Second,
17+
DualStack: true,
18+
}).DialContext,
19+
MaxIdleConns: cfg.MaxIdleConns,
20+
MaxIdleConnsPerHost: cfg.MaxIdleConnsPerHost,
21+
IdleConnTimeout: 90 * time.Second,
22+
23+
DisableKeepAlives: !cfg.KeepAlive,
24+
TLSHandshakeTimeout: cfg.Timeouts.TLSHandshake,
25+
ExpectContinueTimeout: cfg.Timeouts.ExpectContinue,
26+
ResponseHeaderTimeout: cfg.Timeouts.ResponseHeader,
27+
}
28+
29+
return &http.Client{
30+
Transport: tr,
31+
}
32+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// +build integration,perftest
2+
3+
package main
4+
5+
import (
6+
"flag"
7+
"fmt"
8+
"net/http"
9+
"strings"
10+
"time"
11+
12+
"github.com/aws/aws-sdk-go-v2/service/s3/s3manager"
13+
)
14+
15+
type Config struct {
16+
Bucket string
17+
Size int64
18+
LogVerbose bool
19+
20+
SDK SDKConfig
21+
Client ClientConfig
22+
}
23+
24+
func (c *Config) SetupFlags(prefix string, flagset *flag.FlagSet) {
25+
flagset.StringVar(&c.Bucket, "bucket", "",
26+
"The S3 bucket `name` to download the object from.")
27+
flagset.Int64Var(&c.Size, "size", 0,
28+
"The S3 object size in bytes to be first uploaded then downloaded")
29+
flagset.BoolVar(&c.LogVerbose, "verbose", false,
30+
"The output log will include verbose request information")
31+
32+
c.SDK.SetupFlags(prefix, flagset)
33+
c.Client.SetupFlags(prefix, flagset)
34+
}
35+
36+
func (c *Config) Validate() error {
37+
var errs Errors
38+
39+
if len(c.Bucket) == 0 || c.Size <= 0 {
40+
errs = append(errs, fmt.Errorf("bucket and filename/size are required"))
41+
}
42+
43+
if err := c.SDK.Validate(); err != nil {
44+
errs = append(errs, err)
45+
}
46+
if err := c.Client.Validate(); err != nil {
47+
errs = append(errs, err)
48+
}
49+
50+
if len(errs) != 0 {
51+
return errs
52+
}
53+
54+
return nil
55+
}
56+
57+
type SDKConfig struct {
58+
PartSize int64
59+
Concurrency int
60+
BufferProvider s3manager.WriterReadFromProvider
61+
}
62+
63+
func (c *SDKConfig) SetupFlags(prefix string, flagset *flag.FlagSet) {
64+
prefix += "sdk."
65+
66+
flagset.Int64Var(&c.PartSize, prefix+"part-size", s3manager.DefaultDownloadPartSize,
67+
"Specifies the `size` of parts of the object to download.")
68+
flagset.IntVar(&c.Concurrency, prefix+"concurrency", s3manager.DefaultDownloadConcurrency,
69+
"Specifies the number of parts to download `at once`.")
70+
}
71+
72+
func (c *SDKConfig) Validate() error {
73+
return nil
74+
}
75+
76+
type ClientConfig struct {
77+
KeepAlive bool
78+
Timeouts Timeouts
79+
80+
MaxIdleConns int
81+
MaxIdleConnsPerHost int
82+
}
83+
84+
func (c *ClientConfig) SetupFlags(prefix string, flagset *flag.FlagSet) {
85+
prefix += "client."
86+
87+
flagset.BoolVar(&c.KeepAlive, prefix+"http-keep-alive", true,
88+
"Specifies if HTTP keep alive is enabled.")
89+
90+
defTR := http.DefaultTransport.(*http.Transport)
91+
92+
flagset.IntVar(&c.MaxIdleConns, prefix+"idle-conns", defTR.MaxIdleConns,
93+
"Specifies max idle connection pool size.")
94+
95+
flagset.IntVar(&c.MaxIdleConnsPerHost, prefix+"idle-conns-host", http.DefaultMaxIdleConnsPerHost,
96+
"Specifies max idle connection pool per host, will be truncated by idle-conns.")
97+
98+
c.Timeouts.SetupFlags(prefix, flagset)
99+
}
100+
101+
func (c *ClientConfig) Validate() error {
102+
var errs Errors
103+
104+
if err := c.Timeouts.Validate(); err != nil {
105+
errs = append(errs, err)
106+
}
107+
108+
if len(errs) != 0 {
109+
return errs
110+
}
111+
return nil
112+
}
113+
114+
type Timeouts struct {
115+
Connect time.Duration
116+
TLSHandshake time.Duration
117+
ExpectContinue time.Duration
118+
ResponseHeader time.Duration
119+
}
120+
121+
func (c *Timeouts) SetupFlags(prefix string, flagset *flag.FlagSet) {
122+
prefix += "timeout."
123+
124+
flagset.DurationVar(&c.Connect, prefix+"connect", 30*time.Second,
125+
"The `timeout` connecting to the remote host.")
126+
127+
defTR := http.DefaultTransport.(*http.Transport)
128+
129+
flagset.DurationVar(&c.TLSHandshake, prefix+"tls", defTR.TLSHandshakeTimeout,
130+
"The `timeout` waiting for the TLS handshake to complete.")
131+
132+
flagset.DurationVar(&c.ExpectContinue, prefix+"expect-continue", defTR.ExpectContinueTimeout,
133+
"The `timeout` waiting for the TLS handshake to complete.")
134+
135+
flagset.DurationVar(&c.ResponseHeader, prefix+"response-header", defTR.ResponseHeaderTimeout,
136+
"The `timeout` waiting for the TLS handshake to complete.")
137+
}
138+
139+
func (c *Timeouts) Validate() error {
140+
return nil
141+
}
142+
143+
type Errors []error
144+
145+
func (es Errors) Error() string {
146+
var buf strings.Builder
147+
for _, e := range es {
148+
buf.WriteString(e.Error())
149+
}
150+
151+
return buf.String()
152+
}

0 commit comments

Comments
 (0)