Lambda × GoでS3間並列コピーのパフォーマンスを検証してみた
Mobility Technologiesでサーバサイドのエンジニアをしているteriyakisanです。
最近はAWSのCLIをもっと使っていきたいと思いつつ、無意識にブラウザからコンソールを開いてAuthyを立ち上げてしまう自分と葛藤しています。
はじめに
STAY HOMEだったGWあたりから「プログラミング言語Go完全入門」でGoに入門し、高鮮度地図メンテナンスのためのデータ処理の一部をAWS LambdaとGoで構築しています。
開発を進める中で、大量の動画データが格納されたS3から条件に合致する動画をフォルダ整理した上で別のS3にコピー配置したい要件があり、とりあえずループで実行したところ数ファイルでももっさりしたため並列化を検討することにしました。
AWS上でバッチオペレーションを組むこともできるようなのですが、データ処理をおこなう中で必要な動画を選別したり、配置先ディレクトリを振り分けたりする必要があったためLambda側で並列処理をおこなうことにしています。
実運用に向けてどれくらいの最大並列数にするのが良いかわからなかったため、goroutineにてAWS SDKの(中の人からS3コピーで最も効率的と聞いた)S3 CopyObjectコールを並列化し、1Lambdaで並列度を変えながら検証してみました。
検証手順
- 同一リージョンにS3バケットを2つ(from, to)用意
- コピー元(to)のバケットに20MBのファイルを1,000個用意
- 1Lambdaにてgoroutineの最大並列処理数を変えて5回ずつ処理時間を計測し、各回の平均値推移を見る
検証準備
テストファイル生成とS3バケットの作成&配置などをおこない、検証用Lambdaを準備します。
テストファイルの準備
mkdir parallel_test
cd parallel_test
for i in `seq 1 1000`
do
mkfile 20m ${i}.mp4 # 20MBのダミー動画ファイル
done
ls -1 | wc -l
# => 1000
du -sm
# => 20001
検証用バケットの作成とファイル配置
aws s3 mb s3://parallel-test-from # コピー元
aws s3 mb s3://parallel-test-to # コピー先
aws s3 sync . s3://parallel-test-from
Lambdaの設定
- リージョン:ap-northeast-1
- メモリ:512MB
- タイムアウト:15分
- ポリシー:Lambda実行権限、ClowdWatch書き込み権限、S3の読み書き権限
Lambdaの中身
package main
import (
"context"
"fmt"
"strconv"
"sync"
"time"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
)
const fromBucket = "parallel-test-from"
const toBucket = "parallel-test-to"
const fileCount = 1000
type params struct {
ParallelLimit string `json:"parallel_limit"` // 最大並列処理数
}
func handler(ctx context.Context, params *params) error {
parallelLimit, _ := strconv.Atoi(params.ParallelLimit)
sess, _ := session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
})
client := s3.New(sess)
// コピー先のファイルをすべて削除
if err := s3manager.NewBatchDeleteWithClient(client).Delete(
ctx, s3manager.NewDeleteListIterator(client,
&s3.ListObjectsInput{
Bucket: aws.String(toBucket),
},
)); err != nil {
return err
}
// 並列コピー
startedAt := time.Now()
fmt.Printf("started=%s, limit=%d\n", startedAt, parallelLimit)
var wg sync.WaitGroup
parallel := make(chan bool, parallelLimit)
for i := 1; i <= fileCount; i++ {
wg.Add(1)
go func(i int) {
defer func() {
<-parallel
wg.Done()
}()
path := fmt.Sprintf("%d.mp4", i)
parallel <- true
if _, err := client.CopyObjectWithContext(ctx, &s3.CopyObjectInput{
CopySource: aws.String(fromBucket + "/" + path),
Bucket: aws.String(toBucket),
Key: &path,
}); err != nil {
panic(err)
}
}(i)
}
wg.Wait()
fmt.Printf("completed=%s, limit=%d, elapsed(ms)=%d\n", time.Now(), parallelLimit, time.Since(startedAt)/1000/1000)
return nil
}
func main() {
lambda.Start(handler)
}
Lambdaの配置
GOOS=linux GOARCH=amd64 go build -o parallel_test
zip parallel_test.zip ./parallel_test
→ AWS Management Console上でbuildファイルを配置 🐤
検証実施
最初、直列実行と10・100・1000並列を試しましたがもう少し間も見たくなり、
結果的には10・20・50・100・200・500・1000並列で5回ずつ実行して平均値を比較しました。
実行(パラメータで最大並列度を変える)
aws lambda invoke --invocation-type Event --function-name parallelTest --region ap-northeast-1 --payload `echo -n '{"parallel_limit":"1"}' | base64` /tmp/lambda.log
ログ確認
aws logs tail --since 5m --follow /aws/lambda/parallelTest
検証結果
結果を並べたところ以下のようになりました。
直列だと8分弱(489.58秒)もかかっていたのですが、10並列で48.73秒とちょうど1/10になり、100並列だと5.98秒と1/80くらい。
以降は試行ごとにばらつきもでてしまい頭打ちな感じで、100並列よりも平均の処理時間が早くなることはありませんでした。
今回の検証結果だと、20並列が19.86秒と1/25くらいなので一番コスパは良いですね。
対象ファイルの容量やAWS側の環境要因でブレる可能性もありますが、現状だと100並列くらいまではパフォーマンスが出るようです。
ちなみに今回Lambdaの設定は512MBで実行していましたが、メモリ使用量は100並列までは60〜70MB程度、以降は80〜140MB程度まであがっていました(設定メモリは十分と判断して特にいじらず)。
処理時間計測の元データは以下です。
最大並列数 | 平均値(秒) | 1回目(ms) | 2回目(ms) | 3回目(ms) | 4回目(ms) | 5回目(ms) |
---|---|---|---|---|---|---|
1 | 489.58 | 515409 | 509299 | 514344 | 396320 | 512533 |
10 | 48.73 | 47290 | 53987 | 53213 | 46242 | 42901 |
20 | 19.86 | 15764 | 18584 | 17131 | 18840 | 29004 |
50 | 14.32 | 22469 | 12696 | 11442 | 9869 | 10475 |
100 | 5.98 | 5975 | 5355 | 4354 | 5111 | 9090 |
200 | 9.28 | 4260 | 16536 | 5870 | 5049 | 14672 |
500 | 11.22 | 5720 | 14331 | 12004 | 17471 | 6590 |
1000 | 7.55 | 6184 | 5926 | 8934 | 7035 | 9655 |
おわりに
当初は開発中のプログラムを20並列程度の設定で動かそうとしていましたが、検証結果からするともう少し並列度上げても十分動いてくれそうなので強気の設定でいきたいと思います。
わりとアドベントカレンダー駆動で検証まで持ち込んだところもありますが、実際に動かして実データを見てみることは大事だなと改めて。
また、Lambdaの場合は実行時間で課金料金も変わるのでリソースをできる限りフルに使って短くすることで、システム全体のコストダウンにも貢献できそうです。
Mobility Technologies Advent Calendar 2020 の19日目は、sobachankoさんによる「カメラが欲しいなって雑談チャンネルに書いたらカメラ沼に引きずりこまれた話」です。お楽しみに!
Discussion