单机、主从、集群,不是「初级中级高级」,是三种不同的账。 主从分走的是读,分不走写;写压满了,只能把数据切开。 选哪个不看想不想高级,看「写能不能分散」这一条。
上一篇看清了 8.0 的新东西,这一篇看机器怎么摆。 它是第 24 章项目实战的背景:那个网络商城为什么要做主从,答案在这一篇。 和第 12 章(索引与优化)的顺序不能反。 先优化 SQL,再谈加机器——反过来做,等于用钱买自己写错的 SQL。
一、单机:一台机器全包
单机就是一台机器上装着服务端,应用也在这台或者隔壁。 它的上限很清楚:这一台机器的 CPU、内存、磁盘。 最难受的地方是读写抢同一套资源。 一个大查询扫全表,能把热数据全挤出缓冲池,后面所有写都跟着变慢。 教材把这种部署叫单机模式,适合开发、测试、小站点。 它不是「低端」:很多内部系统一辈子就一台机器,够用就是好。 判断要不要升级,看一个信号:读请求把 CPU 打满了,而写还很闲。 这个信号说明该上主从了——不是因为慢,是因为读和写在互相挤。
二、主从:把读分出去
主从=一台主库负责写,若干从库跟着同步,读的请求分给从库。
| 角色 | 能做什么 | 怎么同步 |
|---|---|---|
| 主库(master) | 读写都行 | 把每次改动记进 binlog |
| 从库(slave) | 默认只读 | 拉主库的 binlog 重放一遍 |
「默认只读」这个设定很关键:在从库上执行写会报 ERROR 1290。
它是保护不是限制——从库的数据是主库的影子,自己改了就跟主库对不上。
主从解决的是读的压力,不是写的压力。
写永远只有主库那一台在扛,加十台从库也不会让写变快。
这一条是整篇的枢纽,记住它,后面所有取舍都顺。
三、复制延迟:主从的硬伤
从库不是实时镜像,它总比主库慢一点。慢的原因有三个。
| 原因 | 怎么回事 |
|---|---|
| 网络往返 | binlog 要从主库传到从库,跨机房就是几十毫秒 |
| 重放要时间 | 从库得把主库干过的活再干一遍 |
| 大事务 | 删十万行主库三秒,从库重放可能三十秒 |
所以「写完立刻读」是个陷阱:写入落在主库,读去了从库,读不到。 用户看到的就是「刚下的单不见了」。 解法只有一个方向:这类读强制走主库。 常见做法是在同一个事务里读,或者给这条查询加注释提示路由层。 具体延迟多大、影响多少请求,Demo 里有一张档位表。
四、分片:把写也分出去
读能靠加从库分担,写不能。写到顶了,只能把数据切开,一片一台机器。
切开就得先回答一个问题:一条记录该去哪个库?
最常见的答案是取模:uid % 库数。
NODES = ["db0", "db1", "db2", "db3"]
def route(uid):
return NODES[uid % len(NODES)]
bucket = {n: 0 for n in NODES}
for uid in range(1, 10001):
bucket[route(uid)] += 1
print("1 万个用户按 uid % 4 分到 4 个库:")
for n in NODES:
print(" %s %5d 人 占 %.1f%%" % (n, bucket[n], bucket[n] / 100.0))
print("再试按 uid 的尾号分:")
bucket2 = {n: 0 for n in NODES}
for uid in range(1, 10001):
bucket2[NODES[uid % 10 % len(NODES)]] += 1
for n in NODES:
print(" %s %5d 人" % (n, bucket2[n]))
uid % 4 的结果是均匀的,四个库各 2500 人。
按「尾号」分就出问题了:0 到 9 十个尾号映射到 4 个库,必然有两个库多分一份。
这就是分片键的全部难点:选一个「取值均匀、且查询总带着它」的列。
不均匀会让某个库先爆;查询不带它,就得广播到所有库再拼结果,慢得没法用。
还有一条常被忽略:分片之后,跨片的事务和跨片的 join 都做不了了。
这两样一丢,很多业务逻辑得改成「最终一致」,工作量不在数据库这边。
五、怎么选:三档账
三档总量、三种方案各要几台,Demo 里算过一遍,这里只留结论。
| 看到的信号 | 该做什么 |
|---|---|
| 读多写少,单机 CPU 打满 | 上主从,把读分出去 |
| 写已经压满主库 | 加从库没用,只能分片 |
| 查询跨很多表、要强一致 | 别急着分片,先优化 SQL 和索引 |
顺序永远是:优化 SQL 和索引、加缓存、主从、分片。 每一步都比下一步便宜一个数量级。 最容易犯的错是从最后一步开始做。 上来就分片,等于把「一条 SQL 该建的索引」这个简单问题, 做成了「跨片查询怎么拼」这个复杂问题。
六、动手 Demo:三档总量,单机/主从/集群各要几台
这一课用一个容量模型算账,不是算命。 假设单机能扛 8000 QPS 的读、2500 QPS 的写,业务里读占八成。 三档总量:五千、五万、五十万。看三种方案各要几台机器。
READ, WRITE = 8000, 2500 # 单机读、写能力
def plan(total):
reads = int(total * 0.8)
writes = total - reads
if writes <= WRITE:
master = 1 + -(-reads // READ) # 主 + 若干从
s2 = "%d 台" % master
else:
s2 = "写爆,加从库没用"
shards = -(-writes // WRITE)
s3 = "%d 台(%d 片)" % (shards * 2, shards)
return reads, writes, s2, s3
for total in (5000, 50000, 500000):
r, w, s2, s3 = plan(total)
print("%7d 读 %6d 写 %6d 主从 %-14s 集群 %s"
% (total, r, w, s2, s3))
实跑输出:
--------------------------------------------------------------------
第六课:单机、主从、集群,分别能扛多少
--------------------------------------------------------------------
**教材把应用环境分三种:单机、主从(读写分离)、集群。**
**它们不是「高级/低级」的关系,是三种不同的账。**
**这一课用一个容量模型算清楚:三档总量下,三种方案各要几台机器。**
一、先定几个数
--------------------------------------------------------------------
**假设单机能扛 8000 QPS 的读、2500 QPS 的写(这是台不错的机器,真实值要看 SQL 复杂度)。**
**业务里写占两成、读占八成。下面按三档总量各算一遍。**
二、三档总量,三种方案各要几台
--------------------------------------------------------------------
总量 读/写 单机负载 主从 集群(每片一主一从)
-------------------------------------------------------------------------------------
5000 QPS 4000/1000 读 50% 写 40% 2 台 2 台(1 片)
50000 QPS 40000/10000 读 500% 写 400% 写爆,加从库没用 8 台(4 片)
500000 QPS 400000/100000 读 5000% 写 4000% 写爆,加从库没用 80 台(40 片)
**竖着读这张表,能看出三件事:**
**第一,五千这档,单机一台就够,连主从都不必要——加机器是浪费。**
**第二,五万这档,写已经超过单机能扛的 2500 QPS,主从直接出局。**
**因为写不能靠加从库分担:从库是只读的,写全压在主库那一台上。**
**第三,要到五十万这档,集群才成为唯一选择,代价是 80 台机器和一堆运维活。**
**所以顺序永远是:先把 SQL 和索引优化到不能再优化,再看主从,最后才谈分片。**
**五千这档就上集群,是把简单问题做成了复杂问题。**
三、主从真正换来的东西
--------------------------------------------------------------------
**主从不是为了更快,是为了【读写互不干扰】。**
**单机上读写共用一套缓冲池,一个大查询扫全表,能把热数据全挤出内存,后面所有写都变慢。**
**拆开之后,从库上的慢查询拖不动主库上的写。**
四、主从的代价:延迟
--------------------------------------------------------------------
**主从也不是免费午餐,代价是【复制延迟】——从库的数据比主库晚一点。**
延迟 刚下单的人去查订单 该怎么办
--------------------------------------------------------
0.1 秒 九成能看到 一般可接受
1 秒 约一半看不到 下单后强制读主库
5 秒 基本看不到 必须读主库
30 秒 肯定看不到 主从已经出问题了,查延迟原因
**第二列是我按「用户 1 秒内会刷新一次」估的,不是实测。**
**但结论是真的:只要走主从,就必须定一条规矩。**
**「写完立刻要读」的那些查询,得钉死在主库上。**
**MySQL 里这叫「强制走主库」,常见做法是在同一个事务里读,或者用 hint 指定。**
五、集群不是「主从加几台」
--------------------------------------------------------------------
维度 主从 集群/分片
--------------------------------------------------------------
解决什么 读的压力 读写都能扩
数据在哪 每台都有一份全量 每台只有一片
扩容 加从库,加完就有全量 要搬数据,最麻烦的一步
跨片查询 不存在这问题 要么应用层拼,要么放弃
故障切换 主挂了提升一个从 那一片的主挂了,那一片不可用
运维复杂度 低 高一个量级
**最后两行才是关键:集群换来的是容量,付出的是「跨片查询做不了」和「运维复杂度上一个量级」。**
先看五千那一档:单机负载才 50%,一主一从两台就够,集群也就两台。 台数一样,那当然选简单的——这就是「五千这档上集群是把简单问题做复杂了」。 五万那一档是关键转折。 写请求一万,单机只能扛 2500,主从方案直接出局——因为从库是只读的,写不会因为加了从库而分散。 所以那张表里「写爆,加从库没用」出现了两次,不是偷懒,是想让你看见两次。 五十万那档更明显:集群要 80 台机器,代价是运维复杂一个量级。 延迟那张档位表里的第二列是我按「用户一秒内会刷新一次」估的,不是实测。 但结论是真的:只要走主从,「写完立刻要读」的查询就得钉死在主库上。 最后那张集群对照表,最后两行才是关键。 集群换来的是容量,付出的是「跨片查询做不了」和「运维复杂一个量级」。
30 秒实验
- 把 Demo 里的写比例从 20% 改成 50%,再跑一遍。 五万那一档的写变成两万五,是单机能力的十倍,主从一样出局。 结论没变,但分片从 4 片变成 10 片——代价涨得更快。
- 把单机写能力从 2500 改成 5000(换块 SSD、加内存),五万那档还得出局吗? 两万五除以五千还是 5,照样出局。 单机能力提一倍,往往只是把分片的临界点往后推一档。
- 想一个你熟悉的系统,估它的读写比例。 新闻、朋友圈这类读远多于写;下单、打卡这类写占比高得多。 比例决定了该先解决哪一头。
常见误区
- 「上了主从,写就快了。」 不会。写只在主库发生,从库是只读的。 主从改善的是读的吞吐,以及读写互不干扰。
- 「分片以后查询一定更快。」 只有带分片键的查询更快——因为它只落在一个片上,表小了。 不带分片键的查询要广播到所有片再合并,比不分片更慢。
- 「等慢了再优化 SQL 也来得及。」 慢 SQL 在每一片上都是慢的,分片之后还会被放大——原来扫一张表,现在扫 N 张表。
自测三问(点开对答案)
1. 主从架构里,写操作发生在哪一台? —— **只有主库。** **从库默认只读,在从库上写会报 `ERROR 1290`。** 2. 复制延迟 5 秒,用户下单后立刻刷新订单页,会看到什么? —— **大概率看不到自己刚下的单**——因为读走了从库,而从库还差 5 秒。**这类查询得强制走主库。** 3. 十个尾号分到四个库,为什么分不均? —— **因为 10 不是 4 的倍数。** **0、4、8 三个尾号落在一个库,2、6 两个落在另一个库,自然是 3000 对 2000。**下一篇是本章唯一标两星的一篇:装 MySQL 时向导问的那一路问题,
到底在定什么,以及那个 my.ini 里最该改的是哪个参数。
- ☐ (本篇完,下一篇 2.4 MySQL 服务器的安装和配置)
相关笔记
- MySQL 8.0 的新特性
- MySQL 服务器的安装和配置
- 13 课 服务器工作原理:一台服务器到底在干什么
- 🛢️ MySQL从入门到精通 · 索引