Как я перетащил пакет Tastypie на Pony ORM и что из этого получилось.
Данная статья продолжает историю моей борьбы за производительность приложений на python и django.
Краткое содержание предыдущих серий:
— ORMы тормозят, причем очень сильно — в 3-5 и более раз
— А вот заляпочка для Django ORM
— Ух ты, вот этот ORM почти не тормозит и кеширует все что только можно
— Вот так можно прикрутить высокопроизводительный Pony ORM к проекту Django и использовать его в ключевых местах
Введение
Получив вполне удовлетворительные (по сравнению с Django ORM) результаты по скорости обращения к СУБД с помощью Pony ORM, я начал думать, как мне использовать их на практике. Для начала, я сделал отображение модели данных Django на модель данных Pony. Теперь, готовую модель данных можно использовать в проекте.
Там где код пишется с нуля, использовать модель Pony вместо Django стало не просто, а очень просто: атрибут `p` у всех моделей Django теперь указывает на модель Pony. Однако — что же делать с теми готовыми пакетами, которые уже используют Django ORM?
К сожалению, ответ один: переписывать или дописывать, поскольку синтаксис и семантика обращения к моделям и коллекциям у Pony ORM кардинально отличается от Django ORM — настолько, что замаскировать обращение к Pony ORM под обращение к Django ORM пока не представляется возможным. Кроме того, я сильно подозреваю, что такая маскировка, будучи произведена, убьет все преимущества использования Pony ORM за счет потерь на промежуточном слое. Все-таки python — интерпретатор, и потери например на вызове функции велики достаточно, чтобы быть заметными даже на фоне обращения к СУБД. Именно такие потери, в числе прочих, вероятно убивают производительность Django ORM и SQLAlchemy.
Мы планируем перевести весь интерфейс (или его бОльшую часть) нашего приложения на клиентскую сторону, используя сервер как поставщик данных через HTTP API. Таким образом, ключевым элементом становится API, которое мы реализуем с помощью пакета Tastypie, так что повышение его производительности самым непосредственным образом должно отразиться на производительности интерфейса приложения и снижении общей нагрузки на сайт.
И я взялся за Tastypie.
Как мне помогла структура Tastypie
Библиотека Tastypie, хоть и опирается на Django, имеет слой абстракции источников данных (Resource), независимый от Django ORM. Лишь следующий наследник, ModelResource, использует специфические для Django ORM обращения к данным. Таким образом, как я предполагал вначале, мне нужно было только унаследоваться от Resource и реализовать функционал, аналогичный ModelResource.
На практике, мне пришлось также сделать класс, параллельный ToManyField, поскольку к моему разочарованию, в последнем оказалось несколько строк, ориентированных именно на Django ORM. Свой класс я обозвал SetField, поскольку именно класс Set используется в Pony ORM для обозначения отношения один-к-многим.
Что получилось
Получилось все просто замечательно. В качестве образца, я использовал модель данных из django.contrib.auth, заполнив ее примерно 1000 пользователей. Одному из пользователей я присвоил 159 прав на различные типы данных в системе (их — типов данных — у нас много накопилось) и запрашивал этого пользователя вместе с его правами через API, используя этот вызов в качестве образца.
Вот как выглядело определение источника данных для старого API v2, использовавшего Django ORM (обратите внимание, что аутентификация и авторизация исключены — для корректного измерения производительности).
class PermissionResource(ModelResource):
class Meta:
queryset = auth_models.Permission.objects.all()
object_model = queryset.model
filtering = dict([(n,ALL_WITH_RELATIONS) for n in object_model._meta.get_all_field_names()])
resource_name = 'auth/permission'
class UserResource(ModelResource):
user_permissions = ToManyField(PermissionResource,'user_permissions',related_name='user_set',null=True)
class Meta:
queryset = auth_models.User.objects.all()
object_model = queryset.model
filtering = dict([(n,ALL_WITH_RELATIONS) for n in object_model._meta.get_all_field_names()])
resource_name = 'auth/user'
Ну а вот так выглядит определение для нового API v3, ориентированного на использование Pony ORM совместно с пакетом Djony:
class PermissionResource(DjonyResource):
class Meta:
object_model = auth_models.Permission
filtering = dict([(n,ALL_WITH_RELATIONS) for n in object_model._meta.get_all_field_names()])
resource_name = 'auth/permission'
class UserResource(DjonyResource):
user_permissions = SetField(PermissionResource,'user_permissions',related_name='user_set',null=True)
class Meta:
object_model = auth_models.User
filtering = dict([(n,ALL_WITH_RELATIONS) for n in object_model._meta.get_all_field_names()])
resource_name = 'auth/user'
Обратите внимание, что в определении метаданных используется модель Django а не Pony. Это связано с тем, что модель Pony не содержит многих атрибутов, характерных для Django ORM, но не нужных для определения собственно структуры данных, таких как например help_text. Вопрос о размещении таких метаданных на самом деле важен, но вероятно, его обсуждение выходит за рамки данной статьи.
Сервер был развернут на моем скромном рабочем компьютере, имеющим 2 ядра на 2.2 ГГц и 3 Гб оперативной памяти. Тестовый клиент запускался с того же компьютера. В качестве последнего, запускался пакет ab (Apache Benchmark):
seva@SEVA (2):~/djony$ ab -n 100 -c 4 "http://localhost:8080/api/v2/auth/user/3/?format=json"
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient).....done
Server Software: nginx/1.1.19
Server Hostname: localhost
Server Port: 8080
Document Path: /api/v2/auth/user/3/?format=json
Document Length: 5467 bytes
Concurrency Level: 4
Time taken for tests: 17.331 seconds
Complete requests: 100
Failed requests: 0
Write errors: 0
Total transferred: 582900 bytes
HTML transferred: 546700 bytes
Requests per second: 5.77 [#/sec] (mean)
Time per request: 693.256 [ms] (mean)
Time per request: 173.314 [ms] (mean, across all concurrent requests)
Transfer rate: 32.84 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 1.5 0 9
Processing: 313 685 486.1 575 3357
Waiting: 312 684 485.8 574 3355
Total: 313 685 486.7 575 3357
Percentage of the requests served within a certain time (ms)
50% 575
66% 618
75% 647
80% 670
90% 819
95% 1320
98% 2797
99% 3357
100% 3357 (longest request)
seva@SEVA (2):~/djony$ ab -n 100 -c 4 "http://localhost:8080/api/v3/auth/user/3/?format=json"
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient).....done
Server Software: nginx/1.1.19
Server Hostname: localhost
Server Port: 8080
Document Path: /api/v3/auth/user/3/?format=json
Document Length: 5467 bytes
Concurrency Level: 4
Time taken for tests: 8.339 seconds
Complete requests: 100
Failed requests: 0
Write errors: 0
Total transferred: 582900 bytes
HTML transferred: 546700 bytes
Requests per second: 11.99 [#/sec] (mean)
Time per request: 333.557 [ms] (mean)
Time per request: 83.389 [ms] (mean, across all concurrent requests)
Transfer rate: 68.26 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 137 317 375.9 243 2753
Waiting: 137 316 375.7 243 2751
Total: 137 317 375.9 243 2753
Percentage of the requests served within a certain time (ms)
50% 243
66% 264
75% 282
80% 299
90% 351
95% 433
98% 2670
99% 2753
100% 2753 (longest request)
Как видите, применение Pony ORM дало здесь повышение производительности API в 2 раза.
Во втором примере, я запрашиваю все объекты из таблицы. Здесь повышение производительности еще более значительно:
seva@SEVA (2):~/djony$ ab -n 20 -c 4 "http://localhost:8080/api/v2/auth/user/?format=json&limit=0"
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient).....done
Server Software: nginx/1.1.19
Server Hostname: localhost
Server Port: 8080
Document Path: /api/v2/auth/user/?format=json&limit=0
Document Length: 306326 bytes
Concurrency Level: 4
Time taken for tests: 40.891 seconds
Complete requests: 20
Failed requests: 0
Write errors: 0
Total transferred: 6133760 bytes
HTML transferred: 6126520 bytes
Requests per second: 0.49 [#/sec] (mean)
Time per request: 8178.157 [ms] (mean)
Time per request: 2044.539 [ms] (mean, across all concurrent requests)
Transfer rate: 146.49 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 0
Processing: 6235 7976 1035.4 7980 10671
Waiting: 6225 7959 1033.0 7958 10654
Total: 6235 7976 1035.4 7980 10671
Percentage of the requests served within a certain time (ms)
50% 7980
66% 8177
75% 8287
80% 8390
90% 10001
95% 10671
98% 10671
99% 10671
100% 10671 (longest request)
seva@SEVA (2):~/djony$ ab -n 20 -c 4 "http://localhost:8080/api/v3/auth/user/?format=json&limit=0"
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient).....done
Server Software: nginx/1.1.19
Server Hostname: localhost
Server Port: 8080
Document Path: /api/v3/auth/user/?format=json&limit=0
Document Length: 306326 bytes
Concurrency Level: 4
Time taken for tests: 11.841 seconds
Complete requests: 20
Failed requests: 0
Write errors: 0
Total transferred: 6133760 bytes
HTML transferred: 6126520 bytes
Requests per second: 1.69 [#/sec] (mean)
Time per request: 2368.136 [ms] (mean)
Time per request: 592.034 [ms] (mean, across all concurrent requests)
Transfer rate: 505.88 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.1 0 0
Processing: 1024 2269 806.2 2227 4492
Waiting: 1017 2252 803.6 2211 4472
Total: 1024 2269 806.2 2227 4492
Percentage of the requests served within a certain time (ms)
50% 2227
66% 2336
75% 2395
80% 2406
90% 4140
95% 4492
98% 4492
99% 4492
100% 4492 (longest request)
Производительность API повысилась более чем в 4 раза!
Что еще предстоит
Пока не реализованы авторизация и аутентификация, впрочем я не вижу особых проблем при их реализации.
Есть проблема с реализацией фильтров по критериям вида regex, week_day и search, поскольку в Pony ORM аналогичные критерии пока отсутствуют.
Заключение
Внедрение Pony ORM в качестве замены Django ORM в готовые пакеты-приложения Django является вполне возможным и очень полезным в плане повышения производительности занятием. Делу сильно помогает хорошая абстракция классов внутри пакета, в частности — наличие слоя, не зависящего от Django ORM.
Я приглашаю всех, кто заинтересован в повышении производительности своих приложений Django, присоединяться к открытым проектам djony (скрещивание Pony и Django) и tastypie_djony (ускорение Tastypie с помощью Pony ORM) для их быстрейшего доведения до устойчивого промышленного образца.
Проект Pony ORM является проектом с открытым исходным кодом.
Традиционно напоминаю, что разработчики Pony ORM являются пока еще readonly-пользователями Хабрахабра:
AlexeyMalashkevich m.alexey@gmail.com
metaprogrammer alexander.kozlovsky@gmail.com
Автор: nnseva