マルチテナントサービスのDB選定(MySQLかPostgreSQLか?)

マルチテナントサービス、高速なMySQLと高機能なPostgreSQL、そしてRow Level Security(RLS)について


TL;DR

  • PostgreSQLにはRow Level Security(RLS)機能があるよ!
  • マルチテナント的なサービスでFail Safeな設計を実現できるよ!
  • 「クエリ間違えた」-->「センシティブな企業情報が誰でも閲覧可能」みたいな事を防げるよ!

前提知識

マルチテナントサービスとは?

「テナント」という用語は難しく感じてしまいますが、要は「組織」です。 複数組織に対応したサービスはマルチテナントに対応したサービスと言うこと出来ます。 そして「組織」を扱うサービスなのでB向けサービスであることが多いです。

例: クラウド人事労務サービスのSmartHRさん等

シングルテナントの例

もう少ししっかり理解するために、マルチテナントでは無いサービスを考えてみましょう。 例えばECサイトのAmazon等はどうでしょうか?(Amazonは法人向けサービスも提供しているようですが、ここでは簡略化してC向けサービスとします)

AmazonのサインアップフローはEメールとパスワードさえあれば誰でもサインアップできる仕様になっています。 また通常ログイン後も自分が何処かの組織に属していなければならないといった制限もありません。 ざっくりいうとこれはマルチテナントでは「ない」サービスです。

マルチテナントの例

上記と比較して、クラウド人事労務サービスのSmartHRさんについて考えてみましょう SmartHRさんは「人事労務サービス」と銘打ったSaasなため、当然「組織」を前提としています。

組織を前提としていることの証左としてログイン画面に「新しく企業アカウントを作成する」という文言が記載されています。 これは明らかにマルチテナント的なサービスです。 smart-hr-login

本題: マルチテナントサービスのDB選定(MySQLかPostgreSQLか?)

DBコネクションのコスト

本題に入ります。マルチテナントサービスのDBとしてMySQLかPostgreSQLのどちらが適しているでしょうか? おおざっぱに言ってMySQLはマルチスレッド型で軽量なRDBかと思います。 一方でPostgreSQLはマルチプロセス型で機能が豊富なRDB(ORDBMS)と理解して良いかと思います。

マルチスレッドがマルチプロセスかという違いが大きな影響を与えるのは新規コネクション数の生成に必要なコストです。 特に1コネクション当たりに必要なメモリ量はPostgresSQLでは10MB程度必要という説明を良く見かけます。

詳細なレポートの掲載は割愛しますが、PostgreSQLのコネクション生成コストが高いことの対策として、pgbouncerやpgpoolといったコネクションプーリング用のツールはある程度普及しているかと思います。

一方でMySQLのコネクションプーリングは相対的にPostgreSQLに比べて小さくWebシステムにおけるデータベース接続アーキテクチャ概論に記載されているように、当時のMobageはMySQLをコネクションプーリング無しで運用していたようです。

さて、DB選定は選定フローチャートに従えば答えが出るような簡単な話ではありません。 DBのパフォーマンスについてはMySQLとPostgreSQLのどちらが優れているかはサービスの性質によります。 ただし上記のMySQLはPostgreSQLに比べてコネクションプーリングを考えなくて良いと言う点は大きなメリットです。

高速なMySQLと高機能なPostgreSQL、そしてRow Level Security(RLS)

さて、前述のマルチテナント的なサービスについてはMySQLかPostgreSQLかを余り迷うこと無く、決め手となる一つのキーポイントがあります。 それはRow Level Security(RLS)と呼ばれる、MySQL(2022/6時点)には存在しないPostgreSQLだけが持つ機能です。

大雑把に言うとこの機能を使うことで、クエリの書き間違いをした場合でも、A社のテナントがB社のテナントの情報を閲覧したり編集したりすることを防止できます。

# 記載例
CREATE POLICY "tenant_policy" ON "table_name"
USING ("tenant_id" = current_setting('tenant.id'));

Row Level Security(RLS)を利用しない場合のリスク

さて、RLSを使用しないことはどれだけ危険なのでしょうか?

RLSを使用しない場合自前のコードだけで他のテナントへのアクセスを防止する必要があります。 例えばテーブルのマイグレーションに関わるPRが数年で100回発生した場合に100回全て、

  • アプリケーション側のクエリが完璧である
  • 上記で実装ミスしていたとしてもPRのレビューで防止できる

といった運用は100%可能でしょうか? 私の体感ではメンバーの力量にもよりますが、1回や2回以上はテストもレビューもすり抜けてしまうのではないでしょうか? そしてその結果、契約企業の情報が閲覧し放題になってしまったとすると、ビジネス的なダメージは深刻ではないでしょうか?

というわけで、マルチテナント対応なアプリの場合はRow Level Security(RLS)機能のあるPostgreSQLを選ぶのが無難かなぁというお話でした。

補足: Row Level Security(RLS)とコネクション

さてこのRLSですが以下の2つの値を比較した結果がTRUE / FALSEのどちらになるかが重要です

  • connectionの設定(set_configで指定)
    • 上記例ではcurrent_setting('tenant.id')
  • レコード内の特定のカラムの値
    • 上記例ではtenant_id

つまり、PostgreSQLをコネクションプーリングと共に運用している場合、 別テナントのユーザからのリクエストにも関わらず古いコネクションを再利用するような実装は避けなければなりません。

これを実現する最も簡単な実装は、クライアントからのリクエストの度にコネクションを張り直すことです。 NestjsではInjection scopesに記載されているScope.REQUESTで実現できます。 (set_configし直すことでも実現できますが、set_configしたコネクションが他のテナントに再利用されないことを担保する設計が必要です)

サーバフレームワークやORMの中には自動でコネクションプーリングしてくれる(有る意味してしまう)物が多いので注意が必要です。

補足: MySQL互換とPostgreSQL互換のDBについて

GCPやAWSにはMySQL互換とPostgreSQL互換のDBが用意されていますが、Row Level Security(RLS)がPostgreSQL互換でしか利用できないという点は同じかと思います。