...

вторник, 3 марта 2015 г.

Атомный реактор в каждый сайт

Все слышали о том, что PHP создан, чтобы умирать. Так вот, это не совсем правда. Если захотеть — PHP может не умирать, работать асинхронно, и даже поддерживает честную многопоточность. Но не всё сразу, в этот раз поговорим о том, как сделать чтобы он жил долго, и поможет нам в этом атомный реактор!





Атомный реактор — это проект ReactPHP, в описании указано «Nuclear Reactor written in PHP». На знакомство с ним меня подтолкнула вот эта статья (картинка выше оттуда). Я перечитывал её несколько раз на протяжении года, но никак не получалось добраться до имплементации на практике, хотя рост производительности более чем на порядок в перспективе очень радовал.


Исходное состояние




В качестве подопытной системы выступает 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 воркеров, статически):


Скрытый текст
nazar-pc@nazar-pc ~> ab -n5000 -c128 cscms.org:8080/uk

This is ApacheBench, Version 2.3

Copyright 1996 Adam Twiss, Zeus Technology Ltd, www.zeustech.net/

Licensed to The Apache Software Foundation, www.apache.org/

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 запросов (почему), потом выполним тест:


Скрытый текст
nazar-pc@nazar-pc ~> ab -n5000 -c256 cscms.org:8000/uk

This is ApacheBench, Version 2.3

Copyright 1996 Adam Twiss, Zeus Technology Ltd, www.zeustech.net/

Licensed to The Apache Software Foundation, www.apache.org/

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 наполнялись вручную.

Системные классы разрушались после каждого запроса и создавались при новом.

В целом работало, но были нюансы:


  • В случае использования асинхонных операций где больше одного запроса будут выполняться одновременно всё накроется медным тазом

  • Создание всех ситемных объектов всё ещё создавало существенные накладные расходы, хотя это и работало быстрее чем полный перезапуск скрипта

  • Не запускалось из-под PHP-CLI, для отправки заголовков нужен PHP-CGI, у которого течет память (по неведомой причине) при долгоиграющем процессе

  • Если кто-то решил вызвать exit()/die() — всё умирает




Оптимизации, поддержка асинхронности




Во-первых системные объекты были разделены на две группы — первая, запросы которые зависят от пользователя и конкретного запроса, вторая — полностью независимые.

Независимые объекты перестали разрушаться после каждого запроса что дало существенный прирост скорости.

Объект, который принимает запрос от 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 запросов на пул):


Скрытый текст
nazar-pc@nazar-pc ~> ab -n5000 -c256 cscms.org:9990/uk

This is ApacheBench, Version 2.3

Copyright 1996 Adam Twiss, Zeus Technology Ltd, www.zeustech.net/

Licensed to The Apache Software Foundation, www.apache.org/

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 запросов на пул):

Скрытый текст
nazar-pc@nazar-pc ~> ab -n5000 -c256 cscms.org:9990/uk

This is ApacheBench, Version 2.3

Copyright 1996 Adam Twiss, Zeus Technology Ltd, www.zeustech.net/

Licensed to The Apache Software Foundation, www.apache.org/

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 на пару мегабайт больше.

Советы по отлову утечек:


  • Обрезать объем выполняемого кода до минимума, запустить 5000 запросов, логируя объем памяти после каждого выполнения, сравнить потребление

  • Добавить немного выполняемого кода, повторить

  • Продолжать до проверки всего кода, количество запросов можно опускать постепенно до 2000 (для того чтобы не ждать долго), но в случае когда есть сомнения — накинуть ещё несколько тысяч запросов будет не лишним

  • Несколько запросов может потребоваться для стабилизации потребления памяти, сначала до 100 запросов, иногда при запуске полной системы бывало до 800 запросов на стабилизацию потребления памяти, после этого объем потребляемой памяти перестает расти.

  • Так как ситуация не очень мейнстримная, может случиться так, что память течет не в вашем коде, а в сторонней библиотеке, либо вообще расширении PHP (PHP-CGI как пример) — тут можно пожелать удачи и не забывать про супервизор над сервером:)




Второе — соединение с БД — оно может оторваться, будьте готовы его поднимать при падении. Это совершенно не актуально при популярном подходе, тут же может создать проблем.

Третье — ловите ошибки и не используйте exit()/die() если только вы не имеете ввиду именно это.

Четвертое — вам нужно каким-то образом отделять глобальное состояние разных запросов если собираетесь работать с асинхронным кодом, если асинхронного кода нет — глобальное состояние достаточно просто подделать, главное не используйте зависимые от запроса константы, статические переменные в функциях и подобные штуки, если только не хотите внезапно сделать гостя админом:)

Заключение




С подобным подходом существенного роста производительности можно достичь либо без изменений, либо с минимальными (автоматический поиск и замена), а с Request/Response фреймворками это ещё проще сделать.

Прирост скорости зависит от интерпретатора и того, что код делает — при тяжелых вычислениях HHVM компилирует тяжелые участки в машинный код, при запросам ко внешним API можно использовать менее производительный асинхронный режим, но асинхронно грузить данные с внешнего API (если запрос к API занимает сотни милисекунд это даст существенный прирост в общей скорости обработки запросов).

Если есть желание попробовать — в CleverStyle CMS это и многое другое доступно из коробки и просто работает.

Исходники




Исходников не много, при желании можно модифицировать и использовать в многих других системах.

Класс в Request.php принимает запрос от ReactPHP и отправляет ответ, functions.php содержит функции для работы с глобальным контекстом (в том числе несколько специфических для CleverStyle CMS), Superglobals_wrapper.php содержит класс, который используется для массиво-подобных суперглобальных объектов, Singleton.php — модифицированная версия трейта, который используется вместо системного для создания системных объектов (он же и определяет какие объекты общие для всех запросов, а какие нет).

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий