在 Debian 12 上从源码编译 FrankenPHP

为什么是 FrankenPHP

FrankenPHP 是构建在 Caddy 之上的 PHP 应用服务器。它和传统 PHP 部署最大的不同,是把 PHP 解释器直接嵌进了 Caddy 进程里:本质上,FrankenPHP 是通过 cgo 在 Go 进程内跑 PHP 的。

这带来的直接好处是 worker 模式。常规的 PHP-FPM 每个请求都要重新走一遍启动、加载、销毁的生命周期;worker 模式让应用只引导一次,然后常驻内存反复处理请求,省掉了每次请求的冷启动开销。对 Symfony、Laravel 这类框架尤其明显——框架的 bootstrap 本身就不轻。除此之外它还顺带提供了 Early Hints、自动 HTTPS、HTTP/2 和 HTTP/3。

就快慢而言,目测是比 FastCGI / PHP-FPM 快的。它也可以当作一个独立的 Go 库,把 PHP 嵌到任何用 net/http 的程序里。

我们选择自己编译,而不是用现成二进制,是因为要按自己的需要裁剪 PHP 扩展——下面会看到我们专门把 SNMP 之类的扩展编进去,目的是让 Cacti / Zabbix / LibreNMS 这类网络监控服务也能跑在这套运行时上。

编译前准备

0. 安装依赖

apt-get install libbrotli-dev \
                libsnmp-dev \
                libxml2-dev \
                libsqlite-dev \
                libsnmp-dev \
                libxml2-dev \
                libsqlite3-dev \
                libzstd-dev \
                liblz4-dev \
                libjpeg-dev \
                libpng-dev \
                libpcre3-dev \
                libmcrypt-dev \
                libgd-dev \
                libbz2-dev \
                libonig-dev \
                libpq-dev \
                libreadline-dev \
                libsodium-dev

1. 编译 PHP

./configure --prefix=/usr --libdir=/usr/local/lib --enable-embed=static --enable-static  --enable-zts --disable-shared --disable-cgi --disable-fpm --disable-phpdbg  --with-config-file-path=/etc/frankenphp --with-config-file-scan-dir=/etc/frankenphp/php.d --enable-zend-max-execution-timers --with-snmp --enable-bcmath --enable-mbstring --enable-ctype --enable-dom --enable-session --enable-simplexml --enable-sockets --enable-tokenizer --enable-xml --enable-xmlreader --enable-xmlwriter --enable-soap --enable-opcache --enable-pdo --with-pdo-mysql --with-pdo-pgsql --with-pgsql --with-zip --with-curl --with-zlib --with-readline --with-iconv --with-libxml --with-openssl --with-bz2 --enable-gd --with-jpeg --with-webp --with-freetype --with-sodium --enable-sysvsem --enable-sysvshm --enable-sysvmsg --enable-pcntl --enable-shmop --enable-phar
make -j $(nproc)
make install

这条 configure 的几个开关是 FrankenPHP 能跑起来的前提,不是可选项:

  • --enable-embed=static --enable-static --disable-shared:FrankenPHP 要把 PHP 作为静态库嵌进 Caddy,所以必须编成静态嵌入模式,而不是普通的动态库。
  • --enable-zts:worker 模式是多线程的,PHP 必须编成线程安全(ZTS)版本,否则无从谈起。
  • --enable-zend-max-execution-timers:ZTS 下用它来实现 max_execution_time,这是官方推荐配合 ZTS 的做法。
  • --disable-cgi --disable-fpm --disable-phpdbg:我们不需要 CGI、FPM、phpdbg 这些 SAPI,运行时只有嵌入这一种,关掉省事也省体积。

其余 --with-* / --enable-* 就是按业务挑的扩展。这里我们特意编进了 --with-snmp,是因为我们希望 Cacti / Zabbix / LibreNMS 这类网络监控服务也能通过这套运行时来跑。你可以按自己的需要增删。

2. 设置环境变量

PHP 装好后,用 xcaddy 构建 FrankenPHP 之前,要把编译器和链接器指向刚装好的 PHP:

export PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/local/lib/pkgconfig
export LIBRARY_PATH=/usr/local/lib
export C_INCLUDE_PATH=/usr/include/php:/usr/include/php/main:/usr/include/php/TSRM:/usr/include/php/Zend:/usr/include/php/ext
export LD_LIBRARY_PATH=/usr/local/lib
export PHP_CONFIG=/usr/bin/php-config
export CGO_ENABLED=1
export XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx,nowatcher"
export CGO_CFLAGS=$(php-config --includes)
export CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)"

几点说明:

  • CGO_ENABLED=1 是整件事的开关——FrankenPHP 通过 cgo 调用 PHP,关掉它就无从链接。
  • CGO_CFLAGS / CGO_LDFLAGS 直接取自 php-config,保证 Go 侧用的头文件和链接参数,和上一步编出来的那个 PHP 完全一致。版本或路径对不上,链接阶段就会报错。
  • C_INCLUDE_PATH 把 PHP 的几个核心头目录(main / TSRM / Zend / ext)都显式列出来,cgo 编译时才找得到。
  • XCADDY_GO_BUILD_FLAGS 里的 -w -s 去掉调试符号减小体积;nobadger,nomysql,nopgx,nowatcher 这几个 build tag 关掉用不到的 Caddy 组件。

小结

把 FrankenPHP 编出来的关键,全在"PHP 要编成什么样"这一步:静态嵌入(--enable-embed=static)加线程安全(--enable-zts),缺一不可。环境变量那一层只是把 xcaddy 的 cgo 构建对准这个 PHP——用 php-config 取 CGO_CFLAGS / CGO_LDFLAGS,就不会出现头文件和库版本错位的问题。扩展按业务裁剪,比如我们编进 SNMP 是为了跑监控类应用。

Published At (Updated At) Tagged with