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 – бібліятэку encoding/binary.

Аднаго пратакола нам падалося мала - і мы дадалі магчымасць выклікаць 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 лямбда, стварэнне надзейных разборшчыкаў чэргаў і нават даданне gRPC у нашы прыкладанні.

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

Заключэнне

Некаторыя ўсё яшчэ знаходзяцца ў палоне састарэлага ўяўлення аб PHP як аб павольнай грувасткай мове, прыдатным толькі для напісання плагінаў пад WordPress. Гэтыя людзі нават могуць сказаць, што ў PHP ёсць такое абмежаванне: калі прыкладанне становіцца досыць вялікім, прыходзіцца выбіраць больш "сталую" мову і перапісваць назапашаную за шмат гадоў базу кода.

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

Папрацаваўшы са звязкам Go і PHP, мы можам сцвярджаць, што пакахалі іх. Мы не плануем ахвяраваць адным на карысць іншага - наадварот, будзем шукаць спосабы атрымаць яшчэ больш карысці з гэтага падвойнага стэка.

UPD: вітаем стваральніка RoadRunner і суаўтара арыгінальнага артыкула Lachezis

Крыніца: habr.com

Дадаць каментар