Рискну на оффтоп. Я могу совершенно точно сказать, как получается хороший и плохой код. Возьмите программиста и усадите его на тачку с 4 ядрами и 4 гигами. Поручите написать код. Потом усадите его на жалкийт пент 900мгц с одним ядром и полугигом мозга. И поручите ту же задачу. Уверяю — качество кода во 2м случае будет несравненно лучше.
Простите, но SQLAlchemy мне конечно удалось заставить аж на 30% джангу обогнать… но только после того, как я заюзал кеш (скомпилированных) запросов (и то, маппер мне заставить его использовать так и не удалось, поэтому результаты получены только для случая, когда я добываю конкретные значения).
До использования кеша SQLAlchemy отставал от джанги на 30%-50%. Обратите внимание на результаты:
django req/seq: 1106.3929598 req time (ms): 0.903838
django (1) req/seq: 1173.20686476 req time (ms): 0.8523646
alchemy req/seq: 512.719730527 req time (ms): 1.9503833
alchemy (2) req/seq: 526.34332554 req time (ms): 1.8999006
Первые два — алхимия с маппером вида session.query(AUser)…
Вторые два — джанго вида User.objects.all()…
Функциональность приведенных тестов — идентичная, поскольку джанго изначально ничего не кеширует, ибо не умеет, а у алхимии кеш запросов на маппере без бубна не заводится.
Вот и думается — что лучше, кеш компилированных запросов к джанге подвешивать, или искать тормоза в алхимии? Мне лично алхимия больше нравится, ибо гибче. Но джанга зело соблазняет — проще в использовании и в сравнимых условиях — производительнее.
О, отлично, спасибо за наводку. Нет, проект полностью пустой, ни одного приложения кроме джанговского стандартного стартового набора.
И да, сбор в запрос просто таки обязан тормозить.
Меня интересовало нет ли чего-то еще, что я мог упустить. Вот проверю еще раз акселераторы, которые упомянул nimnull, и подумаю крепко, нельзя ли сбоку прикрутить к алхимии кеш всего что там накручено. И взгляну еще на эликсир, он вроде бы не совсем сверху, а как-то наискосок к алхимии приделан, может там чего полезное увижу.
А что касаемо архитектурных элементов — лет 5 назад широко обсуждалась тема, что ормы вообще принципиально ухудшают производительность. Не просто потому что добавляют абстракции и соответствующий рантайм, а потому что как раз таки архитектурно с скулем несовместны. И где-то как-то я готов согласиться хотя бы исходя из практики, поскольку нормального орма ни одного так и не увидел за всю свою жизнь.
Ну вот когда-то я написал на C аналог strcpy и получил тормоза в 2 раза. Там проблема была в том, что ассемблерный strcpy использовал loop, а эту команду иначе чем использованием switch из C не сгенерить. Ну если не вставками, разумеется. А любой цикл в C генерил мне jmp/jne или что-то наподобие. Что на тех платформах (8086) составляло в несколько раз больше времени чем loop (насколько я понимаю, сейчас это не актуально).
Это я понимаю. Однако очень хорошо помню, что первоначально написав тот код, я потерял не в 2, а в 4 раза. На чем? На ширине слова. Стандартный strcpy копировал по слову, в то время как мой первый код копировал побайнтно.
Вот здесь ровно то же самое. Я готов мириться с потерями на абстракцию в 2 раза. А в 5 — имхо многовато.
Вот вариант «alchemy (3)» как раз почти ровно то самое и делает, о чем вы и говорите, и показывает 1400 оборотов в сек. То есть он берет единожды созданную Table из маппера и ее использует.
Мне не удалось заставить класс-маппер использовать кеш компилированных запросов, поэтому результаты «alchemy (1)» и «alchemy (2)» такие унылые.
Насчет кеша запросов — разработчики где-то в недрах гугл-группы рекомендовали использовать некую заглушку для класс-маппера, я в ней до конца не разобрался и пока не прикручивал.
Так об том и речь. test_native() (первый кусок кода) именно это и делает — берет РОВНО такой же, что и сгенеренный запрос и выполняет его безо всяких мапперов и остальных слоев.
Конкретные затраты: на собственно запрос и получение данных в libmysql уходит ~0.1ms, на заворачивание в «объект» ~0.1ms, на лишнее создание курсора — еще ~0.1ms.
Остаются еще ~0.3-0.5ms — непонятно на что. Неужто промежуточные слои так неэффективны?
Про акселераторы я уточню, спасибо за наводку. Вообще, я ставил sudo pip install sqlalchemy и больше никаких телодвижений не предпринимал, подразумевая что акселераторы должны завестись автоматически.
Хранимых процедур, как и всего остального специфического для SQL, мне очень хочется избежать, поскольку я не исключаю смены платформы SQL.
Про курсоры я не забываю, потеря времени на создание курсора составила около одной десятой миллисекунды, это вообще-то говоря больше, чем мне бы хотелось.
В джанге соединение насколько я понял, заводится одно на тред. Что касается алхимии, посмотрю, какие там есть политики.
Про компилятор и остальной «внушительный слой» я вот как раз и думаю, как бы закешировать ВСЮ предварительную обработку? В алхимии кешируется подготовленный запрос — я использовал кеш запросов и получил прирост почти в 3 раза от исходной производительности (с 500 до 1400 запросов в сек), но максимум-то 1400 запросов, а не 3800 получаемых мной на голом SQL даже с созданием курсора на каждом обороте. Без пересоздания — эти данные не вошли — там было 5400 оборотов в сек.
Проблема в том, что когда я использую django ORM — у меня почти нет шансов получить все данные, которые мне нужны, одним запросом. При том, что такой запрос на голом SQL я напишу. И половину обработки данных туда же засуну. И получу то, о чем вы говорите.
А когда ORM берется добывать для меня объекты из базы ПО ОЧЕРЕДИ — вот тут то и возникают тормоза, поскольку на добывание КАЖДОГО из этих отдельных (с точки зрения ORM) объектов — уходят пресловутые полмиллисекунды.
И даже использовав связку django+aldjemy+SQLAlchemy (которая дает мне надежду, что все не так плохо и я смогу сконструировать нужный мне супер-пупер-мега-запрос, пользуясь чисто фишками алхимии), пока план запроса остается в рамках ссылок по ключам, останется вопрос, куда же все-таки девается почти полмиллисекунды при получении КАЖДОЙ строки данных — потому что сам-то запрос будет тратить время в пределах той же одной десятой миллисекунды на строку, а то и существенно меньше, если я получаю много строк. Не говоря уже о нагрузке на проц, который не безразмерен.
Вот наблюдаемый мной результат: до оптимизации нагрузка при выполнении обработки данных распределялась как 5% ядра на MySQL и 80%-90% ядра на python. После оптимизации (перехода на чистый SQL) я наблюдаю 50% на 50% при увеличении производительности в 5 раз (примерно с 200 до 1000 обработанных приложением элементов приходящих данных в секунду), как я уже говорил. Последняя цифра говорит мне, что примерно половину времени обработки python стоит, дожидаясь доставки данных из MySQL — что не так уж и плохо. Но это в свою очередь означает, что MySQL так и не стал узким местом — таким образом, нагрузив питоновскую часть даже относительно производительной алхимией, я получу существенное снижение производительности.
Какое профилирование простите вам нужно? Голая база, в которой ровно одна запись в одной таблице пользователей. Запросы почти идентичны во всех четырех рассмотренных случаях, в двух из четырех эти запросы напрямую приведены в статье.
Добавлю еще, что специфика MySQL такова, что его производительность очень медленно снижается при увеличении количества записей в таблицах (ну пока на индексы с таблицами мозга хватает, разумеется) — поэтому на результатах тестирования почти никак не сказывается, одна запись в таблице или сотня тысяч.
Еще добавлю, что запросы, подобные приведенному (с поправкой на поиск по первичному ключу) — очень характерны при использовании ORM, особенно django ORM. По сути, невинное обращение по атрибуту, соответствующему foreign key, выполняет почти такой же запрос. С соответствующими, описанными в статье, потерями.
Косвенным подтверждением выводов статьи явилось для меня то, что переписывание критической части кода приложения на прямое обращение к SQL дало нам как раз тот самый прирост производительности в 5 раз.
В результате получается, что потери на SQL ничтожны по сравнению с потерями на ORM.
Это не совсем веб. Веб там интерфейс, а еще нужно анализировать данные с сотни тысяч приборов по 3 — 4 записи в мин с каждого. Приложение почти реал тайм.
А на чьем же еще? Я своими руками переписал узкий кусок кода, потратил на это месяц, получил прирост производительности в 5 раз и теперь смотрю на эту дикую смесь, хорошо понимая — изменись структура данных, и мне понадобится еще месяц на переписывание и отладку. При том, что сохрани я орм, на те же изменения у меня уйдет 2-3 дня.
Вот и пытаюсь понять — сохранять ли мне мобильность кода, платя 5-кратными потерями на эксплуатации, или тратить уйму времени на ввод каждой новой фичи.
Поэтому и пост написал — может кто что посоветует.
Логгеры были отключены разумеется, DEBUG=False.
Что касается количества полей — на ограничении количества запрашиваемых полей из приведенного примера хорошо видно, что и django ORM, и SQLAlchemy довольно много теряют на полях. Однако если вы обратили внимание, даже в примерах, в которых ORMы берут ровно одно поле данных, они проигрывают эквивалентному «голому» запросу в несколько раз.
Это означает, что ORMы выполняют какую-то совершенно ненужную работу в случаях, когда эта работа никому не нужна.
Я был готов заплатить за удобство ORMа понижением производительности условно говоря в 2 раза. Но не в 5 же раз! 5-1=4 хороших производительных сервера стОят поллимона рублей. За эти деньги можно нанять команду программеров, которые с нуля напишут производительный ORM или его подобие на C++ и прикрутят его к питону.
Про удобства ORMа я и сам прекрасно знаю. Но реальность такова, что срок выпуска приложения в результате не приближается, а растягивается — поскольку борьба с его производительностью, вместе с мучительными выяснениями, на каком же слое происходят потери — с глубинным анализом чужого кода, изучением кучи документации и так далее — занимает в совокупности больше времени, чем написание лобового кода доступа к данным, пусть непрозрачного и абсолютно нерасширяемого.
Анализировать я конечно буду еще — это рабочий проект, который мне нужно как-то выводить на рабочие рельсы.
Бессмысленно — именно из за производительности. Да, разумеется, можно ОЧЕНЬ быстро смастрячить приложение, которое будет прекрасно работать в тестовом режиме — на малых нагрузках. Однако его готовность к выпуску будет неявно сильно переоценена — так как для того, чтобы оно реально заработало на реальных данных — то есть стало работоспособным в той степени, которая необходима для выпуска приложения — нужно будет переписать условно 90% кода, который так замечательно быстро был написан на первой стадии.
При этом, надо заметить — то что на первой стадии было понятно, просто и совместимо — станет сложным, непонятным и абсолютно несовместимым.
И наконец — иллюзия быстроты написания приложения — а это получается именно иллюзией в силу вышеприведенных причин — приводит еще и к тому, что часть архитектуры приложения, завязанной поначалу на использование ОРМ, становится балластом, с которым приходится дополнительно бороться в процессе приведения приложения в состояние, пригодное для выпуска.
Проценты от общей производительности в общем случае столь же некорректны, так как зависят от внешних условий — производительности СУБД например. Поэтому в разных местах — разные сравнения, и в разах, и в процентах, и в миллисекундах.
Вопрос на самом деле в том, что применяя существующие ORMы, мы очень сильно теряем в производительности — настолько сильно, что применение ORM становится бессмысленным, поскольку приводит к неработоспособности приложения в условиях, когда оно — по объемам производимой полезной нагрузки — работать должно.
The ORM currently uses 4x the time in Python compared to the time the DB needs to parse, plan and
execute a query like this: Model.objects.get(pk=1).
Anssi Kääriäinen
Товарисч получил аналогичный результат. При этом он считает, что оптимизировать можно и нужно.
До использования кеша SQLAlchemy отставал от джанги на 30%-50%. Обратите внимание на результаты:
Первые два — алхимия с маппером вида session.query(AUser)…
Вторые два — джанго вида User.objects.all()…
Функциональность приведенных тестов — идентичная, поскольку джанго изначально ничего не кеширует, ибо не умеет, а у алхимии кеш запросов на маппере без бубна не заводится.
Вот и думается — что лучше, кеш компилированных запросов к джанге подвешивать, или искать тормоза в алхимии? Мне лично алхимия больше нравится, ибо гибче. Но джанга зело соблазняет — проще в использовании и в сравнимых условиях — производительнее.
И да, сбор в запрос просто таки обязан тормозить.
А что касаемо архитектурных элементов — лет 5 назад широко обсуждалась тема, что ормы вообще принципиально ухудшают производительность. Не просто потому что добавляют абстракции и соответствующий рантайм, а потому что как раз таки архитектурно с скулем несовместны. И где-то как-то я готов согласиться хотя бы исходя из практики, поскольку нормального орма ни одного так и не увидел за всю свою жизнь.
Это я понимаю. Однако очень хорошо помню, что первоначально написав тот код, я потерял не в 2, а в 4 раза. На чем? На ширине слова. Стандартный strcpy копировал по слову, в то время как мой первый код копировал побайнтно.
Вот здесь ровно то же самое. Я готов мириться с потерями на абстракцию в 2 раза. А в 5 — имхо многовато.
Мне не удалось заставить класс-маппер использовать кеш компилированных запросов, поэтому результаты «alchemy (1)» и «alchemy (2)» такие унылые.
Насчет кеша запросов — разработчики где-то в недрах гугл-группы рекомендовали использовать некую заглушку для класс-маппера, я в ней до конца не разобрался и пока не прикручивал.
Конкретные затраты: на собственно запрос и получение данных в libmysql уходит ~0.1ms, на заворачивание в «объект» ~0.1ms, на лишнее создание курсора — еще ~0.1ms.
Остаются еще ~0.3-0.5ms — непонятно на что. Неужто промежуточные слои так неэффективны?
Хранимых процедур, как и всего остального специфического для SQL, мне очень хочется избежать, поскольку я не исключаю смены платформы SQL.
Про курсоры я не забываю, потеря времени на создание курсора составила около одной десятой миллисекунды, это вообще-то говоря больше, чем мне бы хотелось.
В джанге соединение насколько я понял, заводится одно на тред. Что касается алхимии, посмотрю, какие там есть политики.
Про компилятор и остальной «внушительный слой» я вот как раз и думаю, как бы закешировать ВСЮ предварительную обработку? В алхимии кешируется подготовленный запрос — я использовал кеш запросов и получил прирост почти в 3 раза от исходной производительности (с 500 до 1400 запросов в сек), но максимум-то 1400 запросов, а не 3800 получаемых мной на голом SQL даже с созданием курсора на каждом обороте. Без пересоздания — эти данные не вошли — там было 5400 оборотов в сек.
А когда ORM берется добывать для меня объекты из базы ПО ОЧЕРЕДИ — вот тут то и возникают тормоза, поскольку на добывание КАЖДОГО из этих отдельных (с точки зрения ORM) объектов — уходят пресловутые полмиллисекунды.
И даже использовав связку django+aldjemy+SQLAlchemy (которая дает мне надежду, что все не так плохо и я смогу сконструировать нужный мне супер-пупер-мега-запрос, пользуясь чисто фишками алхимии), пока план запроса остается в рамках ссылок по ключам, останется вопрос, куда же все-таки девается почти полмиллисекунды при получении КАЖДОЙ строки данных — потому что сам-то запрос будет тратить время в пределах той же одной десятой миллисекунды на строку, а то и существенно меньше, если я получаю много строк. Не говоря уже о нагрузке на проц, который не безразмерен.
Вот наблюдаемый мной результат: до оптимизации нагрузка при выполнении обработки данных распределялась как 5% ядра на MySQL и 80%-90% ядра на python. После оптимизации (перехода на чистый SQL) я наблюдаю 50% на 50% при увеличении производительности в 5 раз (примерно с 200 до 1000 обработанных приложением элементов приходящих данных в секунду), как я уже говорил. Последняя цифра говорит мне, что примерно половину времени обработки python стоит, дожидаясь доставки данных из MySQL — что не так уж и плохо. Но это в свою очередь означает, что MySQL так и не стал узким местом — таким образом, нагрузив питоновскую часть даже относительно производительной алхимией, я получу существенное снижение производительности.
Добавлю еще, что специфика MySQL такова, что его производительность очень медленно снижается при увеличении количества записей в таблицах (ну пока на индексы с таблицами мозга хватает, разумеется) — поэтому на результатах тестирования почти никак не сказывается, одна запись в таблице или сотня тысяч.
Еще добавлю, что запросы, подобные приведенному (с поправкой на поиск по первичному ключу) — очень характерны при использовании ORM, особенно django ORM. По сути, невинное обращение по атрибуту, соответствующему foreign key, выполняет почти такой же запрос. С соответствующими, описанными в статье, потерями.
Косвенным подтверждением выводов статьи явилось для меня то, что переписывание критической части кода приложения на прямое обращение к SQL дало нам как раз тот самый прирост производительности в 5 раз.
В результате получается, что потери на SQL ничтожны по сравнению с потерями на ORM.
Вот и пытаюсь понять — сохранять ли мне мобильность кода, платя 5-кратными потерями на эксплуатации, или тратить уйму времени на ввод каждой новой фичи.
Поэтому и пост написал — может кто что посоветует.
Что касается количества полей — на ограничении количества запрашиваемых полей из приведенного примера хорошо видно, что и django ORM, и SQLAlchemy довольно много теряют на полях. Однако если вы обратили внимание, даже в примерах, в которых ORMы берут ровно одно поле данных, они проигрывают эквивалентному «голому» запросу в несколько раз.
Это означает, что ORMы выполняют какую-то совершенно ненужную работу в случаях, когда эта работа никому не нужна.
Я был готов заплатить за удобство ORMа понижением производительности условно говоря в 2 раза. Но не в 5 же раз! 5-1=4 хороших производительных сервера стОят поллимона рублей. За эти деньги можно нанять команду программеров, которые с нуля напишут производительный ORM или его подобие на C++ и прикрутят его к питону.
Про удобства ORMа я и сам прекрасно знаю. Но реальность такова, что срок выпуска приложения в результате не приближается, а растягивается — поскольку борьба с его производительностью, вместе с мучительными выяснениями, на каком же слое происходят потери — с глубинным анализом чужого кода, изучением кучи документации и так далее — занимает в совокупности больше времени, чем написание лобового кода доступа к данным, пусть непрозрачного и абсолютно нерасширяемого.
Анализировать я конечно буду еще — это рабочий проект, который мне нужно как-то выводить на рабочие рельсы.
При этом, надо заметить — то что на первой стадии было понятно, просто и совместимо — станет сложным, непонятным и абсолютно несовместимым.
И наконец — иллюзия быстроты написания приложения — а это получается именно иллюзией в силу вышеприведенных причин — приводит еще и к тому, что часть архитектуры приложения, завязанной поначалу на использование ОРМ, становится балластом, с которым приходится дополнительно бороться в процессе приведения приложения в состояние, пригодное для выпуска.
Вопрос на самом деле в том, что применяя существующие ORMы, мы очень сильно теряем в производительности — настолько сильно, что применение ORM становится бессмысленным, поскольку приводит к неработоспособности приложения в условиях, когда оно — по объемам производимой полезной нагрузки — работать должно.