Pull to refresh

Comments 58

Пожалуй, тут уместно будет упомянуть мою прошлогоднюю статью о том, почему парсинг python-скрипта требует стократного объёма памяти. По мотивам этой статьи я месяц назад выступил на EuroPython 2017, и один из комментариев к моему выступлению был «твоему скрипту не хватило 2ГБ памяти? ха! ну купи ты ещё 2ГБ, это же копейки!»
Не привыкли питонисты память считать :-/
Да. Python программирование живёт по принципу «Ресурсы сейчас дешевле, чем время специалиста». Если программист, получающий 2 000 USD в месяц потратит 8 часов на оптимизацию памяти, мы получим около 95 долларов за решение этой проблемы. Это гораздо дороже, чем покупка 2ГБ ОЗУ. Так что, в этом есть рациональное звено.
Не везде Питонисты получают такие деньги. Где-то получают меньше.
Да и потом, одно дело 1 раз потратить 8 часов, другое дело — каждому пользователю данного скрипта докупать дополнительно ОЗУ, потому что на оптимизации сэкономили.

Не знаю вариантов, при которых могло бы потребоваться докупать ОЗУ на пользовательских машинах ради исполнения python скрипта. У языка немного другая прикладная область и иные принципы использования. Python используется для быстрого решения сложных задач. Точнее, для быстрого кодинга. Он всегда занимает много памяти, жрёт ресурсы и медленно работает. Но реализация на нём сложного алгоритма обходится дешевле по времени, чем реализация на многих других языках. Сам принцип работы python — запустить его там, где требуется особый функционал, и не смотря на ресурсы. Поэтому, почти никто не пишет на нём клиентский софт.

Поэтому, почти никто не пишет на нём клиентский софт.

Ничего себе! Ну-ка, попробую-ка я прикинуть, каким несерверным софтом на питоне я пользуюсь или активно пользовался раньше: Zim, Gramps, Gajim, Deluge, Puddletag, Calibre, MComix… И это только на ПК, без учёта смартфона, на котором у меня тоже было что-то написанное на питоне.

И представьте себе, меньше месяца назад репортил баг об утечке памяти в менеджере заметок Zim. Да, мне существенно мешало, что он начинал отжирать гигабайты памяти вместо обычных 50-100 МБ.
Gajim когда-то пользовался. Сейчас из такого чисто пайтоновского использую Ansible (кстати, весьма неплохо работает). Gajim в своё время был оооочень глючным в сравнении с Pidgin. А вообще, удивительно, что вы не вспомнили самый яркий пример коммерческого софта на пайтоне: GuitarPro. Вроде, 6-й версии. Но, в любом случае, реальные масштабные приложения можно по пальцам сосчитать. Поскольку те же Gajim, Deluge выглядят пока разработкой энтузиастов или мелких команд. И, думаю, до решения вопросов с памятью такими они останутся. Вроде бы, сейчас решена проблема с многопоточностью. Не сказать, что до конца, но некоторые паттерны позволяют рулить несколькими процессами и потоками. В любом случае, не решается проблема веса приложения. Пайтон же модульный.
Я перечислял только то, чем сам активно пользовался. GuitarPro я не пользовался, поэтому не вспомнил.

Впрочем кое-что я действительно забыл указать: Anki.
UFO just landed and posted this here
Если питонисту в 2017 году не хватило ума найти работу с нормальной зарплатой, то и на оптимизацию потребления памяти и подавно не хватит

Умножаем на количество установок, количество экземпляров на серверах — и внезапно может оказаться, что программисту доплатить дешевле.

