こんにちは。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
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
どうしたら良いか考えてみる
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
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では、必要に応じてライブラリのソースを読み解いて問題解決に挑む開発メンバーを募集しています!少しでも興味があれば、ぜひ下記採用サイトからエントリーください。
Meetyによるカジュアル面談も行っていますので、この記事の話をもっと聞きたい!という方がいらっしゃいましたら、お気軽にお声がけください。