网站刚上线时,一台 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 SSD | 5GB | 企业官网、后台管理 |
| 中型(日 PV 5 万 - 50 万) | 8 核 32G + 500GB - 1TB SSD | 20GB | 电商、内容站 |
| 大型(日 PV 50 万 - 500 万) | 16 核 64G + 1TB - 2TB NVMe | 45GB | 多商户平台、交易系统 |
| 超大型 | 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 的团队;自建则在成本与可控性上更灵活,适合有运维能力且规模较大的业务。
五、从共享部署迁移到独立数据库服务器的步骤
- 评估现状:统计数据总量、QPS、连接数、慢查询分布,确定目标规格。
- 采购目标机器,先在同可用区部署好 MySQL(版本与现有实例一致),并配置内网安全组只放行 Web 服务器网段。
- 把新实例配置为现库的从库:用
mysqldump --single-transaction --master-data=2或 XtraBackup 建立基线,再用 CHANGE MASTER 挂上去。 - 观察 Seconds_Behind_Master(建议用 pt-heartbeat)追平,确认数据一致。
- 在测试环境先把应用配置指向新库跑一遍回归;同时把应用侧连接池参数按新规格重新计算。
- 低峰期灰度切换:先切一台 Web 服务器,观察 15 分钟慢查询与错误日志。
- 全量切换并记录回滚点:保留旧库只读运行至少 24 小时,随时可以切回。
- 确认稳定后再下线旧实例上的 MySQL,并把原机器的内存/IO 留给 Web 层。
- 补上配套:Redis 是否也要独立、是否需要第二台从库承担备份。
- 加监控告警:连接数、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 的连接数、命中率、复制延迟和磁盘水位,才是长期稳定的关键。
企业QQ咨询




