Kafkaに接続するJavaアプリケーションをGravitonインスタンスへ移行してパフォーマンスを改善する

社内向けのドキュメントと兼用したので、前提とかメンバー向けの解説が含まれているので、前後のパフォーマンスの変化だけを見たい人は、下の方のグラフ画像までスクロールしてください。

gravitonインスタンスを活用するモチベーション

ワークロードによる相性はあるが、特にマルチスレッド性能で既存のインスタンスより性能向上が見られる上にコストが安いため、うまくフィットすれば性能改善とコスト削減の双方でメリットがある。

また、周辺ハードウェアもアップデートされているため、エフェメラルストレージ付きのインスタンスのストレージサイズが増えているなどのメリットもある。

特に現時点ではr6gdインスタンスが利用したかった。

gravitonインスタンスを利用するためarm64アーキテクチャへの対応

gravitonインスタンスはarm64 (aarch64) アーキテクチャのCPUのため、既存のx86_64アーキテクチャでビルドされたアプリケーションは動作しない。

現状、弊社ではほとんどのアプリケーションはコンテナとして動作しているのでコンテナイメージをarm64アーキテクチャで構築しなおす必要がある。

arm64でコンテナイメージを作成する主な方法として以下の様なものがある。

arm64アーキテクチャで動作するビルドサーバーを構築する

弊社ではこの方針を採用している。元々docker imageのキャッシュ管理やインフラリソースの自由度のためにDockerイメージビルドサーバーがあったので、arm64対応のために新規でビルドサーバーを増築した。

arm64アーキテクチャで動作するビルドサーバー上でイメージを構築すれば自動的にarm64アーキテクチャのコンテナイメージが構築できる。

構築済みのイメージのアーキテクチャは以下のコマンドで確認できる。

docker inspect <image-id> | jq '.[].Architecture'
"arm64"

docker buildx

Dockerの実験的機能として実装されているbuildxを利用することでマルチアーキテクチャイメージを構築することができる。

docker buildx create --name arm64builder --platform linux/arm64
docker buildx use arm64builder
docker buildx inspect --bootstrap

これでビルド準備が出来る。 ビルドする時は以下のコマンドを利用する。

docker buildx build --platform linux/arm64 -t imagetag --load .

しかし、手元で実験した所、何故かgradleによるビルドでqemuが死んでビルド出来ないというエラーが発生した。

dockerやqemuのバージョンで結構動作状況が違う可能性があり、まだ安定して何にでも使えるという感じではない。

arm64で動作するCIサービスの利用

例えば、circleciではmachineタイプのexecutorを指定することでarm64インスタンスを利用してCIを実行できる。 このmachine executor上でdockerを直接操作してイメージ構築とプッシュを実行できる。

https://circleci.com/docs/2.0/arm-resources/

arm64アーキテクチャのAMI構築

弊社ではpackerを利用してAMIを構築しているが、AMI構築時にもCPUアーキテクチャの変更を考慮する必要がある。 base AMIにarm64アーキテクチャ対応のものを指定し、gravitonインスタンスを設定してビルドする。

ビルド上の注意

graviton上でより良いパフォーマンスを出すためには、いくつかの最適化が必要になる場合がある。具体的には以下のドキュメントを一通り読んでおくことを推奨する。

https://github.com/aws/aws-graviton-getting-started

特にコンパイルが必要な言語に関しては、推奨されるコンパイルフラグがあるため、より意識しておく必要がある。

Javaアプリケーションに関しては、Java11以降のOpenJDKなら概ねarm64アーキテクチャを十分にサポートしているが、Java8以前のアプリケーションでパフォーマンスを十分に発揮させるためにはAmazon CorrettoというJDKの利用を検討してみると良いと書かれています。

また、graviton2の分岐予測に影響があるため、 tiered compilationとコードキャッシュの制限を変更するオプションが有効である場合があることが記載されています。 ワークロードによって有利・不利があるので適宜検証しつつ有効化するのが良いらしい。

-XX:-TieredCompilation -XX:ReservedCodeCacheSize=64M -XX:InitialCodeCacheSize=64M

弊社では、USE_GRAVITON_OPTIMIZATION環境変数に値を入れてコンテナを起動すると、アプリケーション起動時に上記オプションを有効にする様にエントリポイントスクリプトを更新した。

