Производительность в Django

34
1 Производительность в Django Иван Вирабян [email protected]

description

Заметки к презентации: http://dl.dropbox.com/u/23767208/django-perfomance-notes.pdf

Transcript of Производительность в Django

Page 1: Производительность в Django

1

Производительность в Django

Иван Вирабян

[email protected]

Page 2: Производительность в Django

2

db_index=True не творит чудеса

class Post(models.Model):category = models.CharField()is_editorial = models.BooleanField()created = models.DateTimeField()

Post.objects.filter(category=‘news’, is_editorial=True) \ .order_by(‘-created’)

Page 3: Производительность в Django

3

db_index=True не творит чудеса

class Post(models.Model):category = models.CharField(db_index=True)is_editorial =

models.BooleanField(db_index=True)created = models.DateTimeField(db_index=True)

Post.objects.filter(category=‘news’, is_editorial=True) \ .order_by(‘-created’)[:30]

Page 4: Производительность в Django

4

db_index=True не творит чудеса

class Post(models.Model):category = models.CharField(db_index=True)is_editorial =

models.BooleanField(db_index=True)created = models.DateTimeField(db_index=True)

Post.objects.filter(category=‘news’, is_editorial=True) \ .order_by(‘-created’)[:30]

blog/sql/post.sql (либо в миграции south):CREATE INDEX idx_category_editorial_createdON blog_post(category, is_editorial, created);

Page 5: Производительность в Django

5

Замеры

Таблица содержащая 200 тыс. записей:

Три одиночных индекса: 170мсКомбинированный индекс: 1мс

Page 6: Производительность в Django

6

Пока нет поддержки в Django

Очень вероятно, что в Django 1.5 наряду с параметром unique_together появится index_together.

https://code.djangoproject.com/ticket/5805

Page 7: Производительность в Django

7

db_index=True не творит чудеса

class Post(models.Model): category = models.CharField() is_editorial = models.BooleanField() created = models.DateTimeField()

class Meta: index_together = (‘category’, ‘is_editorial, ‘created’)

Post.objects.filter(category=‘news’, is_editorial=True) \ .order_by(‘-created’)[:30]

Page 8: Производительность в Django

8

Денормализация

class Post(models.Model): category = models.CharField(max_length=100) author = models.ForeignKey(Profile, related_name=‘posts’)

Post.objects.filter(category=‘news’).order_by(‘author__name’)[:30]

Page 9: Производительность в Django

9

mysql> show profile;+----------------------+----------+| Status | Duration |+----------------------+----------+| init | 0.000039 || optimizing | 0.000029 || statistics | 0.000148 || preparing | 0.000028 || Creating tmp table | 0.000073 || executing | 0.000010 || Copying to tmp table | 4.347774 || Sorting result | 0.090562 || Sending data | 0.000132 || removing tmp table | 0.000024 || query end | 0.000012 || freeing items | 0.000060 || cleaning up | 0.000014 |+----------------------+----------+

Page 10: Производительность в Django

10

Выход есть!

class Post(models.Model): category = models.CharField(max_length=100) author = models.ForeignKey(Profile, related_name=‘posts’) author_name = models.CharField(max_length=100)

Post.objects.filter(category=‘news’).order_by(‘author_name’)

@receiver(signals.post_save, sender=User)def update_author_name(instance, **kwargs): instance.posts.update(author_name=instance.name)

Page 11: Производительность в Django

11

Multi-table Inheritanceиспользовать с осторожностью!

class Employee(models.Model): name = models.CharField(max_length=100) salary = models.PositiveIntegerField()

class Developer(Employee): lang = models.CharField(max_length=50)

Developer.objects.filter(lang=‘python’).order_by(‘salary’)

медленно при большом объеме данных

Page 12: Производительность в Django

12

Что это запросы?

users = User.objects.filter(username__in=[‘vasya’, ‘petya’]) Post.objects.filter(author__in=users)

Post.objects.filter(author__isnull=True)

Page 13: Производительность в Django

13

Что это запросы?

