高流量网站如何进行服务器架构规划?Web、数据库与缓存分层部署

促销开始前一周,老板要求“这次绝对不能崩”。你看着那台跑着 Nginx + PHP + MySQL 的单机,心里没底。加配置是最直觉的做法——把 4 核 8G 升到 16 核 32G,确实能扛一阵,但只要所有组件还在同一台机器上,瓶颈会从一个组件转移到另一个组件:CPU 空了,磁盘先扛不住;磁盘换了 SSD,数据库连接数又爆了;最后永远有那一个“拖后腿的”。

真正能线性扩展的做法是分层:把 Web、缓存、数据库、存储拆开,各自按自己的瓶颈点扩容。本文给出一套从“单机全能”到“分层集群”的演进路径,包含容量换算方法、各层的部署要点、不同流量级别的推荐配置,以及一份可执行的迁移清单。所有推荐值都给出判断依据,方便你按自己的业务特征调整。

一、先把需求量化:从 PV 到并发再到机器数

架构规划的第一步不是画拓扑图,而是算清楚到底要扛多少量。很多人张口就是“日活百万”,但真正决定机器数量的是峰值每秒请求数(QPS)。

1.1 换算公式

基础参数
  DAU(日活)= 100,000
  人均日请求数 = 20(页面 + 接口 + 静态资源)
  日总请求数 = 100,000 × 20 = 2,000,000

峰值集中系数
  经验值:约 60%-80% 的流量集中在 20% 的时间内(晚 8-10 点)
  峰值小时请求数 = 2,000,000 × 0.6 = 1,200,000
  峰值 QPS = 1,200,000 ÷ 3600 ≈ 333 QPS

考虑秒杀/抢购的尖峰倍数
  普通场景峰值 QPS × 2~3 作为设计目标
  秒杀场景峰值 QPS × 10 以上,且必须靠缓存和队列削峰

Web 层机器数估算
  单台 4 核 Nginx 静态+反代约 5000-10000 QPS
  单台 4 核 PHP-FPM 动态接口约 200-800 QPS(视业务复杂度)
  所需机器 = 峰值 QPS ÷ 单机能力 × 1.5(冗余系数)

这里的关键洞察是:静态资源和动态接口的承载能力差了一个数量级以上。把它们混在一台机器上,等于用最贵的资源做最便宜的事。

1.2 不同量级的推荐架构

阶段日 PV峰值 QPS推荐架构大致机器数
起步< 5 万< 50单机:Nginx + PHP + MySQL 一体1 台 2-4 核
成长5-50 万50-500Web 与数据库分离,引入 Redis 与 CDN2-3 台
规模50-500 万500-3000Web 集群 + 负载均衡 + Redis 主从 + MySQL 主从读写分离5-10 台
大型> 500 万> 3000多层 LB + 服务拆分 + Redis 集群 + 分库分表 + 对象存储10 台以上

注意表格里的机器数是按业务复杂度中估算的参考值,实际要以上面压测出来的单机能力为准。千万不要照抄。

二、分层架构总览

一个成熟的高流量网站典型拓扑如下,每一层都可以独立扩容:

                     用户
                      │
                 ┌────▼────┐
                 │   CDN   │  静态资源(图片/CSS/JS/视频)
                 └────┬────┘      承担 70%-90% 的请求量
                      │
                 ┌────▼────┐
                 │ SLB/LVS │  四层负载均衡 + 高可用
                 └────┬────┘
        ┌─────────────┼─────────────┐
   ┌────▼────┐   ┌────▼────┐   ┌────▼────┐
   │ Nginx-1 │   │ Nginx-2 │   │ Nginx-3 │  Web 接入层(可横向扩展)
   └────┬────┘   └────┬────┘   └────┬────┘
        └─────────────┼─────────────┘
                      │
        ┌─────────────┼─────────────┐
   ┌────▼────┐   ┌────▼────┐   ┌────▼────┐
   │  App-1  │   │  App-2  │   │  App-3  │  应用层(PHP/Go/Java)
   └────┬────┘   └────┬────┘   └────┬────┘
        └─────────────┼─────────────┘
             ┌────────┼────────┐
        ┌────▼───┐ ┌──▼───┐ ┌──▼──────┐
        │ Redis  │ │ MQ   │ │ MySQL   │  数据层
        │ 主从   │ │ 队列 │ │ 主从读写│
        └────────┘ └──────┘ │ 分离    │
                            └─────────┘

