一台服务器跑多个站:Nginx配置与资源隔离

单服务器跑多个站:Nginx server 块与资源隔离

很多个人站长手上一台 2 核 4G 的 VPS,上面挂着三五个站:一个主力博客、一个工具站、两个放着不管的实验站。一开始都能跑,等到某一站长起来,或者某一站被爬虫抓爆,整台机器一起挂。这篇讲的就是「多站点」这件事怎么做对:Nginx 层怎么切、PHP 层怎么隔、MySQL 层怎么分权,以及出问题时怎么一眼看出是哪个站的锅。

先说结论:多站共用一个 Nginx 很容易,共用一个 PHP-FPM 就危险,共用一套 MySQL 账号则是隐患。按这个顺序做隔离,成本最低、收益最明显。

Nginx 层的多站点:server_name 与 default_server

多个站点的最小配置是给每个域名写一个 server 块,listen 80 后面不再跟域名,靠 server_name 匹配。这里最容易出问题的是「没有匹配到任何 server_name」时怎么办——Nginx 会把请求交给监听同一端口的第一个 server 块,或者显式标了 default_server 的那个。

server {
    listen 80 default_server;
    server_name _;
    access_log /var/log/nginx/default_access.log;
    return 444;
}

写一个兜底 server 直接返回 444(Nginx 特有,不回任何响应直接断连接),好处有两个:一是别人拿 IP 直接访问时不会看到你某个站的首页,避免重复内容进搜索引擎;二是能挡掉大量扫描器和空 Host 请求。这件事必须在加站点之前就做好,否则等三个站上线再回头加 default_server,Nginx 会因为端口冲突拒绝启动——一个端口只能有一个 default_server。

每个站的配置单独一个文件放在 /etc/nginx/conf.d/ 或 sites-enabled/,命名带前缀编号,比如 10-blog.conf、20-tools.conf。这样 nginx -T 的输出顺序稳定,出问题时能快速定位是哪个文件。

日志必须按站分开:否则排障全靠猜

共用 /var/log/nginx/access.log 是最常见的偷懒做法,也是多站运维里最贵的偷懒。两个站共用一份日志,你没法统计单站流量,也没法用 fail2ban 针对某一站封 IP(一封就是两站一起封)。

每个 server 块里显式指定日志路径,并且统一加一个站点前缀字段,方便后续 awk 汇总:

server {
    listen 443 ssl http2;
    server_name blog.example.com;
    access_log /var/log/nginx/blog.access.log sitefmt;
    error_log  /var/log/nginx/blog.error.log warn;
    ssl_certificate     /etc/letsencrypt/live/blog.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/blog.example.com/privkey.pem;
}

# http 段里定义带站点名的日志格式
log_format sitefmt '$host $remote_addr - [$time_local] "$request" '
                   '$status $body_bytes_sent $request_time "$http_user_agent"';

注意 log_format 必须写在 http 段(通常在 nginx.conf 里),不能写在 server 块里。改完先 nginx -t 再 systemctl reload nginx,不要直接 restart,reload 是平滑的,不会断掉正在处理的请求。

