LAMP 与 LNMP 架构搭建
本文合并自:02-LAMP环境搭建-yum版.md、03-LAMP环境搭建-源码版.md、02-LAMP与LNMP环境搭建.md、02-LNMP环境搭建.md、3-2-LNMP架构搭建.md
一、架构概述
1.1 什么是 LAMP 与 LNMP
LAMP 与 LNMP 是 Linux 平台上最经典的两套动态网站运行环境,它们的区别只在于 Web 服务器组件:
| 缩写 | 全称组合 | Web 服务器 | PHP 接入方式 |
|---|---|---|---|
| LAMP | Linux + Apache + MySQL/MariaDB + PHP | Apache(httpd) |
mod_php 模块(PHP 作为 Apache 模块内嵌运行) |
| LNMP | Linux + Nginx + MySQL/MariaDB + PHP | Nginx | FastCGI 协议转交 php-fpm 独立进程 |
1.2 各组件的角色分工
| 组件 | 角色 | 说明 |
|---|---|---|
| Linux | 操作系统底座 | 提供进程调度、文件系统、网络栈、权限体系;本文以 CentOS 6 / CentOS 7 为例 |
| Apache / Nginx | Web 服务器(HTTP 服务) | 监听 80/443 端口,接收 HTTP 请求;静态资源(html/css/js/图片)直接读磁盘返回;动态请求(.php)交给 PHP 处理 |
| MySQL / MariaDB | 关系型数据库 | 持久化存储业务数据(用户、文章、订单等),监听 3306 端口 |
| PHP | 服务端脚本解释器 | 解析 .php 文件,执行业务逻辑,通过驱动(mysql/mysqli/PDO)读写数据库,生成 HTML 返回给 Web 服务器 |
四者的关系可以概括为:Linux 承载一切,Web 服务器负责收发 HTTP,PHP 负责算,数据库负责存。
1.3 动态网页的请求处理全流程
以用户访问 http://www.example.com/index.php?id=1 为例:
① 浏览器发起请求
用户输入 URL → DNS 解析域名得到服务器 IP → 与服务器 80 端口建立 TCP 连接
→ 发送 HTTP 请求报文(GET /index.php?id=1 HTTP/1.1)
② Web 服务器接收请求
Apache / Nginx 监听 80 端口收到请求,根据 server_name / VirtualHost 匹配虚拟主机,
根据 root / DocumentRoot 定位文件真实路径,例如 /var/www/html/index.php
③ 判断静态还是动态
- 若是 .html / .jpg / .css 等静态文件 → Web 服务器直接读取磁盘内容返回(不经过 PHP)
- 若是 .php 文件 → 需要交给 PHP 解释器
④ PHP 解析(两种方式,见 1.4)
- LAMP:Apache 的 mod_php 模块在 httpd 进程内部直接调用 PHP 解释器
- LNMP:Nginx 按 FastCGI 协议把请求转发给 php-fpm 进程(TCP 9000 或 unix socket)
⑤ PHP 执行业务逻辑
PHP 逐行执行脚本;遇到数据库操作时,通过 mysqli / PDO 驱动
连接 MySQL/MariaDB 的 3306 端口(或本地 unix socket)
⑥ 数据库处理
MySQL/MariaDB 解析 SQL → 执行查询 → 把结果集返回给 PHP 进程
⑦ PHP 生成响应
PHP 用查询结果拼装出纯 HTML 文本,返回给 Web 服务器
(LAMP 是模块内返回,LNMP 是通过 FastCGI 通道回传给 Nginx)
⑧ Web 服务器返回浏览器
Web 服务器把 HTML 封装成 HTTP 响应报文(含状态码、Content-Type 等头部)发回浏览器
⑨ 浏览器渲染
浏览器解析 HTML,再对其中引用的 css/js/图片发起新的请求(这些通常是静态请求,
由 Web 服务器直接返回),最终渲染出完整页面
要点:数据库永远不直接对浏览器提供服务,浏览器也永远看不到 PHP 源码——它只能看到 PHP 执行后的产物 HTML。如果浏览器里看到了 PHP 源码或者提示下载 .php 文件,说明第 ④ 步没有生效。
1.4 mod_php 与 FastCGI + php-fpm 的原理差异
Apache + mod_php(LAMP 的经典方式)
PHP 在编译时通过 --with-apxs2 参数被编译成 Apache 的动态共享模块 libphp5.so / libphp7.so,Apache 启动时用 LoadModule 把它加载进 httpd 进程的地址空间。此后每一个处理请求的 httpd 子进程/线程内部都自带一份 PHP 解释器,遇到 .php 请求时在进程内直接调用,不存在跨进程通信。
浏览器 → httpd 进程(内含 mod_php → PHP 解释器)→ MySQL
Nginx + FastCGI + php-fpm(LNMP 的方式)
Nginx 本身不能解析 PHP,也没有类似 mod_php 的内嵌模块机制。它把 .php 请求按 FastCGI 协议打包,通过 TCP(默认 127.0.0.1:9000)或 Unix Socket(如 /tmp/php-fpm.sock)转发给独立运行的 php-fpm 进程池;php-fpm 的 worker 进程执行 PHP 脚本后把结果沿同一通道回传给 Nginx。
浏览器 → Nginx(fastcgi_pass)→ php-fpm 进程池(worker)→ MySQL
两种方式对比
| 对比项 | Apache + mod_php | Nginx + php-fpm(FastCGI) |
|---|---|---|
| PHP 运行位置 | httpd 进程内部(同一地址空间) | 独立的 php-fpm 进程 |
| 通信开销 | 无跨进程通信,单请求延迟略低 | 存在 FastCGI 通信开销(socket 比 TCP 略快) |
| 内存占用 | 每个 httpd 进程都加载 PHP,处理静态请求的进程也白占 PHP 内存,高并发下内存消耗大 | 静态请求由 Nginx 处理不涉及 PHP;PHP 进程数可独立控制,内存可控 |
| 静态资源性能 | prefork/worker 模型下并发能力有限 | epoll 事件驱动,单机可支撑数万并发连接,静态资源性能强 |
| 稳定性隔离 | PHP 扩展崩溃可能拖垮 httpd 进程 | PHP 崩溃只影响 php-fpm,Nginx 仍在(表现为 502 而非整站不可达) |
| 配置复杂度 | 简单:装上 php 包即自动生效 |
需要额外配置 fastcgi_pass、SCRIPT_FILENAME,两端 listen 必须对齐 |
| 平滑重启/升级 | 重启 httpd 会中断 PHP | php-fpm 与 Nginx 可分别平滑重载,升级 PHP 不影响 Nginx |
| 动静分离 | 不易拆分 | 天然支持,静态归 Nginx、动态归 php-fpm |
.htaccess 支持 |
支持(目录级配置,虚拟主机场景方便) | 不支持,需在 Nginx 配置里写 rewrite 规则 |
| 典型适用 | 遗留系统、需要 .htaccess、模块生态依赖(如某些 CMS) |
高并发、静态资源多、需要精细控制资源的生产环境 |
| 也可反过来 | Apache 亦可用 mod_proxy_fcgi + php-fpm(Apache 2.4 event MPM 推荐做法) |
Nginx 无法使用 mod_php |
1.5 CGI / FastCGI / php-fpm 概念辨析
| 概念 | 本质 | 说明 |
|---|---|---|
| CGI | 一种协议/规范 | Common Gateway Interface。规定了 Web 服务器如何把请求信息(环境变量 + stdin)传给外部程序,以及如何取回结果(stdout)。缺陷:每个请求都要 fork 一个新进程,进程创建/销毁开销巨大,还要重复加载配置和初始化解释器,高并发下性能极差。 |
| FastCGI | CGI 的改进协议 | 把"每请求一进程"改为常驻进程池:进程启动一次后长期驻留,通过 socket 反复接收请求。省掉了 fork 与解释器初始化开销,性能提升一个数量级。协议本身与语言无关(PHP/Python/Perl 都能实现)。 |
| php-fpm | FastCGI 协议的一个具体实现程序 | PHP FastCGI Process Manager,PHP 官方的 FastCGI 进程管理器(PHP 5.3.3 起并入 PHP 源码树,编译时用 --enable-fpm 启用)。它负责:启动/回收 worker 进程、按 pm 策略动态伸缩进程数、平滑重载配置、慢日志、进程隔离与故障重启。 |
| mod_php | Apache 的内嵌模块 | 与上面三者是不同的技术路线:PHP 直接作为 Web 服务器模块运行,不走 CGI/FastCGI 协议。 |
| SAPI | PHP 的服务端接口抽象层 | Server API。PHP 通过不同 SAPI 与外界对接:apache2handler(mod_php)、fpm-fcgi(php-fpm)、cli(命令行)、cgi-fcgi 等。php -v 或 phpinfo() 中的 "Server API" 一行即显示当前使用的 SAPI。 |
一句话串起来:CGI 是最早的规范但太慢 → FastCGI 用常驻进程池改进了它 → php-fpm 是 PHP 对 FastCGI 的官方实现 → 而 mod_php 走的是另一条把 PHP 塞进 Apache 的路。
1.6 常见组件版本搭配与选型建议
| 系统版本 | 数据库(默认仓库) | PHP(默认仓库) | 服务管理 | 说明 |
|---|---|---|---|---|
| CentOS 6 | mysql / mysql-server(MySQL 5.1) |
PHP 5.3 | service + chkconfig |
PHP 5.3 过旧,很多现代 CMS 不支持,建议用 remi/webtatic 源升级 |
| CentOS 7 | mariadb / mariadb-server(MariaDB 5.5) |
PHP 5.4 | systemctl |
CentOS 7 起默认数据库换成了 MariaDB(MySQL 的社区分支,命令与协议完全兼容) |
| CentOS 7 + 第三方源 | MySQL 5.7 / 8.0、MariaDB 10.x | PHP 7.x(php72w、php73 等) |
systemctl |
生产推荐;PHP 7 相比 5.6 性能提升约一倍 |
| 源码编译 | MySQL 5.7 二进制包 / 源码 cmake | PHP 5.6 / 7.3 自选 | 自建 init 脚本或 systemd unit | 可完全定制路径、参数、扩展 |
选型建议:
- 学习/测试、快速出环境 → 用 yum 版,10 分钟搞定,包依赖由 yum 处理,升级方便。
- 生产环境、需要特定版本或特定扩展 → 用源码编译,可控制安装路径、编译参数、精简模块;但升级需重新编译,需自行维护启动脚本。
- 新项目 Web 服务器 → 优先 Nginx(LNMP);除非应用强依赖
.htaccess或某个 Apache 专有模块。 - PHP 版本 → 新项目直接上 PHP 7.4+ / 8.x;老项目(如 Discuz X3.2 及以前)可能只兼容 PHP 5.6。
- 数据库 → CentOS 7 上如无特殊要求直接用 MariaDB;需要 MySQL 8.0 特性(窗口函数、CTE、原生 JSON 优化)时用官方 MySQL 源。
1.7 环境准备(LAMP / LNMP 通用)
搭建前统一完成以下基础配置,后文各方案不再重复。
1)设置主机名并绑定 hosts
hostnamectl set-hostname web01 # CentOS 7
echo "192.168.1.10 web01" >> /etc/hosts
CentOS 6 修改主机名:
vim /etc/sysconfig/network
# HOSTNAME=web01
hostname web01
2)关闭防火墙与 SELinux(实验环境)
CentOS 7(firewalld):
systemctl stop firewalld
systemctl disable firewalld
CentOS 6(iptables):
service iptables stop
chkconfig iptables off
# 或仅清空规则
iptables -F
关闭 SELinux(临时 + 永久):
setenforce 0
sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
getenforce # 确认结果为 Permissive 或 Disabled
3)生产环境的正确放行方式(不建议直接关闭)
CentOS 7 放行 HTTP/HTTPS 服务:
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reload
firewall-cmd --list-all # 确认已放行
也可按端口放行:
firewall-cmd --permanent --add-port=80/tcp
firewall-cmd --permanent --add-port=3306/tcp
firewall-cmd --reload
CentOS 6 放行 80 端口:
iptables -I INPUT -p tcp --dport 80 -j ACCEPT
service iptables save
SELinux 保持开启时,需放行 Web 相关布尔值(详见第三章常见问题):
setsebool -P httpd_can_network_connect_db on
setsebool -P httpd_can_network_connect on
4)关闭 NetworkManager 并配置静态 IP
systemctl stop NetworkManager
systemctl disable NetworkManager
vim /etc/sysconfig/network-scripts/ifcfg-eth0
BOOTPROTO=static
ONBOOT=yes
IPADDR=192.168.1.10
NETMASK=255.255.255.0
GATEWAY=192.168.1.1
DNS1=223.5.5.5
systemctl restart network
5)配置 yum 源
安装 EPEL 源(Nginx、php-pecl-zendopcache 等包依赖该源):
yum install -y epel-release
# 或(本地源环境下)
yum install -y epel-*
注意:EPEL 源必须先有可用的 CentOS 自带(或本地)yum 源才能安装成功。
配置本地 ISO 光盘源(离线环境常用)。先挂载光盘:
mount /dev/sr0 /yum
永久挂载写入 /etc/fstab:
vim /etc/fstab
/dev/sr0 /yum iso9660 defaults 0 0
备份系统自带源:
mkdir /etc/yum.repos.d/back
mv /etc/yum.repos.d/C* /etc/yum.repos.d/back
编写本地源配置:
vim /etc/yum.repos.d/local.repo
[abc]
name=local_yum
baseurl=file:///yum
enable=1
gpgcheck=0
完整的多源示例(本地源 + EPEL 内网镜像 + 163 镜像):
[local]
name=local
baseurl=file:///yum/
enabled=1
gpgcheck=0
[epel]
name=epel
baseurl=http://10.1.1.1/epel
enabled=1
gpgcheck=0
[centos163]
name=centos163
baseurl=http://10.1.1.1/centos163
enabled=1
gpgcheck=0
重建 yum 缓存:
yum clean all
yum makecache
6)安装依赖与开发工具组(源码编译方式必备)
yum groupinstall -y "Development Tools"
yum install -y gcc gcc-c++ make cmake autoconf \
libxml2 libxml2-devel openssl openssl-devel curl-devel \
libjpeg-devel libpng-devel freetype-devel libmcrypt-devel \
net-snmp-devel readline-devel expat-devel \
zlib zlib-devel pcre pcre-devel bzip2 bzip2-devel \
gmp gmp-devel libxslt libxslt-devel ncurses-devel \
perl libao-devel wget
7)创建服务运行用户
# Apache 运行用户
useradd -s /sbin/nologin apache
# Nginx 运行用户
groupadd nginx
useradd -r -s /usr/sbin/nologin -g nginx nginx
# MySQL 运行用户
groupadd mysql
useradd -r -s /usr/sbin/nologin -g mysql mysql
# php-fpm 运行用户(源码版若指定 --with-fpm-user=php 则建 php 用户)
useradd -s /usr/sbin/nologin php
说明:php-fpm 的运行用户建议与 Nginx 保持一致(都用
nginx),否则 php-fpm 可能因权限不足读不到网站文件。
8)时间同步
yum install -y ntpdate
ntpdate ntp.aliyun.com
echo "*/10 * * * * /usr/sbin/ntpdate ntp.aliyun.com &> /dev/null" >> /var/spool/cron/root
二、LAMP —— yum 快速搭建
适用场景:学习、测试、快速交付。全部组件来自发行版仓库,依赖自动解决,systemctl 统一管理。
2.1 安装 Apache(httpd)
yum install -y httpd httpd-devel
也可以一次装齐所有 httpd 相关包:
yum install -y http*
启动并设置开机自启:
# CentOS 7
systemctl start httpd
systemctl enable httpd
# CentOS 6
service httpd start
chkconfig httpd on
确认 80 端口已监听:
lsof -i:80
netstat -tunlp | grep 80
浏览器访问 http://服务器IP/,看到 Apache 默认测试页即表示 Web 服务正常。
2.2 安装数据库(CentOS 7 用 MariaDB / CentOS 6 用 MySQL)
CentOS 7 —— MariaDB
yum install -y mariadb mariadb-server
# 或一次装齐相关包
yum install -y mariadb*
systemctl start mariadb
systemctl enable mariadb
systemctl status mariadb
CentOS 6 —— MySQL
yum install -y mysql mysql-server
service mysqld start
chkconfig mysqld on
CentOS 6 与 7 的数据库差异:CentOS 6 默认仓库提供的是 MySQL 5.1(包名
mysql-server,服务名mysqld);CentOS 7 起默认换成了 MariaDB 5.5(包名mariadb-server,服务名mariadb)。MariaDB 是 MySQL 的社区分支,客户端命令(mysql、mysqladmin、mysqldump)、SQL 语法与通信协议完全兼容,日常使用无差别。
确认 3306 端口已监听:
lsof -i:3306
无密码登录验证(初装时 root 无密码):
mysql
MariaDB [(none)]> quit
2.3 数据库安全初始化与设置 root 密码
方式一:一条命令直接设置 root 密码(最快)
mysqladmin -uroot password "123"
之后用新密码登录:
mysql -uroot -p123
方式二:执行安全初始化脚本(推荐,生产必做)
mysql_secure_installation
交互过程逐项说明(首次执行时当前 root 密码为空,直接回车):
Enter current password for root (enter for none):
→ 当前 root 密码,初装为空,直接回车
Set root password? [Y/n] Y
→ 是否设置 root 密码,输入 Y
New password:
→ 输入新密码(不回显)
Re-enter new password:
→ 再次输入确认
Password updated successfully!
Remove anonymous users? [Y/n] Y
→ 是否删除匿名用户。匿名用户允许任何人不用账号登录,务必删除
Disallow root login remotely? [Y/n] Y
→ 是否禁止 root 远程登录。生产环境务必禁止,只允许本机 root 登录
Remove test database and access to it? [Y/n] Y
→ 是否删除默认的 test 库(任何用户都可读写),务必删除
Reload privilege tables now? [Y/n] Y
→ 是否立即重新加载权限表使以上修改生效,输入 Y
All done! ... Thanks for using MariaDB!
忘记 root 密码的处理
在配置文件 [mysqld] 段下加入 skip-grant-tables(跳过授权表,登录不验证密码):
vim /etc/my.cnf
[mysqld]
skip-grant-tables
重启数据库:
# yum 版 CentOS 7
systemctl restart mariadb
# yum 版 CentOS 6
service mysqld restart
# 源码版
/etc/init.d/mysqld restart
免密登录后改密:
-- MySQL 5.6 之前版本
UPDATE mysql.user SET password=PASSWORD("123")
WHERE user="root" AND host="localhost";
-- MySQL 5.6 之后版本(密码字段改为 authentication_string)
UPDATE mysql.user SET authentication_string=PASSWORD("123")
WHERE user="root" AND host="localhost";
FLUSH PRIVILEGES;
改密完成后务必删除 skip-grant-tables,再重启数据库,用新密码登录。
2.4 创建测试库与授权用户
mysql -uroot -p
CREATE DATABASE testdb;
GRANT ALL ON testdb.* TO testuser@'localhost' IDENTIFIED BY '123456';
FLUSH PRIVILEGES;
-- 查看结果
SHOW DATABASES;
SELECT user,host FROM mysql.user;
2.5 安装 PHP 及常用模块
yum install -y php php-mysql php-gd php-ldap php-odbc php-pear php-xml \
php-xmlrpc php-mbstring php-snmp php-soap curl curl-devel
各软件包作用:
| 软件包 | 作用 |
|---|---|
php |
PHP 主程序;在 Apache 环境下会自动带出 mod_php 模块 |
php-mysql |
连接 MySQL/MariaDB 的驱动(提供 mysql/mysqli 扩展) |
php-gd |
图像处理库(验证码、缩略图、水印等) |
php-ldap |
LDAP 目录服务支持 |
php-odbc |
ODBC 数据源连接 |
php-pear |
PHP 扩展与应用库(PEAR) |
php-xml |
XML 处理支持 |
php-xmlrpc |
XML-RPC 远程调用支持 |
php-mbstring |
多字节字符串处理(中文必备) |
php-snmp |
SNMP 网络管理协议支持 |
php-soap |
SOAP 协议支持 |
php-bcmath |
高精度数学运算 |
php-mhash |
哈希算法支持 |
php-pecl-zendopcache |
PHP 字节码缓存加速(需 EPEL 源) |
curl / curl-devel |
URL 传输工具及开发库 |
CentOS 6 与 7 的 PHP 版本差异:CentOS 6 默认仓库为 PHP 5.3,CentOS 7 默认为 PHP 5.4,两者均较旧。若应用需要 PHP 7,可使用 webtatic / remi 源:
# CentOS 7 安装 PHP 7.2(webtatic) rpm -Uvh https://mirror.webtatic.com/yum/el7/webtatic-release.rpm yum install -y php72w php72w-mysqlnd php72w-gd php72w-mbstring php72w-fpm php72w-opcache
查看已安装的 PHP 模块:
php -m
[PHP Modules]
bcmath
bz2
calendar
Core
ctype
curl
date
...
gd
...
mbstring
mysql
mysqli
...
Zend OPcache
zip
zlib
[Zend Modules]
Zend OPcache
模块(扩展)的 .so 文件位置:
ls /usr/lib64/php/modules/
bcmath.so json.so odbc.so pdo_sqlite.so sysvsem.so xmlwriter.so
curl.so ldap.so opcache.so phar.so sysvshm.so xsl.so
dom.so mbstring.so pdo_mysql.so posix.so wddx.so zip.so
fileinfo.so mysqli.so pdo_odbc.so sqlite3.so xmlreader.so
gd.so mysql.so pdo.so sysvmsg.so xmlrpc.so
PHP 主配置文件为 /etc/php.ini,扩展的独立配置在 /etc/php.d/*.ini。
2.6 验证 Apache 已加载 PHP 模块
yum 安装 php 包时会自动生成 /etc/httpd/conf.d/php.conf,其中包含 LoadModule php5_module modules/libphp5.so 与 AddHandler php5-script .php 等指令。重启 Apache 后确认模块已加载:
systemctl restart httpd
# 列出所有已加载模块并过滤 php
httpd -M | grep php
php5_module (shared)
也可用以下方式确认:
# 查看 php.conf 是否存在
cat /etc/httpd/conf.d/php.conf
# 检查 Apache 配置语法
httpd -t
apachectl configtest
2.7 编写测试页并浏览器验证
phpinfo 测试页
echo "<?php phpinfo(); ?>" > /var/www/html/index.php
或用编辑器写入:
vim /var/www/html/index.php
<?php
phpinfo();
?>
重启 Apache 使 PHP 模块生效:
systemctl restart httpd
浏览器访问 http://服务器IP/ 或 http://服务器IP/index.php,出现蓝紫色的 PHP 信息页面(显示 PHP 版本、Server API、已加载扩展、php.ini 路径等)即表示 PHP 已成功与 Apache 整合。其中 Server API 显示为 Apache 2.0 Handler 说明走的是 mod_php 方式。
安全提示:
phpinfo()会暴露服务器路径、扩展、环境变量等敏感信息,验证完成后务必删除该文件。
数据库连接测试页
先确保已按 2.4 创建好 testdb 库与 testuser 用户,然后创建 /var/www/html/mysqltest.php:
<?php
$link = mysqli_connect('localhost', 'testuser', '123456', 'testdb');
if (!$link) {
die('连接失败: ' . mysqli_connect_error());
}
echo '数据库连接成功!';
mysqli_close($link);
?>
更完整的连接 + 查询测试脚本:
<?php
header('Content-Type: text/html; charset=utf-8');
$host = 'localhost';
$user = 'testuser';
$pass = '123456';
$db = 'testdb';
// 建立连接
$link = mysqli_connect($host, $user, $pass, $db);
if (!$link) {
die('连接失败: ' . mysqli_connect_error());
}
echo "数据库连接成功!<br>";
// 设置字符集,避免中文乱码
mysqli_set_charset($link, 'utf8');
// 查询数据库版本
$result = mysqli_query($link, 'SELECT VERSION() AS ver');
$row = mysqli_fetch_assoc($result);
echo "数据库版本:" . $row['ver'] . "<br>";
// 列出所有库
$result = mysqli_query($link, 'SHOW DATABASES');
echo "现有数据库:<br>";
while ($row = mysqli_fetch_row($result)) {
echo " " . $row[0] . "<br>";
}
mysqli_free_result($result);
mysqli_close($link);
?>
使用 PDO 方式的等价测试脚本:
<?php
try {
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=testdb;charset=utf8';
$pdo = new PDO($dsn, 'testuser', '123456');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
echo 'PDO 连接成功!版本:' . $pdo->query('SELECT VERSION()')->fetchColumn();
} catch (PDOException $e) {
echo 'PDO 连接失败:' . $e->getMessage();
}
?>
浏览器访问 http://服务器IP/mysqltest.php,显示"数据库连接成功!"即表示 PHP 与数据库协同正常。测试完成后建议删除该脚本。
2.8 服务管理命令汇总(yum 版)
# Apache(CentOS 7)
systemctl start httpd
systemctl stop httpd
systemctl restart httpd
systemctl reload httpd # 平滑重载配置,不中断连接
systemctl enable httpd
systemctl status httpd
# Apache(CentOS 6)
service httpd start|stop|restart|reload|status
chkconfig httpd on
# MariaDB(CentOS 7)
systemctl start mariadb
systemctl stop mariadb
systemctl restart mariadb
systemctl enable mariadb
systemctl status mariadb
# MySQL(CentOS 6)
service mysqld start|stop|restart|status
chkconfig mysqld on
# 端口检查
lsof -i:80
lsof -i:3306
netstat -tunlp | grep -E '80|3306'
Apache 关键默认路径(yum 版):
| 项目 | 路径 / 说明 |
|---|---|
| 网站根目录 | /var/www/html/ |
| 主配置文件 | /etc/httpd/conf/httpd.conf |
| 扩展配置目录 | /etc/httpd/conf.d/(含 php.conf、welcome.conf) |
| PHP 主配置 | /etc/php.ini |
| PHP 扩展配置 | /etc/php.d/*.ini |
| 模块目录 | /etc/httpd/modules/ → /usr/lib64/httpd/modules/ |
| 访问日志 | /var/log/httpd/access_log |
| 错误日志 | /var/log/httpd/error_log |
| 服务管理 | systemctl {start\|stop\|restart\|status} httpd |
2.9 常见问题(yum 版 LAMP)
1)PHP 页面被浏览器下载而不解析(或直接显示源码)
现象:访问 index.php 时浏览器弹出下载框,或页面上原样显示 <?php phpinfo(); ?>。
原因与排查:
# ① 确认 php 包已装
rpm -qa | grep php
# ② 确认 php.conf 存在(yum 版由它注册 LoadModule 与 AddHandler)
ls -l /etc/httpd/conf.d/php.conf
# ③ 确认模块已加载
httpd -M | grep php
# ④ 装完 php 后必须重启(而非 reload)httpd
systemctl restart httpd
若 php.conf 缺失或需手工声明,可在 /etc/httpd/conf/httpd.conf 中添加:
LoadModule php5_module modules/libphp5.so
AddType application/x-httpd-php .php .phtml
AddType application/x-httpd-php-source .phps
DirectoryIndex index.php index.html
2)连接数据库失败
# ① 确认数据库服务已启动、端口已监听
systemctl status mariadb
lsof -i:3306
# ② 确认 root/业务账号密码正确
mysql -uroot -p
# ③ 确认 php-mysql 扩展已装(缺少时报 Call to undefined function mysqli_connect)
php -m | grep -i mysql
# ④ 确认授权的 host 与实际连接来源一致
mysql -uroot -p -e "SELECT user,host FROM mysql.user;"
localhost 与 127.0.0.1 的区别是常见坑:localhost 走 Unix Socket,127.0.0.1 走 TCP。若报 socket 文件找不到(Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock'),把连接地址由 localhost 改成 127.0.0.1 通常即可绕过;或在 php.ini 里把 mysqli.default_socket 指向真实 socket 路径。
3)SELinux 导致 PHP 无法连接数据库或读写文件
现象:命令行 mysql 能连、php 命令行脚本能连,但通过浏览器访问就连不上;错误日志中出现 Permission denied。这是 SELinux 在拦截 httpd 进程的网络/文件访问。
# 允许 httpd 连接数据库(远程或 TCP 方式连接数据库时必须开)
setsebool -P httpd_can_network_connect_db on
# 允许 httpd 发起任意网络连接(调用外部 API、连远程 Redis 等)
setsebool -P httpd_can_network_connect on
# 允许 httpd 发送邮件
setsebool -P httpd_can_sendmail on
# 查看所有 httpd 相关布尔值
getsebool -a | grep httpd
# 修正网站目录的 SELinux 上下文(如从其它目录 mv 过来的文件)
chcon -R -t httpd_sys_content_t /var/www/html/
# 允许写入的目录(上传目录、缓存目录)
chcon -R -t httpd_sys_rw_content_t /var/www/html/data/
# 查看被 SELinux 拒绝的记录
grep denied /var/log/audit/audit.log
排查期间可临时关闭验证是否为 SELinux 所致:
setenforce 0
4)浏览器访问不到页面
- 检查防火墙:CentOS 7
firewall-cmd --list-all;CentOS 6iptables -L -n,必要时iptables -F。 - 检查 SELinux:
getenforce,必要时setenforce 0。 - 检查 80 端口是否被其它服务(如 Nginx)占用:
lsof -i:80。 - 检查服务是否真的启动:
systemctl status httpd。
三、LAMP —— 源码编译搭建
适用场景:需要指定版本、自定义安装路径、精简或增加模块的生产环境。本章路径规划:Apache 装在 /usr/local/apache243,MySQL 装在 /usr/local/mysql(数据目录 /data),PHP 装在 /usr/local/php,Apache 配置文件放 /etc/httpd。
3.1 依赖库编译安装
先按 1.7 第 6 项安装好开发工具组与各类 -devel 包:
yum install -y gcc gcc-c++ libxml2 libxml2-devel openssl openssl-devel \
curl-devel libjpeg-devel libpng-devel freetype-devel libmcrypt-devel \
net-snmp-devel readline-devel expat-devel
下面这些依赖若系统仓库版本过低,需自行源码编译。
1)zlib(压缩库,Apache 的 --with-z 与 PHP 的 --with-zlib-dir 依赖它)
wget http://www.zlib.net/zlib-1.2.11.tar.gz
tar xf zlib-1.2.11.tar.gz -C /usr/src/
cd /usr/src/zlib-1.2.11/
./configure --prefix=/usr/local/zlib
make && make install
2)pcre(正则库,Apache 的 rewrite 模块与 Nginx 均依赖)
wget https://ftp.pcre.org/pub/pcre/pcre-8.43.tar.gz
tar xf pcre-8.43.tar.gz -C /usr/src/
cd /usr/src/pcre-8.43/
./configure --prefix=/usr/local/pcre --enable-utf8 --enable-unicode-properties
make && make install
若使用系统自带的 pcre(
yum install -y pcre pcre-devel),Apache 编译时直接写--with-pcre=/usr/即可。
3)libxml2(XML 解析,PHP 编译的硬依赖)
wget ftp://xmlsoft.org/libxml2/libxml2-2.9.9.tar.gz
tar xf libxml2-2.9.9.tar.gz -C /usr/src/
cd /usr/src/libxml2-2.9.9/
./configure --prefix=/usr/local/libxml2 --without-python
make && make install
4)libpng(PNG 图像支持,GD 库依赖)
wget https://download.sourceforge.net/libpng/libpng-1.6.37.tar.gz
tar xf libpng-1.6.37.tar.gz -C /usr/src/
cd /usr/src/libpng-1.6.37/
./configure --prefix=/usr/local/libpng
make && make install
5)jpeg(JPEG 图像支持,GD 库依赖)
wget http://www.ijg.org/files/jpegsrc.v9c.tar.gz
tar xf jpegsrc.v9c.tar.gz -C /usr/src/
cd /usr/src/jpeg-9c/
./configure --prefix=/usr/local/jpeg --enable-shared --enable-static
make && make install
6)freetype(字体渲染,验证码/水印文字依赖)
wget https://download.savannah.gnu.org/releases/freetype/freetype-2.10.0.tar.gz
tar xf freetype-2.10.0.tar.gz -C /usr/src/
cd /usr/src/freetype-2.10.0/
./configure --prefix=/usr/local/freetype --enable-freetype-config
make && make install
7)gd(图形绘制库,PHP 的 --with-gd 依赖)
wget https://github.com/libgd/libgd/releases/download/gd-2.2.5/libgd-2.2.5.tar.gz
tar xf libgd-2.2.5.tar.gz -C /usr/src/
cd /usr/src/libgd-2.2.5/
./configure --prefix=/usr/local/gd \
--with-jpeg=/usr/local/jpeg \
--with-png=/usr/local/libpng \
--with-freetype=/usr/local/freetype \
--with-zlib=/usr/local/zlib
make && make install
8)libmcrypt(加密库,PHP 5 的 --with-mcrypt 依赖;PHP 7.2 起已移除该扩展)
wget https://sourceforge.net/projects/mcrypt/files/Libmcrypt/2.5.8/libmcrypt-2.5.8.tar.gz
tar xf libmcrypt-2.5.8.tar.gz -C /usr/src/
cd /usr/src/libmcrypt-2.5.8/
./configure --prefix=/usr/local/libmcrypt
make && make install
编译完自定义前缀的库后,需让系统链接器找到它们:
echo "/usr/local/zlib/lib" >> /etc/ld.so.conf.d/local.conf
echo "/usr/local/libpng/lib" >> /etc/ld.so.conf.d/local.conf
echo "/usr/local/jpeg/lib" >> /etc/ld.so.conf.d/local.conf
echo "/usr/local/freetype/lib" >> /etc/ld.so.conf.d/local.conf
echo "/usr/local/gd/lib" >> /etc/ld.so.conf.d/local.conf
echo "/usr/local/libxml2/lib" >> /etc/ld.so.conf.d/local.conf
echo "/usr/local/libmcrypt/lib" >> /etc/ld.so.conf.d/local.conf
ldconfig -v | grep -E 'zlib|png|jpeg|freetype|gd'
3.2 Apache 源码编译安装
Apache httpd 2.4 依赖 apr(Apache Portable Runtime,跨平台运行时库)与 apr-util(apr 工具库),必须先编译安装这两个包。
1)安装 apr
下载:
wget http://mirrors.shu.edu.cn/apache//apr/apr-1.6.5.tar.gz
解压:
tar xf apr-1.6.5.tar.gz -C /usr/src/
编译并安装:
cd /usr/src/apr-1.6.5/
./configure --prefix=/usr/local/apr-httpd/ && make && make install
2)安装 apr-util
下载:
wget https://mirrors.tuna.tsinghua.edu.cn/apache//apr/apr-util-1.6.1.tar.gz
解压:
tar xf apr-util-1.6.1.tar.gz -C /usr/src/
编译并安装(必须通过 --with-apr 指向前面安装的 apr):
cd /usr/src/apr-util-1.6.1/
./configure --prefix=/usr/local/apr-util-httpd/ --with-apr=/usr/local/apr-httpd/ && make && make install
踩坑:apr-util 1.6.x 编译时若报
expat.h: No such file or directory,安装expat-devel即可(yum install -y expat-devel)。
3)编译安装 Apache httpd
下载源码包(两个可用版本):
wget http://mirror.bit.edu.cn/apache/httpd/httpd-2.4.37.tar.gz
# 或
wget http://mirror.bit.edu.cn/apache/httpd/httpd-2.4.38.tar.gz
解压:
tar xf httpd-2.4.37.tar.gz -C /usr/src/
配置编译参数:
cd /usr/src/httpd-2.4.37/
./configure --prefix=/usr/local/apache243 \
--enable-mods-shared=all \
--enable-deflate \
--enable-speling \
--enable-cache \
--enable-file-cache \
--enable-disk-cache \
--enable-mem-cache \
--enable-so \
--enable-expires=shared \
--enable-rewrite=shared \
--enable-static-support \
--sysconfdir=/etc/httpd \
--with-z=/usr/local/zlib/ \
--with-apr=/usr/local/apr-httpd/ \
--with-apr-util=/usr/local/apr-util-httpd/ \
--with-pcre=/usr/ \
--disable-userdir
编译并安装:
make && make install
echo $? # 返回 0 表示成功
configure 参数逐项说明:
| 参数 | 说明 |
|---|---|
--prefix=/usr/local/apache243 |
Apache 安装主目录(另一套文档使用 /usr/local/apache,可自选,后续路径需对应改) |
--sysconfdir=/etc/httpd |
配置文件安装到 /etc/httpd,与系统习惯一致,便于管理 |
--enable-mods-shared=all |
以 DSO(动态共享对象)方式编译所有模块,可随时用 LoadModule 增删而无需重编译 |
--enable-so |
启用 DSO 动态模块加载支持(mod_so);PHP 用 --with-apxs2 集成时必须开启 |
--enable-deflate |
启用 mod_deflate 压缩模块,可对文本响应做 gzip 压缩,节省带宽 |
--enable-speling |
启用 mod_speling,自动纠正 URL 中的大小写与拼写错误 |
--enable-cache |
启用缓存基础框架 mod_cache |
--enable-file-cache |
启用 mod_file_cache,把常用文件预加载到内存 |
--enable-disk-cache |
启用磁盘缓存 mod_cache_disk |
--enable-mem-cache |
启用内存缓存模块(2.4 中已被 mod_cache_socache 取代,旧参数保留兼容) |
--enable-expires=shared |
以共享模块方式启用 mod_expires,用于设置静态资源过期头(浏览器缓存) |
--enable-rewrite=shared |
以共享模块方式启用 mod_rewrite,URL 重写/伪静态必备 |
--enable-static-support |
编译静态链接版本的支持程序(ab、apachectl 等辅助工具) |
--with-z=/usr/local/zlib/ |
指定 zlib 库路径(mod_deflate 依赖) |
--with-apr=/usr/local/apr-httpd/ |
指定 apr 安装路径 |
--with-apr-util=/usr/local/apr-util-httpd/ |
指定 apr-util 安装路径 |
--with-pcre=/usr/ |
指定 PCRE 正则库路径(用系统自带的即写 /usr/;用自编译的则写 /usr/local/pcre) |
--disable-userdir |
禁用用户家目录访问(http://host/~user/),减少攻击面 |
4)验证安装结果
ls /usr/local/apache243
bin build cgi-bin error htdocs icons include logs man manual modules
查看编译时使用的参数(事后回查):
cat /usr/local/apache243/build/config.nice
/usr/local/apache243/bin/httpd -V # 查看版本与 MPM
/usr/local/apache243/bin/httpd -l # 查看静态编入的模块
/usr/local/apache243/bin/httpd -M # 查看已加载的所有模块
5)编辑配置文件
在主配置文件中添加 ServerName,避免启动时出现 Could not reliably determine the server's fully qualified domain name 告警:
vim /etc/httpd/httpd.conf
ServerName localhost:80
同时建议指定运行用户(先按 1.7 建好 apache 用户):
useradd -s /sbin/nologin apache
User apache
Group apache
6)启动 Apache 并验证
/usr/local/apache243/bin/apachectl configtest # 语法检查
/usr/local/apache243/bin/apachectl start
lsof -i:80
浏览器输入服务器 IP,看到 It works! 即表示 Apache 源码安装成功。
7)把 apachectl 与 systemd 接入系统
添加环境变量与软链接,避免每次输全路径:
echo "export PATH=\$PATH:/usr/local/apache243/bin" >> /etc/profile
source /etc/profile
ln -s /usr/local/apache243/bin/apachectl /usr/sbin/apachectl
CentOS 6 —— 用 apachectl 生成 SysV 启动脚本:
cp /usr/local/apache243/bin/apachectl /etc/init.d/httpd
vim /etc/init.d/httpd
在文件开头(#!/bin/sh 之后)加入 chkconfig 声明:
# chkconfig: 35 85 15
# description: Apache HTTP Server
chmod 755 /etc/init.d/httpd
chkconfig --add httpd
chkconfig httpd on
service httpd start
CentOS 7 —— 编写 systemd unit 文件:
vim /usr/lib/systemd/system/httpd.service
[Unit]
Description=Apache HTTP Server (compiled from source)
After=network.target remote-fs.target nss-lookup.target
[Service]
Type=forking
PIDFile=/usr/local/apache243/logs/httpd.pid
ExecStart=/usr/local/apache243/bin/apachectl start
ExecReload=/usr/local/apache243/bin/apachectl graceful
ExecStop=/usr/local/apache243/bin/apachectl stop
KillSignal=SIGCONT
PrivateTmp=true
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl start httpd
systemctl enable httpd
systemctl status httpd
CentOS 6 与 7 的服务管理差异:CentOS 6 使用 SysV init(
service xxx start+chkconfig xxx on,脚本放/etc/init.d/);CentOS 7 使用 systemd(systemctl start xxx+systemctl enable xxx,unit 文件放/usr/lib/systemd/system/)。CentOS 7 上service命令仍可用,但会被转发给 systemctl。
3.3 MySQL 安装(二进制包方式,推荐)
MySQL 源码编译(cmake)耗时极长(半小时到数小时),生产中通常使用官方预编译的 glibc 二进制包,解压即用。以下步骤 LAMP 与 LNMP 通用。
MySQL 官网:http://www.mysql.org / https://dev.mysql.com/downloads/
1)安装依赖并下载二进制包
yum install -y perl libao-devel libaio libaio-devel numactl
可选的几个版本(按需选一):
# 方案 A:MySQL 5.7.23 glibc 通用包
wget 192.168.2.254/mysql/pack/mysql-5.7.23-linux-glibc2.12-x86_64.tar.gz
# 方案 B:MySQL 5.7.26 el7 包
wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.26-el7-x86_64.tar.gz
# 方案 C:MySQL Cluster 7.6 包
wget https://cdn.mysql.com//Downloads/MySQL-Cluster-7.6/mysql-cluster-gpl-7.6.7-linux-glibc2.12-x86_64.tar.gz
2)解压到 /usr/local 并重命名
# 方案 A
tar xf mysql-5.7.23-linux-glibc2.12-x86_64.tar.gz -C /usr/local
mv /usr/local/mysql-5.7.23-linux-glibc2.12-x86_64 /usr/local/mysql
# 方案 B
tar xf mysql-5.7.26-el7-x86_64.tar.gz -C /usr/local
mv /usr/local/mysql-5.7.26-el7-x86_64/ /usr/local/mysql
# 方案 C
tar -xvf mysql-cluster-gpl-7.6.8-linux-glibc2.12-x86_64.tar.gz -C /usr/local
mv /usr/local/mysql-cluster-gpl-7.6.8-linux-glibc2.12-x86_64/ /usr/local/mysql
3)添加环境变量
echo "export PATH=$PATH:/usr/local/mysql/bin" >> /etc/profile
source /etc/profile
# 或做软链接
ln -s /usr/local/mysql/bin/mysql /usr/bin/mysql
4)创建目录与配置文件
方案一(数据目录 /data,配合源码版 LAMP):
mkdir /data /var/log/mysqld /usr/local/mysql/etc/
vim /usr/local/mysql/etc/my.cnf
[mysqld]
datadir=/data
user=mysql
port=3306
basedir=/usr/local/mysql
socket=/data/mysql.sock
pid-file=/data/mysql.pid
log-error=/var/log/mysqld/mysql-err.log
[mysqld_safe]
pid-file=/data/mysql.pid
log-error=/var/log/mysqld/mysql-err.log
[client]
socket=/data/mysql.sock
方案二(数据目录 /data/mysql):
mkdir -p /usr/local/mysql/etc
vim /usr/local/mysql/etc/my.cnf
[mysqld]
user=mysql
port=3306
datadir=/data/mysql
pid-file=/var/run/mysql/mysql.pid
socket=/data/mysql/mysql.sock
[mysqld_safe]
log-error=/var/log/mysql/mysql_err.log
[client]
socket=/data/mysql/mysql.sock
方案三(数据目录 /var/lib/mysql,配合源码版 LNMP,与 yum 版路径习惯一致):
mkdir /usr/local/mysql/etc
vim /usr/local/mysql/etc/my.cnf
[mysqld]
user=mysql
port=3306
datadir=/var/lib/mysql
pid-file=/var/run/mysql/mysql.pid
socket=/var/lib/mysql/mysql.sock
[mysqld_safe]
log-error=/var/log/mysql/mysql_err.log
[client]
socket=/var/lib/mysql/mysql.sock
三套方案数据目录不同(
/data、/data/mysql、/var/lib/mysql),按环境统一选一套即可。关键是 socket 路径要与后续 PHP 的--with-mysql-sock参数保持一致,否则 PHP 用localhost连库时会报找不到 socket。单独把
my.cnf放在/usr/local/mysql/etc/而不用/etc/my.cnf,是为了避免与系统自带(或 yum 装过的)MySQL 配置冲突。
生产环境可补充的基础参数:
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
max_connections=1000
innodb_buffer_pool_size=1G
innodb_flush_log_at_trx_commit=1
sync_binlog=1
slow_query_log=1
slow_query_log_file=/var/log/mysql/slow.log
long_query_time=2
log-bin=/data/mysql/mysql-bin
server-id=1
skip-name-resolve
5)创建 mysql 用户与目录授权
useradd -s /usr/sbin/nologin mysql
# 或更规范地指定系统用户与组
groupadd mysql
useradd -r -s /usr/sbin/nologin -g mysql mysql
# 按所选方案创建目录并授权
chown -R mysql.mysql /data /var/log/mysqld # 方案一
mkdir -p /var/log/mysql /data/mysql /var/run/mysql # 方案二
chown -R mysql.mysql /var/log/mysql /data/mysql /var/run/mysql
mkdir -p /var/log/mysql /var/lib/mysql /var/run/mysql # 方案三
chown -R mysql.mysql /var/log/mysql /var/lib/mysql /var/run/mysql
6)初始化数据目录
MySQL 5.7 及以上使用 mysqld --initialize:
# 方案一
/usr/local/mysql/bin/mysqld --initialize \
--user=mysql \
--datadir=/data \
--basedir=/usr/local/mysql
# 方案二
/usr/local/mysql/bin/mysqld --initialize --user=mysql \
--basedir=/usr/local/mysql --datadir=/data/mysql
# 方案三(已配好环境变量可直接用 mysqld)
mysqld --initialize --user=mysql --basedir=/usr/local/mysql --datadir=/var/lib/mysql
初始化参数说明:
| 参数 | 说明 |
|---|---|
--initialize |
执行初始化操作,创建系统库并生成随机 root 临时密码 |
--initialize-insecure |
同上,但不生成临时密码(root 密码为空),便于自动化脚本 |
--basedir |
指定 MySQL 安装路径 |
--datadir |
指定 MySQL 数据目录(必须为空目录,否则初始化失败) |
--user |
指定运行 MySQL 的系统用户 |
MySQL 5.6 及以前(含 MariaDB 5.5)使用 mysql_install_db:
/usr/local/mysql/scripts/mysql_install_db \
--user=mysql \
--basedir=/usr/local/mysql \
--datadir=/data
初始化完成后数据目录会生成系统库:
ls /data/mysql
auto.cnf ib_buffer_pool ibdata1 ib_logfile0 ib_logfile1 mysql ndbinfo performance_schema sys
或(非 Cluster 版本):
auto.cnf ib_buffer_pool ibdata1 ib_logfile0 ib_logfile1
mysql performance_schema sys
初始化输出的最后一行会给出 root 临时密码,务必记录:
... [Note] A temporary password is generated for root@localhost: _R#f2oxqp1oa
若未记录,也可从错误日志中找回:
cat /var/log/mysqld/mysql-err.log | grep password
grep 'temporary password' /var/log/mysql/mysql_err.log
若临时密码彻底丢失,只能删空数据目录重新初始化,或用
skip-grant-tables强制改密(见 2.3)。
7)配置启动脚本(SysV)
cp /usr/local/mysql/support-files/mysql.server /etc/init.d/mysqld
# 另一套文档命名为 mysql
cp /usr/local/mysql/support-files/mysql.server /etc/init.d/mysql
修改启动脚本中的路径变量:
vim /etc/init.d/mysqld
basedir=/usr/local/mysql
datadir=/data
conf=$basedir/etc/my.cnf
对应方案二/方案三:
basedir=/usr/local/mysql
datadir=/data/mysql
conf=/$basedir/etc/my.cnf
basedir=/usr/local/mysql
datadir=/var/lib/mysql
conf=$basedir/etc/my.cnf
chmod 755 /etc/init.d/mysqld
chkconfig --add mysqld
chkconfig mysqld on
8)启动 MySQL 并验证
/etc/init.d/mysqld start
# 或
/etc/init.d/mysql start
lsof -i:3306
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
mysqld 77152 mysql 19u IPv6 137347 0t0 TCP *:mysql (LISTEN)
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
mysqld 54384 mysql 16u IPv6 97299 0t0 TCP *:opsession-prxy (LISTEN)
注意:以这种二进制包 + SysV 脚本方式安装的 MySQL 无法用
systemctl管理,只能用/etc/init.d/mysqld {start|stop|restart|status}。
9)接入 systemd(可选,CentOS 7)
vim /usr/lib/systemd/system/mysqld.service
[Unit]
Description=MySQL Server
After=network.target
[Service]
User=mysql
Group=mysql
Type=forking
PIDFile=/data/mysql.pid
ExecStart=/usr/local/mysql/bin/mysqld_safe --defaults-file=/usr/local/mysql/etc/my.cnf
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl start mysqld
systemctl enable mysqld
10)修改初始 root 密码
# 用临时密码改密
/usr/local/mysql/bin/mysqladmin -uroot -p password "123"
# 或直接把临时密码写在命令里
mysqladmin -uroot -p'_R#f2oxqp1oa' password "123"
验证登录:
/usr/local/mysql/bin/mysql -uroot -p123
# 或(已做软链接/环境变量)
mysql -p123
能登录成功即可。也可登录后用 SQL 改密:
ALTER USER 'root'@'localhost' IDENTIFIED BY '123'; -- MySQL 5.7+
SET PASSWORD FOR 'root'@'localhost' = PASSWORD('123'); -- 旧语法
FLUSH PRIVILEGES;
MySQL 5.7 默认启用密码强度策略(
validate_password),设置弱密码如123会报Your password does not satisfy the current policy requirements。临时放宽(生产不建议):SET GLOBAL validate_password_policy = 0; SET GLOBAL validate_password_length = 1;或在
my.cnf中加validate_password = OFF。
11)MySQL 源码编译方式(cmake,需要完全定制时使用)
yum install -y cmake ncurses-devel bison boost-devel
wget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.26.tar.gz
tar xf mysql-5.7.26.tar.gz -C /usr/src/
cd /usr/src/mysql-5.7.26/
mkdir build && cd build
cmake .. \
-DCMAKE_INSTALL_PREFIX=/usr/local/mysql \
-DMYSQL_DATADIR=/data/mysql \
-DSYSCONFDIR=/usr/local/mysql/etc \
-DMYSQL_UNIX_ADDR=/data/mysql/mysql.sock \
-DDEFAULT_CHARSET=utf8mb4 \
-DDEFAULT_COLLATION=utf8mb4_general_ci \
-DEXTRA_CHARSETS=all \
-DWITH_INNOBASE_STORAGE_ENGINE=1 \
-DWITH_MYISAM_STORAGE_ENGINE=1 \
-DWITH_ARCHIVE_STORAGE_ENGINE=1 \
-DWITH_BLACKHOLE_STORAGE_ENGINE=1 \
-DWITH_FEDERATED_STORAGE_ENGINE=1 \
-DWITH_PARTITION_STORAGE_ENGINE=1 \
-DWITH_EMBEDDED_SERVER=1 \
-DWITH_SYSTEMD=1 \
-DWITH_BOOST=/usr/src/boost_1_59_0 \
-DDOWNLOAD_BOOST=1 \
-DENABLED_LOCAL_INFILE=1 \
-DWITH_SSL=system \
-DWITH_ZLIB=system \
-DMYSQL_TCP_PORT=3306
make -j $(nproc) && make install
cmake 关键参数说明:
| 参数 | 说明 |
|---|---|
-DCMAKE_INSTALL_PREFIX |
安装目录 |
-DMYSQL_DATADIR |
数据目录 |
-DSYSCONFDIR |
默认配置文件(my.cnf)搜索目录 |
-DMYSQL_UNIX_ADDR |
Unix socket 文件路径 |
-DDEFAULT_CHARSET / -DDEFAULT_COLLATION |
默认字符集与排序规则,中文推荐 utf8mb4 |
-DEXTRA_CHARSETS=all |
编入所有字符集 |
-DWITH_*_STORAGE_ENGINE=1 |
编入对应存储引擎(InnoDB、MyISAM、分区等) |
-DWITH_SYSTEMD=1 |
生成 systemd unit,可用 systemctl 管理 |
-DWITH_BOOST / -DDOWNLOAD_BOOST=1 |
MySQL 5.7 编译需要 boost 库,可指定本地路径或自动下载 |
-DWITH_SSL=system / -DWITH_ZLIB=system |
使用系统自带的 openssl / zlib |
-DENABLED_LOCAL_INFILE=1 |
允许 LOAD DATA LOCAL INFILE |
-DMYSQL_TCP_PORT=3306 |
监听端口 |
3.4 PHP 源码编译安装(LAMP 方式:编译为 Apache 模块)
PHP 官网:https://www.php.net
1)下载源码并做前置软链接
wget 192.168.2.254/mysql/pack/php/php-5.6.37.tar.gz
# 或官方源
wget http://cn2.php.net/distributions/php-5.6.37.tar.gz
wget https://www.php.net/distributions/php-5.6.40.tar.gz
wget https://www.php.net/distributions/php-7.3.6.tar.gz
踩坑:配置 PHP 之前先创建一个软链接,否则 configure 阶段会报找不到 libmysqlclient_r 的错误:
ln -s /usr/local/mysql/lib/libmysqlclient.so.20 /usr/local/mysql/lib/libmysqlclient_r.so
解压:
tar xf php-5.6.37.tar.gz -C /usr/src/
cd /usr/src/php-5.6.37/
2)configure 编译参数(LAMP:与 Apache 集成)
./configure --prefix=/usr/local/php \
--with-config-file-path=/usr/local/php/etc \
--with-apxs2=/usr/local/apache243/bin/apxs \
--with-mysql=/usr/local/mysql/ \
--with-mysqli=/usr/local/mysql/bin/mysql_config \
--enable-soap \
--enable-mbstring=all \
--enable-sockets \
--datarootdir=/usr/share \
--enable-fpm \
--with-fpm-user=php \
--with-fpm-group=php \
--with-curl \
--with-mysql-sock=/data/mysql.sock \
--with-readline \
--with-snmp \
--enable-soap \
--with-pear \
--with-pdo-mysql=/usr/local/mysql
若 Apache 安装在
/usr/local/apache,则把--with-apxs2改为/usr/local/apache/bin/apxs。
一份更完整的 PHP 5.6 参数(含 GD/图像/加密相关,LNMP 与 LAMP 都可参考):
./configure \
--prefix=/usr/local/php \
--with-config-file-path=/usr/local/php/etc \
--enable-fpm \
--with-fpm-user=php-fpm \
--with-fpm-group=php-fpm \
--with-mysql=/usr/local/mysql \
--with-mysql-sock=/var/lib/mysql/mysql.sock \
--with-libxml-dir \
--with-gd \
--with-jpeg-dir \
--with-png-dir \
--with-freetype-dir \
--with-iconv-dir \
--with-zlib-dir \
--with-mcrypt \
--enable-soap \
--enable-gd-native-ttf \
--enable-ftp \
--enable-mbstring \
--enable-exif \
--disable-ipv6 \
--with-pear \
--with-curl \
--with-openssl
PHP 7 的差异:PHP 7 已移除 --with-mysql(旧 mysql 扩展被删除),必须改用 mysqlnd 驱动:
./configure \
--prefix=/usr/local/php \
--with-config-file-path=/usr/local/php/etc \
--enable-fpm \
--with-fpm-user=php-fpm \
--with-fpm-group=php-fpm \
--enable-mysqlnd \
--with-mysqli \
--with-pdo-mysql=mysqlnd \
--with-mysql-sock=/var/lib/mysql/mysql.sock \
--with-libxml-dir \
--with-gd \
--with-jpeg-dir \
--with-png-dir \
--with-freetype-dir \
--with-iconv-dir \
--with-zlib-dir \
--enable-soap \
--enable-ftp \
--enable-mbstring \
--enable-exif \
--disable-ipv6 \
--with-pear \
--with-curl \
--with-openssl
configure 参数逐项说明:
| 参数 | 说明 |
|---|---|
--prefix=/usr/local/php |
PHP 安装主目录 |
--with-config-file-path=/usr/local/php/etc |
php.ini 的存放(搜索)路径 |
--with-apxs2=/usr/local/apache243/bin/apxs |
LAMP 关键参数:调用 Apache 的 apxs 工具把 PHP 编译成 Apache 动态模块(生成 libphp5.so / libphp7.so),并自动向 httpd.conf 写入 LoadModule。用此参数时 Apache 必须已编译且开启 --enable-so |
--enable-fpm |
LNMP 关键参数:启用 php-fpm(FastCGI 进程管理器)。可与 --with-apxs2 同时存在,编出模块和 fpm 两种 SAPI |
--with-fpm-user=php / --with-fpm-group=php |
php-fpm worker 进程的默认运行用户/组(可在 php-fpm.conf 中覆盖) |
--with-mysql=/usr/local/mysql/ |
旧 mysql_* 扩展支持,指定 MySQL 安装路径(PHP 5 专有,PHP 7 已移除) |
--with-mysqli=/usr/local/mysql/bin/mysql_config |
MySQLi 扩展(改进版 MySQL 接口,支持预处理语句、事务),指向 mysql_config 工具 |
--with-pdo-mysql=/usr/local/mysql |
PDO 的 MySQL 驱动(统一数据库抽象层,推荐现代应用使用) |
--with-pdo-mysql=mysqlnd |
用 PHP 原生 MySQL 驱动(mysqlnd)实现 PDO,无需外部 MySQL 客户端库 |
--enable-mysqlnd |
启用 PHP 原生 MySQL 驱动,内存占用更低、无需 libmysqlclient(PHP 7 推荐) |
--with-mysql-sock=/data/mysql.sock |
指定 MySQL 的 Unix socket 路径,必须与 my.cnf 中的 socket 一致,否则用 localhost 连库会失败 |
--enable-mbstring / --enable-mbstring=all |
启用多字节字符串扩展(处理中文/UTF-8 必备) |
--enable-sockets |
启用 socket 网络编程支持 |
--enable-soap |
启用 SOAP Web Service 支持 |
--enable-ftp |
启用 FTP 客户端函数 |
--enable-exif |
启用读取图片 EXIF 元信息 |
--enable-gd-native-ttf |
启用 GD 的原生 TrueType 字体支持 |
--disable-ipv6 |
禁用 IPv6 支持(纯 IPv4 环境可减小体积) |
--with-openssl |
启用 OpenSSL 支持(HTTPS 请求、加密函数、file_get_contents('https://...')) |
--with-curl |
启用 cURL 扩展(发起 HTTP 请求,调用第三方 API 必备) |
--with-readline |
启用命令行交互式编辑(php -a 时可用方向键、历史记录) |
--with-snmp |
启用 SNMP 网络管理协议支持 |
--with-gd |
启用 GD 图形库(生成验证码、缩略图、图表) |
--with-jpeg-dir / --with-png-dir / --with-freetype-dir |
GD 的 JPEG / PNG / 字体支持库路径(不带值表示自动搜索) |
--with-libxml-dir |
libxml2 库路径 |
--with-iconv-dir |
iconv 字符编码转换库路径 |
--with-zlib-dir |
zlib 压缩库路径 |
--with-mcrypt |
启用 mcrypt 加密扩展(PHP 7.2 起已移除,改用 openssl/sodium) |
--with-pear |
安装 PEAR 包管理器 |
--datarootdir=/usr/share |
数据文件(文档、man 页)根目录 |
3)编译并安装
配置结束后若看到 Thank you for using PHP. 即表示 configure 成功,可用 echo $? 确认(返回 0 为成功):
echo $?
make # 出现 "Build complete." 即成功;可用 make -j $(nproc) 加速
echo $?
make install
echo $?
编译耗时较长(10~40 分钟)。若
make报某个扩展相关错误,通常是对应的-devel包缺失,装上后重新./configure再make。修改参数重编译前建议先make clean。
4)配置 php.ini
PHP 源码包提供两份模板:
| 模板 | 用途 |
|---|---|
php.ini-development |
开发环境用。开启 display_errors、error_reporting=E_ALL,方便调试 |
php.ini-production |
生产环境用。关闭 display_errors(错误只写日志不显示给用户),关闭 expose_php 等,安全性更高 |
# 开发环境
cp php.ini-development /usr/local/php/etc/php.ini
# 生产环境(推荐)
cp /usr/src/php-5.6.37/php.ini-production /usr/local/php/etc/php.ini
vim /usr/local/php/etc/php.ini
关键参数说明:
; ── 时区 ──
; 找到 date.timezone 一行,去掉前面的 ; 注释并设置为中国时区
; 不设置会导致 date() 等时间函数报 warning,且时间不准
date.timezone = PRC
; 等效写法
;date.timezone = Asia/Shanghai
; ── 短标签 ──
; 是否允许使用 <? ... ?> 短标签(默认 Off,只能用 <?php ... ?>)
; 一些老程序(如旧版 Discuz、DedeCMS)使用短标签,页面空白时可尝试打开
short_open_tag = On
; ── 错误显示 ──
; 是否把 PHP 错误直接输出到页面。开发环境 On 便于调试,
; 生产环境必须 Off(避免暴露路径、SQL 等敏感信息)
display_errors = Off
display_startup_errors = Off
; 错误级别;开发用 E_ALL,生产建议屏蔽通知与废弃提示
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
; 生产环境把错误写进日志
log_errors = On
error_log = /var/log/php/php_error.log
; ── 上传与请求体大小(三个参数需配合,见排错章节)──
; 单个上传文件最大尺寸
upload_max_filesize = 50M
; 整个 POST 请求体最大尺寸,必须 >= upload_max_filesize
post_max_size = 60M
; 单次请求允许的最大上传文件个数
max_file_uploads = 20
; 是否允许文件上传
file_uploads = On
; 上传临时目录
upload_tmp_dir = /tmp
; ── 资源限制 ──
; 单个脚本可用内存上限,必须 >= post_max_size;大型 CMS 建议 128M~256M
memory_limit = 128M
; 脚本最大执行时间(秒),0 为不限制(CLI 默认不限)
max_execution_time = 300
; 脚本解析请求数据的最大耗时(秒)
max_input_time = 300
; 表单字段数量上限(大表单/复杂后台需调大)
max_input_vars = 3000
; ── 安全相关 ──
; 是否在 HTTP 响应头 X-Powered-By 中暴露 PHP 版本,生产建议 Off
expose_php = Off
; 限制 PHP 可访问的目录(防止跨站目录穿越),多个目录用 : 分隔
;open_basedir = /var/www/html/:/tmp/
; 禁用危险函数
disable_functions = exec,passthru,shell_exec,system,popen,proc_open
; ── 会话 ──
session.save_handler = files
session.save_path = "/var/lib/php/session"
session.gc_maxlifetime = 1440
; ── 默认字符集 ──
default_charset = "UTF-8"
mbstring.internal_encoding = UTF-8
创建日志与 session 目录:
mkdir -p /var/log/php /var/lib/php/session
chown -R apache.apache /var/log/php /var/lib/php/session # LAMP
chown -R nginx.nginx /var/log/php /var/lib/php/session # LNMP
验证 PHP 安装与配置:
/usr/local/php/bin/php -v # 查看版本
/usr/local/php/bin/php -m # 查看已编译进的扩展
/usr/local/php/bin/php -i | grep 'Loaded Configuration File' # 确认 php.ini 被读取
/usr/local/php/bin/php --ini
ln -s /usr/local/php/bin/php /usr/bin/php
3.5 配置 Apache 使 PHP 生效
编辑 Apache 主配置文件:
vim /etc/httpd/httpd.conf
1)添加 PHP MIME 类型(新增该行) —— 没有这一行,Apache 不知道 .php 该交给 PHP 处理,浏览器会下载文件或显示源码:
AddType application/x-httpd-php .php .phtml
2)把 index.php 加入默认首页 —— 找到 DirectoryIndex 一行,在 index.html 前加上 index.php:
DirectoryIndex index.php index.html
PHP 编译时通过
--with-apxs2已经自动向httpd.conf写入了LoadModule php5_module modules/libphp5.so,无需手动添加。可以搜索确认:grep -n 'LoadModule php' /etc/httpd/httpd.conf grep -n 'DirectoryIndex' /etc/httpd/httpd.conf
完整的 PHP 相关配置段(推荐写法,Apache 2.4):
<IfModule php5_module>
AddType application/x-httpd-php .php .phtml
AddType application/x-httpd-php-source .phps
DirectoryIndex index.php index.html
php_value date.timezone "PRC"
</IfModule>
3.6 整体联调验证
重启 Apache:
/usr/local/apache243/bin/apachectl configtest
/usr/local/apache243/bin/apachectl stop
/usr/local/apache243/bin/apachectl start
# 确认 php 模块已加载
/usr/local/apache243/bin/httpd -M | grep php
创建 PHP 测试文件:
vim /usr/local/apache243/htdocs/index.php
<?php phpinfo(); ?>
或:
rm -rf /usr/local/apache/htdocs/index.html
echo "<?php phpinfo(); ?>" > /usr/local/apache/htdocs/index.php
浏览器输入服务器 IP,如果能看到 PHP 信息页面,说明源码版 LAMP 环境搭建完成。检查页面上的几个关键项:
| phpinfo 项 | 期望值 |
|---|---|
| Server API | Apache 2.0 Handler(mod_php 生效) |
| Loaded Configuration File | /usr/local/php/etc/php.ini(php.ini 被正确读取) |
| mysql / mysqli / pdo_mysql | 各有独立段落(数据库扩展已编入) |
| date/date.timezone | PRC |
再用 3.4 节之外的数据库联调脚本验证 PHP → MySQL 链路(脚本见 2.7),注意源码版的 socket 路径为 /data/mysql.sock,若用 localhost 连接失败请改用 127.0.0.1。
3.7 源码版关键路径汇总
| 组件 | 路径 |
|---|---|
| Apache 安装目录 | /usr/local/apache243(或 /usr/local/apache) |
| Apache 配置文件 | /etc/httpd/httpd.conf |
| Apache 网站根目录 | /usr/local/apache243/htdocs |
| Apache 启动脚本 | /usr/local/apache243/bin/apachectl |
| Apache 模块目录 | /usr/local/apache243/modules(含 libphp5.so) |
| apr 安装目录 | /usr/local/apr-httpd |
| apr-util 安装目录 | /usr/local/apr-util-httpd |
| MySQL 安装目录 | /usr/local/mysql |
| MySQL 数据目录 | /data(或 /data/mysql、/var/lib/mysql) |
| MySQL 配置文件 | /usr/local/mysql/etc/my.cnf |
| MySQL socket | /data/mysql.sock |
| MySQL 错误日志 | /var/log/mysqld/mysql-err.log |
| MySQL 启动脚本 | /etc/init.d/mysqld |
| PHP 安装目录 | /usr/local/php |
| PHP 配置文件 | /usr/local/php/etc/php.ini |
| php-fpm 配置文件 | /usr/local/php/etc/php-fpm.conf |
| php-fpm 可执行文件 | /usr/local/php/sbin/php-fpm |
3.8 源码版常用管理命令
# Apache
/usr/local/apache243/bin/apachectl start
/usr/local/apache243/bin/apachectl stop
/usr/local/apache243/bin/apachectl restart
/usr/local/apache243/bin/apachectl graceful # 平滑重启,不断开现有连接
/usr/local/apache243/bin/apachectl configtest
# MySQL
/etc/init.d/mysqld start
/etc/init.d/mysqld stop
/etc/init.d/mysqld restart
/usr/local/mysql/bin/mysql -uroot -p
# 端口检查
lsof -i:80
lsof -i:3306
四、LNMP 搭建
LNMP 使用 Nginx 作为 Web 服务器,PHP 通过 php-fpm(FastCGI 进程管理器)解析,Nginx 与 php-fpm 之间通过 Unix Socket 或 TCP 通信。
4.1 Nginx 安装
详见 10-02-Nginx服务.md
方式一:yum 安装(需 EPEL 源)
yum install -y epel-release
yum install -y nginx
systemctl start nginx
systemctl enable nginx
systemctl status nginx
yum 版 Nginx 关键路径:
| 项目 | 路径 |
|---|---|
| 主配置文件 | /etc/nginx/nginx.conf |
| 虚拟主机配置目录 | /etc/nginx/conf.d/*.conf |
| 默认 server 片段目录 | /etc/nginx/default.d/*.conf |
| 网站根目录 | /usr/share/nginx/html/ |
| 日志目录 | /var/log/nginx/ |
| 模块目录 | /usr/share/nginx/modules/ |
方式二:源码编译安装
# 依赖
yum install -y gcc gcc-c++ pcre pcre-devel zlib zlib-devel openssl openssl-devel
# 下载(两个可用版本)
wget http://nginx.org/download/nginx-1.16.0.tar.gz
# 或
wget http://nginx.org/download/nginx-1.8.0.tar.gz
tar xf nginx-1.16.0.tar.gz -C /usr/src/
cd /usr/src/nginx-1.16.0/
最简配置(仅指定安装路径):
./configure --prefix=/usr/local/nginx
echo $?
make
echo $?
make install
echo $?
带运行用户与 SSL 模块的配置:
./configure --prefix=/usr/local/nginx \
--user=nginx --group=nginx \
--with-http_ssl_module
echo $?
make && make install
echo $?
生产环境推荐的完整 configure 参数:
./configure --prefix=/usr/local/nginx \
--sbin-path=/usr/local/nginx/sbin/nginx \
--conf-path=/usr/local/nginx/conf/nginx.conf \
--pid-path=/usr/local/nginx/logs/nginx.pid \
--error-log-path=/usr/local/nginx/logs/error.log \
--http-log-path=/usr/local/nginx/logs/access.log \
--user=nginx --group=nginx \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_realip_module \
--with-http_stub_status_module \
--with-http_gzip_static_module \
--with-http_sub_module \
--with-stream \
--with-pcre \
--with-file-aio
Nginx configure 参数说明:
| 参数 | 说明 |
|---|---|
--prefix=/usr/local/nginx |
安装主目录 |
--sbin-path / --conf-path / --pid-path |
可执行文件、配置文件、PID 文件路径 |
--error-log-path / --http-log-path |
错误日志与访问日志路径 |
--user=nginx --group=nginx |
worker 进程默认运行用户/组(应与 php-fpm 一致) |
--with-http_ssl_module |
HTTPS 支持(必备) |
--with-http_v2_module |
HTTP/2 支持 |
--with-http_realip_module |
获取真实客户端 IP(在 CDN/负载均衡后必备) |
--with-http_stub_status_module |
状态监控页(查看活动连接数、请求数) |
--with-http_gzip_static_module |
直接发送预压缩的 .gz 文件 |
--with-http_sub_module |
响应内容替换 |
--with-stream |
四层(TCP/UDP)代理与负载均衡 |
--with-file-aio |
异步文件 I/O,提升大文件传输性能 |
添加环境变量:
echo "export PATH=$PATH:/usr/local/nginx/sbin" >> /etc/profile
source /etc/profile
源码版 Nginx 的 SysV 启动脚本
vim /etc/init.d/nginx
#!/bin/bash
# chkconfig: - 30 21
# description: http service.
# Source Function Library
. /etc/init.d/functions
# Nginx Settings
NGINX_SBIN="/usr/local/nginx/sbin/nginx"
NGINX_CONF="/usr/local/nginx/conf/nginx.conf"
NGINX_PID="/usr/local/nginx/logs/nginx.pid"
RETVAL=0
prog="Nginx"
start()
{
echo -n $"Starting $prog: "
mkdir -p /dev/shm/nginx_temp
daemon $NGINX_SBIN -c $NGINX_CONF
RETVAL=$?
echo
return $RETVAL
}
stop()
{
echo -n $"Stopping $prog: "
killproc -p $NGINX_PID $NGINX_SBIN -TERM
rm -rf /dev/shm/nginx_temp
RETVAL=$?
echo
return $RETVAL
}
reload()
{
echo -n $"Reloading $prog: "
killproc -p $NGINX_PID $NGINX_SBIN -HUP
RETVAL=$?
echo
return $RETVAL
}
restart()
{
stop
start
}
configtest()
{
$NGINX_SBIN -c $NGINX_CONF -t
return 0
}
case "$1" in
start) start ;;
stop) stop ;;
reload) reload ;;
restart) restart ;;
configtest) configtest ;;
*)
echo $"Usage: $0 {start|stop|reload|restart|configtest}"
RETVAL=1
esac
exit $RETVAL
赋予执行权限并加入开机自启:
chmod 755 /etc/init.d/nginx
chkconfig --add nginx
chkconfig nginx on
Nginx 服务管理命令:
| 命令 | 作用 |
|---|---|
/etc/init.d/nginx start |
启动 |
/etc/init.d/nginx stop |
停止 |
/etc/init.d/nginx restart |
重启 |
/etc/init.d/nginx reload |
平滑重载配置(不中断连接) |
/etc/init.d/nginx configtest |
检测配置语法 |
chkconfig --add nginx |
加入系统服务 |
chkconfig nginx on |
设置开机自启动 |
源码版 Nginx 的 systemd unit(CentOS 7 可选):
vim /usr/lib/systemd/system/nginx.service
[Unit]
Description=nginx - high performance web server
After=network.target remote-fs.target nss-lookup.target
[Service]
Type=forking
PIDFile=/usr/local/nginx/logs/nginx.pid
ExecStartPre=/usr/local/nginx/sbin/nginx -t -c /usr/local/nginx/conf/nginx.conf
ExecStart=/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl start nginx
systemctl enable nginx
4.2 一次性安装 LNMP 全套软件包(yum 方式)
yum install -y epel-release
yum install -y nginx mariadb mariadb-server php php-mysql php-gd libjpeg* \
php-ldap php-odbc php-pear php-xml php-xmlrpc php-mbstring php-bcmath \
php-mhash php-fpm php-pecl-zendopcache
另一种等价写法(把 nginx 放在最后):
yum install epel-release -y
yum install mariadb mariadb-server php php-mysql php-gd libjpeg* php-ldap \
php-odbc php-pear php-xml php-xmlrpc php-mbstring php-bcmath php-mhash \
php-fpm php-pecl-zendopcache nginx -y
php-pecl-zendopcache需要 EPEL 源,用于 PHP 字节码缓存加速;php-fpm是 LNMP 的关键包,LAMP 不需要它。
4.3 MariaDB / MySQL 安装与初始化
yum 版 MariaDB(CentOS 7)
systemctl start mariadb.service
systemctl enable mariadb.service
systemctl status mariadb.service
# 登录验证
mysql
MariaDB [(none)]> quit
安全初始化(设置 root 密码等,交互全过程见 2.3):
mysql_secure_installation
CentOS 6 上用 MySQL
yum install -y mysql mysql-server
service mysqld start
chkconfig mysqld on
mysqladmin -uroot password "123"
源码/二进制版 MySQL 5.7
完整步骤见 3.3 节(下载、解压、my.cnf、创建用户与目录、mysqld --initialize、启动脚本、改密)。LNMP 场景下常用数据目录 /var/lib/mysql、socket /var/lib/mysql/mysql.sock,需与 PHP 编译时的 --with-mysql-sock 一致。
创建应用库与授权:
mysql -uroot -p
CREATE DATABASE testdb DEFAULT CHARACTER SET utf8mb4;
GRANT ALL ON testdb.* TO testuser@'localhost' IDENTIFIED BY '123456';
FLUSH PRIVILEGES;
4.4 php-fpm 安装与配置
PHP-FPM(FastCGI Process Manager)是 PHP 的 FastCGI 模式实现,简单说就是 PHP 的服务进程管理器。
1)确认 PHP 与模块已就绪(yum 版)
php -m # 查看已加载扩展
ls /usr/lib64/php/modules/ # 扩展 .so 文件位置
PHP 主配置文件为 /etc/php.ini,如需优化参数或加载源码编译的模块,修改此文件(关键参数说明见 3.4 节的 php.ini 部分)。
2)配置 php-fpm 进程池(yum 版)
yum 版 php-fpm 有两级配置:
| 文件 | 作用 |
|---|---|
/etc/php-fpm.conf |
全局配置:master 进程的 PID、错误日志、进程数上限、include=/etc/php-fpm.d/*.conf |
/etc/php-fpm.d/www.conf |
进程池(pool)配置:监听地址、运行用户、进程管理策略等,日常调优主要改这里 |
/etc/php-fpm.conf 全局段常用项:
[global]
; master 进程 PID 文件
pid = /run/php-fpm/php-fpm.pid
; php-fpm 自身的错误日志
error_log = /var/log/php-fpm/error.log
; 日志级别:alert/error/warning/notice/debug
log_level = notice
; 出现多少个子进程异常退出后重启 master(0 为关闭该保护)
emergency_restart_threshold = 10
emergency_restart_interval = 1m
; master 等待子进程响应的超时
process_control_timeout = 5s
; 全局所有 pool 的子进程总数上限(仅 dynamic/ondemand 有意义)
;process.max = 128
; 是否以守护进程运行(用 systemd 管理时可设为 no)
daemonize = yes
; 引入各进程池配置
include=/etc/php-fpm.d/*.conf
编辑进程池配置:
vim /etc/php-fpm.d/www.conf
; ── 第 12 行:进程池名称 ──
[www]
; ── 监听方式,可使用 socket 或 tcp/ip ──
; 本机连接可用 socket(性能更好),远程连接只能用 tcp/ip
listen = /var/run/php-fpm/fastcgi.socket
; TCP 方式(Nginx 与 php-fpm 不在同一台机器时必须用此方式)
;listen = 127.0.0.1:9000
;listen = 0.0.0.0:9000
; ── 第 31-33 行:socket 文件权限(用 unix socket 时必须配)──
; socket 文件的属主/属组必须让 Nginx 能读写,否则 502
listen.owner = nginx
listen.group = nginx
listen.mode = 0666
; ── 使用 TCP 监听时,限制允许连接的客户端 IP ──
listen.allowed_clients = 127.0.0.1
; ── 第 39、41 行:worker 进程运行用户和组,需与 Nginx 一致 ──
user = nginx
group = nginx
; ── 第 60 行:进程管理模式 ──
; dynamic 表示动态调整;专用服务器可设为 static
pm = dynamic
; ── 第 70 行:子进程最大数(根据系统资源调整)──
pm.max_children = 64
; ── 第 75 行:启动时默认启动的进程数(仅 dynamic 有效)──
pm.start_servers = 20
; ── 第 80 行:空闲进程最小值(仅 dynamic 有效)──
pm.min_spare_servers = 5
; ── 第 85 行:空闲进程最大值(仅 dynamic 有效)──
pm.max_spare_servers = 35
; ── 每个子进程处理多少个请求后自动重启,防止内存泄漏累积 ──
pm.max_requests = 500
; ── 第 160 行:每个子进程可打开的文件描述符数量 ──
rlimit_files = 65535
; ── 慢日志:执行超过 request_slowlog_timeout 的请求会记录调用栈 ──
slowlog = /var/log/php-fpm/www-slow.log
request_slowlog_timeout = 5s
; ── 脚本执行超时(0 为不限,交由 php.ini 的 max_execution_time 控制)──
request_terminate_timeout = 60s
; ── 第 218 行:打开 PHP 错误显示(调试用,生产环境建议关闭)──
php_flag[display_errors] = on
php_admin_value[error_log] = /var/log/php-fpm/www-error.log
php_admin_flag[log_errors] = on
php_admin_value[memory_limit] = 128M
; ── 状态监控页(配合 Nginx 的 location /fpm-status 使用)──
pm.status_path = /fpm-status
ping.path = /fpm-ping
; ── session 与上传临时目录 ──
php_value[session.save_handler] = files
php_value[session.save_path] = /var/lib/php/session
php_value[soap.wsdl_cache_dir] = /var/lib/php/wsdlcache
关键配置项说明表
| 配置项 | 示例取值 | 说明 |
|---|---|---|
listen |
/var/run/php-fpm/fastcgi.socket 或 127.0.0.1:9000 |
php-fpm 的监听地址,必须与 Nginx 的 fastcgi_pass 完全一致 |
listen.owner / listen.group |
nginx |
socket 文件属主/属组,保证 Nginx 有权访问(不匹配 → 502) |
listen.mode |
0660 / 0666 |
socket 文件权限位 |
listen.allowed_clients |
127.0.0.1 |
TCP 模式下允许连接的客户端 IP 白名单 |
user / group |
nginx |
worker 进程运行身份,需与 Nginx 一致以便读取网站文件 |
pm |
static / dynamic / ondemand |
进程管理策略,详见下表 |
pm.max_children |
64 |
最大 worker 进程数,直接决定 PHP 并发处理能力与内存占用上限 |
pm.start_servers |
20 |
dynamic 模式下启动时创建的进程数 |
pm.min_spare_servers |
5 |
dynamic 模式下保持的最少空闲进程数,低于此值时新建进程 |
pm.max_spare_servers |
35 |
dynamic 模式下允许的最多空闲进程数,高于此值时回收进程 |
pm.process_idle_timeout |
10s |
ondemand 模式下空闲多久后杀掉进程 |
pm.max_requests |
500 |
每个 worker 处理 N 个请求后自杀重生,规避第三方扩展的内存泄漏 |
rlimit_files |
65535 |
子进程的文件描述符上限 |
slowlog |
/var/log/php-fpm/www-slow.log |
慢请求日志文件路径 |
request_slowlog_timeout |
5s |
超过该时间的请求写入慢日志(含 PHP 调用栈,定位慢代码利器);0 表示关闭 |
request_terminate_timeout |
60s |
强制终止超时请求,防止进程被卡死请求长期占用 |
php_flag[display_errors] |
on |
在页面中显示 PHP 错误(调试用,生产建议关闭) |
php_admin_value[xxx] |
— | 以管理员方式设置 php.ini 参数,脚本内 ini_set() 无法覆盖 |
php_value[xxx] |
— | 设置 php.ini 参数,脚本内可用 ini_set() 覆盖 |
pm.status_path |
/fpm-status |
php-fpm 运行状态页路径 |
pm 三种进程管理方式对比
| 模式 | 行为 | 内存 | 适用场景 |
|---|---|---|---|
static |
启动时就创建 pm.max_children 个进程并固定不变,不增不减 |
占用恒定且较高 | 内存充足的专用 PHP 服务器;没有进程创建/销毁开销,响应最快、最稳定,生产高负载首选 |
dynamic |
启动 pm.start_servers 个进程,按负载在 pm.min_spare_servers ~ pm.max_children 之间动态增减 |
随负载浮动 | 默认且最通用;与其它服务混跑、负载波动大的场景 |
ondemand |
启动时不创建任何 worker,收到请求才创建,空闲 pm.process_idle_timeout 后销毁 |
空闲时几乎为 0 | 内存紧张的小内存 VPS、访问量很低的站点、大量共享主机的多 pool 场景;缺点是冷启动有延迟 |
使用 static 模式时,pm.start_servers / pm.min_spare_servers / pm.max_spare_servers 均被忽略:
pm = static
pm.max_children = 50
pm.max_requests = 1000
使用 ondemand 模式的配置示例:
pm = ondemand
pm.max_children = 30
pm.process_idle_timeout = 10s
pm.max_requests = 500
3)设置系统文件描述符限制
ulimit -n
# 1024
ulimit -SHn 65535
echo "ulimit -SHn 65535" >> /etc/rc.local
chmod +x /etc/rc.d/rc.local # CentOS 7 需要给 rc.local 执行权限
永久生效(推荐写进 limits):
cat >> /etc/security/limits.conf <<'EOF'
* soft nofile 65535
* hard nofile 65535
EOF
4)创建目录并修正权限
chown nginx.nginx /var/run/php-fpm/
chown -R nginx.nginx /var/run/php-fpm/
mkdir -p /var/log/php-fpm /var/lib/php/session
chown -R nginx.nginx /var/log/php-fpm /var/lib/php/session
5)启动 php-fpm 并设置开机自启(yum 版)
# CentOS 7
systemctl start php-fpm.service
systemctl enable php-fpm.service
systemctl status php-fpm.service
# CentOS 6
service php-fpm start
chkconfig php-fpm on
验证:
ps -ef | grep php-fpm
ls -l /var/run/php-fpm/fastcgi.socket
netstat -tunlp | grep 9000 # TCP 模式时
CentOS 6 与 7 的 php-fpm 差异:CentOS 6 默认 PHP 5.3,
php-fpm需从 EPEL 或 remi 源安装,服务用service php-fpm start+chkconfig php-fpm on管理,配置文件同为/etc/php-fpm.conf与/etc/php-fpm.d/www.conf;CentOS 7 默认 PHP 5.4,php-fpm在基础仓库中,用systemctl管理。CentOS 6 的 php-fpm 默认listen = 127.0.0.1:9000(TCP),CentOS 7 的部分版本默认 socket,配置前先确认。
6)源码版 php-fpm 配置
源码编译时必须带 --enable-fpm(参数说明见 3.4)。安装完成后手工创建 php-fpm 配置:
# 先放好 php.ini
cp /usr/src/php-5.6.37/php.ini-production /usr/local/php/etc/php.ini
# 或
cp /usr/src/php-5.6.40/php.ini-production /usr/local/php/etc/php.ini
vim /usr/local/php/etc/php-fpm.conf
[global]
pid = /var/run/php/php-fpm.pid
error_log = /var/log/php/php-fpm_err.log
[www]
listen = /tmp/php-fpm.sock
listen.mode = 666
user = nginx
group = nginx
pm = dynamic
pm.max_children = 50
pm.start_servers = 20
pm.min_spare_servers = 5
pm.max_spare_servers = 35
pm.max_requests = 500
rlimit_files = 1024
源码包也提供了模板
/usr/local/php/etc/php-fpm.conf.default,可cp php-fpm.conf.default php-fpm.conf后再改。
创建目录、用户并授权:
mkdir -p /var/log/php /var/run/php
useradd -s /usr/sbin/nologin nginx
# 或规范建法
groupadd nginx
useradd -r -s /usr/sbin/nologin -g nginx nginx
chown -R nginx.nginx /var/log/php /var/run/php
拷贝并修改启动脚本:
cp /usr/src/php-5.6.37/sapi/fpm/init.d.php-fpm /etc/init.d/php-fpm
chmod 755 /etc/init.d/php-fpm
vim /etc/init.d/php-fpm
将脚本中的 php_fpm_PID 改为配置文件里的 pid 路径:
php_fpm_PID=/var/run/php/php-fpm.pid
也可用 sed 一步替换:
sed -i 's#^php_fpm_PID=.*#php_fpm_PID=/var/run/php/php-fpm.pid#' /etc/init.d/php-fpm
测试配置语法:
/usr/local/php/sbin/php-fpm -t
... /usr/local/php/etc/php-fpm.conf test is successful
启动 php-fpm:
/etc/init.d/php-fpm start
# 输出:Starting php-fpm done
ps -ef | grep php-fpm
chkconfig --add php-fpm
chkconfig php-fpm on
源码版 php-fpm 的 systemd unit(CentOS 7 可选):
vim /usr/lib/systemd/system/php-fpm.service
[Unit]
Description=The PHP FastCGI Process Manager
After=syslog.target network.target
[Service]
Type=forking
PIDFile=/var/run/php/php-fpm.pid
ExecStart=/usr/local/php/sbin/php-fpm --fpm-config /usr/local/php/etc/php-fpm.conf
ExecReload=/bin/kill -USR2 $MAINPID
PrivateTmp=true
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl start php-fpm
systemctl enable php-fpm
php-fpm 常用信号:
| 信号 | 命令 | 作用 |
|---|---|---|
SIGINT / SIGTERM |
kill -INT <master_pid> |
立即终止 |
SIGQUIT |
kill -QUIT <master_pid> |
平滑停止(处理完当前请求再退出) |
SIGUSR1 |
kill -USR1 <master_pid> |
重新打开日志文件(日志切割后用) |
SIGUSR2 |
kill -USR2 <master_pid> |
平滑重载配置并重启所有 worker |
4.5 Nginx 配置解析 PHP(fastcgi_pass)
Nginx 配置文件的基本结构
全局配置(运行用户、进程数、日志、PID 等)
events {
事件(并发数、网络 I/O 模型)
}
http {
所有 server 的全局配置
server {
虚拟主机一
}
server {
虚拟主机二
}
}
对应的骨架写法:
# 全局配置
user nginx;
worker_processes auto;
events {
worker_connections 1024;
}
http {
# 针对所有 server 的全局配置
server {
# 虚拟主机一
}
server {
# 虚拟主机二
}
}
yum 版完整配置示例(/etc/nginx/nginx.conf)
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 65535;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/conf.d/*.conf;
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name 10.1.1.2;
root /usr/share/nginx/html;
index index.php index.html;
include /etc/nginx/default.d/*.conf;
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
# PHP 解析配置
location ~ \.php$ {
fastcgi_pass unix:/var/run/php-fpm/fastcgi.socket;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
}
源码版完整配置示例(/usr/local/nginx/conf/nginx.conf)
user nginx;
worker_processes auto;
error_log logs/error.log;
pid logs/nginx.pid;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log logs/access.log main;
sendfile on;
keepalive_timeout 65;
server {
listen 80;
server_name localhost;
index index.html index.htm index.php;
root /usr/local/nginx/html;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/tmp/php-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /usr/local/nginx/html$fastcgi_script_name;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
fastcgi_pass的 socket 路径必须与源码版php-fpm.conf里的listen = /tmp/php-fpm.sock保持一致。
location 段各指令逐项说明
| 配置项 | 说明 |
|---|---|
root |
网站根目录。yum 版默认 /usr/share/nginx/html,源码版默认 /usr/local/nginx/html |
index |
默认首页顺序,index.php 放在前面以优先解析 PHP |
location ~ \.php$ |
正则匹配所有以 .php 结尾的请求,交给 FastCGI 处理(~ 表示区分大小写的正则;~* 为不区分大小写) |
fastcgi_pass |
php-fpm 的监听地址,必须与 www.conf / php-fpm.conf 中的 listen 完全一致。socket 写 unix:/path/to.sock,TCP 写 127.0.0.1:9000 |
fastcgi_index index.php |
当请求以 / 结尾时,自动补上的默认脚本名 |
fastcgi_param SCRIPT_FILENAME |
告知 php-fpm 要执行的脚本真实绝对路径。$document_root$fastcgi_script_name 缺一不可,写错会报 File not found |
include fastcgi_params |
引入标准 FastCGI 参数集合 |
try_files $uri =404 |
建议加上:URI 对应文件不存在时直接返回 404,可防止 PHP 文件解析漏洞(如 xx.jpg/xx.php) |
fastcgi_params 与 fastcgi.conf 的区别
两个文件都在 Nginx 的 conf 目录下,内容几乎一致,唯一差别是 fastcgi.conf 多了一行 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;。
| 文件 | 是否自带 SCRIPT_FILENAME | 用法 |
|---|---|---|
fastcgi_params |
❌ 不含 | 必须自己额外写一行 fastcgi_param SCRIPT_FILENAME ...,否则 PHP 收不到脚本路径,报 No input file specified 或 File not found |
fastcgi.conf |
✅ 已含 | 直接 include fastcgi.conf; 即可,无需再写 SCRIPT_FILENAME(Nginx 0.8.30 起提供) |
三种等价写法:
# 写法一:include fastcgi_params + 手写 SCRIPT_FILENAME(最常见、最通用)
location ~ \.php$ {
fastcgi_pass unix:/var/run/php-fpm/fastcgi.socket;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# 写法二:直接 include fastcgi.conf(更简洁)
location ~ \.php$ {
fastcgi_pass unix:/var/run/php-fpm/fastcgi.socket;
fastcgi_index index.php;
include fastcgi.conf;
}
# 写法三:SCRIPT_FILENAME 写绝对路径(root 变更时容易忘改,不推荐)
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/tmp/php-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /usr/local/nginx/html$fastcgi_script_name;
}
注意顺序坑:如果先写
fastcgi_param SCRIPT_FILENAME ...再include fastcgi.conf;,后者会重复设置该参数导致混乱。同时include fastcgi_params;与自定义参数的相对顺序不影响结果(后设置的生效),但建议统一把include放在最前或最后并保持全站一致。
生产环境增强版 PHP location(含超时、缓冲区、安全加固)
location ~ \.php$ {
# 防止解析漏洞:文件不存在直接 404,不交给 PHP
try_files $uri =404;
fastcgi_pass unix:/var/run/php-fpm/fastcgi.socket;
fastcgi_index index.php;
include fastcgi.conf;
# 超时设置
fastcgi_connect_timeout 60s; # 与 php-fpm 建立连接的超时
fastcgi_send_timeout 120s; # 向 php-fpm 发送请求的超时
fastcgi_read_timeout 120s; # 等待 php-fpm 返回响应的超时(504 相关)
# 缓冲区设置(响应头较大时避免 upstream sent too big header 错误)
fastcgi_buffer_size 64k;
fastcgi_buffers 8 64k;
fastcgi_busy_buffers_size 128k;
fastcgi_temp_file_write_size 128k;
# 隐藏 PHP 版本
fastcgi_hide_header X-Powered-By;
}
# 禁止访问敏感文件
location ~ /\.(ht|git|svn) {
deny all;
}
# 上传目录禁止执行 PHP(防止上传木马被执行)
location ~ ^/(uploads|data|attachment)/.*\.(php|php5|phtml)$ {
deny all;
}
# php-fpm 状态监控(仅允许内网访问)
location ~ ^/(fpm-status|fpm-ping)$ {
allow 127.0.0.1;
allow 192.168.1.0/24;
deny all;
fastcgi_pass unix:/var/run/php-fpm/fastcgi.socket;
include fastcgi.conf;
}
Unix Socket 与 TCP 的选择
| 方式 | 写法 | 优点 | 缺点 / 适用 |
|---|---|---|---|
| Unix Socket | listen = /var/run/php-fpm/fastcgi.socketfastcgi_pass unix:/var/run/php-fpm/fastcgi.socket; |
不走网络协议栈,性能约高 5%~15%,无端口占用 | 仅限同一台机器;需注意 socket 文件的属主/权限(listen.owner/listen.group/listen.mode),配错就是 502;高并发下可能出现 connect() to unix:... failed (11: Resource temporarily unavailable),需调大 listen.backlog 与内核 net.core.somaxconn |
| TCP | listen = 127.0.0.1:9000fastcgi_pass 127.0.0.1:9000; |
支持跨机器(Nginx 与 PHP 分离部署)、便于接入 upstream 做多台 PHP 负载均衡 | 有 TCP 协议栈开销;需注意 listen.allowed_clients 白名单与防火墙 |
跨机器/多 PHP 节点的 upstream 写法:
upstream php_backend {
server 192.168.1.11:9000 weight=1 max_fails=2 fail_timeout=30s;
server 192.168.1.12:9000 weight=1 max_fails=2 fail_timeout=30s;
keepalive 32;
}
server {
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass php_backend;
fastcgi_index index.php;
include fastcgi.conf;
}
}
4.6 启动 Nginx 并测试
# 语法检查
nginx -t
/usr/local/nginx/sbin/nginx -t
nginx: the configuration file /usr/local/nginx/conf/nginx.conf syntax is ok
nginx: configuration file /usr/local/nginx/conf/nginx.conf test is successful
启动(yum 版):
systemctl start nginx.service
systemctl enable nginx.service
systemctl status nginx.service
# 验证
curl 127.0.0.1
lsof -i:80
启动(源码版):
nginx
# 或
/etc/init.d/nginx start
lsof -i:80
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 60709 root 6u IPv4 208102 0t0 TCP *:http (LISTEN)
nginx 60710 nginx 6u IPv4 208102 0t0 TCP *:http (LISTEN)
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 57434 root 6u IPv4 176424 0t0 TCP *:http (LISTEN)
nginx 57435 nobody 6u IPv4 176424 0t0 TCP *:http (LISTEN)
注意:确保 80 端口未被 httpd 等其它服务占用。若之前搭过 LAMP,需先
systemctl stop httpd && systemctl disable httpd。
浏览器访问 IP,出现 Welcome to nginx! 即表示 Nginx 安装成功。
4.7 整体联调与 phpinfo 验证
创建 PHP 测试页
# yum 版
vim /usr/share/nginx/html/test.php
<?php
phpinfo();
?>
或缩写形式:
<?php
phpinfo();
?>
# yum 版:清空默认页并创建 index.php
rm -rf /usr/share/nginx/html/*
echo "<?php phpinfo(); ?>" > /usr/share/nginx/html/index.php
# 源码版
rm -rf /usr/local/nginx/html/*
echo "<?php phpinfo(); ?>" > /usr/local/nginx/html/index.php
# 或只删默认首页
cd /usr/local/nginx/html
rm -rf index.html
echo "<?php phpinfo(); ?>" > index.php
浏览器验证
http://10.1.1.2/—— 看到 Nginx 默认页面(或 index.php 的 PHP 信息页)http://10.1.1.2/test.php—— 看到 PHP 信息页面
看到 PHP 配置信息页面即表示 LNMP 环境搭建完成。重点核对 phpinfo 页上的几项:
| phpinfo 项 | 期望值 | 说明 |
|---|---|---|
| Server API | FPM/FastCGI |
确认走的是 php-fpm 而非 mod_php |
| Loaded Configuration File | /etc/php.ini 或 /usr/local/php/etc/php.ini |
php.ini 被正确加载 |
| DOCUMENT_ROOT | 与 Nginx root 一致 |
SCRIPT_FILENAME 拼装正确的前提 |
| SCRIPT_FILENAME | <root>/test.php |
与实际文件路径一致 |
| mysqli / pdo_mysql | 存在独立段落 | 数据库扩展就绪 |
| Zend OPcache | 存在 | 字节码缓存已启用 |
访问不存在的页面会触发 404 错误页面(
/usr/share/nginx/html/404.html),服务器错误会显示 50x 页面。Nginx 默认错误页面位于/usr/share/nginx/html/:404.html 50x.html
数据库连接联调
使用 2.7 节的 mysqltest.php 脚本,放到 Nginx 的网站根目录:
vim /usr/share/nginx/html/mysqltest.php
<?php
$link = mysqli_connect('localhost', 'testuser', '123456', 'testdb');
if (!$link) {
die('连接失败: ' . mysqli_connect_error());
}
echo '数据库连接成功!';
mysqli_close($link);
?>
浏览器访问该页面,显示"数据库连接成功!"即表示 Nginx → php-fpm → MariaDB 全链路通畅。测试完成后建议删除该脚本。
命令行方式快速验证 FastCGI 链路(不经浏览器):
curl -I http://127.0.0.1/test.php # 期望 200 且 Content-Type: text/html
curl http://127.0.0.1/test.php | head -20
4.8 动静分离配置
动静分离指静态资源(图片、CSS、JS、字体、下载文件)由 Nginx 直接读磁盘返回并设置浏览器缓存,动态请求(.php)才转交 php-fpm。这样能显著降低 PHP 进程压力、减少带宽消耗、提升响应速度。
server {
listen 80;
server_name www.example.com;
root /usr/share/nginx/html;
index index.php index.html index.htm;
charset utf-8;
access_log /var/log/nginx/example_access.log main;
error_log /var/log/nginx/example_error.log;
# ── 动态请求:交给 php-fpm ──
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/var/run/php-fpm/fastcgi.socket;
fastcgi_index index.php;
include fastcgi.conf;
fastcgi_read_timeout 120s;
}
# ── 静态图片/多媒体:Nginx 直接返回,缓存 30 天 ──
location ~* \.(gif|jpg|jpeg|png|bmp|webp|ico|svg|mp4|mp3|flv|swf)$ {
expires 30d;
access_log off; # 静态资源不记访问日志,减少 IO
add_header Cache-Control "public, max-age=2592000";
try_files $uri =404;
}
# ── CSS / JS:缓存 12 小时(更新较频繁)──
location ~* \.(css|js)$ {
expires 12h;
access_log off;
add_header Cache-Control "public";
gzip_static on; # 优先发送预压缩的 .gz 文件
}
# ── 字体文件:缓存 1 年,并允许跨域 ──
location ~* \.(woff|woff2|ttf|eot|otf)$ {
expires 1y;
access_log off;
add_header Access-Control-Allow-Origin *;
}
# ── 下载类文件:不缓存,强制下载 ──
location ~* \.(zip|rar|gz|tar|pdf|doc|docx|xls|xlsx)$ {
expires -1;
add_header Content-Disposition "attachment";
}
# ── 静态资源与目录:优先找文件,其次目录,最后交给 index.php(伪静态入口)──
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# ── 禁止访问隐藏文件与备份文件 ──
location ~ /\.(ht|git|svn|env) {
deny all;
}
location ~* \.(bak|sql|log|swp|old)$ {
deny all;
}
}
expires 指令取值说明:
| 取值 | 效果 |
|---|---|
expires 30d; |
30 天后过期(d 天、h 小时、m 分钟、y 年) |
expires 1y; |
一年,适合带版本号/哈希文件名的静态资源 |
expires -1; |
立即过期,Cache-Control: no-cache |
expires epoch; |
设为 1970-01-01,等同于永不缓存 |
expires max; |
设为 2037 年,永久缓存 |
expires off; |
不添加 Expires 头,由上层/浏览器默认策略决定 |
更彻底的动静分离:把静态资源单独放到另一台服务器(或对象存储/CDN),Nginx 通过 proxy_pass 或直接 DNS 拆分到不同域名(如 static.example.com),PHP 服务器只处理动态请求。
五、应用部署实战
以下案例在 LAMP 与 LNMP 上均可部署,区别仅在于网站根目录与运行用户。
各环境的网站根目录与运行用户对照表:
| 环境 | 网站根目录 | 运行用户 |
|---|---|---|
| yum 版 Apache(LAMP) | /var/www/html/ |
apache |
| 源码版 Apache(LAMP) | /usr/local/apache/htdocs/ 或 /usr/local/apache243/htdocs/ |
apache(或 daemon) |
| yum 版 Nginx(LNMP) | /usr/share/nginx/html/ |
nginx |
| 源码版 Nginx(LNMP) | /usr/local/nginx/html/ |
nginx |
5.1 部署 Discuz! 论坛
Discuz! 是基于 PHP + MySQL 的论坛程序。
1)安装 git 并下载源码
新版本 Discuz! 源码托管在 Git 仓库上,需要先安装 git:
yum install -y git
git clone https://gitee.com/ComsenzDiscuz/DiscuzX.git
2)调整目录结构
Discuz! 的仓库根目录下有 upload/(程序主体)、utility/(工具)等,实际要部署的是 upload/ 里的内容。
方式 A:先在原地把 upload/ 内容提到上层,再整体移走:
mv DiscuzX/upload/* DiscuzX/
方式 B:直接把 upload/ 内容移到网站根目录(按实际安装方式选择其一):
# 源码版 Apache(LAMP)
mv DiscuzX/upload/* /usr/local/apache/htdocs/
# yum 版 Apache(LAMP)
mv DiscuzX/upload/* /var/www/html/
# 源码版 Nginx(LNMP)
mv DiscuzX/upload/* /usr/local/nginx/html/
# yum 版 Nginx(LNMP)
mv DiscuzX/upload/* /usr/share/nginx/html/
方式 C:源码版 Apache 的两步走法:
mv DiscuzX/* /usr/local/apache243/htdocs/
cd /usr/local/apache243/htdocs
mv upload/* ./
方式 D:使用官方 ZIP 包(部署到子目录 discuz/):
mkdir /usr/share/nginx/html/discuz/
unzip Discuz_X3.3_SC_UTF8.zip -d /usr/share/nginx/html/discuz/
cd /usr/share/nginx/html/discuz/
mv upload/* .
rm -rf upload/
3)修改目录权限与属主
按 Web 服务运行用户设置属主(推荐做法):
# Apache 环境
chown -R apache.apache /usr/local/apache/htdocs/
chown -R apache.apache /var/www/html/
# Nginx 环境
chown -R nginx.nginx /usr/local/nginx/html/
chown -R nginx.nginx /usr/share/nginx/html/
chown nginx.nginx /usr/share/nginx/html/discuz -R
快速做法(仅限实验环境,生产禁用 777):
chmod 777 ./ -R
生产环境的精细权限:只有需要写入的目录才给写权限。
cd /usr/share/nginx/html/
# 目录 755,文件 644
find . -type d -exec chmod 755 {} \;
find . -type f -exec chmod 644 {} \;
# Discuz 需要可写的目录
chmod -R 777 ./config ./data ./uc_client/data ./uc_server/data
chown -R nginx.nginx ./config ./data ./uc_client/data ./uc_server/data
4)创建数据库并授权
mysql -uroot -p
# 或(密码为 123 时)
mysql -p123
示例一(库名 discuz):
CREATE DATABASE discuz;
GRANT ALL ON discuz.* TO discuz@'localhost' IDENTIFIED BY '123';
FLUSH PRIVILEGES;
示例二(库名 lnmp_discuz):
create database lnmp_discuz;
grant all on lnmp_discuz.* to 'lnmpdiscuz'@'localhost' identified by '123';
flush privileges;
quit
推荐补上字符集(避免中文乱码):
CREATE DATABASE discuz DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
5)浏览器运行安装向导
访问地址(按部署位置选择):
http://IP/DiscuzX
http://服务器IP/
http://192.168.224.11/discuz/
按向导逐步操作:检查环境 → 设置运行环境(全新安装)→ 填写数据库信息 → 创建管理员账号 → 安装完成。
数据库信息填写示例一:
| 项目 | 值 |
|---|---|
| 数据库服务器 | localhost(连不上时改 127.0.0.1) |
| 数据库名 | discuz |
| 数据库用户名 | discuz |
| 数据库密码 | 123 |
| 数据表前缀 | pre_(默认) |
| 管理员账号 | admin |
| 管理员密码 | 自定义 |
数据库信息填写示例二:
- 数据库名:
lnmp_discuz - 数据库用户名:
lnmpdiscuz - 数据库密码:
123 - 管理员密码:
123
6)安装完成验证
http://服务器IP/forum.php
http://192.168.224.11/discuz/forum.php
能看到论坛首页即部署成功。安装完成后建议删除 install/ 目录或其中的 index.php,防止被重装:
mv /usr/share/nginx/html/install /usr/share/nginx/html/install.bak
7)Discuz 部署常见报错
| 报错 | 原因与解决 |
|---|---|
| 提示无法连接数据库 / socket 找不到 | 核对数据库地址、端口、用户名、密码;把 localhost 改为 127.0.0.1。原因是 localhost 会走 Unix socket,而 127.0.0.1 走 TCP 连接,能避开 socket 路径不一致的问题 |
./config/config_global.php 不可写 |
目录属主/权限不对:chown -R nginx.nginx ./config 且 chmod 755(或临时 777) |
| 安装向导页面空白 | PHP 版本过高(Discuz X3.2 及以前不兼容 PHP 7);或 display_errors=Off 掩盖了报错,先打开错误显示定位 |
| 页面样式错乱、图片不显示 | 静态资源被 Nginx 的 location 规则拦截;或 root 路径不对 |
| 附件/头像上传失败 | data/attachment、uc_server/data/avatar 目录不可写;或 client_max_body_size / upload_max_filesize 太小 |
| 中文乱码 | 库/表字符集与程序编码不一致;下载 UTF8 版本源码,库用 utf8mb4 |
8)Discuz 的 Nginx 伪静态规则
location / {
rewrite ^([^\.]*)/topic-(.+)\.html$ $1/portal.php?mod=topic&topic=$2 last;
rewrite ^([^\.]*)/article-([0-9]+)-([0-9]+)\.html$ $1/portal.php?mod=view&aid=$2&page=$3 last;
rewrite ^([^\.]*)/forum-(\w+)-([0-9]+)\.html$ $1/forum.php?mod=forumdisplay&fid=$2&page=$3 last;
rewrite ^([^\.]*)/thread-([0-9]+)-([0-9]+)-([0-9]+)\.html$ $1/forum.php?mod=viewthread&tid=$2&extra=page%3D$4&page=$3 last;
rewrite ^([^\.]*)/group-([0-9]+)-([0-9]+)\.html$ $1/forum.php?mod=group&fid=$2&page=$3 last;
rewrite ^([^\.]*)/space-(username|uid)-(.+)\.html$ $1/home.php?mod=space&$2=$3 last;
rewrite ^([^\.]*)/blog-([0-9]+)-([0-9]+)\.html$ $1/home.php?mod=space&uid=$2&do=blog&id=$3 last;
rewrite ^([^\.]*)/archiver/(fid|tid)-([0-9]+)\.html$ $1/archiver/index.php?action=$2&value=$3 last;
if (!-e $request_filename) {
return 404;
}
}
Apache 环境下 Discuz 的伪静态由自带的 .htaccess 处理,需确保:
<Directory "/var/www/html">
AllowOverride All
Require all granted
</Directory>
且 mod_rewrite 已加载(httpd -M | grep rewrite)。
5.2 部署 WordPress 博客
1)下载并解压
cd /usr/src/
wget https://cn.wordpress.org/latest-zh_CN.tar.gz
tar xf latest-zh_CN.tar.gz
# 部署到网站根目录
cp -r wordpress/* /usr/share/nginx/html/ # LNMP
cp -r wordpress/* /var/www/html/ # LAMP
2)目录权限与属主
# LNMP
chown -R nginx.nginx /usr/share/nginx/html/
# LAMP
chown -R apache.apache /var/www/html/
# 目录 755,文件 644
cd /usr/share/nginx/html/
find . -type d -exec chmod 755 {} \;
find . -type f -exec chmod 644 {} \;
# 上传目录必须可写
mkdir -p wp-content/uploads
chown -R nginx.nginx wp-content/uploads
chmod -R 775 wp-content/uploads
3)创建数据库与授权
CREATE DATABASE wordpress DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL ON wordpress.* TO 'wpuser'@'localhost' IDENTIFIED BY 'WpPass_123';
FLUSH PRIVILEGES;
4)修改配置文件
cd /usr/share/nginx/html/
cp wp-config-sample.php wp-config.php
vim wp-config.php
<?php
// ** MySQL 设置 ** //
define( 'DB_NAME', 'wordpress' );
define( 'DB_USER', 'wpuser' );
define( 'DB_PASSWORD', 'WpPass_123' );
define( 'DB_HOST', 'localhost' ); // 连不上时改为 127.0.0.1
define( 'DB_CHARSET', 'utf8mb4' );
define( 'DB_COLLATE', '' );
// 数据表前缀(同库装多站时改这里)
$table_prefix = 'wp_';
// 安全密钥:到 https://api.wordpress.org/secret-key/1.1/salt/ 生成后粘贴替换
define( 'AUTH_KEY', '把这里换成随机字符串' );
define( 'SECURE_AUTH_KEY', '把这里换成随机字符串' );
define( 'LOGGED_IN_KEY', '把这里换成随机字符串' );
define( 'NONCE_KEY', '把这里换成随机字符串' );
define( 'AUTH_SALT', '把这里换成随机字符串' );
define( 'SECURE_AUTH_SALT', '把这里换成随机字符串' );
define( 'LOGGED_IN_SALT', '把这里换成随机字符串' );
define( 'NONCE_SALT', '把这里换成随机字符串' );
// 调试模式:排错时打开
define( 'WP_DEBUG', false );
// 直接文件写入方式,避免安装插件时要求 FTP 账号
define( 'FS_METHOD', 'direct' );
5)Web 安装向导
浏览器访问 http://服务器IP/,进入 WordPress 安装界面,填写:
- 站点标题
- 用户名、密码(管理员)
- 电子邮箱
点击"安装 WordPress",随后用管理员账号登录 http://服务器IP/wp-admin/。
6)WordPress 伪静态规则(Nginx)
WordPress 的"固定链接"功能需要伪静态支持:
location / {
try_files $uri $uri/ /index.php?$args;
}
# 后台与登录页限流(可选,防暴力破解)
location = /wp-login.php {
limit_req zone=one burst=5 nodelay;
fastcgi_pass unix:/var/run/php-fpm/fastcgi.socket;
include fastcgi.conf;
}
# 禁止执行上传目录中的 PHP
location ~* /wp-content/uploads/.*\.php$ {
deny all;
}
# 保护配置文件
location = /wp-config.php {
deny all;
}
Apache 环境下由 .htaccess 处理(需 AllowOverride All + mod_rewrite):
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
7)常见安装报错
| 报错 | 解决 |
|---|---|
建立数据库连接时出错 |
核对 wp-config.php 中的库名/用户/密码/主机;localhost 改 127.0.0.1;确认 php-mysql 扩展已装 |
| 安装插件/主题时要求 FTP 账号 | 目录属主不是 Web 运行用户;chown -R nginx.nginx 并在 wp-config.php 加 define('FS_METHOD','direct'); |
| 上传媒体文件失败 | wp-content/uploads 不可写;或 upload_max_filesize / client_max_body_size 太小 |
| 固定链接改成非默认后 404 | 未配置伪静态(try_files ... /index.php?$args)或 Apache 未开 mod_rewrite / AllowOverride All |
| 后台白屏 | 内存不足,php.ini 调大 memory_limit = 256M;或插件冲突,重命名 wp-content/plugins 目录排查 |
提示 PHP 版本过低 |
用 remi/webtatic 源升级到 PHP 7.4+ |
5.3 部署 phpMyAdmin(数据库 Web 管理工具)
1)安装
yum 方式(需 EPEL 源):
yum install -y epel-release
yum install -y phpMyAdmin
yum 版会自动生成 /etc/httpd/conf.d/phpMyAdmin.conf(Apache 环境),程序目录为 /usr/share/phpMyAdmin。
源码方式:
cd /usr/src/
wget https://files.phpmyadmin.net/phpMyAdmin/5.2.1/phpMyAdmin-5.2.1-all-languages.zip
unzip phpMyAdmin-5.2.1-all-languages.zip
mv phpMyAdmin-5.2.1-all-languages /usr/share/nginx/html/phpmyadmin
chown -R nginx.nginx /usr/share/nginx/html/phpmyadmin
2)修改配置文件
cd /usr/share/nginx/html/phpmyadmin
cp config.sample.inc.php config.inc.php
vim config.inc.php
<?php
// blowfish 密钥,必须是 32 位随机字符串,否则登录报警告
$cfg['blowfish_secret'] = 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6';
$i = 0;
$i++;
$cfg['Servers'][$i]['auth_type'] = 'cookie'; // cookie 登录方式
$cfg['Servers'][$i]['host'] = '127.0.0.1'; // 数据库地址,socket 有问题时用 IP
$cfg['Servers'][$i]['port'] = '3306';
$cfg['Servers'][$i]['compress'] = false;
$cfg['Servers'][$i]['AllowNoPassword'] = false; // 禁止空密码登录
// 上传/导入临时目录
$cfg['UploadDir'] = '';
$cfg['SaveDir'] = '';
$cfg['TempDir'] = '/tmp/';
创建临时目录(导入大文件、生成缓存需要):
mkdir -p /usr/share/nginx/html/phpmyadmin/tmp
chown -R nginx.nginx /usr/share/nginx/html/phpmyadmin/tmp
chmod 700 /usr/share/nginx/html/phpmyadmin/tmp
3)Apache 环境的访问控制(yum 版)
vim /etc/httpd/conf.d/phpMyAdmin.conf
Alias /phpMyAdmin /usr/share/phpMyAdmin
Alias /phpmyadmin /usr/share/phpMyAdmin
<Directory /usr/share/phpMyAdmin/>
AddDefaultCharset UTF-8
<IfModule mod_authz_core.c>
# Apache 2.4:只允许指定网段访问
Require ip 127.0.0.1
Require ip 192.168.1.0/24
</IfModule>
<IfModule !mod_authz_core.c>
# Apache 2.2 写法
Order Deny,Allow
Deny from All
Allow from 127.0.0.1
Allow from 192.168.1.0/24
</IfModule>
</Directory>
systemctl restart httpd
4)Nginx 环境的访问控制
location /phpmyadmin {
# 仅允许内网访问(phpMyAdmin 是高危入口,务必限制)
allow 127.0.0.1;
allow 192.168.1.0/24;
deny all;
# 可选:加一层 HTTP Basic 认证
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/htpasswd;
index index.php;
try_files $uri $uri/ /phpmyadmin/index.php?$args;
}
生成 Basic 认证密码文件:
yum install -y httpd-tools
htpasswd -c /etc/nginx/htpasswd dbadmin
chmod 640 /etc/nginx/htpasswd
chown root:nginx /etc/nginx/htpasswd
5)SELinux 放行(保持 SELinux 开启时必须)
setsebool -P httpd_can_network_connect_db on
6)访问与常见报错
访问 http://服务器IP/phpmyadmin/,用数据库账号(如 root)登录。
| 报错 | 解决 |
|---|---|
The $cfg['blowfish_secret'] must be at least 32 characters |
在 config.inc.php 中填一个 32 位随机字符串 |
mysqli_real_connect(): (HY000/2002): No such file or directory |
socket 路径不对:把 host 改为 127.0.0.1 走 TCP;或修正 php.ini 的 mysqli.default_socket |
Cannot log in to the MySQL server |
账号密码错误;或 root 被限制只能本机登录(AllowNoPassword、bind-address) |
| 缺少 mbstring 扩展提示 | yum install -y php-mbstring && systemctl restart php-fpm |
| 导入 SQL 文件超过大小限制 | 调大 upload_max_filesize、post_max_size、memory_limit、max_execution_time,Nginx 侧调大 client_max_body_size |
| 页面提示"高级功能未完全配置" | 导入 sql/create_tables.sql 建立 phpmyadmin 配置存储库(可忽略) |
安全提醒:phpMyAdmin 是最常被扫描爆破的路径之一。生产环境建议:改成非默认路径别名、限制来源 IP、加 Basic 认证、启用 HTTPS,或干脆不部署(改用 SSH 隧道 + 本地客户端)。
5.4 部署 DedeCMS(织梦内容管理系统)
1)下载并解压
cd /usr/src/
wget http://updatenew.dedecms.com/base-v57/package/DedeCMS-V5.7-UTF8-SP2.tar.gz
tar xf DedeCMS-V5.7-UTF8-SP2.tar.gz
cd DedeCMS-V5.7-UTF8-SP2/
DedeCMS 压缩包内也是 uploads/ 目录存放程序主体:
cp -r uploads/* /usr/share/nginx/html/ # LNMP
cp -r uploads/* /var/www/html/ # LAMP
2)目录权限与属主
cd /usr/share/nginx/html/
chown -R nginx.nginx .
find . -type d -exec chmod 755 {} \;
find . -type f -exec chmod 644 {} \;
# DedeCMS 必须可写的目录
chmod -R 777 ./data ./uploads ./a ./install ./special ./templets
chown -R nginx.nginx ./data ./uploads ./a ./special ./templets
各目录作用:
| 目录 | 作用 | 权限要求 |
|---|---|---|
data/ |
配置、缓存、备份 | 可写(777 / 属主 + 755) |
uploads/ |
上传的图片附件 | 可写 |
a/ |
生成的静态 HTML 页面(文章列表/内容页) | 可写 |
special/ |
专题生成目录 | 可写 |
templets/ |
模板文件 | 可写(后台在线改模板时需要) |
install/ |
安装程序,装完必须删除 | 装完删除 |
dede/ |
后台管理目录,建议改名 | 只读 |
3)创建数据库与授权
CREATE DATABASE dedecms DEFAULT CHARACTER SET utf8;
GRANT ALL ON dedecms.* TO 'dedeuser'@'localhost' IDENTIFIED BY 'Dede_123';
FLUSH PRIVILEGES;
4)Web 安装向导
访问 http://服务器IP/install/index.php,依次:
- 同意授权协议
- 环境检测(PHP 版本、目录权限,红色项必须先解决)
- 填写参数:
| 项目 | 值 |
|---|---|
| 数据库主机 | localhost(或 127.0.0.1) |
| 数据库名称 | dedecms |
| 数据库用户 | dedeuser |
| 数据库密码 | Dede_123 |
| 数据表前缀 | dede_ |
| 管理员账号/密码 | 自定义 |
| 网站网址 | http://服务器IP |
- 点击"继续"完成安装
5)安装后的加固
cd /usr/share/nginx/html/
# 删除安装目录(不删会被人重装覆盖整站)
rm -rf install/
# 后台目录改名,降低被扫描风险
mv dede admin_x7k9
# 修改 data/ 目录下的敏感文件权限
chmod 644 data/common.inc.php
6)DedeCMS 的 Nginx 配置注意点
server {
listen 80;
server_name www.example.com;
root /usr/share/nginx/html;
index index.html index.php;
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/var/run/php-fpm/fastcgi.socket;
fastcgi_index index.php;
include fastcgi.conf;
# DedeCMS 生成全站静态页面时较慢,超时给足
fastcgi_read_timeout 300s;
}
# 静态生成的 HTML 页面优先直接返回
location ~* ^/a/.*\.html$ {
expires 1h;
}
# 禁止上传目录执行 PHP(DedeCMS 历史漏洞较多,此项必加)
location ~ ^/(uploads|data|a|special)/.*\.(php|php5|phtml)$ {
deny all;
}
# 保护 data 目录中的配置与备份
location ~ ^/data/(common\.inc\.php|backupdata|admin) {
deny all;
}
}
7)DedeCMS 常见安装报错
| 报错 | 解决 |
|---|---|
环境检测中 data/、uploads/ 等目录显示"不可写" |
chown -R nginx.nginx + chmod -R 777 对应目录;SELinux 开启时用 chcon -R -t httpd_sys_rw_content_t |
Deprecated: Function ereg() is deprecated 之类大量警告 |
DedeCMS 5.7 老版本对 PHP 5.4+ 兼容性差;把 php.ini 的 error_reporting 调低,或打补丁/换 SP2 及以上版本 |
PHP 7 下大量 Fatal error |
DedeCMS 5.7 SP2 之前不支持 PHP 7,需降级到 PHP 5.6 或使用官方 PHP 7 补丁版 |
| 安装后首页空白 | 未生成首页 HTML:登录后台 → 生成 → 更新主页 HTML;或 short_open_tag = On |
| 后台登录验证码不显示 | 缺少 GD 扩展:yum install -y php-gd && systemctl restart php-fpm |
| 中文乱码 | 下载的是 GBK 版而库是 utf8(或反之);保持源码版本编码与库字符集一致 |
| 上传图片提示"上传失败" | uploads/ 不可写;或 upload_max_filesize / client_max_body_size 太小 |
5.5 部署通用检查清单
无论部署哪个应用,按以下顺序检查可覆盖 90% 的问题:
# ① 三个服务都在跑
systemctl status nginx php-fpm mariadb # LNMP
systemctl status httpd mariadb # LAMP
# ② 端口/socket 都在
lsof -i:80; lsof -i:3306
ls -l /var/run/php-fpm/fastcgi.socket
# ③ 网站目录属主是 Web 运行用户
ls -ld /usr/share/nginx/html/
ps aux | grep -E 'nginx|php-fpm' | head # 确认 worker 运行用户
# ④ 数据库账号能连通
mysql -u<用户> -p<密码> -h127.0.0.1 <库名> -e "SELECT 1;"
# ⑤ PHP 扩展齐全
php -m | grep -E 'mysqli|pdo_mysql|gd|mbstring|curl|zip|xml'
# ⑥ 配置语法没问题
nginx -t
httpd -t
/usr/local/php/sbin/php-fpm -t
# ⑦ 三份日志同时盯
tail -f /var/log/nginx/error.log
tail -f /var/log/php-fpm/www-error.log
tail -f /var/log/mysql/mysql_err.log
六、性能优化
6.1 Nginx 连接与进程参数
# ── 全局段 ──
user nginx;
# worker 进程数:设为 auto 即等于 CPU 核心数;也可手写核心数
worker_processes auto;
# 把 worker 绑定到指定 CPU 核,减少上下文切换与 cache miss
worker_cpu_affinity auto;
# 每个 worker 可打开的文件描述符上限(应 >= worker_connections)
worker_rlimit_nofile 65535;
events {
# 单个 worker 的最大并发连接数
# 理论最大并发 = worker_processes × worker_connections
worker_connections 65535;
# Linux 下高效的事件模型(Nginx 通常会自动选择)
use epoll;
# 一个 worker 一次尽可能多地接受新连接
multi_accept on;
}
http {
# ── 高效文件传输 ──
sendfile on; # 零拷贝,内核态直接发送文件,不经用户态
tcp_nopush on; # 配合 sendfile,累积数据后一次性发送,减少报文数
tcp_nodelay on; # 长连接下禁用 Nagle 算法,小包立即发送,降低延迟
# ── 连接保持 ──
keepalive_timeout 65; # 长连接超时时间
keepalive_requests 1000; # 单个长连接可处理的请求数
# ── 超时 ──
client_header_timeout 15s;
client_body_timeout 15s;
send_timeout 25s;
reset_timedout_connection on; # 关闭超时连接并释放内存
# ── 缓冲区 ──
client_header_buffer_size 4k;
large_client_header_buffers 4 8k; # 大 Cookie / 长 URL 时需调大
client_body_buffer_size 128k;
client_max_body_size 100m; # 允许的最大请求体(上传大文件必调)
# ── 哈希表 ──
types_hash_max_size 2048;
server_names_hash_bucket_size 128;
# ── 打开文件缓存(静态资源多时效果显著)──
open_file_cache max=100000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
# ── 隐藏版本号 ──
server_tokens off;
}
配套的内核参数调优:
cat >> /etc/sysctl.conf <<'EOF'
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_keepalive_time = 600
net.ipv4.ip_local_port_range = 1024 65000
net.ipv4.tcp_syncookies = 1
fs.file-max = 999999
EOF
sysctl -p
6.2 Apache 连接与进程参数(MPM 调优)
Apache 2.4 有三种 MPM(多处理模块),查看当前使用的:
httpd -V | grep MPM
apachectl -V | grep -i mpm
| MPM | 模型 | 特点 | 适用 |
|---|---|---|---|
prefork |
多进程,每进程单线程 | 最稳定、隔离性最好,内存占用最高 | mod_php 必须用它(PHP 部分扩展非线程安全) |
worker |
多进程多线程 | 内存占用低于 prefork,并发更高 | 不用 mod_php 时可选 |
event |
worker 的改进,专门线程处理 keepalive 连接 | 并发能力最强,Apache 2.4 推荐 | 配合 mod_proxy_fcgi + php-fpm |
切换 MPM(编辑 /etc/httpd/conf.modules.d/00-mpm.conf,只保留一行取消注释):
#LoadModule mpm_prefork_module modules/mod_mpm_prefork.so
#LoadModule mpm_worker_module modules/mod_mpm_worker.so
LoadModule mpm_event_module modules/mod_mpm_event.so
prefork 参数(配合 mod_php):
<IfModule mpm_prefork_module>
StartServers 8 # 启动时创建的子进程数
MinSpareServers 8 # 最少空闲子进程数
MaxSpareServers 32 # 最多空闲子进程数
ServerLimit 512 # MaxRequestWorkers 的硬上限
MaxRequestWorkers 256 # 最大并发请求数(= 最大子进程数)
MaxConnectionsPerChild 10000 # 每个子进程处理 N 个连接后重启,防内存泄漏
</IfModule>
event / worker 参数:
<IfModule mpm_event_module>
StartServers 4
MinSpareThreads 75
MaxSpareThreads 250
ThreadsPerChild 25 # 每个子进程的线程数
ServerLimit 32
MaxRequestWorkers 800 # = ServerLimit × ThreadsPerChild
MaxConnectionsPerChild 10000
</IfModule>
其它 Apache 优化项(httpd.conf):
# 关闭 DNS 反查(每个请求都反查会严重拖慢)
HostnameLookups Off
# 关闭不需要的目录选项,减少磁盘 stat 调用
<Directory "/var/www/html">
Options -Indexes -FollowSymLinks +SymLinksIfOwnerMatch
# 不使用 .htaccess 时设为 None,可省掉逐级目录查找的开销
AllowOverride None
Require all granted
</Directory>
# 长连接
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5
# 隐藏版本信息
ServerTokens Prod
ServerSignature Off
# 超时
Timeout 60
# 卸载不用的模块(在 conf.modules.d/ 中注释掉对应 LoadModule)
# 如 mod_status、mod_info、mod_userdir、mod_autoindex
6.3 php-fpm 进程数按内存估算
核心公式:
pm.max_children = (可分配给 PHP 的内存) ÷ (单个 php-fpm 进程平均内存占用)
可分配给 PHP 的内存 ≈ 总内存 − 系统预留 − MySQL 占用 − Nginx 占用 − 其它服务
第一步:测出单进程实际内存占用
# 方法一:查看每个 php-fpm 进程的 RSS(单位 KB),并算平均值
ps --no-headers -o rss,cmd -C php-fpm | awk '{sum+=$1; n++} END {printf "进程数: %d 平均: %.1f MB\n", n, sum/n/1024}'
# 方法二:按进程逐个列出
ps -ylC php-fpm --sort:rss | awk '{print $8/1024" MB\t"$13}'
# 方法三:用 pmap 看更精确的独占内存(PSS)
pmap -x $(pgrep -f 'php-fpm: pool' | head -1) | tail -1
典型经验值:
| 应用类型 | 单进程内存 |
|---|---|
| 简单 API / 轻量脚本 | 15~30 MB |
| WordPress / Discuz 等常规 CMS | 40~80 MB |
| Laravel / ThinkPHP 等框架应用 | 80~150 MB |
| 图片处理 / 大数据导出 | 150~300 MB |
第二步:按内存规格估算
假设单进程 50 MB:
| 服务器内存 | MySQL 等预留 | 可给 PHP | pm.max_children 建议 |
|---|---|---|---|
| 1 GB | 约 500 MB | 约 400 MB | 8(并用 ondemand 模式) |
| 2 GB | 约 800 MB | 约 1 GB | 20 |
| 4 GB | 约 1.5 GB | 约 2.2 GB | 40 |
| 8 GB | 约 2.5 GB | 约 5 GB | 100 |
| 16 GB | 约 5 GB | 约 10 GB | 200 |
对应的 dynamic 模式配比经验(各值不要超过 max_children):
pm = dynamic
pm.max_children = 40 # 由内存算出的上限
pm.start_servers = 10 # ≈ max_children × 25%(或 CPU核数 × 4)
pm.min_spare_servers = 5 # ≈ max_children × 12%
pm.max_spare_servers = 20 # ≈ max_children × 50%
pm.max_requests = 1000 # 防内存泄漏,有泄漏的扩展可降到 500
第三步:验证是否够用
开启 php-fpm 状态页后查看:
curl 'http://127.0.0.1/fpm-status?full'
pool: www
process manager: dynamic
idle processes: 12
active processes: 8
total processes: 20
max active processes: 35
max children reached: 0 ← 关键指标!
slow requests: 3
listen queue: 0 ← 关键指标!
max listen queue: 0
判断标准:
| 指标 | 含义 | 处理 |
|---|---|---|
max children reached > 0 |
曾达到进程数上限,有请求排队(会导致 502/504) | 调大 pm.max_children(前提是内存够);否则优化代码或加机器 |
listen queue > 0 |
有连接在积压等待 | 同上;并调大 listen.backlog 与 net.core.somaxconn |
slow requests 持续增长 |
有慢脚本 | 查 slowlog 定位具体代码行 |
idle processes 长期很高 |
进程开太多,浪费内存 | 调小 pm.max_children 或改用 ondemand |
也可用命令行直接统计当前进程状态:
# 当前 php-fpm 进程数
ps -C php-fpm --no-headers | wc -l
# 慢日志分析:找出最常出现的慢函数
grep -A5 'script_filename' /var/log/php-fpm/www-slow.log | sort | uniq -c | sort -rn | head -20
6.4 开启 OPcache(字节码缓存)
OPcache 把 PHP 脚本编译后的字节码(opcode)缓存在共享内存中,免去每次请求重复解析和编译源码的开销,通常能带来 2~3 倍性能提升,是 PHP 最重要的单项优化。
安装
# PHP 5.4(CentOS 7):从 EPEL 装 zendopcache
yum install -y php-pecl-zendopcache
# PHP 5.5+ / PHP 7:OPcache 已内置,yum 版装扩展包即可
yum install -y php-opcache
yum install -y php72w-opcache # webtatic PHP 7.2
# 源码编译:configure 时加上(PHP 5.5+ 默认已编入,可显式启用)
./configure ... --enable-opcache
配置(/etc/php.d/10-opcache.ini 或 php.ini 末尾)
[opcache]
zend_extension=opcache.so
; 是否启用(CLI 单独控制)
opcache.enable=1
opcache.enable_cli=0
; ── 内存 ──
; 共享内存大小(MB)。中小站点 128,大型站点 256~512
opcache.memory_consumption=256
; 字符串驻留池大小(MB),存放重复的字符串字面量
opcache.interned_strings_buffer=16
; 可缓存的脚本文件数上限。应大于项目实际 PHP 文件总数
; 用 find . -name '*.php' | wc -l 统计后取略大的质数
opcache.max_accelerated_files=20000
; ── 失效检查 ──
; 是否检查文件时间戳判断脚本是否更新
; 开发环境 1(改代码立即生效);生产环境设 0 性能最好,但每次发布必须重启 php-fpm
opcache.validate_timestamps=1
; 检查时间戳的间隔(秒)。设 0 表示每个请求都检查(开发用)
opcache.revalidate_freq=60
; 是否检查被 include/require 的文件路径
opcache.revalidate_path=0
; ── 内存回收 ──
; 浪费内存达到该百分比时触发重启
opcache.max_wasted_percentage=10
; 快速关闭(请求结束时快速释放内存)
opcache.fast_shutdown=1
; ── 优化 ──
; 优化级别位掩码,默认值即可
opcache.optimization_level=0x7FFFBFFF
; 是否保存注释(部分框架靠注解工作,如 Doctrine、Symfony,必须设 1)
opcache.save_comments=1
; 从缓存中加载注释
opcache.load_comments=1
; ── 黑名单 ──
; 指定不缓存的文件列表(每行一个路径前缀)
;opcache.blacklist_filename=/etc/php.d/opcache-blacklist.txt
; ── 错误处理 ──
opcache.log_verbosity_level=1
opcache.error_log=/var/log/php-fpm/opcache.log
; ── 文件缓存(PHP 7+,把 opcode 也落盘,二级缓存)──
opcache.file_cache=/var/cache/php-opcache
opcache.file_cache_only=0
opcache.file_cache_consistency_checks=1
mkdir -p /var/cache/php-opcache
chown -R nginx.nginx /var/cache/php-opcache
systemctl restart php-fpm
验证与监控
php -v | grep -i opcache
php -i | grep opcache.enable
浏览器 phpinfo 页应出现独立的 Zend OPcache 段落,Opcode Caching 显示 Up and Running。
写脚本查看命中率:
<?php
$s = opcache_get_status();
echo "已用内存:" . round($s['memory_usage']['used_memory']/1024/1024, 2) . " MB\n";
echo "空闲内存:" . round($s['memory_usage']['free_memory']/1024/1024, 2) . " MB\n";
echo "浪费内存:" . round($s['memory_usage']['wasted_memory']/1024/1024, 2) . " MB\n";
echo "缓存脚本数:" . $s['opcache_statistics']['num_cached_scripts'] . "\n";
echo "命中次数:" . $s['opcache_statistics']['hits'] . "\n";
echo "未命中:" . $s['opcache_statistics']['misses'] . "\n";
echo "命中率:" . round($s['opcache_statistics']['opcache_hit_rate'], 2) . "%\n";
echo "已满次数:" . $s['opcache_statistics']['oom_restarts'] . "\n";
?>
调优判据:
| 现象 | 处理 |
|---|---|
| 命中率 < 95% | 调大 opcache.memory_consumption 与 max_accelerated_files |
oom_restarts > 0(内存不足重启) |
调大 opcache.memory_consumption |
wasted_memory 占比高 |
调大内存,或 validate_timestamps=0 减少失效重编译 |
| 代码更新后页面不变 | validate_timestamps=0 时必须 systemctl reload php-fpm,或代码里调 opcache_reset() |
生产环境最佳实践:
validate_timestamps=0+ 发布流程中加入systemctl reload php-fpm(或kill -USR2),既拿到最高性能又不会读到旧代码。
6.5 静态资源缓存与 gzip 压缩
Nginx gzip 配置
http {
gzip on;
# 最小压缩长度(字节)。太小的文件压缩反而更慢/更大
gzip_min_length 1k;
# 压缩缓冲区:4 个 16k 内存块
gzip_buffers 4 16k;
# 压缩级别 1~9。级别越高压缩率越好但 CPU 消耗越大,推荐 4~6
gzip_comp_level 5;
# 支持压缩的 HTTP 协议最低版本
gzip_http_version 1.1;
# 需要压缩的 MIME 类型(text/html 默认已包含,无需再写)
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript application/x-javascript
application/xml application/xml+rss application/rss+xml
application/atom+xml image/svg+xml font/ttf font/otf;
# 在响应头加 Vary: Accept-Encoding,便于代理/CDN 正确缓存
gzip_vary on;
# 对老版本 IE6 及不支持 gzip 的客户端禁用
gzip_disable "MSIE [1-6]\.";
# 对反向代理来的请求也压缩(any 表示所有情况都压缩)
gzip_proxied any;
# 直接发送已存在的 .gz 预压缩文件(需 --with-http_gzip_static_module)
gzip_static on;
}
注意:不要对图片(jpg/png/gif)、视频、已压缩的 zip/gz 再做 gzip——它们本身已压缩,再压只浪费 CPU 且体积可能变大。
Apache gzip(mod_deflate)配置
LoadModule deflate_module modules/mod_deflate.so
<IfModule mod_deflate.c>
# 按 MIME 类型压缩
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css
AddOutputFilterByType DEFLATE application/javascript application/x-javascript
AddOutputFilterByType DEFLATE application/json application/xml
AddOutputFilterByType DEFLATE image/svg+xml
# 压缩级别 1~9
DeflateCompressionLevel 6
# 排除已压缩的文件类型
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|ico|zip|gz|rar|mp4|flv)$ no-gzip dont-vary
# 老版本浏览器兼容
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
Header append Vary User-Agent env=!dont-vary
</IfModule>
Apache 静态资源过期时间(mod_expires)
LoadModule expires_module modules/mod_expires.so
<IfModule mod_expires.c>
ExpiresActive On
ExpiresDefault "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/gif "access plus 1 year"
ExpiresByType image/webp "access plus 1 year"
ExpiresByType text/css "access plus 1 month"
ExpiresByType application/javascript "access plus 1 month"
ExpiresByType font/woff2 "access plus 1 year"
# HTML 不缓存或短缓存
ExpiresByType text/html "access plus 0 seconds"
</IfModule>
Nginx FastCGI 页面级缓存(把 PHP 输出结果也缓存起来)
对于内容变化不频繁的页面(文章页、列表页),可以直接缓存 PHP 的响应,命中时完全不走 PHP:
http {
# 定义缓存区:路径、目录层级、共享内存区名与大小、失效时间、最大磁盘占用
fastcgi_cache_path /var/cache/nginx/fcgi levels=1:2
keys_zone=FCGI_CACHE:100m inactive=60m max_size=2g;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header updating http_500 http_503;
fastcgi_cache_lock on;
server {
set $skip_cache 0;
# POST 请求不缓存
if ($request_method = POST) { set $skip_cache 1; }
# 带查询串不缓存
if ($query_string != "") { set $skip_cache 1; }
# 后台、登录、购物车、API 等动态路径不缓存
if ($request_uri ~* "/(wp-admin|wp-login\.php|admin|member|cart|checkout|api)") {
set $skip_cache 1;
}
# 已登录用户(带 session/登录 cookie)不缓存
if ($http_cookie ~* "wordpress_logged_in|PHPSESSID|comment_author") {
set $skip_cache 1;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/var/run/php-fpm/fastcgi.socket;
include fastcgi.conf;
fastcgi_cache_bypass $skip_cache; # 满足条件时绕过缓存读取
fastcgi_no_cache $skip_cache; # 满足条件时不写入缓存
fastcgi_cache FCGI_CACHE;
fastcgi_cache_valid 200 301 302 30m;
fastcgi_cache_valid 404 1m;
fastcgi_cache_min_uses 1;
# 响应头中输出缓存状态,便于调试(HIT/MISS/BYPASS/EXPIRED)
add_header X-FastCGI-Cache $upstream_cache_status;
}
}
}
mkdir -p /var/cache/nginx/fcgi
chown -R nginx.nginx /var/cache/nginx
nginx -t && systemctl reload nginx
# 验证缓存是否命中
curl -I http://127.0.0.1/index.php | grep X-FastCGI-Cache
6.6 MySQL / MariaDB 基础参数优化
[mysqld]
# ── 字符集 ──
character-set-server = utf8mb4
collation-server = utf8mb4_general_ci
# ── 连接 ──
# 最大并发连接数。需 > (php-fpm 进程数 + 预留),否则会 Too many connections
max_connections = 1000
# 每个用户的最大连接数(0 为不限)
max_user_connections = 0
# 允许的最大连接错误次数,超过会拉黑该主机(用 FLUSH HOSTS 解除)
max_connect_errors = 100000
# 连接等待超时(秒),过大会积压空闲连接
wait_timeout = 600
interactive_timeout = 600
# 关闭 DNS 反查,加速连接建立(此后授权只能用 IP 不能用主机名)
skip-name-resolve
# 连接请求队列
back_log = 1024
# ── InnoDB 核心参数 ──
# 缓冲池:InnoDB 最重要的参数,缓存数据与索引
# 独立数据库服务器设为物理内存的 50%~70%;与 Web 混跑设 20%~30%
innodb_buffer_pool_size = 2G
# 缓冲池实例数:buffer_pool >= 1G 时,按每实例 1G 划分,减少锁争用
innodb_buffer_pool_instances = 2
# redo 日志文件大小,越大写性能越好但崩溃恢复越慢
innodb_log_file_size = 512M
innodb_log_files_in_group = 2
# 日志缓冲区
innodb_log_buffer_size = 64M
# 事务提交时的刷盘策略:
# 1 = 每次提交都刷盘(最安全,默认,金融类必须)
# 2 = 每次提交写 OS 缓存,每秒刷盘(性能好,OS 崩溃可能丢 1 秒数据)
# 0 = 每秒写日志并刷盘(性能最好,MySQL 崩溃即可能丢 1 秒数据)
innodb_flush_log_at_trx_commit = 1
# 每张表独立表空间文件,便于单表回收空间
innodb_file_per_table = 1
# 刷盘方式,直接 IO 绕过 OS 缓存,避免双重缓存
innodb_flush_method = O_DIRECT
# IO 能力,SSD 可设 2000~20000,机械盘 200
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
# 并发读写线程数
innodb_read_io_threads = 8
innodb_write_io_threads = 8
# 行锁等待超时(秒)
innodb_lock_wait_timeout = 50
# ── 缓存与缓冲区 ──
# 表定义与表文件描述符缓存
table_open_cache = 4096
table_definition_cache = 2048
# 线程缓存,复用线程减少创建开销
thread_cache_size = 256
# 单个客户端可发送的最大数据包(导入大 SQL / BLOB 时需调大)
max_allowed_packet = 64M
# 排序、连接、随机读缓冲(会话级,不要设太大,乘以连接数会爆内存)
sort_buffer_size = 2M
join_buffer_size = 2M
read_buffer_size = 2M
read_rnd_buffer_size = 4M
# 内存临时表上限
tmp_table_size = 128M
max_heap_table_size = 128M
# ── 查询缓存(MySQL 5.7 起不推荐,8.0 已移除;MariaDB 仍可用)──
# 高并发写场景下 query_cache 反而是瓶颈,建议关闭
query_cache_type = 0
query_cache_size = 0
# ── 慢查询日志(性能排查的主要依据)──
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1
min_examined_row_limit = 100
# ── 二进制日志(主从复制、时间点恢复必备)──
log-bin = /data/mysql/mysql-bin
server-id = 1
binlog_format = ROW
expire_logs_days = 7
max_binlog_size = 512M
sync_binlog = 1
# ── 错误日志 ──
log-error = /var/log/mysql/mysql_err.log
慢查询分析:
# 用官方 mysqldumpslow 汇总
mysqldumpslow -s t -t 20 /var/log/mysql/slow.log
# 用 percona-toolkit 更详细
pt-query-digest /var/log/mysql/slow.log > /tmp/slow_report.txt
# 实时查看当前连接与执行中的语句
mysql -uroot -p -e "SHOW FULL PROCESSLIST;"
mysql -uroot -p -e "SHOW GLOBAL STATUS LIKE 'Threads_connected';"
mysql -uroot -p -e "SHOW GLOBAL STATUS LIKE 'Max_used_connections';"
mysql -uroot -p -e "SHOW ENGINE INNODB STATUS\G"
6.7 把 session 放到 Redis 或 Memcached
为什么要做:PHP 默认把 session 以文件形式存在本地磁盘(session.save_path)。单机没问题,但一旦做了负载均衡(多台 PHP 服务器),用户第一次请求落在 A 机、第二次落在 B 机,B 机上没有该 session 文件,就会表现为反复掉登录。把 session 集中存到 Redis/Memcached 即可让所有节点共享。同时内存存储也比磁盘文件快。
详见 11-07-Redis.md
方案一:session 存 Redis(推荐)
安装 Redis 与 PHP 扩展:
yum install -y redis
systemctl start redis
systemctl enable redis
# PHP redis 扩展
yum install -y php-pecl-redis
# 或用 pecl 编译
/usr/local/php/bin/pecl install redis
修改 php.ini:
session.save_handler = redis
; 基本写法
session.save_path = "tcp://127.0.0.1:6379"
; 带密码与库号
session.save_path = "tcp://127.0.0.1:6379?auth=YourRedisPass&database=1&timeout=2.5"
; 带前缀与超时
session.save_path = "tcp://127.0.0.1:6379?prefix=PHPSESS:&weight=1"
session.gc_maxlifetime = 1440
也可在 php-fpm 的 www.conf 中按池设置:
php_value[session.save_handler] = redis
php_value[session.save_path] = "tcp://127.0.0.1:6379?auth=YourRedisPass"
systemctl restart php-fpm
验证:
# 访问一个会写 session 的页面后,在 Redis 中查看
redis-cli -a YourRedisPass keys 'PHPREDIS_SESSION:*'
redis-cli -a YourRedisPass keys 'PHPSESS:*'
测试脚本:
<?php
session_start();
if (!isset($_SESSION['count'])) {
$_SESSION['count'] = 0;
}
$_SESSION['count']++;
echo "SESSION ID: " . session_id() . "<br>";
echo "访问次数: " . $_SESSION['count'] . "<br>";
echo "存储方式: " . ini_get('session.save_handler') . "<br>";
echo "存储位置: " . ini_get('session.save_path') . "<br>";
?>
刷新页面计数递增,且 Redis 里能查到对应 key,即配置成功。
方案二:session 存 Memcached
yum install -y memcached php-pecl-memcached
systemctl start memcached
systemctl enable memcached
# 调整 memcached 启动参数
vim /etc/sysconfig/memcached
PORT="11211"
USER="memcached"
MAXCONN="4096"
CACHESIZE="512" # 分配 512MB 内存
OPTIONS="-l 127.0.0.1" # 只监听本机
修改 php.ini:
; memcached 扩展(带 d)
session.save_handler = memcached
session.save_path = "127.0.0.1:11211"
; 或 memcache 扩展(不带 d,老版本)
;session.save_handler = memcache
;session.save_path = "tcp://127.0.0.1:11211"
; 多台 memcached(自动做一致性哈希分布)
;session.save_path = "127.0.0.1:11211,127.0.0.1:11212"
systemctl restart php-fpm
# 验证
echo "stats items" | nc 127.0.0.1 11211
Redis 与 Memcached 对比
| 对比项 | Redis | Memcached |
|---|---|---|
| 数据持久化 | 支持(RDB / AOF),重启不丢 session | 不支持,重启后所有用户掉登录 |
| 数据结构 | 丰富(string/hash/list/set/zset) | 仅 key-value |
| 主从复制/高可用 | 支持(主从、Sentinel、Cluster) | 原生不支持 |
| 单 key 大小 | 512MB | 1MB(默认) |
| 多线程 | 6.0 起支持多线程 IO | 原生多线程 |
| 内存效率 | 略低 | 纯 KV 场景略高 |
| 推荐 | 优先选 Redis,可同时做缓存、队列、session | 仅需简单缓存时可选 |
方案三:应用层缓存(不只是 session)
除了 session,还可以把数据库查询结果、页面片段缓存到 Redis:
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->auth('YourRedisPass');
$key = 'article:list:page1';
$data = $redis->get($key);
if ($data === false) {
// 缓存未命中,查数据库
$pdo = new PDO('mysql:host=127.0.0.1;dbname=testdb;charset=utf8mb4', 'testuser', '123456');
$rows = $pdo->query('SELECT id,title FROM article ORDER BY id DESC LIMIT 20')->fetchAll(PDO::FETCH_ASSOC);
$data = json_encode($rows, JSON_UNESCAPED_UNICODE);
// 写入缓存,5 分钟过期
$redis->setex($key, 300, $data);
echo "[MISS] ";
} else {
echo "[HIT] ";
}
echo $data;
?>
6.8 架构演进路线
阶段一:单机 All-in-One
┌────────────────────────────────┐
│ 一台服务器 │
│ Nginx + php-fpm + MySQL │
└────────────────────────────────┘
特点:部署最简单,成本最低
瓶颈:任一组件吃满资源就影响全部;单点故障即全站不可用;无法水平扩展
适用:日 PV 1 万以下的小站、测试环境
↓ 流量上升,PHP 与 MySQL 争抢 CPU/内存
阶段二:动静分离 + 服务拆分
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Nginx │ │ php-fpm │ │ MySQL │
│ 静态资源 │──▶│ 动态逻辑 │──▶│ 数据存储 │
└──────────────┘ └──────────────┘ └──────────────┘
措施:静态资源交 Nginx 直出并设长过期(见 4.8);数据库独立部署到单独机器;
加 OPcache;session 与热点数据放 Redis
效果:各组件独立扩容、独立调优,互不抢资源
适用:日 PV 10 万级
↓ 单台 Web 服务器扛不住 / 需要消除单点
阶段三:负载均衡(Web 层水平扩展)
┌─────────────┐
用户请求 ─────▶ │ LVS/Nginx │ (四层/七层负载均衡 + Keepalived 高可用)
└──────┬──────┘
┌──────────┼──────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Web-01 │ │ Web-02 │ │ Web-03 │ (Nginx + php-fpm,代码一致)
└────┬───┘ └────┬───┘ └────┬───┘
└──────────┼──────────┘
▼
┌──────────────────┐
│ MySQL + Redis │
└──────────────────┘
关键点:
· session 必须集中存储(Redis),否则用户反复掉登录
· 上传的文件必须共享存储(NFS / GlusterFS / 对象存储),否则 A 机上传 B 机看不到
· 多台 Web 的代码需一致(Git 部署 / rsync / CI-CD / 容器镜像)
· 负载均衡器本身要做高可用(Keepalived + VIP 双机热备)
适用:日 PV 百万级
> 详见 12-01-LVS负载均衡.md
↓ 数据库读压力成为新瓶颈(读多写少)
阶段四:数据库读写分离 + 主从复制
┌────────────────┐
写请求 ─────────▶│ MySQL Master │ (处理 INSERT/UPDATE/DELETE)
└────────┬───────┘
│ binlog 复制
┌───────────┼───────────┐
▼ ▼ ▼
┌──────────┐┌──────────┐┌──────────┐
读请求▶│ Slave-01 ││ Slave-02 ││ Slave-03 │ (处理 SELECT)
└──────────┘└──────────┘└──────────┘
实现方式:
· 应用层:代码中区分主从连接(框架大多内置读写分离配置)
· 中间件层:MyCat / ProxySQL / MaxScale / Atlas 自动路由 SQL
注意点:
· 主从复制存在延迟,写后立即读需强制走主库
· 主库单点问题用 MHA / Orchestrator / MGR 解决
适用:日 PV 千万级
> 详见 11-06-主从复制与读写分离.md
↓ 继续演进
阶段五:更大规模
· CDN 边缘加速:静态资源全部下沉到 CDN,回源率降到 5% 以内
· 分库分表:单表数据量过亿后按业务垂直拆库、按 ID/时间水平分表
· 消息队列(RabbitMQ / Kafka):把耗时操作异步化(发信、生成报表)
· 微服务化:单体 PHP 应用拆成多个服务,各自独立部署与扩容
· 容器化编排:Docker + Kubernetes 实现弹性伸缩与滚动发布
· 全链路监控:Prometheus + Grafana + ELK + APM(Zipkin / SkyWalking)
各阶段的关键指标参考:
| 阶段 | 架构 | 承载能力(估算) | 主要瓶颈 |
|---|---|---|---|
| 一 | 单机 | 日 PV < 1 万,并发 < 50 | 资源争抢、单点 |
| 二 | 动静分离 + 独立 DB | 日 PV 10 万,并发 200~500 | 单台 Web 的 PHP 进程数 |
| 三 | 负载均衡多 Web | 日 PV 百万,并发数千 | 数据库读 QPS |
| 四 | 读写分离 | 日 PV 千万,并发 1 万+ | 数据库写 QPS、单表容量 |
| 五 | CDN + 分库分表 + 微服务 | 亿级 PV | 架构复杂度与运维成本 |
七、排错清单
7.1 速查表
| 问题 | 可能原因 | 解决方法 |
|---|---|---|
| 80 端口被占用 | httpd 等服务占用 | lsof -i:80 查看并停止冲突服务 |
| PHP 页面显示源码或被下载 | Web 服务器未配置 PHP 解析 | Nginx 检查 location ~ \.php$;Apache 检查 LoadModule php 与 AddType |
| 502 Bad Gateway | php-fpm 未启动 / socket 路径或权限不对 | 检查 php-fpm 状态与 fastcgi_pass 路径(见 7.2) |
| 504 Gateway Time-out | PHP 脚本执行太久 | 调大 fastcgi_read_timeout 与 max_execution_time(见 7.3) |
| File not found / No input file specified | root 与 SCRIPT_FILENAME 不匹配 |
见 7.4 |
| 页面完全空白(白屏) | PHP 致命错误但 display_errors=Off |
见 7.5 |
| 上传大文件失败 | client_max_body_size / php.ini 三个参数过小 |
见 7.6 |
| 中文乱码 | 页面/库/连接三处字符集不一致 | 见 7.7 |
| 数据库连接数耗尽 | max_connections 不足或连接未释放 |
见 7.8 |
| PHP 文件权限错误 | Web 用户无权读取 | 检查文件 owner 是否为 nginx / apache |
| socket 文件权限拒绝 | listen.owner / listen.group 不匹配 |
确保 php-fpm 的 listen.owner/group 与 Nginx 运行用户一致 |
| MySQL 无法远程连接 | 绑定了 127.0.0.1 / 授权 host 不对 | 检查 bind-address 与 mysql.user 的 host 字段 |
nginx -t 报错 |
配置语法错误 | 根据错误提示定位行号修正 |
| 浏览器访问不到页面 | 防火墙 / SELinux / 服务未启动 | firewall-cmd --list-all、getenforce、systemctl status |
7.2 502 Bad Gateway
含义:Nginx 把请求转给了 php-fpm,但没能建立连接或没收到有效响应。Nginx 本身是好的,问题在后端。
排查顺序:
# ① php-fpm 到底有没有在跑?(最常见原因)
systemctl status php-fpm
ps -ef | grep php-fpm
# 没跑就启动,启动失败看错误日志
systemctl start php-fpm
journalctl -xe -u php-fpm
tail -50 /var/log/php-fpm/error.log
tail -50 /var/log/php/php-fpm_err.log
# ② socket 文件存在吗?权限对吗?
ls -l /var/run/php-fpm/fastcgi.socket
ls -l /tmp/php-fpm.sock
# 期望:属主/属组为 nginx,权限 srw-rw-rw- 或 srw-rw----
# ③ Nginx 的 fastcgi_pass 与 php-fpm 的 listen 是否完全一致?
grep -r 'fastcgi_pass' /etc/nginx/
grep -E '^\s*listen' /etc/php-fpm.d/www.conf
grep -E '^\s*listen' /usr/local/php/etc/php-fpm.conf
# ④ TCP 模式下端口是否监听?
netstat -tunlp | grep 9000
telnet 127.0.0.1 9000
# ⑤ 看 Nginx 错误日志的确切原因
tail -f /var/log/nginx/error.log
tail -f /usr/local/nginx/logs/error.log
按错误日志中的具体信息对症处理:
| 错误日志内容 | 原因 | 解决 |
|---|---|---|
connect() to unix:/xxx.sock failed (2: No such file or directory) |
socket 文件不存在 | php-fpm 没启动,或两边 listen/fastcgi_pass 路径写的不是同一个文件 |
connect() to unix:/xxx.sock failed (13: Permission denied) |
socket 权限问题 | 设 listen.owner = nginx、listen.group = nginx、listen.mode = 0660;并确认 socket 所在目录(如 /var/run/php-fpm/)对 nginx 可进入:chown -R nginx.nginx /var/run/php-fpm/。SELinux 开启时还要 setenforce 0 验证一下 |
connect() to 127.0.0.1:9000 failed (111: Connection refused) |
php-fpm 未监听该端口 | 确认 php-fpm 在跑,且 listen = 127.0.0.1:9000 |
connect() to unix:/xxx.sock failed (11: Resource temporarily unavailable) |
socket 积压队列满 | 调大 listen.backlog = 65535,内核 net.core.somaxconn = 65535 |
recv() failed (104: Connection reset by peer) |
php-fpm worker 崩溃 | PHP 段错误(扩展冲突/内存越界),看 /var/log/messages 里的 segfault;降低 pm.max_requests;排查最近装的扩展 |
upstream sent too big header while reading response header |
响应头超出缓冲区(大 Cookie、大量 Set-Cookie、超长重定向) | 调大 fastcgi_buffer_size 64k; fastcgi_buffers 8 64k; fastcgi_busy_buffers_size 128k; |
pool www: server reached pm.max_children setting |
PHP 进程数用尽 | 调大 pm.max_children(先确认内存够,见 6.3) |
Primary script unknown |
SCRIPT_FILENAME 路径错 | 见 7.4 |
一键修复常见 socket 权限问题:
# 1. 确认 nginx 的实际运行用户
ps aux | grep 'nginx: worker' | head -1
# 2. 统一 php-fpm 配置
sed -i 's/^user = .*/user = nginx/' /etc/php-fpm.d/www.conf
sed -i 's/^group = .*/group = nginx/' /etc/php-fpm.d/www.conf
sed -i 's/^;\?listen.owner = .*/listen.owner = nginx/' /etc/php-fpm.d/www.conf
sed -i 's/^;\?listen.group = .*/listen.group = nginx/' /etc/php-fpm.d/www.conf
sed -i 's/^;\?listen.mode = .*/listen.mode = 0660/' /etc/php-fpm.d/www.conf
# 3. 修正目录属主并重启
mkdir -p /var/run/php-fpm
chown -R nginx.nginx /var/run/php-fpm
systemctl restart php-fpm
systemctl reload nginx
7.3 504 Gateway Time-out
含义:Nginx 成功连上 php-fpm,但等响应等超时了。说明 PHP 脚本执行时间过长。
需要同时调整三处(取最小值生效,任何一处小了都会超时):
# ① Nginx 侧
http {
fastcgi_connect_timeout 60s; # 建立连接超时
fastcgi_send_timeout 300s; # 发送请求超时
fastcgi_read_timeout 300s; # ★ 等待响应超时,504 的直接原因
# 若用了反向代理,还需
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
; ② php.ini 侧
max_execution_time = 300 ; 脚本最大执行秒数
max_input_time = 300 ; 解析输入数据的最大秒数
; ③ php-fpm 的 www.conf 侧
request_terminate_timeout = 300s ; 超过此值 php-fpm 会强杀该请求
nginx -t && systemctl reload nginx
systemctl restart php-fpm
但调大超时只是止血,根因要靠慢日志找:
# 开启 php-fpm 慢日志
# www.conf 中:
# slowlog = /var/log/php-fpm/www-slow.log
# request_slowlog_timeout = 5s
systemctl restart php-fpm
# 慢日志会记录卡住时的完整 PHP 调用栈
tail -f /var/log/php-fpm/www-slow.log
[22-Aug-2026 10:30:15] [pool www] pid 12345
script_filename = /usr/share/nginx/html/report.php
[0x00007f...] mysqli_query() /usr/share/nginx/html/lib/db.php:88
[0x00007f...] getReport() /usr/share/nginx/html/report.php:12
配合 MySQL 慢查询日志定位(见 6.6)。常见的慢原因:
| 原因 | 处理 |
|---|---|
| SQL 没走索引、全表扫描 | EXPLAIN 分析并加索引 |
| 循环里查数据库(N+1 问题) | 改成批量查询 / JOIN |
| 调用第三方 API 无超时设置 | cURL 设 CURLOPT_TIMEOUT,或改为异步队列 |
| 一次性导出/处理大量数据 | 改为分批处理 + 队列异步执行,前端轮询进度 |
| 死循环 / 递归无出口 | 修代码 |
| 文件锁 / 数据库锁等待 | 看 SHOW ENGINE INNODB STATUS 找锁冲突 |
7.4 File not found / No input file specified / Primary script unknown
含义:php-fpm 收到了请求,但按 SCRIPT_FILENAME 给的路径找不到那个 PHP 文件。本质是 Nginx 的 root 与传给 PHP 的路径不匹配。
排查:
# ① 看 Nginx 的 root 到底是什么
grep -rn 'root ' /etc/nginx/nginx.conf /etc/nginx/conf.d/
# ② 看 SCRIPT_FILENAME 是怎么拼的
grep -rn 'SCRIPT_FILENAME' /etc/nginx/
# ③ 文件真的在那儿吗?
ls -l /usr/share/nginx/html/index.php
# ④ php-fpm 的运行用户能读到吗?
sudo -u nginx cat /usr/share/nginx/html/index.php
namei -l /usr/share/nginx/html/index.php # 逐级检查路径上每一层的权限
常见的四种错法:
① root 写在 location 里但 PHP 的 location 没有
# ✗ 错误:root 只在 location / 里,进入 location ~ \.php$ 后 $document_root 为空
server {
location / {
root /usr/share/nginx/html;
}
location ~ \.php$ {
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
# ✓ 正确:root 提到 server 段,所有 location 共享
server {
root /usr/share/nginx/html;
location / { }
location ~ \.php$ {
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
② 用了 include fastcgi_params 却忘了写 SCRIPT_FILENAME
# ✗ 错误:fastcgi_params 不含 SCRIPT_FILENAME → No input file specified
location ~ \.php$ {
fastcgi_pass unix:/var/run/php-fpm/fastcgi.socket;
include fastcgi_params;
}
# ✓ 正确:补上,或改用 fastcgi.conf
location ~ \.php$ {
fastcgi_pass unix:/var/run/php-fpm/fastcgi.socket;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
③ SCRIPT_FILENAME 写了绝对路径但与 root 不一致
# ✗ 错误:root 改了,SCRIPT_FILENAME 的硬编码路径忘了改
server {
root /data/www; # 改到了新目录
location ~ \.php$ {
fastcgi_param SCRIPT_FILENAME /usr/local/nginx/html$fastcgi_script_name; # 还是老目录
}
}
# ✓ 正确:用变量,永远跟着 root 走
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
④ 用了 alias 时不能用 $document_root
# ✗ 错误:alias 场景下 $document_root 不是 alias 的值
location /app/ {
alias /data/app/;
location ~ \.php$ {
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
# ✓ 正确:用 $request_filename,或改用 root + 目录同名
location ~ ^/app/(.+\.php)$ {
alias /data/app/$1;
fastcgi_pass unix:/var/run/php-fpm/fastcgi.socket;
fastcgi_param SCRIPT_FILENAME $request_filename;
include fastcgi_params;
}
其它可能:
- SELinux 拦截:
getenforce为 Enforcing 时,chcon -R -t httpd_sys_content_t /data/www/。 open_basedir限制:php.ini 的open_basedir没包含网站目录,PHP 拒绝读取。- 路径上某级目录无 x 权限:
namei -l检查,父目录需要o+x才能穿越。 - chroot 场景:php-fpm 配了
chroot,则 SCRIPT_FILENAME 必须是 chroot 内的相对路径。
7.5 页面空白(白屏)
含义:HTTP 200 但页面一个字都没有。通常是 PHP 发生致命错误,而 display_errors 关着,所以什么也没输出。
排查:
# ① 先让错误显示出来(临时,排查完关掉)
vim /etc/php.ini
display_errors = On
display_startup_errors = On
error_reporting = E_ALL
log_errors = On
error_log = /var/log/php/php_error.log
systemctl restart php-fpm # LNMP
systemctl restart httpd # LAMP
# ② 或者不改全局,只在被测脚本顶部临时加
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
// ... 你的代码
# ③ 直接看日志(最推荐,生产环境不该开 display_errors)
tail -f /var/log/php/php_error.log
tail -f /var/log/php-fpm/www-error.log
tail -f /var/log/nginx/error.log
tail -f /var/log/httpd/error_log
# ④ 命令行跑一遍,语法错误立刻暴露
php -l /usr/share/nginx/html/index.php # 只检查语法
php /usr/share/nginx/html/index.php # 实际执行
# ⑤ 看响应头判断是否真的 200
curl -i http://127.0.0.1/index.php
白屏的常见根因:
| 根因 | 表现 / 定位 | 解决 |
|---|---|---|
| PHP 语法错误 | php -l 报 Parse error |
修语法 |
| 内存超限 | 日志 Allowed memory size of X bytes exhausted |
调大 memory_limit = 256M;或优化代码(别一次性加载大数组) |
| 执行超时 | 日志 Maximum execution time exceeded |
调大 max_execution_time,优化慢代码 |
| 调用了不存在的函数 | 日志 Call to undefined function xxx() |
缺扩展:yum install -y php-xxx 后重启 php-fpm |
| 缺少扩展导致类不存在 | Class 'PDO' not found、Class 'Redis' not found |
装对应扩展 |
| 短标签未开启 | 源码用 <? ... ?> 而 short_open_tag = Off,PHP 当纯文本原样输出(可能输出空) |
short_open_tag = On,或把代码改成 <?php |
| 输出缓冲被吃掉 | 代码里 ob_start() 后异常退出没 flush |
检查输出缓冲逻辑 |
| 文件是 UTF-8 with BOM | BOM 字节导致 header 已发送错误或空白 | 用 dos2unix / 编辑器转成无 BOM 的 UTF-8 |
| 段错误(segfault) | /var/log/messages 里有 segfault at ... |
扩展冲突:逐个禁用 /etc/php.d/*.ini 排查;升级 PHP/扩展版本 |
| WordPress 等插件冲突 | 后台白屏 | 重命名 wp-content/plugins 目录,逐个恢复排查 |
排查完毕后,生产环境务必把
display_errors改回Off,只保留log_errors = On,避免向用户暴露路径、SQL、账号等信息。
7.6 上传大文件失败
关键:四个参数必须同时调大,任何一个卡住都会失败。 且大小关系必须满足:
client_max_body_size >= post_max_size >= upload_max_filesize,memory_limit >= post_max_size。
① Nginx 侧:client_max_body_size
http {
# 全局生效
client_max_body_size 200m;
client_body_buffer_size 1m;
# 大文件上传时请求体会先落临时文件,指定目录并确保可写
client_body_temp_path /var/cache/nginx/client_temp 1 2;
# 上传耗时长,超时给足
client_body_timeout 300s;
}
# 也可只对上传接口放开,其它路径保持严格
server {
location = /upload.php {
client_max_body_size 500m;
fastcgi_pass unix:/var/run/php-fpm/fastcgi.socket;
include fastcgi.conf;
fastcgi_read_timeout 600s;
}
}
超限时的表现:Nginx 直接返回 413 Request Entity Too Large,错误日志中:
client intended to send too large body: 104857600 bytes。
② php.ini 侧:三个核心参数
; 单个上传文件的最大尺寸
upload_max_filesize = 100M
; 整个 POST 请求体的最大尺寸
; 必须 > upload_max_filesize(要容纳文件 + 其它表单字段 + MIME 分隔开销),建议留 20% 余量
post_max_size = 120M
; 脚本可用内存上限,必须 >= post_max_size(老版本 PHP 会把整个请求体读进内存)
memory_limit = 256M
; ── 配套参数 ──
; 单次请求允许上传的文件个数(批量上传时要调大)
max_file_uploads = 50
; 上传大文件耗时长,执行时间要给足
max_execution_time = 600
max_input_time = 600
; 临时目录,必须存在且对 php-fpm 用户可写,且磁盘空间要够
upload_tmp_dir = /var/tmp/php_upload
file_uploads = On
mkdir -p /var/tmp/php_upload /var/cache/nginx/client_temp
chown -R nginx.nginx /var/tmp/php_upload /var/cache/nginx/client_temp
chmod 1777 /var/tmp/php_upload
nginx -t && systemctl reload nginx
systemctl restart php-fpm
③ Apache 侧(LAMP)
Apache 通常不额外限制,如需限制用:
# 限制请求体最大字节数(0 表示不限)
LimitRequestBody 209715200
# 在虚拟主机/目录中覆盖 PHP 参数(需 mod_php)
<Directory "/var/www/html">
php_value upload_max_filesize 100M
php_value post_max_size 120M
php_value memory_limit 256M
php_value max_execution_time 600
</Directory>
④ 验证生效
# 命令行看
php -i | grep -E 'upload_max_filesize|post_max_size|memory_limit|max_file_uploads'
# 浏览器 phpinfo() 页面搜索这几项,确认 Local Value 与 Master Value
phpinfo 中的值没变的常见原因:改了错误的 php.ini(php-fpm 与 CLI 用的可能不是同一份),用 php -i | grep 'Loaded Configuration File' 与 phpinfo 页上的路径对比确认。
⑤ 上传目录权限
# 目标目录必须对 Web 运行用户可写
cd /usr/share/nginx/html/
chown -R nginx.nginx ./uploads ./wp-content/uploads ./data/attachment
chmod -R 755 ./uploads
# 若还是不行,检查 SELinux
chcon -R -t httpd_sys_rw_content_t ./uploads
setsebool -P httpd_unified on
⑥ 上传失败的对照表
| 现象 | 原因 | 解决 |
|---|---|---|
| 413 Request Entity Too Large | Nginx client_max_body_size 太小 |
调大并 reload nginx |
上传后 $_FILES 为空数组 |
post_max_size 被超过(PHP 直接丢弃整个 POST) |
调大 post_max_size |
$_FILES['x']['error'] = 1 |
超过 upload_max_filesize(UPLOAD_ERR_INI_SIZE) |
调大 upload_max_filesize |
$_FILES['x']['error'] = 2 |
超过 HTML 表单里的 MAX_FILE_SIZE |
改前端表单值 |
$_FILES['x']['error'] = 3 |
文件只上传了一部分(网络中断) | 检查网络与超时设置 |
$_FILES['x']['error'] = 6 |
找不到临时目录 | 创建 upload_tmp_dir 并授权 |
$_FILES['x']['error'] = 7 |
写入磁盘失败 | 临时目录不可写或磁盘满(df -h) |
| 上传到一半 504 | 超时 | 调大 fastcgi_read_timeout / max_execution_time |
| 上传成功但文件不在目标目录 | 目标目录不可写,move_uploaded_file() 失败 |
修属主与权限,SELinux 放行 |
7.7 中文乱码
乱码的根源是页面编码、数据库编码、连接编码三者不一致。逐层统一即可。
① HTML / HTTP 响应头层
<?php
// PHP 显式声明编码(必须在任何输出之前)
header('Content-Type: text/html; charset=utf-8');
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
Nginx 层统一声明:
http {
charset utf-8;
# 或只对特定类型
charset_types text/html text/css application/json text/xml application/javascript;
# 默认字符集
source_charset utf-8;
}
Apache 层:
AddDefaultCharset UTF-8
# 若想让文件自身的 meta 生效,改为
#AddDefaultCharset Off
注意 Apache 的
AddDefaultCharset UTF-8会强制覆盖页面里的<meta charset>,若网站是 GBK 编码则会乱码,此时应设为Off或改为GB2312。
② php.ini 层
default_charset = "UTF-8"
mbstring.internal_encoding = UTF-8
mbstring.http_output = UTF-8
mbstring.encoding_translation = Off
mbstring.detect_order = auto
mbstring.func_overload = 0
③ 数据库层(服务端字符集)
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_general_ci
# 强制客户端连接也用该字符集(会忽略客户端的 SET NAMES,慎用)
#init_connect='SET NAMES utf8mb4'
skip-character-set-client-handshake
[client]
default-character-set = utf8mb4
[mysql]
default-character-set = utf8mb4
systemctl restart mariadb
mysql -uroot -p -e "SHOW VARIABLES LIKE 'char%'; SHOW VARIABLES LIKE 'collation%';"
期望输出:
character_set_client | utf8mb4
character_set_connection | utf8mb4
character_set_database | utf8mb4
character_set_results | utf8mb4
character_set_server | utf8mb4
④ 库/表/字段字符集
-- 建库时指定
CREATE DATABASE mydb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
-- 已有库/表的修改
ALTER DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
ALTER TABLE mytable CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
-- 查看现状
SHOW CREATE DATABASE mydb;
SHOW CREATE TABLE mytable;
SELECT table_name, table_collation FROM information_schema.tables WHERE table_schema='mydb';
⑤ PHP 连接层(最容易漏的一环)
<?php
// mysqli:连接后立即设置字符集(不要用 SET NAMES 的 query 方式)
$link = mysqli_connect('127.0.0.1', 'user', 'pass', 'mydb');
mysqli_set_charset($link, 'utf8mb4'); // ✓ 推荐
// mysqli_query($link, "SET NAMES utf8mb4"); // △ 可用但不更新客户端内部编码状态
// PDO:在 DSN 里指定 charset
$pdo = new PDO('mysql:host=127.0.0.1;dbname=mydb;charset=utf8mb4', 'user', 'pass');
// 面向对象写法
$mysqli = new mysqli('127.0.0.1', 'user', 'pass', 'mydb');
$mysqli->set_charset('utf8mb4');
?>
⑥ utf8 与 utf8mb4 的区别(重要)
| 字符集 | 每字符最大字节 | 能存 emoji / 生僻汉字 | 说明 |
|---|---|---|---|
utf8(即 utf8mb3) |
3 | ❌ 不能 | MySQL 的"残缺 UTF-8",插入 emoji 会报 Incorrect string value: '\xF0\x9F...' |
utf8mb4 |
4 | ✅ 能 | 真正完整的 UTF-8,新项目一律用这个 |
⑦ 文件本身的编码
# 查看文件编码
file -i index.php
enca -L zh_CN index.php
# GBK 转 UTF-8
iconv -f GBK -t UTF-8 old.php -o new.php
# 批量转换整个目录
find . -name '*.php' -exec sh -c 'iconv -f GBK -t UTF-8 "$1" -o "$1.tmp" && mv "$1.tmp" "$1"' _ {} \;
# 去掉 UTF-8 BOM(BOM 会导致 header already sent / 页面顶部出现空白字符)
sed -i '1s/^\xEF\xBB\xBF//' index.php
⑧ 乱码速查表
| 现象 | 原因 |
|---|---|
| 页面全是"锟斤拷" | UTF-8 数据被当 GBK 解读,或反复转码 |
| 页面是"?????" | 数据在写入数据库时字符集不支持,被替换成 ?(如往 latin1 字段写中文) |
| 页面是"ä½ å¥½" | UTF-8 字节被当 latin1 解读,通常是连接字符集没设 |
| 数据库里正常,页面乱码 | 连接字符集(mysqli_set_charset)或 HTTP 响应头没设 |
| 页面正常,数据库里乱码 | 建库/建表字符集不对,或连接字符集不对导致写入时已损坏 |
| emoji 存不进去 | 用了 utf8 而非 utf8mb4 |
页面顶部有奇怪空白/Cannot modify header information |
文件带 UTF-8 BOM |
7.8 数据库连接数耗尽
典型报错:Too many connections、Can't connect to MySQL server、SQLSTATE[HY000] [1040]。
① 查看现状
-- 允许的最大连接数
SHOW VARIABLES LIKE 'max_connections';
-- 当前连接数
SHOW STATUS LIKE 'Threads_connected';
-- 历史峰值(这个最有参考价值)
SHOW STATUS LIKE 'Max_used_connections';
-- 因连接数满而被拒绝的次数(> 0 说明确实爆过)
SHOW STATUS LIKE 'Connection_errors_max_connections';
-- 当前所有连接的明细
SHOW FULL PROCESSLIST;
-- 按用户/主机统计连接数,找出是哪个应用占满的
SELECT user, host, COUNT(*) AS cnt FROM information_schema.processlist
GROUP BY user, host ORDER BY cnt DESC;
-- 找出长期 Sleep 的连接(未正常释放)
SELECT id, user, host, db, command, time, state FROM information_schema.processlist
WHERE command='Sleep' AND time > 300 ORDER BY time DESC;
② 应急处理
-- 临时调大(重启后失效,但无需重启数据库)
SET GLOBAL max_connections = 2000;
-- 杀掉长时间 Sleep 的连接
KILL 12345;
-- 批量生成 KILL 语句
SELECT CONCAT('KILL ', id, ';') FROM information_schema.processlist
WHERE command='Sleep' AND time > 600;
如果连 root 都连不进去(连接数已满),MySQL 会为 SUPER 权限用户保留 1 个连接位;仍不行则只能重启服务。
③ 永久调整
[mysqld]
# 最大连接数
max_connections = 2000
# 每个用户最大连接数(防止单个应用占满)
max_user_connections = 500
# 空闲连接超时回收(秒),默认 28800(8 小时)太长
wait_timeout = 600
interactive_timeout = 600
# 连接请求队列长度
back_log = 1024
# 允许的连接错误次数上限(防止被误拉黑)
max_connect_errors = 100000
systemctl restart mariadb
注意: max_connections 受操作系统文件描述符限制。每个连接至少占 1 个 fd,还要加上表文件等。需同步调大:
[mysqld]
open_files_limit = 65535
# systemd 管理的 MySQL 需在 unit 里设
mkdir -p /etc/systemd/system/mariadb.service.d/
cat > /etc/systemd/system/mariadb.service.d/limits.conf <<'EOF'
[Service]
LimitNOFILE=65535
EOF
systemctl daemon-reload
systemctl restart mariadb
# 验证
mysql -uroot -p -e "SHOW VARIABLES LIKE 'open_files_limit';"
cat /proc/$(pgrep -o mysqld)/limits | grep 'open files'
④ 容量规划公式
max_connections 应 >= (php-fpm 进程总数 × 每请求平均并发连接数) + 运维预留
例:3 台 Web,每台 pm.max_children = 100,每个请求 1 条数据库连接
→ 3 × 100 × 1 + 50(预留) = 350,设 max_connections = 500 较稳妥
内存消耗估算(每连接会分配会话级缓冲区):
单连接内存 ≈ sort_buffer_size + join_buffer_size + read_buffer_size
+ read_rnd_buffer_size + thread_stack + net_buffer_length
以本文 6.6 的配置:2M + 2M + 2M + 4M + 256K + 16K ≈ 10.3 MB
max_connections = 2000 时,理论峰值 ≈ 20 GB(实际远小于此,因为不会同时用满)
★ 所以不要盲目把 sort_buffer_size 等会话级参数设大,也不要盲目把
max_connections 设成上万——先确认内存够。
⑤ 根因排查(连接数满通常不是配置问题,而是代码问题)
| 根因 | 定位 | 解决 |
|---|---|---|
| 代码没关闭连接 | PROCESSLIST 里大量 Sleep 且 time 很大 |
代码里显式 mysqli_close() / $pdo = null;或依赖脚本结束自动释放(不要用持久连接) |
用了持久连接(pconnect / PDO::ATTR_PERSISTENT) |
连接数等于 php-fpm 进程数且不下降 | php-fpm 场景下持久连接收益小、风险大,建议关闭 |
| 慢查询堆积 | PROCESSLIST 里大量 Query 且 time 很大 |
优化 SQL、加索引(见 6.6 慢查询日志) |
| 锁等待 | state 为 Waiting for table lock / Lock wait |
SHOW ENGINE INNODB STATUS 找锁冲突;减小事务粒度 |
| php-fpm 进程数开得过多 | pm.max_children 远超实际需要 |
按 6.3 重新估算 |
| 遭遇攻击/爬虫刷接口 | 访问日志中大量同源请求 | Nginx 限流 limit_req、封 IP |
| 未使用连接池 | 每请求新建连接,高并发时握手开销大 | 引入 ProxySQL / MaxScale 做连接池复用 |
Nginx 限流配置(防止请求洪峰打穿数据库):
http {
# 按客户端 IP 限流,每秒 10 个请求,共享内存区 10MB
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
# 按服务端限流
limit_req_zone $server_name zone=perserver:10m rate=1000r/s;
# 并发连接数限制
limit_conn_zone $binary_remote_addr zone=addr:10m;
server {
location ~ \.php$ {
limit_req zone=perip burst=20 nodelay;
limit_req zone=perserver burst=2000;
limit_conn addr 20;
limit_req_status 429;
fastcgi_pass unix:/var/run/php-fpm/fastcgi.socket;
include fastcgi.conf;
}
}
}
7.9 通用排查命令集
# ── 配置语法检查 ──
nginx -t # Nginx(yum 版)
/usr/local/nginx/sbin/nginx -t # Nginx(源码版)
httpd -t # Apache
apachectl configtest # Apache
/usr/local/php/sbin/php-fpm -t # php-fpm(源码版)
php-fpm -t # php-fpm(yum 版)
php -l script.php # PHP 语法检查
mysqld --verbose --help > /dev/null # MySQL 配置检查
# ── 日志(排错第一现场)──
tail -f /var/log/nginx/error.log
tail -f /usr/local/nginx/logs/error.log
tail -f /var/log/nginx/access.log
tail -f /var/log/httpd/error_log
tail -f /var/log/httpd/access_log
tail -f /var/log/php-fpm/error.log
tail -f /var/log/php-fpm/www-error.log
tail -f /var/log/php-fpm/www-slow.log
tail -f /var/log/php/php-fpm_err.log
tail -f /var/log/mysql/mysql_err.log
tail -f /var/log/mysqld/mysql-err.log
tail -f /var/log/mysql/slow.log
tail -f /var/log/messages # 系统级错误、OOM、segfault
grep denied /var/log/audit/audit.log # SELinux 拒绝记录
journalctl -xe -u nginx
journalctl -xe -u php-fpm
journalctl -xe -u mariadb
# ── 端口与连接 ──
lsof -i:80
lsof -i:443
lsof -i:3306
lsof -i:9000
netstat -tunlp | grep -E '80|9000|3306'
ss -tunlp | grep -E '80|9000|3306'
ss -s # 连接状态汇总
netstat -an | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' # TCP 状态统计
# ── 进程 ──
ps -ef | grep -E 'nginx|php-fpm|mysql|httpd'
ps aux --sort=-%mem | head -20 # 按内存排序
ps -C php-fpm --no-headers | wc -l # php-fpm 进程数
ps -C nginx --no-headers | wc -l
pstree -p $(pgrep -o php-fpm) # 看主从进程树
# ── 资源 ──
top
htop
free -m
df -h # 磁盘空间(满了会导致各种诡异错误)
df -i # inode 使用率(小文件多时会先耗尽)
iostat -x 1 5 # 磁盘 IO
vmstat 1 5
uptime # 负载
# ── 文件描述符 ──
ulimit -n
cat /proc/$(pgrep -o nginx)/limits | grep 'open files'
lsof -p $(pgrep -o php-fpm) | wc -l
# ── HTTP 请求测试 ──
curl -I http://127.0.0.1/ # 只看响应头
curl -v http://127.0.0.1/index.php # 详细过程
curl -o /dev/null -s -w '连接:%{time_connect}s 首字节:%{time_starttransfer}s 总计:%{time_total}s 状态:%{http_code}\n' http://127.0.0.1/index.php
curl -H 'Host: www.example.com' http://127.0.0.1/ # 测虚拟主机
# ── 压力测试 ──
ab -n 1000 -c 100 http://127.0.0.1/index.php # Apache Bench
ab -n 1000 -c 100 -k http://127.0.0.1/ # 开启 keepalive
webbench -c 500 -t 30 http://127.0.0.1/
# ── php-fpm 状态 ──
curl 'http://127.0.0.1/fpm-status'
curl 'http://127.0.0.1/fpm-status?full'
curl 'http://127.0.0.1/fpm-ping'
# ── Nginx 状态(需 --with-http_stub_status_module)──
curl http://127.0.0.1/nginx_status
# ── PHP 信息 ──
php -v # 版本
php -m # 已加载扩展
php -i | grep 'Loaded Configuration File' # php.ini 实际路径
php --ini
php -r 'phpinfo();' | grep -i opcache
# ── 数据库 ──
mysql -uroot -p -e "SHOW FULL PROCESSLIST;"
mysql -uroot -p -e "SHOW GLOBAL STATUS LIKE 'Threads%';"
mysql -uroot -p -e "SHOW GLOBAL STATUS LIKE 'Max_used_connections';"
mysql -uroot -p -e "SHOW VARIABLES LIKE 'max_connections';"
mysql -uroot -p -e "SHOW ENGINE INNODB STATUS\G"
mysqladmin -uroot -p status
mysqladmin -uroot -p extended-status
mysqladmin -uroot -p processlist
# ── SELinux ──
getenforce
getsebool -a | grep httpd
ls -Z /usr/share/nginx/html/
# ── 防火墙 ──
firewall-cmd --list-all
iptables -L -n -v
八、练习
8.1 概念题
1. LAMP 与 LNMP 的区别是什么?各组件分别承担什么角色?
参考答案:区别只在 Web 服务器——LAMP 用 Apache(httpd),LNMP 用 Nginx。角色分工:Linux 是操作系统底座,提供进程调度、文件系统、网络栈与权限体系;Apache/Nginx 是 Web 服务器,监听 80/443 接收 HTTP 请求,静态资源直接读盘返回、动态请求交给 PHP;MySQL/MariaDB 是关系型数据库,监听 3306 持久化存储业务数据;PHP 是服务端脚本解释器,执行业务逻辑、通过驱动读写数据库并生成 HTML。
2. 描述用户访问 http://www.a.com/index.php 时,LNMP 环境下的完整请求处理流程。
参考答案:① 浏览器 DNS 解析域名得到 IP,与服务器 80 端口建立 TCP 连接并发送 HTTP 请求;② Nginx 按 server_name 匹配虚拟主机、按 root 定位文件路径;③ 请求匹配 location ~ \.php$,Nginx 按 FastCGI 协议封装请求,通过 fastcgi_pass 指定的 socket/TCP 转发给 php-fpm,并用 fastcgi_param SCRIPT_FILENAME 告知脚本真实路径;④ php-fpm 的 worker 进程调用 PHP 解释器执行脚本;⑤ 脚本中的数据库操作通过 mysqli/PDO 连接 MySQL 3306(或本地 socket),MySQL 执行 SQL 返回结果集;⑥ PHP 拼装 HTML 沿 FastCGI 通道回传给 Nginx;⑦ Nginx 封装成 HTTP 响应返回浏览器;⑧ 浏览器渲染 HTML 并对其中的 css/js/图片发起新请求,由 Nginx 直接返回静态资源。
3. 解释 CGI、FastCGI、php-fpm、mod_php 四者的关系。
参考答案:CGI 是一种协议/规范,定义 Web 服务器与外部程序交换数据的方式,缺陷是每请求 fork 一个新进程、开销巨大;FastCGI 是 CGI 的改进协议,把"每请求一进程"改为常驻进程池,通过 socket 反复接收请求,省掉 fork 与解释器初始化开销;php-fpm 是 FastCGI 协议的一个具体实现程序,即 PHP 官方的 FastCGI 进程管理器,负责启动/回收 worker、动态伸缩进程数、平滑重载与慢日志;mod_php 是不同的技术路线,PHP 直接编译成 Apache 动态模块运行在 httpd 进程内,不走 CGI/FastCGI 协议。一句话:CGI 太慢 → FastCGI 用常驻进程池改进 → php-fpm 是 PHP 对 FastCGI 的官方实现 → mod_php 走的是把 PHP 塞进 Apache 的另一条路。
4. 为什么 Nginx 不能像 Apache 那样用 mod_php 直接解析 PHP?这带来了什么优劣?
参考答案:Nginx 的架构是 epoll 事件驱动的异步非阻塞模型,没有 Apache 那样的模块内嵌解释器机制(Apache 的 apxs/DSO + prefork 每进程一请求的同步模型才适合内嵌 PHP),因此只能通过 FastCGI 把请求转给独立的 php-fpm 进程。优势:静态请求完全不涉及 PHP,内存占用可控;PHP 崩溃不影响 Nginx(只表现为 502);Nginx 与 PHP 可独立平滑重载、独立扩容,甚至部署在不同机器;天然支持动静分离。劣势:存在跨进程的 FastCGI 通信开销;配置更复杂(fastcgi_pass 与 listen 必须对齐、SCRIPT_FILENAME 必须正确);不支持 .htaccess,伪静态规则必须写在 Nginx 配置里。
5. pm 的三种进程管理模式有何区别?分别适用什么场景?
参考答案:static 启动时就创建 pm.max_children 个进程并固定不变,内存占用恒定且较高,无进程创建/销毁开销,响应最快最稳定,适合内存充足的专用 PHP 服务器与生产高负载;dynamic 启动 pm.start_servers 个进程,按负载在 pm.min_spare_servers ~ pm.max_children 之间动态增减,内存随负载浮动,是默认且最通用的模式,适合与其它服务混跑、负载波动大的场景;ondemand 启动时不创建任何 worker,收到请求才创建、空闲 pm.process_idle_timeout 后销毁,空闲时内存几乎为 0,适合内存紧张的小内存 VPS、低访问量站点与多 pool 共享主机,缺点是冷启动有延迟。
6. fastcgi_params 与 fastcgi.conf 的区别是什么?
参考答案:两个文件内容几乎完全一致,唯一差别是 fastcgi.conf 多了一行 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;。所以 include fastcgi_params; 时必须自己额外写一行 SCRIPT_FILENAME,否则 PHP 收不到脚本路径,报 No input file specified 或 File not found;而 include fastcgi.conf; 可直接使用,无需再写(Nginx 0.8.30 起提供)。
7. Unix Socket 与 TCP 方式连接 php-fpm 各有什么优劣?如何选择?
参考答案:Unix Socket 不走网络协议栈,性能约高 5%~15%,不占端口,但仅限同一台机器,且必须正确设置 socket 文件的 listen.owner / listen.group / listen.mode(配错就是 502 Permission denied),高并发下还可能出现 Resource temporarily unavailable 需调大 listen.backlog 与内核 net.core.somaxconn。TCP 方式有协议栈开销,但支持 Nginx 与 PHP 跨机器分离部署,便于用 upstream 做多台 PHP 节点负载均衡,需注意 listen.allowed_clients 白名单与防火墙。单机部署选 socket,需要跨机或水平扩展 PHP 层选 TCP。
8. CentOS 6 与 CentOS 7 在数据库、服务管理、PHP 版本上有哪些差异?
参考答案:数据库方面,CentOS 6 默认仓库提供 MySQL 5.1(包名 mysql-server,服务名 mysqld),CentOS 7 起默认换成 MariaDB 5.5(包名 mariadb-server,服务名 mariadb),MariaDB 是 MySQL 的社区分支,客户端命令、SQL 语法与通信协议完全兼容。服务管理方面,CentOS 6 用 SysV init(service xxx start + chkconfig xxx on,脚本放 /etc/init.d/),CentOS 7 用 systemd(systemctl start/enable xxx,unit 文件放 /usr/lib/systemd/system/)。PHP 方面,CentOS 6 默认 PHP 5.3 且 php-fpm 需从 EPEL/remi 源装、默认监听 127.0.0.1:9000;CentOS 7 默认 PHP 5.4、php-fpm 在基础仓库中、部分版本默认用 socket。防火墙上 CentOS 6 用 iptables,CentOS 7 用 firewalld。
9. php.ini-development 与 php.ini-production 有什么区别?生产环境该用哪个?
参考答案:php.ini-development 面向开发环境,开启 display_errors、display_startup_errors,error_reporting=E_ALL,便于调试;php.ini-production 面向生产环境,关闭 display_errors(错误只写日志不显示给用户)、关闭 expose_php、调低 error_reporting 级别,安全性更高。生产环境必须用 php.ini-production,避免向用户暴露服务器路径、SQL 语句、账号等敏感信息。
10. 为什么做了负载均衡之后必须把 session 集中存储?
参考答案:PHP 默认把 session 以文件形式存在本机磁盘的 session.save_path。做了负载均衡后,用户第一次请求可能落在 A 机(session 文件写在 A 机),第二次请求落在 B 机,而 B 机上没有该 session 文件,就会表现为反复掉登录、购物车丢失等。把 session 集中存到 Redis 或 Memcached,所有 Web 节点读写同一份数据即可解决;同理,用户上传的文件也必须放共享存储(NFS/对象存储),否则 A 机上传后 B 机看不到。
11. utf8 与 utf8mb4 有什么区别?为什么新项目应该用 utf8mb4?
参考答案:MySQL 的 utf8(即 utf8mb3)每个字符最多只占 3 字节,是"残缺的 UTF-8",无法存储 emoji 和部分生僻汉字,插入时会报 Incorrect string value: '\xF0\x9F...';utf8mb4 每字符最多 4 字节,是完整的 UTF-8 实现,可存 emoji 与全部 Unicode 字符。新项目一律用 utf8mb4 + utf8mb4_general_ci(或 utf8mb4_unicode_ci)。
12. OPcache 的作用原理是什么?生产环境的 validate_timestamps 该怎么设?
参考答案:OPcache 把 PHP 脚本编译后的字节码(opcode)缓存在共享内存中,后续请求直接执行缓存的 opcode,免去每次请求重复的词法分析、语法分析和编译过程,通常带来 2~3 倍性能提升。validate_timestamps 控制是否检查源文件时间戳判断缓存是否失效:开发环境设 1 并把 revalidate_freq=0,改代码立即生效;生产环境设 0 性能最好(完全不做 stat 调用),但代价是发布新代码后必须 systemctl reload php-fpm(或 kill -USR2)、或在代码里调 opcache_reset(),否则会一直执行旧代码。
8.2 实操题
1. 用 yum 方式在 CentOS 7 上搭建完整 LAMP 环境,并写出验证 PHP 与数据库均可用的完整命令与代码。
参考答案:
# 环境准备
systemctl stop firewalld && systemctl disable firewalld
setenforce 0 && sed -i 's/SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
yum install -y epel-release
# Apache
yum install -y httpd httpd-devel
systemctl start httpd && systemctl enable httpd
# MariaDB
yum install -y mariadb mariadb-server
systemctl start mariadb && systemctl enable mariadb
mysqladmin -uroot password "123"
# PHP
yum install -y php php-mysql php-gd php-mbstring php-xml php-pear curl curl-devel
systemctl restart httpd
# 验证模块与端口
httpd -M | grep php
lsof -i:80 ; lsof -i:3306
# 建测试库
mysql -uroot -p123 -e "CREATE DATABASE testdb; \
GRANT ALL ON testdb.* TO testuser@'localhost' IDENTIFIED BY '123456'; FLUSH PRIVILEGES;"
# 测试页
echo "<?php phpinfo(); ?>" > /var/www/html/index.php
cat > /var/www/html/db.php <<'EOF'
<?php
header('Content-Type: text/html; charset=utf-8');
$link = mysqli_connect('127.0.0.1', 'testuser', '123456', 'testdb');
if (!$link) { die('连接失败: ' . mysqli_connect_error()); }
mysqli_set_charset($link, 'utf8mb4');
$row = mysqli_fetch_assoc(mysqli_query($link, 'SELECT VERSION() AS ver'));
echo "数据库连接成功!版本:" . $row['ver'];
mysqli_close($link);
EOF
chown -R apache.apache /var/www/html/
# 浏览器验证
curl -I http://127.0.0.1/index.php
curl http://127.0.0.1/db.php
浏览器访问 http://IP/index.php 应见 phpinfo 页(Server API = Apache 2.0 Handler),访问 http://IP/db.php 应显示"数据库连接成功"。
2. 写出源码编译 Apache 2.4 的完整依赖顺序与 configure 命令,并说明 --enable-so、--with-apr、--sysconfdir 三个参数的作用。
参考答案:依赖顺序为 apr → apr-util → httpd(apr-util 需 --with-apr 指向 apr;httpd 需同时指向两者)。
# apr
wget http://mirrors.shu.edu.cn/apache//apr/apr-1.6.5.tar.gz
tar xf apr-1.6.5.tar.gz -C /usr/src/ && cd /usr/src/apr-1.6.5/
./configure --prefix=/usr/local/apr-httpd/ && make && make install
# apr-util
wget https://mirrors.tuna.tsinghua.edu.cn/apache//apr/apr-util-1.6.1.tar.gz
tar xf apr-util-1.6.1.tar.gz -C /usr/src/ && cd /usr/src/apr-util-1.6.1/
./configure --prefix=/usr/local/apr-util-httpd/ --with-apr=/usr/local/apr-httpd/ && make && make install
# httpd
wget http://mirror.bit.edu.cn/apache/httpd/httpd-2.4.37.tar.gz
tar xf httpd-2.4.37.tar.gz -C /usr/src/ && cd /usr/src/httpd-2.4.37/
./configure --prefix=/usr/local/apache243 --enable-mods-shared=all --enable-deflate \
--enable-speling --enable-cache --enable-file-cache --enable-disk-cache \
--enable-mem-cache --enable-so --enable-expires=shared --enable-rewrite=shared \
--enable-static-support --sysconfdir=/etc/httpd --with-z=/usr/local/zlib/ \
--with-apr=/usr/local/apr-httpd/ --with-apr-util=/usr/local/apr-util-httpd/ \
--with-pcre=/usr/ --disable-userdir
make && make install
三个参数的作用:--enable-so 启用 DSO 动态模块加载支持(mod_so),这是后续 PHP 用 --with-apxs2 把自己编译成 Apache 模块的前提,不开就无法加载 libphp5.so;--with-apr / --with-apr-util 指定 Apache 依赖的 APR 跨平台运行时库与其工具库的安装路径,不指定会使用源码内置的旧版本或直接编译失败;--sysconfdir=/etc/httpd 把配置文件安装到 /etc/httpd 而不是默认的 $prefix/conf,符合 Linux 系统惯例便于统一管理。
3. 写出源码编译 PHP 时 LAMP 与 LNMP 各自必需的关键参数,并解释区别。
参考答案:
# LAMP:编译成 Apache 模块
./configure --prefix=/usr/local/php \
--with-config-file-path=/usr/local/php/etc \
--with-apxs2=/usr/local/apache243/bin/apxs \
--with-mysqli=/usr/local/mysql/bin/mysql_config \
--with-pdo-mysql=/usr/local/mysql \
--with-mysql-sock=/data/mysql.sock \
--enable-mbstring --with-curl --with-openssl
# LNMP:编译出 php-fpm
./configure --prefix=/usr/local/php \
--with-config-file-path=/usr/local/php/etc \
--enable-fpm --with-fpm-user=nginx --with-fpm-group=nginx \
--enable-mysqlnd --with-mysqli --with-pdo-mysql=mysqlnd \
--with-mysql-sock=/var/lib/mysql/mysql.sock \
--enable-mbstring --with-curl --with-openssl --with-gd
区别:--with-apxs2 是 LAMP 的关键,它调用 Apache 的 apxs 工具把 PHP 编译成 Apache 动态模块(生成 libphp5.so)并自动向 httpd.conf 写入 LoadModule;--enable-fpm 是 LNMP 的必需参数,它编译出独立的 php-fpm 可执行文件(/usr/local/php/sbin/php-fpm)作为 FastCGI 进程管理器供 Nginx 调用,同时 --with-fpm-user/group 指定其运行身份(应与 Nginx 一致)。两个参数可以同时使用,这样会同时产出 apache2handler 与 fpm-fcgi 两种 SAPI。
4. 在 LNMP 上写出一个完整的 server 段,要求:网站根目录 /data/www,PHP 走 unix socket,静态图片缓存 30 天且不记日志,禁止上传目录执行 PHP,支持伪静态。
参考答案:
server {
listen 80;
server_name www.example.com;
root /data/www;
index index.php index.html;
charset utf-8;
access_log /var/log/nginx/example_access.log main;
error_log /var/log/nginx/example_error.log;
client_max_body_size 100m;
# 伪静态入口
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# 动态请求
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/var/run/php-fpm/fastcgi.socket;
fastcgi_index index.php;
include fastcgi.conf;
fastcgi_read_timeout 120s;
fastcgi_buffer_size 64k;
fastcgi_buffers 8 64k;
fastcgi_hide_header X-Powered-By;
}
# 静态图片缓存 30 天且不记日志
location ~* \.(gif|jpg|jpeg|png|bmp|webp|ico|svg)$ {
expires 30d;
access_log off;
add_header Cache-Control "public, max-age=2592000";
try_files $uri =404;
}
location ~* \.(css|js)$ {
expires 12h;
access_log off;
}
# 禁止上传目录执行 PHP
location ~ ^/(uploads|data|attachment)/.*\.(php|php5|phtml)$ {
deny all;
}
# 禁止访问隐藏文件与备份文件
location ~ /\.(ht|git|svn|env) { deny all; }
location ~* \.(bak|sql|log|swp|old)$ { deny all; }
}
5. 一台 8GB 内存的服务器,同时跑 Nginx + php-fpm + MySQL,实测单个 php-fpm 进程占用 60MB。请计算并写出 php-fpm 的进程管理配置,并说明如何验证是否够用。
参考答案:
总内存 8 GB = 8192 MB
系统预留 约 500 MB
MySQL(buffer_pool 2G + 其它) 约 2500 MB
Nginx 约 200 MB
其它(Redis、监控等) 约 500 MB
──────────────────────────────
可分配给 PHP ≈ 8192 - 3700 = 4492 MB
pm.max_children = 4492 ÷ 60 ≈ 74 → 保守取 70
pm = dynamic
pm.max_children = 70
pm.start_servers = 18 ; ≈ max_children × 25%
pm.min_spare_servers = 8 ; ≈ max_children × 12%
pm.max_spare_servers = 35 ; ≈ max_children × 50%
pm.max_requests = 1000
rlimit_files = 65535
slowlog = /var/log/php-fpm/www-slow.log
request_slowlog_timeout = 5s
pm.status_path = /fpm-status
验证方法:开启 pm.status_path 后 curl 'http://127.0.0.1/fpm-status?full',重点看两个指标——max children reached 若大于 0 说明曾达到进程上限、有请求排队(会引发 502/504),需要在内存允许的前提下调大 pm.max_children 或优化代码;listen queue 大于 0 说明连接在积压,同时要调大 listen.backlog 与内核 net.core.somaxconn。反之若 idle processes 长期居高不下,说明进程开多了在浪费内存,应调小或改用 ondemand。同时用 ps --no-headers -o rss -C php-fpm | awk '{s+=$1} END {print s/1024" MB"}' 持续观察实际总内存占用,确保不触发 swap。
6. 用户反馈网站打开是 502 Bad Gateway,写出你的完整排查步骤。
参考答案:
# ① 先看 Nginx 错误日志拿到确切原因(最重要的一步)
tail -50 /var/log/nginx/error.log
# ② php-fpm 是否在运行
systemctl status php-fpm ; ps -ef | grep php-fpm
# 没跑则启动,启动失败看原因
systemctl start php-fpm ; journalctl -xe -u php-fpm
# ③ socket 是否存在、属主与权限是否正确
ls -l /var/run/php-fpm/fastcgi.socket # 期望属主属组为 nginx
# ④ 两端地址是否一致
grep -r 'fastcgi_pass' /etc/nginx/
grep -E '^\s*listen' /etc/php-fpm.d/www.conf
# ⑤ TCP 模式检查端口
netstat -tunlp | grep 9000
# ⑥ 检查是否进程数用尽
curl 'http://127.0.0.1/fpm-status' # 看 max children reached
grep 'max_children' /var/log/php-fpm/error.log
# ⑦ 检查是否 worker 崩溃
grep segfault /var/log/messages
# ⑧ 检查 SELinux
getenforce ; grep denied /var/log/audit/audit.log
再按日志内容对症处理:No such file or directory → socket 路径不一致或 php-fpm 未启动;Permission denied → 设 listen.owner/group = nginx、listen.mode = 0660,并 chown -R nginx.nginx /var/run/php-fpm/;Connection refused → php-fpm 未监听该端口;Resource temporarily unavailable → 调大 listen.backlog 与 net.core.somaxconn;Connection reset by peer → PHP worker 段错误,排查扩展冲突;upstream sent too big header → 调大 fastcgi_buffer_size 与 fastcgi_buffers;reached pm.max_children → 调大进程数。
7. 用户上传 30MB 的文件失败,返回 413,写出需要修改的所有参数及其大小关系。
参考答案:返回 413 说明是 Nginx 层拦截的(client_max_body_size 默认仅 1MB)。但只改 Nginx 还不够,需四处同时调整,且必须满足 client_max_body_size >= post_max_size >= upload_max_filesize、memory_limit >= post_max_size:
# ① Nginx
http {
client_max_body_size 100m;
client_body_buffer_size 1m;
client_body_timeout 300s;
}
; ② php.ini
upload_max_filesize = 50M ; 单文件上限(> 30M)
post_max_size = 60M ; 整个 POST 体,须 > upload_max_filesize
memory_limit = 128M ; 须 >= post_max_size
max_execution_time = 600
max_input_time = 600
max_file_uploads = 50
upload_tmp_dir = /var/tmp/php_upload
; ③ php-fpm www.conf
request_terminate_timeout = 600s
# ④ 若上传耗时长,还要调大读超时
location ~ \.php$ { fastcgi_read_timeout 600s; }
mkdir -p /var/tmp/php_upload && chown nginx.nginx /var/tmp/php_upload && chmod 1777 /var/tmp/php_upload
nginx -t && systemctl reload nginx && systemctl restart php-fpm
php -i | grep -E 'upload_max_filesize|post_max_size|memory_limit'
同时确认目标上传目录对 nginx 用户可写(chown -R nginx.nginx ./uploads),SELinux 开启时执行 chcon -R -t httpd_sys_rw_content_t ./uploads。
8. 在 LNMP 上部署 Discuz! 论坛,写出从下载到安装完成的完整命令,包括权限与数据库配置。
参考答案:
# ① 下载
yum install -y git
cd /usr/src/
git clone https://gitee.com/ComsenzDiscuz/DiscuzX.git
# ② 部署到网站根目录
mv DiscuzX/upload/* /usr/share/nginx/html/
# ③ 权限与属主
cd /usr/share/nginx/html/
chown -R nginx.nginx .
find . -type d -exec chmod 755 {} \;
find . -type f -exec chmod 644 {} \;
chmod -R 777 ./config ./data ./uc_client/data ./uc_server/data
chown -R nginx.nginx ./config ./data ./uc_client/data ./uc_server/data
# ④ 建库授权
mysql -uroot -p123 <<'EOF'
CREATE DATABASE discuz DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
GRANT ALL ON discuz.* TO discuz@'localhost' IDENTIFIED BY '123';
FLUSH PRIVILEGES;
EOF
# ⑤ 确认服务与 PHP 扩展
systemctl status nginx php-fpm mariadb
php -m | grep -E 'mysqli|gd|mbstring|zip'
# ⑥ 浏览器访问 http://IP/ 进入安装向导
# 数据库服务器: localhost(连不上改 127.0.0.1)
# 数据库名: discuz 用户: discuz 密码: 123 表前缀: pre_
# 管理员账号密码自定义
# ⑦ 安装完成后加固
mv /usr/share/nginx/html/install /usr/share/nginx/html/install.bak
# ⑧ 验证
curl -I http://127.0.0.1/forum.php
若安装向导报"无法连接数据库/socket 找不到",把数据库地址由 localhost 改为 127.0.0.1(localhost 走 Unix socket,127.0.0.1 走 TCP,可绕开 socket 路径不一致的问题)。
9. 配置 Nginx 实现动静分离,并把 session 存到 Redis,写出关键配置。
参考答案:
# 动静分离
server {
listen 80;
server_name www.example.com;
root /usr/share/nginx/html;
index index.php index.html;
# 动态 → php-fpm
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/var/run/php-fpm/fastcgi.socket;
include fastcgi.conf;
}
# 静态 → Nginx 直出 + 长缓存
location ~* \.(gif|jpg|jpeg|png|webp|ico|svg|mp4)$ {
expires 30d; access_log off;
add_header Cache-Control "public, max-age=2592000";
}
location ~* \.(css|js)$ { expires 12h; access_log off; gzip_static on; }
location ~* \.(woff|woff2|ttf|eot)$ { expires 1y; access_log off; }
location / { try_files $uri $uri/ /index.php?$query_string; }
}
# session 存 Redis
yum install -y redis php-pecl-redis
systemctl start redis && systemctl enable redis
; php.ini
session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379?auth=YourRedisPass&database=1&timeout=2.5"
session.gc_maxlifetime = 1440
systemctl restart php-fpm
# 验证:访问会写 session 的页面后
redis-cli -a YourRedisPass keys 'PHPREDIS_SESSION:*'
10. 描述从单机 LNMP 演进到"负载均衡 + 读写分离"架构的完整路径,以及每一步需要解决的关键问题。
参考答案:
- 阶段一 单机 All-in-One:Nginx + php-fpm + MySQL 同机。瓶颈是资源争抢与单点故障。
- 阶段二 动静分离 + 服务拆分:静态资源交 Nginx 直出并设长过期,数据库独立到单独机器,开启 OPcache,热点数据与 session 放 Redis。解决的关键问题是让各组件独立扩容与调优、互不抢占 CPU/内存。
- 阶段三 负载均衡(Web 层水平扩展):前端加 LVS 或 Nginx 做四层/七层负载均衡,后端多台 Web(Nginx + php-fpm)。必须解决四个问题:① session 集中存储到 Redis,否则用户反复掉登录;② 上传文件放共享存储(NFS/GlusterFS/对象存储),否则 A 机上传 B 机看不到;③ 多台 Web 代码一致性(Git 部署 / rsync / CI-CD / 容器镜像);④ 负载均衡器自身高可用(Keepalived + VIP 双机热备),否则只是把单点从 Web 移到了 LB。(详见 12-01-LVS负载均衡.md)
- 阶段四 数据库读写分离 + 主从复制:一主多从,写走 Master、读走 Slave,通过 binlog 复制。实现方式为应用层区分主从连接或用 MyCat/ProxySQL/MaxScale 中间件自动路由。需解决:① 主从复制延迟导致"写后立即读"读到旧数据,此类场景需强制走主库;② Master 单点问题用 MHA/Orchestrator/MGR 解决。(详见 11-06-主从复制与读写分离.md)
- 阶段五 更大规模:CDN 边缘加速降低回源率、分库分表应对单表过亿、消息队列异步化耗时操作、微服务拆分、容器化编排(K8s)弹性伸缩、全链路监控(Prometheus + Grafana + ELK + APM)。
8.3 思考题
- 为什么 mod_php 方式下 Apache 必须使用
preforkMPM,而不能用worker或event?如果想在 Apache 上用eventMPM 又要跑 PHP,应该怎么做? pm.max_requests = 500这个参数存在的意义是什么?如果 PHP 代码和扩展都没有内存泄漏,把它设为 0(不限制)会更快吗?- 生产环境把
opcache.validate_timestamps设为 0 能提升性能,但发布代码后必须 reload php-fpm。如果一次 reload 会导致瞬时 502,如何做到无损发布? localhost与127.0.0.1连接 MySQL 的区别导致了很多"玄学"问题。除了改成127.0.0.1,还有哪些更根本的解决办法?- 动静分离把静态资源交给 Nginx 处理,但如果静态资源需要鉴权(比如付费下载的文件),单纯的
location匹配就不够了,该如何设计? - 假设某个 PHP 接口平均耗时 200ms,服务器
pm.max_children = 50,理论上这台机器每秒最多能处理多少个该接口的请求?如果要支撑每秒 1000 次调用,有哪几种思路?
九、附录:三种搭建方式速查对照
| 项目 | yum 版 LAMP | 源码版 LAMP | yum 版 LNMP | 源码版 LNMP |
|---|---|---|---|---|
| Web 服务器 | httpd(yum) |
/usr/local/apache243 |
nginx(yum) |
/usr/local/nginx |
| Web 配置文件 | /etc/httpd/conf/httpd.conf |
/etc/httpd/httpd.conf |
/etc/nginx/nginx.conf |
/usr/local/nginx/conf/nginx.conf |
| 网站根目录 | /var/www/html/ |
/usr/local/apache243/htdocs |
/usr/share/nginx/html/ |
/usr/local/nginx/html |
| PHP 接入方式 | mod_php(php.conf 自动配) |
mod_php(--with-apxs2) |
php-fpm | php-fpm(--enable-fpm) |
| PHP 配置文件 | /etc/php.ini |
/usr/local/php/etc/php.ini |
/etc/php.ini |
/usr/local/php/etc/php.ini |
| php-fpm 配置 | — | — | /etc/php-fpm.d/www.conf |
/usr/local/php/etc/php-fpm.conf |
| PHP↔Web 通道 | 进程内调用 | 进程内调用 | /var/run/php-fpm/fastcgi.socket |
/tmp/php-fpm.sock |
| 数据库 | mariadb(C7)/ mysqld(C6) |
/usr/local/mysql |
mariadb |
/usr/local/mysql |
| 数据目录 | /var/lib/mysql |
/data |
/var/lib/mysql |
/var/lib/mysql |
| 数据库 socket | /var/lib/mysql/mysql.sock |
/data/mysql.sock |
/var/lib/mysql/mysql.sock |
/var/lib/mysql/mysql.sock |
| 运行用户 | apache |
apache |
nginx |
nginx |
| 服务管理 | systemctl(C7)/ service(C6) |
apachectl + 自建 unit/init |
systemctl |
/etc/init.d/{nginx,php-fpm} + 自建 unit |
| 优点 | 最快、依赖自动、升级方便 | 版本与参数完全可控 | 最快、性能好 | 版本与参数完全可控、性能好 |
| 缺点 | 版本旧、路径固定 | 编译耗时、需自维护脚本 | 版本旧 | 编译耗时、需自维护脚本 |
| 适用 | 学习、测试、快速交付 | 生产、特定版本需求 | 学习、测试、中小生产 | 生产、高并发定制 |
此处评论已关闭