Ruby のクラス拡張を利用して監視の実装をうまく軽量化した話(現場での実装方式検討の例つき)

この記事では、Ruby の非同期処理ライブラリである Sidekiq を使って定期実行処理を行う Sidekiq-Cron の監視方法について、チームでの方式検討の様子を交えながらご紹介します。

目次

はじめに

こんにちは、クラウド & ネットワークサービス部で SDPF のベアメタルサーバー1・ハイパーバイザー2の開発をしている山中です。

私のチームでは Ruby でサービスを開発しています。 API リクエスト受付など、さまざまな処理を gem を利用しながら実装しており、中でも定期実行処理は Sidekiq-Cron という gem を利用して実現しています。

先日チームで Sidekiq-Cron の監視の実装について議論している最中、チームメンバーから巧いと言われた実装がありました。

今回の記事では、実装内容を簡単に紹介しながら、普段チームでどういった観点で実装の方式を検討しているかご紹介したいと思います。

Sidekiq-Cron について

Rails でもよく使用されている Sidekiq という非同期処理ライブラリを使って、定期実行を可能とする gem です。

定期実行処理は cron job という単位で定義でき、cron job ごとに status を enabled / disabled に設定することも可能となっています。

Sidekiq では、メインであるジョブを処理するプロセスの他に GUI のプロセスも起動でき、Sidekiq-Cron を導入すると以下のように GUI にもページが増え、cron job ごとの status や実行状況などを確認できるようになります。

Sidekiq-Cron の cron job の status の監視

私たちが運用しているサービスでは、各 cron job の status は全て enabled であることが期待値です。disabled があるとサービス影響につながる場合があります。

一方、cron job の status はメンテナンス作業の中で一時的に disabled に変更されることがあり、メンテナンス作業後は status を enabled に戻さなければいけません。

しかし、メンテナンスは人間が行う作業というのもあり、enabled に戻し忘れてしまうという問題が時折発生していました。

私達のチームではこの問題への対策として、enabled に戻し忘れたことに気付けるように一定間隔で status を取得し、enabled ではない場合にアラートを上げるような監視の仕組みも作っています。

既存の status 監視の問題点

この監視の仕組みですが、1つ問題が存在していました。それは1番重要な1つの cron job しか status を監視していなかったという問題です。

この問題により、監視対象ではない cron job の status が長期間 disabled になっていることに気付けなかったという問題が先日発生してしまいました。

振り返りの結果、全て(16個)の cron job の status を監視しようとチームで合意し、実装方式の検討が始まりました。

既存の監視の仕組みの問題点

既存の status 監視では、以下のような Ruby スクリプトを実行して status を取得しています。 (本筋から外れるので説明は割愛しますが、Consul を使用してスクリプトを定期的に実行しています。)

# cron job 名を引数に渡して実行することで status を取得

require 'sidekiq-cron'

<SidekiqRedis に接続する記述>

job = Sidekiq::Cron::Job.find(ARGV[0])

if job.present?
  puts job.status
else
  puts "#{ARGV[0]} does not exist in cron jobs"
  exit 1
end

当初は 16個の cron job それぞれについてスクリプトを実行し、それぞれの cron job の status を取得すれば良いと考えていました。

ところが、スクリプト実行時の負荷を計測した結果、16個並列でスクリプトを実行すると CPU 使用率が 75% 以上に高騰してしまうことが判明してしまいました。

status 取得自体の負荷は軽いのですが、Ruby プロセスを起動してライブラリのロードをする部分の負荷が高く、既存の監視スクリプトを安直に16個並列で実行するのはやめようという流れになりました。

負荷が低い監視の仕組みの検討

結論から述べると、最終的にチームで合意したのは以下の案4です。 メンテナンス性や心理的な受け入れやすさ、動作の軽量さなどを考慮した結果、この案を採用することになりました。

ここでは各案の内容と、実際にチームで議論したときに上げられたメリット・デメリットを簡単に紹介します。

案1:全 cron job の status を定期的にダンプし、ダンプ結果を読み取って監視する

1回の Ruby スクリプトの実行で全ての cron job の status を取得してファイルにダンプし、各 cron job の監視ではダンプファイルを読み取って status を取得して監視をするという案です。

  • メリット
    • 負荷が高い Ruby プロセスの起動やライブラリのロード部分を1回に集約することで負荷の軽減が可能
  • デメリット
    • cron job の status をダンプする処理を別途実装する必要がある
    • 厳密に現在の cron job の status を取得できるわけではない
    • 仕組みとしてややこしい感が強い

案2:Redis を直接参照して cron job の status を読み取る

Sidekiq は Redis にデータを書き込んでいるため、Redis に書き込まれている cron job の status を以下のように redis-cli コマンドで直接取得するというアイデアです。

redis-cli -a <password> -h <redis_address> hget <application_name>:cron_job:<cron_job_name> status
  • メリット
    • Ruby を使用しないため動作が軽い
  • デメリット
    • Sidekiq がラップしている部分(Sidekiq の利用者が本来意識しなくて良い Redis の中身の部分)を意識する必要がある
      • 深いところを直接触りすぎている感がある

案3:Sidekiq の GUI の html ページの内容をパースして status を取得