三条核心原则:

  • 无状态化:应用层不存会话、不存上传文件,才能随意加减机器。会话放 Redis,文件放对象存储。
  • 能缓存的不要计算:CDN 缓存静态资源,Redis 缓存热点数据与查询结果,Nginx 可以做页面级缓存。
  • 能异步的不要同步:下单后的发短信、加积分、生成报表全部丢进消息队列。

三、Web 层规划:Nginx 与应用服务器

3.1 Nginx 基础调优

# /etc/nginx/nginx.conf 核心参数
user  nginx;
worker_processes auto;              # 等于 CPU 核数

events {
    worker_connections 65535;       # 单 worker 最大连接数
    multi_accept on;
    use epoll;                      # Linux 下性能最好的事件模型
}

http {
    sendfile        on;             # 零拷贝发送静态文件
    tcp_nopush      on;
    tcp_nodelay     on;
    keepalive_timeout 65;
    server_tokens   off;
    client_max_body_size 20m;

    # 打开静态资源缓存(文件描述符缓存)
    open_file_cache max=100000 inactive=30s;
    open_file_cache_valid 60s;
    open_file_cache_min_uses 2;

    gzip on;
    gzip_comp_level 5;
    gzip_types text/plain text/css application/json application/javascript image/svg+xml;

    limit_conn_zone $binary_remote_addr zone=perip:10m;
    limit_req_zone  $binary_remote_addr zone=qps:10m rate=20r/s;
}
# 配套的系统参数 /etc/sysctl.conf
net.core.somaxconn = 65535          # 默认 128,高并发必改
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
net.core.netdev_max_backlog = 262144
fs.file-max = 2000000               # 系统级文件句柄上限

# 生效
sysctl -p

# 用户级句柄限制 /etc/security/limits.conf
nginx soft nofile 100000
nginx hard nofile 200000

3.2 应用层:PHP-FPM 与 PFM 池的合理配置

Web 层最容易低估的就是 PHP-FPM 进程数与内存的换算。公式很简单,但很多人没算过:

可用内存 = 物理内存 × 0.7(留 30% 给系统页缓存与其他进程)

pm.max_children ≈ 可用内存 ÷ 单个 PHP 进程平均内存占用

实测单进程占用(RSS,单位 KB):
ps --no-headers -o rss -C php-fpm | awk '{s+=$1;n++} END {print "进程数:",n,"平均RSS(KB):",s/n}'

示例:8G 内存机器,单进程 40MB
  pm.max_children = 8192 × 0.7 ÷ 40 ≈ 143 → 保守取 100

对应的池配置参考:

# /etc/php/8.1/fpm/pool.d/www.conf
pm = static                  # 高流量场景推荐 static,避免动态创建进程的抖动
pm.max_children = 100
pm.start_servers = 20        # dynamic 模式下生效
pm.min_spare_servers = 20
pm.max_spare_servers = 40
pm.max_requests = 1000       # 处理 1000 个请求后重启子进程,防内存缓慢泄漏
request_terminate_timeout = 30s
request_slowlog_timeout = 3s
slowlog = /var/log/php8.1-fpm.slow.log
listen.backlog = 2048        # 与 net.core.somaxconn 配套调整
pm.status_path = /fpm-status # 状态页,务必限制访问来源

3.3 Web 层的横向扩展要点

  1. 所有 Web 节点使用完全相同的配置,建议用 Ansible 或镜像批量下发,避免“某台机器配置不一样”的诡异问题。
  2. 代码发布采用软链接切换(/data/www/current -> /data/www/release-20241014),回滚只需改软链,秒级完成。
  3. 上传目录、会话、日志不要放本地——会话进 Redis,文件进对象存储或 NFS,日志集中采集。
  4. 每台节点都要配好健康检查接口(如 /health 返回 200),供负载均衡器探活。
  5. Nginx access_log 在高 QPS 下会成为磁盘 IO 瓶颈,静态资源 access_log off,动态日志用 buffer 写入。
