DIGGLE開発者ブログ

予実管理クラウドDIGGLEのプロダクトチームが、技術や製品開発について発信します

検証用スクリプトの実行環境として AWS Cloud9 を採用した話

こんにちは。DIGGLE エンジニアの miyakawa です。
普段はバックエンドとインフラを中心に開発しています。

今回は、前回の記事で触れたレポート機能の数値検証の「実行環境」についてお話したいと思います。

レポート機能の数値検証の内容については前回の記事をご覧ください。 diggle.engineer

数値検証の実行環境

まず結論として、現在は数値検証の実行環境として AWS の Cloud9 というサービスを利用しています。

Cloud9 はクラウドベースの統合開発環境(IDE)を提供してくれるサービスです。

docs.aws.amazon.com

IDE なので開発環境としての用途が一般的かと思いますが、弊チームでは検証用スクリプトの共有実行環境として活用しています。

次に Cloud9 を採用した経緯についてお話しします。

実行環境選定の経緯

ローカル環境での課題

当初、数値検証の実行は開発メンバーのローカル環境 (PC)上で行っていました。

数値検証には本番環境のデータベースのデータを複製したデータベース(以降、数値検証用 DB)を用いますが、セキュリティ上ネットワーク外部からの直接アクセスはできないようになっています。

そこで、ローカル環境から数値検証用 DB へアクセスするために、EC2 インスタンスによる踏み台サーバを用意し AWS Systems Manager の Session Manager を使ってセキュアにログイン、数値検証用DBまでポートフォワード接続できる環境を構築していました。

しかしながら、このローカル環境で数値検証を行う方法では以下の問題がありました。

  • 数値検証の実行完了まで数時間かかり、その間ローカル環境のリポジトリのブランチは固定かつ PC リソースも消費されるため開発作業に支障が発生する
  • 40〜60分ほどで Session Manager の接続が切れて数値検証の実行が途中で止まってしまう*1

二つ目の問題については、数値検証スクリプトに途中の段階からリトライするオプションがあるため、実行が止まるたび都度リトライすることで対処していましたがかなりの手間となっていました。

ローカル環境以外の方法の模索

ローカル環境を使う運用は難しいと判断し、それ以外の方法を検討し始めました。

ここで一度数値検証の実行環境の要件をまとめます。

  • Rake タスクを実行できる or Docker コンテナを実行できる
  • リポジトリの任意のブランチで実行することができる
  • AWS のプライベートネットワークにある数値検証用 DB にアクセスできる

これを踏まえて検討した結果、以下のような案が出ました。

  • EC2 インスタンスにローカル環境と同様の環境(git + Docker)を用意して数値検証を実行する
  • 上記に加えてジョブ管理ツール(Rundeck など)を導入して Web ベースで作業できるようにする
  • Github Actions のワークフローとして実行する*2
    • 数値検証用 DB アクセスのために EC2 インスタンスで self-hosted runners を構築する

いずれも AWS 上に EC2 インスタンスを作成するという点は同じで費用面でのコストに差はありませんでした。
後は工数との兼ね合いかと考えていたところで、AWS のソリューションアーキテクトの方と相談の機会があり本件についても相談してみました。

すると
「Cloud9 が適しているのはないか」
とのご助言が...!

それまで Cloud9 は使ったことがあってもあくまで開発環境用というイメージしかなかったため目から鱗でした。

早速 Cloud9 による環境を検証したところ、次に述べる Cloud9 の利点から、先に検討していた各案よりも優れていると判断し最終的に採用へ至りました。

Cloud9 の利点

環境作成が容易

Cloud9 の EC2 環境では EC2インスタンスをベースにクラウド IDE 環境を構築できます。
AWS コンソールからネットワーク(VPC)や接続方法等の最低限の情報を設定するだけで、すぐに開発を始められる EC2 インスタンスが作成されます。
(Cloud9 の環境作成方法の詳細については こちら を参照ください)

IDE の機能を除いたとしても、一から EC2 インスタンスをセットアップする場合と比べて多くの利点があり、セットアップの工数を減らすことができます。
例えばですが、

  • (接続方法として選択した場合)Session Manager の設定が自動で行われる
  • git、docker といった開発に必要なツール群がプレインストールされている

などが挙げられます。

IAM ユーザによる共有

Cloud9 の環境は作成時点では作成者のみが利用できる状態です。 そこに IAM ユーザを招待する形で他の開発者に環境を共有することができます。

仮に素の EC2 インスタンスへの Session Manager によるアクセスを IAM ユーザごとに可否設定する場合は各 IAM ポリシーを設定する必要が出てきますが、Cloud9 であればクラウド IDE 上で操作するだけなので非常に管理が楽になります。

また CloudTrail から証跡(いつ、どの IAM ユーザが Cloud9 を利用したか)も確認できるため、万が一の時のためにも安心です。

費用削減の機能がある

開発・検証用の環境は常に利用するわけではないため、夜間停止などの仕組みを入れたくなると思います。

EC2 インスタンスであれば AWS Systems Manager Automation を利用するなど一工夫が必要になります。

一方で Cloud9 にはセッションがなくなった後に一定時間が経過後 EC2 インスタンスを自動で停止する機能があります(停止するまでの時間は選択可能です)。
これにより、Cloud9 を使用する時のみ EC2 インスタンスが起動している状態となるので、費用の無駄を防ぐことができます。

Cloud9 環境セットアップ時の注意点

Docker Compose はインストールされていない

前節で docker がプリインストールされていると言いましたが Docker Compose はインストールされていません。

Docker Compose が必要な場合は、Docker の公式ドキュメント を参考にインストールしましょう。

ディスクサイズの拡張

Cloud9 のデフォルトのディスクサイズ*3は 10GB でその内大半はシステムで利用されており、環境作成時点でユーザが利用可能な領域は 2GB 程度です。
これでは多少大きめの Docker イメージをいくつかプルするだけであっという間にストレージが枯渇してしまいます。

そのため、環境を作成した時点でディスクサイズを拡張しておくことをおすすめします。

docs.aws.amazon.com

まとめ

今回は数値検証の実行環境として Cloud9 を採用するまでの経緯について紹介しました。

Cloud9 は他にもいろいろと活用できそうに思っているので、今後も新しいユースケースが出てきたら紹介していければと思います。

We're hiring!

DIGGLEでは最高のプロダクトを一緒に作ってくれるエンジニアを募集しています!少しでも興味があれば、ぜひ下記採用サイトからエントリーください。

open.talentio.com

Meetyによるカジュアル面談も行っていますので、この記事の話をもっと聞きたい!という方がいらっしゃいましたら、お気軽にお声がけください。

meety.net

