ぱろっと・すたじお

技術メモなどをまったりと / my site : http://parrot-studio.com/

性能と無関係にUnicornからPumaに移行した件

今回の結論を先に書けば・・・

CapistranoとPumaをあわせて使うとめっちゃはかどる」

・・・って話でございます

Unicornにこだわりがなければ、Pumaは便利だと思います、以上Σ(・ω・ノ)ノ

(私には必要だが一般的には無視していい)前置きというか経緯

そもそもCapistranoはお仕事で使ったことがありましたが、
チェンクロパーティーシミュレーター(以下、ccpts)ではまだ導入してませんでした

お仕事でごりごり使っていたのはRails4.2とかの頃で、
Passengerは重たいよねって認識*1から、Unicornを当たり前のように使っており、
ccptsでもNginx+Unicornを手動デプロイで運用してました

ccpts.parrot-studio.com

別に手動でも困ってなかったのですが、一つだけ問題がありまして

サーバダウンから再起動した際、サービスも一緒に起動してほしかったので、
init.dから(ccpts内の)unicornを起動する仕組みにしていたのですが、
実行ユーザーを考えてなかったので、いろいろなファイルがroot権限になってしまいましてΣ(゚Д゚)ガーン *2

それを直すのも面倒なのでそのまま運用していたのですが、違和感があったのも事実で、
そこを修正するついでにデプロイプロセスを改善したいってのもあり、
最初はDockerを検討したわけですよ、Dockerを

最近はいくらでもドキュメントがあるので、DB+Redis+Railsのコンテナを作成し、
サービスを起動するところまではできたわけです

・・・もちろん、ローカルで(´-ω-)

問題は「これをどうやってデプロイするか?」とか、
「修正からデプロイまでどうやって運用するか」なのですが・・・
ccptsのような「普通のサイト」だと、あまりに重すぎなんですよね

たぶん、「Dockerでデプロイする環境」は作れます
ただ、それが「メリットをデメリットが上回った状態」なのかというと、
激しく疑問だったわけです(´・ω・`) *3

とはいえ、「メリットをデメリットが上回った状態」を証明するには、
「もっと低コストなデプロイプロセスで回せる」ことを示す必要があり、
Capistranoならもっと低コストなはずだよね・・・?」という流れでのCapistranoでございます

CapistranoでPumaだと、Unironより何がいいのか?

前置きが長かったので、結論から先に...φ(・ω・`)

  • unicornと違い、Rails5.xでは標準でpumaがついてくる(追加でgemを管理しなくていい)
  • puma.rbをcapistrano3-pumaが自動生成してくれるので、pumaのconfigを書かなくていい


Rails5.0からWebrickを捨ててPumaに移行したのは、
ActionCableを使うのに、スレッドベースのAPサーバが必要だったからですが、
そもそもUnicornとPumaは昔からライバル関係にはあったわけです

とはいえ、Unicornのノウハウが成熟しているに比べ、
Rails5.0RC当時のPumaはまだ詰まるポイントが多く、
ccptsのRails5移行の時にも結構苦労した記憶があります(´-ω-)

それでもやはり、「Rails標準」の勢いは重要で、
久々にCapistranoの記事を探したら、みんなPumaとの連動になっていて、
私も試してみるか・・・と、軽い気持ちだったのですが、これがめっちゃ便利なのです

そもそも、Unicornはmasterプロセスとworkerプロセスが協調して動き、
workerを増減させたり、メモリを使いすぎたら再起動させたり・・・というのが、
無停止で行えるってのがポイントでした*4

とはいえ、そのworkerがDBへのコネクションを持ったままだと、
workerが作り直されるたびにリソースを食い尽くしてしまうので、
おまじないのようにこんなconfigを書いていたはずです

https://github.com/tablexi/capistrano3-unicorn/blob/master/examples/unicorn.rb

