Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

Иван Вирабян

По большей части речь шла об ORM. Иван объяснил, почему «db_index=True не творит чудеса», показал каким иногда непредсказуемым может быть генератор SQL, объяснил как нужно использовать «Defer()», а так же описал основные подходы к инвалидации кеша и оптимизации контекст процессоров в Django. Самое продвинутое выступление явно не для новичков, жаль, что всего лишь на 10 минут, такая тема достойна больших временных рамок. Обязательно посмотрите слайды, очень полезный и наиболее прикладной доклад.

Moscow Python Meetup
PRO

May 10, 2012
Tweet

More Decks by Moscow Python Meetup

Other Decks in Programming

Transcript

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

    View Slide

  2. 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’)  
     

    View Slide

  3. 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]  
     

    View Slide

  4. 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);  
     

    View Slide

  5. 5
    Замеры
    Таблица содержащая 200 тыс. записей:
    Три одиночных индекса: 170мс
    Комбинированный индекс: 1мс

    View Slide

  6. 6
    Пока нет поддержки в
    Django
    Очень вероятно, что в Django 1.5 наряду с
    параметром  unique_together появится
    index_together.  
    https://code.djangoproject.com/ticket/5805

    View Slide

  7. 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]  
     

    View Slide

  8. 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]  
     

    View Slide

  9. 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  |  
    +-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐+-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐+  

    View Slide

  10. 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)  
     

    View Slide

  11. 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’)
    медленно при большом объеме данных

    View Slide

  12. 12
    Что это запросы?
    users  =  User.objects.filter(username__in=[‘vasya’,  ‘petya’])  
    Post.objects.filter(author__in=users)  
     
     
     
             
     
     
    Post.objects.filter(author__isnull=True)  
     

    View Slide

  13. 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  

    View Slide

  14. 14
    Django Debug Toolbar

    View Slide

  15. 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  
     
     
     

    View Slide

  16. 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  полей  

    View Slide

  17. 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  

    View Slide

  18. 18
    Запросы в цикле
    posts  =  list(Post.objects.all()[:100])  
     
    {%  for  post  in  posts  %}  
     {{  post.author.username  }}  
    {%  endfor  %}  
    В цикле происходит примерно следующее:
    User.objects.get(pk=post.author_id)  

    View Slide

  19. 19
    Запросы в цикле
    Замеряем время цикла: ~250мс !?
    Да-да, мы слышали про select_related(),
    но неужели БД настолько уныла?
    Проверим!
     

    View Slide

  20. 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)  

    View Slide

  21. 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  

    View Slide

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

    View Slide

  23. 23
    Нужно отрендерить
    много-много всего…
    На это уходит большая часть времени
    Время рендеринга:
    Django ~1сек
    Jinja ~0.3сек
    С Jinja можно получить
    ускорение до 10 раз

    View Slide

  24. 24
    Рендеринг на клиенте
     <br/>        <li>${title}  <div>${views}</div></li>  <br/>  
     
     <br/>        $(function()  {  <br/>                var  template  =  $('#post-­‐tmpl');  <br/>                $.tmpl(template,  {{  posts  }}).appendTo('#posts');  <br/>        });  <br/>  
     
     

    View Slide

  25. 25
    Рендеринг на клиенте
    + Освобождаются драгоценные ресурсы
    сервера.
    - Нельзя использовать имеющиеся template-теги
    и фильтры

    View Slide

  26. 26
    Кеширование
     
    val  =  cache.get("somekey")  
    if  val  is  None:  
           val  =  compute_val()  
           cache.set("somekey",  val)  
     
    Скорость повышается в разы!
    Но возникает проблема актуальности
    данных.

    View Slide

  27. 27
    Инвалидация
    Пути решения:
    1.  Инвалидация по таймауту  
    +  Легко  реализовать  
    -­‐  Не гарантируется актуальность данных
    2.  Инвалидация по событию  
    + Данные всегда актуальны
    - Есть некоторые сложности (решаемые)  

    View Slide

  28. 28
    Инвалидация по событию
    @receiver(post_save,  sender=Game)  
    def  invalidate_games(**kwargs):  
           cache.delete(‘game_list’)  
     
    А что если ключ с суффиксом:
    ‘game_list:%s’  %  suffix  
     
    Как инвалидировать все комбинации?

    View Slide

  29. 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’)  

    View Slide

  30. 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)  

    View Slide

  31. 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’)  

    View Slide

  32. 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’)  

    View Slide

  33. 33
    Оптимизация
    Иногда есть смысл оптимизировать код,
    работающий лишь несколько миллисекунд:
    •  Middleware
    •  Context processors
    •  Template tags в базовом шаблоне
    Если среднее время ответа 100мс, а время
    работы middleware – 11мс, то снизив его до
    1мс мы сможем обслуживать на 10%
    больше запросов.

    View Slide

  34. 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)  

    View Slide