4.21 项目实战:搭建企业级 Web 应用——Nginx+PHP+MySQL+Redis+监控

预计阅读时间:17 分钟

📖 目录

学习目标

本项目整合第三编所学知识,完成以下目标:

  • 部署 Nginx + PHP-FPM + MySQL + Redis 完整 Web 环境
  • 配置 SSL/TLS 证书(Let's Encrypt),实现 HTTPS 安全访问
  • 搭建 Prometheus + Grafana 监控体系
  • 配置 rsyslog + logrotate 日志收集与轮转
  • 编写自动化备份脚本,确保数据安全可恢复

前置知识

建议已掌握以下内容再开始本项目:

知识领域具体技能对应章节
服务管理systemd 服务管理、端口与防火墙基础2.4:进程管理5.5:防火墙实战
网络基础TCP/IP 基础、端口概念4.5:网络故障排查
Nginx安装、基本配置、虚拟主机3.4:Nginx Web服务器
MySQL安装、SQL 基础、用户权限3.6:MySQL 数据库
Redis安装、基本数据类型、持久化3.10:Redis 缓存服务
SSL/TLS证书概念、HTTPS 工作原理3.11:SSL/TLS 证书管理
Shell 脚本Bash 基础编程、变量、循环2.1:Shell 脚本入门2.8:Shell 脚本进阶

项目背景与需求分析

假设你是一家初创公司的运维工程师,需在单台服务器上部署内部管理系统(OA/CRM),要求:HTTPS 安全访问、数据库+缓存、监控告警、日志可追溯、数据自动备份。

组件选型理由
Web 服务器Nginx高性能反向代理,支持 SSL 终止
应用运行时PHP 8.2 + PHP-FPM成熟的 Web 开发语言,FPM 提供进程管理
关系数据库MySQL 8.0ACID 事务,InnoDB 引擎成熟稳定
缓存Redis 7内存数据库,毫秒级响应
SSL 证书Let's Encrypt免费、自动续期、浏览器信任
监控Prometheus + Grafana云原生监控标准,可视化强大
日志rsyslog + logrotate系统原生工具,无需额外依赖

技术架构

反向代理(Reverse Proxy):客户端请求先到达 Nginx,由 Nginx 转发给后端应用服务器。反向代理可以隐藏后端服务器真实 IP、实现负载均衡和 SSL 终止。SSL 终止(SSL Termination):在 Nginx 上解密 HTTPS 流量,后端通信使用明文 HTTP,减轻后端负担。

用户浏览器 → Nginx (反向代理+SSL 终止)
                 │
        ┌────────┼────────┐
        ▼        ▼        ▼
   PHP-FPM    MySQL     Redis
  (应用逻辑)  (持久存储)  (缓存层)

   Prometheus + Grafana (监控)
   rsyslog + logrotate (日志)

环境准备

本项目基于 Ubuntu 24.04 LTS,推荐 4 核 8GB 内存配置。确保有 root 或 sudo 权限、公网 IP 和已解析的域名。

SSL/TLS(Secure Sockets Layer / Transport Layer Security):用于加密客户端与服务器之间通信的安全协议,HTTPS 就是 HTTP over TLS。PHP-FPM(PHP FastCGI Process Manager):PHP 的进程管理器,以 FastCGI 协议处理 PHP 请求,比传统的 mod_php 更高效、更稳定。OPcache:PHP 内置的字节码缓存器,将 PHP 脚本编译后的字节码缓存到共享内存中,避免重复编译,可提升 50%+ 的执行性能。

项目配置
操作系统Ubuntu 24.04 LTS
最低配置2 核 CPU / 4GB 内存 / 40GB 磁盘
用户权限root 或具有 sudo 权限的用户
# 端口规划
# 22:SSH  80:HTTP→HTTPS  443:HTTPS  3306:MySQL  6379:Redis
# 9090:Prometheus  3000:Grafana  9100:Node  9104:MySQL  9121:Redis  9113:Nginx

# 防火墙配置
ufw allow 22/tcp    # SSH
ufw allow 80/tcp    # HTTP(重定向到 HTTPS)
ufw allow 443/tcp   # HTTPS
ufw allow 3000/tcp  # Grafana 面板
ufw enable
ufw status verbose  # 验证规则

第一步:安装 MySQL

# 安装 MySQL
sudo apt update
sudo apt install -y mysql-server mysql-client

# 安全加固
sudo mysql_secure_installation
# 选择 MEDIUM 密码策略 → 设置 root 密码 → 删除匿名用户 → 禁止 root 远程登录 → 删除测试库

1.1 配置 MySQL

# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
bind-address = 127.0.0.1              # 仅本机监听
character-set-server = utf8mb4
innodb_buffer_pool_size = 1G          # 根据内存调整
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 2
log_bin = /var/log/mysql/mysql-bin.log # 二进制日志
expire_logs_days = 14

sudo systemctl restart mysql

1.2 创建数据库和用户

sudo mysql -u root -p
-- 创建应用数据库
CREATE DATABASE webapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 创建专用用户(最小权限原则)
CREATE USER 'webapp_user'@'localhost' IDENTIFIED BY 'YourStr0ng!Passw0rd';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX ON webapp.* TO 'webapp_user'@'localhost';
FLUSH PRIVILEGES;

-- 验证连接
-- mysql -u webapp_user -p webapp -e "SELECT '连接成功' AS status;"

第二步:安装 Redis

sudo apt install -y redis-server

2.1 配置 Redis

# /etc/redis/redis.conf
bind 127.0.0.1 ::1                     # 仅本机
protected-mode yes
requirepass YourRedisStr0ngPass!        # 必须设置密码

# 持久化:RDB + AOF
save 900 1
save 300 10
save 60 10000
appendonly yes
appendfsync everysec

# 危险命令重命名(可选)
rename-command FLUSHALL ""
rename-command CONFIG "CONFIG_a1b2c3d4"

sudo systemctl restart redis-server

# 验证
redis-cli -a YourRedisStr0ngPass! ping   # → PONG

第三步:安装 PHP-FPM

# 安装 PHP 及扩展
sudo apt install -y php8.2-fpm php8.2-mysql php8.2-redis \
    php8.2-curl php8.2-gd php8.2-mbstring php8.2-xml php8.2-opcache

3.1 配置 PHP-FPM

# /etc/php/8.2/fpm/php.ini 关键配置
memory_limit = 256M
max_execution_time = 60
upload_max_filesize = 50M
display_errors = Off
log_errors = On
opcache.enable = 1
opcache.memory_consumption = 128
date.timezone = Asia/Shanghai

# /etc/php/8.2/fpm/pool.d/www.conf 进程池
listen = /run/php/php8.2-fpm.sock      # Unix Socket(比 TCP 更高效)
pm = dynamic
pm.max_children = 50                   # 根据内存调整
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500                  # 防止内存泄漏
slowlog = /var/log/php8.2-fpm/www-slow.log
request_slowlog_timeout = 5s

# 启用 Nginx stub_status(监控用)
# 在 server 块中添加:
# location /nginx_status { stub_status on; allow 127.0.0.1; deny all; }

# 启动
sudo php-fpm8.2 -t                    # 检查配置
sudo systemctl restart php8.2-fpm
ps aux | grep php-fpm                  # 验证进程
# 预期输出:看到多个 php-fpm: pool www 进程

第四步:安装 Nginx

sudo apt install -y nginx

4.1 主配置优化

# /etc/nginx/nginx.conf
user www-data;
worker_processes auto;
events { worker_connections 1024; use epoll; }
http {
    include /etc/nginx/mime.types;
    sendfile on;
    tcp_nopush on;
    keepalive_timeout 65;
    client_max_body_size 64m;

    # Gzip 压缩
    gzip on;
    gzip_types text/plain text/css text/javascript application/json application/javascript image/svg+xml;
    gzip_min_length 1000;

    # 安全头
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;

    # SSL 全局配置
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;

    include /etc/nginx/sites-enabled/*;
}

4.2 虚拟主机配置

# /etc/nginx/sites-available/webapp
# HTTP → HTTPS 重定向
server {
    listen 80;
    server_name nb.iohow.com www.nb.iohow.com;
    location /.well-known/acme-challenge/ { root /var/www/html; }
    location / { return 301 https://$host$request_uri; }
}

# HTTPS 服务器
server {
    listen 443 ssl http2;
    server_name nb.iohow.com www.nb.iohow.com;
    ssl_certificate /etc/letsencrypt/live/nb.iohow.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/nb.iohow.com/privkey.pem;

    root /var/www/webapp/public;
    index index.php index.html;
    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    # PHP 转发给 PHP-FPM
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    }

    # 禁止访问隐藏文件和敏感文件
    location ~ /\. { deny all; }
    location ~* \.(env|log|ini|conf)$ { deny all; }

    # 静态文件缓存 30 天
    location ~* \.(jpg|jpeg|png|gif|css|js|svg|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # Nginx 状态页(仅本机)
    location /nginx_status { stub_status on; allow 127.0.0.1; deny all; }
}

# 启用站点
sudo ln -sf /etc/nginx/sites-available/webapp /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl restart nginx
# 预期输出:nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
#            nginx: configuration file /etc/nginx/nginx.conf test is successful

第五步:配置 Let's Encrypt

# 安装 Certbot
sudo apt install -y certbot python3-certbot-nginx

# 申请证书(确保域名 DNS 已解析到服务器 IP)
sudo certbot --nginx -d nb.iohow.com -d www.nb.iohow.com \
    --email admin@nb.iohow.com --agree-tos --no-eff-email

# 验证证书
sudo certbot certificates

# 测试自动续期
sudo certbot renew --dry-run

# 自动续期由 cron 自动配置,每天尝试两次

第六步:部署示例应用

6.1 应用配置

# /var/www/webapp/config/app.php
<?php
return [
    'database' => [
        'host'     => '127.0.0.1',
        'dbname'   => 'webapp',
        'username' => 'webapp_user',
        'password' => 'YourStr0ng!Passw0rd',
    ],
    'redis' => [
        'host'     => '127.0.0.1',
        'port'     => 6379,
        'password' => 'YourRedisStr0ngPass!',
    ],
];
?>

6.2 PHP 应用入口

# /var/www/webapp/public/index.php
<?php
date_default_timezone_set('Asia/Shanghai');
$config = require __DIR__ . '/../config/app.php';
$page = ltrim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/') ?: 'home';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>企业级 Web 应用</title>
<style>
body { font-family: sans-serif; background: #f5f7fa; margin: 0; }
header { background: linear-gradient(135deg, #1890ff, #096dd9); color: #fff; padding: 30px; text-align: center; }
.container { max-width: 960px; margin: 0 auto; padding: 20px; }
.nav { background: #fff; padding: 15px; text-align: center; border-radius: 8px; margin: 20px 0; }
.nav a { margin: 0 15px; color: #1890ff; text-decoration: none; }
.card { background: #fff; border-radius: 8px; padding: 25px; margin: 20px 0; box-shadow: 0 1px 3px rgba(0,0,0,.1); }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #f0f0f0; }
.ok { color: #52c41a; font-weight: bold; }
</style>
</head>
<body>
<header><h1>企业级 Web 应用</h1><p>Nginx + PHP-FPM + MySQL + Redis</p></header>
<div class="container">
<nav class="nav">
    <a href="/">首页</a>
    <a href="/status">服务状态</a>
    <a href="/database">数据库</a>
    <a href="/cache">缓存</a>
</nav>

<?php if ($page === 'home'): ?>
<div class="card">
    <h2>欢迎使用</h2>
    <p>本项目整合了企业级 Web 环境所需的所有核心组件。</p>
</div>

<?php elseif ($page === 'status'): ?>
<div class="card">
    <h2>服务状态</h2>
    <table>
    <tr><th>服务</th><th>状态</th></tr>
    <tr><td>MySQL</td><td><?php try { $pdo = new PDO('mysql:host=127.0.0.1','webapp_user','YourStr0ng!Passw0rd'); echo '<span class="ok">运行中</span> (' . $pdo->query("SELECT VERSION()")->fetchColumn() . ')'; } catch(Exception $e) { echo '异常'; } ?></td></tr>
    <tr><td>Redis</td><td><?php try { $r = new Redis(); $r->connect('127.0.0.1',6379); $r->auth('YourRedisStr0ngPass!'); echo '<span class="ok">运行中</span>'; } catch(Exception $e) { echo '异常'; } ?></td></tr>
    <tr><td>PHP</td><td><span class="ok">运行中</span> (<?= phpversion() ?>)</td></tr>
    </table>
</div>

<?php elseif ($page === 'database'): ?>
<div class="card">
    <h2>数据库操作</h2>
    <?php
    $msg = '';
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        $pdo = new PDO('mysql:host=127.0.0.1;dbname=webapp','webapp_user','YourStr0ng!Passw0rd');
        $act = $_POST['action'] ?? '';
        if ($act === 'create') {
            $pdo->exec("CREATE TABLE IF NOT EXISTS users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100), email VARCHAR(255) UNIQUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)");
            $msg = 'users 表创建成功';
        } elseif ($act === 'insert' && !empty($_POST['name'])) {
            $st = $pdo->prepare("INSERT IGNORE INTO users (name,email) VALUES (?,?)");
            $st->execute([$_POST['name'], $_POST['email']]);
            $msg = '数据插入成功';
        }
    }
    if ($msg) echo "<p class='ok'>$msg</p>";
    ?>
    <form method="post" style="margin-bottom:15px;">
        <button name="action" value="create">创建 users 表</button>
    </form>
    <form method="post">
        <input type="hidden" name="action" value="insert">
        <input name="name" placeholder="姓名" required>
        <input name="email" placeholder="邮箱" required>
        <button type="submit">插入</button>
    </form>
</div>

<?php elseif ($page === 'cache'): ?>
<div class="card">
    <h2>Redis 缓存测试</h2>
    <?php
    $r = new Redis(); $r->connect('127.0.0.1',6379); $r->auth('YourRedisStr0ngPass!');
    $msg = '';
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        if (($_POST['action'] ?? '') === 'set') {
            $r->setex($_POST['key'], (int)($_POST['ttl'] ?? 300), $_POST['value']);
            $msg = "已设置: {$_POST['key']} = {$_POST['value']}";
        } elseif (($_POST['action'] ?? '') === 'get') {
            $v = $r->get($_POST['key'] ?? '');
            $msg = $v !== false ? "命中: {$_POST['key']} = $v (TTL:{$r->ttl($_POST['key'])}s)" : "未命中";
        }
    }
    if ($msg) echo "<p class='ok'>$msg</p>";
    ?>
    <form method="post">
        <input type="hidden" name="action" value="set">
        <input name="key" placeholder="Key" required>
        <input name="value" placeholder="Value" required>
        <input name="ttl" value="300" style="width:60px">秒
        <button>设置</button>
    </form>
    <form method="post" style="margin-top:10px;">
        <input type="hidden" name="action" value="get">
        <input name="key" placeholder="Key" required>
        <button>读取</button>
    </form>
</div>

<?php else: ?>
<div class="card"><h2>404</h2></div>
<?php endif; ?>
</div>
</body></html>

第七步:配置监控

7.1 安装 Prometheus + Exporters

# Prometheus
sudo useradd --no-create-home --shell /bin/false prometheus
PROM_VER="2.48.0"
cd /tmp && wget https://github.com/prometheus/prometheus/releases/download/v${PROM_VER}/prometheus-${PROM_VER}.linux-amd64.tar.gz
tar xzf prometheus-${PROM_VER}.linux-amd64.tar.gz
sudo cp prometheus-${PROM_VER}.linux-amd64/prometheus /usr/local/bin/
sudo mkdir -p /etc/prometheus /var/lib/prometheus
sudo chown prometheus:prometheus /var/lib/prometheus

# Node Exporter(系统指标)
NODE_VER="1.9.0"
wget -q https://github.com/prometheus/node_exporter/releases/download/v${NODE_VER}/node_exporter-${NODE_VER}.linux-amd64.tar.gz
tar xzf node_exporter-${NODE_VER}.linux-amd64.tar.gz
sudo cp node_exporter-${NODE_VER}.linux-amd64/node_exporter /usr/local/bin/

# MySQL Exporter
sudo mysql -u root -p -e "CREATE USER 'exporter'@'localhost' IDENTIFIED BY 'ExporterPass'; GRANT PROCESS,REPLICATION CLIENT ON *.* TO 'exporter'@'localhost'; FLUSH PRIVILEGES;"
echo -e "[client]\nuser=exporter\npassword=ExporterPass" | sudo tee /etc/.mysqld_exporter.cnf
sudo chmod 600 /etc/.mysqld_exporter.cnf
MYSQL_EXP_VER="0.15.1"
wget -q https://github.com/prometheus/mysqld_exporter/releases/download/v${MYSQL_EXP_VER}/mysqld_exporter-${MYSQL_EXP_VER}.linux-amd64.tar.gz
tar xzf mysqld_exporter-${MYSQL_EXP_VER}.linux-amd64.tar.gz
sudo cp mysqld_exporter-${MYSQL_EXP_VER}.linux-amd64/mysqld_exporter /usr/local/bin/

# Redis Exporter
REDIS_EXP_VER="1.55.0"
wget -q https://github.com/oliver006/redis_exporter/releases/download/v${REDIS_EXP_VER}/redis_exporter-v${REDIS_EXP_VER}.linux-amd64.tar.gz
tar xzf redis_exporter-v${REDIS_EXP_VER}.linux-amd64.tar.gz
sudo cp redis_exporter-v${REDIS_EXP_VER}.linux-amd64/redis_exporter /usr/local/bin/

# 清理
rm -rf /tmp/prometheus-* /tmp/node_exporter-* /tmp/mysqld_exporter-* /tmp/redis_exporter-*

7.2 Prometheus 配置与服务

# /etc/prometheus/prometheus.yml
global:
  scrape_interval: 15s
scrape_configs:
  - job_name: "prometheus"
    static_configs: [{ targets: ["localhost:9090"] }]
  - job_name: "node"
    static_configs: [{ targets: ["localhost:9100"] }]
  - job_name: "mysql"
    static_configs: [{ targets: ["localhost:9104"] }]
  - job_name: "redis"
    static_configs: [{ targets: ["localhost:9121"] }]
  - job_name: "nginx"
    static_configs: [{ targets: ["localhost:9113"] }]

# 创建 systemd 服务(以 node_exporter 为例,其他类似)
for svc in node_exporter mysqld_exporter redis_exporter; do
cat > /etc/systemd/system/${svc}.service << EOF
[Unit]
Description=${svc}
After=network.target
[Service]
User=prometheus
ExecStart=/usr/local/bin/${svc}
Restart=always
[Install]
WantedBy=multi-user.target
EOF
done
sudo systemctl daemon-reload
sudo systemctl enable --now prometheus node_exporter mysqld_exporter redis_exporter
# 验证:curl http://localhost:9090/-/healthy

7.3 安装 Grafana

wget -q -O - https://apt.grafana.com/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/grafana.gpg
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | sudo tee /etc/apt/sources.list.d/grafana.list
sudo apt update && sudo apt install -y grafana
sudo systemctl enable --now grafana-server
# 默认访问: http://服务器IP:3000  用户名/密码: admin/admin
# 首次登录后请立即修改密码

7.4 添加 Prometheus 数据源

# 通过 API 自动配置 Prometheus 为 Grafana 默认数据源
curl -s -X POST http://localhost:3000/api/datasources \
    -H "Content-Type: application/json" \
    -u admin:admin \
    -d '{
        "name": "Prometheus",
        "type": "prometheus",
        "url": "http://localhost:9090",
        "access": "proxy",
        "isDefault": true
                    }'
# 预期返回: {"status":"success","message":"Datasource Prometheus added"}
echo "数据源配置完成"

第八步:配置日志

# rsyslog 分类日志
sudo tee /etc/rsyslog.d/50-webapp.conf << 'EOF'
:programname, isequal, "nginx"  /var/log/webapp/nginx.log
:programname, isequal, "mysqld" /var/log/webapp/mysql.log
:programname, isequal, "php-fpm" /var/log/webapp/php-fpm.log
& stop
EOF
sudo mkdir -p /var/log/webapp
sudo systemctl restart rsyslog

# logrotate 轮转
sudo tee /etc/logrotate.d/webapp << 'EOF'
/var/log/webapp/*.log /var/log/nginx/webapp-*.log {
    daily
    rotate 30
    compress
    delaycompress
    notifempty
    create 0640 www-data adm
    sharedscripts
    postrotate
        [ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid) 2>/dev/null || true
    endscript
}
EOF

# 测试
sudo logrotate -d /etc/logrotate.d/webapp  # 干跑检查
sudo logrotate -f /etc/logrotate.d/webapp   # 强制执行一次

第九步:配置备份

sudo mkdir -p /opt/backup/db /var/log/backup
sudo tee /opt/backup/backup.sh << 'BASHEOF'
#!/bin/bash
set -euo pipefail
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/opt/backup/db/${DATE}"
RETENTION=30
MYSQL_PASS="YourStr0ng!Passw0rd"
REDIS_PASS="YourRedisStr0ngPass!"

mkdir -p "${BACKUP_DIR}"
echo "[$(date)] 开始备份..."

# MySQL 完整备份
mysqldump -u root -p"${MYSQL_PASS}" --single-transaction --all-databases | gzip > "${BACKUP_DIR}/mysql.sql.gz"

# 配置文件备份
tar czf "${BACKUP_DIR}/config.tar.gz" -C / etc/nginx etc/mysql etc/redis etc/php/8.2/fpm var/www/webapp/config 2>/dev/null || true

# Redis RDB
redis-cli -a "${REDIS_PASS}" --rdb "${BACKUP_DIR}/redis.rdb" 2>/dev/null || true

# 校验和
cd "${BACKUP_DIR}" && md5sum * > checksums.md5

# 清理旧备份
find /opt/backup/db -maxdepth 1 -type d -mtime +${RETENTION} -exec rm -rf {} \; 2>/dev/null || true
echo "[$(date)] 备份完成: $(du -sh ${BACKUP_DIR} | cut -f1)"
BASHEOF
sudo chmod +x /opt/backup/backup.sh

# 定时任务
sudo crontab -e
# 添加: 0 2 * * * /opt/backup/backup.sh >> /var/log/backup/cron.log 2>&1

# 手动测试
sudo /opt/backup/backup.sh
ls -la /opt/backup/db/  # 验证备份文件

测试验证

echo "===== 服务状态 ====="
for svc in nginx php8.2-fpm mysql redis-server prometheus grafana-server; do
    echo "[$(sudo systemctl is-active $svc)] $svc"
done

echo "===== 端口检查 ====="
ss -tlnp | grep -E ':(80|443|3306|6379|9090|3000) '

echo "===== SSL 检查 ====="
curl -sk -o /dev/null -w "HTTPS: %{http_code}\n" https://nb.iohow.com/

echo "===== 数据库连接 ====="
mysql -u webapp_user webapp -e "SELECT 'OK' AS status;" 2>/dev/null

echo "===== Redis 连接 ====="
redis-cli -a YourRedisStr0ngPass! ping 2>/dev/null

echo "===== 备份检查 ====="
LATEST=$(ls -t /opt/backup/db/ 2>/dev/null | head -1)
[ -n "$LATEST" ] && echo "最近备份: $LATEST" && ls -lh "/opt/backup/db/${LATEST}/" || echo "未找到备份"

echo "===== Prometheus 健康 ====="
curl -s http://localhost:9090/-/healthy

echo "===== Grafana 健康 ====="
curl -s http://localhost:3000/api/health | python3 -m json.tool 2>/dev/null | grep -o '"database":"[^"]*"'

常见错误

错误原因解决方案
502 Bad GatewayPHP-FPM 未运行或 Socket 路径不匹配检查 systemctl status php8.2-fpm,确认 fastcgi_passlisten 路径一致
Connection refused (MySQL)MySQL 未启动或 bind-address 配置错误检查 systemctl start mysqlbind-address
NOAUTH (Redis)Redis 设置了密码但未认证所有连接加 -a 参数或代码中调用 auth()
ERR_TOO_MANY_REDIRECTSHTTP→HTTPS 重定向循环确认 Nginx 不会对 HTTPS 请求再做重定向
Certbot 限流7 天内申请超 5 次等 7 天后重试,或用 --staging 测试
Permission denied (Nginx)Nginx 用户无法访问文件chown -R www-data:www-data /var/www/webapp
Grafana 页面无法访问服务未启动或端口未放行检查 systemctl status grafana-server,确认安全组放行 3000
Prometheus targets DOWNExporter 未启动逐个检查 Exporter 服务,访问 http://localhost:9100/metrics
备份文件为空MySQL 密码错误或 Redis 未运行检查 /var/log/backup/cron.log 中的错误信息

最佳实践

类别实践说明
安全最小权限原则数据库用户只授必要权限,禁用 root 远程登录
安全仅本机监听MySQL、Redis 只绑定 127.0.0.1
性能Unix SocketNginx↔PHP-FPM 用 Socket 比 TCP 更高效
性能OPcache + GzipPHP OPcache 减少 50%+ 加载时间,Gzip 压缩 60-80% 传输
运维日志轮转logrotate 防止日志无限增长
运维自动备份每日备份 + 30 天保留 + 定期恢复测试
运维监控告警Prometheus 采集 + Grafana 可视化
运维SSL 自动续期certbot cron 确保证书不过期

练习题

  1. (基础)按照本项目步骤搭建完整环境,记录每步结果。
  2. (进阶)在 Grafana 中创建包含 MySQL 连接数、查询速率、慢查询统计的仪表盘。
  3. (进阶)编写 Shell 脚本自动检测所有服务状态,异常时发送告警。
  4. (高级)将部署过程改写为 Ansible Playbook,实现一键部署。
  5. (高级)配置 Prometheus Alertmanager,CPU >80% 或磁盘 <20% 时发送告警。
  6. (探究)研究 Nginx upstream 模块,画出多服务器负载均衡架构图。
  7. (探究)对比 Nginx 与 Apache 的工作模型差异,解释为什么 Nginx 在高并发场景下性能更好。
  8. (探究)研究 MySQL 主从复制原理,思考如何将本项目的单机数据库扩展为读写分离架构。
  9. (探究)研究 Docker 容器化部署方案,思考如何将本项目的各组件容器化管理。

恢复测试

备份的最终目的是能恢复。建议每月执行一次恢复演练:

# 在测试环境恢复数据库(注意:会覆盖现有数据)
LATEST=$(ls -t /opt/backup/db/ | head -1)
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS restore_test;"
gunzip -c "/opt/backup/db/${LATEST}/mysql.sql.gz" | mysql -u root -p restore_test
echo "恢复完成,检查数据完整性"
mysql -u root -p -e "SHOW TABLES FROM restore_test;"
mysql -u root -p -e "DROP DATABASE restore_test;"  # 清理测试数据

学习检查点

学完本章后,请检验自己是否掌握以下内容:

检查项自测问题验证方法
概念理解能用自己的话解释企业级 Web 应用的完整技术栈架构尝试向他人讲解
命令操作能不查文档完成 MySQL、Redis、Nginx 的部署和配置在终端实际执行
原理掌握能说出 Web 应用的请求处理流程和各组件协作原理画出流程图
故障排查能独立排查 Web 应用 502/504 错误或数据库连接失败模拟故障并修复
最佳实践能说明为什么需要为每个组件配置独立的日志和监控对比不同方案

本章总结

本项目从零搭建了企业级 Web 应用环境,覆盖了从数据库到监控的完整技术栈。以下是核心要点回顾:

组件核心配置关键文件
MySQL仅本机、强密码、最小权限/etc/mysql/mysql.conf.d/mysqld.cnf
Redis密码、仅本机、RDB+AOF/etc/redis/redis.conf
PHP-FPMUnix Socket、OPcache/etc/php/8.2/fpm/pool.d/www.conf
Nginx反向代理、SSL、Gzip/etc/nginx/sites-available/webapp
SSLLet's Encrypt、自动续期/etc/letsencrypt/live/
监控Prometheus + Grafana/etc/prometheus/prometheus.yml
日志rsyslog 分类 + logrotate/etc/rsyslog.d/50-webapp.conf
备份每日备份 + 30 天保留/opt/backup/backup.sh

掌握这套环境的搭建与运维,是进入 DevOps 领域的重要一步。实际生产中还需考虑高可用、灾备、CI/CD 等进阶话题。

延伸阅读

↑ 回到顶部