access_log /var/log/nginx/access.log main buffer=32k flush=5s;

location ~* \.(jpg|jpeg|png|gif|css|js|woff2)$ {
    access_log off;
    expires 30d;
}

四、缓存层:挡住 80% 的请求

缓存是分层架构里性价比最高的一层。按顺序做四级缓存,每一级都能挡掉大量请求:

层级位置缓存内容典型命中率生效范围
浏览器缓存客户端CSS/JS/图片高单用户
CDN 缓存边缘节点静态资源、整页70%-90%全国/全球
Nginx 缓存Web 机本地代理响应、热点页中单节点
Redis独立缓存服务查询结果、会话、计数取决于键设计全站共享

4.1 Redis 部署与容量要点

# 生产建议:主从 + 哨兵,或 Redis Cluster
redis-cli info Replication
redis-cli info Memory          # used_memory_human / maxmemory_policy
redis-cli info Stats    | grep -E 'keyspace_hits|keyspace_misses'
redis-cli info Clients  | grep connected_clients

# 命中率计算:
# 命中率 = hits ÷ (hits + misses)
# 低于 90% 说明键设计或过期策略有问题,缓存形同虚设
  • 必须设置 maxmemory(建议物理内存的 50%-60%)与淘汰策略(maxmemory-policy allkeys-lru 或 volatile-lru),否则内存会被写满。
  • 禁止在生产使用 KEYS *,用 SCAN 替代;避免大 key(单 value 超过 10KB 就要警惕,超过 1MB 必须拆)。
  • 热 key(如首页配置)可以做本地缓存 + 短 TTL,避免单分片被打爆。
  • 缓存三大问题要提前防:穿透(查不存在的 key)用布隆过滤器或空值缓存;击穿(热点 key 过期)用互斥锁或逻辑过期;雪崩(大批 key 同时过期)给 TTL 加随机抖动。

4.2 Nginx 代理缓存(可选的一层)

proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=pagecache:100m
                 inactive=60m max_size=10g use_temp_path=off;

server {
    location / {
        proxy_cache pagecache;
        proxy_cache_key $scheme$host$request_uri;
        proxy_cache_valid 200 302 10m;
        proxy_cache_valid 404 1m;
        proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
        add_header X-Cache-Status $upstream_cache_status;
        proxy_pass http://backend;
    }
}

X-Cache-Status 返回 HIT / MISS / BYPASS,是验证缓存是否生效最快的方法。

五、数据库层:最贵的瓶颈

数据库是分层里最难水平扩展的一层,所以规划时要格外保守。

# 关键监控指标与判断标准
mysql -e "SHOW STATUS LIKE 'Threads_connected'; SHOW VARIABLES LIKE 'max_connections';"
# Threads_connected 持续 > max_connections × 0.8 → 需要加连接池或扩容

mysql -e "SHOW STATUS LIKE 'Slow_queries'; SHOW VARIABLES LIKE 'long_query_time';"
# Slow_queries 每分钟增长超过 10 个 → 必须优化 SQL 或索引

mysql -e "SHOW ENGINE INNODB STATUS\G" | grep -A5 'BUFFER POOL AND MEMORY'
# Buffer pool hit rate 应 > 99%,低于则 innodb_buffer_pool_size 偏小

mysql -e "SELECT * FROM information_schema.processlist WHERE command!='Sleep' AND time>5;"
# 长事务是锁等待与连接耗尽的元凶

