- PVSM.RU - https://www.pvsm.ru -
Все слышали о том, что PHP создан, чтобы умирать [1]. Так вот, это не совсем правда. Если захотеть — PHP может не умирать, работать асинхронно, и даже поддерживает честную многопоточность. Но не всё сразу, в этот раз поговорим о том, как сделать чтобы он жил долго, и поможет нам в этом атомный реактор!

Атомный реактор — это проект ReactPHP [2], в описании указано «Nuclear Reactor written in PHP». На знакомство с ним меня подтолкнула вот эта [3] статья (картинка выше оттуда). Я перечитывал её несколько раз на протяжении года, но никак не получалось добраться до имплементации на практике, хотя рост производительности более чем на порядок в перспективе очень радовал.
В качестве подопытной системы выступает CleverStyle CMS, движок кэшировния APCu, версия в разработке, то есть установлены все возможные компоненты, в тестах открывается страница модуля Static pages.
В качестве тестовой железки выступает рабочий ноутбук с Core i7 4900MQ (4 ядра, 8 потоков), ОС Ubuntu 15.04 x64, дисковая подсистема состоит из двух SATA3 SSD в RAID0 (soft, btrfs, пока не лучший вариант для БД, оказалось достаточно узким местом в тестах, но есть что есть), перед каждым тестом запускается sudo sync, при каждом запросе производится 2-4 запроса в БД (создание сессии посетителя, не кэшируются на уровне БД), у Nginx 16 воркеров.
Условия не лабораторные, но с чем-то нужно работать)
Тестировать производительность будем простым Apache Benchmark.
Сначала PHP-FPM (PHP 5.5, 16 воркеров, статически):
Benchmarking cscms.org (be patient)
Completed 500 requests
Completed 1000 requests
Completed 1500 requests
Completed 2000 requests
Completed 2500 requests
Completed 3000 requests
Completed 3500 requests
Completed 4000 requests
Completed 4500 requests
Completed 5000 requests
Finished 5000 requests
Server Software: nginx/1.6.2
Server Hostname: cscms.org
Server Port: 8080
Document Path: /uk
Document Length: 99320 bytes
Concurrency Level: 128
Time taken for tests: 22.280 seconds
Complete requests: 5000
Failed requests: 4239
(Connect: 0, Receive: 0, Length: 4239, Exceptions: 0)
Total transferred: 498328949 bytes
HTML transferred: 496603949 bytes
Requests per second: 224.41 [#/sec] (mean)
Time per request: 570.373 [ms] (mean)
Time per request: 4.456 [ms] (mean, across all concurrent requests)
Transfer rate: 21842.25 [Kbytes/sec] received
Connection Times (ms)
min mean[±sd] median max
Connect: 0 0 0.5 0 3
Processing: 26 563 101.6 541 880
Waiting: 24 559 101.3 537 872
Total: 30 564 101.4 541 881
Percentage of the requests served within a certain time (ms)
50% 541
66% 559
75% 572
80% 584
90% 759
95% 795
98% 817
99% 829
100% 881 (longest request)
Конкурентность 128, поскольку при 256 PHP-FPM просто падает.
Теперь HHVM, для начала прогреем HHVM с помощью 50 000 запросов (почему [7]), потом выполним тест:
Benchmarking cscms.org (be patient)
Completed 500 requests
Completed 1000 requests
Completed 1500 requests
Completed 2000 requests
Completed 2500 requests
Completed 3000 requests
Completed 3500 requests
Completed 4000 requests
Completed 4500 requests
Completed 5000 requests
Finished 5000 requests
Server Software: nginx/1.6.2
Server Hostname: cscms.org
Server Port: 8000
Document Path: /uk
Document Length: 99309 bytes
Concurrency Level: 256
Time taken for tests: 20.418 seconds
Complete requests: 5000
Failed requests: 962
(Connect: 0, Receive: 0, Length: 962, Exceptions: 0)
Total transferred: 498398875 bytes
HTML transferred: 496543875 bytes
Requests per second: 244.88 [#/sec] (mean)
Time per request: 1045.408 [ms] (mean)
Time per request: 4.084 [ms] (mean, across all concurrent requests)
Transfer rate: 23837.54 [Kbytes/sec] received
Connection Times (ms)
min mean[±sd] median max
Connect: 0 0 1.5 0 8
Processing: 505 1019 102.6 1040 1582
Waiting: 505 1017 102.9 1039 1579
Total: 513 1019 102.5 1040 1586
Percentage of the requests served within a certain time (ms)
50% 1040
66% 1068
75% 1080
80% 1087
90% 1108
95% 1126
98% 1179
99% 1397
100% 1586 (longest request)
Получили 245 запросов в секунду, с этим и будем работать.
Хочется чтобы код не зависел от того, запускается ли он из-под HTTP сервера написанного на PHP, или в более привычном режиме.
Для этого были утилизированы headers_list()/header_remove() и http_response_code(), суперглобальные $_GET, $_POST, $_REQUEST, $_COOKIE, $_SERVER наполнялись вручную.
Системные классы разрушались после каждого запроса и создавались при новом.
В целом работало, но были нюансы:
Во-первых системные объекты были разделены на две группы — первая, запросы которые зависят от пользователя и конкретного запроса, вторая — полностью независимые.
Независимые объекты перестали разрушаться после каждого запроса что дало существенный прирост скорости.
Объект, который принимает запрос от ReactPHP и формирует ответ получил дополнительное поле __request_id. При получении системного объекта, который зависит от конкретного запроса с помощью debug_backtrace() достается этот __request_id, что позволяет разделить эти объекты для каждого отдельного запроса даже при асинхронности.
Так же были выделены отдельно системные функции, которые работают с глобальным состоянием, для HTTP сервера подключались модифицированные их версии, которые учитывают __request_id. Были добавлены функции _header() вместо header() (для работы заголовков под PHP-CLI), _http_response_code() вместо http_response_code(), уже существующие _getcookie() и _setcookie() были модифицированы, последняя под капотом вручную формирует заголовки для изменения cookie и отправляет их в _header().
Суперглобальные переменные заменяются массиво-подобными объектами, и при доступе к элементам такого странного массива мы получим данные, соответствующие конкретному запросу — тут совместимость с обычным кодом высока, главное не перезаписывать суперглобальные переменные, и иметь ввиду что там может быть не совсем массив (например, если использовать с array_merge()).
В качестве ещё одного компромиссного решения в систему был добавлен ExitException, которым заменяются вызовы exit()/die() (в том числе модифицируются сторонние библиотеки при надобности, кроме ситуаций когда реально нужно завершение всего скрипта), это позволяет перехватить выход на самом верху, и избежать завершения выполнения скрипта.
Тестируем результат на пуле из 16 запущенных Http серверов (интерпретатор HHVM), Nginx балансирует запросы (прогрев 50 000 запросов на пул):
Benchmarking cscms.org (be patient)
Completed 500 requests
Completed 1000 requests
Completed 1500 requests
Completed 2000 requests
Completed 2500 requests
Completed 3000 requests
Completed 3500 requests
Completed 4000 requests
Completed 4500 requests
Completed 5000 requests
Finished 5000 requests
Server Software: nginx/1.6.2
Server Hostname: cscms.org
Server Port: 9990
Document Path: /uk
Document Length: 99323 bytes
Concurrency Level: 256
Time taken for tests: 16.092 seconds
Complete requests: 5000
Failed requests: 1646
(Connect: 0, Receive: 0, Length: 1646, Exceptions: 0)
Total transferred: 498418546 bytes
HTML transferred: 496643546 bytes
Requests per second: 310.71 [#/sec] (mean)
Time per request: 823.928 [ms] (mean)
Time per request: 3.218 [ms] (mean, across all concurrent requests)
Transfer rate: 30246.49 [Kbytes/sec] received
Connection Times (ms)
min mean[±sd] median max
Connect: 0 0 0.9 0 6
Processing: 100 804 308.3 750 2287
Waiting: 79 804 308.2 750 2285
Total: 106 804 308.1 750 2287
Percentage of the requests served within a certain time (ms)
50% 750
66% 841
75% 942
80% 990
90% 1180
95% 1381
98% 1720
99% 1935
100% 2287 (longest request)
Уже неплохо, 310 запросов в секунду это в 1,26 раза больше чем HHVM в обычном режиме.
Поскольку изначально код не писался асинхронным — один запрос перед другим не выскочит, поэтому можно добавить обычный, не асинхронный режим, и допустить что запросы будут исполняться строго по очереди.
В таком случае мы можем обойтись обычными массивами в суперглобальных переменных, не нужно делать debug_backtrace() при создании системных объектов, а некоторые системные объекты вместо полного пересоздания можно частично переинициализировать и тоже сэкономить.
Вот какой результать это дает на пуле из 16 запущенных Http серверов (HHVM), Nginx балансирует запросы (прогрев 50 000 запросов на пул):
Benchmarking cscms.org (be patient)
Completed 500 requests
Completed 1000 requests
Completed 1500 requests
Completed 2000 requests
Completed 2500 requests
Completed 3000 requests
Completed 3500 requests
Completed 4000 requests
Completed 4500 requests
Completed 5000 requests
Finished 5000 requests
Server Software: nginx/1.6.2
Server Hostname: cscms.org
Server Port: 9990
Document Path: /uk
Document Length: 8497 bytes
Concurrency Level: 256
Time taken for tests: 5.716 seconds
Complete requests: 5000
Failed requests: 4983
(Connect: 0, Receive: 0, Length: 4983, Exceptions: 0)
Total transferred: 44046822 bytes
HTML transferred: 42381822 bytes
Requests per second: 874.69 [#/sec] (mean)
Time per request: 292.676 [ms] (mean)
Time per request: 1.143 [ms] (mean, across all concurrent requests)
Transfer rate: 7524.85 [Kbytes/sec] received
Connection Times (ms)
min mean[±sd] median max
Connect: 0 0 0.9 0 7
Processing: 6 284 215.9 241 976
Waiting: 6 284 215.9 241 976
Total: 6 284 215.8 241 976
Percentage of the requests served within a certain time (ms)
50% 241
66% 337
75% 409
80% 442
90% 623
95% 728
98% 829
99% 869
100% 976 (longest request)
875 запросов в секунду, это в 3.57 раза больше чем изначальный вариант с HHVM, что не может не радовать (иногда бывает на пару сотен больше запросов в секунду, бывает на пару сотен меньше, погода на десктопе бывает разная, но на момент написания статьи результаты таковы).
Так же есть перспективы для ещё большего увеличения производительности (например ожидается поддержка keep-alive и других вещей в ReactPHP), но тут уже многое зависит от проекта где это используется.
Так как мы сохраняем максимальную совместимость с любым существующим кодом — при асинхронном режиме при разных временных зонах пользователей нужно использовать их явно, иначе date() может вернуть неожиданный результат.
Так же пока не поддерживается загрузка файлов, но 2 pull request'а для поддержки multipart уже есть, в ближайшее время могут быть включены в react/http, тогда заработает и здесь.
Главный подводный камень в таком режиме — утечка памяти. Когда после выполнения 1000 запросов потребление памяти было одно, а после 5000 на пару мегабайт больше.
Советы по отлову утечек:
Второе — соединение с БД — оно может оторваться, будьте готовы его поднимать при падении. Это совершенно не актуально при популярном подходе, тут же может создать проблем.
Третье — ловите ошибки и не используйте exit()/die() если только вы не имеете ввиду именно это.
Четвертое — вам нужно каким-то образом отделять глобальное состояние разных запросов если собираетесь работать с асинхронным кодом, если асинхронного кода нет — глобальное состояние достаточно просто подделать, главное не используйте зависимые от запроса константы, статические переменные в функциях и подобные штуки, если только не хотите внезапно сделать гостя админом:)
С подобным подходом существенного роста производительности можно достичь либо без изменений, либо с минимальными (автоматический поиск и замена), а с Request/Response фреймворками это ещё проще сделать.
Прирост скорости зависит от интерпретатора и того, что код делает — при тяжелых вычислениях HHVM компилирует тяжелые участки в машинный код, при запросам ко внешним API можно использовать менее производительный асинхронный режим, но асинхронно грузить данные с внешнего API (если запрос к API занимает сотни милисекунд это даст существенный прирост в общей скорости обработки запросов).
Если есть желание попробовать — в CleverStyle CMS это и многое другое доступно из коробки и просто работает.
Исходников не много [8], при желании можно модифицировать и использовать в многих других системах.
Класс в Request.php принимает запрос от ReactPHP и отправляет ответ, functions.php содержит функции для работы с глобальным контекстом (в том числе несколько специфических для CleverStyle CMS), Superglobals_wrapper.php содержит класс, который используется для массиво-подобных суперглобальных объектов, Singleton.php — модифицированная версия трейта, который используется вместо системного для создания системных объектов (он же и определяет какие объекты общие для всех запросов, а какие нет).
Автор: nazarpc
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/cms/84512
Ссылки в тексте:
[1] PHP создан, чтобы умирать: http://habrahabr.ru/post/179399/
[2] ReactPHP: https://github.com/reactphp/react
[3] вот эта: http://marcjschmidt.de/blog/2014/02/08/php-high-performance.html
[4] cscms.org: http://cscms.org
[5] www.zeustech.net/: http://www.zeustech.net/
[6] www.apache.org/: http://www.apache.org/
[7] почему: http://hhvm.com/blog/1817/fastercgi-with-hhvm
[8] Исходников не много: https://github.com/nazar-pc/CleverStyle-CMS/tree/master/components/modules/Http_server
[9] Источник: http://habrahabr.ru/post/252013/
Нажмите здесь для печати.