拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

麒麟V10上源码编译安装PHP与配置PHP-FPM实战指南

麒麟V10上源码编译安装PHP与配置PHP-FPM实战指南

上个月接了一台跑在麒麟 V10 (Sword) 上的业务服务器,需要把 PHP 从无到有部署起来。本来以为照着 CentOS 那套思路走就完事,结果真踩进去才发现,Kylin Linux Advanced Server V10 这系统虽然跟 RHEL 系长得像,但坑全藏在细节里。这篇文章就记录我在这套系统上从环境准备到编译装完 PHP,再到配好 PHP-FPM 和 Nginx 的完整过程,重点是每一步背后的理由和实际操作中遇到的那些坑。

1. 为什么非要在麒麟V10上源码编译PHP

1.1 麒麟V10的系统特殊性

Kylin Linux Advanced Server V10 (Sword) 是国内服务器领域很常见的一套操作系统,基于 Linux 内核,整体兼容 RHEL/CentOS 的包管理方式和编译习惯。我第一次拿到系统时先执行了cat /etc/os-release,能看到发行版信息,再执行uname -m确认架构,这套操作跟 CentOS 上几乎一模一样。

但它的软件仓库策略跟 CentOS 有明显区别。直接用yum install php装出来的版本往往比较保守,而且官方源里的 PHP 扩展不太全,某些自研模块(比如自带的加密组件、特定的数据库驱动)跟业务代码的兼容性也没法保证。我这次遇到的场景更直接:业务方要求 PHP 8.1 以上,并且要启用pcntl、swoole这类扩展,仓库里的版本满足不了,那就只能走源码编译这条路。

1.2 源码编译vs二进制包:选择理由和代价

很多人一听到编译安装就觉得麻烦,其实选择源码编译通常就三个原因:版本定制、扩展定制、目录定制。仓库里的 RPM 包虽然装起来快,但版本被锁死,扩展开哪些不开哪些也是别人帮你定好的;源码编译则可以从头指定 PHP 版本、指定安装前缀、按需启用或禁用扩展,甚至可以针对特定的 CPU 指令集做优化。

代价就是编译过程需要一定耐心,依赖库要自己装,configure、make、make install 这套流程跑下来,顺利的话半小时,不顺利的话一下午。我这次实际编译了三次才把扩展选型确认下来,后面我会一个个说明原因。对于一台生产服务器来说,多花点时间在构建上,换来的是后续运行期的稳定和可控。

注意:如果业务方对 PHP 版本没有要求,系统仓库自带的 PHP 其实完全够用。只有在需要特定版本或特定扩展时才值得源码编译,这个决策要先想清楚。

2. 编译前的系统摸底与依赖准备

2.1 检查系统版本和编译工具链

编译 PHP 的第一步不是下载源码,而是摸清系统底子。我先确认了这几项:

  • 系统版本:cat /etc/os-release
  • 架构:uname -m(麒麟 V10 有 x86_64 和 aarch64 两种常见架构,编译参数基本一致,但某些扩展在 aarch64 下需要额外配置)
  • 现有 gcc 版本:gcc --version
  • 是否已有基础编译工具链:which make、which cmake、which autoconf

麒麟 V10 默认安装的 gcc 版本通常够用,但如果系统是精简安装版,可能需要先补上基础开发工具。我在第一次编译时就是因为系统没装make和gcc,configure 阶段直接卡住,后来统一安装了一组编译工具集。

yum install -y gcc gcc-c++ make automake autoconf

建议装完之后顺手验证一下编译链是否正常,写个简单 C 程序跑一遍,确认 gcc 能正确输出可执行文件。这一步能规避一大批后续编译报错,别嫌麻烦,真出了问题再排查要费更多时间。

2.2 一次性装齐编译依赖库

PHP 编译时不光需要编译工具,还需要一堆开发库。这些库的作用是为 PHP 提供各种基础能力的底层接口,比如访问 MySQL 需要libmysqlclient的头文件、处理 XML 需要libxml2的头文件、做 HTTPS 请求需要 OpenSSL 的开发库。缺了这些头文件,configure 阶段就会报configure: error: xml2-config not found或类似错误。

