Пишу, по большей части, про историю, свою жизнь и немного про программирование.

Процедурное настоящее PHP

ПХП, к сожалению, в сердце ещё глубоко процедурный язык. Наткнулся на очередную особенность. Сначала небольшая предыстория.

Многие знают, что FastCGI в ПХП не настоящий — при каждом запросе весь код интерпретируется заново, это позволяет не заботиться об утечке памяти, сбросе состояния между запросами и ещё о многих вещах, а значит упрощает вообще всё, что соответствует духу ПХП — понизить до минимума порог вхождения. Конечно, это сказывается на производительности, но в некоторых случаях ПХП научился смягчать этот недостаток.

Например, есть такая вещь как «постоянные соединения» (persistent connections) — это специальные режим, в котором на уровне процесса PHP FastCGI соединения с чем-либо не закрываются и выдаются в следующий запуск кода уже открытыми. Чудесная вещь, ведь операция соединения, например, с «Ораклом» очень дорогая.

Конечно, кое-чем приходится расплачиваться — например, в открытом соединении «Оракла» остаются установленные предыдущим кодом параметры сессии, временные таблицы, незавершённые транзакции, наверное что-то ещё. Первые два пункта меня лично не пугают, так как параметры сессии у нас везде одинаковые и временные таблицы мы не используем, а вот последний — очень.

Как известно, ПХП может свалиться в фатальной ошибкой по многим причинам, одна из них — превышение времени обработки скрипта. Соответственно, скрипт может упасть и в середине транзакции, она останется незакрытой и может остаться висеть долгое время, пока соединение не получит скрипт, в котором встретится команда закрытия транзакции.

А пока этого не произошло, блокировки на базе могут привести к коллапсу.

В общем-то, есть хороший выход — у нас же есть сервис-объект для работы с базой? Вставим в деструктор принудительный откат транзакции (rollback) и все дела! Если транзакция была не завершена в силу фатальной ошибки, она откатится и всё! Ведь деструктор должен вызвааться на уничтожение объекта!

Как показывает практика, должен, но не обязан. Ниже простой код:

class ImportantToClose {
    public function __destruct()
    {
        // закрываем что-то важное
        echo "Вызвался деструктор\n";
    }
}

$itc = new ImportantToClose;

register_shutdown_function(function()  {
    echo "Вызвалась shutdown function\n";
});

set_time_limit(1);

for(;;);

Если запустить его в интерпретаторе и подождать секунду, то мы получим следующую ошибку:

PHP Fatal error: Maximum execution time of 1 second exceeded in fatal.php on line 18

Fatal error: Maximum execution time of 1 second exceeded in fatal.php on line 18 Вызвалась shutdown function

Можно ещё попробовать записать что-нибудь в файл, результат будет тот же — деструктор не вызовется, тогда как-то старая добрая процедурная shutdown function — да. Поэтому, если у вас есть нечто важное, что необходимо аккуратно закрывать, не делайте это в деструкторе, делайте в этой магической функции. В нашем случае, можно реализовать в каждом сервисе метод close и в этой функции вызвать этот метод для кажого открытого сервиса.

Кстати, не забудьте выставить внутри shutdown function нужное вам время выполнения, если предел будет превышен, то и эта функция будет прервана, в этом случае вам уже ничего не поможет закрыть ресурс корректно.

10 комментариев
dinoelq 2014

Ладно хоть анонимные функции внутри register_shutdown_function теперь можно использовать.

Евгений Степанищев (bolknote.ru) 2014

Комментарий для dinoelq:

Это да, удобно.

Сергей Морозов (morozov.livejournal.com) 2014

Есть ещё одна причина, по которой стоит использовать shutdown function вместо деструктора для важных задач. Если ваш деструктор вызывается при выходе из скрипта, не факт, что в этот момент весь объект будет в целостном состоянии. Допустим, у объекта есть свойство соединения с БД, которое (соединение) также доступно через глобальную переменную. Вполне возможно, что деструктор объекта будет вызван после того, как соедиенение закрыто и значение глобальной переменной (а также и самого свойства) очищено.

Евгений Степанищев (bolknote.ru) 2014

Комментарий для morozov.livejournal.com:

Это просто ещё одна причина не использовать глобальные переменные :)

Сергей Морозов (morozov.livejournal.com) 2014

Комментарий для Евгения Степанищева:

В нашем случае это была статическая переменная класса (то ли синглтон, то ли реестр), но сути это не меняет, правда :)

Евгений Степанищев (bolknote.ru) 2014

Комментарий для morozov.livejournal.com:

Ну да, если всё организовано через сервисы и сервисы передаются в другие только через входные параметры, то всё дерево зависимостей будет согласовано через ссылки друг на друга, автоматически.

Александр Макаров 2014

Всё хорошо, но при переполнении доступной памяти это не сработает. Приходится резервировать память заранее и чистить при фатале, чтобы хватило на нужные вызовы.

Евгений Степанищев (bolknote.ru) 2014

Комментарий для Александр Макаров:

Я боюсь, что и в этом случае гарантии нет. В некоторых интерпретаторах память в этом случае резервируется самим интерпретатором.

Кстати, в «Яндексе» я писал обвязку к libmapi на «Пайтоне» с использованием ctypes, поскольку библиотека тогда была очень текучая (не знаю как сейчас), я пользовался тем же приёмом — выделял память в интерпретаторе специальным вызвовом (PyMem_Malloc), а потом освобождал в «трудную минуту».

программист 2014
  1. Есть вот такой вопрос о PHP.
    Каковы внутренние ограничения по производительности самого PHP.
    А именно какова корреляция между использованными аппаратными ресурсами и количеством одновременно используемых классов/объектов, количеством вызываемых методов и их вложенностью и т. д.?
  1. И есть вот такой вопрос по аппаратным ресурсам. Какова стандартная аппаратно-системная конфигурация (процессоры, память, ОС и пр.) для реально сложного (скажем, просто сложного, не вдаваясь в детали) проекта на PHP, который должен обслуживать десятки и сотни тысяч параллельно работающих в системе пользователей? (Хотелось бы просто понять общие характеристики одной строкой. К примеру, в таком стиле «для прохождения этой дороги хватит и старого российского авто, здесь нужен внедорожник, а здесь и танк не пройдет».)

Спрашиваю у вас, поскольку по-моему натакой вопрос может ответить только гуру.

Евгений Степанищев (bolknote.ru) 2014

Комментарий для программист:

1) понятия не имею. В современном мире очень важна скорость разработки. У ООП в этом плане больше плюсов по сравнению с процедурным стилем, так что я выберу ООП, разве что сделаю исключения для каких-то скриптов с простым процессом внутри.

2) на такой вопрос отвечает системный администратор, а я даже не программист (я — топ-менеджер, хоть и программирующий время от времени, но бывает что я полгода-год ничего не программирую по работе). Кроме того, вы вопрос некорректно поставили, на него невозможно ответить. Что с того, что их десятки и сотни тысяч? Совершенно неизвестно что у вас за проект будет, где его узкие места, что эти пользователи там делают? В такой общей формулировке никто на него не ответит.