高并发网站为什么需要独立数据库服务器?架构设计与部署建议

网站刚上线时,一台 4 核 8G 的云服务器塞进去 Nginx、PHP-FPM、Redis、MySQL,跑得很顺畅,成本还低。流量起来之后问题开始呈现:GC 频繁、MySQL 莫名其妙被 OOM Kill、磁盘 IO 打满导致接口响应从 50 毫秒变成 3 秒。此时最有效的一次改造往往只有一句话:把数据库拆出去,给它一台独立的机器。本文讨论什么时候必须拆、拆完之后收益在哪里、以及具体的部署与迁移建议。

一、共享部署到底会带来什么问题

把 Web 与数据库放同一台机器,本质上是让两个资源画像完全不同的服务抢同一份资源。

1.1 内存:最致命的竞争

MySQL 的 innodb_buffer_pool_size 建议是物理内存的 60%-75%,Redis 的 maxmemory 建议是 45%-65%,PHP-FPM 每个 worker 常驻几十 MB,Java 应用一个堆就可能吃掉好几个 GB。这些需求是叠加的,谁都不会主动让。当总量超过物理内存时,Linux 内核按 OOM Killer 的评分杀进程——而 MySQL 因为占用内存大、且持有大量共享内存页,往往被评为"最该杀的那个"。一旦 mysqld 被杀,不但数据页丢失(内存中未刷盘的脏页),恢复还需要跑崩溃恢复流程,代价极高。

# 查看是否发生过 OOM
dmesg -T | grep -iE 'oom_kill|Out of memory|Killed process'
journalctl -k | grep -i oom

# 查看当前内存与可用量(关注 available,而不是 free)
free -h
cat /proc/meminfo | grep -E 'MemAvailable|CommitLimit|Committed_AS'

# swap 一旦被使用,数据库延迟会立刻恶化
vmstat 1 5          # 关注 si / so 两列,持续大于 0 说明已经在换页
sar -W 1 5          # pswpin/s 与 pswpout/s

1.2 磁盘 IO:第二致命

Web 层有大量的日志写入(access.log、error.log)、临时文件上传、静态资源读取;数据库有 16KB 随机读写与 redo/fsync。两者混在一块盘上时,日志的连续追加会把 IO 队列塞满,让数据库的随机读被拖延。表现就是:接口偶发性慢,但 CPU 和内存看起来都正常。

# 观察 IO 等待占比
iostat -xm 2 5        # %util 接近 100、await 升高即瓶颈
iotop -o              # 看谁在写盘
pidstat -d 1 5        # 按进程统计 IO

1.3 三个容易被忽视的问题

  • 故障域耦合:一次恶意爬虫把 PHP 进程跑满,数据库跟着没有 CPU 可用;反过来一次慢查询拖垮磁盘,静态资源也加载不出来。任何一个组件挂掉,整站全挂。
  • 无法独立扩容:业务高峰时 Web 层需要加 CPU 和横向扩容,数据库需要加内存和存储。混布时只能"整机升配",其中一半升级是完全浪费的。
  • 内核与安全策略冲突:数据库要求 vm.swappiness 低、vm.overcommit_memory=1、关闭 THP;Web 应用(尤其 Java)对 THP 的态度相反。同时 Nginx 需要开放 80/443、数据库只能内网访问,混布会让防火墙规则和安全边界变得模糊。

二、什么时候该拆:给出可衡量的阈值

不要用感觉判断,下面是可以写进技术规范的具体阈值,满足任意两条就该考虑独立部署:

指标可继续共享部署建议拆分
日均 PV< 5 万> 10 万 - 50 万
峰值 QPS(数据库侧)< 200> 500
数据文件总量< 10 GB> 20 GB,或索引已超过内存
MySQL 常驻内存需求< 2 GB> 4 GB
磁盘 await(SSD)< 5 ms持续 > 10 ms
swap 使用从不使用出现过 si/so 非零
主从复制延迟常态 0高峰长期 > 5 秒

还有一个业务角度的判断:如果这台机器宕机造成的损失(订单中断、数据丢失)已经超过了再买一台机器的成本,那么拆分就是纯赚的成本。

三、拆分后的典型架构

最小可用形态建议是三个角色分离:

  • Web / 应用层:无状态,可横向扩展,前面挂负载均衡(Nginx / SLB),不存任何本地状态。
  • 缓存层:Redis 独立部署(至少主从 + Sentinel,或用 Redis Cluster),承接热点读与会话。
  • 数据库层:一主一从起手,主库承担写,从库承担读与备份;DDL、报表、慢查询都放从库。
用户 → CDN → 负载均衡(SLB/Nginx)
                ├→ Web-1 (无状态)
                ├→ Web-2
                └→ Web-3
                     │
        ┌────────────┼──────────────┐
        ↓            ↓              ↓
    Redis主从      MySQL主(写)   MySQL从(读/备份)
        │              └── binlog 复制 ──┘
        ↓
   独立ECS + 内网万兆互联

网络是关键设计点:Web 与数据库必须同机房、同内网。跨机房会把每次 SQL 的网络往返从 0.2 毫秒拉到几毫秒,一个页面 30 次查询就是几十毫秒的固定开销;更重要的是跨机房网络抖动会直接转换为数据库连接超时。若必须做异地容灾,主库与从库的 RTT 建议在 2 毫秒以内(同城双活),并不要把异地从库作为强一致读的来源。

# 测量应用服务器到数据库的网络质量
ping -c 100 <数据库内网IP> | tail -3
iperf3 -c <数据库内网IP> -t 30
mtr --report <数据库内网IP>

