Pull to refresh

Как мы наш большой проект на KPHP мигрировали

Reading time 12 min
Views 5.1K

История о том, как мы мигрировали нашу систему управления проектами на KPHP. Если у вас есть PHP-проект с длинной историей и вы хотите запуститься на KPHP для получения выгод, то приготовьтесь! Будет сложно, больно, сборка будет падать много раз. И если у вас останутся силы подняться вместе со сборкой, вы победите.

KPHP?! Зачем, а главное зачем?

KPHP — компилятор PHP-кода, разработанный в VK. Он переводит PHP в С++ и собирает бинарник, работающий на скорости С++. Главный недостаток — “обычный PHP код” он не возьмёт, код должен быть написан по принципам типизации. А также там из коробки нет postgres и sqlite — а нам нужно. В общем, когда его выложили в опен сорс пару лет назад, было не ясно, можно ли на нём запустить что-то кроме самого ВК. Но исходники самого ВК не выложили :)

Так зачем?
Во-первых, скорость. Да, обычно в веб-приложениях нагрузка приходится на базу данных, а прослойка между БД и API очень тонкая. Но у нас реально много вычислений и работы на графах в бизнес-логике, поэтому хотелось попробовать, а ускоримся ли.

Но главное — во-вторых. Уже много лет мы работаем в облаке, такой SaaS сервис. А заказчики просят коробочную версию (а недавно сильно просят, импортозамещение там, вот это всё). Но как поставить “коробку” из PHP-кода? Он очень слабо защищаем. Механизмы типа Ioncube и ZendEncoder если и живые ещё, то снимаются быстро через декодирование байткода. А KPHP выглядит идеальным вариантом: на выходе настоящий исполняемый модуль, то есть мы не поставляем исходники, хотя вшиваем лицензию внутрь.

Итак, есть огромный рабочий PHP-проект. Хотим скорость + коробку. Цель ясна, давайте пробовать.

Что было на старте

Наш продукт — это Система управления проектами. Это примерно как если взять MS Project Server + MS Project Pro + Atlassian Jira + Power BI, и к этому ещё куча интеграций с другими системами управления задачами. С инженерной точки зрения это куча алгоритмов на графах, выборок из БД и расчёта статистики.

  • У системы очень длинная история, и где-то в ядре был ещё код, совместимый с PHP3/PHP4, фреймворки не использовались.

  • Мы уже завершили миграцию c XSLT на React/JSL, поэтому не требовалась поддержка XSLT от KPHP.

  • Свой собственный ORM для работы с Sqlite, PostgreSQL.

  • Используется много рефлексии для работы с БД: чтение, запись и создание полей, обращения по динамическим именам.

  • Много интерфейсов и паттернов Команда, Стратегия. А также мест, где использовались особенности PHP для типовой обработки в формате “этот объект похож на тот” без объединения классов одним интерфейсом.

  • Динамические объявления полей. “В этом режиме мы опубликуем данные в этом поле, а в другом режиме не будем”.

  • Типизация если и была, то очень местами. Много использований stdClass.

И вот с таким наследством мы попытались взлететь. Мы ожидали, что будет больно, но не представляли, насколько! Но у нас было самое главное! 600 модульных тестов и 1600 приёмочных, плюс в команде были те, кто знают и PHP и C++/C# (что очень пригодилось, когда падал с ошибкой уже С++ компилятор).

Забегая вперёд: результаты

Полгода!!! Да, на это ушло куча времени. Естественно, мы не всё время занимались чисто переездом — но настрадаться и нырнуть с головой пришлось более чем полностью.
Зато вот что мы получили в итоге:

  1. Расчёт расписания стал быстрее в 3 раза.

  2. Коробочная версия: пакет поставки это один исполняемый модуль, который является сервером обработки логики запросов и сам отдаёт статику (скрипты, стили, картинки), что ограничивает вмешательство в клиентскую бизнес-логику приложения.

  3. Появилась лицензионная защита.

  4. Есть понятная модель горизонтального масштабирования, предлагаемая KPHP.

Ради этих результатов стоило полгода заниматься рефакторингом всей системы.

