ホットデプロイを実現する2つの方法


Webアプリケーションのデプロイが始まってからデプロイが終わるまでの数分間、ユーザーはそのWebアプリを使い続けることはできるのでしょうか?

この記事では、デプロイ中もユーザーがWebアプリを使い続けられるようにする技術である「ホットデプロイ」について解説します。

ホットデプロイとはなにか

一番簡単なデプロイ方法を考えてみましょう。

  1. Webアプリをkillする
  2. Webアプリのソースコードを差し替える1
  3. Webアプリを起動する

このようにすれば、Webアプリのデプロイ自体はできます。しかし、この方法だと、1でアプリをkillするときに処理中のリクエストが途中で終了してしまいますし、1から3までの間はリクエストに応答することができません。

趣味のサービスだったり、メンテナンスを頻繁に設けても良いサービスであれば、これでも問題はありせん。しかし、24時間常にユーザーが利用するサービスでは、このようなデプロイ方法はユーザーにとって不便です2

この問題を解決するために、「デプロイ中もサービスを停止しないデプロイ方法」を確立する必要があります。これをホットデプロイといいます。

ホットデプロイを実現する2つの方法

ホットデプロイを実現する方法は大きく分けて2つあります。

ロードバランサを用いる方法

システムがロードバランサを使っている場合、以下の手順でホットデプロイを実現できます。

  1. デプロイ対象のWebサーバを適当に選ぶ(Aとする)
  2. ロードバランサの設定を変えて、Aにはリクエストが行かないようにする
  3. Aを停止して、ソースコードを差し替えてアプリケーションを再起動する
  4. ロードバランサの設定を変えて、Aにもリクエストが行くようにする
  5. すべてのWebサーバがデプロイ済みになるまで1~4を繰り返す

3でサーバを停止していますが、リクエストはすべて他のサーバに流れているため、ユーザーには影響が出ません。
これによってホットデプロイが実現されます。

ロードバランサを用いたホットデプロイのやり方は他にもあります3が、デプロイ対象のサーバをクラスタから外してデプロイし、デプロイが終わったらクラスタに入れるという点では共通しています。

プロセスのforkを利用する方法

RackのWebアプリケーションサーバであるUnicornを例に説明します。とはいえ大抵のWebアプリケーションサーバは似たような機能を持っていると思います(多分)456

Unicornでは、ホットデプロイは以下の手順で実現できます。

  1. アプリケーションのソースコードを差し替える
  2. UnicornのmasterプロセスにUSR2シグナルを送る7
  3. UnicornのmasterプロセスにQUITシグナルを送る8

この操作をしたときのUnicornの挙動について詳しく説明します。

まず、稼働中のUnicornのプロセスツリーは以下のようになっています。つまり、一つのmasterに複数のworkerが子としてぶら下がっている状態です。

$ pstree
\- 22000 unicorn master
  \- 22001 unicorn worker
  \- 22002 unicorn worker

これらのプロセスは1つのソケットを共有しており9、workerはソケットをlistenしています(masterはlistenしない)。

ここで、masterプロセスにUSR2シグナルを送るとmasterがforkし、新しいmasterとworkerが子プロセスとして生成されます。

$ kill -s USR2 22000 # masterにUSR2シグナルを送信
$ pstree
\- 22000 unicorn master (old)
  \- 22001 unicorn worker (old)
  \- 22002 unicorn worker (old)
  \- 23000 unicorn master
    \- 23001 unicorn worker
    \- 23002 unicorn worker

これら6つのプロセスは1つのソケットを共有しており、4つのworkerがソケットをlistenしています10
新しいmaster/workerは、更新されたあとのソースコードを基に起動されます。したがってこの状態では、更新前と更新後の両方のworkerがソケットをlistenしていることになります11

そして、古い方のmasterにQUITシグナルを送ると、古いmasterとworkerが停止します。

$ kill -s QUIT 22000 # 古いmasterのPID
$ pstree
\- 23000 unicorn master # => 新しいmasterとworkerだけが残る
  \- 23001 unicorn worker
  \- 23002 unicorn worker

古いworkerが停止するときは、自分が処理中のすべてのリクエストの処理が終わってから停止します。また、古いworkerが全部停止しても新しいworkerが生きているので、リクエストは途切れることなく処理されます。これによって、ホットデプロイが実現されます。

参考:

まとめ

ホットデプロイの手法について、ロードバランサを使う方法とプロセスのforkを利用する方法を解説しました。
細かく見るともっと多くの手法があるようですが、いずれも本記事で解説したいずれかの方法を応用したものです(知っている範囲では)12
これら2つの手法を理解しておけば、他のホットデプロイ手法についても容易に理解できると思います。

落ち穂拾い

主題から外れるためこの記事では取り上げませんが、どの手法を取るにせよ、「デプロイ中は新旧2種類のアプリケーションが同時に稼働する」という状況は避けられません。したがって、ホットデプロイをしたいのであれば、新しいサーバーが後方互換性を保つ必要があります。


  1. あるいは、コンパイル言語でWeb開発をしている場合はそのバイナリ。 

  2. 当然、停止期間は利益も得られません 

  3. この記事で解説した方法は「ローリングデプロイ」です。その他の手法についてはこの記事に記載があります。  

  4. pumaにも似たような機能がありそう。 https://github.com/puma/puma/blob/master/docs/restart.md 

  5. Webアプリケーションサーバにこの機能を持たせるのではなく、独立したスーパーバイザを使うケースもある模様(というかその方が一般的?)。やってることは多分同じ。 https://github.com/lestrrat-go/server-starter 

  6. 説明のためにWINCHについては触れていません。詳しく知りたい方は https://bogomips.org/unicorn/SIGNALS.html を読んでください。 

  7. kill -s USR2 $(cat tmp/unicorn.pid) https://bogomips.org/unicorn/SIGNALS.html 

  8. kill -s QUIT $(cat tmp/unicorn.pid.oldbin) https://bogomips.org/unicorn/SIGNALS.html 

  9. 起動時にまずmasterがソケットをオープンし、その後forkしてworkerを起動しているのでソケットが共有される模様。 

  10. forkなので親子間でファイルディスクリプタが共有される。 

  11. つまり、ユーザーがリクエストを送ったときに、古いレスポンスと新しいレスポンスのどちらが返ってくるかはわからない。 

  12. もしどちらとも異なる手法があったら教えて下さい。