别只给 WordPress 加两台服务器:AWS 三层高可用,从部署到故障演练
假设半夜网站打不开,重启完服务器,文章恢复了,图片却一会儿有、一会儿没有。
明明部署了两台机器,故障还是没绕过去,排查的时候还得来回登录两台服务器。
遇到这种问题,我会把入口、应用、数据库和上传文件一起检查。下面这套 AWS 三层方案,就按这个思路展开:怎么搭,怎么验证,出问题怎么退回来。
机器可以换,文章和图片不能跟着消失
我看 WordPress 高可用,通常会先问一句:拿走一台服务器,网站还能不能正常登录、发文、看图片?
文章在数据库里,图片默认在本地目录里,主题和插件又是另一套文件。只加负载均衡,解决不了这些状态的一致性。
我的方案选一个 Region、两个可用区:
入口层用 ALB,应用层用 EC2,数据层用 RDS。EFS 是应用节点共用的文件存储。
WordPress 核心、插件、主题固化进同一版本的 AMI,只有 wp-content/uploads 挂到 EFS。这是本文选择的发布方式,AWS 也有整站放 EFS 的参考方案。
高可用的关键,是让一台机器的故障,不带走网站的状态。
别急着挑最便宜的实例。ALB、NAT、RDS 主备、EFS、备份和日志都有费用,得按部署区域、容量和流量一起算。
网络摆对了,后面少折腾不少
VPC 可以用 10.20.0.0/16,两个可用区各放三类子网:
| 用途 | AZ-A | AZ-B | 默认路由 |
|---|---|---|---|
| 公网入口 | 10.20.0.0/24 | 10.20.1.0/24 | Internet Gateway |
| 私网应用 | 10.20.10.0/24 | 10.20.11.0/24 | 各区自己的 NAT |
| 隔离数据库 | 10.20.20.0/24 | 10.20.21.0/24 | 无互联网默认路由 |
这里选择每个可用区一个 zonal NAT Gateway,应用节点通过本区 NAT 出网。若改用 VPC Endpoint,要补齐 SSM、Secrets Manager 等依赖;软件仓库和插件站的访问另外处理。
安全组按访问关系放行:
| 对象 | 入站规则 |
|---|---|
| ALB | 443;80 用于跳转 HTTPS |
| EC2 | 80,仅允许 ALB 安全组 |
| RDS | 3306,仅允许 EC2 安全组 |
| EFS mount target | 2049,仅允许 EC2 安全组 |
EC2 不分配公网 IP,通过 Session Manager 管理,不开放公网 SSH。实例角色只允许读取指定机密,并具备必要的 SSM、日志权限。
ALB 选择 Internet-facing,覆盖两个公网子网。ACM 申请证书并完成 DNS 验证,443 监听器转发到目标组,80 监听器跳转 HTTPS。
RDS 数据库子网组选择两个隔离子网。部署类型选 Multi-AZ DB instance,关闭公网访问,开启存储加密、自动备份和删除保护。
这个部署是一主一备,备用实例不承担读请求。别把它和 Multi-AZ DB cluster 混在一起。
数据先安顿好,再考虑扩容
下面的命令用于新建或隔离测试环境。应用镜像以 Ubuntu 24.04、Apache、PHP 8.3 为例,预装 AWS CLI v2、Python 3、amazon-efs-utils 和 PHP 所需扩展。
管理员连接数据库后,建立独立库和应用账户:
CREATE DATABASE wordpress
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
CREATE USER 'wp_app'@'%'
IDENTIFIED BY '<替换为生成的强密码>';
GRANT ALL PRIVILEGES ON wordpress.*
TO 'wp_app'@'%';权限限定在 wordpress 库内,应用不要使用 RDS 管理员账户。
数据库连接填写 RDS 官方 endpoint,不填 IP,也别随意换成自己的别名。故障切换和证书校验都依赖正确的连接地址。
这里有一道必须完成的上线门禁:应用数据库连接要加密,也要验证服务端身份。
RDS 参数组设置 require_secure_transport=ON。应用镜像中的数据库接入层,需要支持显式加载 AWS CA、校验证书和 endpoint,例如经过测试的 WordPress db.php 接入层。具体配置按所用驱动执行,不能只加一个 MYSQLI_CLIENT_SSL 标志就宣布完成。
验收时,在实际 WordPress 数据库会话中检查:
SHOW SESSION STATUS LIKE 'Ssl_cipher';结果应该非空;隔离环境使用错误 CA、错误主机名时,还必须拒绝连接。管理客户端连通不能代替这项应用验收。证书配置可对照 AWS RDS MySQL TLS 文档。没通过,就先留在测试环境。
图片这边创建 Regional EFS,两个应用可用区各建一个 mount target,绑定 EFS 安全组。
Access Point 根目录设为 /wordpress-uploads,POSIX 身份对应 www-data,Ubuntu 通常是 33:33,实际用 id www-data 确认。根目录创建信息也要填写 Owner UID、GID 和权限,例如 0775。
在新节点挂载,替换示例资源 ID:
sudo mkdir -p /var/www/html/wp-content/uploads
sudo mount -t efs \
-o tls,accesspoint=fsap-EXAMPLE \
fs-EXAMPLE:/ /var/www/html/wp-content/uploads/etc/fstab 增加:
fs-EXAMPLE:/ /var/www/html/wp-content/uploads efs _netdev,tls,accesspoint=fsap-EXAMPLE 0 0检查实际挂载:
findmnt -rn -M /var/www/html/wp-content/uploads \
-o TARGET,SOURCE,FSTYPE应该有这个精确挂载点的 NFS 输出。没有输出,先查 mount target、安全组、DNS 和 Access Point 权限。
别用 chmod 777 解决所有问题。权限不对,应该对齐身份和目录权限。
第一台装扎实,第二台才不会复制故障
密码和 WordPress 的八个 keys、salts 放 Secrets Manager,所有节点使用同一份。代码进镜像,机密在实例启动时读取。
机密 JSON 使用这些字段:
DB_NAME DB_USER DB_PASSWORD DB_HOST
AUTH_KEY SECURE_AUTH_KEY LOGGED_IN_KEY NONCE_KEY
AUTH_SALT SECURE_AUTH_SALT LOGGED_IN_SALT NONCE_SALT我会把加载脚本放在 /usr/local/sbin/wp-load-secrets,属主 root:root、权限 0750:
#!/usr/bin/env bash
set -euo pipefail
umask 077
install -d -o root -g www-data -m 0750 /etc/wordpress
tmp="$(mktemp /etc/wordpress/.runtime.XXXXXX)"
trap 'rm -f -- "$tmp"' EXIT
aws secretsmanager get-secret-value \
--region '<替换为Region>' \
--secret-id '<替换为Secret ARN>' \
--query SecretString --output text > "$tmp"
python3 - "$tmp" <<'PY'
import json, sys
required = """
DB_NAME DB_USER DB_PASSWORD DB_HOST
AUTH_KEY SECURE_AUTH_KEY LOGGED_IN_KEY NONCE_KEY
AUTH_SALT SECURE_AUTH_SALT LOGGED_IN_SALT NONCE_SALT
""".split()
try:
with open(sys.argv[1], encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict) or any(
not isinstance(data.get(k), str) or not data[k]
for k in required
):
raise ValueError()
except Exception:
sys.exit("WordPress secret validation failed")
PY
chown root:www-data "$tmp"
chmod 0640 "$tmp"
mv -f -- "$tmp" /etc/wordpress/runtime.json先验证再原子替换,避免应用读到半份配置。不要开启脚本调试输出,也不要把 runtime.json 留在 AMI 里。
wp-config.php 在引导 WordPress 前读取:
$config = json_decode(
file_get_contents('/etc/wordpress/runtime.json'),
true, 512, JSON_THROW_ON_ERROR
);
$required = explode(' ',
'DB_NAME DB_USER DB_PASSWORD DB_HOST ' .
'AUTH_KEY SECURE_AUTH_KEY LOGGED_IN_KEY NONCE_KEY ' .
'AUTH_SALT SECURE_AUTH_SALT LOGGED_IN_SALT NONCE_SALT'
);
foreach ($required as $key) {
if (!isset($config[$key]) ||
!is_string($config[$key]) || $config[$key] === '') {
throw new RuntimeException('Invalid runtime configuration');
}
define($key, $config[$key]);
}
unset($config, $required, $key);
define('DB_CHARSET', 'utf8mb4');
define('DB_COLLATE', '');
if (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https') {
$_SERVER['HTTPS'] = 'on';
}
define('WP_HOME', 'https://blog.example.com');
define('WP_SITEURL', 'https://blog.example.com');
define('FORCE_SSL_ADMIN', true);
define('DISALLOW_FILE_MODS', true);
define('DISABLE_WP_CRON', true);代理协议处理能避免 HTTPS 重定向循环,前提是 EC2 只接受 ALB 流量。站点域名替换成自己的,原配置里的重复常量要删除。
禁止后台改插件和主题,否则 A 更新、B 没更新,故障又开始随机出现。
启动顺序也要固定。创建 /etc/systemd/system/wp-secrets.service:
[Unit]
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/wp-load-secrets
RemainAfterExit=yes创建 /etc/systemd/system/apache2.service.d/wordpress.conf,父目录不存在就先创建:
[Unit]
Requires=wp-secrets.service
After=wp-secrets.service
RequiresMountsFor=/var/www/html/wp-content/uploads
[Service]
ExecStartPre=/usr/bin/findmnt -rn -M /var/www/html/wp-content/uploads -t nfs4这样,机密没读取成功、EFS 没挂载成功,Apache 就不能启动。机密轮换后要重新执行加载服务,并逐节点验证数据库连接,这套 oneshot 不会自动追踪轮换。
Apache 开启 rewrite,站点目录允许 .htaccess;上传目录禁止覆盖配置和执行脚本:
<Directory /var/www/html>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
<Directory /var/www/html/wp-content/uploads>
AllowOverride None
<FilesMatch "(?i)\.(php[0-9]?|phtml|phar)$">
Require all denied
</FilesMatch>
</Directory>代码目录保持只读,构建阶段预置 /var/www/html/.htaccess,不要依赖后台写入:
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]在新节点检查并启动:
sudo a2enmod rewrite
sudo apachectl configtest
sudo apachectl -M
php -v
sudo systemctl daemon-reload
sudo systemctl restart apache2语法应为 Syntax OK,模块列表有 rewrite_module,PHP 版本符合镜像约定。已有生产节点改配置前,先备份配置、确认其他节点容量,再逐台排空流量操作。
新站只在第一台执行一次安装,配置管理员、固定链接,测试登录、发文和上传。安装脚本不能让每台新实例都初始化数据库。
页面能打开,还得看看故障来了会怎样
确认镜像不含运行机密后,创建 Launch Template。ASG 覆盖两个应用子网,最小和期望容量设为 2,并关联 ALB 目标组。
显式启用 ELB 健康检查。只关联目标组,不代表应用异常后 ASG 就会替换实例。宽限期按机密读取、挂载和应用启动的实测时间设置。
镜像预置 /healthz.php:
<?php
if (!extension_loaded('mysqli') ||
!is_readable(__DIR__ . '/wp-load.php') ||
!is_readable('/etc/wordpress/runtime.json')) {
http_response_code(503);
exit('not-ready');
}
http_response_code(200);
echo 'ok';目标组检查该路径,成功码设为 200。间隔 15 秒、超时 5 秒、连续失败 2 次,可以作为测试起点,再按实际情况调整。
它检查本机就绪状态,不加载 WordPress、不查询数据库。数据库读写、上传和文件读取由独立合成监控检查,避免 RDS 短暂切换引发整组 EC2 被替换。
检查目标状态:
aws elbv2 describe-target-health \
--target-group-arn '<目标组ARN>' \
--query 'TargetHealthDescriptions[].{Id:Target.Id,State:TargetHealth.State,Reason:TargetHealth.Reason}'两个节点应为 healthy。异常时结合 Reason 查日志、端口和启动依赖。
第二台健康后,Route 53 Alias 指向 ALB。验证跨节点图片、登录、固定链接,再放开访问。
WP-Cron 放到独立受控执行器,监控失败和长时间未运行,配置冷备接管。接管前确认旧执行器停止;flock 只能防同机重叠,双机执行需要分布式锁。这套方案的自动高可用重点是网站访问和数据路径,任务执行器不能顺手漏掉。
演练前准备可恢复的备份、正常容量和停止条件:
- 移除一台测试 EC2,观察网站、登录、图片和 ASG 补机。
- 验证剩余可用区能承担流量,看错误率、响应时间和数据库压力。
- 在维护窗口受控切换 RDS,记录应用实际恢复时间。
终止一台实例,只能证明节点恢复,不能代替完整可用区故障验收。
RDS Multi-AZ 切换通常约 60~120 秒,也可能更久。连接和写入会受影响,高可用不是零中断。表单、评论、订单的重试还要考虑幂等。
另外,ALB 全部注册目标都不健康时,会出现 fail-open,继续向这些目标转发。别把健康检查理解成“全红了自动关门”。
图片间歇 404,证据往往就在两台机器的差别里
下面用一个构造的演练场景说明,时间和现象均为示例。
14:10,B 节点加入后,文章正常,图片间歇 404。数据库查询正常,上传也有成功记录。
我会对照 ALB 和 Apache 日志,看失败请求落在哪台。假设错误集中在 B,再分别检查挂载:A 有 EFS,B 没有;B 本地 uploads 却存在新上传图片。
这就能解释现象:B 挂载失败,应用照常启动。共享图片在 B 看不到,B 的本地新图片在 A 看不到。
止损时先确认 A 容量,摘除 B、等待连接排空,暂停上传。保存日志、文件清单和本地文件,同时防止异常实例被自动回收后丢失证据。
不要直接挂上 EFS。本地目录会被遮住,先隔离保存,再按文件名、校验值和业务记录合并。同名冲突单独处理,禁止整个目录覆盖。
修复镜像里的启动依赖后,在隔离测试节点使用错误 Access Point 模拟失败,确认 Apache 为 failed 或 inactive,没有本地新增上传。恢复正确配置后应为 active,再测跨节点文件访问。
运行中的 EFS 卡顿仍需监控。挂载点存在,不代表它一直能正常读写。
上线前,我会把这张清单再过一遍
- 不要只测首页。登录、发文、上传、固定链接、跨节点图片都要验收。
- 不要一次替换全部节点。新镜像先灰度一台,错误率、耗时或上传异常就停止扩大,排空新节点,恢复旧 Launch Template,保留健康容量。
- 不要把回镜像当成回数据。插件改过数据库结构,必须验证旧代码兼容性;不兼容就按预案隔离恢复、验证,再受控切换。
- 不要覆盖线上数据库来试恢复。RDS 恢复到新实例,EFS 恢复到隔离位置,核对文章与附件;暂停写入并处理新增数据后再切换。
- 不要把备份成功当成恢复成功。定期实际恢复,确认时间点和数据匹配。
- 不要省告警。ALB 5xx、健康目标数、应用错误、RDS 连接、EFS 延迟、上传失败和任务失败都要有人接。
如果迁移旧站,切流前冻结写入,做末次数据库和上传文件同步,保证只有一个站点接受写入。新站已经产生数据后,不能直接改 DNS 回旧站,把新增内容丢在后面。
我更在意的是:机器坏了,文章和图片还在;换节点有步骤,恢复数据有把握。单 Region 双可用区解决区域内高可用,整区故障还要另外设计跨区域灾备。
你可以从一件小事开始:检查自己的 uploads,到底落在哪块盘上。
觉得有用,点个赞、点个在看,也转发给正在搭站的同事。关注 @运维躬行录,我们继续聊能少踩坑、少熬夜的生产实践。
公众号:耕云躬行录
个人博客:躬行笔记