以下是我在麒麟 V10 上实际执行过的依赖安装命令,覆盖了最常见的扩展需求:

yum install -y libxml2-devel openssl-devel curl-devel libjpeg-devel \ libpng-devel freetype-devel bzip2-devel libzip-devel oniguruma-devel \ readline-devel sqlite-devel libicu-devel openldap-devel

这里特别要提两个容易漏的库:

  • oniguruma-devel:如果 PHP 要启用正则相关的多字节支持(mbstring扩展依赖它),缺少这个库会导致 configure 报错。我第一次编译时漏了这个,后来补上重跑 configure 才顺利通过。
  • libzip-devel:PHP 8.0 之后zip扩展是必不可少的,如果不装这个开发库,--with-zip会失败。

还有一个需要注意的点:麒麟 V10 的默认 yum 源里可能没有全部包,如果执行 yum install 时提示No package,需要先启用 EPEL 源或者补充麒麟自己的 DVD 源。epel-release通常可以直接yum install epel-release安装。

提示:从 PHP 7.4 开始,phpize编译扩展时需要pcre2开发库。如果后续要装 PECL 扩展,建议提前yum install -y pcre2-devel,否则phpize会报找不到pcre2.h。

2.3 确认扩展库的底层依赖是否存在

这一步很多人忽略,但恰恰是最容易埋雷的。libxml2-devel装了,不代表libxml2本体就能正常工作;openssl-devel装了,不代表 PHP 就能成功启用 OpenSSL 扩展。在 configure 阶段,PHP 会通过 pkg-config 或小型的探测程序检查这些库是否可用。

常用的检查指令:

pkg-config --exists libxml-2.0 && echo "libxml ok" pkg-config --exists openssl && echo "openssl ok" pkg-config --exists libcurl && echo "curl ok"

如果 pkg-config 报错,说明对应的-devel包没有正确安装,或者安装路径不在 pkg-config 搜索范围。麒麟 V10 上我遇到过 pkg-config 路径不包含/usr/local/lib/pkgconfig的情况,可以设置环境变量解决:

export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/usr/lib/pkgconfig:$PKG_CONFIG_PATH

另外,在 ARM64 (aarch64) 架构下,某些库的安装路径是/usr/lib64而不是/usr/lib,PHP 的 configure 脚本默认搜索路径可能漏掉。此时可以在 configure 时手动指定--with-libdir=lib64,这个参数在 x86_64 上一般不用管,但在麒麟 V10 的 ARM 版上非常重要。

3. configure配置阶段:参数怎么选、为什么这么选

3.1 核心配置参数拆解

依赖准备好之后,下载 PHP 源码就是常规操作。我选择的是 PHP 8.1.27 稳定版,这个版本在性能和生态兼容性之间比较均衡。下载解压之后,进入源码目录,开始 configure。

configure 是编译安装的核心环节,它的作用有点类似“量体裁衣”——根据你传入的参数检查系统能力、决定哪些功能启用、哪些扩展禁用,最终生成 Makefile。参数选得好,后面 make 阶段基本一次过;参数选得差,编译到一半发现缺库,返工成本极高。

以下是我这次使用的完整 configure 命令,并对关键参数做拆解说明:

./configure \ --prefix=/usr/local/php \ --with-config-file-path=/usr/local/php/etc \ --with-config-file-scan-dir=/usr/local/php/etc/php.d \ --enable-fpm \ --with-fpm-user=www \ --with-fpm-group=www \ --enable-cli \ --enable-mbstring \ --enable-zip \ --enable-pcntl \ --enable-sockets \ --enable-opcache \ --enable-bcmath \ --with-mysqli=mysqlnd \ --with-pdo-mysql=mysqlnd \ --with-openssl \ --with-curl \ --with-zlib \ --with-bz2 \ --with-gettext \ --with-iconv \ --with-libdir=lib64

