Share
Embed
Производительность в Django
Иван Вирабян
По большей части речь шла об ORM. Иван объяснил, почему «db_index=True не творит чудеса», показал каким иногда непредсказуемым может быть генератор SQL, объяснил как нужно использовать «Defer()», а так же описал основные подходы к инвалидации кеша и оптимизации контекст процессоров в Django. Самое продвинутое выступление явно не для новичков, жаль, что всего лишь на 10 минут, такая тема достойна больших временных рамок. Обязательно посмотрите слайды, очень полезный и наиболее прикладной доклад.
Moscow Python Meetup PRO
May 10, 2012
Transcript
34 slides
-
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’) -
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] -
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_created ON blog_post(category, is_editorial, created); -
5 Замеры Таблица содержащая 200 тыс. записей: Три одиночных индекса:
170мс Комбинированный индекс: 1мс -
6 Пока нет поддержки в Django Очень вероятно, что в
Django 1.5 наряду с параметром unique_together появится index_together. https://code.djangoproject.com/ticket/5805 -
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] -
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] -
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 | +-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐+-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐+ -
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) -
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’) медленно при большом объеме данных -
12 Что это запросы? users = User.objects.filter(username__in=[‘vasya’, ‘petya’]) Post.objects.filter(author__in=users)
Post.objects.filter(author__isnull=True) -
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 -
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 -
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 полей -
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 -
18 Запросы в цикле posts = list(Post.objects.all()[:100]) {%
for post in posts %} {{ post.author.username }} {% endfor %} В цикле происходит примерно следующее: User.objects.get(pk=post.author_id) -
19 Запросы в цикле Замеряем время цикла: ~250мс !? Да-да,
мы слышали про select_related(), но неужели БД настолько уныла? Проверим! -
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) -
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 -
23 Нужно отрендерить много-много всего… На это уходит большая часть
времени Время рендеринга: Django ~1сек Jinja ~0.3сек С Jinja можно получить ускорение до 10 раз -
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> -
25 Рендеринг на клиенте + Освобождаются драгоценные ресурсы сервера. -
Нельзя использовать имеющиеся template-теги и фильтры -
26 Кеширование val = cache.get("somekey") if val is
None: val = compute_val() cache.set("somekey", val) Скорость повышается в разы! Но возникает проблема актуальности данных. -
27 Инвалидация Пути решения: 1. Инвалидация по таймауту +
Легко реализовать -‐ Не гарантируется актуальность данных 2. Инвалидация по событию + Данные всегда актуальны - Есть некоторые сложности (решаемые) -
28 Инвалидация по событию @receiver(post_save, sender=Game) def invalidate_games(**kwargs):
cache.delete(‘game_list’) А что если ключ с суффиксом: ‘game_list:%s’ % suffix Как инвалидировать все комбинации? -
29 Инвалидация по событию Вариант 1: список всех имеющихся ключей
key = ‘game_list:%s’ % suffix val = 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’) -
30 Инвалидация по событию Вариант 2: версионирование key = ‘game_list:%s’
% suffix version = 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) -
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’) -
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’) -
33 Оптимизация Иногда есть смысл оптимизировать код, работающий лишь несколько
миллисекунд: • Middleware • Context processors • Template tags в базовом шаблоне Если среднее время ответа 100мс, а время работы middleware – 11мс, то снизив его до 1мс мы сможем обслуживать на 10% больше запросов. -
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)