RoadRunner: PHP не створено, щоб вмирати, або Golang поспішає на допомогу

RoadRunner: PHP не створено, щоб вмирати, або Golang поспішає на допомогу

Привіт, Хабре! Ми в Badoo активно працюємо над продуктивністю PHPОскільки у нас досить велика система цією мовою і питання продуктивності — це питання економії грошей. Більше десяти років тому ми створили для цього PHP-FPM, який спочатку був набором патчів для PHP, а пізніше увійшов в офіційне постачання.

За останні роки PHP сильно просунувся вперед: покращився збирач сміття, підвищився рівень стабільності — сьогодні на PHP можна без особливих проблем писати демони і довготривалі скрипти. Це дозволило Spiral Scout піти далі: RoadRunner, на відміну від PHP-FPM, не очищає пам'ять між запитами, що дає додатковий виграш у продуктивності (хоча цей підхід ускладнює процес розробки). Ми зараз експериментуємо з цим інструментом, але ми поки що не маємо результатів, якими можна було б поділитися. Щоб чекати на них було веселіше, публікуємо переклад анонсу RoadRunner від Spiral Scout.

Підхід зі статті нам близький: при вирішенні своїх завдань ми теж найчастіше використовуємо зв'язку PHP та Go, отримуючи переваги від обох мов і не відмовляючись від однієї на користь іншої.

Насолоджуйтесь!

В останні десять років ми створювали програми і для компаній зі списку Фортуна 500, та для бізнесу з аудиторією не більше 500 користувачів. Весь цей час наші інженери розробляли бекенд переважно на PHP. Але два роки тому дещо сильно вплинуло не тільки на продуктивність наших продуктів, а й на їх масштабованість — ми ввели Golang (Go) у наші технології.

Майже відразу ми виявили, що Go дозволяє нам створювати більші програми із збільшенням продуктивності до 40 разів. За допомогою нього ми змогли розширювати існуючі продукти, написані на PHP, покращуючи їх завдяки поєднанню переваг обох мов.

Ми розповімо, як зв'язка Go і PHP допомагає вирішувати реальні завдання розробки і як вона перетворилася для нас на інструмент, здатний позбавити частини проблем, пов'язаних з моделлю «вмирання» PHP.

Ваше повсякденне середовище PHP-розробки

Перш ніж розповідати, як за допомогою Go можна оживити модель «вмирання» PHP, розглянемо ваше стандартне середовище PHP-розробки.

У більшості випадків ви запускаєте програму за допомогою комбінації веб-сервера nginx та сервера PHP-FPM. Перший обслуговує статичні файли та перенаправляє до PHP-FPM специфічні запити, а сам PHP-FPM виконує PHP-код. Можливо, ви використовуєте менш популярну зв'язку з Apache та mod_php. Але хоча вона працює трохи інакше, принципи самі.

Розглянемо, як PHP-FPM виконує код програми. Коли надходить запит, PHP-FPM ініціалізує дочірній PHP-процес, а деталі запиту передає як частину його стану (_GET, _POST, _SERVER тощо).

Стан не може змінюватися під час виконання PHP-скрипту, тому отримати новий набір вхідних даних можна лише одним способом: очистивши пам'ять процесу та ініціалізувавши його заново.

Така модель виконання має багато переваг. Вам не потрібно сильно турбуватися про споживання пам'яті, всі процеси повністю ізольовані, і якщо один із них «вмирає», то він буде автоматично відтворений і це ніяк не торкнеться інших процесів. Але є такий підхід і недоліки, які виявляються при спробі масштабувати додаток.

Недоліки та неефективність звичайного PHP-середовища

Якщо ви займаєтеся професійною розробкою на PHP, то знаєте, з чого потрібно починати новий проект — з вибору фреймворку. Він являє собою бібліотеки для впровадження залежностей, ORM'и, переклади та шаблони. І, звичайно ж, всі вхідні дані користувача можна зручно помістити в один об'єкт (Symfony/HttpFoundation або PSR-7). Фреймворки - це кльово!

Але все має свою ціну. У будь-якому фреймворку ентерпрайз-рівня для обробки простого запиту користувача або звернення до БД доведеться завантажувати як мінімум десятки файлів, створювати численні класи і парсить кілька конфігурацій. Але найгірше те, що після виконання кожного завдання потрібно буде все скинути і почати заново: весь щойно ініційований вами код стає марним, з його допомогою ви вже не обробите ще один запит. Скажіть про це будь-якому програмісту, який пише якоюсь іншою мовою, і ви побачите здивування на його обличчі.

PHP-інженери роками шукали способи вирішення цієї проблеми, використовували продумані методики «лінивого» завантаження, мікрофреймворки, оптимізовані бібліотеки, кеш і т. д. Але зрештою все одно доводиться скидати все додаток і починати спочатку, знову і знову. (Примітка перекладача: частково цю проблему буде вирішено з появою попереднє навантаження у PHP 7.4)

Чи може PHP за допомогою Go пережити більше одного запиту?

Можна написати PHP-скрипти, які проживуть довше кількох хвилин (аж до годин або днів): наприклад, cron-завдання, CSV-парсери, розбирачі черг. Усі вони працюють за одним сценарієм: витягують завдання, виконують його, чекають наступне. Код постійно знаходиться в пам'яті, заощаджуючи дорогоцінні мілісекунди, оскільки для завантаження фреймворку та програми потрібно виконувати безліч додаткових дій.

Але розробляти довгоживучі скрипти не так просто. Будь-яка помилка повністю вбиває процес, діагностика витоків пам'яті доводить до сказу, а використовувати налагодження F5 вже не можна.

Ситуація покращилася з виходом PHP 7: з'явився надійний збирач сміття, полегшало обробляти помилки, а розширення ядра тепер захищені від витоків. Правда, інженерам все ще потрібно обережно поводитися з пам'яттю і пам'ятати про проблеми стану в коді (а чи існує мова, в якій можна не приділяти увагу цим речам?). І все ж у PHP 7 нас чатує менше несподіванок.

Чи можна взяти модель роботи з довгоживучими PHP-скриптами, адаптувати її під більш тривіальні завдання на кшталт обробки HTTP-запитів і тим самим позбавитися необхідності завантажувати все з нуля при кожному запиті?

Для вирішення цього завдання спочатку потрібно було реалізувати серверну програму, здатну приймати HTTP-запити і перенаправляти їх один за одним PHP-воркеру, не вбиваючи його щоразу.

Ми знали, що зможемо написати веб-сервер на чистому PHP (PHP-PM) або за допомогою С-розширення (Swoole). І хоча кожен спосіб має свої переваги, обидва варіанти нас не влаштовували — хотілося чогось більшого. Потрібен був не просто веб-сервер — ми розраховували отримати рішення, здатне позбавити нас проблем, пов'язаних з «важким стартом» у PHP, яке при цьому можна легко адаптувати та розширювати під конкретні програми. Тобто нам потрібен був сервер додатків.

Чи може Go допомогти у цьому? Ми знали, що може, тому що ця мова компілює додатки до одиночних бінарних файлів; він кросплатформовий; використовує власну, дуже елегантну модель паралельної обробки (concurrency) і бібліотеку для роботи з HTTP; і, нарешті, нам будуть доступні тисячі open-source-бібліотек та інтеграцій.

Проблеми поєднання двох мов програмування

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

Наприклад, за допомогою чудової бібліотеки Алекса Палаестраса можна було реалізувати спільне використання пам'яті процесами PHP та Go (аналогічно mod_php в Apache). Але ця бібліотека має особливості, що обмежують її застосування для вирішення нашого завдання.

Ми вирішили використати інший, більш поширений підхід: побудувати взаємодію між процесами через сокети/конвеєри. Цей підхід за останні десятиліття довів свою надійність та був добре оптимізований на рівні операційної системи.

Для початку ми створили нескладний бінарний протокол для обміну даними між процесами та обробки помилок передачі. У своїй простій формі протокол цього типу схожий netstring с заголовком пакета фіксованого розміру (У нашому випадку 17 байт), який містить інформацію про тип пакета, його розмір та двійкову маску для перевірки цілісності даних.

На стороні PHP ми використовували функцію pack, а на стороні Go - бібліотеку кодування/двійковий.

Одного протоколу нам здалося мало - і ми додали можливість викликати Go-сервіси net/rpc прямо з PHP. Пізніше нам це дуже допомогло у розробці, оскільки ми могли легко інтегрувати Go-бібліотеки до PHP-додатків. Результат цієї роботи можна побачити, наприклад, в іншому нашому open-source-продукті Goridge.

Розподіл завдань за кількома PHP-воркерами

Після реалізації механізму взаємодії ми почали думати, як найефективніше передавати завдання PHP-процесам. Коли приходить завдання, сервер додатків повинен вибрати для виконання вільний воркер. Якщо воркер/процес завершив роботу з помилкою або «помер», ми позбавляємося його і створюємо новий натомість. А якщо воркер/процес відпрацював успішно, ми повертаємо його до пулу воркерів, доступних для виконання завдань.

