こんにちは、エムスリーエンジニアリンググループの福林 (@fukubaya) です。 本記事はエムスリー Advent Calendar 2021 の13日目の記事です。
先日、AWS FargateのGraviton2への対応が発表、リリースされました。
Apple M1の処理性能でも話題になったARMプロセッサをFargateでも使えるようになったということで、 今回はちょうど開発中のAPIサーバの負荷テストも兼ねて、x86とARM(Graviton2)の性能を比較してみました。
価格は文字通り20%オフ
費用に関してはCPUもメモリも文字通り20%オフになっているようです(東京リージョン)。
x86 | ARM | |
---|---|---|
1時間あたりのvCPU単位 | 0.05056USD | 0.04045USD |
1時間あたりのGB単位 | 0.00553USD | 0.00442USD |
費用が20%下がるのは分かるのですが、「コストパフォーマンスが最大40%向上」がよく分からないですね。
AWS Graviton2 プロセッサーを搭載した AWS Fargate は、コンテナー化されたアプリケーション向けの Intel x86 ベースの Fargate に比べて 20% 低費用で、コストパフォーマンスが最大 40% 向上しています。
Lamdaでは「最大 19% 優れたパフォーマンス」と書いてあるので、性能が向上してさらに料金が下がるので40%ということでしょうか。
AWS Graviton2 を搭載した AWS Lambda 関数は、x86 ベースのインスタンスで実行することに比べると 20% 低いコストで、最大 19% 優れたパフォーマンスを提供します。
性能はどうなのか
価格が20%下がるだけでもコストがそのまま20%下がるので、迷う点はないと思うのですが、特定の条件で性能が悪化しちゃうとか、そもそも動かないとかあると不安なので、 本番サービスにいきなり適用する前にまずは動作確認と負荷試験をして性能を調べることにしました。
x86とARMの比較ができればよいだけなので、どちらの場合も0.25vCPU、512MBのコンテナ1個で確認しています。
対象サービス
今回調査に使用するのは、ALB、ECS Fargate、DynamoDBから構成されるシンプルなREST APIです。
実装はGoのGinを使いました。Goならクロスコンパイルが用意されていて、依存ライブラリによる処理への影響も受けづらいと判断したためです。
以下がビルドに使ったDockerfileです。ビルド時にGOARCHを指定して、amd64(x86)かarm64を切り替えます。 実行用のイメージは gcr.io/distroless/static を使いました。 このイメージはマルチCPUアーキテクチャに対応しているので、amd64(x86)でもarm64でも同じイメージがそのまま使用できます。
# ビルド FROM golang:1.17.3 AS builder COPY ./go /workspace WORKDIR /workspace ARG VERSION # amd64(x86) # RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-X main.version=${VERSION}" -o main main.go routers.go wire_gen.go # arm64 RUN GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -ldflags "-X main.version=${VERSION}" -o main main.go routers.go wire_gen.go # 本体イメージの生成 # 2021/12/10時点 の latest FROM gcr.io/distroless/static@sha256:1cc74da80bbf80d89c94e0c7fe22830aa617f47643f2db73f66c8bd5bf510b25 AS app # set timezone ENV TZ="Asia/Tokyo" # releaseモードで動かす ENV GIN_MODE=release # 本体 COPY --from=builder /workspace/main /main # user USER nonroot # 起動 ENTRYPOINT ["/main"]
Fargateの設定と確認
ECSのタスク定義でOSとCPUを指定します。 新規に作る場合の例は見かけるのですが、既存のタスクを更新する例があまり見当たらなかったので説明します。 なお、弊社ではterraformでAWSリソースを管理しているのですが、まだterraformのaws providerではECSでCPU指定ができないので、コンソールから直接指定します。
まず、新しいECSコンソール画面ではタスク定義の更新ができないようなので、新画面になっている場合は、旧画面に戻します。
旧画面になったら、対象のタスク定義を選んで「新しいリビションを作成」をクリックします。
UI画面ではCPUを選べないので、JSONで設定します。画面下部の「JSONによる設定」をクリックし、JSONを表示させます。
下の方に "runtimePlatform"
があるので、ここでOSとCPUを指定します。
"runtimePlatform": { "operatingSystemFamily": "LINUX", "cpuArchitecture": "ARM64" },
ここまで設定できたら、「作成」をクリックして定義を新規作成します。
さらに、ECSサービスを更新して使用するタスク定義を今作ったタスク定義に更新します。これでARM64で実行する環境が用意できました。
と、ここまで書いたところで再度確かめたところ、これを書いている数時間前くらいにterraformのaws providerの 3.69.0 がリリースされて対応されてました。
タスク定義に runtime_platform
を指定すればよいです。
resource "aws_ecs_task_definition" "app" { family = "family" ... requires_compatibilities = [ "FARGATE" ] runtime_platform { operating_system_family = "LINUX" cpu_architecture = "ARM64" } }
実際にARM64で実行できているか確かめるのは、ECSタスクの詳細画面を見ればよいのですが、旧画面ではCPUアーキテクチャは表示されないので、 今度は新画面に切り替えて確認します。
比較1 固定レスポンスを返す
まずはWebアプリケーションとしての単純な性能を検証するため、固定の文字列を返すだけのエンドポイントへ、一定の負荷をかけて、その際のCPU負荷を比較してみます。
# バージョンを返すだけ $ curl http://api-on-alb/healthcheck {"version":"v1"}
対象がGolangなので、負荷ツールもGolangにしてみました。今回はvegetaを使いました。
$ go install github.com/tsenart/vegeta@latest
x86とARMそれぞれで、このエンドポイントに800req/秒の負荷を5分かけました。
$ echo "GET http://api-on-alb/healthcheck" | vegeta attack -rate=800 -duration=300s > x86_64-hc-rate800.bin $ echo "GET http://api-on-alb/healthcheck" | vegeta attack -rate=800 -duration=300s > arm64-hc-rate800.bin
それぞれリクエスト側(MacBookPro)で劣化することなく、ほぼ800req/分の負荷をかけられているのを確認しました。
$ vegeta report x86_64-hc-rate800.bin Requests [total, rate, throughput] 240000, 800.00, 799.83 Duration [total, attack, wait] 5m0.064334597s, 4m59.998558329s, 65.776268ms Latencies [mean, 50, 95, 99, max] 35.664921ms, 28.940342ms, 73.603096ms, 119.791629ms, 853.529323ms Bytes In [total, mean] 10320000, 43.00 Bytes Out [total, mean] 0, 0.00 Success [ratio] 100.00% Status Codes [code:count] 200:240000 Error Set: $ vegeta report arm64-hc-rate800.bin Requests [total, rate, throughput] 240000, 800.00, 793.51 Duration [total, attack, wait] 5m0.462191291s, 4m59.998757496s, 463.433795ms Latencies [mean, 50, 95, 99, max] 53.014128ms, 36.928443ms, 119.650186ms, 300.502333ms, 1.115359148s Bytes In [total, mean] 10252060, 42.72 Bytes Out [total, mean] 0, 0.00 Success [ratio] 99.34% Status Codes [code:count] 0:1580 200:238420 Error Set:
CPU負荷はCloudWatchのLog Insightsのログをクエリして取得しました。ロググループは /aws/ecs/containerinsights/クラスター名/performance
を選びます。
fields @timestamp, (CpuUtilized/256)*100, (MemoryUtilized/512)*100 | filter Type = 'Container' and TaskId = 'タスクID' | sort @timestamp asc
以下が結果です(メモリは大差なかったのでCPUだけです)。
ARMの方が10%程度低い負荷で処理できていそうです。x86より20%程度性能が向上していると言ってもよさそうです。
比較2 DynamoDBから取得したデータをシリアライズして返す
実際のWebアプリケーションは固定内容をだけでなく、外のデータソースからデータを取得し、加工して返します。 そこで、DynamoDBからデータを取得して、JSONにシリアライズして返すAPIで負荷を調べます。
$ curl "http://api-on-alb/v1/ranking/get?x=a&y=b" [{"id":...},{"id":...},...]
同じくx86とARMそれぞれで、このエンドポイントに90req/秒の負荷を5分かけました。負荷はDynamoDBの読み取り上限(RCU=100)に達しない程度にしてあります。
以下が結果です。
ほぼ差がないように見えます。若干ARMの方が負荷が高いくらいでしょうか。
rateを下げて(30req/秒)実施したところ、少しの差ですがARMの方が優位な結果が得られました。 もしかしたら90req/秒だとDynamoDBの読み取り上限に近く、平均ではRCU=100には届いていませんでしたが、瞬間的にDynamoDBの待ちが発生していたかもしれません。 やはりDBなどのI/Oや外部リソースが絡むと、それらの待ちや負荷の影響が支配的になって、CPUの性能差が生かしきれない場合があると思います。
まとめ
- I/Oなどの影響が少ない処理で比較するとARMの方がx86より20%程度は性能が高いと言えそう
- DBなどのI/O待ちがあると、そちらの方が支配的になるので、CPUの性能差は生かしきれない
- I/OがないWebアプリケーションはまずないので、全体的なパフォーマンスを向上させたないならCPUよりI/Oの方を気にするべき
- とは言っても、ARMの方が料金で20%安いので、大幅に性能が落ちなければそれだけで移行する価値あり
We are hiring!
今回紹介したように、弊社ではエンジニアのインフラ設計、技術選定の自由度が高い環境が用意されています。一緒に参加してくれる仲間を募集中です。お気軽にお問い合わせください。