4/1付けで Hadoop やらなんやらを運用している部署に異動してから、ひたすら新しいツールの実装をしていた。 この度、そのツールの最初の機能要件は実装し終わって最初のデプロイをするフェーズに入ったので、そのツールを運用に載せるためのアプリケーションコードを書いていた。

運用に載せるためのアプリケーションコードは、機能要件とは別の所にある非機能要件であって運用の肌感がわかっていないと要件を出すのは難しい。 自分は元々ウェブアプリエンジニアをやっていたのだけど、現職ではインフラの部署に所属している。 インフラエンジニアに転籍したのは、元々その辺の肌感を掴みたかったからで、今回、実際にその経験を活かせたので結構満足している。

書いてるときは思いつきベースでゴリゴリ書いてしまったのだけど、チェックリストにしておくといつかまた役に立ちそうだと思ったので、リストにしてブログにまとめておく。 もっと書くべきコードもあるかもしれないし、思いついたら追記していこうと思う。ご意見もウェルカムである。

Graceful Restart, Graceful Shutdown

エラーを避けるために、デプロイの度に Web プロセスをサービスアウトおく必要があったりすると、運用の手間が増える。無停止で再起動できるようになっていると楽になる。

今回のアプリは Ruby で書いていて、Web (Unicorn) と Worker (Sidekiq)、および Scheduler (Clockwork) プロセスがある。

まず、Unicorn を Graceful restart に対応させた。cf. 「Server::Starterに対応するとはどういうことか」の補足 - Unicorn

Worker プロセスも、デプロイすると処理が強制終了されてしてまうようでは困るので、graceful shutdown ができるフレームワークを選んだ.

Sidekiq は以下のような動きをする。requeue してくれるので、途中で SIGTERM を送ってしまっても問題ないように内部の処理を設計した。

  • SIGUSR1 を送ると、新しいジョブを受け付けなくなる
  • SIGTERM を送ると、timeout (デフォルト: 8)秒の間処理を待って、終わらなかったら終了する。処理が終わらなかった場合、requeue してから例外を投げて終了する

Scheduler プロセスも同様だけど、Clockwork は元々は対応してなかったので実装して PR 送った >graceful shutdown #148。 これで一通り、Graceful 対応ができた。

都度接続 or 再接続

弊社のインフラを利用すると、背後の MySQL サーバが落ちても、自動で fail over してくれるのだけど、 その時にアプリが永続接続しているようだと、接続先を切り替えるためにアプリを再起動しなければならない。

都度接続するか、再接続するようになっているとアプリの再起動が必要ないので運用が楽になる。

今回、redis も使っているのだけど、redis-rb および hiredis は標準で再接続してくれることを確認したので、それで良しとした。

DNS キャッシュ

関連するが、都度接続するような場合は、毎回名前解決が走るので(IPアドレス指定ではなくホスト名指定の場合)、DNS キャッシュしておくべきである。

max requests per child

なんらかの問題があって、Ruby プロセスのメモリが太り続けるような状況になると大変困る。

もちろんアプリの改修が一番良いのだけど、こういうのは得てして使っている gem の深遠な部分が問題だったりして、問題特定まで時間がかかるので、 ある一定数以上のリクエストを受け取ったらプロセスを再起動する、メモリを一定数以上使用してしまっていたらプロセスを再起動する、といったことが行えるようにしておくと運用でカバーできる。

Unicorn プロセスの自動再起動には、unicorn-worker-killer を利用できるので、それを利用した。Sidekiq プロセスの自動再起動には、sidekiq-recycler が利用できるようだ。

デーモン化

不意にプロセスが死んでしまっても再起動してくれるように daemontools, upstart, systemd のようなデーモン支援ツールを使ってデーモン化しておくと運用が楽になる。

今回は daemontools でデーモン化した(弊社は daemontools が標準なので)

ログローテーション

ログが延々と手続けるようだとサーバのディスクを圧迫して掃除業が必要になってしまうので、ログローテーションを有効化して運用を楽にする。

  • ruby の標準 Logger はログローテーションの機能を持っているのでそれを利用できる
  • daemontools の multilog を使うと、ログローテーションしてくれる

ログの永続保存

関連するが、古いログを残しておいて後でみたい、分析したい、という要求がある場合は、Fluentd を使って転送しておくなどすべきであろう。 今回は幸いなことに、Vertica と言った大容量データストレージが裏にあったので、Fluentd でそこに転送しておくことにした。