Технические параметры результата:

  • 8000 PHP-файлов исходников, превращаются в 22 тысячи KPHP-целей.

  • Результирующий бинарник 150 МБ, и 50 в установочном пакете (хорошо жмётся %).

  • Туда упакована вся статика с ресурсами (1300 файлов) — это обеспечивает хороший кеш-контроль, быструю отдачу из памяти и защиту от изменений.

  • Пересборка с нуля на одном ПК идёт 2 часа. Долго, но с это с нуля, а при разработке пересобирается только дифф, это быстро. Если захочется ускорить, попробуем параллелить через nocc.

  • Сайт по-прежнему работает и на обычном PHP тоже.

Через тернии к продакшену — с этим столкнутся все, кто захочет переехать на KPHP

Все возникшие сложности делятся на несколько категорий:

  • PHP-код изначально писался в свободном стиле, используя принципиально некомпилируемые возможности, поэтому KPHP на него ругается, и ругается по делу. Такой код нужно переписывать.

  • В KPHP нет нативной поддержки sqlite / postgres, а с PHP-шными extension’ами он не совместим. Но даже это получилось!

  • В KPHP нет некоторых стандартных PHP-шных функций, и каждый раз изобрести замену это творческая задача.

  • Иногда что-то идёт не так — например, ошибка указывает вникуда, или падает g++, или есть отличие в поведении от PHP. И пойди разбери.

  • Отсутствие коммюнити, не у кого спросить, некуда подсмотреть, много вещей не отражено в документации. Надо развивать, ребята! Инструмент реально крутой.

Ниже расскажу про каждый из этих пунктов.

Сложность 1: код не рассчитан на строгость

Любой PHP-проект пользуется PHP-преимуществами, многие из которых в строгих языках вообще недопустимы. Например, вычленив из url вида /view?act=schedule переменную $act , вызвать метод ViewController::$act(). В KPHP так нельзя. Или рефлексия, или геттеры-сеттеры по именам полей, типа $obj->$field = $val, это тоже запрещено, хотя у нас использовалось сплошь и рядом, особенно в ORM и парсинге инпута.
Такие вещи решаются кодогенерацией (при этом для PHP-режима допустимо оставлять старое поведение).

Написали свой генератор рефлексии. Если точнее, то не рефлексии, а геттеров-сеттеров по именам полей с кастовками типов. По факту, это такой switch по строкам с ветками вида case 'user_id': $this->user_id = (int)$val. Сделали кодогенерацию на основании метаданных по sql-структуре. После небольших подпорок заработал ORM-маппинг из входных объектов в классы и обратно. Потом часть работы по установке/чтению значений полей перенесли в сам ORM с числовой адресацией вместо строк.

Избавились от динамического объявления свойств. Раньше часто использовалось $obj->unexisting_prop = $val, и свойство появлялось (использовалось с запросами с join’ами и т.п.). Даже при наличии геттеров-сеттеров такие свойства всё равно некуда записывать. В PHP это просто работало, потому что все собранные динамически свойства класса в конечном итоге сериализовались клиенту. Долго ловили подобные “магические” появления свойств, чтобы их все объявить. Зато теперь ясно видна структура, никакой магии.

Всегда нужно думать о типах. Хорошо, что наши инженеры свободно плавают в С++, и мы понимаем важность типизации. Но всё равно в PHP/JS делаем по-простому, запихиваем разные типы в одно и то же свойство, передаём разные классы в функцию и разгребаем через instanceof и duck typing, сгоняем числа+строки+объекты в одну хеш-таблицу. IDE часто не понимает, что имеется в виду, но работает же. KPHP опять же не позволяет такие вольности. Нельзя вызвать f(new A) и f(new B), если A и B не имеют общего предка или интерфейса. Типа object, как в джаве, также нет. Нельзя сделать return ['count' => 0, 'user' => new User], потому что это массив с несовместимыми элементами. А что можно? Можно описывать классы с типизированными полями и делать иерархию, чтобы были общие предки где нужно. Можно создавать массивы/хешмапы с совместимыми элементами. Можно использовать кортежи (tuples) и именованные кортежи (shapes). Часто вместо обычного callable нужны типизированные callable — просто “callable”, как и просто “object” (да и даже просто “array”!) не является валидным типом. В общем, типизация — это правильный путь, но весьма чуждый динамическим языкам, и уж тем более все эти принципы раньше не соблюдались в нашем коде. Кстати, тут помогает KPHPStorm, в IDE хоть часть ошибок видно на этапе написания.