顺便把 logrotate 里每个站的日志都加上。默认的 /etc/logrotate.d/nginx 只覆盖 /var/log/nginx/*.log,如果你把某个站的日志放到了 /data/logs/,它会一直涨到把分区写满,然后那个站写日志失败,Nginx 报 write() to "/data/logs/x.log" failed (28: No space left on device),同时其他站也跟着出问题。

PHP-FPM:一个站一个 pool,别共用

这是多站隔离的核心。所有站共用一个 www pool 的后果是:A 站一个慢脚本把 pm.max_children 占满,B 站的请求全部排队,用户看到的是「B 站挂了」,但 B 站的日志里干干净净,什么都没有。

每个站建独立 pool,配置文件放 /etc/php/8.3/fpm/pool.d/blog.conf:

[blog]
user = blog
group = blog
listen = /run/php/blog.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
pm = dynamic
pm.max_children = 12
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
request_terminate_timeout = 45s
request_slowlog_timeout = 3s
slowlog = /var/log/php/blog-slow.log
php_admin_value[error_log] = /var/log/php/blog-error.log
php_admin_flag[log_errors] = on
php_admin_value[open_basedir] = /var/www/blog:/tmp
php_admin_value[upload_tmp_dir] = /var/www/blog/tmp

几个关键点:

1. 用 Unix socket 而不是 127.0.0.1:9000。socket 路径带站点名,Nginx 的 fastcgi_pass unix:/run/php/blog.sock 一眼能看出对接的是哪个 pool,不会串。端口方式的经典事故是复制配置时忘了改端口,两个站都指向 9000,B 站的 PHP 代码跑在 A 站的数据库配置下。

2. pm.max_children 要按站分配,总和不能超过内存。每个 PHP 进程实际占用约 40~80MB(看框架),4G 内存的机器留 1G 给系统和 MySQL,剩 3G 大概能开 40 个进程。主力站拿大头,实验站给 4~6 个就够。分配时写张表贴在运维笔记里,别凭感觉改。

3. open_basedir 是 PHP 层的护栏。一旦某个站被上传了 webshell,open_basedir 能阻止它读到其他站的配置文件和数据库密码。它不影响性能,属于无脑该开的东西。注意要带上 /tmp 或该站自己的 tmp 目录,否则 session 和临时文件会报错。

4. 每个 pool 必须有独立的 user。建系统用户时用 useradd -r -s /usr/sbin/nologin -d /var/www/blog blog,目录属主改成 blog:blog,权限 755。如果所有站都跑 www-data,那么一个站的代码可以随意读写另一个站的整个目录,隔离等于没做。

MySQL:一个站一个库一个用户

权限隔离比性能隔离更重要。每个站一个数据库,一个专用账号,只授该库的权限:

CREATE DATABASE blog DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'blog'@'localhost' IDENTIFIED BY '强密码';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP,
      REFERENCES, LOCK TABLES ON blog.* TO 'blog'@'localhost';
FLUSH PRIVILEGES;

不要给 GRANT ALL ON *.*,也不要给 FILE 权限(能读写服务器任意文件)。@'localhost' 而不是 @'%',PHP 走 socket 或 127.0.0.1 都够用。

连接数要算总账。max_connections 默认 151,三个站的 pool 如果各开 30 个常驻连接,加起来就逼近上限了。更麻烦的是 PHP 的持久连接(pconnect):每个 FPM 进程一条常驻连接,进程数 × 站数就是 MySQL 的连接数。SHOW PROCESSLIST 里看到一堆 Sleep 状态的连接,基本都是它。关掉持久连接(PDO 的 PDO::ATTR_PERSISTENT 设为 false)后连接数会明显下降,代价是每个请求多一次握手,本地 socket 上这个代价可以忽略。

如果某一站的慢查询拖垮了全库,可以给它单独设资源组(MySQL 8 的 CREATE RESOURCE GROUP),或者把最重的站迁到独立实例——这台机器的 IO 才是最终的瓶颈。

磁盘与并发:多站最容易一起挂的两个点

多站共用一块盘,任何一站的日志、备份、缓存目录失控,全站一起写不进去。分开目录是第一步:每个站的代码在 /var/www/<site>,日志在 /var/log/nginx/<site>.access.log,备份在 /var/backups/<site>/,临时文件在站点自己的 tmp 里。这样 du -sh /var/www/* 就能一眼看出谁在膨胀。

第二步是给每个站设个软配额。Linux 的磁盘 quota 在 ext4 上要重新挂载并开启,稍重。更轻的办法是写个 cron 脚本,每天检查各站目录大小和 inode 数,超阈值就发邮件:

#!/bin/bash
LIMIT_MB=2048
for d in /var/www/*/; do
  site=$(basename "$d")
  mb=$(du -sm "$d" | cut -f1)
  if [ "$mb" -gt "$LIMIT_MB" ]; then
    echo "$site 目录 ${mb}MB 超过阈值 ${LIMIT_MB}MB" | mail -s "磁盘告警 $site" you@example.com
  fi
