2015年05月

運用を楽にするためのアプリケーションコードを書くということ

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

Growing Rails Application in Practice を読んだ

これ > https://pragprog.com/book/d-kegrap/growing-rails-applications-in-practice

1つの Monolithic な Rails アプリケーションをどのようにリファクタリングというか綺麗な状態を保って開発していくか解説している本

英語の本だけど、2時間ぐらいでさらっと読んだ。

Code Climate の人が 7 Patterns to Refactor Fat ActiveRecord Models で言っているような手法について整理して書いてあるのかなぁと期待して読んだのだけど、思ったほど書いてなかったので、飛ばし読みしてしまった。

なるほど、そうやってるのか〜って思えたのが1つだけあったので、まとめておくと、例えば、SignUp フォーム、PasswordReset フォームがあった場合に、User モデルを

class User < ActiveRecord::Base
  validate :email, presence: true
end

class User::AsSignUp < User
  validate :password, resence: true
  after_create :send_welcome_email

  private

  def send_welcome_email
    Mailer.welcome(user).deliver
  end
end

class User::AsPasswordReset < User
  # blah blah
end

のように分割して、そのフォーム固有の処理を個別のクラスに書いて分離するようにしているらしい。Code Climate の人の Form Objects に近いけど、ちょっとアプローチが違う。色んな所で応用利きそうな気がした。

さらっと読んじゃったので、これぐらい

Slack の通知にまつわる知見

背景: daioikachan 開発で得た知見を、コードのコメントにしか書いてなかったので、目の届きやすい場所にまとめておく。

Slack の API を使って開発をしていると、メンションや Highlight Keywords で表示上光りはするものの Desktop Notification (デスクトップ Slack アプリはもちろん、スマフォ Slack アプリも含む)による通知ポップアップが出るケースと出ないケースがあることに気づいた。通知が飛ばないとなると夜間のアラートに気づけないので大問題である。

リファレンスに書いてないので自分で調べて、中の人にも Support Help で聞いて確認取った。
条件を表にしてまとめておく。


縦軸が「 API の種別 / メンションか Highlight Words」で横軸が「テキストを指定する API のパラメータ」である。メンションは「@名前」のようにしてメンションを飛ばした時に通知が飛ぶかどうかで、Highligt Words は slack の Highlight Words に設定した単語が POST された時に通知が飛ぶかどうかである。 

textattachmentstext (link_names=1)attachments (link_names=1)
Web API a.k.a. Bots /
@mention
×××
Web API a.k.a. Bots /
Highlight Words
××××
Incoming Webhooks  /
@mention
×××
Incoming Webhooks /
Highlight Words
××××
Slackbot /
@mention
非対応非対応非対応
Slackbot /
Highlight Words
非対応非対応非対応

○: 通知が飛ぶ場合 ×: 飛ばない場合

日本語で説明すると、

  1. Web API (Bots) および Incoming Webhooks において、APIのパラメータに link_names=1 を設定しないと通知が飛ばない
    • 飛ぶとしてもメンションのみで、Highlight Words のほうは飛ばない (ハイライトはされるが、実は通知が飛ばないので注意)
    • color を付けたくて attachments 引数を使おうなんてすると、attachments 内の text では通知が飛ばない。トップレベルの text 引数でしか飛ばない。
  2. Slackbot は color などリッチな機能がないが、メンション、Highlight Words ともに通知が飛ぶ
つまり「Highlight Words でも通知を飛ばしたい場合は Slackbot を使う必要がある」ということです。

そんなこんなあって daioikachan (fluent-plugin-slack) は Slackbot にも対応しています。早く Web API (Bots) でも Highlight Words 通知対応して欲しい。
A Ruby and Fluentd committer working at DeNA. 記事本文および記事中のコード片は引用および特記あるものを除いてすべて修正BSDライセンスとします。 #ruby #fluentd #growthforecast #haikanko #yohoushi #specinfra #serverspec #focuslight
はてぶ人気エントリー