company_banner

Пробуем preload (PHP 7.4) и RoadRunner



    Привет, Хабр! 

    Мы часто пишем и говорим о производительности PHP: как мы ей занимаемся в целом, как мы сэкономили 1 млн долларов при переходе на PHP 7.0, а также переводим разные материалы на эту тему. Это вызвано тем, что аудитория наших продуктов растёт, а масштабирование PHP-бэкенда при помощи железа сопряжено со значительными затратами — у нас 600 серверов с PHP-FPM. Поэтому инвестирование времени в оптимизацию для нас выгодно.

    Прежде мы говорили в основном об обычных и уже устоявшихся способах работы с производительностью. Но сообщество PHP не дремлет! В PHP 8 появится JIT, в PHP 7.4 — preload, а за пределами core-разработки PHP развиваются фреймворки, подразумевающие работу PHP как демона. Пора поэкспериментировать с чем-то новым и посмотреть, что это может нам дать.

    Так как до релиза PHP 8 ещё далеко, а асинхронные фреймворки плохо подходят для наших задач (почему — расскажу ниже), сегодня остановимся на preload, который появится в PHP 7.4, и фреймворке для демонизации PHP — RoadRunner.

    Это текстовая версия моего доклада с Badoo PHP Meetup #3. Видео всех выступлений мы собрали в этом посте.

    PHP-FPM, Apache mod_php и подобные способы запуска PHP-скриптов и обработки запросов (на которых работает подавляющее большинство сайтов и сервисов; для простоты я буду называть их «классическим» PHP) работают по принципам shared-nothing в широком смысле этого термина:

    • состояние не шарится между воркерами PHP;
    • состояние не шарится между различными запросами.

    Рассмотрим это на примере простейшего скрипта:

    // инициализация
    $app = \App::init();
    $storage = $app->getCitiesStorage();
    
    // полезная работа
    $name = $storage->getById($_COOKIE['city_id']);
    
    echo "Ваш город: {$name}";

    Для каждого запроса скрипт выполняется с первой до последней строчки: несмотря на то, что инициализация, скорее всего, не будет отличаться от запроса к запросу и её потенциально можно выполнить единожды (сэкономив ресурсы), всё равно её придётся повторять для каждого запроса. Мы не можем просто взять и сохранить переменные (например, $app) между запросами из-за особенностей того, как работает «классический» PHP.

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

    // инициализация
    $app = \App::init();
    $storage = $app->getCitiesStorage();
    
    $cities = $storage->getAll();
    
    // цикл обработки запросов
    while ($req = getNextRequest()) {
        $name = $cities[$req->getCookie('city_id')];
    
        echo "Ваш город: {$name}";
    }

    Мы смогли не только избавиться от повторяющейся для каждого запроса инициализации, но и сохранить список городов единожды в переменную $cities и использовать её из различных запросов, не обращаясь никуда кроме памяти (это самый быстрый способ получить какие-либо данные).

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

    Для этого давайте немного усложним наш скрипт и вместо вывода переменной $name будем заполнять массив:

    -    $name = $cities[$req->getCookie('city_id')];
    +    $names[] = $cities[$req->getCookie('city_id')];
    

    В случае «классического» PHP никаких проблем не возникнет — в конце исполнения запроса переменная $name уничтожится и каждый следующий запрос будет работать так, как ожидается. В случае же запуска PHP как демона каждый запрос будет добавлять в эту переменную очередной город, что приведёт к бесконтрольному росту массива, пока на машине не закончится память.

    Вообще может не только закончиться память — могут произойти какие-то другие ошибки, которые приведут к смерти процесса. С такими проблемами «классический» PHP справляется автоматически. В случае же запуска PHP как демона нам нужно как-то следить за этим демоном, перезапускать его, если он упал.

    Такого типа ошибки неприятны, но для них существуют эффективные решения. Куда хуже, если из-за ошибки скрипт не упадёт, а непредсказуемо изменит значения каких-то переменных (например, очистит массив $cities). В таком случае все последующие запросы будут работать с некорректными данными. 

    Если подытожить, то для «классического» PHP (PHP-FPM, Apache mod_php и подобных) проще писать код — он освобождает нас от целого ряда проблем и ошибок. Но за это мы платим производительностью.

    Из примеров выше мы видим, что в некоторых частях кода на обработку каждого запроса «классического» PHP тратит ресурсы, которые можно было бы не тратить (или тратить однократно). Это следующие области:

    • подключение файлов (include, require и т. п.);
    • инициализация (фреймворк, библиотеки, DI-контейнер и т. д.);
    • запрос данных из внешних хранилищ (вместо хранения в памяти).

    PHP существует много лет и, возможно, даже стал популярным благодаря такой модели работы. За это время было выработано множество способов разной степени успешности для решения описанной проблемы. Некоторые из них я упоминал в своей предыдущей статье. Сегодня же остановимся на двух достаточно новых для сообщества решениях: preload и RoadRunner.

    Preload


    Из трёх пунктов, перечисленных выше, preload призван бороться с первым — оверхедом при подключении файлов. На первый взгляд это может показаться странным и бессмысленным, ведь в PHP уже есть OPcache, который был создан именно для этой цели. Для понимания сути давайте попрофилируем при помощи perf реальный код, над которым включён OPcache, у которого hit rate равен 100%. 



    Несмотря на OPcache, мы видим, что persistent_compile_file занимает 5,84% времени выполнения запросов. 

    Для того чтобы разобраться, почему так происходит, мы можем посмотреть исходники zend_accel_load_script. Из них видно, что, несмотря на наличие OPcache, при каждом вызове include/require копируются сигнатуры классов и функций из общей памяти в память процесса-воркера, а также делается различная вспомогательная работа. И эта работа должна быть сделана для каждого запроса, так как по его окончании память процесса-воркера очищается.



    Это усугубляется большим количеством вызовов include/require, которое мы обычно производим за один запрос. Например, Symfony 4 подключает порядка 310 файлов до выполнения первой полезной строчки кода. Иногда это происходит неявно: чтобы создать инстанс класса A, приведённого ниже, PHP выполнит автозагрузку всех остальных классов (B, C, D, E, F, G). И особенно в этом плане выделяются зависимости Composer’а, объявляющие функции: чтобы гарантировать, что эти функции будут доступны во время выполнения пользовательского кода, Composer вынужден всегда подключать их вне зависимости от использования, так как в PHP отсутствует автозагрузка функций и они не могут быть подгружены в момент вызова.

    class A extends \B implements \C {
        use \D;
    
        const SOME_CONST = \E::E1;
        private static $someVar = \F::F1;
    
        private $anotherVar = \G::G1;
    }


    Как работает preload


    Preload имеет одну-единственную основную настройку opcache.preload, в которую передаётся путь до PHP-скрипта. Этот скрипт будет выполнен однократно при запуске PHP-FPM/Apache/и т. п., а все сигнатуры классов, методов и функций, которые будут объявлены в этом файле, станут доступны всем скриптам, обрабатывающим запросы, с первой строчки их выполнения (важное замечание: это не относится к переменным и глобальным константам — их значения обнулятся после окончания фазы preload). Больше не нужно делать вызовы include/require и копировать сигнатуры функций/классов из общей памяти в память процесса: все они объявляются immutable и за счёт этого все процессы могут ссылаться на один и тот же участок памяти, содержащий их.

    Обычно нужные нам классы и функции лежат в разных файлах и объединять их в один preload-скрипт неудобно. Но этого делать и не нужно: так как preload — это обычный PHP-скрипт, мы можем просто использовать include/require или opcache_compile_file() из preload-скрипта для всех нужных нам файлов. Кроме того, так как все эти файлы будут подгружены единожды, PHP сможет произвести дополнительные оптимизации, которые невозможно было сделать, пока мы по отдельности подключали эти файлы в момент выполнения запроса. PHP делает оптимизации только в рамках каждого отдельного файла, но в случае с preload — для всего кода, подгруженного в фазе preload.

    Бенчмарки preload


    Для того чтобы продемонстрировать на практике пользу от preload, я взял один CPU-bound endpoint Badoo. Для нашего бэкенда в целом характерна CPU-bound-нагрузка. Этот факт является ответом на вопрос, почему мы не рассматривали асинхронные фреймворки: они не дают никакого преимущества в случае CPU-bound-нагрузки и при этом ещё больше усложняют код (его нужно писать иначе), а также для работы с сетью, диском и прочим требуются специальные асинхронные драйвера. 

    Чтобы в полной мере оценить пользу от preload, для эксперимента я загрузил с помощью него вообще все файлы, которые необходимы тестируемому скрипту во время работы, и нагрузил его подобием обычной production-нагрузки при помощи wrk2 — более продвинутого аналога Apache Benchmark, но такого же простого.

    Чтобы попробовать preload, нужно сначала перейти на PHP 7.4 (у нас сейчас PHP 7.2). Я замерил производительность PHP 7.2, PHP 7.4 без preload и PHP 7.4 с preload. Получилась вот такая картина:



    Таким образом, переход c PHP 7.2 на PHP 7.4 даёт +10% к производительности на нашем endpoint’е, а preload даёт ещё 10% сверху. 

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

    Нюансы preload


    У того, что увеличивает производительность, обычно есть обратная сторона. У preload есть множество нюансов, которые я приведу ниже. Их все нужно учитывать, но только один (первый) может быть принципиальным.

    Изменение — перезапуск


    Так как все preload-файлы компилируются только при запуске, помечаются как immutable и не перекомпилируются в дальнейшем, единственный способ применить изменения в этих файлах — перезапустить (reload или restart) PHP-FPM/Apache/и т. п.

    В случае reload PHP старается произвести перезапуск максимально аккуратно: запросы пользователей не будут оборваны, но тем не менее, пока идёт preload-фаза, все новые запросы будут ждать её завершения. Если в preload будет не много кода, это может не доставить проблем, но если попытаться загрузить всё приложение, то это чревато значительным увеличением времени ответа во время перезапуска.

    Также у перезапуска (вне зависимости от того, reload это или restart) есть важная особенность — в результате этого действия очищается OPcache. То есть все запросы после него будут работать с холодным опкод-кешем, что может увеличить время ответа ещё больше.

    Неопределённые символы


    Чтобы preload смог подгрузить класс, всё, от чего он зависит, должно быть определено до этого момента. Для класса ниже это означает, что перед компиляцией этого класса должны быть доступны все другие классы (B, C, D, E, F, G), переменная $someGlobalVar и константа SOME_CONST. Так как preload-скрипт — это обычный PHP-код, мы можем определить автолоадер. В таком случае всё, что связано с другими классами, будет подгружено им автоматически. Но это не работает с переменными и константами: мы сами должны гарантировать, что они определены на момент объявления этого класса.

    class A extends \B implements \C {
        use \D;
    
        const SOME_CONST = \E::E1;
        private static $someVar = \F::F1;
    
        private $anotherVar = \G::G1;
        private $varLink = $someGlobalVar;
        private $constLink = SOME_CONST;
    }

    К счастью, preload содержит достаточно инструментов для понимания того, получилось разрезолвить что-то или нет. Во-первых, это warning-сообщения с информацией о том, что не удалось подгрузить и почему:

    PHP Warning: Can't preload class MyTestClass with unresolved initializer for constant RAND in /local/preload-internal.php on line 6
    PHP Warning: Can't preload unlinked class MyTestClass: Unknown parent AnotherClass in /local/preload-internal.php on line 5

    Во-вторых, preload добавляет отдельную секцию в результат функции opcache_get_status(), в которой видно, что было успешно загружено в фазе preload:



    Оптимизация полей/констант класса


    Как я писал выше, preload резолвит значения полей/констант класса и сохраняет их. Это позволяет оптимизировать код: во время обработки запроса данные уже готовы и их не нужно выводить из других данных. Но это может приводить к неочевидным результатам, которые демонстрирует следующий пример:

    const.php:
    <?php
    define('MYTESTCONST', mt_rand(1, 1000));

    preload.php:
    <?php
    
    include 'const.php';
    class MyTestClass {
        const RAND = MYTESTCONST;
    }

    script.php:
    <?php
    
    include 'const.php';
    echo MYTESTCONST, ', ', MyTestClass::RAND;
    // 32, 154

    Получается контринтуитивная ситуация: казалось бы, константы должны быть равны, так как одной из них присваивалось значение другой, но на деле это оказывается не так. Связано это с тем, что глобальные константы, в отличие от констант/полей класса, принудительно очищаются после окончания фазы preload, в то время как константы/поля класса — резолвятся и сохраняются. Это приводит к тому, что во время выполнения запроса нам приходится определять глобальную константу заново, в результате чего она может получить другое значение.

    Cannot redeclare someFunc()


    В случае с классами ситуация простая: обычно мы не подключаем их в явном виде, а пользуемся автолоадером. Это означает, что если класс определён в фазе preload, то во время запроса автолоадер просто не выполнится и мы не будем пытаться подключить этот класс второй раз. 

    С функциями ситуация иная: мы должны подключать их явно. Это может привести к ситуации, когда мы в preload-скрипте подключим все необходимые файлы с функциями, а во время запроса попытаемся сделать это снова (типичный пример — загрузчик Composer’а: он всегда будет пытаться подключить все файлы с функциями). В таком случае мы получим ошибку: функция уже была определена и переопределить её нельзя. 

    Решить эту проблему можно по-разному. В случае с Composer’ом можно, например, подключить вообще всё в preload-фазе, а во время запросов не подключать вообще ничего, что относится к Composer’у. Другое решение — не подключать файлы с функциями напрямую, а делать это через прокси-файл с проверкой на function_exists(), как, например, делает Guzzle HTTP.



    PHP 7.4 ещё официально не вышел (пока)


    Этот нюанс станет неактуальным через какое-то время, но пока версия PHP 7.4 ещё официально не вышла и команда PHP в release notes явно пишет: «Please DO NOT use this version in production, it is an early test version». Во время наших экспериментов с preload мы наткнулись на несколько багов, сами фиксили их и кое-что даже отправили в апстрим. Чтобы избежать неожиданностей, лучше дождаться официального релиза.

    RoadRunner


    RoadRunner — это демон, написанный на Go, который, с одной стороны, создаёт PHP-воркеры и следит за ними (запускает/завершает/перезапускает по мере необходимости), а с другой — принимает запросы и передаёт их на выполнение этим воркерам. В этом смысле его работа ничем не отличается от работы PHP-FPM (где тоже есть мастер-процесс, который следит за воркерами). Но отличия всё-таки есть. Ключевое заключается в том, что RoadRunner не обнуляет состояние скрипта после окончания выполнения запроса. 

    Таким образом, если вспомнить наш список того, на что тратятся ресурсы в случае «классического» PHP, RoadRunner позволяет бороться со всеми пунктами (preload, как мы помним, — только с первым):

    • подключение файлов (include, require и т. п.);
    • инициализация (фреймворк, библиотеки, DI-контейнер и т. д.);
    • запрос данных из внешних хранилищ (вместо хранения в памяти).

    Hello World-пример в случае с RoadRunner выглядит примерно так:

    $relay = new Spiral\Goridge\StreamRelay(STDIN, STDOUT);
    $psr7 = new Spiral\RoadRunner\PSR7Client(new Spiral\RoadRunner\Worker($relay));
    
    while ($req = $psr7->acceptRequest()) {
            $resp = new \Zend\Diactoros\Response();
            $resp->getBody()->write("hello world");
            $psr7->respond($resp);
    }

    Мы будем пытаться наш текущий endpoint, который мы тестировали с preload, без модификаций запустить на RoadRunner, нагружать его и измерять производительность. Без модификаций — потому что в противном случае бенчмарк будет не совсем честным.

    Давайте попробуем адаптировать Hello World-пример для этого.

    Во-первых, как я писал выше, мы не хотим, чтобы воркер падал в случае ошибки. Для этого нам нужно всё обернуть в глобальный try..catch. Во-вторых, поскольку наш скрипт ничего не знает о Zend Diactoros, для ответа нам нужно будет его результаты сконвертировать. Для этого воспользуемся ob_- функциями. В-третьих, наш скрипт ничего не знает о сущности PSR-7 запроса. Решение — заполнить стандартное PHP-окружение из этих сущностей. И в-четвёртых, наш скрипт рассчитывает на то, что запрос умрёт и всё состояние очистится. Поэтому с RoadRunner нам нужно будет эту очистку делать самостоятельно.

    Таким образом, изначальный Hello World-вариант превращается примерно в такой:

    while ($req = $psr7->acceptRequest()) {
        try {
            $uri = $req->getUri();
    
            $_COOKIE = $req->getCookieParams();
            $_POST = $req->getParsedBody();
            $_SERVER = [
                'REQUEST_METHOD' => $req->getMethod(),
                'HTTP_HOST' => $uri->getHost(),
                'DOCUMENT_URI' => $uri->getPath(),
                'SERVER_NAME' => $uri->getHost(),
                'QUERY_STRING' => $uri->getQuery(),
    
                // ...
            ];
    
            ob_start();
    
            // our logic here
    
            $output = ob_get_contents();
            ob_clean();
            
            $resp = new \Zend\Diactoros\Response();
            $resp->getBody()->write($output, 200);
            $psr7->respond($resp);
        } catch (\Throwable $Throwable) {
            // some error handling logic here
        }
    
        \UDS\Event::flush();
        \PinbaClient::sendAll();
        \PinbaClient::flushAll();
        \HTTP::clear();
        \ViewFactory::clear();
        \Logger::clearCaches();
        
        // ...
    }


    Бенчмарки RoadRunner


    Ну что, пришло время запускать бенчмарки.



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



    В результатах perf мы видим phar_compile_file. Это вызвано тем, что мы подключаем некоторые файлы во время выполнения скрипта и, так как OPcache не включён (RoadRunner запускает скрипты как CLI, где OPcache по умолчанию выключен), эти файлы компилируются заново при каждом запросе. 

    Отредактируем конфигурацию RoadRunner — включим OPcache:





    Эти результаты уже больше похожи на то, что мы ожидали увидеть: RoadRunner начал показывать бОльшую производительность, чем preload. Но, возможно, нам удастся получить ещё больше!

    В perf вроде бы нет больше ничего необычного — давайте посмотрим на PHP-код. Самый простой способ профилировать его — это использовать phpspy: он не требует никакой модификации PHP-кода — нужно просто запустить его в консоли. Сделаем это и построим flame graph:



    Так как логику нашего приложения мы договорились не модифицировать для чистоты эксперимента, нас интересует ветка стеков, связанная с работой RoadRunner:



    Основная часть её сводится к вызову fread(), с этим вряд ли что-то можно сделать. Но мы видим какие-то ещё ветки в \Spiral\RoadRunner\PSR7Client::acceptRequest(), кроме самого fread. Понять их смысл можно, заглянув в исходный код:

       /**
         * @return ServerRequestInterface|null
         */
        public function acceptRequest()
        {
            $rawRequest = $this->httpClient->acceptRequest();
            if ($rawRequest === null) {
                return null;
            }
    
            $_SERVER = $this->configureServer($rawRequest['ctx']);
    
            $request = $this->requestFactory->createServerRequest(
                $rawRequest['ctx']['method'],
                $rawRequest['ctx']['uri'],
                $_SERVER
            );
    
            parse_str($rawRequest['ctx']['rawQuery'], $query);
    
            $request = $request
                ->withProtocolVersion(static::fetchProtocolVersion($rawRequest['ctx']['protocol']))
                ->withCookieParams($rawRequest['ctx']['cookies'])
                ->withQueryParams($query)
                ->withUploadedFiles($this->wrapUploads($rawRequest['ctx']['uploads']));

    Становится понятно, что RoadRunner по рассериализованному массиву пытается создать объект PSR-7-совместимого запроса. Если ваш фреймворк работает с PSR-7-объектами запросов напрямую (например, Symfony не работает), то это вполне оправданно. В остальных случаях PSR-7 становится лишним звеном перед тем, как запрос будет сконвертирован в то, с чем может работать ваше приложение. Давайте уберём это промежуточное звено и посмотрим на результаты снова:



    Тестируемый скрипт был достаточно лёгким, поэтому удалось выжать ещё весомую долю производительности — +17% по сравнению с чистым PHP (напомню, что preload даёт +10% на том же скрипте).

    Нюансы RoadRunner


    В целом использование RoadRunner — это более серьёзное изменение, чем просто включение preload, поэтому нюансы здесь ещё более значимые.

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

    Во-вторых, если мы хотим выжать из RoadRunner максимум, то недостаточно просто запустить «классический» код на нём — нужно изначально писать код под него. В таком случае мы избежим хаков с перегоном запроса/ответа между форматами RoadRunner и приложения; возможно, мы сразу будем писать код так, чтобы ему не потребовалась очистка в конце запроса, а также сможем в большем объёме использовать возможности общей памяти между запросами, например что-то кешируя в ней.

    В-третьих, все результаты были получены на нашем конкретном endpoint’е, который, возможно, не лучший кандидат для запуска под RoadRunner. На других скриптах возможны абсолютно другие результаты.

    Заключение


    Итак, мы рассмотрели архитектуру «классического» PHP, разобрались с тем, как ей может помочь preload и чем от неё отличается архитектура RoadRunner.

    PHP в «классическом» варианте использования (PHP-FPM, Apache mod_php и другие) помогает упростить разработку и избежать ряда проблем. В случае если бэкенд не имеет каких-то особых требований к производительности, это самый эффективный способ разработки из рассмотренных. Кроме того, придумано множество способов выжать из этого решения максимум производительности, к которым уже добавился preload и скоро добавится JIT.

    Если изначально понятно, что бэкенд будет нагружен, возможно, имеет смысл сразу смотреть в сторону RoadRunner, так как он потенциально может дать ещё больше производительности.

    Ещё раз приведу результаты, полученные в ходе наших экспериментов (повторюсь: с большой долей вероятности прирост будет разный в зависимости от приложения):

    • PHP 7.2 — 845 RPS;
    • PHP 7.4 — 931 RPS;
    • RoadRunner без оптимизаций — 987 RPS; 
    • PHP 7.4 + preload — 1030 RPS;
    • RoadRunner после оптимизации — 1089 RPS. 

    Мы в Badoo приняли решение о переходе на PHP 7.4 и сейчас адаптируем к нему наш код, чтобы перейти на него сразу после официального релиза (когда он станет достаточно стабильным). 

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

    Спасибо за внимание!
    Badoo
    305,98
    Big Dating
    Поделиться публикацией

    Комментарии 51

      +2

      А можно показать, что именно делает скрипт?

        +4

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


        Что, наверное, может быть важно — в скрипте мы не ходим в базы и внешние хранилища. Он CPU-bound (как я писал выше). Если сильно упростить, то он сводится к тому, что мы получаем большие пачки данных от клиентов, рассериализовываем их, преобразуем в сущности (создаём много объектов разных классов — их может быть несколько десятков на запрос), преобразуем их по-всякому, сериализуем-пакуем и отправляем в LSD.

          +3
          да и в этом нет большого смысла

          Единственное что срезает RoadRunner — это бутстрап. И вот в вашем варианте не ясно, сколько занимала изначально инициализация, а сколько непосредственно работа.


          получаем большие пачки данных от клиентов

          Network bound? Это всё потенциально даёт флуктации в измерениях, которые сильно искажают результаты тестирования. В этом случае, как мне кажется, будет более уместен hello world.

            +3

            Там был ещё огромный запас до того, чтобы упереться в сеть — никаких флуктуаций в данном случае от этого не было.
            Точную долю на бутстрап назвать сложно (если в середине логики мы троагем какой-то новый класс и выполняется его автолоадинг — это тоже в какой-то степени бутстрап, как и какое-то условное ленивое поднятие конфигов/библиотек, которое может случиться тоже где-то посередине скрипта). За какой-то примерный ориентир можно взять 1/4 времени выполнения скрипта как затраты на бутстрап — примерно такая цифра получается в результате анализа того, что показывают профайлеры.

              0

              Я не про упереться говорю, а про то, что сетевое взаимодействие не является операцией с константным временем выполнения и сильно зависит от внешних факторов. То есть затуп в сети может запросто дать большое искажение в общий результат.

              +4

              Не только, он еще срежет время на keep-alive соединения и на коннект с базой.

                +3

                Верно. На всё, что инициализировано или закешировано.

                  +2
                  К базе можно использовать persistent connect.
                    0

                    Сразу пол него вспомнил, когда прочитал, что всё очищается после окончания обработки запроса.

            +1

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

              +1
              На CPU-bounded выжать скорость больше чем может дать CPU уже не получится.

              С этим сложно спорить. :) Но суть как раз заключается в том, на что именно тратится CPU. В статье я писал, на чём может потенциально помочь сэкономить RoadRunner (подключение файлов, инициализация, кешировние чего-то в памяти и т. д.). Всё это присутствовало в нашем тестовом скрипте, из-за чего и получился выигрыш.

                +1

                Если 90 процентов жрёт инициализация (вполне реально для какого-то di/sl контейнера без ленивого инстанцирования), то можно выжать на порядок большую скорость.

                0
                Есть вопрос, о котором мы с приятелем спорим.

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

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

                Какие есть мнения у общества?
                  0
                  С другой стороны — никто не оптимизирует программу чтобы собрать все замыкания в условном бустрапе.

                  На ларавеле возможно нет, но это один из лучших и проверенных способов оптимизации для long-running приложений.

                    +1
                    Такая оптимизация по цене — примерно как написать свой фреймверк. Хотя начиная с определенной нагрузки — безусловно необходимая.
                      0

                      Все верно, мы и написали свой фреймворк. :)

                    +4

                    У Yii не так много замыканий. Я запустил Yii 2 и получил ответ за 1-2 мс. А в Yii 3 поддержка RoadRunner и Swoole, вероятно, будет из коробки.

                      +1

                      И это на винде где разрешение таймера как раз и есть 1-2 мс. На Linux должно быть меньше.

                    –5
                    Переписывайте web часть на swoole, 600 серверов php-fpm — кошмар.
                      +12
                      Напишите статью как вы переписали на swoole и покажите результаты. Я думаю всем будет очень интересно.
                        0
                        Так там ничего сложного такого прям нет, чтобы портировать на Swoole. Сейчас веду доработку с пулером заранее установленных соединений по работе с разными БД в асинхронном режиме с использованием PHP + Swoole:

                        github.com/eltaline/swoole-pool — тут пока черновики, но скоро сделаю нормальные классы, которыми без try catch удобно будет пользоваться, а вся логика отказоустойчивости и распределения данных будет убрана в классы. Собственно я и перевожу все свои проекты на swoole.

                        Так как асинхронность дает отличные показатели и к тому же используется гораздо меньше памяти. Разница в количестве одновременно обслуживаемых запросов колоссальна. Да и портировать легко в отличии от React PHP.
                          +9
                          Много слов. Как раз повод написать статью! Ждем с нетерпением!
                      –3
                      Я не оч. понимаю, зачем все это нужно )

                      Ну т.е. типичное приложение на php зачастую занимается тем, что добрую половину времени ждет ответа от базы данных и ничего по сути само не делает. Ну увеличили вы на пару процентов инициализацию, но в итоге как висел php-fpm процесс, так и висит, ждет ответа от базы/апи и т.д.

                      И если будет одновременно 10000 запросов идти, всё упадет, потому что придется держать 10000 воркеров php-fpm одновременно. И даже если переписать это на RoadRunner, ничего не изменится — нужно будет также 10000 воркеров, которые будут ждать ответа от базы данных.

                      Короче, имхо для хайлоада лучше php всё же не использовать. Ни с RoadRunner ни с preload
                        +6

                        Всё это зависит от конкретного приложения: характера того, что оно делает, и степени его "оптимизированности". У нас бОльшую часть времени выполнения клиентского запроса занимает непосредственно работа PHP: все горячие данные лежат в быстрых сервисах (от которых время ответа в пределах единиц милисекунд).
                        10000 запросов мы выдерживаем без проблем, но если на нас внезапно увеличится траффик в несколько раз, то в первую очередь мы ощутим это именно по CPU, а не по сервисам/базам.


                        В общем, всё то, что вы говорите — верно, но только для ограниченного набора случаев. Часто бывает по-другому и даже с точностью до наоборот :)

                          –7
                          а почему всё же php? Т.е. вся эта возня с прелоадом и RoadRunner просто будет не нужна, если взять golang, rust, nodejs и т.д.
                          +9
                          Я не оч. понимаю,

                          Я тоже не очень понимаю на чем основано ваше мнение? Я вот работал с одним из самых высоконагруженных сайтов в мире и там используется PHP. И в Badoo и в Facebook довольно высокая нагрузка и используется PHP уже много лет.
                          Вы вот видели хоть один вебсайт на Rust написанный?
                          Я думаю это будет отличная статья! Я готов на вас даже подписаться, если у вас есть пример сайта с высокой нагрузкой работающий на Rust.
                          И даже если переписать это на RoadRunner, ничего не изменится

                          То есть опять, люди — специалисты, потратили время и силы чтобы все протестировать и обосновать, подтвердить все доказательствами и измерениями, написать статью, чтобы поделиться своим мнением, идеями и опытом. Но вам просто все и так понятно — все врут, а вы просто знаете истину (аки бог?) и голословно утверждаете что «ничего не изменится» в ответ на развернутую статью с доказательствами что, и как именно, и почему изменится?
                            –1
                            Я тоже не очень понимаю на чем основано ваше мнение?

                            На опыте и логике

                            Я работал с хайлоадом на php, но по сути просто потому, что это было легаси, и потому что код писать немного проще. У меня были кейсы, когда всё упиралось именно в проблематику php-fpm. Например, нужно параллельно дернуть бд и несколько API — php не подходит (или нужно жестко костылять). Нужно, чтобы сервера с бд работали чуть дольше чем какой-то минимум, то php не подходит, нужно слишком много воркеров, всё умирает. И т.д.

                            Если взять Golang, то никаких таких проблем нет. Если взять nodejs, то тоже можно разрулить (но там побольше возни, так как nodejs юзает одно ядро). На расте тоже можно.

                            специалисты, потратили время и силы чтобы все протестировать и обосновать, подтвердить все доказательствами и измерениями, написать статью, чтобы поделиться своим мнением, идеями и опытом.

                            Спасибо, что они поделились, было интересно. Я плюсанул статью, если чо. Однако вопрос, почему php, остается открытым. Один воркер php-fpm в один момент времени может обслуживать только один http-запрос. Это накладывает кучу ограничений. Потому что нельзя просто так взять и наплодить 10000 процессов в ОС.

                            вы просто знаете истину (аки бог?) и голословно утверждаете

                            я же привел аргументы. Причем тут вообще бог )

                            Вы вот видели хоть один вебсайт на Rust написанный?

                            При чем тут, видел, не видел. Rust точно подходит, так как там есть например библиотека tokio, заточенная на такие вещи. По сути аналог зеленым тредам в го
                              +10
                              При чем тут, видел, не видел. Rust точно подходит, так как там есть например библиотека tokio, заточенная на такие вещи. По сути аналог зеленым тредам в го

                              Я попробую объяснить почему " видел, не видел" тут при чем. Во-первых вы его упомянули как альтернативу «неприемлемому» на ваш взгляд PHP. А во-вторых, очень много вещей и теорий выглядящих логично на первый взгляд не выдерживают проверки временем и практикой. Поэтому любые теоретические преимущества очень полезно проверять экспериментально.

                              Например, в далекие 80-ые было расхожее утверждение что параллельный LPT порт должен быть быстрее чем последовательный COM, поскольку мы передаем данные «одновременно» и пропускная способность должна расти соответственно. Но уже лет через 10-15 стало ясно что по технологическим причинам реализация высокоскоростных последовательных интерфейсов намного проще и на свет появился USB, который развивается и здравствует и по сей день.

                              Или вот возьмем эту статью, RoadRunner показал себя не лучшим образом при первом запуске. Пришлось покопаться авторам чтобы заставить его принести какую-то пользу. Но, как они написали в заключении, они не собираются пока его ставить в продакшн. (Я бы тоже не поставил). Потому что за все надо платить. И приложение и разработчики должны будут постоянно сверять свой код на предмет «совместимости» с RoadRunner. И к багам и ошибкам самого. кода могут прибавиться какие-то новые баги и ошибки вызванные подобным архитектурным решением.

                              Насчет Rust, я почти уверен что мы никогда не увидим сколь угодно значимых внедрений
                              на backend там где активно используется PHP. У PHP есть своя ниша в вебразработке которую он хорошо держит вместе с кучей фрэймворков. Есть хоть один на Rust (ориентированный именно на MVC)? мне кажется максимум что сможет тут предложить Rust это какой-нибудь RESTfull фрэймворк. Хотя поскольку с Rust я не знаком вообще, все же подозреваю что даже слово «фрэймворк» там слабо применимо.

                              Даже если предположить, что Rust может быть удобен для веба. Пожелание переписать Badoo на Rust не может вызвать ничего кроме улыбки. Я не знаю сколько раз в своей жизни вы переписывали что-то действительно большое и сложное полностью с одного языка и парадигмы на другой язык с другой парадигмой. На практике это АД. Реально это возможно только по кусочкам. Как и делают многие компании, начиная дробить монолиты на микросервисы и уже на уровне микросервиса выбирая технологический стек. Для любого ИТ менеджара-практика, ваше предложение переписать все на Rust как алтьтернатива перейти на PHP 7.4 + RoadRunner далеки друг от друга как небо и земля.

                              PS: Но я вам даже где-то завидую. Уже много лет я не могу произнести ничего настолько простого как: «При чем тут, видел, не видел. Rust точно подходит, так как там есть например библиотека tokio, заточенная на такие вещи.». Это так просто и так наивно, что даже слова трудно подобрать, чтобы начать возражать. На Марс тоже просто полететь, ведь есть же Роскосмос, заточенный пот такие вещи?
                                0
                                Где конкретно я предлагал переписывать весь старый код Badoo на Раст?

                                Все что я хочу сказать, что для хайлоада есть более удачные альтернативы, чем PHP, на мой взгляд:

                                1) Если нужна относительная простота синтаксиса и заточенность языка на конкаренси, то Go. К примеру, как показывает мой личный опыт, не так уж и сложно переучить команду пхп-шников на го.

                                2) Если нужно выжать все соки производительности, то Rust или C/C++. Полно веб-фреймворков на Rust, если что. Единственный минус — порог входа в язык, но это цена за суперпроизводительность и безопасность по памяти без GC. Смотря что нужно.

                                3) Есть также удобства в nodejs: все знают язык Javascript, например.

                                У PHP, кмк, меньше всего преимуществ (именно для хайлоада, сам язык норм). И я написал почему, в предыдущих коментах. Если вы не согласны, аргументируйте.

                                Теперь про переписывание/непереписывание кода, раз уж зашла речь. Это зависит от бизнеса. Вполне может быть, что и на Раст стоит переписать самые нагруженные части. Если в Badoo крутятся 100500 серверов, то переписав с PHP на Раст можно сэкономить кучу денег. Даже если писать на Раст очень дорого и сложно. И совсем это не наивно. Это зависит от ситуации.

                                И давайте уже обойдемся без переходов на личности(«так просто и наивно»), а также без неуместных сравнений (Марс, Роскосмос) и без апеллирования к авторитету («специалисты написали статью»). Я предпочитаю конкретные аргументы, и всегда рад оказаться неправым и узнать что-то новое в конструктивной беседе.
                                  +2
                                  не так уж и сложно переучить команду пхп-шников на го.

                                  При условии, что они пишут в стиле PHP3? :) А если серьёзно, то очень сложно переходить, если нет какого-то квалифицированного фидбэка. Пишешь код, он работает, пускай и его в 2 раза больше чем было на PHP хотя бы за счёт if err!=nil через строку, но не понимаешь то ли ты говнокод написал, то ли в мире Go это говнокодом не считается.

                                    0

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

                                      0

                                      Ну вот решили попробовать что-то на Go написать в виде эксперимента что ли, но который в продакшен пойдёт 99% — типа важная инфраструктурная часть, которая должн работать быстро и асинхронно, вроде типовая задача для Go. Нанимать тругошника никто не будет под это, даже пехепешника с Го в бэкграунде. И если среди 5 пехепешников ни у кого опыта с Го нет, то как-то непонятно вообще как оценивать результат: работать работает, заметно быстрее чем PHP. Как работает обычному пехепешнику вроде понятно, даже логику где-то можно изменить относительно быстро. Но вот код выглядит как сборник PHP антипаттернов большей частью. И не понятно, это просто языки настолько разные, или умения готовить Go не хватает

                                    0
                                    У PHP, кмк, меньше всего преимуществ (именно для хайлоада, сам язык норм). И я написал почему, в предыдущих коментах.

                                    В php давно есть API для выполнения параллельных curl-запросов, запросов в БД, которые можно использовать в стандартном php-fpm. Есть асинхронные фреймворки amphp, ReactPHP, которые правда накладывают ограничения на код (эти ограничения есть и в nodejs и в других языках).


                                    Если в Badoo крутятся 100500 серверов, то переписав с PHP на Раст можно сэкономить кучу денег.

                                    Нужно иметь ввиду, что в проектах с многолетней историей, вроде Badoo, уже написано очень много кода. Поэтому на переписывание этого кода нужно будет потратить кучу (большую) денег на разработчиков, а так же кучу времени, вместо выпуска новых фич. Как уже писали выше, перейти на PHP 7.4 с прелоадом проще. От себя добавлю, что это гораздо дешевле, экономит сервера (пусть в меньшей степени), и самое главное точно окупается.


                                    Так или иначе в баду есть сервисы для которых php не очень подходит, и они написаны не на php.

                                      0
                                      С чего ты взял, что тот же Go быстрее php? github.com/swoole/swoole-src/issues/1401
                                      С JIT те же CPU bound tasks станут быстрее.
                                    +5
                                    > Rust точно подходит, так как там есть например библиотека tokio, заточенная на такие вещи

                                    Я сейчас переписываю проект с питона (не совсем веб, но очень близко) на раст и токио.

                                    Представте что у вас есть команда для поддержки проекта. Для переписывания на асинхронный раст надо каждому добавить по 2 звездочки в лычку.

                                    Джун для будущего развития — теперь синьер разработчик.
                                    Мидл выполняющий основную работу — уже лид разработчик.
                                    Синьер управляющий разработкой модуля — становится чиф или сто средней конторы в сотню другую человек.

                                    Представте сколько стоит собрать _такую_ команду? А сколько времени подобрать людей, чтобы они были согласны на такую работу и вообще друг с другом? Потому что тут будут все опытные, знать свою цену и права.
                                      –3
                                      да, но если у тебя 100500 серверов, то выгода от переписывания может превысить затраты на написание
                                        +3
                                        вообще-то нет.

                                        На миллионе серверов команда администрирования (чисто зарплаты не весь суппорт) ест больше, чем разработчики. Каждый релиз, без исключенияБ стоит денег. На миллионе серверов проблемы есть всегда (теория больших чисел в теории вероятности). То есть каждые изменение есть двойную цену — и на само изменение и на исправление появившихся проблем.

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

                                        Именно поэтому на таких проектах строгость и придирки к качеству на уровне авиации. И именно из-за строгости микросервисы, как концепт, работают на таких маштабах.
                                          0
                                          внедрение дорогое (двойная-тройная цена), но и экономия в результате внедрения немалая. Тут надо сравнивать дебет с кредитом и решать
                                            +3
                                            Знаете, я бы прокомментировал.

                                            Но что-то при наличии исключительно плюсов за комментарии я почему-то получил минус в карму. И я действительно совершенно не понимаю за что.

                                            Так что наверное пора сворачивать этот диалог.
                                              0
                                              Я тоже получил в карму от какого-то «доброжелателя»
                                                +2

                                                Плюсанул вам в карму

                                                  0
                                                  Спасибо
                                                    0
                                                    Первый раз на Хабре получил минус за «спасибо», причем через неделю, явно от кого-то кто вообще не участвовал в обсуждении. Это уже за гранью. :-)
                                                    –1
                                                    спасибо
                                                    я уже ответил ниже
                                                  0
                                                  Чтобы отбить двойную-тройную (имхо больше) цену — сервера должны проработать на этом новом коде.

                                                  Но кроме рефактора есть и обычные релизы — багафикс, мелькие фичи. То есть обновления постоянно накатываются. Чтобы получить экономию от оптимизации, последняя должна быть или маленькии изменением или существенной по оптимизации. То есть не 10%-20%, а в разы. В случае микросервесов — возможно даже на порядки.
                                      0
                                      Roadrunner это PHP-FPM, по сути вид сбоку. А чтобы не переписывать код на Rust или Go, то можно использовать Swoole, оно очень быстрое если правильно его использовать и требует минимум усилий для того чтобы адаптировать текущий код PHP, потому что Swoole и есть сам по себе модуль к PHP. Даже есть отдельный модуль swoole_serialize, работает в 2-2.5 раза быстрее чем serialize/unserialize. А автор статьи указал, что они имеют множество сериализаций в коде. Можно запросто в 2 раза быстрее сделать сервисы опирающиеся в основном на CPU, только подключив swoole_serialize. Банально сериализатор подключить и автозаменой в коде пройтись, и вот уже профит будет. P.S. Для PHP 7.4 еще не выпустили модуль swoole_serialize.

                                      Чтобы не быть совсем голословным, вот небольшой тест по сериализации:

                                      string(29) «json_encode :0.81906008720398»
                                      string(28) «json_decode :3.0121519565582»
                                      string(26) «serialize :0.5388970375061»
                                      string(28) «unserialize :1.5703389644623»
                                      string(30) «msgpack_pack :0.55169486999512»
                                      string(31) «msgpack_unpack :1.2311758995056»
                                      string(34) «swoole_serialize :0.24641895294189»
                                      string(36) «swoole_unserialize :0.50728607177734»

                                      Скрипт бенчмарка брал отсюда: github.com/swoole/ext-serialize
                                        –2
                                        Если изначально понятно, что бэкенд будет нагружен, возможно, имеет смысл сразу смотреть в сторону RoadRunner, так как он потенциально может дать ещё больше производительности.

                                        Вероятно в этом случае надо сразу смотреть в сторону более эффективного в highload языка? Кмк PHP пишут в основном из-за того, что прототип будущего highload сервиса был на нем написан и оброс бизнес логикой и в среднем дешевле придумывать обходные пути для текущего решения, чем переписывать на другой язык?
                                          0
                                          Я так и не понял, с какой именно версией PHP работал RoadRunner, и позволяет ли он использовать preload дополнительно?
                                            0

                                            RoadRunner в экспериментах работал с версией PHP 7.4.


                                            RoadRunner фактически запускает скрипт как CLI, т.е. он позволяет делать практически всё, что можно делать в CLI, в том числе использовать preload. Но вот смысла в этом значительно меньше, чем в случае PHP-FPM, mod_php и подобных: в RoadRunner память не очищается между запросами, поэтому все классы, функции и прочие символы нужно загрузить и так только однажды (т.е. тут теряется основной смысл включения preload).
                                            Единственная разница — preload может сделать дополнительные кросс-файловые оптимизации, что не может сделать сам RoadRunner (без дополнительного включения preload). Но это обычно даёт гораздо меньший выигрыш производительности в сравнении с избеганием подключения файлов на каждый запрос.

                                              0
                                              Теоретически это может ускорить старт RR.
                                                0

                                                Да, но фактически это время просто перенесётся из периода обработки скрипта в фазу прелоада. Общее суммарное время старта не изменится.

                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                          Самое читаемое