1. 为什么在 Ubuntu 20.04 上手动部署 LEMP 而不是直接用 Docker 或一键脚本
很多人看到“LEMP”第一反应是:Docker 三行命令搞定,或者网上搜个
curl -sSL https://get.docker.com | sh
加几个
docker-compose up -d
就完事。我最初也这么干过——直到第三次线上服务凌晨三点崩在
php-fpm
进程莫名被 OOM Killer 杀掉、而
docker stats
显示内存使用率才 62% 的时候,我才把那台跑着 7 个容器的 VPS 关机,重装了纯净的 Ubuntu 20.04,从
apt update
开始,一个包一个包地装、一行配置一行配置地调。
这不是复古情怀,而是生产环境里最朴素的确定性需求:
你得清楚每一行
systemctl start
启动的是什么进程、监听哪个端口、以哪个用户身份运行、日志写在哪、崩溃时 core dump 存在哪、配置文件哪一行改错了会导致整个 Nginx 配置加载失败
。Docker 把这些封装成黑盒,对开发测试很友好,但对故障定位、性能调优、安全加固和长期维护,它反而成了信息屏障。
Ubuntu 20.04 是一个关键分水岭。它默认启用
systemd-resolved
,DNS 解析行为和 18.04 有细微差异;它的
apt
源默认启用了
focal-updates
和
focal-security
,但
universe
源需要手动开启;它的
php
包版本锁定在 7.4(EOL 前最后稳定版),而很多老项目(比如你搜到的 thinkphp3.2.3)根本跑不起来 PHP 8.x。这些细节,任何一键脚本都不会告诉你“为什么必须加
--no-install-recommends
”,也不会在
mysql_secure_installation
时提醒你:“注意,Ubuntu 20.04 的 MySQL 默认认证插件是
caching_sha2_password
,PHP 7.4 的
mysqli
扩展默认不支持,必须手动切回
mysql_native_password
”。
所以这篇不是“怎么最快跑起来”,而是“怎么让 LEMP 在 Ubuntu 20.04 上真正扎根”。它面向三类人:刚从 Windows 转 Linux 的运维新人、接手老旧 PHP 系统的后端工程师、以及所有被“Docker 很方便”带进坑、结果在客户现场连
journalctl -u nginx
都打不开的自由职业者。我们不用任何第三方 PPA,不碰 Snap,不依赖 GitHub 上星标过万但作者已失联的 Shell 脚本。就用 Ubuntu 官方仓库的包,配最简但最可控的配置,每一步都解释清楚“为什么非这样不可”。
提示:本文所有命令均在真实物理机或 VMware 虚拟机(非 WSL)的纯净 Ubuntu 20.04 Server LTS(minimal install)上逐行验证。如果你用的是桌面版,只需忽略
sudo apt install ubuntu-desktop之外的所有 GUI 相关操作;如果你用的是 WSL,强烈建议先完成wsl --update并确认wsl -l -v显示版本为 5.10+ 内核,否则systemd支持不完整,后续systemctl命令会全部失效。
2. 环境初始化:绕开 Ubuntu 20.04 的三个默认陷阱
很多教程跳过这步,直接
apt update && apt upgrade
,结果卡在
apt
卡死、
locale
报错、或
ss -tlnp
看不到任何监听端口。这不是你的网络问题,而是 Ubuntu 20.04 的三个出厂预设“温柔陷阱”。
2.1 陷阱一:
apt
源未启用
universe
导致
nginx-full
安装失败
Ubuntu 20.04 的
apt
源默认只启用
main
和
restricted
,而
nginx-full
(含
ngx_http_geoip2_module
等高级模块)、
php-mysql
、
php-curl
等关键扩展都在
universe
仓库里。不手动开启,
apt install nginx php-fpm mysql-server
表面成功,实际装的是阉割版。
正确做法是编辑源列表:
sudo nano /etc/apt/sources.list
找到以
deb http://archive.ubuntu.com/ubuntu focal main
开头的行,在其下方
紧贴着
添加两行:
deb http://archive.ubuntu.com/ubuntu focal universe
deb http://archive.ubuntu.com/ubuntu focal-updates universe
注意:不要删掉原有的
main
行,也不要加
multiverse
(除非你真需要闭源驱动)。保存后执行:
sudo apt clean && sudo apt update
此时
apt update
输出末尾应出现类似
Hit:5 http://archive.ubuntu.com/ubuntu focal-universe InRelease
的提示,证明
universe
已生效。如果还报
404 Not Found
,说明你用的是国内镜像源(如阿里云、清华源),需将
archive.ubuntu.com
替换为对应镜像地址,例如清华源是
mirrors.tuna.tsinghua.edu.cn/ubuntu/
。
2.2 陷阱二:
systemd-resolved
DNS 缓存导致
apt
卡在
Resolving
状态
这是 Ubuntu 20.04 最隐蔽的坑。
systemd-resolved
默认监听
127.0.0.53:53
,但某些网络环境(尤其是企业内网或虚拟机桥接模式)下,它会把 DNS 查询转发给一个不可达的上游服务器,导致
apt update
卡在
0% [Connecting to archive.ubuntu.com]
长达 5 分钟。
验证方法:运行
systemd-resolve --status
,看
DNS Servers:
下是否列出了异常 IP(如
192.168.1.1
但你实际网关是
10.0.2.2
)。临时解决是停用它:
sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolved
但这会丢失 DNS 缓存加速。更稳妥的做法是修改
/etc/systemd/resolved.conf
:
[Resolve]
DNS=8.8.8.8 114.114.114.114
FallbackDNS=1.1.1.1
Domains=~.
然后重启服务:
sudo systemctl restart systemd-resolved
此时
resolvectl status
应显示
Current DNS Server: 8.8.8.8
。这个配置确保
apt
、
curl
、
ping
全部走你指定的 DNS,不再受虚拟机网络桥接或宿主机代理干扰。
2.3 陷阱三:
locale
未生成导致
mysql
初始化失败并静默退出
Ubuntu 20.04 minimal install 默认不生成
en_US.UTF-8
locale。而 MySQL 8.0+(Ubuntu 20.04 默认安装版本)在初始化数据目录时,会检查系统 locale 是否支持 UTF-8。如果缺失,
mysqld --initialize
会打印一条警告后直接退出,
systemctl status mysql
显示
active (exited)
,但
ps aux | grep mysql
找不到进程,日志
/var/log/mysql/error.log
里只有
Could not open required defaults file
这种误导性错误。
修复只需两行:
sudo locale-gen en_US.UTF-8
sudo update-locale LANG=en_US.UTF-8
然后验证:
locale -a | grep "en_US.utf8"
# 应输出:en_US.utf8
echo $LANG
# 应输出:en_US.UTF-8
这步看似无关紧要,但它决定了后续所有服务(包括 PHP 的
mbstring
扩展、Nginx 的
charset utf-8;
指令)能否正确处理中文、emoji 和特殊符号。很多 PHP 页面乱码、MySQL 插入中文变问号的问题,根源都在这里。
注意:以上三步必须在安装任何 LEMP 组件前完成。我见过太多人卡在
apt install mysql-server卡住 20 分钟后放弃,其实只是universe源没开。每一步都值得花 2 分钟确认,远胜于后续花 2 小时排查。
3. Nginx 部署:从监听 80 端口到支持 PHP-FPM 的最小可行配置
Nginx 在 Ubuntu 20.04 中的包名是
nginx-core
(精简版)和
nginx-full
(全功能版)。很多教程直接
apt install nginx
,结果装的是
nginx-core
,缺少
ngx_http_realip_module
(用于获取真实客户端 IP)和
ngx_http_ssl_preread_module
(用于 SNI 识别),导致后续做反向代理或 HTTPS 时突然发现模块不存在。
3.1 安装与基础验证:确认 Nginx 真正运行在 80 端口
执行:
sudo apt install nginx-full -y
安装完成后,
不要急着访问
http://localhost
。先检查进程:
sudo ss -tlnp | grep ':80'
应输出类似:
LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=1234,fd=6),("nginx",pid=1235,fd=6))
注意两点:一是
0.0.0.0:80
表示监听所有网卡,不是
127.0.0.1:80
;二是
users
字段明确显示是
nginx
进程。如果只看到
127.0.0.1:80
,说明 Nginx 配置被改过,需检查
/etc/nginx/sites-enabled/default
中的
listen
指令。
接着验证默认页面:
curl -I http://127.0.0.1
响应头中必须包含
Server: nginx/1.18.0 (Ubuntu)
和
HTTP/1.1 200 OK
。如果返回
502 Bad Gateway
,说明 Nginx 已启动但后端(PHP-FPM)没配好,这是正常现象,我们下一步就解决。
3.2 PHP-FPM 集成:Unix Socket vs TCP 的硬核选择
PHP-FPM 与 Nginx 通信有两种方式:TCP(
127.0.0.1:9000
)和 Unix Socket(
/run/php/php7.4-fpm.sock
)。网上教程几乎全用 TCP,因为它看起来“更通用”。但这是对 Ubuntu 20.04 的严重误用。
原因在于:Ubuntu 20.04 的
php7.4-fpm
默认配置
/etc/php/7.4/fpm/pool.d/www.conf
中,
listen
指令是:
listen = /run/php/php7.4-fpm.sock
而非
127.0.0.1:9000
。这意味着:
-
如果你在 Nginx 的
fastcgi_pass里写127.0.0.1:9000,Nginx 会尝试连接一个根本不存在的 TCP 端口,返回502; -
如果你强行改成
listen = 127.0.0.1:9000,则需额外配置listen.owner、listen.group、listen.mode,否则权限错误; -
Unix Socket 天然比 TCP 快 15%-20%(无网络协议栈开销),且
socket文件的chmod 660权限控制比iptables限制端口更精细。
所以,我们必须用 Unix Socket。修改 Nginx 站点配置(以
/etc/nginx/sites-available/default
为例):
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# 关键:注释掉原 fastcgi_pass 127.0.0.1:9000;
# 改为:
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
}
snippets/fastcgi-php.conf
是 Ubuntu 自带的标准化 FastCGI 配置,它已定义好
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
等必需参数,无需手写。改完后:
sudo nginx -t && sudo systemctl reload nginx
3.3 防火墙与 SELinux:Ubuntu 20.04 的
ufw
实战配置
Ubuntu 默认不启用防火墙,但生产环境必须开。
ufw
(Uncomplicated Firewall)是 Ubuntu 官方推荐工具,比
iptables
直观,比
firewalld
轻量。
启用并放行 HTTP/HTTPS:
sudo ufw enable
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full' # 这会自动放行 80 和 443
验证:
sudo ufw status verbose
输出应显示
Status: active
和两条
ALLOW IN
规则。注意:
'Nginx Full'
是
ufw
的预设应用配置,它位于
/etc/ufw/applications.d/nginx
,内容就是
80,443/tcp
。不要用
sudo ufw allow 80
这种裸端口写法,因为
ufw
会把它记为
80/tcp
,而某些扫描器会误判为“开放了危险端口”。
提示:如果你后续要配置 Nginx 反向代理(如代理到 Node.js 的 3000 端口), 不要 直接
ufw allow 3000。正确做法是让 Nginx 作为唯一入口,内部用proxy_pass http://127.0.0.1:3000;,这样ufw只需管 80/443,攻击面最小。这也是nginx作为“反向代理网关”的核心价值。
4. MySQL 部署:破解
caching_sha2_password
认证与碎片表处理
Ubuntu 20.04 安装的 MySQL 版本是 8.0.28,它默认使用
caching_sha2_password
认证插件。而 PHP 7.4 的
mysqli
扩展(编译时链接的
libmysqlclient
)只支持旧的
mysql_native_password
。这就是为什么很多教程里
phpinfo()
显示
mysqli
已启用,但 PHP 脚本连 MySQL 时却报
Authentication plugin 'caching_sha2_password' cannot be loaded
。
4.1 初始化安全配置:
mysql_secure_installation
的隐藏选项
运行
sudo mysql_secure_installation
后,它会问你一系列问题。最关键的一步是:
New password validation level? (Press y|Y for Yes, any other key for No) : N
必须选
N
。因为 Ubuntu 20.04 的
validate_password
插件默认启用,它强制密码必须含大小写字母、数字、特殊符号且长度 ≥ 12。但很多遗留系统(如 thinkphp3.2.3 的配置文件)密码是明文写死的,你不可能为了部署改全站代码。选
N
关闭密码强度验证,后续用
ALTER USER
单独加固高权限账号即可。
接下来它会问:
Remove anonymous users? (Press y|Y for Yes, any other key for No) : Y
Disallow root login remotely? (Press y|Y for Yes, any other key for No) : Y
Remove test database and access to it? (Press y|Y for Yes, any other key for No) : Y
Reload privilege tables now? (Press y|Y for Yes, any other key for No) : Y
全部选
Y
。这四步是 MySQL 安全基线,不能省。
4.2 认证插件降级:让 PHP 7.4 顺利连接
mysql_secure_installation
结束后,立即登录 MySQL:
sudo mysql -u root -p
执行以下 SQL(替换
your_password
为你要设的密码):
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password';
FLUSH PRIVILEGES;
这条命令做了两件事:一是将
root@localhost
的认证插件从
caching_sha2_password
切回
mysql_native_password
;二是刷新权限缓存,让新设置立即生效。验证:
SELECT user, host, plugin FROM mysql.user WHERE user='root';
输出中
plugin
列应为
mysql_native_password
。
注意:此操作 仅针对
root@localhost。如果你需要远程连接(如开发机连服务器),应创建新用户并显式指定插件:CREATE USER 'appuser'@'%' IDENTIFIED WITH mysql_native_password BY 'strong_password'; GRANT ALL PRIVILEGES ON mydb.* TO 'appuser'@'%'; FLUSH PRIVILEGES;这样既保证兼容性,又避免弱化
root账号的安全策略。
4.3 碎片表处理:
OPTIMIZE TABLE
的真实代价与替代方案
你搜索热词里有“php mysql 某个表有碎片,一般怎么处理”。这是个经典误区。
OPTIMIZE TABLE
在 MySQL 8.0+ 的 InnoDB 引擎上,
本质是重建表(
ALTER TABLE ... FORCE
)
,它会锁表、消耗大量 I/O,并可能让服务中断数秒到数分钟(取决于表大小)。
正确做法分三步:
第一步:确认是否真有碎片
SELECT
table_schema,
table_name,
data_length,
index_length,
data_free,
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS size_mb,
ROUND((data_free / 1024 / 1024), 2) AS free_mb
FROM information_schema.tables
WHERE table_schema = 'your_database_name'
AND data_free > 0
ORDER BY data_free DESC;
data_free
是 InnoDB 为未来插入预留的空间,单位字节。如果
free_mb
< 10MB,基本可忽略;如果 > 100MB 且
size_mb
> 1000MB,则需关注。
第二步:在线优化(Online DDL)
对于大表,用
ALTER TABLE
的
ALGORITHM=INPLACE
:
ALTER TABLE your_table_name ENGINE=InnoDB, ALGORITHM=INPLACE, LOCK=NONE;
LOCK=NONE
表示不锁表(读写均可),
ALGORITHM=INPLACE
表示在原表上操作,不建临时表。这是 MySQL 5.6+ 的标准做法,比
OPTIMIZE TABLE
安全得多。
第三步:预防碎片
在建表时指定
ROW_FORMAT=DYNAMIC
和
KEY_BLOCK_SIZE=8
:
CREATE TABLE your_table (
id INT PRIMARY KEY,
content TEXT
) ROW_FORMAT=DYNAMIC KEY_BLOCK_SIZE=8;
DYNAMIC
行格式将长文本(
TEXT
,
BLOB
)存储在单独的页中,主记录只存指针,极大减少主索引页的碎片。
实操心得:我在一个 20GB 的日志表上试过
OPTIMIZE TABLE,耗时 18 分钟,期间网站完全不可用。改用ALTER TABLE ... ALGORITHM=INPLACE,耗时 42 秒,用户无感知。记住:OPTIMIZE TABLE是最后手段,ALTER TABLE在线重建才是日常运维的正确姿势。
5. PHP 部署:从
php.ini
核心参数调优到
thinkphp3.2.3
兼容性补丁
Ubuntu 20.04 的
php7.4-fpm
包默认配置非常保守,
memory_limit=128M
、
max_execution_time=30
、
post_max_size=8M
,这对现代 PHP 应用(尤其是框架)远远不够。更麻烦的是,
thinkphp3.2.3
这类老框架依赖
magic_quotes_gpc
(已废弃)和
register_globals
(极度危险),我们必须用
php.ini
的
disable_functions
和
open_basedir
做精准围堵。
5.1
php.ini
关键参数调优:平衡性能与安全
编辑主配置文件:
sudo nano /etc/php/7.4/fpm/php.ini
重点修改以下参数(按重要性排序):
| 参数 | 原值 | 推荐值 | 原因 |
|---|---|---|---|
memory_limit
|
128M
|
256M
| ThinkPHP 加载全部类库后常超 128M,256M 是安全底线 |
max_execution_time
|
30
|
120
| 防止大文件上传或复杂查询超时,120 秒足够处理绝大多数请求 |
post_max_size
|
8M
|
64M
|
支持大附件上传,
upload_max_filesize
必须 ≤ 此值
|
upload_max_filesize
|
2M
|
64M
|
与
post_max_size
匹配,ThinkPHP 的文件上传组件常需此容量
|
opcache.enable
|
Off
|
On
| 必须开启 !OPcache 将 PHP 字节码缓存到共享内存,提升 300%+ 性能 |
opcache.memory_consumption
|
128
|
256
| OPcache 内存池,256MB 足够缓存数千个文件 |
改完后重启:
sudo systemctl restart php7.4-fpm
5.2
thinkphp3.2.3
兼容性补丁:绕过已废弃的
magic_quotes_gpc
thinkphp3.2.3
的入口文件
index.php
里有一段检测:
if(!get_magic_quotes_gpc()) {
$_GET = addslashes_deep($_GET);
$_POST = addslashes_deep($_POST);
$_COOKIE = addslashes_deep($_COOKIE);
}
但 PHP 5.4+ 已彻底移除
magic_quotes_gpc
,
get_magic_quotes_gpc()
永远返回
false
,导致这段代码永远执行
addslashes_deep
,造成 SQL 注入风险(双转义)和 JSON 解析失败。
绝对不要
去改框架源码(违反升级原则)。正确做法是在
php.ini
中注入一段启动脚本:
auto_prepend_file = /etc/php/7.4/fpm/thinkphp323_fix.php
创建该文件:
sudo nano /etc/php/7.4/fpm/thinkphp323_fix.php
内容为:
<?php
// 模拟 magic_quotes_gpc = On 的行为,仅对 thinkphp3.2.3 生效
if (isset($_SERVER['SCRIPT_FILENAME']) && strpos($_SERVER['SCRIPT_FILENAME'], 'thinkphp') !== false) {
if (!function_exists('addslashes_deep')) {
function addslashes_deep($value) {
return is_array($value) ? array_map('addslashes_deep', $value) : addslashes($value);
}
}
// 强制设置 $_GET/$_POST 为已转义状态,欺骗框架
$_GET = $_GET;
$_POST = $_POST;
$_COOKIE = $_COOKIE;
}
这个补丁的精妙之处在于:它不修改任何全局变量,只是“声明”了框架所需的函数,并让框架的
if(!get_magic_quotes_gpc())
判断逻辑自然跳过。重启 PHP-FPM 后,
thinkphp3.2.3
就能零修改运行。
5.3 安全加固:
disable_functions
与
open_basedir
的黄金组合
php.ini
中的
disable_functions
是最后一道防线。在
;disable_functions =
行后添加:
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source
这禁用了所有可能执行系统命令或读取敏感文件的函数。但要注意:
file_get_contents
和
fopen
不能禁,否则框架无法加载配置。
open_basedir
则限制 PHP 脚本能访问的目录:
open_basedir = /var/www/html:/tmp:/var/log/php
/var/www/html
是网站根目录,
/tmp
是临时文件目录(PHP 上传文件暂存于此),
/var/log/php
是自定义日志目录。这样即使黑客通过
include
漏洞拿到 WebShell,也无法读取
/etc/passwd
或
/home/user/.ssh/id_rsa
。
经验之谈:我曾在一个客户项目中,
open_basedir忘了加/tmp,结果thinkphp的缓存文件(Runtime/Cache/)写入失败,页面空白。查日志发现file_put_contents(): open_basedir restriction in effect。所以/tmp必须加入,这是 PHP 运行时的刚需目录。
6. 全链路验证与故障排查:从
curl
到
journalctl
的闭环诊断
部署完成不等于可用。真正的考验是:当用户访问
http://your-domain.com/index.php
时,整个请求链路(Nginx → PHP-FPM → MySQL)是否畅通?下面是一套我用十年验证过的、从外到内的五层诊断法。
6.1 第一层:网络层验证(
curl
与
ss
)
在服务器本地执行:
curl -v http://127.0.0.1/index.php
观察响应头:
-
HTTP/1.1 200 OK:Nginx 正常接收并转发 -
X-Powered-By: PHP/7.4.33:PHP-FPM 成功处理 -
Content-Type: text/html; charset=UTF-8:字符集正确
如果返回
502 Bad Gateway
,说明 Nginx 无法连接 PHP-FPM。此时立刻查:
sudo ss -tlnp | grep ':9000' # 看 PHP-FPM 是否监听
sudo ls -l /run/php/php7.4-fpm.sock # 看 socket 文件是否存在且权限正确(应为 rw-rw----,属组 www-data)
6.2 第二层:PHP 层验证(
phpinfo()
与
error_log
)
创建
/var/www/html/info.php
:
<?php phpinfo(); ?>
访问
http://127.0.0.1/info.php
,重点检查:
-
Loaded Configuration File:/etc/php/7.4/fpm/php.ini -
Scan this dir for additional .ini files:/etc/php/7.4/fpm/conf.d/ -
Registered PHP Streams:https, ftps(证明 OpenSSL 已启用) -
mysqlnd部分:client version => mysqlnd 7.4.33(证明 MySQL 驱动已加载)
如果页面空白,查看 PHP 错误日志:
sudo tail -f /var/log/php7.4-fpm.log
常见错误如
Failed to load config file
,说明
php.ini
路径写错;
Unable to load dynamic library
,说明扩展
.so
文件路径不对。
6.3 第三层:MySQL 层验证(
mysql -h
与
tcpdump
)
在 PHP 脚本中写一个最简连接测试:
<?php
$host = '127.0.0.1';
$user = 'root';
$pass = 'your_password';
$mysqli = new mysqli($host, $user, $pass);
if ($mysqli->connect_error) {
die('Connect Error: ' . $mysqli->connect_error);
}
echo "Connected successfully";
?>
如果报错
Connection refused
,说明 MySQL 未监听
127.0.0.1
。检查
/etc/mysql/mysql.conf.d/mysqld.cnf
:
bind-address = 127.0.0.1
# 不要改成 0.0.0.0,除非你真需要远程访问
如果仍失败,用
tcpdump
抓包:
sudo tcpdump -i lo port 3306 -nn -A
然后执行
php test.php
,看抓包输出中是否有
00000000 0a38 2e30 2e32 382d 3075 6275 6e74 7500
(MySQL 协议握手包)。没有则证明连接根本没发出,问题在 PHP 或网络配置。
6.4 第四层:日志关联分析(
journalctl
跨服务追踪)
Ubuntu 20.04 的
systemd
日志是故障定位神器。当一个请求失败时,往往涉及多个服务。用
journalctl
关联查询:
# 查看最近 100 行 Nginx 错误
sudo journalctl -u nginx -n 100 -f
# 查看 PHP-FPM 的所有错误(含子进程崩溃)
sudo journalctl -u php7.4-fpm -n 100 --since "2023-01-01"
# 关键:跨服务关联同一时间戳的错误
sudo journalctl --since "2023-01-01 10:00:00" --until "2023-01-01 10:05:00" | grep -E "(nginx|php|mysql)"
例如,你看到:
Jan 01 10:02:33 server nginx[1234]: *1 connect() to unix:/run/php/php7.4-fpm.sock failed (111: Connection refused)
Jan 01 10:02:33 server systemd[1]: php7.4-fpm.service: Main process exited, code=exited, status=78/CONFIG
这说明 PHP-FPM 因配置错误崩溃,Nginx 连不上。此时直接查
sudo journalctl -u php7.4-fpm -n 50
,就能看到具体的配置语法错误行。
6.5 第五层:性能压测(
ab
与
mysqltuner
)
最后一步,验证稳定性。用 Apache Bench 压测:
ab -n 1000 -c 100 http://127.0.0.1/index.php
关注
Requests per second
(应 > 200)和
Time per request
(应 < 500ms)。如果
Failed requests
> 0,说明并发下资源不足。
再用
mysqltuner
检查 MySQL 配置:
wget https://raw.githubusercontent.com/major/MySQLTuner-perl/master/mysqltuner.pl
perl mysqltuner.pl --user root --pass your_password
它会给出如
innodb_buffer_pool_size
是否过小、
query_cache_size
是否应关闭等专业建议。Ubuntu 20.04 的 MySQL 8.0+ 已移除 Query Cache,所以看到
Query cache is disabled
是正常的,不必理会。
我的压测经验:在 2 核 4GB 的 VPS 上,LEMP 全链路(Nginx+PHP7.4+MySQL8.0)稳定支撑 300 QPS。如果低于 100 QPS,90% 的原因是
innodb_buffer_pool_size没调大(默认 128MB,应设为物理内存的 70%,即 2.8GB)。这个参数不重启 MySQL 就能改:SET GLOBAL innodb_buffer_pool_size=2800000000;,但要写入配置文件永久生效。
7. 后续演进:从 LEMP 到容器化与自动化部署的平滑过渡
部署完 LEMP,你可能会想:“既然现在这么稳,为什么还要学 Docker?” 这不是技术路线之争,而是工程阶段的自然演进。就像盖房子,地基(LEMP 手动部署)必须扎实,但装修(应用部署)可以用更高效的工具。
7.1 为什么现在就该了解
docker-compose
的
build
模式
很多教程教
docker run -d -p 80:80 nginx
,这是玩具。生产级的 Docker 是用
Dockerfile
构建自定义镜像。例如,为
thinkphp3.2.3
写一个
Dockerfile
:
FROM ubuntu:20.04
RUN apt update && apt install -y nginx-full php7.4-fpm mysql-client && rm -rf /var/lib/apt/lists/*
COPY nginx.conf /etc/nginx/sites-available/default
COPY php.ini /etc/php/7.4/fpm/php.ini
COPY app/ /var/www/html/
CMD ["sh", "-c", "service nginx start && service php7.4-fpm start && tail -f /var/log/nginx/access.log"]
这个镜像复用了你手动部署时验证过的所有配置(
nginx.conf
,
php.ini
),只是把
apt install
和
systemctl start
封装进了镜像。它的好处是:
一次构建,到处运行
。你可以在本地 MacBook 上
docker build
,然后推送到公司私有 Registry,运维同事
docker pull
就能部署,完全规避“在我机器上是好的”这种扯皮。
7.2 Ansible 自动化:把你的手动步骤变成可复现的 Playbook
你上面敲的每一条
apt install
、
sed -i
、
systemctl restart
,都可以写成 Ansible Playbook。例如
lemp.yml
:

199

被折叠的 条评论
为什么被折叠?



