Preemptible InstanceをWebサーバーとして利用する際の方法とTips

Posted by

こんにちは!グリフォンでインフラやってる徳田です。

本記事は CyberAgent Developers Advent Calendar 2017 の 24 日目の記事になります。


グリフォンではGCPのPreemptible InstanceをWebサーバーとして活用しているのですが、それについての利用方法やTipsなどを紹介しようと思います。

Preemptible Instanceとは

Preemptible Instanceは以下の特徴があります。

  • 通常VMより低価格(およそ8割引き!!)で作成が可能
  • GCP側の都合でシャットダウン
  • 24時間実行後、確実にシャットダウン
  • 常に利用できるわけではない
  • SLA対象外

お値段大事。8割引きには惹かれちゃいますよね〜。

プリエンプト処理

Preemptible Instanceがプリエンプトされる際は以下の流れで処理が行われます。

  1. インスタンスにAPCI G2ソフトオフを送信
  2. 30秒後に停止しない場合、インスタンスにAPCI G3メカニカルオフを送信
  3. インスタンスをTERMINATED状態へ移行

終了予告を受け取ってから30秒間終了処理をする猶予がある、ということですね。

 グリフォンでのPreemptible Instanceの利用場面

グリフォンで提供しているゲームは、1日の決まった時刻にトラフィックが急激に増大します。

これに対し、以前までは最大のトラフィックに合わせたWebサーバーを常に起動していたのですが、このクラウドのご時世の中それはもったいない!ということで定刻でWebサーバーをスケールアウトするように置き替えました。

スケールアウトする必要がある回数/期間は1日に2回、5時間程度です。そこで、スケールアウトする分のサーバーをPreemptible Instanceとして起動することで更にコストカットを狙えるのではないか、ということでPreemptible Instance利用の検討を行った次第でした。

Webサーバーとして利用するにあたって

Preemptible InstanceをWebサーバーとして使うには突如プリエンプトされるので、その際には新規のリクエストを受け入れないようにした上で、処理中のリクエストにレスポンスを全て返してからシャットダウンされる必要があります。そのためにプリエンプト処理が始まってから30秒の間に

  • ヘルスチェックリクエストの遮断
  • リクエストが処理されるまで待機

を行う必要があります。

これを行うためにGCEの1つの機能であるシャットダウンスクリプトを利用します。

シャットダウンスクリプト

シャットダウンスクリプトは名前の通り、シャットダウンされる際に実行されるスクリプトです。

実装としてはCentos7ではsystemdのoneshot serviceとして実現されています。

[Unit]
Description=Google Compute Engine Shutdown Scripts
After=local-fs.target network-online.target network.target rsyslog.service systemd-resolved.service
After=google-instance-setup.service google-network-setup.serviceWants=local-fs.target network-online.target network.target
[Service]
ExecStart=/bin/trueExecStop=/usr/bin/google_metadata_script_runner --script-type shutdown
Type=oneshot
RemainAfterExit=true
TimeoutStopSec=0
[Install]
WantedBy=multi-user.target

設定方法はインスタンスを作成する際にメタデータにキーを「shutdown-script」、値にスクリプトを記述することで設定できます。

終了処理の実装について

Webサーバーのプリエンプト時の終了処理は上記にもあげた通り

  • ヘルスチェックリクエストの遮断
  • リクエストが処理されるまで待機

を行う必要があります。が!!1個注意点。

サービスの終了順序の指定

シャットダウン時にはsystemdが起動しているサービスを停止させていくわけですが、起動順序の指定がないサービスは適宜起動・停止が行われます。

自分はここでハマってしまったのですが、シャットダウンスクリプトが実行されている最中はフロントサーバーやアプリケーションサーバーは起動していてほしいのに先に終了してしまう、という事案にあたりました。

コレに気づかず検証してたら大量の502 Bad Gatewayが出て随分と悩んでいました・・・w

これの対処として、該当のサービスのUnit設定ファイルにBeforeで「google-shutdown-scripts.service」を指定することでした。なぜBeforeなのかというと、終了時は起動順序の逆順で処理が実行されるためです。起動時はgoogle-shutdown-scripts.serviceより前に起動、終了時はgoogle-shutdown-scripts.serviceの後に終了、という順序になります。

設定例としてnginxを例とすると「/etc/systemd/system/nginx.service.d/override.conf」というようなファイル(ファイル名はなんでも良い)に下記の内容のファイルを配置します。

[Unit]
Before=google-shutdown-scripts.service

これによりgoogle-shutdown-scripts.serviceが停止してからnginx.serviceが停止されます。

シャットダウンスクリプトが完了する後に終了してほしいサービスに対して上記のような設定を入れることで正しい順序で終了処理が実行されます。

 

では、シャットダウンスクリプトの中身を見ていきます。

ヘルスチェックリクエストの遮断

プリエンプトされているインスタンスに新規のリクエストが入ってこないように、ヘルスチェックを失敗させLBから切り離されるようにします。

今回はヘルスチェックのリクエストをリジェクトさせる設定をします。

これまたCentOS7でわざわざfirewalldを使って設定していますw

firewall-cmd --zone=trusted --add-rich-rule='
    rule family="ipv4"
    source address="35.191.0.0/16"
    port port="80"
    protocol="tcp"
    reject type="tcp-reset"'
firewall-cmd --zone=trusted --add-rich-rule='
    rule family="ipv4"
    source address="130.211.0.0/22"
    port port="80"
    protocol="tcp"
    reject type="tcp-reset"'

iptablesでは以下のような設定を行います

iptables -I INPUT 1 -m state --state NEW \
    -s 35.191.0.0/16 \
    -p tcp --destination-port 80 \
    -j REJECT --reject-with tcp-reset
iptables -I INPUT 1 -m state --state NEW \
    -s 130.211.0.0/22 \
    -p tcp --destination-port 80 \
    -j REJECT --reject-with tcp-reset

要はヘルスチェックを行うIPから来る新規接続のパケットをtcp-resetとしてリジェクトする、という内容です。

これをシャットダウンスクリプトで実行し、ヘルスチェックがFailするまで待つことで新規のリクエストが入ってこなくなるようになります。

リクエストが処理されるまで待機

後は処理中のリクエストが処理されるまで待つだけです。

現状グリフォンではただただ可能な限りsleepするという処置をとっています。設定値としては15秒です。

リクエストの処理が15秒もかかっていたらそれ以前の問題ですし、フロントにおいてあるnginxではProxy Timeoutを10秒に設定しているためsleepする時間は15秒で十分だろうというところです。

具体的なシャットダウンスクリプト

firewall-cmd --zone=trusted --add-rich-rule='
    rule family="ipv4"
    source address="35.191.0.0/16"
    port port="80"
    protocol="tcp"
    reject type="tcp-reset"'
firewall-cmd --zone=trusted --add-rich-rule='
    rule family="ipv4"
    source address="130.211.0.0/22"
    port port="80"
    protocol="tcp"
    reject type="tcp-reset"'

sleep 15

このような感じになるかと思います。

必要であれば最後に各種サービスをGracefulに停止するなど、適宜処理を追加する感じになるかと思います。

最後に

Preemptible InstanceをWebサーバーとしての利用する際の方法、Tipsなどを紹介しました。

WebサーバーでこのPreemptible Instanceを使うのは結構攻めてる感じがありますがw
おかげで結構なコストカットをすることができました!

また、別の機会にGCPの他の要素などについても紹介できればなと思っています!