там где высокое количество установок, там и время программиста стоит дороже. К ним прибавляются налоги, время других специалистов (qa, operations), вторичные расходы (офис и т.д.). Но все это обычно копейки по сравнению с недополученной прибылью. Оптимизируя код, программист не пишет фичи, которые приносят деньги. В тех проектах, где используется питон, обычно это главный мотиватор.
Вообще говоря с фразы «Одна из главных проблем при написании крупных (относительно) программ на Python — минимизация потребления памяти» я неплохо посмеялся. Сколько себя помню, во всех проектах на питоне, которые я знаю — плевали на память от слова совсем. В крупных проектах, например, даже отключают сборщик мусора, на столько память не важна.
Хотя статья, безусловно, интересна, спасибо автору. Побольше бы статей о том, что происходит под капотом.
Отключив сборщик мусора, они как раз выиграли по памяти в том числе.
да, но выиграли они его на уровне инициализации форков, т.е. самого интерпретатора, а на уровне самого приложения, то о чем говорит автор — пожертвовали. То, что потребляет само приложение своими интами, строками и словарями — это почти всегда копейки. Если приложение вдруг упирается в память — тут же меняют алгоритм на поточный, что бы не брать все данные в память сразу, и идут дальше. Не влезло сейчас — дальше будет только хуже. А что бы кто-то прямо оптимизацией по памяти занимался в питоне — это задача на уровне экзотики, но никак не «одна из главных проблем».
Ну вообще, такая проблема есть при разработке приложений, которые выполняются постоянно или продолжительное время. Но таких решений очень и очень мало. Проблема оптимизации использования памяти в Python всегда решалась тем, что python скрипт быстро выполняется и завершает работу. И это актуально почти для всех областей применения. Плагин? Он выполнил расчёты и закрылся. Web-приложение? Оно вернуло результат и закрылось. Сложные операции в Web? Обработчик запустился в новом процессе, старый процесс вернул Response и умер, новый процесс отработал минуту и умер. На этом принципе основаны даже сложные многопоточные и многопроцессные фрэймворки: нужно запустить скрипт, который быстро отработает и сдохнет. Нет долгоиграющего скрипта — нет проблем с памятью.
У меня питон приложение работает на сотнях серверов без всяких сдыханий (asyncio). Прод, хайлоад, перезапускаем раз в несколько недель для наката новой версии. Никаких проблем с памятью, хотя у меня один запрос занимает до 50Мб.
Перезапуском решают обычно не проблему зажористости по памяти, а проблему утечек. Очень часто под капотом питон приложений работают Си библиотеки, которые имеют неприятную привычку подтекать. В uwgi это решают ребутом приложения после определенного количества запросов, и обычно оно измеряется тысячами. Ребутать его после каждого запроса — это невероятно накладно.
Так я не говорю про перезапуск uWSGI. Но сам uWSGI разве держит постоянно работающие процессы бэкенда? Бэк живёт от Request до Response. Собственно, и сам принцип работы uWSGI основан на том, что некий демон получает Request, запускает на его основе процесс или поток, ждёт N секунд. Если ответ приходит, возвращает его и убивает процесс (поток), который вернул этот ответ. Если ответ не приходит, убивает процесс (поток) и возвращает Timeout Error.
Да, uwsgi держит бэкенд процессы. А то, что вы описали — это что-то из 2000х. Веб-приложения уже давно так не работают. (и не только в python)
Ага, а на Рождество Санта-Клаус приносит детям подарки. То что вы сейчас написали — бред. Ни один здравомыслящий разработчик не будет держать python бэкенд постоянно запущенным. Если только это не websocket. Видимо, что вы, что kozzztik называете highload web-приложениями какие-то простенькие форумы. У kozzztik вообще один запрос занимает 50 мегабайт, хотя все нормальные прогеры стараются кешировать любой чих. Желательно, в Redis или RabbitMQ. Python вообще должен работать на твоём приложении только тогда, когда надо обработать данные. Всё остальное должны делать Nginx + Redis. И не надо говорить фигни об удержании процесса в uWSGI. Сам принцип работы web-сервера основан на том, что либо бэкенд возвратит Response, либо он будет убит.
Веб, это, внезапно, не только HTTP. В моем случае это фильтрация почты и SMTP протокол, а 50Мб это собственно письмо. Удачи кешировать запросы.
кстати говоря о HTTP и кешах. Тот же Django кеширует разобраные шаблоны страниц в памяти приложения. Частый рестарт бекенда очень сильно убивает производительность, которая достигается этим кешем. Давным давно я даже писал статью с кучей графиков на эту тему.
Впрочем, мы можем говорить об одном и том же, только по-разному. Получается, uWSGI получает некий объект. На этом этапе уже загружены настройки. Т.е. что-то хранится в памяти. Но в памяти uWSGI демона. Лишь при срабатывании __call__() (кстати, это требование к реализации интерфейса WSGI) запускается основной процесс. Т.е. условно Джанга существует в памяти в виде переменных, но выполнение скриптов запускается только во время запроса. Как я понимаю, uWSGI отвечает за то, чтобы call был осуществлён в отдельном потоке или процессе, который дохнет после возврата response. О том и речь веду: да, сам демон uWSGI висит. Но приложение — скрипт запускается в отдельном потоке и благополучно умирает после возврата Response.
Может быть вы все таки до конца разберетесь в вопросе, прежде чем будете весьма категорично высказывать мнение? Очень сложно понять что вы имеете ввиду под «условно джанга существует в памяти в виде переменных». Как насчет импортированных модулей, инициализировнных django приложений, middleware? Django вообще довольно занятно заводится и производит при этом целую кучу разных действий. Но что могу сказать точно, это то что uwsgi не прибивает дочерний процесс после исполнения запроса, если это явно не указать в конфигурации uwsgi.
А может, вы сперва разберётесь с тем как работает импорт в пайтоне? Все импортированные модули будут храниться в памяти в виде статичных ссылок, пока к ним не произойдёт обращение. Поэтому, сам вызов обработчика обёрнут в функцию. Т.е. когда uWSGI получает объект WSGIHandler, в его памяти сторятся те переменные, которые до перезапуска демона не будут высвобождены. Там будут только статичные данные. Все динамические данные приходят только во время Request. А все данные, которые получаются в процессе обработки Request, будут созданы в дочернем процессе, который запускает uWSGI. Это и обращения к БД, и рендер шаблонов, и что там ещё будет наговнокожено джангистом. В основном потоке этих данных не будет. Они обрабатываются отдельным процессом и умирают вместе с ним, когда он возвращает Response.