5.1 数据库层演进建议

  1. 先优化再扩容:加索引、改慢 SQL、拆大表、避免 SELECT * 和深分页(LIMIT 100000,20 改为基于主键的游标分页)。
  2. 读写分离:主库写,多个从库读,应用层或中间件(ProxySQL、ShardingSphere-Proxy)做路由。
  3. 连接池:数据库侧的连接数是有限资源,用连接池控制,不要每个请求都新建连接。
  4. 垂直拆分:把日志表、统计表拆到独立实例,避免拖垮主业务库。
  5. 水平分片:单表超过 2000 万行或单库超过 500GB 才考虑,过早分库分表会显著增加复杂度。
方案解决的问题引入的复杂度建议时机
索引/SQL 优化慢查询、CPU 高无永远第一步
读写分离读压力大低(主从延迟)读 QPS 超过单机 50%
连接池连接数耗尽低Threads_connected 长期高位
垂直拆分大表拖累主库中单库表过多、业务边界清晰
分库分表单库容量/写入瓶颈高(分布式事务、跨片查询)最后手段

六、存储层与运维配套

  • 静态文件:图片、视频、附件一律走对象存储(S3/OSS/COS),不占本地磁盘,也不受 Web 节点增减影响;配合 CDN 回源。
  • 会话共享:PHP 的 session.save_handler = redis,配置 session.save_path = "tcp://redis-ip:6379?auth=密码"。
  • 日志集中:ELK 或 Loki 采集,单机 tail 在多节点时代毫无意义。
  • 监控告警:至少覆盖 CPU、内存、磁盘、网络、Nginx 5xx 比例、Redis 命中率、MySQL 慢查询数、连接数。
  • 压测:上线前用 wrk 或 ab 做基线压测,知道自己的单机上限是多少。
# 基线压测示例(在业务低峰、非生产机器上先做)
wrk -t4 -c200 -d60s --latency http://www.example.com/api/list
ab  -n 10000 -c 200 http://www.example.com/

# 压测时同时观察瓶颈在哪
top -b -n 1 | head -15          # CPU
iostat -x 1 3                   # 磁盘 await > 20ms 说明 IO 瓶颈
free -m                         # 内存与 swap
ss -s                           # 连接数
curl http://127.0.0.1/fpm-status  # PHP 进程池是否打满

七、实施清单:从单机走向分层

  1. 压测拿到单机基线 QPS,明确当前瓶颈是 CPU、内存、磁盘还是数据库。
  2. 先把 Web 与数据库分离到两台机器(投入最小、收益最大)。
  3. 接入 Redis,优先缓存会话和热点查询结果。
  4. 静态资源迁移到对象存储并接入 CDN,让 CDN 吃掉大部分流量。
  5. 应用层无状态化改造,为横向扩展做准备。
  6. Web 层加节点,前面放负载均衡(Nginx upstream 或云 SLB)。
  7. 数据库做主从读写分离,配置监控与慢查询告警。
  8. 引入消息队列,把可异步的逻辑剥离出去。
  9. 完善监控、日志、备份、灰度发布和回滚机制。
  10. 每次大促前重新压测,更新容量模型。

小结

高流量架构的核心不是“买多大的机器”,而是让每一层都能独立扩容、让绝大部分请求不落到最贵的那一层。顺序永远是:先量化 → 再缓存 → 再横向扩展 → 最后才考虑复杂的拆分。很多团队一上来就要搞微服务和分库分表,结果复杂度上去了,SQL 还是没加索引。

避坑清单:

  • 不要迷信配置升级,单机始终有上限,且存在单点风险。
  • 应用层必须无状态,否则加多少台机器都没用。
  • MySQL 的 max_connections 默认 151,连接池参数要和应用线程池对齐,不是越大越好。
  • Redis 一定要设 maxmemory 和淘汰策略,否则内存满会导致写失败甚至被 OOM。
  • 缓存 TTL 要加随机抖动,避免同一时刻大面积过期引发雪崩。
  • net.core.somaxconn 默认 128,高并发下不改会出现连接被丢弃,表现为“明明没到上限却有超时”。
  • 一切优化都要有压测基线,凭感觉调参等于赌博。

关键词回顾:高流量网站架构、服务器架构规划、分层部署、Nginx 调优、Redis 缓存、MySQL 读写分离、容量规划与压测。