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

May 10, 2012
Tweet

More Decks by Moscow Python Meetup

Other Decks in Programming

Transcript

  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. 6 Пока нет поддержки в Django Очень вероятно, что в

    Django 1.5 наряду с параметром  unique_together появится index_together.   https://code.djangoproject.com/ticket/5805
  5. 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]    
  6. 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]    
  7. 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  |   +-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐+-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐-­‐+  
  8. 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)    
  9. 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’) медленно при большом объеме данных
  10. 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  
  11. 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        
  12. 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  полей  
  13. 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  
  14. 18 Запросы в цикле posts  =  list(Post.objects.all()[:100])     {%

     for  post  in  posts  %}    {{  post.author.username  }}   {%  endfor  %}   В цикле происходит примерно следующее: User.objects.get(pk=post.author_id)  
  15. 19 Запросы в цикле Замеряем время цикла: ~250мс !? Да-да,

    мы слышали про select_related(), но неужели БД настолько уныла? Проверим!  
  16. 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)  
  17. 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  
  18. 23 Нужно отрендерить много-много всего… На это уходит большая часть

    времени Время рендеринга: Django ~1сек Jinja ~0.3сек С Jinja можно получить ускорение до 10 раз
  19. 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>  
  20. 25 Рендеринг на клиенте + Освобождаются драгоценные ресурсы сервера. -

    Нельзя использовать имеющиеся template-теги и фильтры
  21. 26 Кеширование   val  =  cache.get("somekey")   if  val  is

     None:          val  =  compute_val()          cache.set("somekey",  val)     Скорость повышается в разы! Но возникает проблема актуальности данных.
  22. 27 Инвалидация Пути решения: 1.  Инвалидация по таймауту   +

     Легко  реализовать   -­‐  Не гарантируется актуальность данных 2.  Инвалидация по событию   + Данные всегда актуальны - Есть некоторые сложности (решаемые)  
  23. 28 Инвалидация по событию @receiver(post_save,  sender=Game)   def  invalidate_games(**kwargs):  

           cache.delete(‘game_list’)     А что если ключ с суффиксом: ‘game_list:%s’  %  suffix     Как инвалидировать все комбинации?
  24. 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’)  
  25. 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)  
  26. 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’)  
  27. 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’)  
  28. 33 Оптимизация Иногда есть смысл оптимизировать код, работающий лишь несколько

    миллисекунд: •  Middleware •  Context processors •  Template tags в базовом шаблоне Если среднее время ответа 100мс, а время работы middleware – 11мс, то снизив его до 1мс мы сможем обслуживать на 10% больше запросов.
  29. 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)