毎回そんなのが必要ならば、いっそう自動生成すればええやん・・・というのが、
capistrano3-pumaのえらい点でございます(`・ω・´)

github.com

例えば、deploy.rbにこんな感じで書くだけで、shared/puma.rbを自動生成してくれます
(sharedに作ってくれるあたり、実に気が利いてます)

# puma
set :puma_threads,    [4, 16]
set :puma_workers,    0
set :puma_bind,       "unix://#{shared_path}/tmp/sockets/puma.sock"
set :puma_state,      "#{shared_path}/tmp/pids/puma.state"
set :puma_pid,        "#{shared_path}/tmp/pids/puma.pid"
set :puma_access_log, "#{release_path}/log/puma.access.log"
set :puma_error_log,  "#{release_path}/log/puma.error.log"
set :puma_preload_app, true
set :puma_worker_timeout, nil
set :puma_init_active_record, true # DBコネクション周りのおまじないがこの1行でOK

怖くて試していないのですが、どうもNginx等のconfigも生成できるようで、
ssl用の鍵指定までできるあたり、かなり本格的に使えるように見えます

もちろん、pumaの再起動も「cap [stage] puma:restart」とかするだけですし、
非常に簡単ですねヽ(`・ω・´)ノ

ccptsの場合、元々Unicornのソケットにリクエストを流していたので、
それをPumaに変えるだけ*5で、移行があっさり完了してしまいました


そんなわけで、あくまで運用上の観点から、UnicornをPumaに置き換えたわけですが、
Rails5系を使っていて、まだUnicornで動かしているって方は、
一度Pumaを評価してみてはいかがでしょうか(´・ω・)っ


おまけ1:サーバ起動時にpumaを自動起動する

これでサーバのPumaを手元から管理できるようになったわけですが、
そもそもUnicornの運用でやっていた「サーバ起動時に自動起動」がこのままだとできません(´-ω-)

かといって、またinit.dにスクリプトを書くのもやりすぎだし、
また同じようなrootユーザー問題を発生させるのも面倒です
要は「サーバを起動したときに一度だけ、一般ユーザー権限でPumaを立ち上げてほしい」わけです

そこでいろいろ調べたところ、cronに「@reboot」と指定すると、
まさに「サーバ起動時に一度だけ」ができるらしいのですね

そこで、このように設定しておきました...φ(・ω・`)

$ crontab -l
@reboot /path/to/startpuma.sh # 一度だけ実行する起動シェル

$ cat /path/to/startpuma.sh
(PATHとかccptsに必要な環境変数とか)

cd /path/to/ccpts/current
bundle exec puma -C /path/to/ccpts/shared/puma.rb --daemon # 初回起動コマンド

なお、初回起動のコマンドは、Capistranoが実行しているのをそのままパクっただけですΣ(・ω・ノ)ノ

おまけ2:capistrano-yarn

最近はjsの管理にnpmではなくyarnを使う事例が増えてきましたが、
Capistranoももちろんyarnに対応しています

github.com

これを突っ込むだけで、デプロイ時に「yarn install」が走るのですが、
無駄な処理をさせないためのポイントが二点ほど

  • node_modulesをlinked_dirsに追加する
  • yarnのオプションに--prefer-offline(キャッシュがあったらそちらを使用)を追加する
# --prefer-offlineだけ追加できないので、他のオプションも全部書く
set :yarn_flags, '--prefer-offline --silent --no-progress --production'

あとはyarn installからassetsのコンパイルまで、Capistranoにお任せでOKです(`・ω・´)

おまけ3:UnicornとPumaのアーキテクチャの違いと、運用まで含めた雑な考察(長文)

今回のccptsの場合は「ダウンタイム・・・知らんな( ゚Д゚)y─~~」ってレベルなので、
別にどっちでもいいのですが、個人的に細かい違いが気になったので調べてみました

抽象的には似たような構造をしているこの二つのAPサーバですが、
Unicornはmaster+workerのマルチプロセス(Nginxと同じ)で、
Pumaはシングルプロセスのマルチスレッドという差違があります
(psを叩いてプロセスを見ればすぐわかります)

アプリを更新する場合、リクエスト受け付けをできるだけ止めたくないのですが、
Pumaは「リクエストが途切れた瞬間にプロセスを再起動」なので、
配慮をしてはいるものの、厳密にいえばダウンタイムが0ではありません(´-ω-)

一方、Unicornでgraceful restartをかけると、masterをforkして新しいmasterを起動し、
そこから新しいworkerを生成した後、古いmasterをkillする・・・という動きをするので、
表面上のダウンタイムは0になります*6

しかし、Pumaもクラスタモードが存在し、workerの数を指定することで、
マルチプロセスのマルチスレッドという状態を作ることができ、
workerプロセスを順番に再起動という機能があるので、これでダウンタイムを0にできます∠( ゚д゚)/

https://github.com/puma/puma/blob/master/docs/restart.md

ならPumaでええやん・・・と思うかもしれませんが、
「マルチプロセスのマルチスレッド」ということは、
プロセス数×スレッド数のrailsインスタンスが存在する、ということでもあります