Типизировать существующий код сложно. Это продолжение прошлого пункта :) Тут была боль и страдания, потому что часть классов использовалась в формате “Этот класс похож на тот, мы переиспользуем этот фрагмент кода”. Также часть полей просто конвертировалась в другой тип при импорте/экспорте без создания новых полей. Или поля int внезапно использовались как string просто потому что удобно было добавить строковый суффикс к ID элемента. На PHP это просто работало, но когда начинаешь проставлять типы, то осознаёшь, что оно работало скорее случайно. Типы это хорошо. Типы, контролируемые компилятором, это вдвойне хорошо. Но больно. Никакого any как в TypeScript, всё строго.

Типизировать существующий код долго. На самом деле это заняло больше половины времени, без этого в компиляции вообще не продвинуться, оно падает. Ну как падает? Просто KPHP выдаёт 30 тысяч строк ошибок. Ты их исправляешь и... переходишь на следующий уровень, там тебе дают ещё 20 тысяч строк ошибок.
Это сильно напоминало игру и строки из старой песни:

Тут бы мне отстать, отдохнуть, остыть,
Но я вошел в игру, как на грех.
Я опять по крохам копил хиты
И по уровням лез наверх.

И когда все стрелы попали в цель,
И последний конверт открыт,
Тут вошел Господь и нажал (^C),
И я вылетел из игры.
(полный текст
https://forum.vingrad.ru/topic-35758.html)

Дженерики. Поскольку эвристику “этот класс похож на тот” использовать нельзя, передавать разные классы в функции нельзя, а писать обобщённые алгоритмы хочется, то мы пользовались шаблонами (дженериками, generics, в С++ template<T>) в KPHP. Например, чтобы бегать по массивам array<T> произвольных классов. Или для ORM, где идёт приведение к интерфейсу, а потом использование результата “как обычного класса”, чтобы избежать копипасты, да ещё паттерны “Фабричный метод” и “Прототип” на каждом углу применяются. Поэтому при переходе в строго-типизированный код и потребовалась шаблонизация. Изначально шаблоны в KPHP были примитивные, но коллеги из VK за них очень плотно взялись, и мы несколько месяцев сидели в отдельной ветке с дженериками, фактически тестируя их на нашем коде %) Без дженериков мы бы умерли. Это хорошо, что “папа может в Си” (с), точнее в С++/C# и шаблоны, и мы точно знаем, что хотим получить и как это должно работать. Скоро в KPHP обещают завести дженерик-классы, и тогда вообще будет красота.

Сложность 2: нет поддержки БД

Мы используем SQLite и PostgreSQL, а в KPHP поддерживается только MySQL из коробки. Почему? Да потому что ВК другое и не нужно, у них свои движки, правда закрытые %)
Казалось бы, безнадёжно? Изначально казалось, что да. И именно поэтому провалилась наша первая попытка переезда. Но год назад в KPHP появился FFI (причём полностью PHP-совместимый), и это стало уже возможным.

Изначальной реализации FFI не хватило, и ребята из команды VK по нашей просьбе поддержали FFI-массивы и raw-указатели, удалось подключить SQLite. Адаптер к PostgreSQL зашёл уже проще. Мы его тоже сделали через FFI напрямую в библиотеку libpq. KPHP не всегда может читать заголовочные файлы “как есть”, поэтому пришлось адаптировать хедеры, описать все константы в PHP коде и убрать все неиспользуемые функции, чтобы не было лишних эффектов.

Как-нибудь выложим наши FFI-обёртки над базами. Хотя конечно лучше, если в KPHP они будут нативными, а ещё лучше, если будет пул коннектов, чтобы не устанавливать соединение каждый раз.

Сложность 3: в KPHP много чего не хватает

Не только баз данных, как оказалось. Часть PHP-функций из стандартной библиотеки отсутствуют — потому что внутри VK они не используются. Некоторые PHP-возможности тоже недоступны, потому что их не реализовали ещё.

Нехватка методов для работы с XML. В KPHP нет модуля simplexml. Не беда, подумали мы, и решили вызывать сторонние либы через popen() и shell_exec(). Нооо… В KPHP их тоже не оказалось %) Потому что в продакшене VK они не используются, а в опенсорсе никому не требовалось, вот и не оказалось. В общем, мы залезли в код рантайма KPHP и добавили нужные нам функции, а через них вывели конверторы XML в утилиты. Так делать не стоит никому и никогда, скоро в KPHP их добавят нативно, и будем собираться из мастера. Но на крайний случай такой вариант тоже рабочий.

