orangain flavor

じっくりコトコト煮込んだみかん2。知らないことを知りたい。

Djangoでメモリに乗らないサイズのDBを扱うときに気をつける点

はじめに

Djangoでメモリに乗らないサイズのデータベースを扱うときに、気をつけるべきポイントをまとめます。メモリを大量に消費していつまで経っても処理が終わらなかったり、OOM Killerに殺されたりといった悲しい結末を回避できたら幸いです。

データ量としては、レコード数が数十万から数百万ぐらいで、サイズにして数GB〜十数GBぐらいのイメージです。インデックスを適切に張るといった、Django特有でないポイントは取り上げません。Djangoのバージョンは1.5系を対象にしています。

バッチ処理のDEBUGに気をつける

症状

DEBUG = Trueの場合、バッチ処理で大量のクエリを発行するとメモリを食いつぶすことがあります。

原因

実行したすべてのSQLが記録されるためです。defaultのデータベースを利用している場合は、django.db.connections['default'].queriesに実行されたSQLが記録されます。HTTPリクエストを処理する際に発行したクエリは、リクエストごとにリセットされますが、バッチ処理では自動でリセットされません。

対策

このように大量のクエリを発行する処理を行う場合はDEBUG = False にするか、適度にリセットしましょう。後述の restordbSouth のData Migrationを実行するときなども注意しましょう。

settings.pyで以下のようにして、DEBUGの値を環境変数で簡単に切り替えられるようにするのがおすすめです。

DEBUG = os.environ.get('DEBUG') == '1'

参考リンク

テーブル全件の処理に気をつける

症状

以下のようなコードでメモリを大量に消費してしまいます。

for blog in Blog.objects.all():
     # do something
for blog in Blog.objects.iterator():
     # do something

原因

all()を使うと確実に全件が取得され、メモリを食いつぶします。

iterator()を使うとメモリ消費が減りそうな気がしますが、データベースに接続するモジュールの実装によっては役に立ちません。少なくともMySQLPostgreSQLでは、iterator()はサーバーサイドのカーソルではなく、クライアント側のカーソルが使われます。クライアント側では一度全件が読み込まれるため、メモリの消費量は all() とほとんど変わりません。

対策

バッチ処理などで不用意に上記のようなコードを書かないようにしましょう。代わりに、1000件ずつなどチャンクに区切って取得します。具体的なコードは参考リンクをご参照ください。

他にも以下のようなことを気をつけることでメモリ消費や性能を改善できる場合があります。

  • できるだけPythonのコードではなくSQLで処理する。
  • QuerySetvalues(), values_list(), only()などを使う。

 参考リンク

dumpdataとloaddataに気をつける

症状

アプリケーションを作る際、最初はSQLiteを使い、ある程度の規模になったときにMySQLなど他のデータベースに移行することがあります。

このようなときに、データを manage.py dumpdata でエクスポートして、 manage.py loaddata でインポートする方法がありますが、 dumpdata / loaddata がメモリを大量に消費してしまいデータを移行できないことがあります。

原因

dumpdataiterator()を使って全件を取得するため、メモリを大量に消費します。 loaddata もファイルをすべてメモリに読み込んで処理するため、メモリに乗らないファイルを読み込めません。

1.5から dumpdata のJSON出力が改善されましたが、全件を取得するという根本的な問題は残ったままです。

対策

このようなときは、django-dumpdb *1 を使うと良いです。INSTALLED_APPS に追加すると dumpdb / restoredb というコマンドが使えるようになります。 dumpdb は、1レコードごとにJSONで出力してくれるため、メモリの消費が増大しません。

restoredbするときにはDEBUG=Falseにするのを忘れないようにしましょう。

参考リンク

 Adminサイトの外部キーに気をつける(リストページ)

症状

Adminサイトで、list_displayに外部キーを指定した場合、リストページの表示に時間がかかることがあります。

原因

list_displayに外部キーを指定した場合、select_relatedSQLのJOIN)が使われます。select_relatedで取得するテーブル数が増えると、データ量によっては取得に時間がかかります。

対策

この問題については、list_displayに含めないようにする以外に効果的な対策を知りません。

参考リンク

Adminサイトの外部キーに気をつける(詳細ページ)

症状

要素数が多いテーブルへのForeignKeyがある場合、Adminサイトの詳細ページの表示に非常に時間がかかることがあります。

原因

これは、選択肢がリストボックスで作られるためです。10万個の選択肢があるリストボックスとかゾッとしますね。

対策

この場合、以下のいずれかで対策ができます。通常は1で問題ないと思います。 raw_id_fields に含まれるフィールドは、リストボックスではなくidの数字をテキストボックスで指定できるようになります。

  1. ModelAdminでraw_id_fieldsにフィールドを指定する。
  2. ModelAdminでreadonly_fieldsにフィールドを指定する。
  3. ModelのFieldでeditable=Falseにする。

参考リンク

まとめ

これらのポイントは完全なリストではなく、私が遭遇したことのあるものだけです。新しく見つけたら追加していこうと思います。こんな問題もあるよとか、対策はこうしたほうがいいよなどありましたら教えていただけると助かります。

あとは全件処理がもう少し簡単に書けるようになると嬉しいですね。次のような議論はあるみたいですが。

*1:PyPIGoogle Codeにあるものよりも新しいのでこちらがおすすめです。