*1:主にポートフォワードの接続が切れるという状況。調査したところ Session Manager のセッションタイムアウト(https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/session-preferences-timeout.html)が原因ではないことは分かりましたが、根本の原因は分かっていません

*2:弊チームでは CI として Github Actions を利用しています

*3:Cloud9 の環境は EC2 インスタンスで構築されているので、ディスクサイズ = EBSのボリュームサイズを示します

レポート機能の計算結果が正しいことをどう保証するのか考えた話

こんにちは。DIGGLEエンジニアのhondaです。
開発メンバーの中では主にバックエンドを担当することが多いです。

今回はDIGGLEの根幹を支えるレポート機能の数値検証についてお話したいと思います。

DIGGLEのレポート機能

DIGGLEのレポート機能

DIGGLEでは取り込まれたデータを集計し予実管理に役立つレポートを自動で生成する機能があります。 データは単月、累計、部門、科目など様々な角度からリアルタイムに分析が可能になっています。
もちろん今後もDIGGLEの根幹の機能として進化を続けていく予定です。

レポート機能の検証に関する課題

そんな素晴らしいDIGGLEのレポート機能ですが、機能の検証に課題がありました。 自由度の高いレポートを作成できるがゆえに 条件の組み合わせはほぼ無限に存在し、レポートの数値の正しさを保証することが難しい状況でした。 もちろんモデルやコントローラレベルでのテストも存在していて、リリースのたびにきちんQAもおこなうのですが、
「本当にちゃんと計算できているだろうか」
「あれだけテストしたから大丈夫なはずだけど」
といった不安な気持ちが常につきまとっていました。

検証の方法を考える

検証作業の課題を洗い出すと以下のような課題が浮き彫りになりました。

  • 手動でテストをおこなうには時間がかかる
  • 条件の組み合わせが多岐に渡るため網羅的にテストすることが大変
  • 機能追加は随時おこなわれるのでテスト項目の管理に継続的にコストがかかる
  • モデルやコントローラーレベルのテストはあるが、ユーザーが直接見る数値に対する検証が薄い

これらの問題を解決するために検討したのが数値検証の自動化でした。
DIGGLEにはレポートの数値をCSVで出力する機能があります。この機能を使って出力されたCSVファイルに対する検証をおこなえば ユーザーが直接見る数値レベルの検証を自動でおこなえるので、工数削減が期待できそうです。
しかし、どうなっていれば数値が正しいと言えるのかということを考えるとなかなか難しいです。
用意したテストデータに対して期待する値が出力されることをテストするということも考えたのですが、 そのデータ自体の正しさや、データと期待する値のメンテナンスの工数などを考えると現実的ではないと判断しました。

入力データと正しい出力結果を簡単に準備する方法を考えたときに思いついたのが本番のデータを使うことでした。 本番のデータであれば多様なデータと多様なレポート形式、そして我々開発チームの血のにじむような努力によって支えられた確からしい出力結果が存在するのです。 もちろん、本番の出力結果が間違っていればテスト結果も間違えることになるのですが、完璧に正しいと言える出力結果が存在しない以上 本番の出力結果はかなり確からしい出力結果であるためメンテナンスのコストも考慮したときに妥当な選択肢と判断しました。

具体的な検証方法

以下のような手順で検証をおこないます。

  1. 数値検証用環境にリリース前のデータベースと同じデータの入ったデータベースを作成する
  2. リリース済みのブランチでこのデータベースに対して全てのレポートでCSV出力をおこなう
  3. リリース予定のブランチに切り替えてmigrationを走らせる
  4. 再度全てのレポートをCSV出力する
  5. 2と4の結果を比較する

これで既存のレポート機能に関しては様々な条件でデグレードしていないことが検証できます。 新規機能については残念ながら工数を抑えてデータの準備をおこなう方法が思いつきませんでした。(というかそんな方法はないんじゃないか?) しかし、既存分だけのテストを自動化しただけでもかなりの工数を削減できたはずなので今回はこれで良しとしました。

また、この検証をおこなうためには本番相当のデータベースの準備が必要になるのですが、 数値検証用環境は、安全のため外部からはアクセスできないようになっています。 今回は数値検証環境の説明については省略します。次回の記事で紹介予定なのでご期待ください。

データベースに保存された全てのレポートに対してCSV出力をおこなう

ようやくコードの話になります💦
やること自体は単純です。DIGGLEではレポートの条件がjson形式でデータベースに保存されています。 そのjsonをコントローラに対してpostするとcsvが返ってくるのでそれを全てのレポートに対しておこないファイルに書き出すということをおこないます。 数値検証はリリースのたびに実行する性格上Rakeタスクとして実装しました。
但し、通常のRakeタスクでは行わない以下のことをあえてやる必要があります。

  • Rakeタスクからコントローラに直接jsonをpostする
  • Deviseによる認証をスキップする

これだけ書くとやばいことをやってる感がありますがライブラリで用意されている機能を使うだけなのでご安心ください。
但し、テスト環境前提の機能なので本番環境等で使うことはおやめください

言葉で説明するよりコードを見たほうが早いと思うので実際のコードを元に簡略化したコードを載せます。 省略して書きましたが大体以下のような処理を行っています。

# wardenのテストモードを使う
# これでテスト用のヘルパーが使えるようになる
def use_warden_test_mode!
  Warden.test_mode!
  # CSRF保護設定を外す
  ApplicationController.allow_forgery_protection = false
  yield
  # rakeのプロセスに閉じているので変更しても影響は無いはずだが念の為元に戻す
  ApplicationController.allow_forgery_protection = true
  Warden.test_reset!
end

# リクエストするユーザーを偽装する
def next_request_user(user)
  Warden.on_next_request do |proxy|
    # Deviseを使っているモデルのインスタンスからスコープを取得する
    scope = Devise::Mapping.find_scope!(user)
    opts = { event: :authentication, scope: scope }
    # 認証されたユーザーを設定する
    proxy.set_user(user, opts)
  end
end

use_warden_test_mode! do
  companies.each do |company|
    # スキーマを切り替える
    company.exec_in_tenant do
      # 全てのレポートを閲覧できるユーザーでリクエストを投げる
      next_request_user(User.find_by(role: :owner))
      session = ActionDispatch::Integration::Session.new(Rails.application)
      status = session.post(url, params: parameter_hash, as: :json)
      # ファイルに書き出す
      write! session.response.body
    end
  end
end

それでは普段やらないであろう箇所について、簡単にどうやっているのかを補足します。

Rakeからコントローラを直接叩くにはActionDispatch::Integration::Sessionを使います。

api.rubyonrails.org

これでsessionを偽装し、コントローラに直接postすることができます。

session = ActionDispatch::Integration::Session.new(Rails.application)
status = session.post(url, params: parameter_hash, as: :json)
# ファイルに書き出す
write! session.response.body

