エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

AWS FargateのARM(Graviton2)は使うべき? (2021年12月時点)

こんにちは、エムスリーエンジニアリンググループの福林 (@fukubaya) です。 本記事はエムスリー Advent Calendar 2021 の13日目の記事です。

先日、AWS FargateのGraviton2への対応が発表、リリースされました。

aws.amazon.com

Apple M1の処理性能でも話題になったARMプロセッサをFargateでも使えるようになったということで、 今回はちょうど開発中のAPIサーバの負荷テストも兼ねて、x86とARM(Graviton2)の性能を比較してみました。

f:id:fukubaya:20211210145139j:plain
伊田商店街は福岡県田川市の商店街。本文には特に関係ありません。

価格は文字通り20%オフ

費用に関してはCPUもメモリも文字通り20%オフになっているようです(東京リージョン)。

x86 ARM
1時間あたりのvCPU単位 0.05056USD 0.04045USD
1時間あたりのGB単位 0.00553USD 0.00442USD

aws.amazon.com

費用が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です。

f:id:fukubaya:20211210152712p:plain
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コンソール画面ではタスク定義の更新ができないようなので、新画面になっている場合は、旧画面に戻します。

f:id:fukubaya:20211210161831p:plainf:id:fukubaya:20211210161848p:plain
旧画面(左)と新画面(右)

旧画面になったら、対象のタスク定義を選んで「新しいリビションを作成」をクリックします。

f:id:fukubaya:20211210162608p:plain
新しいタスク定義を作成

UI画面ではCPUを選べないので、JSONで設定します。画面下部の「JSONによる設定」をクリックし、JSONを表示させます。

f:id:fukubaya:20211210162124p:plain
JSONによる設定

下の方に "runtimePlatform" があるので、ここでOSとCPUを指定します。

    "runtimePlatform": {
        "operatingSystemFamily": "LINUX",
        "cpuArchitecture": "ARM64"
    },

ここまで設定できたら、「作成」をクリックして定義を新規作成します。

さらに、ECSサービスを更新して使用するタスク定義を今作ったタスク定義に更新します。これでARM64で実行する環境が用意できました。

f:id:fukubaya:20211210162827p:plain
ECSサービスの更新

と、ここまで書いたところで再度確かめたところ、これを書いている数時間前くらいにterraformのaws providerの 3.69.0 がリリースされて対応されてました。

github.com

タスク定義に 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アーキテクチャは表示されないので、 今度は新画面に切り替えて確認します。

f:id:fukubaya:20211210163736p:plain
新画面のタスク詳細

比較1 固定レスポンスを返す

まずはWebアプリケーションとしての単純な性能を検証するため、固定の文字列を返すだけのエンドポイントへ、一定の負荷をかけて、その際のCPU負荷を比較してみます。

# バージョンを返すだけ
$ curl http://api-on-alb/healthcheck
{"version":"v1"}

対象がGolangなので、負荷ツールもGolangにしてみました。今回はvegetaを使いました。

github.com

$ 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だけです)。

f:id:fukubaya:20211210170054p:plain
x86とARMの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)に達しない程度にしてあります。

以下が結果です。

f:id:fukubaya:20211210171830p:plain
x86とARMのCPU負荷比較

ほぼ差がないように見えます。若干ARMの方が負荷が高いくらいでしょうか。

rateを下げて(30req/秒)実施したところ、少しの差ですがARMの方が優位な結果が得られました。 もしかしたら90req/秒だとDynamoDBの読み取り上限に近く、平均ではRCU=100には届いていませんでしたが、瞬間的にDynamoDBの待ちが発生していたかもしれません。 やはりDBなどのI/Oや外部リソースが絡むと、それらの待ちや負荷の影響が支配的になって、CPUの性能差が生かしきれない場合があると思います。

f:id:fukubaya:20211210185737p:plain
x86とARMのCPU負荷比較

まとめ

  • I/Oなどの影響が少ない処理で比較するとARMの方がx86より20%程度は性能が高いと言えそう
  • DBなどのI/O待ちがあると、そちらの方が支配的になるので、CPUの性能差は生かしきれない
  • I/OがないWebアプリケーションはまずないので、全体的なパフォーマンスを向上させたないならCPUよりI/Oの方を気にするべき
  • とは言っても、ARMの方が料金で20%安いので、大幅に性能が落ちなければそれだけで移行する価値あり

We are hiring!

今回紹介したように、弊社ではエンジニアのインフラ設計、技術選定の自由度が高い環境が用意されています。一緒に参加してくれる仲間を募集中です。お気軽にお問い合わせください。

jobs.m3.com