afnf.net

Spring Bootでお手軽ゼロダウンタイムデプロイ

Spring Boot nginx itamae 2015/12/15 00:06

この記事は Java EE Advent Calendar 2015 - Qiita の15日目のエントリーです。

昨日は@den2snさんで、Javaバッチの実行環境 EEなのかSEなのか でした。 明日は@glory_ofさんです。

ゼロダウンタイムデプロイとは

読んで字のごとく、サービスの停止時間無しにアプリケーションを配備する事ですね。

無停止リリースとか無停止デプロイ、(ちょっとニュアンス違うけど)ローリングアップデートなどという言われ方もします。Erlang界隈ではホットデプロイという呼ばれ方をするようです。

実現方法は色々あります。自分の知っている限りで、以下に書き出してみました。

PaaSを利用する方法

Google App Engine や AWS Elastic Beanstalk にはアプリのバージョン管理機能があるので、これを利用すれば安全にアプリを入れ替えられますね。

J2EEコンテナを利用する方法

  • Tomcat 7 以降の Parallel Deployment
  • JBoss
  • WebLogic
  • WebSphere(Rollout Updateという名称)

TomcatのParallel Deploymentはメモリリーク等が心配なんですが、どうなんでしょうか。その辺りが問題なければ、良い解法だと思います。

Dockerコンテナクラスタリングを利用する方法

CoreOS + etcd + fleet という組み合わせで、ゼロダウンタイムデプロイが実現できるそうです。

クラスタリングのおまけとしての無停止リリース、って感じですが。Docker勉強しなきゃなぁ・・・

L7スイッチを利用する方法

  • Apache + mod_proxy_balancer
  • nginx
  • HAProxy
  • Amazon ELB(Elastic Load Balancing)
  • BIG-IPなどのハードウェアロードバランサ

この辺はありがちですね。ハードウェアロードバランサの複数台構成は鉄板。予算が許せば。

その他

  • SO_REUSEPORTを利用する方法
    • Linux kernel 3.9以降で利用可能
    • 同一ポートでListenできるようになるため、これを使って系を切り替える事ができるようです
    • nginx 1.9.1でサポートされました
  • AWS Elastic IPの再マップを利用する方法
  • DNSラウンドロビンを利用する方法
    • キャッシュの問題があるため、高頻度のリリースには向きません
  • LVS(Linux Virtual Server)を利用する方法
    • 最近はメンテナンスされていないようです

そもそも不要なケース

PHP等のインタプリタ言語では、ファイルの直接入れ替えやシンボリックリンクの差し替えで問題ないことが多いと思います。

お手軽にゼロダウンタイムデプロイしたい!

長い前置きが終わって、やっと本題w

それなりの規模のシステムなら、ELB使っとけば良いんじゃないですかね←投げやり

今回はシングルホスト向けに、無停止リリースをシンプルに実現する方法を紹介します。

結論から言うと古典的なコールドスタンバイ方式なのですが、 Spring Boot 1.3.0とnginxのロードバランシング機能を使うことで、かなりすっきりした構成になります。

構成

  • CentOS 6.7 (x86_64)
  • OpenJDK 1.8.0_65 (x86_64)
  • nginx 1.8.0 (nginx.orgのrpmを利用)
  • Spring Boot 1.3.0

インストール手順については、githubに上げたitamaeレシピを利用してください。いやしかしitamaeは最高ですね。Chefは見事に挫折しましたが、itamaeはすぐに馴染めました。

nginx設定

nginxの設定は普通です。

upstream backend {
  server 127.0.0.1:8080;
  server 127.0.0.1:8081 backup;
}

backupを指定することで、8080ポートがダウンしている場合だけ、8081ポートにproxyされるようになります。以降、nginxの設定は変更しません。リリース時もこのままです。

Spring Boot 設定

レシピにもあるようにspring-boot:repackageゴールで作成したExecutable JAR(下記ではzdtd-808x.jar)を、/etc/init.dにシンボリックリンクとして設置します。

ln -sf /var/zdtd/zdtd-8080.jar /etc/init.d/zdtd-8080
ln -sf /var/zdtd/zdtd-8081.jar /etc/init.d/zdtd-8081

これはSpring Boot 1.3.0で有効になった新機能で、もはや起動スクリプトを準備する必要がありません。

Listenするポート番号を変更するため、/var/zdtd/zdtd-8080.confを準備します。

PID_FOLDER=/var/zdtd/run
LOG_FOLDER=/var/zdtd/log
JAVA_OPTS="-Xmx64M -Djava.security.egd=file:///dev/./urandom -Dserver.port=8080"

/var/zdtd/zdtd-8081.conf-Dserver.port=8081となっています。

たったこれだけで、同一アプリが別ポートで起動します。素敵!

ちなみに、PIDファイル名やログファイル名は、リンク先のJarファイル名から推測されるので、特別な事をしなくても衝突しません。素敵!

[root@centos6]# ls /var/zdtd/log/
zdtd-8080.log  zdtd-8081.log

平常時は8080側だけ起動しておき、8081側は停止しておくこととします。

デプロイ手順

ようやくデプロイ手順まで来ました。

  1. /var/zdtd/zdtd-8081.jarを新バージョンに変更
  2. /etc/init.d/zdtd-8081 startで起動
    • この段階では、8081ポートにリクエストは流れません
  3. /etc/init.d/zdtd-8080 stopで停止
    • 8081ポートにリクエストが流れるようになります
  4. 新バージョンの動作確認を実施
    • 問題が発覚して切り戻したい場合は、8080をstartするだけです
  5. /var/zdtd/zdtd-8080.jarを新バージョンに変更
  6. /etc/init.d/zdtd-8080 startで起動
    • 8080が起動すると、8081へのリクエストは止まります
  7. /etc/init.d/zdtd-8081 stopで停止
    • 8080だけが起動している状態に戻りました

なおSpring Bootでは、アプリの動作中にJarを置き換えてはいけません。静的ファイル等は随時Jarから取得されるため、エラーになってしまいます。

リリースの無人化

githubのWebhookなどを利用すれば、上記手順は無人化できます。

1の手順をdummy_update.sh、2~7の手順をdeploy.shとしてシェルスクリプト化してみました。

deploy.shの実行結果は以下のようになります。

[root@centos6 ~]# sudo -u zdtd /var/zdtd/deploy.sh
Started [12039]
starting 8081, sleeping 30sec...
Stopped [11856]
stopping 8080, sleeping 10sec...
Started [12114]
starting 8080, sleeping 30sec...
Stopped [12039]
finished.

起動後に30秒のウェイトを入れていますが、アプリによって調整する必要がありそうです。

ほんとにゼロダウンタイムなの?

・・・はい、多分w

JMeterで100req/sec程度の負荷をかけながらデプロイを行い、全てのリクエストが成功する事を確認しました。ただし、8081ポートへの切り替え時に、若干遅延が増大するようです(200ms程度)。

graceful shutdownでない点に注意

/etc/init.d/zdtd-8080 stopを行うと、処理中のリクエストはエラーになってしまいます。どうやら、現状のSpring Bootはgraceful shutdownではないようです。

ドキュメントにはgracefully shutdownという記載があるので、バグなのかもしれません。

nginxがリトライしてくれるのでエラーにはなりませんが、アプリによっては二重リクエストが問題なるかもしれません。

まとめ

ちょっと駆け足気味でしたが、Spring Bootとnginxによるお手軽なゼロダウンタイムデプロイ手法を紹介しました。

何かのお役に立てれば幸いです。

Spring Boot nginx itamae 2015/12/15 00:06
comments (0)

blog-java2 engine (build:2017-09-13 21:46 JST)