1コマンドで複数のSidekiqプロセスを起動・監視するクラスタースクリプトの紹介
Honeybadgerでは、処理パイプラインの中でSidekiqに大きく依存しています。私たちが行うほぼすべての処理は、どこかのタイミングでキューを経由します。そのため、Sidekiqを適切な状態で運用し続けることを常に意識しています。
最近EC2への移行に伴い、長期間稼働する安定したサーバー群から、絶えず入れ替わるインスタンス群でジョブを実行する形へと変わりました。これを受けて、起動時にSidekiqを開始する方法を見直すことになりました。以前に比べて、この部分が格段に重要になっていたからです。
以前は、ワーカープロセスの起動にgodに頼っていました。プロセスを監視し、メモリを使いすぎたものを終了させたかったからです。godの設定ファイルは、当時デプロイされていたサーバー向けに手作業で調整されたものでした。ところが、新しいインスタンス上でsystemd経由で古いgod設定を起動すると、設定で指定した数より多くのワーカーが立ち上がってしまうことがありました。その結果、インスタンスにメモリプレッシャーがかかり、深刻な問題につながりかねません。しばらく格闘した末、「その機能の多くをそもそも活用していない」と気づき、プロセス管理にgodを使うのはやめる決断をしました。
1つのコマンドで複数のSidekiqプロセスを実行する
本当に欲しかったのは、次のようなスクリプトでした。
- systemdサービスとして実行できる
- CPUコアごとに1つのSidekiqプロセスを起動できる
- メモリを消費しすぎたプロセスを自動的に再起動できる
さらに、さまざまなサイズのEC2インスタンスに対応できることも必須条件でした。ワークロードに最適な構成を探る過程で、コア数もRAM容量も変わる可能性があるからです。ちょうど求めている動作をするスクリプトが見つからなかったため、自分で書くことにしました。スクリプトの全コードは、Honeybadger公式ブログの元記事で公開されています。
もちろん、このスクリプトには多少「借り物」のコードも含まれています。process_countメソッドのコードはおそらくStack Overflow由来のもので(unicornの設定で何年も使い続けています)、fork_childメソッドはsidekiq gemに含まれるsidekiqコマンドがベースになっています。
このスクリプトは、搭載されているCPUコアの数に応じて子プロセスを起動します。各子プロセスは、sidekiqコマンドを直接実行した場合とまったく同じものです。そのため、このスクリプトに渡したコマンドラインオプションは、そのままSidekiqのCLIコードに渡されて解析されます。つまり、sidekiqを直接実行する際に指定できるオプションは、基本的にこのスクリプトにもそのまま使えます(ただし、pidファイルオプションのように、ここでは意味をなさないものもあります)。
加えて、各子プロセスのメモリ使用量を定期的にチェックするスレッドも起動されます。いずれかの子プロセスがしきい値を超えると、そのプロセスを終了し、代わりの新しいプロセスを自動的に起動します。
クラスターの操作方法
プロセス数はデフォルトでコア数になります(Sidekiq専用のインスタンスを用意しているケースを想定しています)が、スクリプト実行前に環境変数SK_PROCESS_COUNTを設定すれば上書きできます。
同様に、メモリしきい値はインスタンスの総RAMに対する割合として設定されます(process_countの値に1を加算することで、多少の余裕を持たせています)が、環境変数SK_MEMORY_PCT_LIMITで任意の割合を指定可能です。私たちの場合、UIを支えるRailsアプリが動いているインスタンスでは、この2つの変数を使ってSidekiqのプロセス数とメモリ使用量を制限しています。
おまけとして、このスクリプトはSidekiqプロセスの管理で一般的に使われるシグナルを捕捉し、それを子プロセスへ伝播させます。これにより、pkill -f -USR1 skclusterで子プロセスに新しいジョブの受付を停止させたり(デプロイスクリプトの初期段階で実行)、pkill -f skclusterでプロセス群全体を終了させたりできます。
systemdの設定
systemdのサービス定義自体はごくシンプルです。サービス定義ファイルの内容も、元記事で確認できます。
私たちはすべてのインスタンスでpgbouncerを使ってPostgresへの接続をプロキシしているため、Sidekiqプロセスを起動する前にpgbouncerサービスが動いていることを保証しています。もし同じインスタンス上でRedisを動かすなら、Requires行とAfter行にredis-server.serviceも追加することになるでしょう。
EnvironmentFileのファイル名に-というプレフィックスを付けると、「そのファイルが存在しなくても問題ない」という意味になります。これにより、デフォルトのプロセス数とメモリ上限を使いたい場合には、環境変数ファイルを省略できます。また、--requireオプションを使うことで、Railsアプリケーションの設定やイニシャライザを読み込む場所をSidekiqに教えています。
skclusterスクリプトはSIGTERMを捕捉するため、systemctl restart skcluster.serviceを実行するだけですべてのワーカーを再起動できます(これはデプロイスクリプトの終盤で実行されるタスクです)。
まとめ
このスクリプトは導入後しばらく運用していますが、非常に安定して動いています。起動時にプロセスが過剰に立ち上がる問題はなくなり、モニタリングからのメモリ使用量警告も届かなくなり、好きなタイプのEC2インスタンスを自由に選べるようになりました。手間のかからない運用こそが、理想の運用スタイルだと感じています。
-
ObjectRocketのメトリックダッシュボード徹底解説:Grafanaで実現するデータベース監視
本記事は、2019年9月9日にObjectRocket.com/blogで公開された内容をもとにしています。 Rackspace ObjectRocketのミッションは、お客様のデータベースを「オートパイロット」状態にすること。つまり、データ管理の手間からお客様を解放し、優れたアプリケーション開発に集中していただける環境を提供することです。しかし、データベースが自動化されていても、システムの可観測性(オブザーバビリティ)を維持することは依然として重要な課題です。 データストア内で何が起きているのか、そして直近のアプリケーションの変更がデータベースのパフォーマンスにどのような影響を与えているの
-
Railsでワンタイムスクリプトを書く3つの方法と使い分けのポイント
CSVファイルからアプリに大量のデータをインポートしたいと思ったことはありませんか?あるいは、顧客レビューの中の文字化けした文字を修正する必要があったり、Redisでのデータ保存方針が変わり、古い形式から新しい形式へ全データを移行しなければならなかった経験もあるかもしれません。 Avvoでは、こうしたタスクを「アドホックタスク」と呼んでいました。つまり、おそらく一度しか実行する必要がないタスクです。では、Railsでアドホックタスクを処理する最良の方法は何でしょうか? 1. データベースマイグレーションとして書く データベース内のデータ構造を変更する必要がある場合、マイグレーションはうまく機能