django-shardingを試してみた

Pocket

はじめに

django-sharding を試してみました。

最近までメンテも更新もされているしGitHubのスター数も他のシャーディングライブラリよりは多いです。
実際に動くサンプルプロジェクトはこちら。
DjangoShardingExample

Bookと言うモデルがUser毎に決められたシャード先DBへ保存されます。

気になったところ

透過的なシャーディングではないのである程度は開発者自身がアプリケーションレベルで記述を工夫する必要があります。
以前のバージョンでは透過的シャーディングの機能が少し入っていたようですが、著者の方針でそれらは削除されてしまったようです。
https://github.com/JBKahn/django-sharding/blob/master/CHANGES.md#500-november-30th-2019

このライブラリを試す前はSQLAlchemyのシャーディングライブラリを使っていました。
https://docs.sqlalchemy.org/en/13/orm/extensions/horizontal_shard.html
SQLAlchemyのシャーディングは透過的なシャーディングでしたので、そちらの使い方のイメージに引っ張られてしまい、かなり理解に苦しみました。

SQLAlchemyのシャーディングは1つSELECTを発行すると、(何も最適化しない場合)全シャード先DBをSELECTし、その結果をマージしてあたかも1つのDBからセレクトしたような結果を返します。
しかし、django-shardingは1つのクエリセットは1つのDBへしか発行できませんでした。

あと、ドキュメントが古くて正しくないです。
サンプルプロジェクトはかなりソースを読みながら実装しました。(おかげで?)Djangoについてかなり勉強できました。

現在メンテナンスが進んでいる気配がありますので、これからドキュメントだけでなく機能自体もどんどん改善されていきそうです。

パフォーマンスについては気になりますが、未検証です。

このライブラリを含め、「Django シャーディング」とかで検索しても、殆んど事例やサンプルコードが見当たらないんですが、使われてないんでしょうか…。

シャーディングの仕組み

コアな機能はドキュメントのComponentsの章を読むと良いと思います。

基本的な使い方は、

  • デフォルトのDBにシャードキーを持つテーブルを作る
  • シャーディングさせたいモデルはシャードキーを参照してシャード先DBを決定する
  • シャーディングさせたいモデルはPKが被らないように自動で生成されるIDフィールドを使用する

と言う感じです。

継承するModelだったり、付与するアノテーションなどでテーブルのカラムが変わったり、マイグレーションされるDBやデータが保存されるDBが変わったりと最初かなり混乱しましたが、
理解して使いこなせば便利そうです。

DB間をまたぐFKはDjango自体が対応していないため、その点を考慮してDB設計する必要があります。
うっかり、シャード先DBに保存したいモデルにデフォルトDBのモデルへのFKを書いたりすると、ライブラリが気を利かせてシャード先DBではなくデフォルトDBにデータを入れようとしてエラー、みたいな状況になってハマります。

Django Adminと共存させる

Django Adminとうまく共存させるために色々と工夫しました。
Django AdminはそのままですとデフォルトのDBにしかアクセスしないため、ModelAdminやdjango-shardingの機能を使って、うまく複数のDBにアクセスを振ってやる必要があります。

詳しくはこちら

前述しましたがdjango-shardingのクエリセットは複数のDBに対して実行できないようです。
なので、シャーディング先DB毎にモデルを分けることにしました。
(もっとマシな方法があればいいのですが。)

ここで困ったのが、admin.site.register()は同じモデルを登録できません。
なので、ModelのProxy機能を使用して、実体は同じモデルですが複数のモデルに見せかけ、registerしています。
また、シャード先DBが増えるとモデルの数が多くなり、定義を書くのも面倒になりそうなので、Dynamic Modelを使用しています。

# register Book with dynamic Model
# admin.site.register() can't register same model.
# https://dynamic-models.readthedocs.io/en/latest/
shards = get_shards(Book)
for shard in shards:
    name = 'Books ({})'.format(shard)
    BookProxy = type('BookProxy_{}'.format(shard), (Book, ), {'__module__': 'django_sharding_example.models', 'Meta': type('Meta', (object,), {'proxy': True, 'verbose_name': name, 'verbose_name_plural': name}), 'using': shard})

    admin.site.register(BookProxy, BookAdmin)

おわりに

結論としては実際のプロジェクトにも使えそうだと言う手応えがあります。(今のところ)
とても便利なライブラリで導入した場合のメリットはありますが、実践的なアプリを書いていくと他の問題にぶつかりそうな気もします。
とはいえ、やってみないとわからないのですね。勉強します。

(ドキュメントの修正くらい、自分も貢献した方が良いでしょうか、プルリク送ったりとか。)

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

*

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください