GravitonでPrestoを走らせる
これはArm Treasure Data Advent Calendar 20日目の記事です。
先日のRe:InventでArmベースのプロセッサGraviton 2を搭載したインスタンスタイプがAWSから発表されました。AWSは去年にもArmベースのGravitonプロセッサを使ったA1インスタンスを発表していますが、これはその次世代機になります。
Graviton 2はArmv8-AアーキテクチャをサポートしたNeoverseというマイクロアーキテクチャを元に作られたカスタムチップで、低価格でありながら高い性能を発揮することができるコアです。
Graviton 2はまだpreviewという扱いですぐに利用がすることができませんでしたので、この記事ではGravitonを搭載したA1インスタンスで分散SQLエンジンであるPrestoを動かしてみたいと思います。
ArmをサポートしたPrestoを作る
PrestoはJavaで動くアプリケーションなので、基本的にJVMがArmアーキテクチャで動作すれば動くはずです。ところがPrestoでは様々なプラグインをサポートしている関係で内部的に動作する環境(JDKのバージョンなど)を制限しています。実はCPUのアーキテクチャも厳密に制限していて、そのままではArm(aarch64)で動きません。
PrestoSystemRequirementsというクラスでこの制限が実装されているので、下記のようなパッチを当てる必要があります。チェックしている部分をコメントアウトするだけです。
diff --git a/presto-main/src/main/java/io/prestosql/server/PrestoSystemRequirements.java b/presto-main/src/main/java/io/prestosql/server/PrestoSystemRequirements.java
index 07b7d12c64..b6a1249681 100644
--- a/presto-main/src/main/java/io/prestosql/server/PrestoSystemRequirements.java
+++ b/presto-main/src/main/java/io/prestosql/server/PrestoSystemRequirements.java
@@ -71,9 +71,9 @@ final class PrestoSystemRequirements
String osName = StandardSystemProperty.OS_NAME.value();
String osArch = StandardSystemProperty.OS_ARCH.value();
if ("Linux".equals(osName)) {
- if (!"amd64".equals(osArch) && !"ppc64le".equals(osArch)) {
- failRequirement("Presto requires amd64 or ppc64le on Linux (found %s)", osArch);
- }
+// if (!"amd64".equals(osArch) && !"ppc64le".equals(osArch)) {
+// failRequirement("Presto requires amd64 or ppc64le on Linux (found %s)", osArch);
+// }
if ("ppc64le".equals(osArch)) {
warnRequirement("Support for the POWER architecture is experimental");
}
Prestoメンテナによればこの制限はHadoop関連のプラグインのためのものらしいので、Hive Connectorを使わない限りは結果的にはPresto側の準備はこれだけで済みます。
ArmをサポートしたDockerイメージを作る
今回はDockerを使ってPrestoを走らます。下記のツールでArmをサポートしたイメージを作成します。
DockerにはbuildxというMoby BuildKitを使ってクロスプラットフォームなイメージをビルドする実験的な機能があります。macOSの場合には下記のExperimental Flagを有効にしてDockerを再起動させます。
これでbuildxコマンドが使えるようになりました。下記のコマンドでイメージをビルドします。
$ docker buildx build \
--build-arg VERSION=317-SNAPSHOT \
--platform linux/arm64 \
-f presto-base/Dockerfile-aarch64 \
-t lewuathe/presto-base:317-SNAPSHOT-aarch64 \
presto-base --push
$ docker buildx build \
--build-arg VERSION=317-SNAPSHOT-aarch64 \
--platform linux/arm64 \
-t lewuathe/presto-coordinator:317-SNAPSHOT-aarch64 \
presto-coordinator --push
$ docker buildx build \
--build-arg VERSION=317-SNAPSHOT-aarch64 \
--platform linux/arm64 \
-t lewuathe/presto-worker:317-SNAPSHOT-aarch64 \
presto-worker --push
--platformオプションは複数のターゲットプラットフォームを指定できるのですが、Arm用のOpenJDKのイメージがarm64v8という異なるOrganizationから配布されていたので、Dockerfile自体を分ける必要がありひとつのコマンドで複数プラットフォーム用にビルドすることはできませんでした。
--pushオプションで同時にDocker Hubにあげてくれるので、晴れてArmアーキテクチャで動くPrestoのDockerイメージが利用可能となりました。
- lewuathe/presto-coordinator:327-SNAPSHOT-aarch64
- lewuathe/presto-worker:327-SNAPSHOT-aarch64
A1インスタンスをセットアップする
簡単にクラスタを起動するためdocker-composeを使いたいのですが、docker-composeのArm用ビルド済バイナリは配布されていないようだったので、自分でインストール時にビルドする必要があります。
ArmプラットフォームをサポートしているAmazon Linuxを選択し、A1インスタンスを立ち上げたあと下記のようにdocker-composeをインストールします。
# Dockerをインストール
$ sudo yum update -y
$ sudo amazon-linux-extras install docker -y
$ sudo service docker start
$ sudo usermod -a -G docker ec2-user
# docker-composeをインストール
$ sudo yum install python2-pip gcc libffi-devel openssl-devel -y
$ sudo pip install -U docker-compose
これですべての準備が完了しました。下記のdocker-compose.yamlで1つのcoordinatorと2つのworkerを持つPrestoクラスタをA1インスタンス上で立ち上げることができます。
version: '3'
services:
coordinator:
image: "lewuathe/presto-coordinator:327-SNAPSHOT-aarch64"
ports:
- "8080:8080"
container_name: "coordinator"
command: coordinator
worker0:
image: "lewuathe/presto-worker:327-SNAPSHOT-aarch64"
container_name: "worker0"
ports:
- "8081:8081"
command: worker0
worker1:
image: "lewuathe/presto-worker:327-SNAPSHOT-aarch64"
container_name: "worker1"
ports:
- "8082:8081"
command: worker1
パフォーマンスを比較してみる
せっかくなので、他のインスタンスと実際にクエリを走らせた実行時間を比較してみます。環境は下記のとおりです。
- Prestoはこのコミットまでのmasterブランチに先述のパッチをあてた326と327の間のソースコードを利用
- 1 coordinator, 2 workerのクラスタをdocker-composeで単一インスタンス上で起動
- 使用したインスタンスはa1.4xlargeとコア数、メモリが同じサイズのc5.4xlargeと価格帯が近いm5.2xlargeを使用
- ベンチマークとしてはTPCHのq01, q10, q18, q20を利用。PrestoのTPCHコネクタは外部ストレージに依存せずに内部でデータを生成するので、ネットワーク性能に大きく影響されずにCPUの性能を測ることが可能
- TPCHのscaling factorはtinyとsf1を選択
- クエリを5回走らせ実行時間が安定した後、さらに5回走らせてその平均をとる
下記が結果となります。縦軸はミリ秒(ms)です。左がtiny、右がsf1でのパフォーマンスです。
c5.4xlargeがコンスタントにすべてのケースで速いことがわかります。m5.2xlargeとの比較ではクエリによってはa1.4xlargeと順位が入れ替わることもわかります。
ここではOpenJDK 8のベースイメージを使いましたが古いJDKだとあまりArm用に最適化されてないようで、一般的にJDK 9や11の方がよいパフォーマンスがでるようです。
最新のPrestoは新しいJDKもサポートするので、OpenJDK 11も試してみましょう。JDK 9からAttach APIがデフォルトではオフにされてしまったので、OpenJDK 11で動かすにはこれを許可するオプションだけ有効にする必要があります。jvm.configファイルに下記の1行を追加します。
-Djdk.attach.allowAttachSelf=true
下記がそれぞれのインスタンスタイプ上でOpenJDK 11を使って動かしたパフォーマンスの比較です。
a1.4xlargeとc5.4xlargeではJDK 8を使ったときより常に速くなっています。m5.2xlargeではsf1のスケールの幾つかでOpenJDK 8よりも遅くなっているケースが見られます。
インスタンスタイプごとの比較ではc5.4xlargeが最も速いことに変わりはありませんが、それぞれの速度差はJDK 8を使ったときほどは開いてなさそうです。とりわけ小さいデータセット(tiny)を使ったときは大きなデータ(sf1)を使ったときより速度差が小さくでました。a1.4xlargeがOn-demandで$9.8/day、c5.4xlargeが$16.3/dayと価格差が約40%あることを考えるとデータの大きさやワークロードのタイプを考えてコスト効率よく活用することができそうです。Graviton 2はGravitonの7倍のパフォーマンスらしいので、更に期待できそうです。
ちなみに、Amazon CorrettoもArmアーキテクチャをサポートしており、Armv8対応のDockerイメージを見つけたので、Correttoも試してみました。下記はa1.4xlargeを使ったOpenJDK 8、OpenJDK 11、Corretto 11の比較です。
Correttoの方がOpenJDK 8より速いことはもちろんですが、sf1のスケールではOpenJDK 11よりも比較的よいパフォーマンスを出しています。
まとめ
PrestoはJavaアプリケーションのため、Armアーキテクチャで動かすためにそれほど多くのことは必要ないことがわかりました。今回は簡単のため単一インスタンスでdocker-composeを使いましたが、プロダクション環境では複数インスタンスで分散実行させるため、また違ったパフォーマンス特性になるはずです。実際に利用される場合にはそれぞれのワークロードと環境で改めて試験してみてください。
下記のIssueをコミュニティにあげましたので、公式にArmアーキテクチャがPrestoコミュニティでサポートされるよう議論していきたいと思います。
今回使用したDockerイメージはDocker Hubにあげておきましたので、パブリックに利用可能です。ご興味あれば試してみてください。
# OpenJDK 11を使ったArmv8用イメージ
$ docker pull lewuathe/presto-coordinator:327-SNAPSHOT-aarch64
$ docker pull lewuathe/presto-worker:327-SNAPSHOT-aarch64
# Corretto 11を使ったArmv8用イメージ
$ docker pull lewuathe/presto-coordinator:327-SNAPSHOT-corretto
$ docker pull lewuathe/presto-worker:327-SNAPSHOT-corretto
画像