ログの format

関連するが、ログ収集ツールでログを収集しやすいように、ログの format を統一し、また吐き出すデータの定義をしておくべきである。

Rails のスタックトレースは標準では複数行に吐き出されるが、それだと収集しづらいので、強制的に1行にする oneline_log_formatter というものを書いてエラーログを吐き出すようにしている。 (Fluentd の in_tail は複数行対応もしているので、できないわけではない。ただ conf が書きづらいだけである)

また、アクセスログのような情報は、今回は Line Delimited JSON で吐き出すことにした。 LTSV にしなかったのは、型が全て String になって、Fluentd 内で型変換する必要が発生するためである。 人が読むときは jq コマンドと組み合わせて読み込めばなんとかなるだろう。

また、フレームワークが勝手にログを出してないか、変なフォーマットで出してないか確認する必要がある。 実際、rails, sidekiq はフレームワーク内で独自に吐き出しているログがあるため、それらのフォーマットも変更した。

ログを仕込む

一言でログを仕込む、と言うのは良いが、これは経験がものを言うのかもしれない。言語化が難しい。

  1. エラーが出たら適切にログ出力させておく
  2. その際、エラー情報だけでは追えない可能性があるため、適切に入力情報も吐き出せるようにしておくと良いこともある。

あとは、例えば redis への set が失敗した場合に、ログに情報を吐き出しておいて、最悪手動で recovery できるようにしておく、とか。 そういうログの仕込み方もある。

運用がイメージ出来る人でないと適切なログを仕込むのは難しい。

パフォーマンスログ

Worker の1サイクルがどれぐらい時間を費やしているのかなどパフォーマンスログを出しておくと良い。 想定以上に時間がかかっていることがわかれば、worker の数を増やすなど運用でカバーもできる。

DBの接続先を管理するモジュール

DB接続先およびパスワードなどを、アプリのレポジトリに突っ込んでしまうと、変更があった場合にそこを参照しているアプリコードを全て変更して回る必要があったりして大変になる。 また、パスワードをアプリのレポジトリに突っ込むな、という話も当然ある。

1元管理して、そこから簡単に取り出せるモジュールを用意してあると捗りそうだ。

監視用エンドポイント

監視に使うシンプルなエンドポイントを用意しておくと良い。

監視用エンドポイントからの応答がなくなったら、自動でロードバランサーから外す、といった処理を行うために利用する。 この際、不安定な状態で、出たり入ったりしてしまわないような制御にしておくべきである。 自動で抜けるが、自動では入らない、とか。

自動 fail over

また、1台でしか動かしてはいけないような worker があった場合に、 そのサーバが down したことを検知したら、自動で reserve サーバで worker を立ち上げてくれるような仕組みを作っておく。

アプリコードではなく監視コードだな。

パフォーマンス改善

本番に投入する前に、パフォーマンス改善をしておくべきである。

  • テーブルに適切に index を貼る
  • slow query の撲滅
  • N+1 クエリの撲滅

などがわかりやすい。あとは to_json したり JSON.parse してる所とか、コードとしては1行なのにデータ量によっては非常に CPU を喰ったりするので改善が必要だったりする。

本番相当のデータがあればそれを投入する。負荷をかけるためのツールを作って、負荷をかける、などすると改善させやすい。本番データに個人情報が入っているようであれば、マスクしてロードするようなツールも作る必要があるだろう。

この辺は ISUCON民として、腕がなるところであるし、ISUCON でノウハウを蓄積するのが良い。

パフォーマンス解析ツール

関連するが、パフォーマンス解析ツールの導入が出来るならしておく。

有名どころでは、New Relic とか peek-performance_bar とか。

druby サーバを立ち上げておいて、外から stackprof を ON/OFF できるようにしておくと本番で問題が起こった場合の解決に役にたったりする。

arproxy で slow query をフックして、発行しているコードの行を出すとかしておくと、すぐに該当コードが発見できて捗る。

sigdump を仕込んでおいて、瞬間のスレッドダンプや、メモリ状態を吐き出せるようにしておくと原因究明に役立ったりする。

おわりに

どうだろう?他にもあったような気 and ありそうな気がするので追記します。ご意見 welcome