Deviseの認証をスキップする方法はRakeの認証ミドルウェアであるWardenのtest_mode!を使います。

www.rubydoc.info

名前の通りminitestやrspecなどのテスト環境で使うメソッドですが、今回はRakeタスクで使います。 このメソッドを使うことでWarden.on_next_requestを始めとした様々なテスト用のヘルパーメソッドを使用することができます。 このon_next_requestを使用してリクエストを投げるユーザーを設定することで認証をスキップできます。 scopeやDevise::Mappingが何かという話はDeviseの仕様に踏み込むので今回は説明を割愛します。

# リクエストするユーザーを偽装する
def next_request_user(user)
  Warden.on_next_request do |proxy|
    # Deviseを使っているモデルのインスタンスからスコープを取得する
    scope = Devise::Mapping.find_scope!(user)
    opts = { event: :authentication, scope: scope }
    # 認証されたユーザーを設定する
    proxy.set_user(user, opts)
  end
end

これでRakeタスクを実行すれば、レポートをCSVに出力することができるようになりました🎉
この数値検証の自動化はすぐに威力を発揮し、バグの検出に役立ちました。

次回予告

今回はDIGGLEに数値検証を導入した経緯とその詳細をコードを交えてご紹介しました。 この数値検証を実行している環境がどうなっているかについては次回弊社の優秀なインフラエンジニアがご紹介いたしますのでご期待ください。

We're hiring!

今あるものを更により良くするための方法を、我々と一緒に模索してくれる開発メンバーを募集しています!少しでも興味があれば、ぜひ下記採用サイトからエントリーください。

open.talentio.com

Meetyによるカジュアル面談も行っていますので、この記事の話をもっと聞きたい!という方がいらっしゃいましたら、お気軽にお声がけください。

meety.net

Rails+ApartmentにPostgreSQLのプロシージャを導入して多軸分析の集計速度を向上させた話 その5 ~apartmentへのPR編~

こんにちは。DIGGLEエンジニアのzakkyです。

遂に今回で最終回となります。レポート(多軸分析)の集計処理部分のパフォーマンス向上施策の第5弾です。

前回までの記事の紹介

diggle.engineer diggle.engineer diggle.engineer diggle.engineer

今回のお題

今回はapartmentに対してPRを出すに至ったあれやこれやをご紹介します。

書かないこと

以下の内容は含みませんので予めご了承ください

  • Rails、Apartmentの導入などの基礎的な部分
  • apartmentの説明

どれくらい速くなったのか

前回も記載しましたが、以下が今回改善した集計処理部分のLatency推移となります。 リリース後には低下していることが見て取れるかと思います。

大体3倍速程度には改善することができました。

赤線の辺りで今回の改善版のリリースを行っています

publicスキーマに対する参照があるプロシージャが動かない

ここでまた別の問題が発生しました。

以下のようなプロシージャがあるとします。(サンプルのSQLなので「User.allで取れるじゃん!」とは言わない約束です)

BEBIN
RETURN QUERY SELECT * FROM public.users;
END;

※前後のプロシージャ関連の定型文は省略しています

new_tenantテナントを作って、こちらのプロシージャを見ると…

BEBIN
RETURN QUERY SELECT * FROM new_tenant.users;
END;

と、publicであるべき箇所が、new_tenantとなってしまう問題が発生しました。

今回も、最終的な変更内容については結論に記載していますので、結果だけ知りたい方は最下部を参照ください。

原因を探る

またライブラリ(今度はApartment)を読む時間です。 偶然ですが、以前に余談として書いた部分にヒントがありました。

テナント作成時の流れを追う

重要な部分だけ抜き出してみました。 import_database_schemaはテナント新規作成時に呼び出され、その中のcopy_schema_migrationsでは、pg_dumpをした上でpatch_search_pathというfunctionを呼び出しているようです。

      def import_database_schema
        preserving_search_path do
          clone_pg_schema
          copy_schema_migrations
        end
      end

      def copy_schema_migrations
        pg_migrations_data = patch_search_path(pg_dump_schema_migrations_data)
        Apartment.connection.execute(pg_migrations_data)
      end

      def pg_dump_schema_migrations_data
        with_pg_env { `pg_dump -a --inserts -t #{default_tenant}.schema_migrations -t #{default_tenant}.ar_internal_metadata #{dbname}` }
      end

      def patch_search_path(sql)
        search_path = "SET search_path = \"#{current}\", #{default_tenant};"

        swap_schema_qualifier(sql)
          .split("\n")
          .select { |line| check_input_against_regexps(line, PSQL_DUMP_BLACKLISTED_STATEMENTS).empty? }
          .prepend(search_path)
          .join("\n")
      end

code link

patch_search_pathを追う