RoadRunner: PHP не створено, щоб вмирати, або Golang поспішає на допомогу

Для зберігання пулу активних воркерів ми використовували буферизований каналДля видалення з пулу несподівано «померлих» воркерів додали механізм відстеження помилок і станів воркерів.

В результаті ми отримали робочий PHP-сервер, здатний опрацьовувати будь-які запити, подані в бінарному вигляді.

Щоб наша програма почала працювати як веб-сервер, довелося вибрати надійний PHP-стандарт для представлення будь-яких вхідних HTTP-запитів. У нашому випадку ми просто перетворюємо net/http-запит з Go у формат PSR-7, щоб він був сумісний з більшістю доступних сьогодні PHP-фреймворків.

Оскільки PSR-7 вважається незмінним (хтось скаже, що технічно це не так), розробникам доводиться писати додатки, які в принципі не звертаються із запитом як із глобальною сутністю. Це чудово поєднується з концепцією довготривалих PHP-процесів. Наша фінальна реалізація, яка ще не отримала назви, мала такий вигляд:

RoadRunner: PHP не створено, щоб вмирати, або Golang поспішає на допомогу

Представляємо RoadRunner - високопродуктивний сервер PHP-додатків

Нашим першим тестовим завданням став API-бекенд, на якому періодично непередбачено виникали сплески запитів (набагато частіше, ніж звичайно). Хоча в більшості випадків можливостей nginx було достатньо, ми регулярно стикалися з помилкою 502, тому що не могли досить швидко балансувати систему під очікуване збільшення навантаження.

Для заміни цього рішення на початку 2018 року ми розгорнули наш перший PHP/Go сервер додатків. І одразу отримали неймовірний ефект! Ми не тільки повністю позбулися помилки 502, але ще й змогли на дві третини зменшити кількість серверів, заощадивши купу грошей та таблеток від головного болю для інженерів та менеджерів продуктів.

До середини року ми вдосконалили наше рішення, опублікували його на GitHub під ліцензією MIT та назвали RoadRunner, підкресливши тим самим його неймовірну швидкість та ефективність.

Як RoadRunner може покращити ваш стек розробки

Застосування RoadRunner дозволило нам використовувати Middleware net/http на стороні Go, щоб проводити JWT-верифікацію ще до того, як запит потрапляє до PHP, а також щоб обробляти WebSockets та глобально агрегувати стани в Prometheus.

Завдяки вбудованому RPC можна відкривати API будь-яких Go-бібліотек для PHP без написання екстеншенів-оберток. Що ще важливіше, за допомогою RoadRunner можна розгортати нові сервери, що відрізняються від HTTP. Як приклади можна навести запуск в PHP обробників AWS Lambda, створення надійних розбирачів черг і навіть додавання gRPC у наші програми.

За допомогою спільнот PHP і Go ми підвищили стабільність рішення, в деяких тестах збільшили продуктивність додатків до 40 разів, удосконалили інструменти налагодження, реалізували інтеграцію з фреймворком Symfony та додали підтримку HTTPS, HTTP/2, плагінів та PSR-17.

Висновок

Деякі все ще знаходяться в полоні застарілого уявлення про PHP як про повільну громіздку мову, придатну тільки для написання плагінів під WordPress. Ці люди навіть можуть сказати, що у PHP є таке обмеження: коли додаток стає досить великим, доводиться вибирати більш «зрілу» мову і переписувати базу коду, що накопичилася за багато років.

На це хочеться відповісти: подумайте ще раз. Ми вважаємо, що тільки ви самі задаєте якісь обмеження для PHP. Ви можете витратити все життя на переходи з однієї мови в іншу, намагаючись знайти ідеальне поєднання з вашими потребами, або можете почати сприймати мови як інструменти. Уявні недоліки мови типу PHP насправді можуть бути причинами його успіху. А якщо поєднати його з іншою мовою на кшталт Go, то ви створите набагато потужніші продукти, ніж якби ви обмежилися використанням якоїсь однієї мови.

Попрацювавши зі зв'язкою Go та PHP, ми можемо стверджувати, що полюбили їх. Ми не плануємо жертвувати одним на користь іншого — навпаки, шукатимемо способів отримати ще більше користі з цього подвійного стека.

UPD: вітаємо творця RoadRunner та співавтора оригінальної статті Lachezis

Джерело: habr.com

Додати коментар або відгук