几个关键选择:

  • --prefix=/usr/local/php:指定 PHP 根目录,便于后续维护。个人习惯把自编译软件统一放/usr/local下,升级和删除都方便,不会污染系统目录。
  • --with-config-file-path=/usr/local/php/etc:指定php.ini的存放位置。这个参数在预编译二进制包里经常被忽略,导致后面php --ini显示的加载路径不是预期路径。
  • --enable-fpm和--with-fpm-user/group:启用 PHP-FPM 进程管理器,并指定运行用户。这里先创建好 www 用户,避免后面手动调整权限。
  • --with-mysqli=mysqlnd和--with-pdo-mysql=mysqlnd:采用 mysqlnd(MySQL Native Driver),这是 PHP 官方推荐的驱动,不需要额外安装 MySQL 客户端库,性能也更好。
  • --enable-pcntl:进程控制扩展,很多常驻内存的 PHP 脚本(比如队列消费者、异步任务处理)依赖这个扩展。默认不启用,需要显式声明。
  • --with-libdir=lib64:在 64 位系统下告诉 configure 去lib64目录查找依赖库,这也是我在麒麟 V10 上实际踩过的坑,缺了它会导致找不到libxml2.so等库。

3.2 扩展启用的策略

PHP 的扩展从编译期就可以分为静态编译和动态加载两种。静态编译的扩展直接编进 PHP 二进制本体,性能好、部署简单;动态加载的扩展以.so文件形式存在,通过php.ini里的extension=xxx.so加载,灵活性高。

我个人的策略是:核心扩展(如 opcache、mbstring、pcntl、sockets)静态编译进 PHP,避免 .so 版本与 PHP 主版本不匹配的问题;非核心的(如 redis、swoole)用 PECL 动态安装,方便后续升级。

configure 时还有一类参数值得注意——--disable-*。默认 PHP 会启用一些你也许用不到的扩展,比如--disable-fileinfo可以加快编译速度,--disable-ipv6在某些纯内网环境可以省掉部分依赖。每少一个扩展,make 的时间就能缩短一点,同时也减少潜在的攻击面。但业务要用的功能一定要保留,比如我这次保留的--enable-filter和默认启用的 JSON 支持。

3.3 configure常见失败点

configure 失败是编译安装里最常见的拦截关口,报错信息通常直接告诉你缺了什么库。我整理了三个高概率报错及解决方式:

报错一:

configure: error: xml2-config not found. Please check your libxml2 installation.

原因:libxml2-devel 未安装或其路径不在搜索范围内。解决:yum install -y libxml2-devel,必要时设置LIBXML_CFLAGS和LIBXML_LIBS。

报错二:

configure: error: Please reinstall the libcurl distribution - easy.h should be in <curl-dir>/include/curl/

原因:curl-devel 缺失或版本过旧。解决:yum install -y curl-devel,如果已经安装,检查是否存在/usr/include/curl/easy.h。

报错三:

configure: error: Package requirements (oniguruma) were not met

原因:oniguruma-devel 缺失。解决:安装后重新 configure,注意不要漏掉--enable-mbstring对它的依赖。

还有一个通用排查手段:configure 出错的日志尾部通常会给出 pkg-config 的检测结果,执行pkg-config --list-all | grep <库名>可快速定位是哪个包出了问题。

4. make编译与安装验证

4.1 多核编译与资源控制

configure 顺利通过之后,就进入make阶段。这一步的本质是调用 gcc 将 PHP 源码编译成可执行文件,耗时取决于机器核数和配置的扩展数量。我习惯用make -j$(nproc)让编译进程数与 CPU 核心数匹配,能显著提速。

不过这里有个注意点:如果服务器本身承载着线上业务,建议不要无脑使用全部核心,否则编译瞬间 CPU 飙到 100%,可能影响现有服务的响应。我通常在业务低谷时段用make -j2或make -j4保守编译,宁可慢一点,也别干扰线上业务。

第一次 make 通常会持续 5 到 10 分钟,中间会有大量编译输出,只要没出现Error字样基本不用管。如果中途报错,常见原因是内存不足(gcc 进程被 OOM killer 杀掉)或某个扩展源文件有问题。内存不足时可以减少并行数,或者临时增加 swap。

4.2 安装目录与配置文件初始化

