This article was contributed by The MemCachier Add-on
MemCachier manages and scales clusters of memcache servers so you can focus on your app. Tell us how much memory you need and get started for free instantly. Add capacity later as you need it.
follow @MemCachier on Twitter
Memcache を使用した Django アプリケーションのスケーリング
この記事の英語版に更新があります。ご覧の翻訳には含まれていない変更点があるかもしれません。
最終更新日 2020年08月26日(水)
Table of Contents
Memcache は、Web アプリとモバイルアプリバックエンドのパフォーマンスとスケーラビリティを改善する技術です。ページの読み込みが遅すぎる場合や、アプリにスケーラビリティの問題がある場合は、Memcache の使用を検討してください。小規模なサイトであっても、Memcache の導入によってページの読み込みを高速化し、将来の変化にアプリを対応させることができます。
このガイドでは、単純な Django 2.1 アプリケーションを作成して Heroku にデプロイし、Memcache を追加してパフォーマンスのボトルネックを軽減する方法を示します。
Django 2 以降では Python 2 がサポートされなくなったため、この記事では Python 3 を主なターゲットにしています。ただし、これより古いバージョンの Django で Python 2 を使用する場合も、このガイドの内容は有効です。
前提条件
このガイドの手順を完了する前に、以下のすべての条件を満たしていることを確認してください。
- Python の知識がある (Django についても知識があることが理想的です)
- Heroku ユーザーアカウント (無料ですぐにサインアップ)
- 「Heroku スターターガイド (Python)」の手順を理解している
- Python と Heroku CLI がコンピュータにインストールされている
Heroku 用 Django アプリケーションの作成
次のコマンドは、空の Django アプリを作成します。これらのコマンドの詳細な説明は「Python および Django アプリのデプロイ」にあります。
$ mkdir django_memcache && cd django_memcache
$ python -m venv venv # For Python 2 use `virtualenv venv`
$ source venv/bin/activate
(venv) $ pip install Django django-heroku gunicorn
(venv) $ django-admin.py startproject django_tasklist .
(venv) $ pip freeze > requirements.txt
(venv) $ python manage.py runserver
Performing system checks...
System check identified no issues (0 silenced).
Django version 2.0, using settings 'django_tasklist.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
http://localhost:8000 にアクセスすると “hello, world” ランディングページが表示されます。
Heroku のための Django の設定
Django アプリを Heroku で動作させるには、いくつかの Heroku 固有の設定が必要です。
Procfile
を追加して、アプリの起動方法を Heroku に指示します。$ echo "web: gunicorn django_tasklist.wsgi --log-file -" > Procfile
Django アプリを Heroku で動作させるために必要な、主にデータベースの動作と静的ファイルの供給に関する Heroku 固有の設定を追加します。幸いにも、そのすべてを実行する
django-heroku
パッケージがあります。django_tasklist/settings.py
ファイルの最後に次の行を追加します。# Configure Django App for Heroku. import django_heroku django_heroku.settings(locals())
For more information about these Heroku specific settings see Configuring Django Apps for Heroku. Note:
django-heroku
only supports Python 3. For Python 2 please follow the instructions in Configuring Django Apps for Heroku.
Heroku へのデプロイ
コードを Heroku にデプロイするには、先に Git リポジトリにコードを追加する必要があります。最初に、.gitignore
を編集して次の行を追加し、不要なファイルを除外します。
venv
*.pyc
db.sqlite3
次に、Git リポジトリを初期化し、コミットします。
$ git init
$ git add .
$ git commit -m "Empty django app"
ここで、heroku create
を使用して Heroku アプリを作成します。
$ heroku create
Creating app... done, ⬢ blooming-ridge-97247
https://blooming-ridge-97247.herokuapp.com/ | https://git.heroku.com/blooming-ridge-97247.git
次に、アプリをデプロイします。
$ git push heroku master
最後に、Heroku CLI を使用してブラウザでアプリを表示できます。
$ heroku open
ローカル開発モードと同じ “hello, world” ランディングページが表示されますが、今回は Heroku プラットフォームで実行されている点が異なります。
タスクリスト機能の追加
構築する Django アプリケーションはタスクリストです。リストの表示に加えて、新しいタスクの追加とタスクの削除を実行するアクションがあります。
最初に、Django アプリ mc_tasklist
を作成します。
(venv) $ python manage.py startapp mc_tasklist
django_tasklist/settings.py
で、インストール済みアプリのリストに mc_tasklist
を追加します。
INSTALLED_APPS = (
'django.contrib.admin',
# ...
'mc_tasklist',
)
これで、以下の 4 つの手順でタスクリスト機能を追加できます。
mc_tasklist/models.py
で単純なTask
モデルを作成します。from django.db import models class Task(models.Model): name = models.TextField()
Use
makemigrations
andmigrate
to create a migration for themc_tasklist
app as well as create themc_tasklist_tasks
table locally, along with all other default Django tables:(venv) $ python manage.py makemigrations mc_tasklist (venv) $ python manage.py migrate
django_tasklist/urls.py
で、add、remove、index の各メソッドのルートを設定します。# ... from mc_tasklist import views urlpatterns = [ # ... path('add', views.add), path('remove', views.remove), path('', views.index), ]
mc_tasklist/views.py
で、対応するビューコントローラーを追加します。from django.template.context_processors import csrf from django.shortcuts import render_to_response, redirect from mc_tasklist.models import Task def index(request): tasks = Task.objects.order_by("id") c = {'tasks': tasks} c.update(csrf(request)) return render_to_response('index.html', c) def add(request): item = Task(name=request.POST["name"]) item.save() return redirect("/") def remove(request): item = Task.objects.get(id=request.POST["id"]) if item: item.delete() return redirect("/")
mc_tasklist/templates/index.html
で、表示コード付きのテンプレートを作成します。<!DOCTYPE html> <head> <meta charset="utf-8"> <title>MemCachier Django tutorial</title> <!-- Fonts --> <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.4.0/css/font-awesome.min.css" rel='stylesheet' type='text/css' /> <!-- Bootstrap CSS --> <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" rel="stylesheet" /> </head> <body> <div class="container"> <!-- New Task Card --> <div class="card"> <div class="card-body"> <h5 class="card-title">New Task</h5> <form action="add" method="POST"> {% csrf_token %} <div class="form-group"> <input type="text" class="form-control" placeholder="Task Name" name="name" required> </div> <button type="submit" class="btn btn-default"> <i class="fa fa-plus"></i> Add Task </button> </form> </div> </div> <!-- Current Tasks --> {% if tasks %} <div class="card"> <div class="card-body"> <h5 class="card-title">Current Tasks</h5> <table class="table table-striped"> {% for task in tasks %} <tr> <!-- Task Name --> <td class="table-text">{{ task.name }}</td> <!-- Delete Button --> <td> <form action="remove" method="POST"> {% csrf_token %} <input type="hidden" name="id" value="{{ task.id }}"> <button type="submit" class="btn btn-danger"> <i class="fa fa-trash"></i> Delete </button> </form> </td> </tr> {% endfor %} </table> </div> </div> {% endif %} </div> <!-- Bootstrap related JavaScript --> <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script> </body> </html>
Django will automatically check each apps
templates
folder for templates.
python manage.py runserver
を実行し、http://localhost:8000
に再度アクセスし、いくつかのタスクを追加および削除して、基本的なタスクリストアプリの動作を確認します。
ローカルでは、SQLite データベースを使用してタスクリストを保存します。Heroku では、データベースをプロビジョニングする必要があります。
$ heroku addons:create heroku-postgresql:hobby-dev
ここで、タスクリストを Heroku にデプロイします。
$ git add .
$ git commit -m "Task list functionality"
$ git push heroku master
最後に、Heroku 上でデータベースを移行して mc_tasklist_tasks
テーブルを作成し、Heroku アプリを再起動します。
$ heroku run python manage.py migrate
$ heroku restart
heroku open
でアプリを表示し、いくつかのタスクを追加して、Heroku 上でもアプリが動作することを確認します。
キャッシングを Django に追加する
Memcache はインメモリの分散キャッシュです。そのプライマリ API は、SET(key, value)
と GET(key)
の 2 つの操作で構成されます。
Memcache は、複数のサーバーに分散しているが操作は一定の時間に実行されるハッシュマップ (または辞書) のようなものです。
Memcache の最も一般的な用途は、コストの高いデータベースクエリや HTML レンダリングの結果をキャッシュし、これらの高コスト操作を繰り返す必要をなくすことです。
Memcache のプロビジョニング
Django で Memcache を使用するには、まず実際の Memcached キャッシュをプロビジョニングする必要があります。これは、MemCachier アドオンから無料で簡単に入手できます。
$ heroku addons:create memcachier:dev
新しい Memcache インスタンスが自動的にプロビジョニングされ、MemCachier の資格情報を含む一連の環境設定が公開されます。
Django での MemCachier の設定
Django 1.11 以降では、Django ネイティブの pylibmc
バックエンドを使用できます。これよりも古いバージョンの
Django の場合、django-pylibmc
をインストールする必要があります。詳細は、この記事の
古いバージョン
を参照してください。
django_tasklist/settings.py
の最後に次の内容を追加して、キャッシュを設定します。
def get_cache():
import os
try:
servers = os.environ['MEMCACHIER_SERVERS']
username = os.environ['MEMCACHIER_USERNAME']
password = os.environ['MEMCACHIER_PASSWORD']
return {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
# TIMEOUT is not the connection timeout! It's the default expiration
# timeout that should be applied to keys! Setting it to `None`
# disables expiration.
'TIMEOUT': None,
'LOCATION': servers,
'OPTIONS': {
'binary': True,
'username': username,
'password': password,
'behaviors': {
# Enable faster IO
'no_block': True,
'tcp_nodelay': True,
# Keep connection alive
'tcp_keepalive': True,
# Timeout settings
'connect_timeout': 2000, # ms
'send_timeout': 750 * 1000, # us
'receive_timeout': 750 * 1000, # us
'_poll_timeout': 2000, # ms
# Better failover
'ketama': True,
'remove_failed': 1,
'retry_timeout': 2,
'dead_timeout': 30,
}
}
}
}
except:
return {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
}
}
CACHES = get_cache()
これにより、開発と本番の両環境でキャッシュが設定されます。 MEMCACHIER_*
環境変数が存在する場合、キャッシュは pylibmc
で設定され、MemCachier に接続します。一方、MEMCACHIER_*
環境変数が存在しない (つまり開発モードの) 場合、Django の単純なインメモリキャッシュが代わりに使用されます。
依存関係のインストール
pylibmc
をインストールするには、C ライブラリ libmemcached
が必要です。Heroku では libmemcached
がインストール済みのため、この点は心配ありません。ただし、pylibmc
をローカルでテストする場合はインストールが必要です (プロセスはプラットフォームにより異なります)。
Ubuntu の場合:
$ sudo apt-get install libmemcached-dev
OS X の場合:
$ brew install libmemcached
Libmemcached は、SASL 認証のサポートを有効または無効にして ビルドできます。SASL 認証は、Heroku が提供するユーザー名と パスワードを使用して MemCachier サーバーで認証するために クライアントが使用するメカニズムです。使用する libmemcached のビルドが SASL をサポート していることを確認する必要があります。これを行うには、 ガイドに従ってください。
次に、pylibmc
Python モジュールをインストールします。
(venv) $ pip install pylibmc==1.5.2
pylibmc のインストールが libmemcached
を見つけられない場合、
LIBMEMCACHED=/opt/local pip install pylibmc
のように指定することが必要な場合があります (libmemcached
が
/opt/local
にインストールされている場合の例)。必要な場合、/opt/local
は正しいディレクトリに
置き換えてください。
新しい依存関係で requirements.txt
ファイルを更新します (pylibmc
がローカルにインストールされていない場合、pylibmc==1.5.2
を requirements.txt
に追加するだけです)。
$ pip freeze > requirements.txt
$ cat requirements.txt
...
pylibmc==1.5.2
最後に、これらの変更をコミットしてデプロイします。
$ git commit -am "Connecting to memcache."
$ git push heroku master
Memcache 設定の確認
次に進む前に、memcache が正しく設定されていることを確認します。
これを行うには、Django シェルを実行します。ローカルマシンで python manage.py shell
を実行し、Heroku で heroku run python manage.py shell
を実行します。and in Heroku run
クイックテストを実行して、キャッシュが正しく設定されていることを確認します。
>>> from django.core.cache import cache
>>> cache.get("foo")
>>> cache.set("foo", "bar")
>>> cache.get("foo")
'bar'
Ctrl-d
で終了します。2 番目の get
コマンドの後、foo
がキャッシュから取得されると bar
が画面に出力されるはずです。bar
が出力されない場合、キャッシュは正しく設定されていません。
コストの高いデータベースクエリをキャッシュする
Memcache を使用して、コストの高いデータベースクエリをキャッシュすることはよくあります。この単純な例にはコストの高いクエリは含まれませんが、学習のために、すべてのタスクをデータベースから取得するのはコストの高い操作であると仮定します。
mc_tasklist/views.py
のタスクリストデータベースクエリコードは、次のように、最初にキャッシュをチェックするように変更できます。
# ...
from django.core.cache import cache
import time
TASKS_KEY = "tasks.all"
def index(request):
tasks = cache.get(TASKS_KEY)
if not tasks:
time.sleep(2) # simulate a slow query.
tasks = Task.objects.order_by("id")
cache.set(TASKS_KEY, tasks)
c = {'tasks': tasks}
c.update(csrf(request))
return render_to_response('index.html', c)
# ...
上記のコードでは、最初にキャッシュをチェックして、tasks.all
キーがキャッシュに存在するかどうかを確認します。 存在しない場合、データベースクエリが実行され、キャッシュが更新されます。 これにより、後続のページロードでデータベースクエリを実行する必要がなくなります。 time.sleep(2)
は、低速なクエリをシミュレートするために存在しているだけです。
この新しい機能をデプロイしてテストします。
$ git commit -am 'Add caching with MemCachier'
$ git push heroku master
$ heroku open
キャッシュ内で何が起きているかを確認するために、MemCachier のダッシュボードを開きます。
$ heroku addons:open memcachier
タスクリストを最初に読み込むと、get miss
と set
コマンドが増加しているはずです。それ以降は、タスクリストを再読み込みするたびに get hit
が増加するはずです (ダッシュボードの統計を更新してください)。
キャッシュは機能していますが、まだ大きな問題があります。新しいタスクを追加して結果を確認します。すると、新しいタスクが現在のタスクリストに反映されていません。新しいタスクがデータベースに作成されましたが、アプリで表示されているのはキャッシュから取得した古いタスクリストです。
Memcache の最新状態の維持
キャッシュが古くなる問題に対処する手法は数多くあります。
有効期限: キャッシュが古くなるのを確実に防ぐ最も簡単な方法は、有効期限の設定です。
cache.set
メソッドには、オプションの 3 番目の引数を指定できます。これは、キャッシュキーをキャッシュに保持する秒数です。 このオプションを指定しない場合、デフォルトのTIMEOUT
値 (settings.py
内) が代わりに使用されます。You could modify the
cache.set
method to look like this:cache.set(TASKS_KEY, tasks, 5)
But this functionality only works when it is known for how long the cached value is valid. In our case however, the cache gets stale upon user interaction (add, remove a task).
キャッシュされた値の削除: 簡単な戦略として挙げられるのは、キャッシュが古くなっていることを把握したら
tasks.all
キーを無効にする、具体的にはadd
ビューとremove
ビューを変更してtasks.all
キーを削除することです。# ... def add(request): item = Task(name=request.POST["name"]) item.save() cache.delete(TASKS_KEY) return redirect("/") def remove(request): item = Task.objects.get(id=request.POST["id"]) if item: item.delete() cache.delete(TASKS_KEY) return redirect("/")
キーベースの有効期限: 古くなったデータを無効化する別の手法は、キーを変更することです。
# ... import random import string def _hash(size=16, chars=string.ascii_letters + string.digits): return ''.join(random.choice(chars) for _ in range(size)) def _new_tasks_key(): return 'tasks.all.' + _hash() TASKS_KEY = _new_tasks_key() # ... def add(request): item = Task(name=request.POST["name"]) item.save() global TASKS_KEY TASKS_KEY = _new_tasks_key() return redirect("/") def remove(request): item = Task.objects.get(id=request.POST["id"]) if item: item.delete() global TASKS_KEY TASKS_KEY = _new_tasks_key() return redirect("/")
The upside of key based expiration is that you do not have to interact with the cache to expire the value. The LRU eviction of Memcache will clean out the old keys eventually.
キャッシュの更新: キーを無効化する代わりに、値を更新して新しいタスクリストを反映させることもできます。
# ... def add(request): item = Task(name=request.POST["name"]) item.save() cache.set(TASKS_KEY, Task.objects.order_by("id")) return redirect("/") def remove(request): item = Task.objects.get(id=request.POST["id"]) if item: item.delete() cache.set(TASKS_KEY, Task.objects.order_by("id")) return redirect("/")
値を削除するのではなく更新するようにすることで、最初のページロード時にデータベースにアクセスする必要がなくなります。
2 番目、3 番目、または 4 番目の手法を使用することで、キャッシュが決して古くならないことを保証できます。 いつものように、変更をコミットしてデプロイします。
$ git commit -am "Keep Memcache up to date."
$ git push heroku master
新しいタスクを追加すると、キャッシングの実装以降に追加したすべてのタスクが表示されるようになりました。
Django の統合キャッシングの使用
Django には、Memcache を使用してパフォーマンスを改善する組み込みの方法もいくつか存在します。 これらの主なターゲットは、コストが高く CPU に負担がかかる操作である HTML のレンダリングです。
キャッシングと CSRF
トークンはリクエストごとに変わるため、CSRF トークンを使用するフォームが含まれるビューまたはフラグメントはキャッシュできません。Django の統合キャッシングの使用方法を学ぶために、Django の CSRF ミドルウェアを無効化してみましょう。 このタスクリストは公開されているので問題ありませんが、これは、重要な用途がある本番環境のアプリケーションでは行わないでください。
django_tasklist/settings.py
で、CsrfViewMiddleware
をコメントアウトします。
MIDDLEWARE = [
# ...
# 'django.middleware.csrf.CsrfViewMiddleware',
# ...
]
テンプレートフラグメントをキャッシュする
Django では、レンダリングされたテンプレートフラグメントをキャッシュできます。これは、Flask でのスニペットキャッシング、あるいは Laravel でのレンダリングされたパーシャルのキャッシングに似ています。フラグメントキャッシングを有効にするには、テンプレートの先頭に {% load cache %}
を追加します。
CSRF トークンを使用するフォームが含まれたフラグメントはキャッシュしないでください。
タスクエントリのレンダリングされたセットをキャッシュするために、mc_tasklist/templates/index.html
で {% cache timeout key %}
ステートメントを使用します。
{% load cache %}
<!-- ... -->
<table class="table table-striped">
{% for task in tasks %}
{% cache None 'task-fragment' task.id %}
<tr>
<!-- ... -->
</tr>
{% endcache %}
{% endfor %}
</table>
<!-- ... -->
ここで、タイムアウトは None
であり、キーは連結される文字列のリストです。タスク ID が再利用されない限り、これが、レンダリングされたスニペットのキャッシングのすべてです。Heroku で使用する PostgreSQL データベースでは ID を再利用しないため、条件は整っています。
ID を再利用するデータベースを使用する場合は、それぞれのタスクが削除された時点でフラグメントを削除する必要があります。次のコードをタスク削除ロジックに追加することで、これを実現できます。
from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key("task-fragment", vary_on=[str(item.id)])
cache.delete(key)
アプリケーションでフラグメントをキャッシュした効果を見てみましょう。
$ git commit -am 'Cache task entry fragment'
$ git push heroku master
(最初のリロードを除き) ページをリロードするたびに、リスト内の各タスクで get hit
の増加が確認できるはずです。
ビュー全体をキャッシュする
さらに一歩進んで、フラグメントの代わりにビュー全体をキャッシュすることができます。ビューが頻繁に変更される場合やユーザー入力用のフォームがビューに含まれている場合、意図しない副作用が発生することがあるため、これは慎重に行ってください。今回のタスクリストの例では、タスクが追加または削除されるたびにタスクリストが変更され、ビューにはタスクを追加および削除するためのフォームが含まれているため、この条件の両方が当てはまります。
CSRF トークンを使用するフォームが含まれたビューはキャッシュしないでください。
mc_tasklist/views.py
で @cache_page(timeout)
デコレーターを使用してタスクリストビューをキャッシュできます。
# ...
from django.views.decorators.cache import cache_page
@cache_page(None)
def index(request):
# ...
# ...
タスクを追加または削除するたびにビューが変更されるため、これが起きるたびにキャッシュされたビューを削除する必要があります。これは簡単ではありません。後からビューを削除できるようにするには、ビューがキャッシュされたときにキーを把握しておく必要があります。
# ...
from django.utils.cache import learn_cache_key
VIEW_KEY = ""
@cache_page(None)
def index(request):
# ...
response = render_to_response('index.html', c)
global VIEW_KEY
VIEW_KEY = learn_cache_key(request, response)
return response
def add(request):
# ...
cache.delete(VIEW_KEY)
return redirect("/")
def remove(request):
item = Task.objects.get(id=request.POST["id"])
if item:
# ...
cache.delete(VIEW_KEY)
return redirect("/")
ビューのキャッシングの効果を確認するには、アプリケーションをデプロイします。
$ git commit -am 'Cache task list view'
$ git push heroku master
最初の更新で、存在しているタスクの数に応じて get hit
カウンターが増加するだけでなく、現在キャッシュされているビューに対応する形で get miss
と set
が増加するはずです。ビュー全体が 2 つの get
コマンドで取得されるため、それ以降のリロードでは get hit
カウンターは 2 しか増加しません。
ビューのキャッシングは、コストの高い操作またはテンプレートフラグメントのキャッシングを陳腐化しないことに注意してください。キャッシュされる大きな操作内で小さな操作をキャッシュしたり、大きなフラグメント内で小さなフラグメントをキャッシュしたりするのは良いやり方です。(ロシア人形方式キャッシングと呼ばれる) この手法では、構築単位をゼロから作り直す必要がないため、大きい方の操作、フラグメント、またはビューがキャッシュから削除された場合のパフォーマンスに寄与します。
セッションストレージでの Memcache の使用
Heroku では、再起動時に内容が失われる一時的なファイルシステムが dyno に備わっているため、セッション情報をディスクに保存することは推奨されていません。
Memcache は、タイムアウトがある短命セッションの情報を保存するのには適しています。しかし、Memcache はあくまでキャッシュであって永続的ではないため、寿命の長いセッションについては、データベースなどの永続的なストレージオプションのほうが適しています。
寿命の短いセッションの場合、django_tasklist/settings.py
で、キャッシュバックエンドを使用するように SESSION_ENGINE
を設定します。
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
寿命の長いセッションの場合、Django では、データベースに裏付けられたライトスルーキャッシュを使用できます。これは、永続性を保証しつつパフォーマンスを高めるための最良の選択肢です。ライトスルーキャッシュを使用するには、django_tasklist/settings.py
で、次のように SESSION_ENGINE
を設定します。
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
Django でセッションを使用する方法の詳細は、Django のセッションに関するドキュメントを参照してください。