Все ссылки на сайты и репозитории будут приложены в конце статьи, дабы не пестрить ссылками и сделать прочтение удобнее.
Не так давно, работая с пакетом Octane в Laravel, я открыл для себя интригующую альтернативу традиционным способам развертывания PHP‑приложений. Забудьте о классическом Apache или связке Nginx/PHP‑FPM — познакомьтесь с FrankenPHP, звучит пафосно, но он тоже не идеален, например сейчас в нём имеется проблема с поддержкой Fibers.
Разработчики позиционируют его как «The Modern PHP App Server, written in Go». Если копнуть глубже, FrankenPHP представляет собой элегантное сочетание Caddy и static‑php‑cli (или вашего системного php), где Caddy служит основой, а FrankenPHP выступает связующим модулем. В результате мы получаем единый исполняемый файл, объединяющий все преимущества Caddy с полноценным PHP‑сервером.
Модульность Caddy позволила вместе с PHP сервером интегрировать модуль Mercure HUB, который привносит поддержку SSE‑соединений (Server‑Sent Events) без негативного влияния на производительность вашего приложения. Так при попытке реализовать долгоживущее SSE‑соединение напрямую из PHP, например через Symfony\Component\HttpFoundation\StreamedResponse
, каждое соединение блокирует один поток выполнения. При открытии нескольких таких соединений ресурсы сервера быстро исчерпываются — напоминание о неасинхронной природе PHP.
После разработки значительной части проекта с использованием Symphony‑модуля для FrankenPHP, я столкнулся с ограничением: Mercure использует BoltDB в качестве хранилища данных. Как подтверждают сами разработчики: «Бесплатная версия Mercure.rocks Hub поставляется с транспортами (BoltDB и локальный), которые могут работать только на одном узле»*.
Таким образом, изначально присутствуют ограничения масштабируемости и неудобства отладки. И хотя данная статья не нацелена на сравнение различных key‑value баз данных, проблема требовала решения, так как меня интересовало именно Redis решение для возможности работы других сервисов с Mercure через Redis. Разработчики Mercure хоть и предлагают заплатить за расширенную версию, но также упоминают: «Если вы не хотите приобретать платную версию Mercure.rocks Hub, вы можете создать свою пользовательскую сборку Mercure.rocks»*.
Именно это и заставило заменить BoltDB на Redis. Самому решать эту проблему не хотелось, потому первая идея была зайти в Pull Requests на репо и посмотреть, а сделал ли кто-то уже это за меня ранее, как оказалось да, вот два запроса на слияние которые были отклонены так как предлагаемый функционал предусмотрен только в платной HA версии:
https://github.com/dunglas/mercure/pull/1026
https://github.com/dunglas/mercure/pull/181
запрос под номером 181 довольно старый, но даёт небольшой контекст плюс оттуда можно подсмотреть HA реализацию, а вот 1026 самое нужное. Клонируем себе репо и редактируем, из всех 25 файлов в PR смотрим только на:
caddy/mercure/main.go
caddy/redis.go
redis.go
redis_test.go
Просто добавляем данные файлы в проект, далее в caddy/redis.go
надо поправить структуру настройки (на момент написания статьи актуальная версия Mercure HUB 1.19.2):
type Redis struct {
Address string `json:"address,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
SubscribersSize int `json:"subscribers_size,omitempty"`
DispatcherPoolSize int `json:"dispatcher_pool_size,omitempty"`
RedisChannel string `json:"redis_channel,omitempty"`
transport *mercure.RedisTransport
transportKey string
}
Для справки: делаем поля публичными и сереализуемыми в JSON, соответственно надо переименовать эти же поля ниже (сделать название с заглавной буквы).
Компилируем проект и тестируем, для сборки выполняем:
cd mercure/caddy/mercure
go mod tidy
go build main.go
touch Caddyfile
И вносим такой конфиг в Caddyfile
:443 {
mercure {
transport redis {
address localhost:6379
# username
# password
subscribers_size 100000
dispatcher_pool_size 16
redis_channel channel
}
publisher_jwt !ChangeThisMercureHubJWTSecretKey!
subscriber_jwt !ChangeThisMercureHubJWTSecretKey!
anonymous
authorization_parameter authorization
cors_origins *
subscriptions
}
tls internal {
on_demand
}
}
Из конфигурации стоит выделить subscribers_size — максимальное кол‑во подписчиков, оно же длинна LRU кеша который где‑то там будет использоваться для хранения клиентов и dispatcher_pool_size определяет размер пула горутин которые обрабатывают сообщения для подписчиков, зная это вы сами сможете настроить систему под себя.
На данном этапе мы имеем собранный рабочий Mercure HUB работающий с Redis, но это всё ещё не единый PHP сервер, далее надо включить этот модуль в сборку FrankenPHP, клонируем его себе в директорию рядом с mercure
, это важно. Открываем frankenphp/caddy/go.mod
и добавляем такие строки после первого имеющегося там replace, тем самым мы укажем что модуль надо тянуть не с git, а он имеется уже по другому пути:
replace github.com/dunglas/mercure => ../../mercure
replace github.com/dunglas/mercure/caddy => ../../mercure/caddy
Возвращаемся в корневую директорию проекта frankenphp
и запускаем сборку, есть вариант как динамически подгружать PHP, так и статически. Для большего интереса я собрал статически. Запускаем сборку, обратите внимание на указание env переменных сборки, они описаны в самом файле, так например верно указав PHP_EXTENSIONS
и PHP_EXTENSION_LIBS
можно сократить время сборки и итоговый размер файла. Но перед этим немного его отредактируем:
if [ -z "${XCADDY_ARGS}" ]; then
XCADDY_ARGS="--with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy"
fi
# меняется на
if [ -z "${XCADDY_ARGS}" ]; then
XCADDY_ARGS="--with github.com/dunglas/caddy-cbrotli --with github.com/dunglas/vulcain/caddy"
fi
# ...
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS=${xcaddyGoBuildFlags} \
XCADDY_DEBUG="${XCADDY_DEBUG}" \
${XCADDY_COMMAND} build \
--output "../dist/${bin}" \
${XCADDY_ARGS} \
--with github.com/dunglas/frankenphp=.. \
--with github.com/dunglas/frankenphp/caddy=.
# меняется на
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS=${xcaddyGoBuildFlags} \
XCADDY_DEBUG="${XCADDY_DEBUG}" \
${XCADDY_COMMAND} build \
--output "../dist/${bin}" \
${XCADDY_ARGS} \
--with github.com/dunglas/frankenphp=.. \
--with github.com/dunglas/frankenphp/caddy=. \
--with github.com/dunglas/mercure=../../mercure \
--with github.com/dunglas/mercure/caddy=../../mercure/caddy
Так мы явно укажем xcaddy какие модули собирать и где их искать, теперь запустим:
Во время сборки могут быть ошибки...
Во время сборки могут быть ошибки о несовместимости версии CMake, для этого отредактируйте CMakeLists.txt в проблемных библиотеках в директории dist/static‑php‑cli/source/{lib}, в моём случае из‑за версии CMake 4+ поддержка сборок ниже версии 3.5 не поддерживается, замена версии на более высокую cmake_minimum_required(VERSION 3.5
в файле решила проблему.
В итоге мы получаем собственную статическую сборку PHP с нужными нам модулями и библиотеками, со встроенным Caddy, Mercure который пропатчили на поддержку Redis и научились включать свои модули в сборку Caddy, так например можно расширить свой сервер поискав их тут https://caddy.community/c/plugins/9 и собрать собственного PHP-Франкенштейна.
Данная статья создана для того чтоб показать то на сколько расширяем Caddy, что иногда следует полазить по PR в репо в поисках готового и нового, а так же рассказать о таком интересном проекте как FrankenPHP и о статической компиляции PHP движка.
Ссылки на ресурсы
https://github.com/caddyserver/caddy - Caddy репозиторий
https://github.com/dunglas/frankenphp - FrankenPHP репозиторий
https://github.com/dunglas/mercure - Mercure HUB репозиторий
https://github.com/crazywhalecc/static-php-cli - static-php-cli репозиторий
https://github.com/etcd-io/bbolt BoltDB репозиторий
https://symfony.com/doc/current/mercure.html - Symfony модуль для работы с Mercure