done

并发方面,Nginx 的 limit_conn 和 limit_req 要按 server 块分别配置,而不是在 http 段配一个全局的。全局配置的后果是 A 站被刷时,B 站的正常用户也会被一起限速,因为共享同一个 zone。给每个站一个独立 zone 名字:limit_req_zone $binary_remote_addr zone=blog_req:10m rate=5r/s;,然后在对应的 server 里引用。

出问题时怎么快速定位是哪个站

多站环境下的排障顺序和单站不一样,先「定站」再「定因」:

第一步,看 load 和 CPU。top 里按 CPU 排序,如果是 php-fpm 进程高,用 ps -eo pid,user,%cpu,cmd --sort=-%cpu | head 看进程属主——因为每个站一个 pool 一个 user,属主直接告诉你是哪个站。www-data 就说明某个 pool 没按规范配置。

第二步,看连接。ss -tnp | grep php 看不出站名,但 ss -xp | grep blog.sock 可以,因为 socket 路径带站名。SQL 侧看 SELECT db, COUNT(*) FROM information_schema.processlist GROUP BY db;,哪个库的连接数异常,就是哪个站。

第三步,看日志。分站日志的价值在这一刻体现。用 tail -f /var/log/nginx/*.access.log 同时盯所有站,出问题的那个日志行会疯狂滚动,另外两个安静如常。

第四步,看慢日志。每个 pool 的 slowlog 分开写,谁家脚本慢一看就知道。慢日志里带完整调用栈的栈顶函数,通常直接指向框架里的某个方法或者某个 file_get_contents。

要不要用 Docker 隔开

很多人会问:多站是不是应该上 Docker,一站一个容器。答案是:如果你已经熟练 Docker,值得;如果只是听说 Docker 隔离好,那反而增加故障面。

Docker 在这件事上的优势是资源限制(--cpus、--memory)和依赖隔离(A 站要 PHP 7.4,B 站要 8.3,容器能直接解决)。代价是网络模型复杂了一层,real_ip 要额外配,日志要挂卷,MySQL 跑容器里的备份和升级更麻烦。个人站长常见的折中是:PHP 和 Nginx 上 Docker,数据库留在宿主机,这样既解决了版本冲突,又不用对付数据库容器的持久化焦虑。

如果不用 Docker,那上面的「独立 pool + 独立 user + 独立 open_basedir」就是最低限度的替代方案,它能覆盖 90% 的隔离需求,成本只是多写几个配置文件。

一份可执行的上线检查单

新增一个站点时,按这个顺序过一遍,基本上不会留下隐患:

1. 建系统用户 blog,目录 /var/www/blog 属主改为 blog:blog,权限 755,上传目录 775 且不要给 others 写权限。

2. 建 Nginx server 块,显式指定 access_log / error_log 到带站名的文件,域名写全(不带 www 和带 www 各一个或做 301)。

3. 建 FPM pool,socket 路径带站名,独立 pm.max_children,开 open_basedir。

4. 建库、建专用用户,只授该库权限,字符集 utf8mb4。

5. 加 logrotate 条目,按天的同时设 rotate 14 和 maxsize 100M。

6. 加证书,certbot --nginx -d 会自动写 server 块,但要注意它可能改动你的 listen 行,改完对比一下 nginx -T 的差异。

7. 在监控脚本里加一个探针:请求该站首页,检查 HTTP 200、响应体里有特定字符串(比如站点标题)、耗时小于 3 秒。三个站三条探针,不要只探主站。

8. 把该站的 pm.max_children、库名、日志路径写进运维笔记,下次扩机器时按笔记算总容量。

多站点运维说到底是「把共用变成分开」:日志分开、进程分开、用户分开、权限分开。分开之后,任何一站的故障都被限制在自己的边界里,主机上其他站照常服务——这才是「一台机器跑多个站」真正省钱的姿势,否则省下的服务器钱会用加班时间还回去。

Last modification:October 22nd, 2026 at 07:28 pm

Leave a Comment