Pull to refresh

PHP создан, чтобы умирать

Reading time 8 min
Views 154K
Original author: Software Gunslinger
Дисклеймер: у меня за спиной более десяти лет разработки на PHP. Я начал использовать его, когда PHP4 был совсем малышом, а PHP5 — только мечтой компании Zend. С помощью него я сделал многое, я любил его, проклинал и не без стыда наблюдал за тем, как он растёт и развивается. Я всё ещё использую его в некоторых доставшихся по наследству проектах, но предпочитаю больше его не применять. Также хочу отметить, что я не сотрудничаю с создателями фреймворков или инструментов, упомянутых в статье.

TL;DR (англ. too long; didn't read. Так, в частности, говорят, когда лень читать статью целиком — прим. пер.): если ваш проект основан на функциях фоновых процессов (фоновых служб, демонов — прим. пер.), избегайте PHP.

По-моему, в большинстве случаев ненавистники PHP упускают один весьма важный момент: PHP создан, чтобы умирать. Это не значит, что довольно способный (в какой-то степени) язык программирования исчезнет в никуда; это всего лишь означает, что ваш PHP код не может выполняться вечно. В настоящее время, спустя 13 лет после первого официального релиза в 2000 году, эта мысль до сих пор кажется мне вполне обоснованной.

Модель умирающей программы


Ядро PHP следует самому простому рабочему процессу: получить введенные данные, обработать их, отобразить вывод и умереть. Думаю, сайты вроде PHP Sadness, которые весело и непринужденно помогают выявить несостоятельность языка, лучше всех могут повлиять на его архитектуру в целом. Но, наверняка, были и другие попытки.

В самом начале завершение процесса (англ. dying) не было большой заботой веб-сайтов. Вы считывали что-нибудь из базы данных, применяли логику или форматировали данные и выводили результат среди моря HTML тэгов. PHP до сих пор трудно в этом превзойти (это особенность его ядра), несмотря на весь ужас, привнесенный таким подходом в мир программирования. Хорошо, что все проблемы, которые можно было решить таким способом, уже решены, а оставшиеся решаются более умными и современными инструментами, потому что такая крошечная функциональность — это обычно малая часть в составе большого, сложного проекта. Увы, PHP настаивает на смерти после выполнения. А когда вы пытаетесь сделать наоборот, случаются плохие вещи.

Уже не просто веб-сайт


Хорошей задачей для PHP до сих пор остается отправка информации из формы обратной связи через электронную почту. В этом нет ничего плохого (ну, в случае если я продолжу заниматься такими вещами через десять лет, то возможно, это будет означать, что ранее я принял очень плохие решения). Но это также пример того, что уже решалось миллион раз миллионом разных способов. Фактически любой другой веб-ориентированный язык или фреймворк также могут это сделать.

Давайте пропустим этот момент. Теперь мы пишем реальное и сложное приложение. Одновременно со сложностью растет количество программного кода. Допустим, что вы довольно способный программист. Вы используете PHP 5.x и современные программные методы. Все предельно абстрактно в интерфейсах и классах. Вы знаете, что делать с существующими библиотеками. Теперь вы, возможно, зависите от ваших собственных ORM-моделей, кода сторонних разработчиков, разработанного интерфейса, возможно, ваших клиентских API-функций для сторонних REST-интерфейсов и т.д. Всё написано на PHP.

Тут-то и начинается кошмар: вам неизбежно придётся запускать код в фоновом режиме. Ваше приложение достигло уровня сложности, при котором ожидания выполнения HTTP-запроса не достаточно. Причины могут быть следующие: вам нужно обрабатывать очереди запросов или кэшировать информацию для ускорения HTTP-ответов; вам периодически нужно проверять обязательные платежи и действовать в зависимости от результата или постоянно обновлять данные, получаемые из внешних источников; вам также может понадобиться запись в базу данных целых пакетов данных, чтобы избежать потери производительности; может понадобиться создавать и оставлять открытыми несколько сетевых соединений или выполнять работу серверной части WebSocket-приложения. Это только примеры и их можно перечислять бесконечно, в зависимости от того, что вы разрабатываете.

Вызывайте в интерактивном режиме


Самая плохая реализация вещей, описанных выше, может, однако, показаться довольно знакомой: не важно, чего требует PHP или выбранный фреймворк — наверняка, на вашем хостинге за 5 долларов в месяц нет доступа по SSH, утилиты cron или похожего инструмента.

Какое же из решений самое простое? Конечно, перенести выполнение заданий из фонового режима в интерактивный! Запускайте их случайно через каждое n-ное посещение страницы (for 1/nth of page visits — ориг.). Тогда случайный посетитель будет тратить свое время на ожидание выполнения общесистемной фоновой задачи, и только после ее завершения личный запрос пользователя будет обработан. Становится тревожно при мысли о том, что во многие «серьезные» и «развитые» фреймворки изначально встроена такая функция, потому что довольно сложно не только найти участки кода, отвечающие за этот процесс, но и разбираться в их переплетениях.

Так в чем же беда? Во-первых, нет необходимости в специальных параметрах доступа к серверу; вполне достаточно старого, доброго и бесхитростного FTP. Во-вторых, все срабатывает по запросу клиента, и небольшая задержка для него незаметна. Именно поэтому провал обеспечен: все работает замечательно во время тестирования, также хорошо на этапе внедрения, но некрасиво под реальной нагрузкой. Вы самонадеянно думаете, что посещений будет примерно столько, сколько вам нужно для запуска задач с интервалом в несколько минут. Но псевдослучайность на выборке из n-сотен нельзя сопоставлять с тысячами запросов в минуту. Задачи, которые должны запускаться через определенное время, на деле обрабатываются несколько раз в мгновение ока. Начинают происходить забавные вещи: например, одна и та же задача запускается и завершается в одно и то же время, появляются ошибки, когда несколько процессов пытаются получить доступ к общим ресурсам с механизмом блокировки и т.д. Имейте в виду, что аварийное завершение процессов также приводит к невозможности загрузки запрашиваемой страницы, потому что, как вы знаете, PHP умирает, а вслед за ним всё остальное, включая запрос пользователя. В данном случае смерть — не гарант успеха.

Если вы довольно предприимчивы, то можете переместить фоновые процессы в список задач обработчика cron. Так уже лучше: это работает пока хватает времени, т.к. в данном случае у нас всего одна переменная управления. В случае рекурсивных задач (рекурсия здесь — это сущность ядра демона crond) вы всегда предполагаете, что одна задача завершится до того, как она же вызовется снова. В большинстве случаев это может оказаться так, но, с другой стороны, вам, возможно, придется корректировать интервалы планировщика, если нагрузка на сервер будет возрастать, или надеяться на достаточно долгое выполнение одного из них, что может быть безопасно, но губительно для скорости вашего приложения. В масштабах системы этот метод может оказаться очень и очень сложным. К этому времени вы уже можете задуматься о разбиении общесистемных задач на отдельные пакеты. Но кто-то должен за этим следить, кто-то, кто не умрет.

Призовите демонов


Настоящий кошмар начинается, когда верным решением кажется создание демонов или более живучих процессов. В подтверждение можно привести налаживание и поддержание соединений WebSocket или создание компонентов Изготовитель-потребитель.

Но PHP подведёт вас.

Есть несколько задач, для выполнения которых PHP не годится. Помните, что PHP умирает, как бы вы не старались этому помешать. Во-первых, это проблема с утечкой памяти. PHP никогда не заботится о её освобождении, в случае если она больше не используется, потому что все освободится в конце — после смерти. В непрерывно выполняемых процессах выделяемая память (которая, фактически, пустует) будет накапливаться до достижения порогового значения memory_limit. После этого ваш процесс будет завершен без предупреждения. Проблема тут не в вас, если только вы не ожидали, что процесс будет жить вечно. При реальной нагрузке замените «медленные» процессы на "довольно быстрые".

Конечно, были улучшения в плане «не тратьте память впустую», но их оказалось не достаточно. Как только сложность возрастает или растет нагрузка, все ломается. Рассмотрим следующий фрагмент кода:

class FOO {
    public $f;
}

class BAR {
    public $f;
}

while(1) {
    $a = new FOO();
    $b = new BAR();
    $a->f = $b;
    $b->f = $a;
    print "Memory usage: " . number_format(memory_get_usage(true)) . " bytes\n";
    unset($a);
    unset($b);
}

Здесь представлены так называемые циклические ссылки, объекты, ссылающиеся на другой объект, который в свою очередь ссылается на первоначальный. Когда вы запустите этот скрипт, в зависимости от версии PHP он будет медленно потреблять все больше и больше памяти, потому что во время выполнения проверка на то, что все циклические ссылки больше не используются, выполняться не будет, даже если вы явно попытаетесь освободить их командой unset().

Это довольно распространённый пример. Вы можете найти еще примеры того, что происходит, когда программа становится более сложной, чем в приведённом выше скрипте. Можно поспорить о том, хорошо или плохо использовать циклические ссылки, но есть одно место, где они встречаются в изобилии: ORM. По некоторым уважительным причинам объект, представляющий запись в базе данных, это один и тот же объект в памяти и он ссылается на все другие объекты, если это необходимо, основываясь на ограничениях внешних ключей. Это позволяет избежать многих проблем, таких как каскадное обновление записей через ссылки связанных моделей. А теперь взгляните на схему своей базы данных и посчитайте, сколько у вас внешних ключей… взгляните, сколько циклических ссылок у вас может быть в запущенных процессах? PHP не сможет подсказать, какие из них используются, а какие нет, но рано или поздно свободная память исчерпается, будучи используемая или нет, и PHP умрет.

Важнее того факта, что поддержание работоспособности программы никогда не было приоритетной задачей для PHP, является вопрос: почему подобные проблемы (одна из которых описана выше) никогда серьезно не решались. Механизм сбора мусора (Garbage collection — ориг.) впервые был представлен в PHP 5.3 как подключаемая функция. Всё верно: язык спрашивает вас «пожалуйста, не могли бы вы предотвратить утечку памяти?». Это была середина 2009 года, относительно недавние изменения. Что же происходило до этого? Абсолютно ничего. Никому не было дела до того, предотвращает ли PHP утечку памяти или нет, потому что он создан, чтобы умирать, как только выполнится. С точки зрения PHP, утечки памяти — это нормально.

Речь не только о памяти


Дальше больше. Если вы использовали PHP довольно часто, то вам наверняка должна быть знакома вот эта проблема:

Fatal error: Exception thrown without a stack frame in Unknown on line 0

Что это значит? Честно говоря, я не знаю. Я не могу найти строку с номером 0 в неизвестном PHP-файле. Кажется, есть такие же невежественные люди, как я. Некоторые говорят, что это связано с обработкой ошибок и исключений в PHP. Никто не знает наверняка, я же могу говорить лишь об условиях, предшествующих этой ошибке: постоянно выполняемые процессы, работающие с гигабайтными базами данных, работающие под высокой нагрузкой в реальных условиях. В действительности вы вряд ли обнаружите такие ошибки на своем локальном сервере.

У меня также иногда происходили внезапные аварийные завершения процессов, которые было невозможно отследить. Они просто умирали. PHP полон загадочных, трудно обнаруживаемых или невоспроизводимых ошибок. Взгляните хотя бы на это.

Использование неправильных инструментов


Вы видите системность? У меня есть старые проекты, в которых PHP использовался в фоновых процессах или других местах — не в обычных web-сайтах (здесь в скобках автор пишет «yes, i'm a hyred keyboard» — прим. пер.) — но в каждом из них была одна и та же проблема. Не имеет значения то, насколько хорошо выглядит ваша идея на бумаге; если вы хотите, чтобы процессы работали постоянно, то знайте, что по тем или иным причинам они завершатся, причем быстрее всего под нагрузкой. Нет ничего такого, на что вы могли бы повлиять, потому что PHP создан, чтобы умирать. Основная особенность ядра этого языка заключается в том, что все должно самоликвидироваться, и не важно что это будет.

Для нового проекта я использую Python, virtualenv, Flask, Supervisor и Gunicorn. Я поражен, как хорошо всё это работает, насколько продуман каждый из компонентов — а ведь некоторые из них на этапе бета-тестирования. Но намного важнее то, что они действительно поддерживают работу с фоновыми процессами. Вы спросите: как они это делают? Они не делают ничего, Python просто не кончает жизнь самоубийством. Вот и все, это часть архитектуры ядра. Я не хочу сказать, что это средство от всех бед. Может, я слишком много времени программировал на PHP и просто нахожусь под впечатлением.

Заключение


Конечно, вы всегда можете найти обходной путь. Например, вы можете до смешного увеличить значение memory_limit и немного увеличить время выполнения процессов. Вы также можете поиграть с утилитой cron, shell-скриптами и другими инструментами UNIX. Вы можете использовать PHP для создания web-сайтов, а другие языки — для фоновых процессов. Но имейте ввиду, что мы говорим здесь о средних и высоких уровнях сложности, это касается не только самого языка, но и тех библиотек и инструментов, которые вы применяете, поэтому использование второго языка вместо того, который подвёл вас, может оказаться нетривиальной задачей. Это также касается тех инструментов, для которых 30-секундная задержка может привести к плачевным результатам.

Возможно, найдутся люди, которые думают, что более сервис-ориентированная архитектура может помочь преодолеть ограничения PHP. Может быть, найдутся и те, которые докажут, что PHP 5.4 и PHP 5.5 намного лучше и что язык понемногу улучшается.

К счастью, я уже пошёл дальше.

Обсуждение на HN и Reddit.

Продолжение здесь
Tags:
Hubs:
+98
Comments 260
Comments Comments 260

Articles