users = User.objects.filter(username__in=[‘vasya’, ‘petya’]) Post.objects.filter(author__in=users)

SELECT ... FROM `blog_post`WHERE `blog_post`.`author_id` IN ( SELECT U0.`id` FROM `auth_user` U0 WHERE U0.`username` IN ('vasya', 'petya'))

Post.objects.filter(author__isnull=True)

SELECT ... FROM `blog_post`LEFT OUTER JOIN `auth_user`

ON (`blog_post`.`author_id` = `auth_user`.`id`)WHERE `auth_user`.`id` IS NULL

Page 14: Производительность в Django

14

Django Debug Toolbar

Page 15: Производительность в Django

15

Defer() может быть медленней!

>>> timeit('list(Post.objects.all()[:30])', 'from blog.models import Post', number=100)1.3283531665802002

>>> timeit('list(Post.objects.defer(“author“, “body”)[:30])', 'from blog.models import Post', number=100)1.6067261695861816

Page 16: Производительность в Django

16

Причина?

В случае с defer() в Model.__init__() значения полей передаются как **kwargs

def __init__(self, *args, **kwargs): ... fields_iter = iter(self._meta.fields) if not kwargs: for val, field in izip(args, fields_iter): setattr(self, field.attname, val) # Далее идет длинный (~70 строк) код для случая с kwargs. # Этот код работает на 33% медленней. # Сильно тормозит хак для "умной" обработки ForeignKey полей

Page 17: Производительность в Django

17

Вывод?.defer() имеет смысл только когда из выборки исключается большая часть полей. Но тогда проще использовать .only():

>>> timeit('list(Post.objects.only(“title”)[:30])', 'from blog.models import Post', number=100)

0.88186287879943848

Если нужно только данные (без методов) то .values() будет однозначно быстрее:

>>> timeit('list(Post.objects.values("id", “title")[:30])', 'from blog.models import Post',

number=100)0.25724387168884277

Page 18: Производительность в Django

18

Запросы в цикле

posts = list(Post.objects.all()[:100])

{% for post in posts %}{{ post.author.username }}

{% endfor %}

В цикле происходит примерно следующее:

User.objects.get(pk=post.author_id)

Page 19: Производительность в Django

19

Запросы в цикле

Замеряем время цикла: ~250мс !?

Да-да, мы слышали про select_related(), но неужели БД настолько уныла?

Проверим!

Page 20: Производительность в Django

20

ncalls tottime cumtime percall filename:lineno(function) 100 0.002 0.283 0.003 manager.py:131(get) 100 0.001 0.276 0.003 query.py:337(get)2800/1600 0.001 0.192 0.000 {len} 100 0.001 0.191 0.002 query.py:74(__len__) 200 0.003 0.190 0.001 query.py:214(iterator) 200 0.002 0.172 0.001 compiler.py:673(results_iter) 100 0.001 0.159 0.002 compiler.py:711(execute_sql) 100 0.005 0.086 0.001 util.py:31(execute) 100 0.000 0.075 0.001 base.py:84(execute) 100 0.002 0.075 0.001 cursors.py:139(execute) 100 0.000 0.070 0.001 cursors.py:315(_query) 200 0.002 0.068 0.000 query.py:752(_clone) 200 0.006 0.066 0.000 query.py:223(clone) 100 0.001 0.063 0.001 cursors.py:277(_do_query) 100 0.058 0.058 0.001 {method 'query' of '_mysql.connection' objects} 4300/800 0.018 0.057 0.000 copy.py:144(deepcopy)

Page 21: Производительность в Django

21

К чему нам это знать?

Вопросы на stackoverflow.com:

Say, I have a page with a photo gallery. Each thumbnail has

e.g. a photo, country, author and so on. And it is very slow.

I have performed some profiling using django-debug-toolbar:

SQL Queries: default 84.81 ms (147 queries)

But:

Total CPU time: 5768.360 msec

Page 22: Производительность в Django

22

Шаблонизатор(безбожно тормозит)

Page 23: Производительность в Django

23

