Auto Scaling GroupsとDistributed SQL Engine

この記事は[Amazon Web Service Advent Calendar 25日目](https://qiita.com/advent-calendar/2017/aws)の記事です。

# Auto Scaling Groupsとは

Auto Scaling GroupsはAmazon EC2上で動く機能のひとつで、インスタンスの論理的なグループを定義して、そのグループの総インスタンス数を柔軟に変えることを可能にします。Auto Scaling Groupに対して必要なキャパシティを設定するだけで、Auto Scaling Groupが自動でインスタンスの作成、Bootstrap, Configuration, アプリケーションサービスの立ち上げまでを行ってくれます。この機能を使えばWeb Trafficのピークに合わせて必要な台数を自動で立ち上げていくといったことができるようになります。またコスト削減のため余剰リソースを自動で縮退させていくということも可能になります。

この記事ではTreasure Data内で利用している分散クエリエンジンPrestoを実際にAuto Scaling Groupsを使って運用してみてわかったtipsやハマりどころなどを紹介します。

# Auto Scaling Groupsの使い方

詳細なStep by Stepはここでは割愛させていただきますが、Auto Scaling Groupを使うにあたって知っておかなければいけない用語と知識を簡単に紹介します。

* **Launch Configuration** : Auto Scaling Groupから立ち上げるEC2のインスタンスタイプやAMI、Security Groupsなどを設定する。
* **Desired Capacity** : そのAuto Scaling Groupで満たすべきインスタンス数。Auto Scaling Groupは自動でこの値を満たすようにインスタンスを増減させる。
* **Cooldowns** : Auto Scaling Groupで前回発生したイベントから次のイベントを処理するまでの間の時間。Cooldownsを用いると立て続けにイベントが発生した場合でも(e.g. アクセス数のスパイクが一定時間続く)、その度に次々とインスタンスを追加していくことを防ぐことができる。
* **Lifecycle Hooks** : Auto Scaling Groupから立ち上げられたインスタンスの状態をトリガーにしてアプリケーション側に処理を委譲する機能。後述するCodeDeployとの連携やGraceful Shutdownに用いる。

ここに加えてアプリケーションのデプロイには[AWS CodeDeploy](http://docs.aws.amazon.com/codedeploy/latest/userguide/welcome.html)を用いました。なぜならLaunch Configurationのみだとアプリケーションのバージョン管理やパッケージの作成、保存などが難しかったからです。なおAuto Scaling GroupsとCodeDeployはCodeDeployのdeployment group作成時に簡単に連携させることができます。

```
$ aws deploy create-deployment-group \
  --application-name SimpleDemoApp \
  --auto-scaling-groups CodeDeployDemo-AS-Group \
  --deployment-group-name SimpleDemoDG \
  --deployment-config-name CodeDeployDefault.OneAtATime \
  --service-role-arn service-role-arn
```

詳細は[Tutorialをご覧ください](http://docs.aws.amazon.com/codedeploy/latest/userguide/tutorials-auto-scaling-group-create-auto-scaling-group.html).

# Prestoとは

PrestoはFacebookで開発された分散SQLエンジンです。現在はオープンソースソフトウェアとなっており、[Uber](https://eng.uber.com/presto/)や[Netflix](https://medium.com/netflix-techblog/using-presto-in-our-big-data-platform-on-aws-938035909fd4)など巨大なサービスを支えるデータ分析のためのソフトウェアです。

<img width="1029" alt="Screen Shot 2017-12-02 at 14.56.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/7129/cea43862-96f4-15b6-0102-f655f72c571d.png">
[Presto](https://prestodb.io/)

Prestoはデータを基本的にインメモリで処理するため同種類のHiveなどのソフトウェアとくらべて高速にデータを処理することができます。そのためHadoopなどと比べて比較的小中規模のデータに対してインタラクティブな分析を行いたいときに向いています。ただ0.186から[Spill to Disk](https://prestodb.io/docs/current/admin/spill.html)の機能が入りましたのでより大きなデータでもクエリを完了させることが今後できるようになるかもしれません。

ひとつのPrestoクラスタにはmaster nodeとしての**Coordinator**とslave nodeとしての**Worker**と呼ばれるコンポーネントがあります。クライアントは実行したいクエリをまずCoordinatorに投げます。Coordinatorは専用のHTTPのエンドポイントを提供しているので必要であればセッションパラメタなどとともにクエリを投げます。Coordinatorは受け取ったクエリからクエリの実行計画、最適化を行い各WorkerにクエリのFragmentとしての**Task**を配布します。各Workerは受け取ったTaskを並列に実行し、出力を他のWorkerに投げるなり、どこかのストレージ書くなりといったことをします。

詳細は[Prestoのコンセプト](https://prestodb.io/docs/current/overview/concepts.html)参照のこと

![Presto Overview](https://prestodb.io/static/presto-overview.png)

またPrestoはデータソースとして利用できるストレージへのアクセスを**Connector**と呼ばれるプラグイン経由で行います。PrestoではこのConnectorと分散処理のレイヤーがきれいに分かれているため既に持っている既存のストレージのデータに対してPrestoを使って分析することができます。既にコミュニティから[20以上のプラグイン](https://prestodb.io/docs/current/connector.html)が提供されているので、既にここに必要なものがあれば自前で作る必要もありません。



# Prestoクラスタの作成手順

このAuto Scaling Groupsを用いてPrestoクラスタを作る手順は下記のようになります。

1. Auto Scaling Groupを作成する
  - CoordinatorとWorker用のgroupは別で作成する。なぜならCoordinatorは水平スケーリングしないため
  - CoordinatorのAuto Scaling GroupのMin Capacityを1にしておくと誤って削除することがない
  - クラスタ内の通信レイテンシを抑えるためひとつのクラスタで一つの[Placement Group](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/placement-groups.html)を使うように指定しておく
1. CodeDeployのdeployment groupを作成する
  - Auto Scaling Groupと1対1で対応するように作成する
  - つまりCoordinator用とWorker用のdeployment groupそれぞれ作る
1. CodeDeploy用のパッケージを作成する。詳細は[こちら参照](http://docs.aws.amazon.com/codedeploy/latest/userguide/reference-appspec-file.html)
  - Prestoの場合は必要なJARと`launcher.py`スクリプトをキックするhook scriptが最低必要
  - 後述するようにGraceful Shutdownを実現するのであれば、lifecycle hookをトリガーするscriptなども必要
1. インスタンス数の調整
  - `aws autoscaling set-desired-capacity`で行うことができる。CodeDeployは自動で最後の成功したrevisionをdeployしてくれるのでパッケージdeployのあとでこれをやっても構わない
1. CodeDeploy経由でパッケージをdeployする
  - Coordinator用のdeployment groupとWorker用のdeployment groupからそれぞれパッケージをdeployする (e.g. `aws deploy create-deployment`)
 
これで一つのPrestoクラスタが完成します。各手順の詳細はAuto Scaling Groupsや[CodeDeploy](http://docs.aws.amazon.com/codedeploy/latest/userguide/welcome.html)のドキュメントを参照してください。

ここからはPrestoクラスタを運用する上でAuto Scaling Groupsを使ったときのPros & Consを述べていきたいと思います。

# クラスタの作成/拡大が速い

何よりこれが最も大きな改善でした。後述するようにある特定のケースでは上手くいかないこともあるのですが、CodeDeployのdeployment groupの定義も、Auto Scaling GroupsのLaunch Configurationもテンプレート化できるのでクラスタの設定を簡単に複製させていくことができます。またクラスタに対するロードが上がったときでも

1. Desired Capacityを設定する
1. 待つ

だけで自動でクラスタのCapacityを大きくすることができます。

# 大量インスタンスの追加に失敗する

CodeDeployには[**一度に10個までのdeploymentしか実行できない**](http://docs.aws.amazon.com/codedeploy/latest/userguide/limits.html)という制約があります。このため一度に何十台も追加しようとする場合 (とりわけクラスタのbootstrap時に発生しますが） そういった場合に10個を超えたインスタンスはCodeDeployがdeployしてheatbeatを送ってくれないため、timeoutしてインスタンス立ち上げに失敗してしまう可能性が高まります。

<img width="750" alt="Screen Shot 2017-12-05 at 22.43.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/7129/a6d33c37-8e2d-7467-a9c3-ae5c84435793.png">

この場合にはとれる方法は、
- 一度に10台を目安に追加する
- 各インスタンスのdeployを速くする
などが考えられます。一度に何十台も必要なのは多くの場合クラスタの初期作成時だけですが、何度も失敗するのはとても面倒でした。

# Graceful ShutdownとLifecyle Hook

PrestoはOLAPシステムなので、その上で走るクエリも数秒で終わるものもあれば、数分から数時間かかるものもあります。またPrestoはクエリの実行に関してFault Tolerantでないので、1つのWorkerノードが落ちていたりすると失敗してしまいます。数時間かけて走らせたクエリがWorkerのShutdownで失敗しては悲しいのでGraceful ShutdownはPrestoのようなシステムでは重要になってきます。

Prestoでの通常のGraceful Shutdownのプロセスは下記のようになります。

1. Workerの`/v1/state`にDELETEを送る
2. Workerは`SHUTTING_DOWN`状態になる。このノードでタスクが動いている間この状態が維持される
3. `SHUTTING_DOWN`が終わるとプロセスは停止する

これで晴れてインスタンスと停止することができます。Auto Scaling Groupsでこれを実現する場合にはどうすればよいでしょうか。

Auto Scaling Groupsには[Lifecycle Hook](http://docs.aws.amazon.com/autoscaling/latest/userguide/lifecycle-hooks.html)という仕組みがあり、インスタンス立ち上げと停止時にあるアクションを待たせることができます。

![lifecycle hook](http://docs.aws.amazon.com/autoscaling/latest/userguide/images/lifecycle_hooks.png)

From [Auto Scaling Lifecycle Hooks](http://docs.aws.amazon.com/autoscaling/latest/userguide/lifecycle-hooks.html)

Lifecycle Hookの登録を行っておくと、立ち上げ時と停止時にそれぞれ

* `Pending:Wait`
* `Terminating:Wait`

という状態でインスタンスのbootstrapを待ちます。この間に必要な処理を行い、終わったら`complete-lifecycle-action`コマンドを送り状態を

* `Pending:Proceed`
* `Terminating:Proceed`

に進ませることができます。これをGraceful Shutdownに利用します。

1. `put-lifecycle-hook`でGraceful Shutdown用のLifecycle Hookを登録します。Shutdown用なので`--lifecycle-transition`に`autoscaling:EC2_INSTANCE_TERMINATING`を指定します。

```
$ aws autoscaling put-lifecycle-hook \
    --lifecycle-hook-name presto-graceful-shutdown \
    --auto-scaling-group-name my-asg \
    --lifecycle-transition autoscaling:EC2_INSTANCE_TERMINATING
```

2.Auto Scaling Groups経由でインスタンスを停止させます。(e.g. Desired Capacityを減らす、`terminate-instance-in-auto-scaling-group`コマンドを使う等)

3.先述のように`/v1/state`に対して`DELETE`を送ります。タスクがある間Workerは待ち続けますが、この間、同時に`record-lifecycle-action-heartbeat`を送り続けます。これはAuto Scaling Groupsに「Workerがまだ生きているので`Terminating:Wait`状態のまま待っといて」ということを指示します。

4.`SHUTTIND_DOWN`が完了してWorkerプロセスが死んだことを確認したら、`complete-lifecycle-action`で「`Terminating:Proceed`に進んでいいよ」ということを指示します。これでAuto Scaling Groupsはこのインスタンス停止のためのプロセスを続行できるのです。

問題はこの一連のプロセスを実行する機能はPrestoにはないので、自前で作る必要があります。簡単なのはcronでWorkerプロセスの状態を見て、生きていればheartbeatを自インスタンスIDとともに送り続け、死んだらcompleteを送るというのがよいかと思います。

# Capacity Metrics

Auto Scaling Groupsを使う最大のメリットはあるメトリクスの増減をトリガーにして自動でクラスタのキャパシティを調整できることです。シンプルなWebアプリケーションであれば、アクセス数などから必要キャパシティの見積もりを行うことができます。Prestoでは何をみればよいでしょうか。最後に幾つかPrestoクラスタのキャパシティを計るのに有用なメトリクスを考えてみたいと思います。

## 同時実行クエリ数

クエリが増えればそれだけ必要な台数も増えます。ただPrestoのようなOLAPシステムに流れるクエリの負荷というのはクエリごとにかなり大きな差があります。数百GBのテーブルフルスキャンしてJOINしたクエリと、小さなテーブルの1つのカラムのSELECTで終わるクエリでは全くクラスタに与える負荷は違います。もう少しクエリ毎の負荷を考えたメトリクスが必要そうです。

## メモリ

クラスタ全体で利用できるメモリはWorkerの台数によって決まります。Prestoはオンメモリの分散SQLエンジンなので、すべてのクエリは基本的にメモリリソースを使います。Prestoには3種類のメモリプールがあり、各クエリはここから必要なメモリを割り当ててもらいます。

* General Pool: まずすべてのクエリがメモリを割り当てられるのがGeneral Memory Pool
* Reserved Pool: General Poolを使い切った場合に最もメモリを使っているクエリをReserved Poolに移して実行
* System Pool: Prestoのクエリエンジンが使うMemory Pool

そのため概ねGeneral Pool + Reserved PoolがPresto上で動くクエリのメモリ消費量となります。

## Driver

DriverはPrestoでの最小の並列実行の単位でJVMのスレッド上で動きます。Driverはある入力を得て幾つかもOperatorを適用します。その出力はTaskに集められまた他のTaskのの入力となります。Driverは各クエリが実行するOperatorを集めたものなので、Driverが多いことはつまりCPUを多く使うクエリともいえます。

分散SQLの実行を大きく支えるリソースはメモリとCPUなので最終的には

* General Pool + Rerserved Poolの使用量
* Driver数

をキャパシティを計るターゲットとして[Target Tracking](http://docs.aws.amazon.com/autoscaling/latest/userguide/as-scaling-target-tracking.html)から取れるようになると、本当のAuto Scalingが実現できることになります。

# まとめ

分散SQLエンジンのCapacity PlanningやDeploymentはしばしば手間のかかる仕事となるので、せっかくAWS上でクラスタを運用しているのであれば効果の高いリソース（CodeDeployやAuto Scaling Groups）は今後も積極的に使っていきたいと思いました。

Treasure Dataではこのように分散SQLエンジンの足回りやCloudとのインテグレーションを支えてくれるエンジニアを[絶賛募集](https://www.treasuredata.com/company/careers/jobs/?team=Engineering)しております！

# Reference
* [Troubleshoot Auto Scaling Issues](http://docs.aws.amazon.com/codedeploy/latest/userguide/troubleshooting-auto-scaling.html)
* [AWS CodeDeploy Tutorials](http://docs.aws.amazon.com//codedeploy/latest/userguide/tutorials.html)
* [Target Tracking](http://docs.aws.amazon.com/autoscaling/latest/userguide/as-scaling-target-tracking.html)
* [PrestoDB](http://prestodb.io/)
* [How To: Presto Tuning](https://qubole.zendesk.com/hc/en-us/articles/210266303-How-To-Presto-Tuning)
* [Treasure Data Jobs](https://www.treasuredata.com/company/careers/jobs/?team=Engineering)