移行の流れ

以下の流れでインスタンスを移行していく。

build時にターゲットのCPUアーキテクチャを選択可能にする

環境変数USE_ARM64に値を入れてcapistranoを実行すると、arm64インスタンスでdocker buildを行う様に設定する。

こんな感じで実行する。

USE_ARM64=1 bundle exec cap dev_staging docker:push

ECRはdocker manifestを設定することでマルチアーキテクチャイメージを登録できるが、現状その辺りに対応するのが面倒なため、タグ名に-arm64 suffixを付与することでイメージを分ける。上記の環境変数で判別してsuffixが付与される様に分岐してある。

今回、amazon corretto JVMを利用できる様にarm64向けの新しいDockerfile-arm64を追加した。 落ち着いたらDockerfile側に統合する予定。

arm64対応のautoscaling groupとECS clusterを追加

terraformで新しいクラスタとそのためのautoscaling groupを追加する。 AMIも新しくarm64に構築したものに変更する。 クラスタごと分けるのはインスタンスに対する配置戦略等を設定するのが面倒で、混在していると移行期に分かりにくいので、箱ごと分けることにする。

デプロイ対象のイメージとクラスターを変更し新しい環境にECSサービスを作る

arm64用のクラスタでアプリケーションを起動できる準備をする

arm64クラスタでアプリケーションを起動しつつ徐々に入れ替える

動作自体は、staging環境で事前に確認している。

datadogでモニタしているCPU利用率やメモリ消費のメトリックを中心に入れ替え前後で負荷にどういった変化があったか確認する。

性能比較

r5.xlarge -> r6g.xlargeに変更し数日稼動させた後、2日分のメトリックを1週間前のCPU負荷と比較。 メトリックはdatadog-agentで取得できるdocker.cpu.usageを使う。 (メトリック比較はproduction環境にて実施)

コスト参考: 0.304 USD /hour -> 0.2432 USD

appA (JVM, KafkaStreams, メインスレッド数: 4)

不規則にスパイクが発生するタイプのワークロード。 ベースラインのCPU利用率の減少とスパイク時の利用率のの減少が確認できた。 ただし、日によって負荷傾向に差があるため、安定した性能変化は次のアプリで見た方が良い。

graviton_compare2.png (294.4 kB)

app2 (JVM, KafkaStreams, メインスレッド数: 4)

定常的にデータ受信数に比例した負荷がかかるが概ね一定の負荷傾向で、昼や夕方に高負荷になり夜間は低負荷になる。 負荷が低い時間帯ではほぼ差が無いが、高負荷になる時間帯では20ポイント以上利用率が減少している。中間ぐらいの負荷になる時間帯でも10ポイント前後利用率が減少。 高負荷時にかなりの余裕が出来た。

graviton_compare.png (188.6 kB)

ちなみに、その他のメトリックについては顕著な差分は発生しなかった。

申し訳ありません、適当なこと言ってました

当初、色々作業してすぐだったのでごちゃごちゃしていて、前後比較の対象を間違って見ていたらしく、下記の様なツイートをしていました。

週明けに、ダッシュボードをちゃんと整理してたら、あれ?ちゃんとイメージ通りの結果になったなということが分かり、どうやら私が盛大に勘違いしていた可能性が濃厚です。

AWSさん、間違ったデータを元にネガティブな発言をしてしまい申し訳ありません。ちゃんとパフォーマンス出ました。最高です。ありがとうございます。これからもお世話になります。

皆もgravitonインスタンスをガンガン試していきましょう。

まとめ

インスタンスの変更だけで得られるパフォーマンス改善としてはかなりの効果があった上に1台当たりの基本コストは80%程度にまで抑えられるので、かなりコストパフォーマンスが良くなったと言える。

イメージビルドラインを複数用意したり移行にあたって別クラスタを用意する等の手間をかける必要はあるが、やる価値は十分になると思う。特にマルチスレッドに強いらしいので、GoやJava等のCPUバウンドなアプリケーションでメリットが大きいだろう。

注意点は、インスタンスのワークロード次第ではgravitonを採用できないケースもまだまだ多いので、当分の間arm64とx86_64が混在することは避けられないため、どちらでビルドしているのかをちゃんと意識しておかないとハマる可能性があること。