# 测量单次 SQL 往返开销(排除查询本身的执行时间)
mysql -h  -uroot -p -e "SELECT BENCHMARK(100000, (SELECT 1));"

四、规格怎么选:给几档参考配置

规模典型配置innodb_buffer_pool_size适用业务
小型(日 PV < 5 万)4 核 8G + 200GB SSD5GB企业官网、后台管理
中型(日 PV 5 万 - 50 万)8 核 32G + 500GB - 1TB SSD20GB电商、内容站
大型(日 PV 50 万 - 500 万)16 核 64G + 1TB - 2TB NVMe45GB多商户平台、交易系统
超大型32 核 128G 以上 + NVMe 或分库分表90GB 以上需要读写分离与垂直拆分

这里要把两个 innodb_buffer_pool_size 的错位提醒一遍:它要能装下"热点数据集",而不是"数据总量"。用下面这条查询先算出数据总量,再乘以 0.3-1.0 的热点系数:

SELECT ROUND(SUM(data_length+index_length)/1024/1024/1024, 2) AS total_gb
FROM information_schema.tables
WHERE table_schema NOT IN ('mysql','sys','performance_schema','information_schema');

# 上线后用命中率验证规格是否够(OLTP 应 > 99%)
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';

成本方面,独立数据库服务器通常意味着至少 2 台(主 + 备),整体支出会增加;但换来的是可预期的故障隔离和独立扩缩容能力。云数据库(RDS)与自建的差异也很直接:RDS 省掉了主从搭建、备份、故障切换、补丁升级这些运维负担,通常 视机房与实例规格而定 会比同规格自建贵一部分,适合没有专职 DBA 的团队;自建则在成本与可控性上更灵活,适合有运维能力且规模较大的业务。

五、从共享部署迁移到独立数据库服务器的步骤

  1. 评估现状:统计数据总量、QPS、连接数、慢查询分布,确定目标规格。
  2. 采购目标机器,先在同可用区部署好 MySQL(版本与现有实例一致),并配置内网安全组只放行 Web 服务器网段。
  3. 把新实例配置为现库的从库:用 mysqldump --single-transaction --master-data=2 或 XtraBackup 建立基线,再用 CHANGE MASTER 挂上去。
  4. 观察 Seconds_Behind_Master(建议用 pt-heartbeat)追平,确认数据一致。
  5. 在测试环境先把应用配置指向新库跑一遍回归;同时把应用侧连接池参数按新规格重新计算。
  6. 低峰期灰度切换:先切一台 Web 服务器,观察 15 分钟慢查询与错误日志。
  7. 全量切换并记录回滚点:保留旧库只读运行至少 24 小时,随时可以切回。
  8. 确认稳定后再下线旧实例上的 MySQL,并把原机器的内存/IO 留给 Web 层。
  9. 补上配套:Redis 是否也要独立、是否需要第二台从库承担备份。
  10. 加监控告警:连接数、Buffer Pool 命中率、慢查询条数、磁盘使用率、复制延迟,以及应用到数据库的平均往返延迟。
# 第 3 步:建立从库的关键命令(示意)
mysqldump -h 旧库IP -uroot -p --single-transaction --master-data=2 \
  --routines --triggers --events -A | gzip > /tmp/full.sql.gz

zcat /tmp/full.sql.gz | grep -i "CHANGE MASTER"     # 取出 binlog 文件名与位置

zcat /tmp/full.sql.gz | mysql -h 新库IP -uroot -p    # 这一步会重放全量数据

-- 导入完成后在新库上挂主从复制
CHANGE MASTER TO MASTER_HOST='旧库IP', MASTER_USER='repl', MASTER_PASSWORD='密码',
  MASTER_LOG_FILE='mysql-bin.0000xx', MASTER_LOG_POS=数字;
START SLAVE;
SHOW SLAVE STATUS\G

六、上线前的压测与检查

# sysbench OLTP 混合读写压测
sysbench oltp_read_write --mysql-host=10.0.0.10 --mysql-user=root --mysql-password='密码' \
  --mysql-db=sbtest --tables=16 --table_size=1000000 --threads=64 --time=120 \
  prepare
sysbench oltp_read_write --mysql-host=10.0.0.10 --mysql-user=root --mysql-password='密码' \
  --mysql-db=sbtest --tables=16 --table_size=1000000 --threads=64 --time=120 run
sysbench oltp_read_write ... cleanup

压测结果要看的不是单纯的 QPS,而是 95th percentile latency:参考线可以定为 P95 小于 10 毫秒(简单 OLTP),超过 30 毫秒说明 IO 或连接数已经有瓶颈。同时把 Threads_running 与 iostat 的 await 一起记录下来。

小结

高并发网站需要独立数据库服务器的根本原因是:数据库的资源画像(大内存、随机 IO、fsync 敏感、策略独立)与 Web 层完全不同,混布的结果是互相拖累且无法通过单点升级解决。判断要不要拆可以用量化阈值:日均 PV 超过 10 万、数据库峰值 QPS 超过 500、数据量超过 20GB、或者服务器已经出现过 swap 与 OOM Kill。拆分后按"Web 无状态横向扩展 + Redis 独立 + MySQL 一主一从"的最小架构落地,并用 sysbench 与 hit rate 指标验证。避坑清单:不要让 Web 与数据库跨可用区部署;不要把数据库端口暴露到公网;不要跨过容量规划直接"照抄大厂配置";不要在迁移时一次性切全量流量;不要拆完就不管——监控 MySQL 的连接数、命中率、复制延迟和磁盘水位,才是长期稳定的关键。