Нужно отрендеритьмного-много всего…На это уходит большая часть времени

Время рендеринга:

Django ~1секJinja ~0.3сек

С Jinja можно получитьускорение до 10 раз

Иван
Page 24: Производительность в Django

24

Рендеринг на клиенте<script type="text/x-jquery-tmpl" id="post-tmpl"> <li>${title} <div>${views}</div></li></script>

<script type="text/javascript"> $(function() { var template = $('#post-tmpl'); $.tmpl(template, {{ posts }}).appendTo('#posts'); });</script>

<ul id=“posts"></ul>

Page 25: Производительность в Django

25

Рендеринг на клиенте+ Освобождаются драгоценные ресурсы сервера.

- Нельзя использовать имеющиеся template-теги и фильтры

Page 26: Производительность в Django

26

Кеширование

val = cache.get("somekey")if val is None: val = compute_val() cache.set("somekey", val)

Скорость повышается в разы!

Но возникает проблема актуальности данных.

Page 27: Производительность в Django

27

Инвалидация

Пути решения:1. Инвалидация по таймауту

+ Легко реализовать- Не гарантируется актуальность данных

2. Инвалидация по событию+ Данные всегда актуальны- Есть некоторые сложности (решаемые)

Page 28: Производительность в Django

28

Инвалидация по событию@receiver(post_save, sender=Game)def invalidate_games(**kwargs): cache.delete(‘game_list’)

А что если ключ с суффиксом:

‘game_list:%s’ % suffix

Как инвалидировать все комбинации?

Page 29: Производительность в Django

29

Инвалидация по событиюВариант 1: список всех имеющихся ключейkey = ‘game_list:%s’ % suffixval = cache.get(key)if val is None:

val = ...cache.set(key, val)# Запомним этот ключик для последующей инвалидацииkeys = cache.get(‘game_list_keys’, [])cache.set(‘game_list_keys’, set(keys) | set([key]))

# Инвалидацияkeys = cache.get(‘game_list_keys’)cache.delete_many(keys)cache.delete(‘game_list_keys’)

Page 30: Производительность в Django

30

Инвалидация по событиюВариант 2: версионированиеkey = ‘game_list:%s’ % suffixversion = cache.get(‘top_games_version’, 1)val = cache.get(key, version=version)if val is None: val = ... cache.set(key, val, version=version)

# Инвалидацияtry:

cache.incr(‘top_games_version’)except ValueError:

cache.set(‘top_games_version’, 1)

Page 31: Производительность в Django

31

Инвалидация по событиюМы используем удобный декоратор:gametags.py:

@register.simple_tag@cached(vary_on_args=True)def games(platform=None, genre=None): ...

signals.py:@receiver(post_save, sender=Game)def inval_games(**kwargs):

invalidate(‘games.templatetags.gametags.games’)

Page 32: Производительность в Django

32

Инвалидация по событиюМы используем удобный декоратор:gametags.py:

@register.simple_tag@cached(vary_on_args=True, locmem=True)def games(platform=None, genre=None): ...

signals.py:@receiver(post_save, sender=Game)def inval_games(**kwargs):

invalidate(‘games.templatetags.gametags.games’)

Page 33: Производительность в Django

33

Оптимизация

Иногда есть смысл оптимизировать код,

работающий лишь несколько миллисекунд:

• Middleware• Context processors• Template tags в базовом шаблоне

Если среднее время ответа 100мс, а время

работы middleware – 11мс, то снизив его до

1мс мы сможем обслуживать на 10%

больше запросов.

Page 34: Производительность в Django

34

Делайте их ленивыми

Вы не знаете наверняка, пригодится ли где-нибудь то, что вы насчитали в своем context processor’е. Поэтомуmiddleware и context processors должны быть ленивыми!

from django.utils.functional import lazy

class LocationMiddleware(object):def process_request(self, request):

request.location = lazy(get_location, dict)(request)

def get_location(request):g = GeoIP()remote_ip = request.META.get('REMOTE_ADDR')return g.city(remote_ip)