つまり、Unicornの基準で単純に入れてしまうと、メモリがえらいことになります(lll゚Д゚)

それは調整の問題としても、
そもそも「マルチプロセス」というところに、大きな罠が存在しています

マルチプロセスでgraceful restartする場合「fork」して新しいプロセスを作るので、
元のプロセスから「環境変数の引き継ぎ」が発生します

これによって発生する有名な問題が、「Capistrano+Unicornで運用していたら、
急にgemが読めなくなって落ちる」というやつですΣ(・ω・ノ)ノ
(似た問題として、「新しく追加したgemが反映されない」というのも)

gemファイルの設置ディレクトリは環境変数で管理しており、
forkによって「最初に起動した際に使われていたrevisionのpath」が引き継がれます
その結果、世代が進んで当該revisionが削除されると、急にgemがなくなったように見えるわけです

ここで頷いた方は、おそらく実際に運用していた方だと思いますが、
設定ファイルに一行書けばクリアできる問題ですし、
capistrano3-pumaが生成するpuma.rbにも対応が入ってます(そのように見えますΣ(・ω・ノ)ノ)

問題はそれだけではなく、「古いプロセスがリクエストを受け付ける」こと自体が、
問題を引き起こす可能性があります

ちょっといじってデプロイ程度ならいいですが、DB自体に修正が入っている場合、
それを想定しない古いコードが処理しようとして、
スキーマの違いからエラーになってしまう・・・というのは十分考えられます

https://github.com/puma/puma/blob/master/docs/restart.md
(先ほどのドキュメントにも書いてあります)

エラーになるならまだいい方で、想定しない形でwriteが発生してしまい、
データの整合性が破壊される、なんてことも考えられるわけです(lll゚Д゚)

つまり、「アプリを構築する側」からすると、
「デプロイ時にAPサーバを "完全に" 再起動する」のが安心なわけです

それでもダウンタイムを0にしたいのであれば、もう一つ上のレイヤー・・・インフラで考えるとか、
企画レベルで「本当にダウンタイムが0でないとダメ?」と検討する方が、
実はシンプルな可能性もあります*7

逆に、デプロイするシステムがDBを持たないとか、
ステートレスな構造をしているのであれば、環境変数が更新されるときだけ気をつければ、
問題なくマルチプロセスでgraceful restartできる・・・ということでもあります

なので、APIサーバを叩いてデータを取得するfront側のサーバはgraceful restartして、
DBとつながっているAPIサーバだけ待機系との入れ替え方式を検討する、
なんて運用も可能なはずです

・・・とまあ、だいぶ観点が膨らんできてしまいましたが・・・

要するに「UnicornだろうがPumaだろうが、考えることはたくさんあるので、
世間の流行を考慮しつつも、現在の環境にとってどちらが適切か考えるのが大事」
ということでございます...φ(・ω・`)

*1: WebサーバとAPサーバを個別にスケールできない・・・とか、Webサーバで503を返して、APサーバ側を更新したり・・・といった運用を考えると、Passengerはめんどいのです(´-ω-)

*2: その時に書いた記事 https://parrot.hatenadiary.jp/entry/2014/10/29/123509

*3: デプロイのたびにコンテナ構築に10分とかかかり、そこから数百MBのコンテナを転送し、それを本番でpullしてきて・・・という流れを、修正のたびに回すのは(たとえ自動化しても)辛すぎます 「超軽量なコンテナを組み合わせたシステム(それこそ、AmazonのLambdaのような)」なら使いやすいのですが、Railsのような、それ自体が巨大ライブラリの塊で、モノリシックなシステムの場合、あまり向かないのではと(´-ω-) 極端な話、「サーバごとimmutableにしてデプロイ時に差しかえる」という(古典的だけど確実な)手もあるわけですし・・・

*4: Apache等もgraceful restartできますが、まあ置いておいて・・・Σ(・ω・ノ)ノ

*5: 一応、WebのrootをCapistranoを想定したpublicに変えましたが、Pumaに移行したから発生した修正ではないです

*6: よくあるミスとして、この「古いmasterをkill」がうまく動かず、古いプロセスがそのままリクエストを処理してしまい、デプロイした内容が反映されず・・・というのがありますね(´-ω-)

*7: 私もそうですが、どうしても「自分の領域」で解決策を考えてしまいますが、実際には一つ上の視点から見た方がシンプル・・・なんてことはよくありますね(´・ω・`)