DockerにFerretDBをデプロイする:PostgreSQL上に構築されたMongoDBと互換性のあるオープンソースデータベース

Database tutorial - IT technology blog
Database tutorial - IT technology blog

深夜2時の緊急呼び出し

午前2時17分、Slackにメッセージが届いた。「MongoDBがエラーを出してて、接続できない」。30分ほど調査した結果、原因は接続の問題ではなかった。小さなVPS上でセルフホストしていたMongoDBインスタンスがメモリ上限に達し、オンコールのエンジニアが再起動しようとして誤ってサービスを停止してしまったのだ。考えさせられたのは、インシデントそのものではなく、翌朝9時に交わされた会話だった。「そもそも、なぜMongoDBを使っているんだっけ?PostgreSQLがあるのに」

1週間調べ続けた末に、ある名前にたどり着いた。FerretDBだ。

これまで大小さまざまなプロジェクトでMySQL、PostgreSQL、MongoDBを使ってきた。それぞれに役割がある。MySQLはトランザクション処理向け

PostgreSQLは複雑なクエリと拡張機能を多用するスタック向け。MongoDBはプロジェクト初期に素早く動かしたいときの柔軟なスキーマ向け。ただし、MongoDBには現実的なコストが伴う。2018年のSSPL切り替え以降のライセンス問題だけでなく、小規模チームで独立したデータベースシステムを運用する際の運用オーバーヘッドも無視できない。FerretDBはその中間を取る。既存のMongoDBドライバーとツールをそのまま使いながら、データをPostgreSQLに移行できるのだ。

FerretDBとは何か

FerretDBはMongoDBのフォークではない。プロキシ、つまり変換レイヤーだ。一方ではMongoDBのワイヤープロトコルを話し、もう一方ではPostgreSQLと通信する。アプリケーションはMongoDBに接続するのとまったく同じ方法でFerretDBに接続できる。同じドライバー、同じ接続文字列、同じクエリが使える。内部では、FerretDBがMongoDBの操作をSQLに変換し、標準のPostgreSQLデータベースに対して実行する。

このデザインから生まれる具体的なメリットが2つある:

  • 100%オープンソースライセンス(Apache 2.0)。SSPLなし、ベンダーロックインの心配なし。
  • データはPostgreSQLに保存される。ACIDトランザクション、ポイントインタイムリカバリ、PostgreSQLの豊富な拡張エコシステムがすべて無料でついてくる。

変換の仕組み

db.users.find({age: {$gt: 25}})を実行すると、FerretDBはMongoDBワイヤープロトコルのメッセージを受信し、BSONクエリを解析して、JSONBカラムに対するPostgreSQLクエリを生成する。ドキュメントはJSONB行として保存される。コレクションはテーブルにマッピングされる。この変換はアプリケーションコードからは見えない。アプリはPostgreSQLと通信していることに気づかない。

FerretDBはまだ成熟段階にある。MongoDBとの完全な機能互換はまだ達成されていない。集計サポートは改善中だが、$facet$graphLookupのような演算子は完全には実装されていない。パフォーマンスもネイティブMongoDBのストレージエンジンとは異なる。ただし、シンプルなワークロードであれば安定して動作する。

DockerでFerretDBを起動する

必要なコンテナは2つだ。PostgreSQL(実際のストレージ)とFerretDB(ワイヤープロトコルの変換器)。私が実際にテストして使っているdocker-compose.ymlはこれだ:

version: '3.8'

services:
  postgres:
    image: postgres:16
    container_name: ferretdb-postgres
    environment:
      POSTGRES_USER: ferretdb
      POSTGRES_PASSWORD: secret123
      POSTGRES_DB: ferretdb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - ferretdb-net
    restart: unless-stopped

  ferretdb:
    image: ghcr.io/ferretdb/ferretdb:latest
    container_name: ferretdb
    environment:
      FERRETDB_POSTGRESQL_URL: postgres://ferretdb:secret123@postgres:5432/ferretdb
    ports:
      - "27017:27017"
    networks:
      - ferretdb-net
    depends_on:
      - postgres
    restart: unless-stopped

volumes:
  postgres_data:

networks:
  ferretdb-net:
    driver: bridge

スタックを起動する:

docker compose up -d

PostgreSQLが初期化されるまで約15秒待ってから、FerretDBの出力を確認する:

docker compose logs ferretdb

Listening on 0.0.0.0:27017というメッセージを探そう。それが接続を受け付けている合図だ。

mongoshで接続する

実際のMongoDBインスタンスに接続するのとまったく同じ方法で接続する:

