Optimize Object Storage Performance with the AWS Go SDK
Zuletzt aktualisiert am
Understanding Performance: Workers vs. Concurrency
Section titled “Understanding Performance: Workers vs. Concurrency”To achieve high throughput when interacting with Object Storage, it is essential to understand how the AWS SDK for Go handles data transfer. Performance tuning involves two main dimensions:
- Workers (Horizontal Parallelism): This refers to the number of separate files being uploaded simultaneously. Increasing workers is the most effective way to improve performance for small files, where the overhead of the HTTP handshake is the primary bottleneck.
- Concurrency (Vertical Parallelism): This is managed by the S3 Transfer Manager. It determines how many parts of a single large file are uploaded in parallel using Multipart Upload. This is crucial for saturating bandwidth with large files.
Configuration Guidelines
Section titled “Configuration Guidelines”Based on our testing, here are example starting points for different workloads. These parameters should be adjusted based on your environment’s resources.
| Workload Type | File Size | Recommended Workers | Concurrency (per File) | Part Size |
|---|---|---|---|---|
| Small Files | ~1 MB | 400 | 1 | N/A |
| Medium Files | 10 - 100 MB | 40 - 200 | 4 - 8 | 5 MB |
| Large Files | > 1000 MB | 8 - 16 | 32 - 64 | 64 MB |
How to Determine Worker and Concurrency Values
Section titled “How to Determine Worker and Concurrency Values”There is no single magic formula for the optimal number of workers and concurrency, as it depends entirely on your environment. The key is to measure, tune, and repeat.
Guiding Principles
Section titled “Guiding Principles”- Start with
workersfor file-level parallelism.- Goal: Keep the CPU and network busy by processing multiple files at once. This is most effective for workloads with many small-to-medium-sized files.
- Starting Point: A good starting point is
2to4times the number of CPU cores on your machine. For an 8-core machine, start with16to32workers. - Limiting Factors:
- CPU: Too many workers can cause excessive context switching, where the CPU spends more time switching between tasks than doing actual work.
- Memory: Each worker and its associated upload tasks consume memory.
- File Handles: Your operating system has a limit on the number of open files.
- Tune
concurrencyfor single-file throughput.- Goal: Saturate your network connection when uploading a single large file.
- Starting Point: For large files (>100 MB) on a fast network, values between
8and32are common. The SDK’s default is 10. - Limiting Factors:
- Network Bandwidth: If your network link is saturated, increasing concurrency further will not help and may even slightly degrade performance due to overhead.
- Memory: Each concurrent part consumes a buffer in memory (
PartSize). The total memory for one file upload is roughlyWorkers * Concurrency * PartSize.
The Tuning Process
Section titled “The Tuning Process”Follow this iterative process to find the right balance:
-
Establish a Baseline.
Run the script with low, conservative values (e.g.,
WORKERS=4,CONCURRENCY=4). This is your baseline performance. -
Increase Workers.
Keep
CONCURRENCYfixed and gradually increaseWORKERS(e.g., 4, 8, 16, 32, 64). Monitor your CPU usage and total upload time. You will reach a point where adding more workers no longer improves performance or even makes it worse. This is your optimal worker count for that file set. -
Increase Concurrency.
Using your optimal
WORKERScount, now begin to increaseCONCURRENCY(e.g., 4, 8, 16, 32). This will primarily help if you have large files in your dataset. Again, find the point of diminishing returns. -
Adjust Part Size.
For very large files (multiple GB), a larger
PART_SIZE_MB(e.g., 64, 128, 256) can be more efficient, as it reduces the total number of parts and API calls required for an upload.
By methodically tuning these three parameters, you can tailor the performance to the specific characteristics of your hardware and workload.
Example Go Script
Section titled “Example Go Script”This script configures the S3 Client and the Upload service. It uses a Worker Pool to upload multiple files from a local directory in parallel. You can tune the performance directly within the CONFIGURATION block.
Save as main.go
package main
import ( "context" "fmt" "log" "net/http" "os" "path/filepath" "sync" "time"
"github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/feature/s3/manager" "github.com/aws/aws-sdk-go-v2/service/s3")
func main() { // --- CONFIGURATION --- // 1. Set your S3 Bucket and Region bucket := "your-s3-bucket-name" region := "eu01"
// 2. Set Local Source and Performance Parameters srcDir := "./test-data" // Directory containing your test files
// Performance Tuning workers := 8 // How many files to upload at the same time concurrency := 32 // How many chunks per file to upload at once partSizeMB := 64 // The size of each chunk in Megabytes // ---------------------
ctx := context.TODO()
// 3. Initialize SDK with a 15s Timeout cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region), config.WithHTTPClient(&http.Client{ Timeout: 15 * time.Second, }), ) if err != nil { log.Fatalf("unable to load SDK config: %v", err) }
client := s3.NewFromConfig(cfg) uploader := manager.NewUploader(client, func(u *manager.Uploader) { u.PartSize = int64(partSizeMB) * 1024 * 1024 u.Concurrency = concurrency })
// 4. Gather all files from the source directory files, err := os.ReadDir(srcDir) if err != nil { log.Fatalf("failed to read directory %q: %v", srcDir, err) }
// 5. Worker Pool Logic jobs := make(chan string, len(files)) var wg sync.WaitGroup start := time.Now()
fmt.Printf("Starting benchmark: %d workers, %d concurrency per file, %dMB part size\n", workers, concurrency, partSizeMB)
for w := 1; w <= workers; w++ { wg.Add(1) go func(workerID int) { defer wg.Done() for fileName := range jobs { fullPath := filepath.Join(srcDir, fileName) file, err := os.Open(fullPath) if err != nil { fmt.Printf("[Worker %d] Error opening %s: %v\n", workerID, fileName, err) continue }
_, err = uploader.Upload(ctx, &s3.PutObjectInput{ Bucket: &bucket, Key: &fileName, Body: file, }) file.Close()
if err != nil { fmt.Printf("[Worker %d] Error uploading %s: %v\n", workerID, fileName, err) } } }(w) }
// Send files to the worker pool for _, f := range files { if !f.IsDir() { jobs <- f.Name() } } close(jobs) wg.Wait()
fmt.Printf("\nFinished! Uploaded %d files in %v\n", len(files), time.Since(start))}How to Run the Test script
Section titled “How to Run the Test script”-
Set Credentials
Export your AWS access key and secret key in your terminal. The script will automatically use them to authenticate.
Terminal window export AWS_ACCESS_KEY_ID="YOUR_ACCESS_KEY"export AWS_SECRET_ACCESS_KEY="YOUR_SECRET_KEY" -
Prepare Test Data
The script uploads files from a local directory (default ./test-data) First, create the directory:
Terminal window mkdir -p ./test-dataNext, create some sample files to simulate a workload. The
ddcommand is useful for this.Here are three examples of how to create test files in different sizes. Each for loop will create 1 GB of test files with different file sizes.
Terminal window # Create 4 large 256 MB filefor i in {1..4}; dodd if=/dev/zero of=./test-data/large_256MB_$i.tmp bs=1M count=256 2>/dev/nulldoneTerminal window # Create 20 medium 50 MB filefor i in {1..20}; dodd if=/dev/zero of=./test-data/medium_50MB_$i.tmp bs=1M count=50 2>/dev/nulldoneTerminal window # Create 1024 small 1 MB filefor i in {1..1024}; dodd if=/dev/zero of=./test-data/small_1MB_$i.tmp bs=1M count=1 2>/dev/nulldone -
Tune the Parameters
Go into the main.go script and choose the right parameter under the Configuration Section. Change the following lines when needed:
// Performance Tuningworkers := 8 // How many files to upload at the same timeconcurrency := 32 // How many chunks per file to upload at oncepartSizeMB := 64 // The size of each chunk in Megabytes -
Run the Script
Terminal window go mod init main.gogo mod tidygo run main.go