Más contenido relacionado

Presentaciones para ti(20)

Similar a Производительность в Django(20)

Último(20)

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

  1. Производительность в Django Иван Вирабян i.virabyan@gmail.com 1
  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’) 2
  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] 3
  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); 4
  5. Замеры Таблица содержащая 200 тыс. записей: Три одиночных индекса: 170мс Комбинированный индекс: 1мс 5
  6. Пока нет поддержки в Django Очень вероятно, что в Django 1.5 наряду с параметром unique_together появится index_together. https://code.djangoproject.com/ticket/5805 6
  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] 7
  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] 8
  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 | 9 +----------------------+----------+
  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) 10
  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’) медленно при большом объеме данных 11
  12. Что это запросы? users = User.objects.filter(username__in=[‘vasya’, ‘petya’]) Post.objects.filter(author__in=users) Post.objects.filter(author__isnull=True) 12
  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 13
  14. Django Debug Toolbar 14
  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 15
  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 полей 16
  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 17
  18. Запросы в цикле posts = list(Post.objects.all()[:100]) {% for post in posts %} {{ post.author.username }} {% endfor %} В цикле происходит примерно следующее: User.objects.get(pk=post.author_id) 18
  19. Запросы в цикле Замеряем время цикла: ~250мс !? Да-да, мы слышали про select_related(), но неужели БД настолько уныла? Проверим! 19
  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) 20
  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 21
  22. Шаблонизатор (безбожно тормозит) 22
  23. Нужно отрендерить много-много всего… На это уходит большая часть времени Время рендеринга: Django ~1сек Jinja ~0.3сек С Jinja можно получить ускорение до 10 раз 23
  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> 24
  25. Рендеринг на клиенте + Освобождаются драгоценные ресурсы сервера. - Нельзя использовать имеющиеся template-теги и фильтры 25
  26. Кеширование val = cache.get("somekey") if val is None: val = compute_val() cache.set("somekey", val) Скорость повышается в разы! Но возникает проблема актуальности данных. 26
  27. Инвалидация Пути решения: • Инвалидация по таймауту + Легко реализовать - Не гарантируется актуальность данных • Инвалидация по событию + Данные всегда актуальны - Есть некоторые сложности (решаемые) 27
  28. Инвалидация по событию @receiver(post_save, sender=Game) def invalidate_games(**kwargs): cache.delete(‘game_list’) А что если ключ с суффиксом: ‘game_list:%s’ % suffix Как инвалидировать все комбинации? 28
  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’) 29
  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) 30
  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’) 31
  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’) 32
  33. Оптимизация Иногда есть смысл оптимизировать код, работающий лишь несколько миллисекунд: • Middleware • Context processors • Template tags в базовом шаблоне Если среднее время ответа 100мс, а время работы middleware – 11мс, то снизив его до 1мс мы сможем обслуживать на 10% больше запросов. 33
  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) 34