mongosh mongodb://localhost:27017

ローカルにmongoshがない場合は、何もインストールせずにDockerから直接実行できる:

docker run --rm -it --network ferretdb-net mongo:6 mongosh mongodb://ferretdb:27017

接続したら、いくつかの操作を実行して動作確認しよう:

use testdb

db.servers.insertMany([
  { hostname: 'web-01', role: 'frontend', region: 'us-east' },
  { hostname: 'db-01', role: 'database', region: 'us-east' },
  { hostname: 'cache-01', role: 'cache', region: 'eu-west' }
])

db.servers.find({ region: 'us-east' })

db.servers.updateOne(
  { hostname: 'web-01' },
  { $set: { status: 'active' } }
)

db.servers.countDocuments()

これらはすべて動作する。では、PostgreSQLのシェルを開いて、データが実際にどこに保存されているか確認してみよう:

docker exec -it ferretdb-postgres psql -U ferretdb -d ferretdb
-- 生成されたテーブルを確認する
\dt

-- JSONBとして保存されたドキュメントをクエリする
SELECT * FROM testdb.servers_9c4bc50e LIMIT 5;

MongoDBのドキュメントがPostgreSQLテーブルのJSONB行として保存されている。実際に目にすると、なんとも不思議な満足感がある。

Pythonから接続する

既存のPyMongoコードは変更なしでFerretDBに接続できる:

from pymongo import MongoClient

client = MongoClient('mongodb://localhost:27017')
db = client['myapp']
collection = db['events']

collection.insert_one({
    'event': 'deployment',
    'service': 'api',
    'status': 'success',
    'timestamp': '2024-01-15T02:17:00Z'
})

for doc in collection.find({'status': 'success'}):
    print(doc)

同じドライバー。同じ接続文字列。同じクエリAPI。アプリケーションコードからはFerretDBの存在が見えない。

ハマりやすいポイント

認証:上記の設定にはMongoDBレベルの認証が設定されていない。ローカル開発以外では、FerretDBを公開ポートに晒さないようにしよう。プライベートDockerネットワークにバインドすることを推奨する。アプリケーション層で認証が必要な場合は、FerretDBのドキュメントに環境変数を使ったユーザー名/パスワードの設定方法が記載されている。

集計パイプラインのギャップ:コレクションをまたぐ$lookupや深くネストした$group操作などの複雑なステージは期待通りに動作しない可能性がある。移行を決める前に、使用している集計パイプラインをFerretDBでテストしよう。互換性を過信してはいけない。

インデックスはより重要:明示的なインデックスがないと、JSONBクエリが遅くなる可能性がある。MongoDBと同じ方法で作成しよう:

db.servers.createIndex({ region: 1 })
db.servers.createIndex({ hostname: 1 }, { unique: true })

これらはJSONBパスに対するPostgreSQL B-treeインデックスに変換される。このステップを省略してはいけない。インデックスなしでは、大きなコレクションでパフォーマンスが大幅に低下する

テーブル名:FerretDBはハッシュサフィックス付きのPostgreSQLテーブル名を生成する(例:servers_9c4bc50e)。監視やデバッグのためにPostgreSQLに直接クエリする場合は、SQLを書く前にpsqlで\dtを実行して実際のテーブル名を確認しよう。

FerretDBが適している場面

2つの社内サービスで9ヶ月間FerretDBを本番稼働させてきた経験から、以下のケースで使用を検討したい:

  • すでにPostgreSQLを運用しており、第二のデータベースシステムを維持するよりインフラを統合したいチーム
  • MongoDBのSSPLライセンスがコンプライアンス上または思想的な障壁となっているプロジェクト
  • 中程度のデータ量とシンプルなクエリパターンを持つ社内ツールやマイクロサービス
  • PostgreSQLの実績あるバックアップ・リカバリ・レプリケーションツールを活用しつつ、ドキュメントモデルを使いたい新規プロジェクト

MongoDB Atlas固有の機能、時系列コレクション、高度な全文検索、またはMongoDBのストレージエンジン向けに最適化された高スループットのワークロードに依存している場合は見送った方がいい。複雑なユースケースでのギャップは現実のものだ。

深夜2時のインシデントをきっかけに移行したあの2つのサービス?12ヶ月経っても問題なし。「なぜMongoDBを使っているんだっけ?」と聞いたエンジニアは今、あれをその年最良の偶発的なアーキテクチャの決断と呼んでいる。スタックを実際に改善した深夜2時の呼び出し。最悪の結末よりずっとよかった。

Share: