1. はじめに
Djangoのパフォーマンスを改善する方法はいくつかある。この記事では、最初にパフォーマンス改善前に実施するプロファイリングについて説明し、その後に以下4つの改善手法について紹介する。
対象読者
Python、またはDjangoに興味のある人
Djangoを使っている人
2. プロファイリング
Djangoアプリケーションの最適化を図る前に、まずはプロファイリングを行う必要がある。性能分析のことである。プロファイラを用いることで、如何なるクエリがどのように動いているかを調べることができる。次に、何のために何を改善するのか をはっきりさせる。メモリの使用量を削減するのか、速度を最適化するのか。何も考えずに単に高速化すると、メモリの使用量が膨大になったり、プログラムが複雑になってしまたりすることがある。(参考 : 時間と空間のトレードオフ)
プロファイリングには、決定論的プロファイリング と 統計的プロファイリング という2つのプロファイリング手法がある。
決定論的プロファイリング
逐次的にプログラムを実行しながら、測定していく。
トレーシングという手法を使う。パフォーマンスよりも正確さを重視する。
ただし、負荷がかかる。e.g.) cProfile, Profile, Memray
すべての 関数呼び出し, 関数からのリターン, 例外発生 をモニターし、正確なタイミングを記録することで、イベント間の時間、つまりどの時間にユーザコードが実行されているのかを計測するやり方。(Django公式ドキュメント)
統計的プロファイリング
サンプリングという手法で計測する。
負荷が少ない。プロダクションでのパフォーマンスのオーバーヘッドをより低く、より一貫したものにすることができる。実運用に導入しやすい。 e.g.) memory-profiler
統計的プロファイリング (このモジュールでこの方法は採用していません) とは、有効なインストラクションポインタからランダムにサンプリングを行い、プログラムのどこで時間が使われているかを推定する方法。(Django公式ドキュメント)
主なProfiler
django-debug-tools
Django開発には必須のツール。DjangoアプリケーションのSQLやAPIの実行履歴や速度などを見ることができる。
cProfile & Profile
両方とも決定論的プロファイリングで、cProfileはc言語でProfileを真似たもの。
拡張するならProfileの方がいい。
DjangoではMiddlewareで django-cprofile-middlewareというものがある。
ベンチマーク用途には timeit のほうが正確な計測結果を求められる
snakeviz を使えば可視化することも可能。
使い方は Profiling in Django に書いてある。
デフォルトでは円グラフで可視化される。
Pyinstrument
Django Querycount
djangoにはquerycount.middleware.QueryCountMiddleware
というmiddlewareがある。
Django Silk
Djangoのmiddlewareの名前はsilk.middleware.SilkyMiddleware
である。
Line Profiler
処理時間を計測したいファイルの関数に@profile
をつけて測定する。
import line_profiler
import atexit
profile = line_profiler.LineProfiler()
atexit.register(profile.print_stats)
# 測定したい関数にアノテーションを付ける
@profile
def hoge():
・・・・
Timer unit: 1e-06 s
File: pystone.py
Function: Proc2 at line 149
Total time: 0.606656 s
Line # Hits Time Per Hit % Time Line Contents
==============================================================
149 @profile
150 def Proc2(IntParIO):
151 50000 82003 1.6 13.5 IntLoc = IntParIO + 10
152 50000 63162 1.3 10.4 while 1:
153 50000 69065 1.4 11.4 if Char1Glob == 'A':
154 50000 66354 1.3 10.9 IntLoc = IntLoc - 1
155 50000 67263 1.3 11.1 IntParIO = IntLoc - IntGlob
156 50000 65494 1.3 10.8 EnumLoc = Ident1
157 50000 68001 1.4 11.2 if EnumLoc == Ident1:
158 50000 63739 1.3 10.5 break
159 50000 61575 1.2 10.1 return IntParIO
memory_profiler
同じく@profile()
デコレータをつけることによって使用しているメモリを表示する。
from memory_profiler import profile
# precisionを設定すると精度が上がる
@profile
def hoge(precision=4):
・・・・
Line # Mem usage Increment Occurrences Line Contents
============================================================
3 38.816 MiB 38.816 MiB 1 @profile
4 def my_func():
5 46.492 MiB 7.676 MiB 1 a = [1] * (10 ** 6)
6 199.117 MiB 152.625 MiB 1 b = [2] * (2 * 10 ** 7)
7 46.629 MiB -152.488 MiB 1 del b
8 46.629 MiB 0.000 MiB 1 return a
memray
メモリの使用量を見やすく表示する。実行負荷や余分な命令が増えがちなトレーシング手法を使っているが、極力負荷を減らしたツールとして登場した。
python -X importtime
-X importtime
をつけることで、importにどれだけ時間が掛かっているかを調べれる。
$ python -X importtime -c 'import django'
import time: self [us] | cumulative | imported package
import time: 136 | 136 | _io
import time: 20 | 20 | marshal
import time: 256 | 256 | posix
import time: 261 | 672 | _frozen_importlib_external
...
import time: 499 | 6898 | subprocess
import time: 309 | 309 | weakref
import time: 60 | 60 | org
import time: 10 | 70 | org.python
import time: 7 | 77 | org.python.core
import time: 187 | 572 | copy
import time: 228 | 799 | django.utils.functional
import time: 183 | 982 | django.utils.regex_helper
import time: 340 | 12228 | django.utils.version
import time: 206 | 12434 | django
参考 : Django: How to profile and improve startup time
また、tunaを使えば、可視化することができる。
サードパーティのツール
以下のサービスでは、実際のユーザーエクスペリエンスをリモートの HTTP クライアントの観点からシミュレートしてくれる。
クエリの表示
.query
QuerySet.query
でクエリを確認できる。しかし、.query
で毎回確認しなければいけない、saveなどの処理はSQLを確認できないといった問題点がある。
>>> print(Book.objects.filter(id=1).query)
SELECT "book"."id", "book"."name" FROM "book" WHERE "book"."id" = 1
django.db.connection.queries
django.db.connection.queries
を使えば.query
の問題を解決できる。
from django.db import connection
for q in connection.queries:
print(q)
上記を実行すると、以下のようにクエリが出力され、実行時間も確認できる。
{'sql': 'SELECT "user"."id",
・・・・
, 'time': '0.000'}
QuerySet.explain()
QuerySet.explain() を使うと、使用されているインデックスや結合など、クエリの実行情報などが得られる。
DBのログを見る
ログを見ることでどんなSQLが発行されているか調べられる。
設定によって、遅いクエリだけを見ることもできる。
3. DB最適化
Djangoは主にDjango ORMを用いてDB操作を行うが、Django ORMによるDBへのアクセスは非常にコストが高い。ここでは以下4つの方法について説明する。
Djangoのアルゴリズムの改善
QuerySet Tips
速い | 遅い |
---|---|
QuerySet.contains(obj) | if obj in queryset |
Queryset.count() | len(queryset) |
Queryset.exit() | if queryset |
iterator()
QuerySet のall()
やfilter()
で大量のオブジェクトを取得する際に、QuerySet でキャッシングするのを防ぐ。メモリ消費量が少なくなる。ただし、使用しているDBによって動作に違いがあり、MySQLでは個別にチャンクする実装を行わなければならない(参考: 大量のデータを取得する際に気を付けること)
update()
Queryset.udpate()
やQueryset.delete()
を使って一括でupdateする。
Entry.objects.filter(pub_date__year=2007).update(headline="Everything is the same")
外部キーの取得
DBで確認すれば明らかだが、外部キーは{テーブル名}_id
と名前で保存されている。
○ book.user_id
× book.user.id
bulk処理
bulkで一括で処理を行う。
- bulk_create()
- bulk_update()
- add(xxx, xxx, ・・・)
- remove(xxx, xxx, ・・・)
Entry.objects.bulk_create(
[
Entry(headline="This is a test"),
Entry(headline="This is only a test"),
]
)
ただし、add()
とremove()
で以下のような場合がある。
my_pizza.toppings.remove(pepperoni)
your_pizza.toppings.remove(pepperoni, mushroom)
この場合Q
, through
を用いると一気に処理を行える。
from django.db.models import Q
PizzaToppingRelationship = Pizza.toppings.through
PizzaToppingRelationship.objects.filter(
Q(pizza=my_pizza, topping=pepperoni)
| Q(pizza=your_pizza, topping=pepperoni)
| Q(pizza=your_pizza, topping=mushroom)
).delete()
RawSQL
RawSQL
を使うことで、SQLを明示的にクエリに追加することができます。。SQLインジェクションなどに注意。
N+1問題
select_related
, prefetch_related
, prefetch_related_objects
を用いることでN+1問題を解決できる。
参考 : select_relatedとprefetch_related - Just Python
ぐるぐるSQL
ぐるぐるSQLはN+1問題とは異なる。
例えばテーブル全てを引き出すところをわざわざ1行ずつ取ってくること。
主にpythonのアルゴリズムの問題が原因かと考えられる。
インデックスの効いたカラムでフィルタリングする
インデックスとは何かについては、DBの話になるので、ここでは割愛。(次の記事で紹介します)
entry = Entry.objects.get(id=10) # インデックスが効くので高速
entry = Entry.objects.get(headline="News Item Title") #インデックスが効かないので遅い
entry = Entry.objects.get(headline__startswith="News") # 前方一致検索等はさらに遅い
DBチューニング
DBの設定ファイルやパラメータを適切な値にすることで、スループットの向上を図る。
ここでは詳しく述べない。
SQLチューニング
これに関しては説明すると長くなるので、別記事で詳細を書く予定である。(ほぼ完成している)
-
slow_query_log
を確認する - 適切なインデックスをはる
別のORMを使用する
従来の Django ORM の問題点として、以下のようなものがある。
- 複雑なクエリになると実現が難しい
- N+1問題 などがよく発生する
SQLAlchemyを導入することで、これらの問題を解決することができる。
Djangoに Aldjemy を導入すると、SQLAlchemy と Django ORM を併用できる。
導入方法、使い方 : Django ORMに苦しむ諸氏に贈る「Aldjemy」のススメ
4. キャッシング
QuerySet
QuerySet は速い。
# 速い
users.count()
# オブジェクトを数えてるから遅い
len(users)
# djangoのテンプレートのfilter
{{ my_bicycles|length }}
QuerySet は遅延評価される。
>>> articles = Article.objects.all() # DBアクセスなし(クエリセットを構築するだけ)
>>> len(articles[1:6]) # キャッシュが空のためDBアクセスあり(クエリセットの部分評価 → 過去にキャッシュは設定されていないので、ここでもキャッシュは保存されない)
>>> len(articles[1:6]) # キャッシュが空のためDBアクセスあり(クエリセットの部分評価 → 過去にキャッシュは設定されていないので、ここでもキャッシュは保存されない)
>>> len(articles) # キャッシュが空のためDBアクセスあり(クエリセットの全部評価 → キャッシュが設定される)
>>> len(articles) # DBアクセスなし(クエリセットの全部評価 → キャッシュが設定されているので、DBアクセスなし)
>>> len(articles[1:6]) # DBアクセスなし(クエリセットの部分評価 → キャッシュが設定されているので、DBアクセスなし)
以下のfoo.py
はキャッシュされている。
def foo():
c = []
questions = Question.objects.all()
for qs in questions:
c.append(qs.id)
foo()
$ time python foo.py
0.011981964111328125
以下のhoge.py
はキャッシュが使われていない。
def hoge():
c = []
questions = Question.objects.all()
time1 = time.time()
for i in range(1113):
c.append(questions[i].id)
hoge()
$ time python hoge.py
0.329456090927124
with テンプレートタグ
{% with ~%}
を使うとQuerySetをキャッシングできる。
複雑な表現の変数の値をキャッシュし、簡単な名前で参照できるようにする。呼出しコストの高いメソッド (例えばDBを操作するようなメソッド) に何度もアクセスするときに有効。
{% with total=business.employees.count %}
{{ total }} employee{{ total|pluralize }}
{% endwith %}
データベースによるキャッシング
MemcachedやRedisなどを使うことでキャッシュを保存することができる。
使い方は以下のリンクを参考。
5. 適切なレイヤーで処理を行う
低レイヤーの方が高速になる。
例えば、DBはPythonより高速に処理ができ、PythonはTemplate言語より処理が速い。
下記は先ほども紹介した例。
# QuerySetはDBを操作するので高速
book.count()
# 普通のPython
len(book)
テンプレート言語で行うと遅すぎる。
{{ book | length }}
他にも以下のような例がある。
- フィルタリングはPythonではなく、QuerySetの
filter
やexclude
,F expressions
で対象を絞る - 複雑なソートは、QuerySetの
annotate
を使ってorder_by
をする
6. Pythonコードの最適化
アルゴリズムを改善する
クイックソートなどを使って計算量を減らしたりして改善する。
Pythonプログラマーが知っておくべきコーディングの最適化とリファクタリング方法などを参考。
関数のキャッシュ化
主な手法としては、cachetools や functoolsのlru_cacheやcashデコレータがある。
並列処理
並列処理はここでは触れない。以下のサイトを参考にしてください。
Pythonバインディング
ctype
標準ライブラリのctypesを使用する
C言語でライブラリを作成する
getattr などがCで作られている。
バイトコード最適化
Numba や PyPy を使う