一体どんなことをしているのかと開けてみると、pg_dumpで取得した文字列に対してdefault_tenantに当たる部分をreplaceしていることが分かります。

      def patch_search_path(sql)
        search_path = "SET search_path = \"#{current}\", #{default_tenant};"

        swap_schema_qualifier(sql)
          .split("\n")
          .select { |line| check_input_against_regexps(line, PSQL_DUMP_BLACKLISTED_STATEMENTS).empty? }
          .prepend(search_path)
          .join("\n")
      end

      def swap_schema_qualifier(sql)
        sql.gsub(/#{default_tenant}\.\w*/) do |match|
          if Apartment.pg_excluded_names.any? { |name| match.include? name }
            match
          else
            match.gsub("#{default_tenant}.", %("#{current}".))
          end
        end
      end

code link

どうしたら良いか考えてみる

pg_dumpを行った結果を見てみると分かるのですが、create文にもschema名が記述されているため、単純にswap_schema_qualifierを行わないようにすることはできません。 かと言って、「プロシージャ内のロジックだけ置換を行わない」といった事は難しそうです。 プロシージャ内のtable名を動的に生成しEXECUTEで呼び出す方法もありますが、動的にSQL文を生成する分、性能劣化が発生するため今回は使えません。

と思いつつ、ソースを眺めていると、以下のようなコメントアウトされたロジックが目につきました。

      def pg_dump_schema
        # Skip excluded tables? :/
        # excluded_tables =
        #   collect_table_names(Apartment.excluded_models)
        #   .map! {|t| "-T #{t}"}
        #   .join(' ')

        # `pg_dump -s -x -O -n #{default_tenant} #{excluded_tables} #{dbname}`

        with_pg_env { `pg_dump -s -x -O -n #{default_tenant} #{dbname}` }
      end

code link

Apartmentの仕様をもう一度考える

Apartmentとしてどのように動くのが正しいのかを考えてみます

  • excluded_tablesに記載してあるテーブルについては、publicスキーマ上で管理したいテーブルである
  • publicスキーマ以外にもexcluded_tablesが存在するのは、差分が出ないようにするためで、必須ではない

上記であると考えると、コメント部分を復活させて微調整してあげれば解決しそうです。

解決案

以下のようにすれば解決する筈です。

  • テナント追加時に作成するスキーマにexcluded_tablesは存在する必要はないので、pg_dump時の対象から除く
  • swap_schema_qualifierで置換する際に、default_tenant + excluded_tablesの組み合わせだったときは置換させないようにする
    • 事前にpg_dumpの対象からexcluded_tablesに記載のテーブルは除外しているため、プロシージャ内のロジックでのみ出てきます

結論

モンキーパッチを書く

forkしたリポジトリを運用する方法もありますが、今後のバージョンアップに付いていくのが辛いのと、そこまで大きな改修ではないので、今回はモンキーパッチとします。

module PostgresqlSchemaFromSqlAdapterMonkeyPatch
  private

  def pg_dump_schema
    exclude_table = excluded_tables.map! { |t| "-T #{t}" }.join(' ')
    with_pg_env { `pg_dump -s -x -O -n #{default_tenant} #{dbname} #{exclude_table}` }
  end

  def swap_schema_qualifier(sql)
    sql.gsub(/#{default_tenant}\.\w*/) do |match|
      if Apartment.pg_excluded_names.any? { |name| match.include? name }
        match
      elsif excluded_tables.any?(match)
        match
      else
        match.gsub("#{default_tenant}.", %("#{current}".))
      end
    end
  end

  def excluded_tables
    Apartment.excluded_models.map do |m|
      m.constantize.table_name
    end
  end
end

require 'apartment/adapters/postgresql_adapter'
Apartment::Adapters::PostgresqlSchemaFromSqlAdapter.prepend(PostgresqlSchemaFromSqlAdapterMonkeyPatch)

PRを出してみる

今回はそこそこ修正量もあったので、せっかくなのでPull Requestを出してみようと思います。 かくいう私も初めてPull Requestを投げるのでドキドキしながら行ったのですが、以下を参考にしました。

  • Contributingの内容を確認する
    • 大前提ですのでしっかり読んでおきます
  • OSSへPull Requestを投げてみた各種サイト(「oss pull request」とかでググると色々出てきます)
    • 読みやすくまとめてくださっているサイトが色々ありますので、参考にさせていただきました
  • DeepL
    • 英語が得意ではないので、大活躍していただきました

Pull Requestの内容がニッチすぎるせいか、未だに何の反応も無いのが悲しい限りですが、OSSのライブラリに手を加えてPull Requestを投げるまでの一連の流れを体験できたので個人的に良い経験になったな。と、思っています。

最後に

全5回にわたってここまでお付き合いいただきありがとうございました。

次回からは、また別の題材で記事を書きますので(私ではない別のエンジニアになる予定)、ご期待ください。

We're hiring!

DIGGLEでは、必要に応じてライブラリのソースを読み解いて問題解決に挑む開発メンバーを募集しています!少しでも興味があれば、ぜひ下記採用サイトからエントリーください。

open.talentio.com

Meetyによるカジュアル面談も行っていますので、この記事の話をもっと聞きたい!という方がいらっしゃいましたら、お気軽にお声がけください。

meety.net

Rails+ApartmentにPostgreSQLのプロシージャを導入して多軸分析の集計速度を向上させた話 その4 ~Rails + plv8編~

こんにちは。DIGGLEエンジニアのzakkyです。

今回でレポート(多軸分析)の集計処理部分のパフォーマンス向上施策の第4弾となります。そろそろゴールが見えてきました。

前回までの記事の紹介

diggle.engineer diggle.engineer diggle.engineer

今回のお題

前回はRails+ApartmentでRails Wayに乗っかりつつPL/pgSQLを導入しましたので、今回は同じ流れでRails+ApartmentでRails Wayに乗っかりつつplv8を導入しようとして一筋縄では行かなかったあれやこれやをご紹介します。

書かないこと

以下の内容は含みませんので予めご了承ください

  • Rails、Apartmentの導入などの基礎的な部分
  • PostgreSQLのプロシージャに関する説明
  • plv8に関する説明

どれくらい速くなったのか

前回も記載しましたが、以下が今回改善した集計処理部分のLatency推移となります。 リリース後には低下していることが見て取れるかと思います。

大体3倍速程度には改善することができました。

赤線の辺りで今回の改善版のリリースを行っています

plv8を導入する

いつも通り早速本題に入ります。

前回はPL/pgSQLをRails Wayで管理する所まで説明しましたので、今回はplv8でも同様にRails Wayに乗っかって管理できるようにしてあげようと思います。 最終的な変更内容については結論に記載していますので、結果だけ知りたい方は最下部を参照ください。

localでplv8を動かす

前段の話にはなりますが、そもそもlocalでplv8が入ったPostgreSQLを用意できないと辛い。ということで、Dockerイメージを用意します。

残念ながらPostgreSQLのDockerイメージにはplv8が入っていないので自前で作る必要があります。 基本的にはplv8のドキュメントを見ながらDockerfileを書くだけの簡単なお仕事ですので、そこまで難しい内容ではありません。

参考までに、私が作ったものを下記に置いておきます。

github.com

db:resetでplv8を適用する

plv8はCREATE EXTENSION plv8を実行してあげる必要があるのですが、structure.sqlに上記内容が含まれないため、db:resetを行った際にplv8が存在しないと怒られてしまいます。

Apartmentのドキュメントに沿って修正する(上手くいかない)

以下のREADMEに沿って修正しても、test側でエラーとなってしまいます。 github.com

test側でエラーになる原因を探る

pg_dumpの仕様として、--schemaオプションを付けてるとExtensionはexportされない

要約すると上記の通りとなります。

Apartmentを使っている以上、前回の記事でpublicテナントだけをダンプ対象にする方法として説明した通り、--schemaオプションを付けない道はありません。また、--schemaではなく--exclude-schemaを付ける事でも解消できるとの記載がありますが、schemaがどんどん増えるapartmentとの相性は考えるまでもなく最悪でしょう。

詳しい内容を知りたい方は下記issueが参考になるかと思います。

github.com github.com

ライブラリのソースを追いかける

上記の時点で、かなり詰んでる気がしますが、まだもう少し頑張ります。

ここからは、db:resetの仕様を把握して何とかできる糸口がないかを探していきます。

ActiveRecordのdb:resetの仕様を読み解く

ソースを読み進めていくと、大きな流れとして、db:drop -> db:create -> db:schema:loadの順に動いているようです。

  task reset: [ "db:drop", "db:setup" ]

  namespace :setup do
    task all: ["db:create", :environment, "db:schema:load", :seed]
    ...(略)...
  end

code link

db:createやdb:schema:loadを読み解く

下記に抜粋した通り、db:createやdb:schema:loadのロジックの中で、database全体に対してループしながらcreateやloadを行っていることが分かりました。今回行いたいこととしては、そのロジックの中でextensionの追加をしてあげる必要があります。

先ほどのApartmentのREADMEで指定された方法では、db:createの後にextensionの追加を行っており、この方法ではデフォルト(≒先頭)のデータベースに向けてしか実行できないことが分かります。(つまり2つ目のデータベースであるtest側のデータベースへは適用できずに今回の問題が発生する)

db:create

      def create_all
        old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name)
        each_local_configuration { |db_config| create(db_config) }
        if old_pool
          ActiveRecord::Base.connection_handler.establish_connection(old_pool.db_config)
        end
      end

code link

db:schema:load

    namespace :load do
      ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |name|
        ...(略)...
      end
    end

code link

もっとソースを読む

もう少し読み進めてdb:createのロジックを見てみると、↓のcreateの中でextensionを登録できれば何とかなりそうです。

each_local_configuration { |db_config| create(db_config) }

code link

結論

最終手段ではありますが、以下のようなモンキーパッチを当ててあげることで問題を解決しました。

module PostgreSQLDatabaseTasksMonkeyPatch
  def create(*args)
    super(*args)
    ActiveRecord::Base.connection.execute 'CREATE EXTENSION IF NOT EXISTS plv8;'
  end
end
ActiveRecord::Tasks::PostgreSQLDatabaseTasks.prepend(PostgreSQLDatabaseTasksMonkeyPatch)

We're hiring!

DIGGLEでは、ライブラリのソースを読んで必要な時にはモンキーパッチも書く開発メンバーを募集しています!少しでも興味があれば、ぜひ下記採用サイトからエントリーください。

open.talentio.com

Meetyによるカジュアル面談も行っていますので、この記事の話をもっと聞きたい!という方がいらっしゃいましたら、お気軽にお声がけください。

meety.net

Rails+ApartmentにPostgreSQLのプロシージャを導入して多軸分析の集計速度を向上させた話 その3~Rails + PL/pgSQL編~

こんにちは。DIGGLEエンジニアのzakkyです。最近、長女、次女&三女(双子)がそれぞれ高校、中学校へ入学しました。可愛かったです。

いよいよレポート(多軸分析)の集計処理部分のパフォーマンス向上施策の第3弾となります。

前回までの記事の紹介

diggle.engineer diggle.engineer

今回のお題

今回はRails+ApartmentでRails Wayに乗っかりつつPL/pgSQLを導入した際のあれやこれやをご紹介します。plv8については、今回以上に大変だったので別記事として次回お話しする予定となっています。

書かないこと

以下の内容は含みませんので予めご了承ください

  • Rails、Apartmentの導入などの基礎的な部分
  • PostgreSQLのプロシージャに関する説明

どれくらい速くなったのか

前回も記載しましたが、以下が今回改善した集計処理部分のLatency推移となります。 リリース後には低下していることが見て取れるかと思います。

大体3倍速程度には改善することができました。

赤線の辺りで今回の改善版のリリースを行っています

Railsでプロシージャを呼び出すために具体的にどうしたのか

もう3回目ですので、早速本題へ行きます。 Railsからプロシージャを呼び出すためには、本当に色々なハードルがありました。

※プロシージャの導入に至った詳しい経緯を知りたい方は前回までの記事を参照いただけますと幸いです

プロシージャをRails Wayで管理する

大前提となります。今後のメンテナンス性を考え、あくまでRails Wayに乗っかった形でプロシージャを管理していく方法を考えます。

最終的な変更内容については今回やった内容のまとめに記載していますので、結果だけ知りたい方は最下部を参照ください。

プロシージャをRailsの管理下に置く

Railsガイドに記載されている通り、ダンプフォーマットをsqlへ変更することで解決できます。

railsguides.jp

publicテナントだけをダンプ対象にする

Apartmentを使っていてダンプフォーマットをsqlへ変更した場合、db:migrateを実行すると、structure.sqlに全テナント分のテーブルがダンプされてしまう問題が発生します。

こちらについてはダンプする対象をpublicにすることで回避できます。

db:resetを動くように修正する

schema_search_pathが指定されていてダンプフォーマットをsqlへ変更している場合、以下のissueにある通りdb:resetが動きません。解決策としては、issue中に記載のある通り、structure_dump_flagsを指定することで回避できます。

github.com

Apartmentの新規テナント作成時にプロシージャを適用する

以下のREADMEに記載されている通り、use_schemasとuse_sqlを適用することで新規テナント作成時にプロシージャが適用されます。

github.com

余談

上記READMEを読んでいると気になる一文があります。

this option doesn't use db/structure.sql, it creates SQL dump by executing pg_dump

ソースコードを追いかけると、確かにstructure.sqlを使わずに、pg_dumpをApartmentの中で実行しています。(次回以降にお話ししますが、ここの辺りで問題が発生してapartmentに対してPRを出すことになりました)

db:migrateでプロシージャを登録する

ここでようやくプロシージャをRailsで登録できるようになります。

シンプルに以下で実行可能です。

  def change
    execute <<~SQL
      CREATE PROCEDURE sample_procedure()
      AS $function$

      BEGIN
      -- 処理を書く
      END;

      $function$ LANGUAGE plpgsql;      
    SQL
  end

プロシージャの管理方針を考える

今回作成するプロシージャは弊社の根幹のロジックであり、今後のレポート拡充などやパフォーマンス改善で更に手を加えることが既に見えています。そして、改修の都度migrationを書いていては、どこにどのプロシージャがあるかを確認できず、管理が非常に煩雑になる未来が見えます。

上記などを考慮した結果、弊社ではプロシージャのソースコードを管理しやすいようにプロシージャ用のディレクトリを作成して、その中でプロシージャを管理するようにしました。

このようにmigrationを実行するイメージです。

def change
  execute File.read('db/procedure/sample_procedure.sql')
end

ただ、そうすると、migrationを書き忘れたりlocalのdb:migrateの実行タイミング次第では登録されるプロシージャの内容が個々人で変わったりなど、色々な危険があるのではないかと気付き、最終的には、db:migrateを実行する度にプロシージャの最新ソースを登録するようなrakeを別途作成することとしました。

lib/tasks/procedure.rake
def install_all_procedures
  Dir.glob(Rails.root.join('db/procedure/*.*')).each do |file|
    ActiveRecord::Base.connection.execute File.read(file)
  end
end

task 'db:procedure' => :environment do
  puts 'Re-install procedures..'
  install_all_procedures

  Apartment::Tenant.each do
    puts "At Tenant: #{Apartment::Tenant.current}"
    install_all_procedures
  end

  if ActiveRecord::Base.dump_schema_after_migration
    Rake::Task['db:schema:dump'].invoke
  end
end

Rake::Task['apartment:migrate'].enhance do
  Rake::Task['db:procedure'].invoke
end
rakeの中身の説明

簡単にrakeファイルの仕様を解説をします。

まず、Apartmentの仕様として、db:migrateを実行すると内部でapartment:migrateが実行されます。 今回の場合、apartmentのmigrationが終わった後にプロシージャの登録を行いたいので、Rake::Task['apartment:migrate'].enhanceとしています。

install_all_procedures周りの処理は全テナント(publicスキーマ含む)に対してdb/procedure/配下の全ファイルを実行していくだけなので割愛するとして、Rake::Task['db:schema:dump'].invokeについては、上記のプロシージャ登録をstructure.sqlの取得タイミングの後に実行しているため、再度dumpしてあげる必要があり、このような記述となっています。

結果として、db:migrateを実行するとdb/procedure/配下の全ファイルが常に実行されるようなrakeファイルとなっています。

つまり、db:migrateを行うたびに毎回procedure配下のファイルが実行されてしまうため、各プロシージャの先頭ではDROP-CREATEの形での記述をしてあげる必要があります。

DROP FUNCTION IF EXISTS sample_function;

CREATE FUNCTION sample_function()
--以下略

登録したプロシージャを呼び出す

プロシージャの呼び出し自体は非常に簡単です。

# procedure
ActiveRecord::Base.connection.execute('CALL sample_procedure()')

# function
ActiveRecord::Base.connection.execute('SELECT sample_function()')

※パラメータを渡す際にはsanitize_sqlなどを行う必要がありますので、ご注意ください。

今回やった内容のまとめ

以下のような形となりました。

application.rb
  class Application < Rails::Application
    config.active_record.schema_format = :sql
    config.active_record.dump_schemas = 'public'
    # MEMO: db:resetが動くように調整
    # NOTE: https://github.com/rails/rails/issues/38695
    ActiveRecord::Tasks::DatabaseTasks.structure_dump_flags = ['--clean', '--if-exists']
  end
apartment.rb
Apartment.configure do |config|
  config.use_schemas = true
  config.use_sql = true
end

We're hiring!

DIGGLEでは、必要に応じてライブラリのソースを読み解いて問題解決に挑む開発メンバーを募集しています!少しでも興味があれば、ぜひ下記採用サイトからエントリーください。

open.talentio.com

Meetyによるカジュアル面談も行っていますので、この記事の話をもっと聞きたい!という方がいらっしゃいましたら、お気軽にお声がけください。

meety.net

Rails+ApartmentにPostgreSQLのプロシージャを導入して多軸分析の集計速度を向上させた話 その2 ~プロシージャ導入編~

こんにちは。DIGGLEエンジニアのzakkyです。最近、長男が1歳になり、長女が中学卒業、二女&三女が小学校卒業とイベントが盛りだくさんでした。毎日家の中がにぎやかです。

さて、今回も前回から始まったレポート(多軸分析)の集計処理部分のパフォーマンス向上施策の第2弾となります。

前回までの記事の紹介

diggle.engineer

今回のお題

今回はPostgreSQLのプロシージャ導入時のあれやこれやをご紹介します。

書かないこと

以下の内容は含みませんので予めご了承ください

  • プロシージャの書き方などの基礎的な部分
  • PostgreSQLのパラメータを使ったチューニング方法

どれくらい速くなったのか

前回も記載しましたが、以下が今回改善した集計処理部分のLatency推移となります。 リリース後には低下していることが見て取れるかと思います。

大体3倍速程度には改善することができました。

f:id:zakky21:20220224115950p:plain
赤線の辺りで今回の改善版のリリースを行っています

実装方針を決める

前回のおさらいとなりますが、前回は「単純にSQLを発行するだけのような部分はPL/pgSQLで行い、計算式の値の算出のような複雑な処理はplv8を使うハイブリッド方式を採用する」所まで決定しました。

今回は、具体的にどの部分をPL/pgSQLで書いて、どの部分をplv8にするかを決めて実装していきます。

PL/pgSQLとplv8に振り分ける

実際に行う処理をいくつかに分解し、複雑度によってPL/pgSQLとplv8に振り分けていきます。

計算処理の流れを確認する

こちらも前回のおさらいとなりますが、改善箇所は「登録されているデータをレポート用に加工する」箇所の高速化となります。弊社のサービス上では大きく以下のような流れで処理が構成されています。

  1. 計算式のパース
  2. データの集計&集約
  3. 計算式部分の計算

それぞれの処理の複雑度などを考えて振り分けを行っていきます。

計算式のパース

jsのライブラリであるpegjsで生成したjsを使ってパース処理を行っていますので、複雑度云々ではなく、jsが実行可能な環境が必要となります。

plv8がnodeベースの言語となりますので、plv8で実装します。

データの集計&集約

単純にSQLによる集約ができれば良い部分となります。

シンプルにSQLを書けば良いので、PL/pgSQLで実装します。

計算式部分の計算

一番複雑な部分になります。それぞれの値の相互関係や四則演算などを表現できないといけません。また、今後計算式機能の拡充をしようとした際のメンテナンス性も重要になります。

PL/pgSQLでは荷が重いので、自由度が高いplv8で実装します。

最終的な振り分け結果

上述までの検討の結果、以下の様に振り分けを行いました。

  1. 計算式のパース ⇒ plv8
  2. データの集計&集約 ⇒ PL/pgSQL
  3. 計算式部分の計算 ⇒ plv8

プロシージャへの置き換え

方針も決まりましたので、それぞれの処理をプロシージャに置き換えて高速化を行っていきます。

計算式のパース

前述の通り、jsのライブラリであるpegjsで生成したjsを使っていますが、元々はrails上でjsを実行していました。そのため、お世辞にも処理が速いとは言えず、都度パースするのではなく、DB上にパース後の内容を保存しておくなど、極力パース処理が走らないような工夫を行っていました。

それが今回、pegjsで生成したjsをnodeベースであるplv8上で動かすことでかなり速度が向上しました。そのため、「パース処理の仕様を変えたから、全ての計算式をパースし直したい」のような時にも以下の様にSQL上からも呼び出すことで、今までは数分単位で掛かっていたものが1~2秒で終わるようになりました。

-- formula_tableの全ての計算式(formula)を再度パースしてparsed_jsonに格納する
UPDATE formula_table
SET parsed_json = parse_formula(formula);

※実際には計算式変更後の初回時のみパース⇒DBへ保存する仕様のため、最初の画像のLatencyに対する恩恵はほぼほぼ得られていません。

データの集計&集約の高速化

以前は同様の内容をRails側で取得⇒メモリ上に展開していたので、RDBとのI/Oや取得したデータの展開などのオーバーヘッドが掛かっていました。 今回はPL/pgSQLでSELECT-INSERTを用いてTEMPORARY TABLEへそのまま集計&集約しつつ投入する仕組みにしたため、オーバーヘッドを大幅にカットすることができ、大体1秒程度あればデータの集計ができるようになりました。

計算式部分の計算

今回の目玉です。

元々Railsで記載されていたものをplv8(node)へ置き換える作業となるのですが、試行錯誤の連続でした。

TEMPテーブルを活用する(失敗)

最初に実装した際には、計算式で参照する値がある度に、TEMPテーブルから都度参照する仕組みとしました。

理由としては、TEMPテーブルはオンメモリに展開されている筈で、TEMPテーブルへのSELECTは実体としてはRails上のMapなどに対する参照とコストが大差なく、そこまで大きくオーバーヘッドは掛からないだろう。という推測からでした。

しかし残念ながら結果としては、Rails時より速度低下する事態となってしまいました。

後で調べてみて分かったのですが、以下の記述があるように、どうやらTEMPテーブルもファイルへ保存しているようです。TEMPテーブルから取得したからいくらSELECTしても大丈夫!という訳ではなく、どこかしらでHDDへのI/Oが走ってしまい性能劣化へ繋がったのではないかと推測しています。

しかし一時的なリレーションでは、ファイル名はtBBB_FFFという形になります。

www.postgresql.jp

PostgreSQLのパラメータをチューニングする(失敗)

私よりDBに詳しい方が色々なサイトで纏めてくださっているので詳細は割愛しますが、以下のようなパラメータを調整してみました。

  • shared_buffer
  • temp_buffers
  • work_mem

色々な値を入れて試してみたのですが改善せず、今回は時間の都合で断念となりました😢

www.postgresql.jp

考察:plv8でSQL発行するのは良くないのではないか

上記の調査をしている中で気づいたことなのですが、体感でplv8では各種SQLの発行が遅いように感じました。 特に更新系の処理が顕著で、明らかに遅いと感じられる状況となりました。

後になって思い返してみると、計算式の計算が終わる度にTEMPテーブルへ計算結果をUPDATEする仕組みがあったため、UPDATEのコストが大きかった可能性もあります。(他の計算式から参照される可能性があるため、集計結果は保存しておく必要があるのです)

また、今回plv8で記述した内容を、頑張ってPL/pgSQLで書くようにすれば違った結果になった可能性があります。(ただし、メンテナンス性がかなり落ちるため、今回は採用しませんでした)

最終案:オンメモリに全展開する

最終的には、Railsでやっていたことと同じく、オンメモリに必要な情報を全て展開してから処理を行う。という形となりました。当たり前ですが、RailsでやっていたことをRDB上で実現しましたので速度の向上が見られました。

大まかな処理の流れとしては以下のようになります。

  • TEMPテーブルに対してSELECTを行い、集計結果をメモリ(Map)に展開する
  • 計算式で参照する値がある場合は、メモリ(Map)から参照する
  • 計算式の計算が終わったら、メモリ(Map)に結果を格納する

上記によって懸念されるのが、RDBのメモリが枯渇しないか。という点かと思います。

この問題に関しては、弊社では本番同等の環境&データを用いたパフォーマンス試験を実施する仕組みがありますので、リリース前にしっかりと本番想定の性能試験をすることが可能となっています。

そして、性能試験の結果としてはメモリ枯渇は発生せず問題なし。結果的にこちらが採用となりました。

まとめ

最終的に時間の都合で納得のいく形までは持っていけませんでしたが、3倍速程度には速くできたので、まぁ及第点かな。と。

ただ、計算式の計算部分のパフォーマンス改善や、PostgreSQLのパラメータチューニングなど、まだまだ改善の余地が残っていることは事実ですので、日を改めて再チャレンジをする予定です。

次回予告

「Rails+ApartmentでどうやってRails Wayに乗っかりつつプロシージャを管理するのか」という所を書きたいと思います。そちら側の方が個人的には大変だったので、ご期待いただければと思います。

本当は今回書きたかったのですが、長くなってしまった結果、泣く泣く分割となりましたので、来月までお待ちいただけますと幸いです。

We're hiring!

今あるものを更により良くするための方法を、我々と一緒に模索してくれる開発メンバーを募集しています!少しでも興味があれば、ぜひ下記採用サイトからエントリーください。

open.talentio.com

Meetyによるカジュアル面談も行っていますので、この記事の話をもっと聞きたい!という方がいらっしゃいましたら、お気軽にお声がけください。

meety.net

Rails+ApartmentにPostgreSQLのプロシージャを導入して多軸分析の集計速度を向上させた話 その1 ~技術選定編~

こんにちは。DIGGLEエンジニアのzakkyです。最近、長女の高校受験が終わって少し落ち着きました。

今回から何回かに分けてDIGGLEの肝となるレポート(多軸分析)の集計処理部分のパフォーマンス向上施策についてお話しします。

今回のお題

今回はPostgreSQLのプロシージャ導入に至るまでの技術選定部分をご紹介します。

レポート機能では何ができるのか

改善施策として何を行ったのかを語る前に、まずは背景として弊社のレポート機能を簡単に紹介させてください。

弊社のレポートは自由度が高く、大きく以下の様な機能があります。

  • 任意の計算式を作ることができる(Excelで計算式を作るようなイメージ)
  • 任意の軸を指定した分析ができる
  • 分析結果からドリルダウンができる

それぞれの機能の細かい内容までは掘り下げませんが、「サーバーへの負荷が凄いことになりそう」と感じていただければ幸いです。

diggle.jp

課題となっていたこと

ユーザーの状況

昨年あたりから、弊社営業・マーケ・CSの尽力で様々なお客様と契約いただき、それに伴い事業規模の大きなお客様にご利用いただく機会が増えてきました。

それ自体はとても嬉しいことなのですが、事業規模が大きいため、投入される分析用のデータ量も多くなり、取り扱うべきデータ量が以前と比べて飛躍的に増大する結果となりました。

結果として、よくある話ではあるのですが、パフォーマンス劣化が発生する事態となり、特に大量のデータを使って分析を行うレポート(多軸分析)では、処理時間が長くなってしまうという問題が発生していました。

ボトルネックとなっている箇所の洗い出し

弊社のサービスでは、レポート(多軸分析)を表示するまでの間に行う処理として大きく以下の3つがあります。

  1. 登録されているデータをレポート用に加工する
  2. レポート用に加工したデータをユーザーが指定した任意の軸・条件で取得する
  3. フロントエンド側で描画する

この中で、今回一番パフォーマンス低下が顕著だったのが「登録されているデータをレポート用に加工する」箇所となります。

他の2点に関しては、以前に改善施策を打っていたこともあり、パフォーマンス低下もそこまで顕著では無かったため、今回の改善対象とはしませんでした。

調査の際には、DatadogのAPMなどを使うことで、どこがボトルネックになるのかを視覚的に把握することができます。ご参考までにリンクを貼っておきます。

www.datadoghq.com

具体的にどこに問題があったのか

前述のDatadogのAPMや、サーバーのCPU/メモリなどを調査した結果、以下のような問題があることが分かりました。

  • RDBから取得したデータをRailsに取得するまでに以下のオーバーヘッドが掛かる
    • SQLの実行時間
    • ネットワークI/Oのオーバーヘッド(イントラネット内だとしてもデータ量が多いと問題になり得ます)
    • 取得した結果をRails上に展開するのにもオーバーヘッド
  • 扱うデータ量が増えるとメモリを圧迫し、CPU使用量が増える(スワップが起きていると推測)

なぜ問題が起こっているのか

弊社のサービスの特徴にも関係してくるのですが、前述の問題が起こる理由として以下のようなことがあります。

最初にも軽く触れましたが、弊社のサービスでは「任意の計算式を作ることができる」という機能があり、「登録されているデータをレポート用に加工する」際には、同時に計算式部分の値まで計算しておく必要があります。

そして、「任意の計算式を作ることができる」機能では、集計した結果を別の場所で参照し、更に別の場所で参照して・・・。といった表現が可能となります。

ここまで聞いただけで頭が痛くなってくるかと思いますが、つまり、集計する際は単純にSQLでgroup byするのではなく、それぞれの依存関係を見ながら計算していく必要があり、どうしても一度データをどこかに展開する必要が出てきます。

「だったら集計した結果だけ良い感じに残しておくようにすれば扱うデータ量自体は減るじゃないか!」と、思われるかもしれませんが、そちらも最初に触れた「分析結果からドリルダウンができる」ためには、一番深くまでドリルダウンした時の値まで保持しておく必要があり、簡単には取り扱うデータ量を減らすことができません。

今回行うべき事項は何なのか

ここまでで洗い出した問題点から、満たすべき要件と、改善すべき事項を導き出します。

満たすべき要件

  • 計算式の値を算出できる
  • ドリルダウンができる

改善すべき事項

  • 現在のI/Oと比べて同等か、もしくはそれ以上高速にRDBからデータを取得する
  • 大量のデータをメモリなどのI/Oが高速な場所へスワップが起こらないように負荷なく展開する

何を改善したのか

先ほどまでで課題の検討を行って何を行うべきかを導き出したので、ここからはどうやって実現するかを考えていきます。

今までの構成の延長で何とかならないか

今回の改善を行う前までは、「登録されているデータをレポート用に加工する」機能はRailsサーバー上で実行していました。

計算式の値を算出するためには、かなり複雑なロジックを記述する必要があり、バックエンドサーバーとして使っているRails上で実装するのが現実的である。という判断でした。

そのため、検討当初はRailsサーバー上で何とか改善できないかと検討もしたのですが、実は、高速化については私が入社した2019年当初より、継続的に検討&改善を行ってきた関係で、Rails上で行える改善はやり尽くしている感があり、これ以上の劇的な改善は難しいとの結論を出さざるをえませんでした。

そして結果として、Rails以外で解決する方法が無いかを検討することとなりました。

具体的にどうやって高速化するか

Rails以外にも裾野を広げて検討していく中で、大きく以下のような案が出ました。

  1. Rails以外の高速な言語でデータ集計用にサーバーを新規で立ち上げる
    • メリット 完全に独立した集計特化の構成を組める
    • デメリット どの言語を使うのかの選定が必要/劇的に改善するかはやってみないと分からない/導入までに時間が掛かる
  2. Redshift、BigQuery、Snowflakeなどを導入する
    • メリット 弊社が扱うデータ量であれば超高速に処理できる
    • デメリット RDBからデータを流すオーバーヘッドが掛かる
  3. RDB(PostgreSQL)上でRailsでやっていた内容を実行する
    • メリット 新しく何かを構築する必要が無い/同一サーバー上で処理が完結するのでネットワークI/Oをゼロにできる
    • デメリット PL/pgSQLを使って計算式の値を算出することは難しい

結果としては、上記のメリット/デメリットに加えて、開発工数、リスク、サービスとしての今後の見通しなど様々な要因を鑑みて、一番下の「RDB(PostgreSQL)上でRailsでやっていた内容を実行する」を選択することとなりました。

使用言語の策定

RDB(PostgreSQL)へ処理を移譲するという事で、真っ先に思いつくのがプロシージャ(PL/pgSQL)です。 ただ、前段でデメリットとして挙げた通り、計算式の値の算出のような複雑な処理をPL/pgSQLで書くのは、現実的ではありません。(プロシージャの開発経験がある方であれば、同意いただけるのではないでしょうか)

ということで、PostgreSQL内で実行可能な他の言語の調査を開始しました。

そんな中、nodeベースで記述できるplv8があることを知り、PL/pgSQLでは表現しづらい難しい部分はplv8で実現することにしました。

ただし、PL/pgSQLで書ける部分は極力書くことで速度は出したい。ということから、単純にSQLを発行するだけのような部分はPL/pgSQLで行い、計算式の値の算出のような複雑な処理はplv8を使うハイブリッド方式を採用しました。

最終的にどれくらい速くなったのか

今回プロシージャ化を行ってパフォーマンス改善を行いましたが、気になる改善結果は、というと・・・

以下が今回改善した集計処理部分のLatency推移となります。 リリース後には全体的に数値が低下していることが見て取れるかと思います。

大体3倍速程度には改善することができました。

f:id:zakky21:20220224115950p:plain
赤線の辺りで今回の改善版のリリースを行っています

次回予告

今回は技術選定部分のみにフォーカスを当てましたが、次回からは実際にプロシージャを使ってどのように改善したのかや、どうやってRails Wayに乗っかりつつプロシージャを管理したのか。といった細かい部分に踏み込んでいきたいと思います。

We're hiring!

今あるものを更により良くするための方法を、我々と一緒に模索してくれる開発メンバーを募集しています!少しでも興味があれば、ぜひ下記採用サイトからエントリーください。

open.talentio.com

Meetyによるカジュアル面談も行っていますので、この記事の話をもっと聞きたい!という方がいらっしゃいましたら、お気軽にお声がけください。

meety.net