Аналогично, в памяти uWSGI не хранится код импортированных модулей. Ещё стоит вспомнить реализацию apps в джанге. Обратите внимание на то, что INSTALLED_APPS — это массив строк.
Серьезно? «разберитесь как работает импорт»? Что простите означает «модули хранятся в памяти в виде статичных ссылок пока к ним не произойдет обращение» это что? Это вы так разобрались в импорте? Это у вас модуль импортирован или нет? Разберитесь сначала сами, может начнете писать что-то внятное.

Остальное по большей части все правильно за исключением самой концовки. Дочерний процесс не умрет на возвращении Response.
Ну и про uWSGI и INSTALLED_APPS это вообще отлично. Это вещи между собой вообще не связанные. uWSGI может запускать не django, а flask. Да, кстати, покурите еще такую тему, как постоянные подключения к БД в обоих фреймворках. Пруф docs.djangoproject.com/en/1.11/ref/databases
Интересно как вы это объясните в рамках своей гениальной теории одноразовых процессов.
Да, с импортом не так сказал: код джанги в виде текста сохраняется в памяти (code object). Только эти данные не изменяются и не выгружаются из памяти.
Одна чушь вместо другой. Теперь код джанги сохраняется в виде текста. Что это? Как насчет результата синтаксического анализа? То что модуль это объект и у него есть свои локальные переменные?
uWSGI демон в своём основном потоке не держит подключения к БД. Сперва ему приходит Request. Потом создаётся отдельный поток. В поток передаются данные. Запускается код джанги из code object. Инициируются приложения. В процессе инициации может произойти создание подключения. А может и не произойти. Но это будет в отдельном потоке или процессе.
Так где же будет жить соединение между запросами по вашему? Даю подсказку — в дочернем потоке.
Я особо в архитектуру питоняки не вникал. Но не дорого ли на каждый запрос форкэкзекать процесс? По описанию libphp напоминает. В чем отличие?
Ну мне уже самому стало интересно: вдруг, я совсем не прав. Читаю документацию по uWSGI. Вот. Получается, воркеры блокируются на время Request. Воркеры как-то очищаются. Это уже противоречит некоторым тезисам моего оппонента. Но я не исключаю, что он прав. Только он не хочет пруфы давать.
Это уже противоречит некоторым тезисам моего оппонента.
он не хочет пруфы давать.

