Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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]    

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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]    

Slide 8

Slide 8 text

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]    

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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)    

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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  

Slide 14

Slide 14 text

14 Django Debug Toolbar

Slide 15

Slide 15 text

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        

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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  

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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)  

Slide 21

Slide 21 text

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  

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

    Slide 25

    Slide 25 text

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

    Slide 26

    Slide 26 text

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

    Slide 27

    Slide 27 text

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

    Slide 28

    Slide 28 text

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

    Slide 29

    Slide 29 text

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

    Slide 30

    Slide 30 text

    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)  

    Slide 31

    Slide 31 text

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

    Slide 32

    Slide 32 text

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

    Slide 33

    Slide 33 text

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

    Slide 34

    Slide 34 text

    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)