はじめに
はじめまして。サムザップでSREに所属しています山﨑です。
今回は、「戦国炎舞 -KIZNA-」で行った時限オートスケーリングとスポットインスタンスによるリソース最適化の話をしたいと思います。
背景
自分の担当している「戦国炎舞 -KIZNA-」では、2019年2月にオンプレミスからパブリッククラウドのAWSに移設をしました。
このあたりの話を「あるSREチームの挑戦 運用6年目の大規模ゲームをAWS移設後に安定運用するための技術と今後の展望」と題して弊社SRE チームマネージャーの石原、テックリードの吉岡が発表しています。ご興味のある方はご覧ください。
AWSへ移設してからも使用リソースを最適利用できるような改修をいくつか行なってきました。今回はその中でも時限オートスケーリングとスポットインスタンスについて記載します。
時限オートスケーリング
「戦国炎舞 -KIZNA-」は1日に3回決まった時間に、30分間最大20人対20人で対戦する「合戦」があります。
「合戦」外の時間は比較的アクセスが落ち着きますが、「合戦」中はリクエストが集中します。
このため安定してリクエストを処理するには、負荷に合わせてwebサーバーを増減させることが重要となります。
AWS EC2 オートスケーリングには「スケジュールされたアクション」や「スケーリングポリシー」といった機能があり、定期的であったり負荷に応じたスケールイン・スケールアウトが可能です。
しかし「戦国炎舞 -KIZNA-」では「合戦」が開始すると急激にリクエストが増えるため、スケーリングポリシーで負荷を検知してからスケールアウトを開始していては間に合いません。
また、ゲームイベントの種類によって処理やリクエスト数が異なるので、負荷が変化し必要なインスタンス数も異なってきます。
これらに対処するために、「戦国炎舞 -KIZNA-」ではAmazon CloudWatch Events、AWS Step Functions、AWS Lambdaを用いた上でオートスケーリンググループを時限で操作し、インスタンス数を制御しています。
CloudWatch Eventsは定期実行を行うために、Step FunctionsはLambdaの処理を分割して記述するために用いています。
AWS Lambdaの役割
この仕組みでのAWS Lambdaの役割を簡単に説明します。
先程述べた通り、「戦国炎舞 -KIZNA-」ではゲームイベントの種類によって負荷が異なり、必要なインスタンス数が増減します。
そのため、以下の処理をAWS Lambdaで行い稼働インスタンス数を調整しています。
- ゲームイベントの種類はマスターデータとして持っているので、マスターデータを取ってくる
- イベントの種類に応じた増設インスタンス数定義ファイルから、1.のデータを用いて増設インスタンス数を決定する
- 2.で決めた値にオートスケーリンググループのキャパシティを変更する
以下に3.の処理のサンプルコードを記載します。
#!/usr/bin/env python # -*- coding: utf-8 -*- import boto3 import datetime # Launch Specification ec2 = boto3.client('ec2') autoscaling = boto3.client('autoscaling') elbv2 = boto3.client('elbv2') def lambda_handler(event, context): minSize = int(event.get("minSize", 0)) maxSize = int(event.get("MaxSize", 0)) desiredCapacity = int(event.get("desiredCapacity", 0)) battleTime = event.get("battleTime") autoScalingGroupName = event.get("autoScalingGroupName") actions = [] # 過去のスケジュールは追加できないので必ず未来の時刻になるようにする startTime = datetime.datetime.now() + datetime.timedelta(minutes=5) endTime = datetime.datetime.now() + datetime.timedelta(minutes=battleTime) actions = [ { 'ScheduledActionName': f'{autoScalingGroupName}-out', 'StartTime': startTime, 'MinSize': minSize, 'MaxSize': maxSize, 'DesiredCapacity': desiredCapacity }, { 'ScheduledActionName': f'{autoScalingGroupName}-in', 'StartTime': endTime, 'MinSize': 0, 'MaxSize': 0, 'DesiredCapacity': 0 } ] # スケジュールの追加 autoscaling.batch_put_scheduled_update_group_action( AutoScalingGroupName=autoScalingGroupName, ScheduledUpdateGroupActions=actions )
注意する点としては、オートスケーリンググループに登録する「スケジュールされたアクション」は未来の時刻でなければならないという点です。
過去の時刻を指定してしまうとAPIを呼んでもエラーが返り処理が失敗します。
スポットインスタンス
スポットインスタンスとは、オンデマンド価格より低価で利用できるEC2インスタンスです。AWSのもつEC2インスタンスの中で未使用のEC2インスタンスが利用できます。
低価格で利用できますが、AWS側からインスタンスの返却が求められた場合に、キャパシティをEC2に戻す必要があります。
そのためいつインスタンスが中断しても問題ないよう対応する必要があります。
「戦国炎舞 -KIZNA-」では以下の2点の対応を行っています。
- 終了通知を監視し終了処理をする
- インスタンスタイプを分散する
終了通知を監視し終了処理をする
EC2の中断が発生する2分前に、AWS側から終了通知が来ます。
「戦国炎舞 -KIZNA-」ではログデータコレクタとしてFluentdが稼働しているので、終了通知を監視し、インスタンスの中断前にログのバッファをフラッシュする必要があります。
#!/bin/bash #Desired CapacityはそのままでオートスケーリンググループからEC2インスタンスを切り離す aws autoscaling detach-instances --instance-ids ${instance_id} --auto-scaling-group-name ${auto_scaling_group_name} --no-should-decrement-desired-capacity
まずインスタンスをオートスケーリンググループから切り離し、
#!/bin/bash kill -SIGUSR1 `cat /var/run/td-agent/td-agent.pid`
ログをフラッシュします。
オートスケーリンググループからの切り離しは代わりのインスタンスが立ち上がってきて欲しいので、キャパシティを減らさないよう--no-should-decrement-desired-capacity
オプションを付けて行います。
インスタンスタイプを分散する
スポットインスタンスとして利用できる空きリソースの候補を増やすことでより安定してスポットインスタンスを確保することができます。
EC2 オートスケーリンググループではインスタンスタイプを複数選択できます。
オートスケーリンググループの詳細の編集から、"購入オプションとインスタンスを組み合わせる"オプションを選択した上で、利用するインスタンスタイプを追加します。
※ ここで掲載しているインスタンスタイプは例として作成したものです
スポットインスタンスアドバイザー | AWSをみれば、先月どの程度の頻度で中断が発生したのか、オンデマンドと比較した費用削減がどの程度かを確認できます。
まとめ
「戦国炎舞 -KIZNA-」で行っているリソースの最適化の中で、時限オートスケーリングとスポットインスタンスについて話しました。
AWS Lambda、オートスケーリンググループを用いてゲームイベントにあったインスタンスの増設が行えます。
また、終了処理やインスタンスタイプの分散をすることで、スポットインスタンスを安全に利用できます。
AWS EC2を利用しているプロジェクトであれば適用できるかもしれません。