make 完成之后,执行make install将 PHP 安装到/usr/local/php目录。安装完成后,需要手工完成两件关键事情:复制配置文件、创建必要的用户和目录。

首先复制 php.ini 配置模板:

cp /usr/local/php/etc/php.ini-production /usr/local/php/etc/php.ini

PHP 源码目录里自带两个 php.ini 模板:php.ini-development(适合开发,日志更详细)和php.ini-production(适合生产,禁用了部分危险函数)。即便后续要调整细节,建议也先用 production 模板做基础,再按需修改。

接着确认运行用户存在:

id www || useradd www -s /sbin/nologin -M

PHP-FPM 需要以非 root 用户运行,这是安全基线。如果后续用 Nginx 对接,这个用户最好与 Nginx 的 worker 用户保持一致,避免 socket 权限问题。

最后还要检查/usr/local/php/etc/php-fpm.conf是否存在,如果不存在,需要从源码目录的sapi/fpm/php-fpm.conf复制,并生成对应的php-fpm.d/www.conf。这一步经常被新手忽略,直接导致后面启动 PHP-FPM 失败。

4.3 验证安装结果的几个命令

安装完成后,我习惯用一组命令验证安装是否完整。

/usr/local/php/bin/php -v /usr/local/php/bin/php -m /usr/local/php/bin/php --ini
  • php -v看到版本号,说明主程序没问题。
  • php -m列出已加载的模块,重点检查mysqli、pdo_mysql、mbstring、opcache、pcntl、sockets是否在内。
  • php --ini确认配置文件加载路径符合预期。

这里有一个我在麒麟 V10 上遇到过的问题:php -v执行时提示缺少libonig.so动态库。原因是系统安装了oniguruma的运行库,但 ldconfig 缓存里没有包含它的路径。解决方法是:

echo "/usr/local/lib64" > /etc/ld.so.conf.d/local.conf ldconfig

或者直接以库的实际安装路径为准,执行find / -name "libonig.so*" 2>/dev/null查出路径后,再添加到 ldconfig 配置里。这类动态库加载问题在 aarch64 架构上尤其需要注意。

5. PHP-FPM与Nginx的无缝衔接

5.1 php-fpm.conf与www.conf的调整

PHP-FPM 是 PHP 官方自带的进程管理器,以独立服务形式运行,通过 FastCGI 协议接收 Web 服务器的请求。刚才复制配置时提到了php-fpm.conf和php-fpm.d/www.conf,启动前需要重点调整几个参数:

; php-fpm.conf 中 pid = /run/php-fpm.pid error_log = /usr/local/php/var/log/php-fpm.log ; php-fpm.d/www.conf 中 user = www group = www listen = 127.0.0.1:9000 ; 或者用unix socket ; listen = /run/php-fpm.sock listen.owner = www listen.group = www listen.mode = 0660 pm = dynamic pm.max_children = 50 pm.start_servers = 5 pm.min_spare_servers = 5 pm.max_spare_servers = 10

这里我想解释一下listen的选择。TCP 方式(127.0.0.1:9000)和 Unix Socket 方式各有适用场景。Unix Socket 通过文件系统通信,少了 TCP 协议栈的开销,在同一台机器上性能略有优势,但需要 Nginx 的 worker 进程有权限访问 socket 文件,因此权限配置比较敏感。TCP 方式更适合跨机部署,也容易排查问题。我这次业务全部在同一台机器上,本可以用 Unix Socket,但为了后续可能拆分服务器,最终还是选了 TCP 9000 端口。

pm = dynamic模式适合大多数 Web 场景:FPM 启动时先起一批进程,空闲进程过多时自动释放,请求高峰时自动增开。pm.max_children是最关键的参数,设置太大会把服务器内存耗尽,设置太小遇到流量波动就返回 502。通常可以根据单进程内存占用来估算:假设每个 PHP-FPM 进程占 30MB,机器内存 8GB 留给 PHP 4GB,那么max_children大约 130;但实际写 50 是比较稳妥的起步值,跑一段时间后再用ps aux | grep php-fpm看真实内存占用。