最初に紹介したように Sidekiq には cron job の status を確認できる GUI が存在します。 以下のように curl で GUI の html ページを取得し、取得結果をパースして status を取得するというアイデアです。

curl -ks -u <user>:<pass> http://<sidekiq_gui_address>/cron/<cron_job_name> | grep -o -E 'enabled|disabled'
  • メリット
    • Ruby プロセスを新規に起動しないため動作が軽い
      • GUI サーバとして既に起動している Ruby プロセスを利用することで、負荷が高い Ruby プロセスの起動やライブラリのロードを行わなくて済む
  • デメリット
    • html ページのパースは仕組みとして頑健ではない、強引に欲しい値を取得している感がある
      • html ページがパースされることは Sidekiq の作者も想定していないはず
      • Sidekiq のバージョンアップなどによって html ページの構造が変わったときに、値のパースができず監視の仕組みも動かなくなる恐れがある

[採用] 案4:Sidekiq の GUI に新しいエンドポイントを実装して、そのエンドポイントから status を取得

cron job の status を取得する GUI のエンドポイントを Ruby のクラス拡張で新規に実装し、実装したエンドポイントから status を取得するというアイデアです。

Sidekiq に GUI があるということは「各ページを表示するためのエンドポイントの実装があるはず」というところに着目し、その実装を拡張すれば良いのではないかという流れでこのアイデアを思いつきました。

以下のように Sidkiq の GUI を起動するためのコードを実装し、新しいエンドポイントを実装します。

require 'sidekiq-cron'

<SidekiqRedis に接続する記述>

use Rack::Session::Cookie, secret: SecureRandom.hex(32), same_site: true, max_age: 86_400

module CustomWebExtension
  def self.registered(app)
    app.get '/cron/:name/status' do
      @job = Sidekiq::Cron::Job.find(route_params[:name])

      if @job.present?
        @job.status
      else
        "#{route_params[:name]} does not exist in cron jobs."
      end
    end
  end
end

Sidekiq::Web.register CustomWebExtension

run Sidekiq::Web.new
  • メリット
    • 案3と同じく Ruby プロセスを新規に起動しないため動作が軽い
      • html ページという本来パースして値を取得するためのものではない所から欲しい値を取得しているわけではないため、案3よりも強引感がない
      • クラス構造は html ページの構造よりも変化しづらいはずなので、案3 よりも頑健なはず
  • デメリット
    • Sidekiq のライブラリ構造が変わることで、将来的に動作しなくなる可能性がある
      • 基本的にクラス拡張は避けた方が良い

案5:そもそも複数の cron job の status を監視するのをやめる

うまく実装できる案が無いのであれば、現状を維持し、問題に遭遇するリスクを許容するという案です。

言わば戦略的撤退ような案ですが、他の議論でも時折このような案を採用することがあります。

さいごに

いかがでしたでしょうか?

チームで実装方式について議論するときは、頭を柔らかくして複数の案を考え出し、あえて採用されないであろう案も提示し、その中から現状の最適解を選ぶことで、チーム全員の納得感を高めています。

口頭で話すだけだと各案のデメリットばかりに着目してしまい、議論がなかなか進みませんが、メリット・デメリットを書き下して共有しながら議論を進めることで冷静に各案を比較でき、円滑に議論を進められます。

また、今回採用した案のように、既存ライブラリのモジュールを拡張して新たな機能を簡単に追加できるのは、メタプログラミング3が得意な Ruby ならではだと思います。

既存ライブラリのモジュールに手を加えると、ライブラリのバージョンアップなどで将来的に動作しなくなる可能性ありますが、多用しなければ非常に強い効果を発揮すると思います。 みなさんもライブラリのコードの深い部分まで読みつつ、ここぞという場面で独自の拡張を入れてみるのはいかがでしょうか?


最後になりますが、SDPF クラウドは国内最大級のクラウドサービスです。 開発メンバーは、数千台以上の物理サーバーの操作の自動化をはじめとした、技術的難易度の高い課題に取り組みつつ、日々より良いサービスにしようと邁進しています。

今回紹介した監視の実装など、実際にコードの深い部分まで理解したうえで、自分たちで手を動かして実装方式を検討しています。

直近ではベアメタルサーバー・ハイパーバイザーチームは「B26.大規模国産クラウドを支える IaaS (ベアメタルサーバー/ハイパーバイザー) のソフトウェア開発」というポストで現場受け入れ型インターンシップの募集をしています。 本記事に興味を持った学生の方は是非奮ってご応募ください。

参考

SDPF ベアメタルサーバー・ハイパーバイザー関連資料


  1. 専有型の物理サーバーをオンデマンドに利用可能とするサービス。 https://sdpf.ntt.com/services/baremetal-server/
  2. ベアメタルサーバー上に vSphere ESXi や Hyper-V など代表的なハイパーバイザーを予めセットアップした状態で利用可能とするサービス。https://sdpf.ntt.com/services/vsphere/ https://sdpf.ntt.com/services/hyper-v/
  3. プログラミングコードを記述するコードを記述すること、既存の言語を拡張して動的にコードを変更するといった概念
© NTT Communications Corporation All Rights Reserved.