Что ж в оппонента критикуете, а бревно у себя в глазу не замечаете?

Документация противоречит всем вашим тезисам. Вы сначала пишете «учите матчасть», а потом оказывается что вы сами-то документацию не читали и матчасть толком не знаете? Стыдно должно быть.
Должно быть? У нас тут не институт благородных девиц.

Впрочем, kozzztik действительно прав. И перед вами хочу извиниться за рождественский сарказм.
Ну тогда я спокоен. А то мой уютный мир начал рушиться.
Ага, и вы ещё говорили что-то за хамство :-)
По поводу кеша: сейчас можно не использовать шаблонизатор Django вообще. Лично я предпочитаю отдельный фронтэнд на React или Angular. Всю статику возвращает Nginx, а соединение с сервером происходит посредством REST. Не всегда это хорошо. Но мы можем использовать отдельный фрэймворк для кеширования. Например, посмотри на такой вариант. Архитектура здесь проста до боли: администратор меняет контент в админке. После этого срабатывает сигнал, который компилирует страницу и помещает её в Redis. Это можно делать с частью страниц. Все динамические элементы будет отрабатывать JS. Например, авторизацию. А всё остальное у тебя лежит в кеше и джанга вообще не запускается.

А что касается SMTP, то вообще не понятно причём тут пайтон? Сервер SMTP на пайтоне? в любом случае, если ты откроешь исходник демона, то обнаружишь, что висит там только listener, а сам процесс обработки запроса стартует в отдельном потоке, либо вообще процессе. Т.е. фактически память вычищается после того, как скрипт отработает.
Во первых, для вас — на вы. Во вторых разговор не о том что можно сделать. Можно ракету в космос запустить, какое это имеет отношение в делу?
И да, SMTP сервер на питоне. И в том же приложении еще и HTTP сервер, и много чего еще. Вот, например доклад на PyCon Portland 2017 на тему SMTP сервера на питоне www.youtube.com/watch?v=1Uyo2c2GYKQ. Там упоминаюсь и я, и между строк мой проект.
Исходиники «демона» я читал подробно и очень внимательно. И трейсил, и дебажил. Обработка запроса действительно висит в отдельном процессе или потоке, но где вы наши «память фактически очищается», я не знаю.
Я то как раз мат часть прекрасно знаю. И как работает связка uwsgi джанго тоже. То что вы говорите весьма очевидная чушь, которую вы непонятно откуда взяли. Я даже привел кучу графиков которые это подтверждают. А толку?
Чушь — это доказывать графиками по mod_wsgi тот факт, что вы знаете связку Django + uWSGI. Всё. Надоело. Я на выход. Когда проснутся гуру, я надеюсь, они вам объяснят что вы знаете, а что нет. Без обид.
Обработка запроса действительно висит в отдельном процессе или потоке


Вот отсюда идёт вывод о фактическом очищении памяти. Когда процесс умирает, связанные с ним данные должны удалиться. Да, есть риск, что этого не произойдёт. Но в контексте самого треда получается, что это, как-раз, происходит. Поскольку претензия идёт к управлению освобождаемой памятью внутри процесса. А когда процесс умирает, он перестаёт блокировать области памяти.

Что касается доклада, то обратите внимание на то, что вы и ваш проект упоминаетесь между строк. Да, возможно, у вас хороший проект, но это не делает ваше мнение единственно верным. И, как я помню, после смерти процесса ссылки на него в памяти убивает ось. Т.е. да, память должна вычищаться. На том и основаны применяемые методы. Методы, при которых основная работа Python скрипта производится в отдельном процессе, а в родительском лежат, преимущественно, неизменяемые данные.
Все ваши размышления базируются на тезисе, что после исполнения запроса дочерний процесс умирает. Я могу повторить еще раз — это не правда. Процесс потом принимает следующий запрос, а за ним еще и еще. Он может вообще не умирать — это поведение uwsgi по умолчанию.
В любом случае, этот дочерний процесс должен как-то очищаться и приводиться к дефолтному состоянию. Не так ли?
Прошу прощения. Да, спорил как дурак. Воркер, действительно, содержит код программы для запуска и завершается либо с перезагрузкой, либо если ставится таймаут / принудительное завершение. Вы были правы, а я тупил.
Ничего не очищается.
wsgi(который, в частности реализует uwsgi, так же как и gunicorn и встроенный debug режим фласка например) просто передает построенный обьект запроса (Request) в хендлер приложения

