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

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

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

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

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

1. Webアプリをkillする
1. Webアプリのソースコードを差し替える[^compile-language]
1. Webアプリを起動する

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

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

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

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

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

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

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

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

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

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

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

RackのWebアプリケーションサーバである[Unicorn](https://bogomips.org/unicorn/)を例に説明します。とはいえ大抵のWebアプリケーションサーバは似たような機能を持っていると思います(多分)[^puma][^other-application-server][^unicorn-winch]。

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

1. アプリケーションのソースコードを差し替える
2. Unicornのmasterプロセスに`USR2`シグナルを送る[^usr2-signal]
3. Unicornのmasterプロセスに`QUIT`シグナルを送る[^quit-signal]

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

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

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

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19349/f57279b6-c5b9-c991-a6d3-dbbbea9007e1.png" width="400px" />

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

```shell-session
$ 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しています[^fork]。
新しいmaster/workerは、更新されたあとのソースコードを基に起動されます。したがってこの状態では、更新前と更新後の両方のworkerがソケットをlistenしていることになります[^old-and-new]。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19349/51c45eed-ea76-daa3-a186-25baf2f6e2fe.png" width="400px" />

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

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

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

参考：

- https://bogomips.org/unicorn/SIGNALS.html
- https://blog.shibayu36.org/entry/2012/05/07/201556

## まとめ

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

## 落ち穂拾い

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

[^profit]: 当然、停止期間は利益も得られません
[^compile-language]: あるいは、コンパイル言語でWeb開発をしている場合はそのバイナリ。
[^other-methods]: もしどちらとも異なる手法があったら教えて下さい。
[^other-application-server]: Webアプリケーションサーバにこの機能を持たせるのではなく、独立したスーパーバイザを使うケースもある模様(というかその方が一般的？)。やってることは多分同じ。 https://github.com/lestrrat-go/server-starter
[^usr2-signal]: `kill -s USR2 $(cat tmp/unicorn.pid)` https://bogomips.org/unicorn/SIGNALS.html
[^quit-signal]: `kill -s QUIT $(cat tmp/unicorn.pid.oldbin)` https://bogomips.org/unicorn/SIGNALS.html
[^old-and-new]: つまり、ユーザーがリクエストを送ったときに、古いレスポンスと新しいレスポンスのどちらが返ってくるかはわからない。
[^unicorn-winch]: 説明のためにWINCHについては触れていません。詳しく知りたい方は https://bogomips.org/unicorn/SIGNALS.html を読んでください。
[^rolling-deployment]: この記事で解説した方法は「ローリングデプロイ」です。その他の手法については[この記事](https://garafu.blogspot.com/2018/11/release-strategy.html)に記載があります。 
[^unicorn-spawn]: 起動時にまずmasterがソケットをオープンし、その後forkしてworkerを起動しているのでソケットが共有される模様。
[^puma]: pumaにも似たような機能がありそう。 https://github.com/puma/puma/blob/master/docs/restart.md
[^fork]: forkなので親子間でファイルディスクリプタが共有される。