Производительность в Django
-
Upload
moscowdjango -
Category
Technology
-
view
4.215 -
download
0
description
Transcript of Производительность в 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’)
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_createdON 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
14
Django Debug Toolbar
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
22
Шаблонизатор(безбожно тормозит)
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’ % 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’)
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)
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)