Хендлер должен вернуть Response

Само приложение бежит, запущенное в процессе сервера реализующего wsgi

Сервер может запустить n-ое колва приложений, убивать каждое после n-ого кол-ва запросов (как костыль против утечек памяти)

Для того чтоб лично убедиться — достаточно выставить 1 процесс в uwsgi/gunicorn, убедиться что нет перезапуска после одного запроса(это никогда не дефолт) и записать какие либо данные в какой либо глобальный объект

При другом запросе — считать.
Данные будут там

Более того, подсказка, именно так держатся подключения к БД и именно так в принципе могут работать всякие функции вроде memoized, исключительно в памяти, без редисов и прочего
Более того, ему(python-у) бедному и так сложно. Импорты на большом проекте могут с легкостью занять более секунды. Импорты без побочных эффектов, просто импорты…

Я уже не говорю про инициализацию подключений и другие netI/O задержки, которые вполне могут быть при старте.

И если бы каждый запрос делал все это — лежали бы сервера со 100% оверхедом по cpu, очень тщательно занимаясь построением тяжеловесных объектов(о чем кстати эта статья), вместо того, чтоб обрабатывать предельно простые запросы юзверей, о том по какому же url-у котики лежат, занимая при этом фиксированное значение в легкодоступной памяти, без каких либо значимых овераллокаций.
Точно уверены что без побочных эффектов? Каждый раз когда я подобное наблюдал, всегда в итоге находил побочные эффекты.
Вот например (внезапно, оказывается автор с хабра): github.com/pyca/pyopenssl/issues/137
With latest released pyopenssl, cryptography and cffi «import OpenSSL» takes about 0.5s on a modern i7 CPU with SSD (OS X):


Для 2014 года пацан грамотно понтанулся
Тут конечно следует определить что такое побочные эффекты.

Построение различных объектов для быстрого доступа в работе — как по мне ничем не отличается от определения классов и функций. И там и там при импорте уже бежит много чего. И это неизбежное зло, как следствие хороших сторон языка… В том числе и monkey_patching при импорте. Абсолютно все сделать lazy нельзя… либо ценой неудобного кода

В этом контексте под побочными эффектами подразумеваю I/O, то есть — сеть и ненужные обращения к диску

В таком понимании — все чисто, импорты ~200ms на большом проекте. Но это только импорт
Запуск приложения уже подразумевает намного больше, а там уже вполне допустимо несколько уровней кеша(память-диск) с оригинальными данными из сети, различные библиотеки подгрузят свои конфигурационные файлы для автогенирации API…
И тут секунды для запуска уже реальны. Что явно расточительно, для обработки одиночного запроса :)

То что вы сейчас написали — бред. Ни один здравомыслящий разработчик не будет держать python бэкенд постоянно запущенным.

Лол, а в чем проблема держать его запущенным, если он никакой работы в фоне не выполняет? Чему там течь?
Вы правда где-то в 2000-х застряли, видимо свое хамство оттуда же принесли.
А если предположить, что по 2Гб надо установить на сотню серверов?
UFO just landed and posted this here
И при этом я не раз слышал от Python-разработчиков, что Java плохая, потому, что требует много памяти. Хм, Забавно.

В статье не учтена работа GC. Принудительный запуск GC в ключевых местах (если, вдруг, при наблюдении за работой приложения, оказалось что стандартной его работы не хватило) — чистит память и нивелирует описанную проблему.


import gc


gc.collect()


Так что на практике, если не писать для встраиваемых систем с парой десятков мегабайт ОЗУ, — проблема, обычно, не является значимой.


А вот подборка размеров стандартных примитивов в статье, на мой взгляд, — действительно ценна.

Sign up to leave a comment.