5.2 systemd管理PHP-FPM服务

默认源码编译的 PHP-FPM 不带 systemd 服务文件,需要手动创建/etc/systemd/system/php-fpm.service,内容如下:

[Unit] Description=PHP FastCGI Process Manager After=network.target [Service] Type=forking PIDFile=/run/php-fpm.pid ExecStart=/usr/local/php/sbin/php-fpm ExecReload=/bin/kill -USR2 $MAINPID ExecStop=/bin/kill -QUIT $MAINPID PrivateTmp=true [Install] WantedBy=multi-user.target

有些版本启动时不会自己创建/run目录下的 pid 文件,需要先执行mkdir -p /run/php-fpm并设置好 owner。还有一种更省事的思路是直接使用php-fpm自带的daemonize行为,让 systemd 通过Type=forking来跟踪主进程。

配置完成后依次执行:

systemctl daemon-reload systemctl enable php-fpm systemctl start php-fpm

启动后立刻检查状态:

systemctl status php-fpm ss -lnt | grep 9000

如果 9000 端口正常监听,说明 FPM 已工作。我遇到的常见启动失败原因是配置文件里某个参数值不合法(比如pid路径无权限写入),查看/usr/local/php/var/log/php-fpm.log通常能直接定位问题。

5.3 Nginx location规则与FastCGI参数

PHP-FPM 就绪之后,还差 Nginx 这最后一块拼图。Nginx 本身并不负责解析 PHP,它只是把以.php结尾的请求通过 FastCGI 协议转发给 PHP-FPM。核心配置如下:

server { listen 80; server_name your.domain.com; root /var/www/html; index index.php index.html; location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }

这段配置里最关键的是fastcgi_param SCRIPT_FILENAME。这个参数告诉 PHP-FPM 要执行的脚本绝对路径,如果配置错误,PHP-FPM 会返回Primary script unknown错误。很多新手把$document_root写成了别的路径,导致请求直接 404。

我习惯在location ~ \.php$块内再加几个常用的 FastCGI 参数优化:

fastcgi_connect_timeout 60; fastcgi_send_timeout 180; fastcgi_read_timeout 180; fastcgi_buffer_size 64k; fastcgi_buffers 32 32k;

这几个超时参数是为长耗时脚本准备的,比如导出报表、批量导入数据等场景。默认的 timeout 很短,遇到耗时的 PHP 脚本很容易 504。但注意超时设太大也有风险——攻击者如果滥用慢请求,可能长期占用 FPM 进程。实际值建议根据业务情况调整。

配置完成后执行nginx -t检查语法,再systemctl reload nginx,然后就能通过浏览器或 curl 验证 PHP 是否正常解析了。

6. 编译安装后的排错与维护经验

6.1 常见启动/运行错误的排查链路

整个流程走完,我整理了这次实际遇到的错误和排查思路,给后来者一条可复用的排查链路。

错误一:php: error while loading shared libraries: libonig.so.5

这个我前面已经提过,属于动态库路径问题。排查方法:ldd /usr/local/php/bin/php查看缺失的库,然后find / -name "libonig.so*"找到库文件实际位置,将目录加入/etc/ld.so.conf.d/并执行ldconfig。

错误二:Failed to connect to FastCGI server: connect() failed (111: Connection refused)

这是 Nginx 访问 PHP-FPM 失败,通常有三种原因:FPM 没启动、FPM 监听地址和 Nginx 的fastcgi_pass不一致、防火墙阻断了 9000 端口。排查链路是:先执行systemctl status php-fpm确认进程状态,再用ss -lntp | grep php确认监听地址,最后检查fastcgi_pass是否写对。

错误三:Primary script unknown

这个错误曾在网上被大量讨论,根因是SCRIPT_FILENAME参数不正确,PHP-FPM 拿到的路径指向不存在的文件。排查方法:在location ~ \.php$块中添加fastcgi_param SCRIPT_FILENAME $request_filename;,因为$request_filename是 Nginx 内部根据 root 和请求 URI 拼出的完整路径,多数情况下比$document_root$fastcgi_script_name更可靠。

