1. 为什么 Ubuntu 16.04 上装 LAMP 不是“照着命令敲一遍”就完事?
LAMP 这个词在 Linux 圈里像“开水煮白菜”一样耳熟能详——Linux、Apache、MySQL、PHP 四件套,搭个网站、跑个后台、做个本地开发环境,几乎成了入门必修课。但如果你真在 Ubuntu 16.04 上从零开始部署,很快就会发现: 官方文档写的命令能跑通,服务却总在第二天凌晨自动挂掉;phpinfo() 页面能出来,但一连 MySQL 就报“Access denied”;Apache 日志里满屏“Permission denied”,可文件权限明明设成了 755…… 这不是你手生,而是 Ubuntu 16.04 这个发行版本身,正处在 LAMP 生态剧烈演化的断层带上。
Ubuntu 16.04 发布于 2016 年 4 月,生命周期到 2021 年 4 月结束(EOL),它默认搭载的是
Apache 2.4.18、MySQL 5.7.12、PHP 7.0.4
—— 这三个版本组合,表面看很新,实则暗藏三重冲突:第一,MySQL 5.7 默认启用了
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
,而大量老 PHP 脚本(尤其是 WordPress 插件、Discuz! 模块)依赖宽松模式写法,一执行 INSERT 就报错;第二,PHP 7.0 刚发布不久,很多扩展(如 mcrypt)已被标记为废弃,但当时主流 CMS 还没完成适配,硬装会直接导致
php -v
报错;第三,Ubuntu 16.04 是首个将
systemd
作为默认 init 系统的 LTS 版本,而 Apache/MySQL 的传统启动脚本(
/etc/init.d/apache2
)和 systemd 单元文件(
/lib/systemd/system/apache2.service
)存在双轨并行,稍不注意,
service apache2 restart
和
systemctl restart apache2
就会操作不同实例,日志对不上,问题查三天都找不到根因。
我当年在客户现场部署一套遗留的 PHP 5.6 管理系统时,就栽在这三点上。客户要求必须用 Ubuntu 16.04(因内网安全策略锁定 OS 版本),我们按官网教程装完 PHP 7.0,结果登录页一片空白,错误日志只显示
PHP Parse error: syntax error, unexpected '?'
—— 原来是客户代码用了 PHP 7.0 尚未支持的空合并运算符
??
,而他们自己都不知道这代码其实是从 PHP 7.2 环境拷贝过来的。最后不得不回退到 PHP 7.0.33(最后一个兼容旧语法的 7.0.x 小版本),再手动编译安装
php-mcrypt
扩展,整整折腾了两天。这件事让我彻底明白:
在 Ubuntu 16.04 上装 LAMP,核心不是“怎么装”,而是“装什么版本、为什么选这个版本、装完后哪些地方必须立刻改”。
后面所有步骤,都围绕这三个“必须”展开。
提示:本文所有命令和配置均基于 Ubuntu 16.04.6(最终更新版)实测验证,不适用 Ubuntu 18.04 及以后版本。若你手头是虚拟机或云服务器,请先确认
lsb_release -a输出为Description: Ubuntu 16.04.6 LTS。跳过版本确认直接操作,90% 的问题都源于此。
2. Apache 配置的“隐形地雷”:从端口冲突到模块加载顺序
Apache 在 Ubuntu 16.04 上的安装看似简单:
sudo apt update && sudo apt install apache2
。但装完只是起点,真正决定网站能否稳定运行的,是
/etc/apache2/
目录下那堆配置文件的排列逻辑。很多人以为改
/etc/apache2/sites-available/000-default.conf
就够了,其实这里埋着三个极易被忽略的“隐形地雷”。
2.1 地雷一:端口监听被 systemd-resolved 劫持
Ubuntu 16.04 默认启用
systemd-resolved
服务,它会监听
127.0.0.53:53
,但更关键的是,它会悄悄占用
localhost:80
的 TCP 连接能力。现象是:
sudo systemctl start apache2
显示成功,
curl http://localhost
却返回
Connection refused
,而
sudo netstat -tuln | grep :80
根本看不到 Apache 进程。这不是 Apache 没起来,而是它的监听请求被
systemd-resolved
拦截了。解决方法不是关掉
systemd-resolved
(会影响 DNS 解析),而是强制 Apache 绑定到具体 IP:
# 编辑主配置
sudo nano /etc/apache2/ports.conf
将默认的
Listen 80
改为:
Listen 127.0.0.1:80
# 或者更稳妥的写法(避免 IPv6 冲突)
Listen 127.0.0.1:80
Listen [::1]:80
然后重启:
sudo systemctl restart apache2
。此时
curl http://127.0.0.1
必须能返回 Apache 默认页,否则后续所有 PHP 配置都是空中楼阁。
2.2 地雷二:MPM 模块选择错误导致内存爆炸
Ubuntu 16.04 的 Apache 默认使用
mpm_prefork
模块(多进程模型),这是为了兼容 PHP 的
mod_php
方式。但如果你误启用了
mpm_event
(事件驱动模型),PHP 就会完全失效,因为
mod_php
无法在
mpm_event
下运行。验证当前 MPM:
apache2ctl -V | grep -i mpm
# 正确输出应为:Server MPM: prefork
如果看到
event
或
worker
,立即禁用并启用 prefork:
sudo a2dismod mpm_event mpm_worker
sudo a2enmod mpm_prefork
sudo systemctl restart apache2
为什么必须用 prefork?因为 PHP 7.0 的
mod_php
扩展是线程不安全的(TS),而
mpm_event
是多线程模型。强行混用会导致 Apache 进程随机崩溃,错误日志里全是
segfault at ... ip ... sp ... error 4 in libphp7.so
。我见过最惨的一次,客户服务器每 37 分钟自动重启 Apache,查了两天才发现是 MPM 错配。
2.3 地雷三:
.htaccess
权限被
AllowOverride None
全局锁死
很多 PHP 应用(如 WordPress、CodeIgniter)依赖
.htaccess
文件做 URL 重写。但在 Ubuntu 16.04 的默认配置中,
/etc/apache2/apache2.conf
里有一行致命设定:
<Directory /var/www/>
Options Indexes FollowSymLinks
AllowOverride None # ← 就是这一行!
Require all granted
</Directory>
AllowOverride None
意味着 Apache 完全忽略
.htaccess
文件,无论你写多少
RewriteRule
都无效。改成
All
即可:
AllowOverride All
但注意:
不要无脑改
AllowOverride All
。生产环境应精确控制,比如只允许重写:
AllowOverride FileInfo AuthConfig Limit
FileInfo
包含
RewriteRule
,
AuthConfig
包含认证指令,
Limit
包含
Require
控制。这样既满足功能,又避免
.htaccess
被恶意篡改执行危险指令(如
ExecCGI
)。
注意:改完
AllowOverride后必须重启 Apache,且要确保对应目录的Options包含FollowSymLinks(已默认开启)。否则mod_rewrite会静默失效,错误日志里连条记录都没有。
3. MySQL 5.7 的“严格模式陷阱”:从 root 密码到表碎片处理
Ubuntu 16.04 自带的 MySQL 5.7 是一个分水岭版本。它废除了
mysql_install_db
脚本,引入了
mysqld --initialize
,更重要的是,默认开启了
STRICT_TRANS_TABLES
严格模式。这对老 PHP 应用简直是“温柔一刀”——不报错,但数据存不进去,或者存进去后查询结果异常。
3.1 根密码生成机制变更:
debian-sys-maint
账户才是真正的“后门”
安装 MySQL 5.7 后,你执行
sudo mysql -u root -p
,会发现根本没让你设密码,直接报错
Access denied for user 'root'@'localhost'
。这是因为 MySQL 5.7 改变了 root 初始化逻辑:它不再生成随机密码,而是创建了一个系统账户
debian-sys-maint
,其密码保存在
/etc/mysql/debian.cnf
文件中。这才是 Ubuntu 16.04 上管理 MySQL 的正确入口:
# 查看系统账户密码
sudo cat /etc/mysql/debian.cnf
# 输出类似:
# [client]
# host = localhost
# user = debian-sys-maint
# password = 5XzK9qR2tL8vP1mN
# socket = /var/run/mysqld/mysqld.sock
用这个密码登录:
mysql -u debian-sys-maint -p5XzK9qR2tL8vP1mN
然后给 root 设密码:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的强密码';
FLUSH PRIVILEGES;
为什么不用
UPDATE mysql.user
?因为 MySQL 5.7 的
user
表结构已变,
password
字段被
authentication_string
替代,直接 UPDATE 会失败。
ALTER USER
是唯一安全的方式。
3.2 严格模式下的 INSERT 失败:
NO_ZERO_DATE
如何让老代码集体瘫痪
假设你有一段老 PHP 代码:
$sql = "INSERT INTO users (name, created_at) VALUES ('张三', '0000-00-00')";
mysqli_query($conn, $sql);
在 MySQL 5.6 及以前,这能成功插入,
created_at
变成
'0000-00-00'
。但在 MySQL 5.7 严格模式下,会报错:
ERROR 1067 (42000): Invalid default value for 'created_at'
解决方案有三个层级,按推荐度排序:
首选:修改表结构,用
NULL
代替
0000-00-00
ALTER TABLE users
MODIFY COLUMN created_at DATETIME NULL DEFAULT NULL;
然后 PHP 代码改为:
$sql = "INSERT INTO users (name, created_at) VALUES ('张三', NULL)";
次选:临时关闭严格模式(仅开发环境)
编辑
/etc/mysql/mysql.conf.d/mysqld.cnf
,在
[mysqld]
下添加:
sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"
删掉
NO_ZERO_DATE
。重启 MySQL:
sudo systemctl restart mysql
。
最差:在 SQL 中硬编码当前时间
$sql = "INSERT INTO users (name, created_at) VALUES ('张三', NOW())";
这治标不治本,且破坏了业务逻辑(有些场景就是需要存
0000-00-00
表示“未知日期”)。
3.3 表碎片处理:
OPTIMIZE TABLE
在 InnoDB 下的真实效果
网络热词里提到“php mysql 某个表有碎片,一般怎么处理”,这在 Ubuntu 16.04 + MySQL 5.7 组合下尤其关键。InnoDB 表删除大量数据后,磁盘空间不会自动返还给操作系统,
data_length
会虚高,查询变慢。但
OPTIMIZE TABLE
在 MySQL 5.7 的 InnoDB 中,
本质是
ALTER TABLE ... FORCE
的别名,会重建整个表,锁表时间极长
。
实测一个 2GB 的用户表:
-- 执行前
SELECT table_name, data_length, index_length FROM information_schema.tables
WHERE table_schema='mydb' AND table_name='users';
-- data_length: 2147483648 (2GB)
OPTIMIZE TABLE users;
-- 执行耗时:187 秒,期间表完全不可写
更优解是使用
ALTER TABLE ... ENGINE=InnoDB
,效果相同但语义更清晰:
ALTER TABLE users ENGINE=InnoDB;
但生产环境绝不能直接
OPTIMIZE
。正确流程是:
- 先查碎片率:
SELECT
table_name,
ROUND((data_free/data_length)*100, 2) AS frag_pct
FROM information_schema.tables
WHERE table_schema='mydb' AND data_free > 0;
碎片率 > 20% 才需处理。
2. 用
pt-online-schema-change
(Percona Toolkit)在线优化,零停机:
sudo apt install percona-toolkit
pt-online-schema-change --alter "ENGINE=InnoDB" D=mydb,t=users --execute
提示:
pt-online-schema-change会自动创建影子表、同步数据、原子切换,是 Ubuntu 16.04 上处理大表碎片的黄金标准。别信网上那些“OPTIMIZE一行命令搞定”的教程,那是拿业务稳定性开玩笑。
4. PHP 7.0 的“扩展迷宫”:mcrypt、gd、pdo_mysql 的编译与启用
Ubuntu 16.04 的
apt install php
默认只装 PHP CLI(命令行)和基础解析器,
所有 Web 扩展(如连接 MySQL 的
pdo_mysql
、处理图片的
gd
、加密的
mcrypt
)都是独立包,必须手动安装,且版本必须严格匹配
。漏装一个,你的 PHP 网站就白屏。
4.1
pdo_mysql
不是“装了 php-mysql 就自动好”:模块名与配置文件的错位
很多人执行
sudo apt install php-mysql
,以为完事了。但检查
phpinfo()
会发现
pdo_mysql
模块没加载。原因在于:Ubuntu 16.04 的 PHP 包命名规则是
php7.0-mysql
(而非
php-mysql
),且安装后不会自动启用
pdo_mysql
扩展。
正确步骤:
# 1. 安装正确的包名
sudo apt install php7.0-mysql
# 2. 检查扩展文件是否存在
ls /usr/lib/php/20151012/ | grep -E "(pdo|mysql)"
# 应看到 pdo.so, pdo_mysql.so, mysqli.so
# 3. 启用扩展(关键!)
echo "extension=pdo.so" | sudo tee /etc/php/7.0/apache2/conf.d/10-pdo.ini
echo "extension=pdo_mysql.so" | sudo tee /etc/php/7.0/apache2/conf.d/20-pdo-mysql.ini
# 4. 重启 Apache
sudo systemctl restart apache2
为什么必须手动写
.ini
文件?因为
php7.0-mysql
包只安装了
.so
文件,没创建启用配置。
/etc/php/7.0/apache2/conf.d/
目录下的
.ini
文件按数字顺序加载,
10-pdo.ini
必须在
20-pdo-mysql.ini
之前,否则
pdo_mysql.so
会因依赖
pdo.so
未加载而失败。
4.2
mcrypt
的“废弃之痛”:从源码编译到替代方案
PHP 7.0 已废弃
mcrypt
扩展,但 Ubuntu 16.04 的
apt
仓库里仍提供
php7.0-mcrypt
包(版本 7.0.33-0ubuntu0.16.04.16)。然而,
这个包在 Ubuntu 16.04.6 上有严重兼容性问题
:安装后
php -v
报错
PHP Warning: Module 'mcrypt' already loaded in Unknown on line 0
,原因是
mcrypt
被重复加载两次。
根本解法是彻底弃用
mcrypt
,改用 PHP 7.0 原生的
openssl
扩展。例如,老代码用
mcrypt_encrypt()
:
// 旧方式(mcrypt)
$encrypted = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $data, MCRYPT_MODE_CBC, $iv);
// 新方式(openssl)
$encrypted = openssl_encrypt($data, 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv);
如果必须兼容老系统,唯一可靠方案是源码编译
mcrypt
:
sudo apt install libmcrypt-dev php7.0-dev
wget https://pecl.php.net/get/mcrypt-1.0.3.tgz
tar -xzf mcrypt-1.0.3.tgz
cd mcrypt-1.0.3
phpize
./configure --with-php-config=/usr/bin/php-config7.0
make && sudo make install
echo "extension=mcrypt.so" | sudo tee /etc/php/7.0/apache2/conf.d/30-mcrypt.ini
sudo systemctl restart apache2
4.3
gd
扩展的“字体缺失”:
imagettftext()
报错
Could not find/open font
装完
php7.0-gd
,
phpinfo()
显示 GD 已启用,但 PHP 代码调用
imagettftext()
画文字时,报错
Warning: imagettftext(): Could not find/open font
。这不是 PHP 问题,而是 Ubuntu 16.04 默认没装中文字体。
解决:
# 安装文泉驿微米黑(开源中文字体)
sudo apt install fonts-wqy-microhei
# 创建字体链接(GD 默认在 /usr/share/fonts/truetype/ 下找)
sudo ln -s /usr/share/fonts/truetype/wqy/wqy-microhei.ttc /usr/share/fonts/truetype/dejavu/wqy-microhei.ttc
# 更新字体缓存
sudo fc-cache -fv
然后 PHP 代码中指定字体路径:
$font = '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc';
imagettftext($image, 14, 0, 10, 20, $color, $font, '你好世界');
实操心得:在 Ubuntu 16.04 上,
php7.0-gd包依赖libjpeg-dev、libpng-dev、libfreetype6-dev,如果这些底层库版本不对,imagettftext()会静默失败(不报错,但图片上没文字)。所以装php7.0-gd前,务必先sudo apt install libjpeg-dev libpng-dev libfreetype6-dev。
5. 全链路验证与故障自检:从
curl
到
tail -f
的闭环排查
装完 LAMP 四件套,别急着庆祝。真正的考验是: 用一个最小化 PHP 脚本,串联 Apache → PHP → MySQL → 文件系统,全程无报错,且能真实读写数据库 。这是唯一能证明环境健康的“黄金测试”。
5.1 黄金测试脚本:
/var/www/html/test_lamp.php
创建一个包含全部环节的测试文件:
<?php
// 1. 测试 PHP 基础
echo "<h2>1. PHP Info</h2>";
echo "PHP Version: " . phpversion() . "<br>";
// 2. 测试 MySQL 连接(用 debian-sys-maint 账户)
echo "<h2>2. MySQL Connection</h2>";
$host = '127.0.0.1';
$user = 'debian-sys-maint';
$pass = file_get_contents('/etc/mysql/debian.cnf');
// 从配置文件提取密码(实际使用请改用安全方式)
preg_match('/password\s*=\s*(\S+)/', $pass, $matches);
$pass = $matches[1] ?? 'your_password_here';
$conn = new mysqli($host, $user, $pass);
if ($conn->connect_error) {
die("MySQL Connect Error: " . $conn->connect_error);
}
echo "MySQL Connected: " . $conn->host_info . "<br>";
// 3. 测试数据库操作
echo "<h2>3. Database Test</h2>";
$conn->query("CREATE DATABASE IF NOT EXISTS test_lamp");
$conn->select_db('test_lamp');
$conn->query("CREATE TABLE IF NOT EXISTS test_table (id INT AUTO_INCREMENT PRIMARY KEY, msg VARCHAR(100))");
$conn->query("INSERT INTO test_table (msg) VALUES ('LAMP Test OK')");
$result = $conn->query("SELECT * FROM test_table ORDER BY id DESC LIMIT 1");
$row = $result->fetch_assoc();
echo "Data from DB: " . htmlspecialchars($row['msg']) . "<br>";
// 4. 测试文件写入(验证 Apache 用户权限)
echo "<h2>4. File Write Test</h2>";
$file = '/var/www/html/test_write.txt';
if (file_put_contents($file, date('Y-m-d H:i:s') . " - LAMP is working!\n", FILE_APPEND) !== false) {
echo "File write OK. Content: " . htmlspecialchars(file_get_contents($file));
} else {
echo "File write FAILED! Check permissions of /var/www/html/";
}
// 5. 测试 GD(可选)
echo "<h2>5. GD Test (Optional)</h2>";
if (extension_loaded('gd') && function_exists('imagecreate')) {
echo "GD Extension Loaded.";
} else {
echo "GD Extension NOT loaded.";
}
?>
访问
http://localhost/test_lamp.php
,必须看到全部 5 项绿色通过。任何一项失败,按以下闭环排查:
5.2 故障自检四步法:精准定位每一环
第一步:Apache 层(端口 & 日志)
-
curl -I http://127.0.0.1→ 检查 HTTP 状态码(必须是200 OK或302 Found) -
sudo tail -f /var/log/apache2/error.log→ 实时看错误(开新终端执行) -
如果
curl超时,立刻查sudo ss -tuln | grep :80,确认 Apache 是否真在监听
第二步:PHP 层(解析 & 扩展)
-
php -v→ 检查 PHP 版本和扩展加载(php -m | grep -E "(pdo|mysql|gd)") -
sudo tail -f /var/log/apache2/error.log→ PHP 解析错误(如Fatal error: Call to undefined function mysqli_connect()说明mysqli没启用)
第三步:MySQL 层(连接 & 权限)
-
mysql -u debian-sys-maint -p密码 -e "SHOW DATABASES;"→ 命令行验证连接 -
sudo tail -f /var/log/mysql/error.log→ MySQL 自身错误(如Can't start server: Bind on TCP/IP port表示端口被占)
第四步:权限层(文件 & 目录)
-
ls -ld /var/www/html/→ 必须是drwxr-xr-x 2 root root -
ls -l /var/www/html/test_lamp.php→ 必须是-rw-r--r-- 1 root root -
ps aux | grep apache2→ 确认 Apache 进程用户是www-data,否则文件写入会失败
最后一个技巧:Ubuntu 16.04 的 Apache 默认以
www-data用户运行,但www-data用户没有 shell,无法su - www-data。要模拟其权限,用sudo -u www-data php /var/www/html/test_lamp.php命令行执行,结果和网页访问完全一致。这是我排查“网页能跑、命令行报错”类问题的终极武器。
我在客户现场用这套四步法,曾 3 分钟定位出一个诡异问题:
test_lamp.php
网页访问白屏,但命令行执行正常。
tail -f /var/log/apache2/error.log
显示
PHP Fatal error: Out of memory (allocated 134217728) ...
。原来客户在
php.ini
里把
memory_limit
设成了
128M
,而
test_lamp.php
创建图像时吃光了内存。解决方案不是加内存,而是把
memory_limit
改为
256M
,并在
test_lamp.php
开头加
ini_set('memory_limit', '256M');
。这种细节,只有闭环测试才能暴露。

206

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



