23
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonAdvent Calendar 2023

Day 25

【完全版】Django高速化、最適化

Last updated at Posted at 2023-12-24

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の実行履歴や速度などを見ることができる。

image.png

cProfile & Profile

両方とも決定論的プロファイリングで、cProfileはc言語でProfileを真似たもの。
拡張するならProfileの方がいい。
DjangoではMiddlewareで django-cprofile-middlewareというものがある。

ベンチマーク用途には timeit のほうが正確な計測結果を求められる

snakeviz を使えば可視化することも可能。
使い方は Profiling in Django に書いてある。

image.png

デフォルトでは円グラフで可視化される。

image.png

Pyinstrument

image.png

Django Querycount

djangoにはquerycount.middleware.QueryCountMiddlewareというmiddlewareがある。

image.png

Django Silk

Djangoのmiddlewareの名前はsilk.middleware.SilkyMiddlewareである。

image.png

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

メモリの使用量を見やすく表示する。実行負荷や余分な命令が増えがちなトレーシング手法を使っているが、極力負荷を減らしたツールとして登場した。

image.png

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を使えば、可視化することができる。

image.png

サードパーティのツール

以下のサービスでは、実際のユーザーエクスペリエンスをリモートの 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のアルゴリズムの問題が原因かと考えられる。

ぐるぐるSQLという用語は止めてくださいという話

インデックスの効いたカラムでフィルタリングする

インデックスとは何かについては、DBの話になるので、ここでは割愛。(次の記事で紹介します)

entry = Entry.objects.get(id=10) # インデックスが効くので高速
entry = Entry.objects.get(headline="News Item Title") #インデックスが効かないので遅い
entry = Entry.objects.get(headline__startswith="News") # 前方一致検索等はさらに遅い

DBチューニング

DBの設定ファイルやパラメータを適切な値にすることで、スループットの向上を図る。
ここでは詳しく述べない。

MySQLデータベースのパフォーマンスチューニングを参考。

SQLチューニング

これに関しては説明すると長くなるので、別記事で詳細を書く予定である。(ほぼ完成している)

  • slow_query_log を確認する
  • 適切なインデックスをはる

別のORMを使用する

logo.png

従来の Django ORM の問題点として、以下のようなものがある。

  • 複雑なクエリになると実現が難しい
  • N+1問題 などがよく発生する

SQLAlchemyを導入することで、これらの問題を解決することができる。
Djangoに Aldjemy を導入すると、SQLAlchemyDjango 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はキャッシュされている。

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などを使うことでキャッシュを保存することができる。
使い方は以下のリンクを参考。

Django's cache framework

5. 適切なレイヤーで処理を行う

低レイヤーの方が高速になる。
例えば、DBはPythonより高速に処理ができ、PythonはTemplate言語より処理が速い。

下記は先ほども紹介した例。

# QuerySetはDBを操作するので高速
book.count()

# 普通のPython
len(book)

テンプレート言語で行うと遅すぎる。

{{ book | length }}

他にも以下のような例がある。

  • フィルタリングはPythonではなく、QuerySetのfilterexclude, F expressionsで対象を絞る
  • 複雑なソートは、QuerySetのannotateを使ってorder_byをする

6. Pythonコードの最適化

アルゴリズムを改善する

クイックソートなどを使って計算量を減らしたりして改善する。

Pythonプログラマーが知っておくべきコーディングの最適化とリファクタリング方法などを参考。

関数のキャッシュ化

主な手法としては、cachetoolsfunctoolsのlru_cacheやcashデコレータがある。

並列処理 

並列処理はここでは触れない。以下のサイトを参考にしてください。

【必読】Pythonの並列処理でタスクを高速化!実例付

Pythonバインディング

ctype

標準ライブラリのctypesを使用する

C言語でライブラリを作成する

getattr などがCで作られている。

C や C++ による Python の拡張

バイトコード最適化

NumbaPyPy を使う

7. Djangoの設計思想に従う

設計思想 | Django ドキュメント

8. 参考文献

  1. Pythonプロファイラ
  2. データベースアクセスの最適化
  3. パフォーマンスと最適化
  4. 静的ファイル(CSS、JavaScript、Image)の保存場所や設定について
  5. Webパフォーマンスガチ勢が本当に使っている技術
  6. Djangoで静的ファイルとうまくやる
  7. データベースの最適化
  8. Profiling Django App
  9. Pythonのスタートアップ時間を可視化した
  10. Django: How to profile and improve startup time
  11. 大量のデータを取得する際に気を付けること
  12. Martiniでレスポンスをgzip compressする
23
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?