Нехватка методов для работы с ZIP. Для ZIP удалось найти на PhpClasses подходящую библиотеку, которая применяла методы gzcompress. Конечно, пришлось пошаманить с типами, подправить местами код, но это сработало.

Нехватка принимающих TCP-сокетов. Эту проблему мы пока не решили, но она на перспективу нам нужна. Надеемся, разработчики реализуют socket stream API.

Генератор (компилятор) файлов ресурсов. В некоторых языках и платформах типа Qt или C# есть компилятор ресурсов, который упакует что нужно внутрь бинарника. Это удобно, потому что меньше файлов таскаешь — меньше ошибок при развёртывании. Для KPHP мы сделали такой компилятор. Но оказалось, что когда реестр файлов загружаешь в обычный PHP-FPM, то он просто умирает. (Ещё бы он не умирал, там 360 мегабайт кода, со строками в hex-кодировке). Поэтому пришлось добавить ветвление по режиму исполнения. Зато теперь в поставке никакой статики, она вшита внутрь.

Десериализатор из json в класс. Казалось бы простая вещь, взять JSON и разобрать его в класс но… KPHP так не умел на тот момент. Типовых решений (да ещё и совместимых с KPHP) найти не удалось, поэтому написали свой. Если у нас есть рефлексия для классов, значит нужно просто понять, что за поле во входном объекте, и создать объект нужного класса.

UPD: в KPHP уже появился нативный JSON со всякими аннотациями, надо посмотреть, можно ли дропнуть наш.

На самом деле, это всё решаемо. Да, некоторой функциональности нет — но она пишется “в стороне” чаще всего без проблем. К тому же, постепенно в рантайм KPHP вносят фичи, и с течением времени нехватки будет всё меньше и меньше. Но если очень хочется, то при знаниях C++ можно влезть "своими грязными руками" в рантайм KPHP и добавить нужные функции.

Сложность 4: иногда что-то идёт не так

Когда код уже компилируется и работает — вопросов нет. А когда код ещё в процессе переделки, то иногда компилятор вылетает, а иногда вообще что-то работает по-другому нежели в PHP.

KPHP не всегда видит классы. Например, есть интерфейс и 2 имплементирующих его класса — но эти классы нигде не создаются и вообще недостижимы по коду, просто лежат как файлики. KPHP их не увидит, он анализирует только достижимый код, а в недостижимом может быть вообще написана всякая чушь. Иногда делали if(0) new MyClass, чтобы KPHP его увидел, пока он ещё не достижим из других мест.

Иногда падает на g++. Уже в конце, когда проект почти собрался, несколько раз падал плюсовый компилятор. KPHP не находил некоторые странные ошибки в типизации или получал невалидный кодген. Да, причина в некорректном PHP-коде изначально, но нужно падать на компиляции, а не на g++. Часть из этих моментов уже пофикшена.

Некоторые баги в самом KPHP. Приведу парочку для примера. Долго не могли выкатиться, потому что ошибки в запросах к базе приводили к аварийному завершению обработчика запроса и незакрытию соединения, в результате чего следующие запросы тоже переставали обрабатываться. Оказалось, дело в том, что коллбеки из register_shutdown_function() не вызывались при таймауте и исключениях, а в коллбеках шла подчистка FFI-ресурсов, они утекали. Потом команда VK исправила это, и мы выкатились в прод. Ещё были какие-то фантомные баги с перегрузкой методов при сложной иерархии, когда почему-то вызывался метод родителя вместо виртуального метода самого класса. Построить минимальный репродьюсер не получилось, на любых тесткейсах работало корректно, но у нас в коде случались странности. В итоге решили избавлением от виртуальности в некоторых местах, так и осталось загадкой.