错误四:504 Gateway Timeout

说明 PHP 脚本执行时间超过了 Nginx 的fastcgi_read_timeout。处理方法有两个方向:一是把超时调大,二是优化脚本本身。我建议先看/usr/local/php/var/log/php-fpm.log里的慢日志,定位执行慢的脚本,再决定是否需要调大 timeout。盲调超时不是长久之计。

6.2 扩展动态安装的两种方式

编译安装完 PHP,后续业务大概率需要加装扩展,比如 Redis、ImageMagick、sodium 这种。动态安装扩展的方式主要有两种:

方式一:PECL 安装

/usr/local/php/bin/pecl install redis /usr/local/php/bin/pecl install swoole

装完后在 php.ini 里加extension=redis.so即可。PECL 方式的好处是自动匹配 PHP 版本,但依赖phpize和php-config,前面已经提醒过要装 pcre2-devel,就是为了让phpize能顺利运行。

方式二:手动编译安装

有些扩展没有发布到 PECL,只能从 GitHub 拉源码。流程相对固定:

cd /path/to/ext-source /usr/local/php/bin/phpize ./configure --with-php-config=/usr/local/php/bin/php-config make && make install

phpize的作用是为外部扩展生成编译框架,--with-php-config则是告诉扩展编译时要匹配哪个 PHP 版本的 API。如果系统里同时存在多个 PHP 版本,这个参数必须写对,否则编译出来的 .so 根本加载不上。

加载扩展之后,用php -m验证是否启用成功。如果加载失败,PHP 启动时通常会在 stderr 输出类似Cannot load dynamic library xxx.so的信息,先确认 .so 路径是否写对,再检查文件权限是不是 www 用户可读。

6.3 升级与卸载时注意的事项

源码编译安装的 PHP,升级逻辑跟 RPM 包完全不同。很多人以为升级就是make install覆盖一下,结果是新老文件混杂,配置错乱。我习惯的升级路径是:

  • 先备份现有配置:cp -r /usr/local/php/etc /usr/local/php/etc.bak
  • 再make install覆盖二进制,但保留旧配置目录作为参考
  • 对比新的php.ini-production与旧配置文件,按需合并修改
  • 重启 PHP-FPM 验证

这里的风险点在于:PHP 小版本升级(比如 8.1.10 升到 8.1.27)通常不会破坏现有扩展,但如果跨主版本升级(8.1 升到 8.2),所有通过编译安装的 PECL 扩展都必须重新安装,因为扩展依赖的是 PHP 内部的 Zend API,版本号不匹配会导致加载失败。

卸载则相对简单,删掉/usr/local/php目录、systemd 服务文件、以及 PATH 环境变量里的相关内容即可。如果系统里没有其他业务依赖这些目录,就可以放心清理。

提示:无论升级还是卸载,都建议先执行php -m导出当前扩展清单,把php.ini里的配置项也留一份备份。这些文件虽然小,但重新还原时能省大量时间。

最后说点实操中的体会

这次在麒麟 V10 (Sword) 上编译安装 PHP,整个过程比想象中曲折一些,但把关键点理顺之后回头看,其实每个坑都有清晰的原因和解决路径。尤其想强调两件事:一是 configure 阶段不要图省事,该装的依赖库一个都不能少,否则 make 中途报错的排查成本远高于一开始装库;二是 PHP-FPM 和 Nginx 的衔接配置不要凭感觉写,SCRIPT_FILENAME这个参数的语义一定要吃透,它能替你省下大量排查 404 的时间。

另外一个很实用的小技巧:编译完成后把完整的 configure 参数存到/usr/local/php/etc/compile-args.txt里。这样半年后要升级或加扩展,直接查看这个文件就知道当初启用过哪些功能、规避过哪些依赖问题,不用重新翻命令行历史。

如果你的业务场景也需要在麒麟这类国产系统上从零部署 PHP 环境,希望这份记录能帮你少绕几个弯。编译安装这件事本身不复杂,但细节决定成败,把每一步的原因搞清楚了,后面要做的只是耐心等待 make 跑完。

返回列表