В рамках iForum 2017 года сооснователь и СТО сервиса для планирования путешествий TripMyDreamТарас Полищук рассказал, как и для чего они с коллегами перевели codebase проекта с PHP на Hack. Для читателей DOU, которые не присутствовали на конференции, Тарас изложил этот опыт в авторской статье.
В 2015 году мы запустили beta-версию TripMyDream — сервиса для поиска путешествий без агентств. Суть продукта — подбор выгодных комбинаций «перелет плюс отель» под бюджет пользователя. На первый взгляд задача не выглядит как rocket science, но на деле реализовать ее очень сложно. В этом нам помог язык Hacklang. Но прежде чем рассказывать о нем, объясню, какие задачи мы решали и почему искали именно такой язык.
Предыстория: какие задачи решал TripMyDream
Итак, мы начали тестировать идею и бизнес-модель, запустили beta-тестирование продукта. Много трафика привлекать не планировали, но сервис заинтересовал СМИ, и после ряда публикаций посетители стали заходить на сайт активным потоком. Посещаемость была такой, что каждый день в пиковые часы нагрузка на 8 серверах держалась на уровне 500%. Чтобы понять причины перегрузок, нужно немного углубиться в логику работы сервиса.
Представим условного пользователя, который зашел на сайт, ввел свои требования и нажал кнопку «Найти». В то время задача одного такого поиска сводилась к асинхронному опросу целого набора внешних API, которые предоставляли информацию о наличии авиабилетов, об отелях в нужном регионе, об актуальных ценах. Параллельно шло комбинирование полученных данных с нашей собственной базой прогнозов цен, в которой уже тогда было около миллиарда записей. Все это нужно было совместить и выдать пользователю в виде списка комбинаций «перелет плюс отель», подходящих под его условия. Отдельным вызовом была скорость: мы поставили цель отдавать результат настолько быстро, насколько ответит первая цепочка API-запросов.
В такой ситуации на наши серверы приходило очень много трафика и ресурсы быстро заканчивались. PHP-процессы просто висели в ожидании ответов внешних API, а количество таких процессов является ограниченным.
Почему не справился PHP
Сразу скажу, что с помощью PHP можно решить множество задач, но конкретно в нашем случае это было сложно. Разберу по пунктам, что именно не устраивало в PHP.
Отсутствие нативной асинхронности
Вы можете создать ее искусственно, можете прибегнуть к помощи экстеншена pthreads, однако проблему перерасхода серверных ресурсов это не решит. Например, pthreads, создавая новый поток, делает форк самого процесса. В нем создается отдельный поток. При этом вместе с форком клонируются уже используемые процессом серверные ресурсы, а именно — память. То есть формально появляется два потока, и фактически они «съедают» ресурсов как два полноценных процесса.
Для асинхронизации могут быть использованы воркеры, работающие с очередью задач: например, Gearman или RabbitMq. Такой подход существенно улучшает ситуацию, но при этом вы все равно ограничены в количестве потоков. Каждый воркер скоро будет «съедать» по 30 MB оперативной памяти, и даже если взять сервер с 32 GB оперативной памяти, этих ресурсов хватит на сотню воркеров. Увеличивая объем памяти, проблему решить не получится: вы упретесь в процессорное время. Получается, что для выполнения длительных задач на PHP на одном сервере возникает ограничение в 100 параллельных потоков, если говорить про долгие потоки, блокирующие I/O.
Чрезмерный расход памяти
PHP потребляет очень много памяти, это не будет открытием для PHP-разработчиков. Причина в том, что PHP — слабо типизированный язык, который интерпретируется при каждом выполнении. И если второе решается с помощью кэша, то с типизацией все сложнее. При всем желании и качественной разметке кода невозможно достигнуть такого потребления памяти, которое есть у строго типизированных языков.
Когда дело доходит до огромных коллекций данных, которые в PHP можно использовать только с помощью массивов, серверные ресурсы начинают растворяться.
Поиск альтернативы: ответы, которые мы искали
Мы пришли к выводу, что на PHP не получится решить поставленную задачу. Пришлось искать другое решение. При этом нужно было учитывать два важных обстоятельства. Первое: мы — стартап, а значит, не можем потратить пару месяцев на смену стэка. Каждый день — драгоценный. Второе: у нас была команда PHP-разработчиков, и решить задачу предстояло им. Я много общаюсь с нашими партнерами, коллегами, конкурентами, и знаю, что большинство решает подобные задачи либо с помощью node.js, либо с помощью Java. Но для перехода на node.js нам потребовалась бы новая команда из nodejs-девелоперов, т. к. в реальности PHP и node.js — это две параллельные вселенные.
Итак, мы начали искать решение, которое предполагало строгую типизацию, аккуратную работу с памятью и асинхронность, которая не расходовала бы так много ресурсов. У нас была команда из PHP-девелоперов, и она должна была быстро мигрировать на новый язык. При этом PHP7 еще не вышел в свет. Таким решением стал Hacklang.
Знакомство с Hack
Hack, он же Hacklang — это полноценный язык программирования, разработанный компанией Facebook. Начиналось все с трансляторов кода, когда лет пять назад Facebook представил HipHop Virtual Machine (HHVM), а ВКонтакте — транслятор Kitten PHP (kPHP). Спустя два года Facebook зарелизил полноценный язык программирования, разработанный под виртуальную машину HHVM, полностью совместимый с PHP.
Декларируемые цели языка — быстрая разработка без багов, быстрый runtime, масштабируемость, положительный опыт в написании кода. Утверждение о «быстрой разработке без багов» кажется фантастическим, но на деле оно очень даже похоже на правду. Hack предусматривает статический анализ кода, который и помогает избежать 99% ошибок еще до runtime. Я подробнее расскажу о нем позже.
Синтаксис Hack очень похож на PHP. Можно сказать, что Hacklang — это как PHP с дополнительным синтаксическим сахаром и новыми возможностями.
Пять преимуществ в пользу Hacklang
Помимо уже заявленных свойств, Hacklang имеет и другие преимущества, которые оказались крайне важными для нас. Перечислю их.
Типизация
Это была одна из ключевых причин, почему мы мигрировали на Hacklang. Hack подразумевает сильную типизацию по умолчанию, при которой разработчик обязан указывать типы декларируемых переменных, аргументов и результатов выполнения функций.
Примитивные типы полностью пересекаются с PHP, за исключением типа void, который позволяет в коде просто написать return с точкой с запятой. При этом в Hack есть другие типы: mixed, nullable, arraykey, num и noreturn. Внимательные PHP-девелоперы могут возразить, что в PHP всегда был тип mixed, но на самом деле это псевдо-тип, используемый для описания языка.
Акцент в Hack ставится на разработку сильно типизированных и масштабируемых систем, в то время как PHP акцентируется на скорости разработки.
Коллекции
Помимо дополнительных типов, в Hacklang есть нативные коллекции. В большинстве случаев они являются универсальным решением всех проблем. У каждой коллекции имеется свой полноценный интерфейс и набор методов для удобной работы с данными, сортировки, фильтрации и конвертации в другие типы или коллекции.
Map — типичный key-value контейнер, ключ которого может быть строкой или целым числом, а значение — любым.
Set — контейнер уникальных строковых или числовых значений. Например, используя метод add, можно быть уверенным, что в контейнере лежит единственный экземпляр значения.
Vector — самый типичный стэк. По сути, это массив, ключ которого является строго числовым и автоинкрементым.
Pair — контейнер, который содержит строго два значения любых типов.
Дженерики
Те, кто разрабатывает на Java, хорошо знакомы с дженериками — параметризацией вызываемых классов и функций. Тип или несколько типов, которые будут использоваться внутри методов или как тип возвращаемого результата, передаются вместе с объявлением класса или вызова метода. На практике это значит, что вам не нужно наследовать и перегружать методы, если вы хотите работать с несколькими разными типами в рамках одной логики. В Hacklang есть полная поддержка ковариативности и контравариативности.
Встроенный тайпчекер
Одна из самых интересных частей — встроенный тайпчекер, который проводит статический анализ кода еще до его выполнения. Собственно, здесь возвращаемся к заявленной возможности писать быстрый код без багов.
Hack поддерживает три режима. Partial modeпозволяет совмещать codebase на PHP и Hack одновременно. Strict mode — это режим строгой типизации всего кода. В этом режиме тайпчекер проверяет абсолютно все файлы проекта и говорит об ошибках. Даже если в одном из сотни файлов будет ошибка, то весь проект в runtime выполняться не будет — идентично со строго типизированными языками. Мы принципиально пишем весь код именно в этом режиме. И, наконец, режим declarative modeполностью отключает тайпчекер.
JIT, just-in-time компиляция
Опять же, те, кто разрабатывает на Java, понимают преимущества JIT. Это компилятор, транслирующий написанный девелопером код в инструкции машинного кода — с рядом последующих оптимизаций. Когда дело доходит до пиковых нагрузок или сложных машинных расчетов, именно JIT обеспечит приложению быструю работу.
Реализация асинхронности в Hacklang
Первой ключевой причиной, по которой мы выбрали Hack, была типизация. Вторая — реализация асинхронности. На ней стоит остановиться подробнее, поэтому я вынес этот вопрос в отдельный раздел.
Следует понимать, что это не многопоточная асинхронность, а однопоточная, так называемая однопоточная многозадачность. Работая в едином потоке, Hack позволяет запускать ряд задач параллельно. Для этой цели служит встроенный планировщик. Кроме того, между этими параллельными задачами можно переключаться. Используется парадигма async/await, которая позволяет декларировать функции асинхронными (с помощью async) и получать указатель на их выполнение (с помощью await). Разберемся, как это работает.
Существует ряд неблокирующих компонентов, встроенных в язык: AsyncMysql, Memcached, cUrl и потоки. Под определением «неблокирующие» имеется в виду время ожидания результата, время простоя, которое может быть использовано для выполнения других задач.
На практике это выглядит так. Когда с помощью cURL вы опрашиваете какой-то URL, то ждете ответа, допустим, 10 секунд. Или же отправляете запрос в MySQL — и ждете выборки 2 секунды. Так вот, Hack позволяет не ждать, а в это время идти дальше по потоку — к другим асинхронным задачам. То есть, эти задачи можно запустить вместе и результат получить вместе с последней функцией. Не нужно ждать завершения работы одной функции, чтобы начать другую.
Для примера приведу задачу, которую легко решить на Javascript — с помощью обычных коллбеков — и достаточно сложно решить на PHP. Она нестандартная, но хорошо демонстрирует возможности Hacklang. Итак, допустим, необходимо каждую секунду отправлять параллельно ровно три API-запроса. Представим, что мы платим деньги за неограниченный доступ, но имеем всего три запроса в секунду. Четыре запроса в секунду отправить нельзя: это блокирует запросы следующей секунды. При получении ответа результат необходимо обрабатывать и сохранять. Время ответа API — от 0,5 до 5 секунд. И все должно работать в едином потоке одного процесса.
Как же это решается на Hack? На минуту вперед создается массив со слотами для async-функций каждого запроса — и запускается одновременно. На каждую секунду мы имеем по 3 функции. Итого, 180 слотов для выполнения функций. Каждая async-функция запускает асинхронный cURL со своим заданным callback. В каждой функции есть проверка, настало ли ее время выполнения. Когда планировщик попадает в async-функцию, время которой еще не пришло, он ставит её выполнение в конец очереди. Плюс небольшой слип, чтобы не перегружать процессор лишними циклами.
Дополнительные возможности для разработчика на Hack
Hacklang имеет немало дополнительных возможностей. Я опишу в общих чертах инструменты и «фишки», которые нахожу полезными, а примеры их использования можно найти, к примеру, в документации HHVMили в моей презентации для iForum
Тип shape
Shape — это специальный тип, объединяющий 0 и больше полей как единое целое. Полная валидация тайпчекером происходит еще до runtime. По сути, тип shape дает возможность не создавать отдельный класс под микромодель данных.
Мемоизация
Над функцией ставится мета поле Memoize, и виртуальная машина кэширует ответ функции с конкретными заданными аргументами. При последующем обращении она выдаст ответ без выполнения функции. Это мелочь, которая позволяет не строить велосипеды там, где это будет избыточно.
Constructor parameter promotion
Объявление свойств класса можно реализовать с помощью аргументов в конструкторе. Достаточно добавить к аргументам конструктора их видимость (private, protected, public).
Операторы Lambda, Null-safe, Pipe
Оператор Lambda ( ==>) для лямбда-функций. Помимо синтаксического сахара, он дает возможность использовать текущий scope в лямбда функциях без использования use.
Оператор Null-safe (?->) позволяет избежать дополнительных проверок на null-ность объекта, предоставляя возможность безопасного доступа к методам.
Оператор Pipe (|>) — синтаксический сахар, позволяющий избежать избыточного вложения вызовов функций.
Режим Repo Authoritative
Стандартно HHVM ведет себя так же, как PHP-движок: загружает и компилирует ход на лету. Режим Repo Authoritative позволяет сразу скомпилировать весь код в бинарный репозиторий, что сильно ускоряет работу на пиковых нагрузках.
Типы серверов FastCGI и Proxygen
В HHVM можно использовать два типа серверов: FastCGI и Proxygen. В первом случае мы видим стандартное использование, схожее с nginx + php-fastcgi. HHVM запускается независимо от web-серверов (apache, nginx и других). Во втором варианте виртуальная машина HHVM имеет встроенный полноценный web-сервер, который позволяет принимать запросы напрямую, исключая промежуточный web-сервер (например, nginx).
Встроенный шаблонизатор
В Hack разработан полноценный шаблонизатор XHP, который предоставляет нативную
IDE, фреймворк и другие «организационные моменты»
Когда мы начинали писать на Hack, еще не существовало полноценного IDE. Мы использовали phpStorm и допиливали в нем распознование Hack как PHP. Сейчас все стало гораздо проще: для знакомого многим компонентного IDE Atom существует компонент Nuclide, созданный Facebook. Он применяется для разработки на ряде языков, включая Hack. Безграничные возможности самого Atom превращают разработку на Hack в приятное времяпрепровождение.
Сейчас в числе слабых сторон можно назвать то, что в Hack отсутствует менеджер расширений и возможность устанавливать внешние экстеншены как таковые. При этом можно добавлять любые PHP-библиотеки через composer или совершать полуавтоматическую конвертацию в Hack-код. Собственно, мы так поступали с клиентами для Google Adwords, Mailchimp, Mandrill. Еще есть вариант скомпилировать свой код на C++ вместе с виртуальной машиной HHVM.
Еще один недостаток — отсутствие полноценного Framework. Если нужно написать плюс-минус стандартизированное
Экосистема Hacklang не такая огромная, как у PHP, но она постоянно развивается. Еще два года назад документация была очень слабая, по многим пунктам просто перекидывала на документацию PHP. Сейчас подобных проблем нет. Можно рассчитывать на полноценный релиз каждые 8 недель, ночные сборки, хороший фидбек. Например, если вы нашли какой-то issue и запостили его в GitHub-репозитории Hack, ответ от одного из девелоперов, скорее всего, появится в течение суток. Если найти что-то стоящее и предложить коммит — его с радостью примут.
Полезные ресурсы: активный GitHub, 280 вопросов на Stackoverflow, официальная документация.
Что касается установки, для Ubuntu, Debian есть установка из готовых пакетов и возможность скомпилировать исходники с GIT. Для OSX — установка через brew и, опять же, возможность скомпилировать исходники с GIT. В случае с OSX бывают сложности установки (если делать это нативно без Docker), однако они длятся недолго, релизы с исправлениям выходят с постоянной частотой. Для других Linux-системы тоже возможно скомпилировать исходники. Существуют образы в Docker Hub, которые можно использовать для контейнеризации HHVM. Для Windows установка пакета пока невозможна, в разработке портирование с помощью MSVC.
Напоследок скажу о миграции. Если вы задумались о переходе на чистый Hack (без использования legacy php кода), то, опираясь на опыт TripMyDream, вам скорее всего придется писать код с нуля. Также для Hack существуют инструменты автомиграции кода из PHP в Hack, однако, используя их, вы можете не ощутить всех преимуществ разработки на чистом Hack. Для разработчиков процесс миграции происходит довольно легко. Уже через несколько недель PHP Developer свободно пишет на Hack, а совсем скоро он проектирует и пишет код полноценно, с измененной парадигмой.