🐤

Lambda × GoでS3間並列コピーのパフォーマンスを検証してみた

2020/12/17に公開

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