Донецкий техникум промышленной автоматики

Оптимізація продуктивності Django проектів (частина 3)

  1. Кеш фреймворк Django
  2. Кешування всього сайту
  3. кешування view
  4. Кешування частини шаблону
  5. низькорівневе кешування
  6. cached_property
  7. Cacheops
  8. HTTP кешування
  9. Vary
  10. Cache-Control
  11. Last-Modified & Etag
  12. Кешування статичних файлів

Інші статті циклу:

У цій частині серії ми розглянемо найважливіший підхід до забезпечення високої продуктивності - кешування. Суть кешування в тому, щоб розміщувати часто використовувані дані в швидкому сховище для прискорення доступу до них. Важливо зрозуміти, що швидке сховище (наприклад, оперативна пам'ять) часто має дуже обмежений обсяг і його потрібно використовувати для зберігання тільки тих даних, які з великою ймовірністю будуть запитані.

Кеш фреймворк Django

Django надає ряд засобів для кешування з коробки. Сховище кешу налаштовується за допомогою словника CACHES в settings.py:

CACHES = { "default": { "BACKEND": "django.core.cache.backends.db.DatabaseCache", "LOCATION": "my_cache_table",}}

Django надає кілька вбудованих бекендов для кеша, розглянемо деякі з них:

  • DummyCache - нічого не кешируєт, використовується при розробці / тестуванні, якщо потрібно тимчасово відключити кешування,
  • DatabaseCache - зберігає кеш в БД, не найшвидший варіант, але може бути корисний для зберігання результатів довгих обчислень або складних SQL запитів,
  • MemcachedCache - використовує Memcached в якості сховища, для використання цього бекенда вам знадобиться підняти сервер (и) Memcached.

Для використання в продакшені найкраще підходить MemcachedCache і в деяких випадках може бути корисний DatabaseCache. Також Django дозволяє використовувати сторонні бекенди, наприклад, вдалим варіантом може бути використання Redis як сховище для кеша. Redis надає більше можливостей ніж Memcached і ви швидше за все і так вже використовуєте його в вашому проекті. Ви можете встановити пакет django-redis і налаштувати його як бекенд для вашого кешу.

Кешування всього сайту

Якщо на вашому сайті немає динамічного контенту, який часто змінюється, то ви можете вирішити проблему кешування просто - включивши кешування всього сайту. Для цього потрібно додати кілька налаштувань в settings.py:

MIDDLEWARE = ​​[ 'django.middleware.cache.UpdateCacheMiddleware', # place all other middlewares here 'django.middleware.cache.FetchFromCacheMiddleware',] # Key in `CACHES` dict CACHE_MIDDLEWARE_ALIAS = 'default' # Additional prefix for cache keys CACHE_MIDDLEWARE_KEY_PREFIX = '' # Cache key TTL in seconds CACHE_MIDDLEWARE_SECONDS = 600

Після додавання показаних вище middleware першим і останнім у списку, все GET і HEAD запити будуть кешуватися на вказане в параметрі CACHE_MIDDLEWARE_SECONDS час.

При необхідності ви навіть можете програмно скидати кеш:

from django.core.cache import caches cache = caches [ 'default'] # `default` is a key from CACHES dict in settings.py ache. clear ()

Або можна скинути кеш безпосередньо в використовуваному сховище. Наприклад, для Redis:

$ Redis-cli -n 1 FLUSHDB # 1 is a DB number specified in settings.py

кешування view

Якщо у вашому випадку недоцільно кешувати весь сайт, то ви можете включити кешування тільки певних view, які створюють найбільше навантаження. Для цього Django надає декоратор cache_page:

from django.views.decorators.cache import cache_page @cache_page (600, cache = 'default', key_prefix = '') def author_page_view (request, username): author = get_object_or_404 (Author, username = username) show_articles_link = author. articles. exists () return render (request, 'blog / author.html', context = dict (author = author, show_articles_link = show_articles_link))

cache_page приймає такі параметри:

  • перший обов'язковий аргумент задає TTL кеша в секундах,
  • cache - ключ в словнику CACHES,
  • key_prefix - префікс для ключів кеша.

Також цей декоратор можна застосувати в urls.py, що зручно для Class-Based Views:

urlpatterns = [url (r '^ $', cache_page (600) (ArticlesListView. as_view ()), name = 'articles_list'), ...]

Якщо частина контенту сайту змінюється в залежності, наприклад, від того який користувач аутентифікований, то такий підхід не підійде. Для вирішення цієї проблеми можна скористатися одним з варіантів описаних нижче.

Кешування частини шаблону

У попередній частині цієї серії статей було описано, що QuerySet об'єкти ледачі і SQL запити не виконуються без крайньої необхідності. Ми можемо скористатися цим і закешовану фрагменти шаблону, що дозволить уникнути SQL запитів на час життя кешу. Для цього потрібно скористатися тегом шаблону cache:

{% Load cache%} <h1> Articles list </ h1> <p> Authors count: {{authors_count}} </ p> <h2> Top authors </ h2> {% cache 500 top_author%} <ul> { % for author in top_authors%} <li> {{author.username}} ({{author.articles_count}}) </ li> {% endfor%} </ ul> {% endcache%} {% cache 500 articles_list% } {% for article in articles%} <article> <h2> {{article.title}} </ h2> <time> {{article.created_at}} </ time> <p> Author: <a href = " {% url 'author_page' username = article.author.username%} "> {{article.author.username}} </ a> </ p> <p> Tags: {% for tag in article.tags.all% } {{tag}} {% if not forloop.last%}, {% endif%} {% endfor%} </ article> {% endfor%} {% endcache%}

Результат додавання тегів cache в шаблон (до і після відповідно):

cache приймає такі аргументи:

  • перший обов'язковий аргумент означає TTL кеша в секундах,
  • обов'язкове назву фрагмента,
  • необов'язкові додаткові змінні, які ідентифікують фрагмент по динамічним даними,
  • ключовий параметр using = 'default', повинен відповідати ключу словника CACHES в settings.py.

Наприклад, якщо потрібно, щоб для кожного користувача фрагмент кешуватися окремо, то потрібно передати в тег cache змінну яка ідентифікує користувача:

{% Cache 500 personal_articles_list request.user.username%} <! - ... -> {%%}

При необхідності можна передавати кілька таких змінних для створення ключів на основі комбінації їх значень.

низькорівневе кешування

Django надає доступ до низкорівневому API кеш фреймворка. Ви можете використовувати його для збереження / вилучення / видалення даних за певним ключу в кеші. Розглянемо невеликий приклад:

from django.core.cache import cache class ArticlesListView (ListView): ... def get_context_data (self, ** kwargs): context = super (). get_context_data (** kwargs) authors_count = cache. get ( 'authors_count') if authors_count is None: authors_count = Author. objects. count () cache. set ( 'authors_count', authors_count) context [ 'authors_count'] = authors_count ... return context

У цьому фрагменті коду ми перевіряємо, чи є в кеші кількість авторів, яке повинно бути по ключу authors_count. Якщо є (cache.get повернувся не None), то використовуємо значення з кешу. Інакше запитуємо значення з БД і зберігаємо в кеш. Таким чином протягом часу життя ключа в кеші ми більше не будемо звертатися до БД.

Крім результатів запитів до БД, також є сенс кешувати результати складних обчислень або звернення до зовнішніх сервісів. Важливо при цьому враховувати, що дані можуть зміниться і в кеші буде застаріла інформація. Для того, щоб мінімізувати ймовірність використання застарілих даних з кешу потрібно:

  • налаштувати адекватне TTL для кешу, яке б відповідало частоті зміни Кешована даних,
  • реалізувати інвалідацію кеша.

Інвалідація кеша повинна відбуватися за подією зміни даних. Розглянемо, як можна реалізувати інвалідацію для прикладу з кількістю авторів:

from django.db.models.signals import post_delete, post_save from django.dispatch import receiver from django.core.cache import cache def clear_authors_count_cache (): cache. delete ( 'authors_count') @receiver (post_delete, sender = Author) def author_post_delete_handler (sender, ** kwargs): clear_authors_count_cache () @receiver (post_save, sender = Author) def author_post_save_handler (sender, ** kwargs): if kwargs [ 'created']: clear_authors_count_cache ()

Були додані 2 обробника сигналів: створення і видалення автора. Тепер при зміні кількості авторів значення в кеші по ключу authors_count буде скидатися і в view буде запитуватися нове кількість авторів з БД.

cached_property

Крім кеш фреймворка Django також надає можливість кешувати звернення до функції прямо в пам'яті процесу. Такий вид кешу можливий тільки для методів не беруть ніяких параметрів крім self. Такий кеш буде жити до тих пір поки існує відповідний об'єкт.

cached_property це декоратор входить в Django. Результат застосування його до методу, крім кешування, метод стає властивістю і викликається неявно без необхідності вказівки круглих дужок. Розглянемо приклад:

class Author (models. Model): username = models. CharField (max_length = 64, db_index = True) email = models. EmailField () bio = models. TextField () @cached_property def articles_count (self): return self. articles. count ()

Перевіримо як працює властивість article_count з включеним логування SQL:

>>> from blog.models import Author >>> author = Author. objects. first () (0.002) SELECT "blog_author". "Id", "blog_author". "Username", "blog_author". "Email", "blog_author". "Bio" FROM "blog_author" ORDER BY "blog_author". "Id" ASC LIMIT 1; args = () >>> author. articles_count (0.001) SELECT COUNT (*) AS "__count" FROM "blog_article" WHERE "blog_article". "Author_id" = 142601; args = (142601,) 28 >>> author. articles_count 28

Як ви бачите, повторне звернення до властивості article_count не викликає SQL запит. Але якщо ми створимо ще один екземпляр автора, то в ньому це властивість не буде закешовану, до того як ми вперше до нього звернемося, тому що кеш в даному випадку прив'язаний до примірника класу Author.

Cacheops

django-cacheops це сторонній пакет, який дозволяє дуже швидко впровадити кешування запитів до БД практично не змінюючи код проекту. Більшу частину випадків можна вирішити просто задавши ряд налаштувань цього пакета в settings.py.

Розглянемо на прикладі простої варіант використання цього пакета. В якості тестового проекту будемо використовувати приклад з минулій частині серії.

Cacheops використовує Redis як сховище кешу, в settings.py потрібно вказати параметри підключення до сервера Redis.

CACHEOPS_REDIS = "redis: // localhost: 6379/1" INSTALLED_APPS = [... 'cacheops',] CACHEOPS = { 'blog. *': { 'Ops': 'all', 'timeout': 60 * 15} , '*. *': { 'timeout': 60 * 60},}

Ось так просто ми додали кешування всіх запитів до моделей з програми blog на 15 хвилин. При цьому cacheops автоматично налаштовує інвалідацію кешу не тільки по часу, але і по подіям поновлення даних, за допомогою сигналів відповідних моделей.

При необхідності можна налаштувати кешування не тільки всіх моделей додатки але і кожну модель окремо і для різних запитів. Кілька прикладів:

CACHEOPS = { 'blog.author': { 'ops': 'all', 'timeout': 60 * 60}, # cache all queries to `Author` model for an hour 'blog.article': { 'ops': 'fetch', 'timeout': 60 * 10}, # cache `Article` fetch queries for 10 minutes # Or 'blog.article': { 'ops': 'get', 'timeout': 60 * 15}, # cache `Article` get queries for 15 minutes # Or 'blog.article': { 'ops': 'count', 'timeout': 60 * 60 * 3}, # cache` Article` fetch queries for 3 hours '*. * ': {' timeout ': 60 * 60},}

Крім цього cacheops має ряд інших функцій, деякі з них:

  • ручне кешування Article.objects.filter (tag = 2) .cache (),
  • кешування результатів виконання функцій з прив'язкою до моделі і автоматичної інвалідаціей,
  • кешування view з прив'язкою до моделі і автоматичної інвалідаціей,
  • кешування фрагментів шаблону і багато іншого.

Рекомендую ознайомиться з README cacheops щоб дізнатися подробиці.

HTTP кешування

Якщо ваш проект використовує HTTP, то крім серверного кешування ви також можете використовувати вбудовані в HTTP протокол механізми кешування. Вони дозволяють налаштувати кешування результатів безпечних запитів (GET і HEAD) на клієнті (наприклад, браузері) і на проміжних проксі-серверах.

Управління кешуванням здійснюється за допомогою HTTP заголовків. Установку цих заголовків можна налаштувати в додатку або, наприклад, на web-сервері (Nginx, Apache, etc).

Django надає middleware і кілька зручних декораторів для управління HTTP кешем.

Vary

Заголовок Vary дозволяє задати список назв заголовків, значення в яких будуть враховуватися при створенні ключа кеша. Django надає view декоратор vary_on_headers для управління цим заголовком.

from django.views.decorators.vary import vary_on_headers @vary_on_headers ( 'User-Agent') def author_page_view (request, username): ...

В даному випадку, для різних значень заголовка User-Agent будуть різні ключі кеша.

Cache-Control

Заголовок Cache-Control дозволяє задавати різні параметри керуючі механізмом кешування. Для завдання цього заголовка можна використовувати вбудований в Django view декортатор cache_control .

from django.views.decorators.cache import cache_control @cache_control (private = True, max_age = 3600) def author_page_view (request, username): ...

Розглянемо деякі директиви заголовка Cache-Control:

  • public, private - дозволяє або забороняє кешування в публічному кеші (проксі серверах і тд). Це важливі директиви, які дозволяють убезпечити приватний контент, який повинен бути доступний тільки певним користувачам.
  • no-cache - відключає кешування, що змушує клієнт робити запит до сервера.
  • max-age - час в секундах, після якого вважається, що контент застарів і його треба запросити заново.

Last-Modified & Etag

HTTP протокол надає і більш складний механізм кешування, який дозволяє уточнювати у сервера є актуальною кешована версія контенту за допомогою умовних запитів. Для роботи цього механізму сервер повинен віддавати такі заголовки (один з них або обидва):

  • Last-Modified - дата і час останньої зміни ресурсу.
  • Etag - ідентифікатор версії ресурсу (унікальний хеш або номер версії).

Після цього при повторному зверненні до ресурсу клієнт повинен використовувати заголовки If-Modified-Since і If-None-Match відповідно. У такому випадку, якщо ресурс не змінився (виходячи з значень Etag і / або Last-Modified), то сервер поверне статус 304 без тіла відповіді. Це дозволяє виконувати повторну завантаження ресурсу тільки в тому випадку, якщо він змінився і тим самим зекономити час і ресурси сервера.

Крім кешування, описані вище заголовки застосовуються для перевірки передумов в запитах змінюють ресурс (POST, PUT і тд). Але обговорення цього питання виходить за рамки даної статті.

Django надає кілька способів завдання заголовків Etag і Last-Modified. Найпростіший спосіб - використання ConditionalGetMiddleware. Цей middleware додає заголовок Etag, на основі відповіді view, до всіх GET запитах програми. Також він перевіряє заголовки запиту і повертає 304, якщо ресурс не змінився.

Цей підхід має ряд недоліків:

  • middleware застосовується відразу до всіх view проекту, що не завжди потрібно,
  • для перевірки актуальності ресурсу необхідно згенерувати повну відповідь view,
  • працює тільки для GET запитів.

Для тонкої настройки потрібно застосовувати декоратор condition, який дозволяє задавати кастомниє функції для генерації заголовків Etag і / або Last-Modified. У цих функціях можна реалізувати більш економний спосіб визначення версії ресурсу, наприклад, на основі поля в БД, без необхідності створення повної відповіді view.

# Models.py class Author (models. Model): ... updated_at = models. DateTimeField (auto_now = True) # views.py from django.views.decorators.http import condition def author_updated_at (request, username): updated_at = Author. objects. filter (username = username). values_list ( 'updated_at', flat = True) if updated_at: return updated_at [0] return None @condition (last_modified_func = author_updated_at) def author_page_view (request, username): ...

У функції author_updated_at виконується простий запит БД, який повертає дату останнього оновлення ресурсу, що вимагає значно менше ресурсів ніж отримання всіх потрібних даних для view з БД і рендеринг шаблону. При цьому при зміні учасника функція поверне нову дату, що призведе до інвалідаціі кеша.

Кешування статичних файлів

Для прискорення повторного завантаження сторінок рекомендується включити кешування статичних файлів, щоб браузер повторно не запитував ті ж скрипти, стилі, картинки і тд.

У продакшн оточенні ви швидше за все не будете віддавати статичні файли через Django, тому що це повільно і не безпечно. Для цього завдання зазвичай використовується Nginx або інший web-сервер. Розглянемо як налаштувати кешування статики на прикладі Nginx:

server {# ... location / static / {expires 360d; alias / home / www / proj / static /; } Location / media / {expires 360d; alias / home / www / proj / media /; }}

де,

  • / Static / це базовий шлях до статичних файлів, куди, в тому числі, копіюються файли при виконанні collectstatic,
  • / Media / базовий шлях до файлів завантажуваних користувачами.

В даному прикладі ми кешіруем всю статику на 360 днів. Важливо, щоб при зміні будь-якого статичного файлу, його URL також змінювався, що призведе до завантаження нової версії файлу. Для цього можна додавати GET параметри до файлів з номером версії: script.js? Version = 123. Але мені більше подобається використовувати Django Compressor , Який крім усього іншого, генерує унікальне ім'я для скриптів і стилів при їх зміні.

Js?