Отличия в поведении от PHP. Обычно, если KPHP скомпилировал код, он работает так же, как и PHP-шный. Почти всегда. Но есть несколько нюансов. Например, если есть int[] массив, то по несуществующему индексу $arr[100500] вернётся 0, а не null. Ещё пример: json_decode() в PHP вовзращает либо хеш-таблицу, либо stdClass. В KPHP нет stdClass, там всегда хеш-таблица. А т.к. у нас в коде просто были расставлены метки “считаем, что результат такого класса” — тут у нас умерло всё. Как только меняешь тип аргумента на массив, то все передачи этого параметра в функции становятся… в один конец. Ведь когда результат был stdClass, то он был объектом, а объекты передавались по ссылке, а для массивов это нужно явно указывать. И пришлось переписывать все адресации к полям с $decoded->prop на $decoded['prop'] и проверять "А как это раньше работало?".

Компилируется, но не работает. Когда мы перевели половину кода и получили первую сборку, у нас упали… модульные тесты. Примерно половина. И почти все приёмочные. Для каждого теста нужно было найти причины. А это или аргументы не принимались серверным обработчиком, или что-то не учли в рефлексии, или ошиблись в типах (компилируется, но не совпадает с базой), или мы отдавали клиенту то, от чего он ужасался и падал с ошибкой. Постепенно, недель за шесть удалось получить 100% срабатывание всех тестов.

Сложность 5: отсутствие сообщества

KPHP крут, очень! Но не у кого спросить и некуда подглядеть. Нам очень повезло, что разработчики VK были на связи и консультировали нас при непонятках, а также снабжали отдельными ветками и дженериками. Думаю, в итоге получился очень клёвый опыт для всех сторон.

Не хватает документации. Да, она есть, но давно не обновлялась. Например, о механизмах параллелизации процессов и отдельной шаренной памяти мы узнали только в личной беседе. Разработчики обещали дополнить доку и внести туда все обновления за прошедшие пару лет.

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

Почему не Go/Rust/Java/XXX

Предчувствую вопросы %)

А если серёзно, то народ, вы серьёзно?! Если вы думаете, что смена языка программирования у больших систем в продакшене это то, чем нужно заниматься — значит, вы не работали над большими системами в продакшене. Мы на KPHP переезжали почти полгода, а на любой другой язык не переехали бы и за 5 лет. Потому что это единомоментное переписывание с нуля — и главное, переписывание не только самой системы, но и тестов. В нашем же случае все модульные и приёмочные тесты остались без изменений. А в них значительно проще сделать ошибку, ведь тесты не покрыты тестами.

Смена языка только добавляет проблем, но не решает их. Смена языка — это цель ради смены языка. Так не работает, это не нужно. У нас была цель другая. Цель в скорости и коробочной версии — а для этого подошёл KPHP, и мы отделались малой кровью.

Хоть миграция и была сложна, но оставался несомненный плюс: в любой момент времени это был обычный PHP-код, который продолжал работать. Сделали немного — проверили. Сделали ещё — проверили. В конечном счёте пришли к цели. Эволюция, вот что нас спасло. А любая смена языка этой эволюции не даёт.

PHP прекрасно выполняет свои задачи, чаще всего большего и не нужно. А теперь есть и ещё скорость. Если вы считаете, что язык, на котором написан продукт, это главное в продукте — задайтесь этим вопросом лет через 20, с горой опыта и мёртвых языков за плечами %)

Выводы

Это как вешаться на бесконечно длинной верёвке в надежде, что в её конце обязательно будет петля :)

Но всё получилось! Мы проделали большую работу по миграции. Команда KPHP из VK проделали большую работу по добавлению дженериков, FFI, информации о точке вызова и многое другое.

И самое главное, цели такой миграции были достигнуты — сайт ускорился, и мы поставляем исполняемый модуль. Теперь мы сможем отгружать коробочные версии не только на 500+ рабочих мест, но и на 10-20 (и даже на демо-стенды), что даёт возможность понятной дистрибуции. Ибо SaaS у нас в стране в сегменте выше среднего бизнеса как-то не очень любят и предпочитают коробочные решения в закрытый контур.

Многие вещи, которых нам не хватало, уже добавлены в KPHP или будут добавлены в ближайшем будущем. Язык и платформа постепенно развиваются — и теперь на нём работает не только ВК.

Если получилось у нас, получится и у других. Но не пускайтесь в такие авантюры без приёмочных тестов! И без инженеров, инфицированных С++ и STL %)

Tags:
Hubs:
+45
Comments 11
Comments Comments 11

Articles