这一章在做什么
数据多到一台机器存不下、写不过来,就要把它切成分片,分给多台机器,每条记录只属于一个分片。这一章回答四个问题:按什么切(键的范围,还是键的哈希);切得不匀、出现热点怎么办;加减机器时怎样少搬数据;请求怎么找到该去的机器。最后一个难点是二级索引:索引跟着数据走,读就要问遍所有分片;索引自己另外分片,写就要改好几处。作者的态度很明确:分片是重型方案,一台机器扛得住就别分。
读之前:先预测
第 6 章把同一份数据复制到多台机器上,解决的是容错和读的扩展;这一章换一个方向,把数据切开,每台机器只管一部分,解决的是数据量和写入量。两者通常一起用,但这一章先把复制放到一边。后面几章都会回到这里:第 8 章的分布式事务,起因是一次写要跨好几个分片;第 10 章的共识,是协调服务记录「分片在哪台机器」的底子;第 11 章的批处理按键把数据分给各个任务,用的也是哈希分片。
数据大到一台机器装不下时,怎样决定每条记录放在哪台机器上,才能同时做到:分得匀(没有哪台被压垮),查得快(知道去哪找),加减机器时搬得少?这三件事能不能兼得,代价落在哪里?
- 按键的范围分片和按键的哈希分片,各自放弃了什么?§3.1§3.2
- 为什么预先切出比机器多得多的分片,就能让加机器时只搬一小部分数据?这个数一开始定错了怎么办?§3.2
- 哈希已经把键分匀了,为什么还会有热点?§3.3
- 二级索引为什么不能顺着主键的分片走?本地索引和全局索引,各把代价放在读上还是写上?§5.1§5.2
这一章的骨架
全章按「切、搬、找」走:§1、§2 先问值不值得切,按租户切有什么好处;§3 是重心,讲按什么切、切不匀怎么办、加减机器时怎么搬;§4 讲请求怎么找到机器;§5 把二级索引放进来。容易乱的是 §3.2:一节里连讲四种做法(取模、固定分片数、哈希范围、一致性哈希),后一种在补前一种的短处。读的时候给每一种记下「加一台机器时搬的是什么」。
边读边填:每行一个问题,读到答案就写一两句。只存在你这台设备的浏览器里,别人看不到。
| 要找的 | 带着这个问题读,找到了就写下来 |
|---|---|
| 分片的代价 | 书里列了分片带来的哪几种麻烦?为什么建议一台机器够用就别分?§1 |
| 按租户分 | 每个租户一个分片,好处里哪一条和故障有关?这条路最大的前提是什么?§2 |
| 时间当键 | 用时间戳做键、按范围分片,为什么写入会挤在一个分片?怎么改,改完付出什么?§3.1 |
| 固定分片数 | 加一台机器时,什么变、什么不变?分片数一开始定错了要付什么代价?§3.2 |
| 拆热键 | 给热键加随机数之后,写分散了,读要多做什么?还要额外记住什么?§3.3 |
| 路由三问 | 不管路由放在哪一层,都要回答的三个问题是什么?协调服务替谁回答?§4 |
| 全局索引 | 查一个条件时它快在哪?写入和多个条件的查询难在哪?§5.2 |
读完了?对照我找到的
- 分片的代价:要选分区键,不知道分区键的查询得搜所有分片;方案难改;关系数据的二级索引和 join 难做;跨分片的写要分布式事务。
- 按租户分:单元化架构把一批租户的服务和存储打包,故障不出单元;前提是每个租户都小到一台机器放得下。
- 时间当键:新数据的键总是最大,全落在最后一段;键前面加上实体的 ID,写入就摊开了,但跨实体查一段时间要分别查。
- 固定分片数:分片数和键到分片的对应不变,只改分片放在哪台机器;定少了机器数受限,定错了要做代价很高的重新分片,把每个分片拆开重写,有的系统还不能边写边改。
- 拆热键:读要把拆出来的所有键都读一遍再合并;还要记住哪些键被拆了,以及怎样把普通键转成热键。
- 路由三问:谁决定分片放哪台(协调者怎么容错、怎么防脑裂);路由的人怎么得知分配变了;搬分片的切换期间请求怎么办。协调服务(ZooKeeper、etcd)保存权威的分配表并通知路由层。
- 全局索引:一个条件只读一个索引分片;但取记录仍要访问多个分片,多条件要跨网络求交集,写一条记录可能要改好几个索引分片。
分节地图
全章约 7900 词(不算图注),是全书较短的一章,建议分三次读。第二次是重心,§3.2 一节就占了五分之一;第三次的两个话题彼此独立,可以分开读。
- 第一次坐下§0–§3.1 · 约 2700 词 · 为什么分、按租户分、按键的范围分
- A distributed database typically distributes…约 500 词开篇复制和分片有什么不同、怎么组合;方框里是分片在各个系统里的不同叫法。
- Pros and Cons of Sharding约 500 词分片的利与弊为什么要分片,为什么作者劝你能不分就不分。分区键在这里第一次出场。
- Sharding for Multitenancy约 600 词为多租户分片每个客户一个分片的七条好处和三个难处。列表长,挑你关心的细读。
- Sharding of Key-Value Data约 300 词键值数据的分片目标是分得匀、能再平衡;倾斜、热点、热键三个词在这里定义。
- Sharding by Key Range约 800 词按键的范围分片边界怎么定、范围查询的好处、时间戳当键的热点,以及分片怎么拆。
- 第二次坐下§3.2–§3.4 · 约 2600 词 · 按哈希分、加机器怎么搬、热点和再平衡
- Sharding by Hash of Key约 1700 词按键的哈希分片全章最长:取模、固定分片数、哈希范围、一致性哈希,一种补一种的短处。
- Skewed Workloads and Relieving Hot Spots约 500 词负载倾斜与缓解热点哈希分匀了键,为什么还会有热点;拆热键的办法和它的代价。
- Operations: Automatic Versus Manual Rebalancing约 400 词运维:自动还是手动再平衡自动再平衡省事,但和自动故障检测叠在一起时可能越帮越忙。
- 第三次坐下§4–§6 · 约 2600 词 · 请求路由和二级索引,加小结
- Request Routing约 800 词请求路由三种把请求送到对的机器的办法,以及谁来维护「分片在哪」这张表。
- Sharding and Secondary Indexes约 200 词分片与二级索引二级索引为什么和分片对不上。
- Local Secondary Indexes约 500 词本地二级索引每个分片只管自己的记录:写简单,读要问遍所有分片。
- Global Secondary Indexes约 500 词全局二级索引索引按被索引的值另外分片:查一个条件快,写和多条件查询变难。
- Summary约 500 词小结全章回顾。先自己回忆一遍再读它。
先用中文过一遍
按键的范围切,还是按键的哈希切
分片要做到两件事:每条记录只属于一个分片;各个分片分到的数据和请求大致一样多。决定一条记录进哪个分片的那个键,叫分区键(partition key)。
第一种切法按键的范围:把键排好序,切成几段连续的区间,每个分片管一段。边界不必等距,要跟着数据的实际分布定,可以由管理员手工指定,也可以让数据库在分片太大时自动一分为二。好处是相邻的键在同一个分片里,按范围查只碰一两个分片。麻烦在写入的键总落在同一头的时候:拿下单时间做键,「现在」那一段所在的分片接下全部新写入,其余分片闲着。
第二种切法先对键算哈希,再按哈希值分。好的哈希函数把挨在一起的键打散到整个取值空间,写入就匀了;代价是键的顺序没了,按键查一个范围要问遍所有分片。折中的办法是键由两部分组成,只拿第一部分当分区键:同一个分区键下的记录还在一个分片里,按第二部分排好序,照样能做范围查询。
键分匀了,负载不一定匀。一个特别热的键(一场热门演出开票时的库存)不管用哪种切法,都只落在一个分片上。这类倾斜要靠别的手段:把热键单独放一个分片,或者在应用里把一个键拆成很多个。
加一台机器,要搬多少数据
最直接的想法是 hash(key) % N,N 是机器数。问题出在 N 变的时候:除数一换,几乎每个键算出来的余数都变了。从 4 台加到 5 台,只有哈希值除以 20 余 0 到 3 的键留在原地,其余全要搬家,而新机器其实只需要接手五分之一。
解决办法是把一层映射拆成两层:键 → 分片,分片 → 机器。一开始就切出比机器多得多的分片,比如 240 个分片放在 6 台机器上,每台 40 个。键进哪个分片由 hash % 240 决定,永远不变;加机器时,只是把一些分片整个搬到新机器,再改一下分配表。代价是分片数要一开始猜对:机器数不能超过分片数;数据涨了几十倍,每个分片也跟着胖了几十倍。
另一条路是让分片数跟着数据走:每个分片管一段连续的哈希值,太大了就拆。有的系统再进一步,让每台机器在哈希值空间里取好几个随机边界,新机器加入时从别人的范围里各切走一小段。这一类「分片数变了也只搬最少的键」的算法,统称一致性哈希(consistent hashing);这里的「一致」和副本一致性没有关系。
请求怎么找到那台机器
知道了键属于哪个分片、分片在哪台机器上,请求还得真的送过去。有三个地方可以做这件事:让任意一台机器接请求,不归它管就转发;在前面放一层专门的路由;让客户端自己记着分配表,直接连对的机器。不管放在哪,都绕不开同一个难题:分配表会变(加机器、搬分片、机器宕掉),谁来决定它、改了以后怎么通知所有做路由的人、搬到一半时发往旧机器的请求怎么办。很多系统把这张表交给一个靠共识算法保证不出错的协调服务;也有系统让节点之间互相传话,把变化慢慢散布出去,代价是可能出现两边对同一个分片看法不一的情况。
二级索引:跟着数据分,还是自己另外分
订单按订单号分片,按订单号查很快:算一下就知道去哪个分片。可是「查杭州的所有订单」用的是城市,城市和订单号没有关系,一个城市的订单散在所有分片里。
一种做法是每个分片只给自己的订单建索引(本地索引)。写一条订单只动一个分片;查杭州就要把查询发给所有分片,再把结果合并,分片越多,一次查询要等的分片越多。另一种做法是把索引本身按城市分片(全局索引):杭州的条目全在同一个索引分片上,查一个条件只问一处;但写一条订单要同时改数据所在的分片和索引所在的分片,两处怎么保持一致就成了问题。
这一章点名的系统
| 英文 | 中文 | 一句话 |
|---|---|---|
| shard / partition | 分片(分区) | 数据切出来的一块,每条记录只属于一个分片(§0)。 |
| partition key | 分区键 | 决定一条记录进哪个分片的那个键或那一列(§1)。 |
| multitenancy | 多租户 | 一套系统服务很多客户,每个客户的数据自成一体(§2)。 |
| cell-based architecture | 单元化架构 | 把一批租户的服务和存储打包成能独立运行的单元,故障不出单元(§2)。 |
| rebalancing | 再平衡 | 加减机器后,把数据和负载重新摊匀(§3)。 |
| skew / hot spot | 倾斜 / 热点 | 有的分片数据或请求远多于别的分片;负载特别高的那个叫热点(§3)。 |
| hot key | 热键 | 单个请求特别多的键(§3)。 |
| key-range sharding | 按键范围分片 | 每个分片管一段连续的键,分片里的键有序(§3.1)。 |
| pre-splitting | 预拆分 | 空库时就按预估的键分布先切好分片(§3.1)。 |
| hash sharding | 按哈希分片 | 先对键算哈希,再按哈希值分;可以按取模、按哈希值的范围等方式分(§3.2)。 |
| consistent hashing | 一致性哈希 | 分片数变化时尽量少搬键的映射算法;和副本一致性无关(§3.2)。 |
| request routing | 请求路由 | 把对某个键的读写送到负责它的机器(§4)。 |
| coordination service | 协调服务 | ZooKeeper、etcd 一类,用共识算法保存分片分配表(§4)。 |
| local secondary index | 本地二级索引 | 每个分片只索引自己的记录;又叫按文档分区的索引(§5.1)。 |
| global secondary index | 全局二级索引 | 索引按被索引的值另外分片;又叫按词分区的索引(§5.2)。 |
动手:模拟器
先猜:6000 个键、4 台机器,用 hash % 机器数,加到第 5 台要搬多少个键?换成固定 24 个分片呢?再想想:按范围分片、每台机器只取 1 段边界时,新机器能分到公平的一份吗?猜好了再点。
加一台机器要搬多少数据
6000 个键,一开始 4 台机器,每台一行,条的长度是它存的键数。点「加一台机器」或「减一台机器」:绿色是这一步非搬不可、搬进来的键(搬去新机器,或从撤掉的机器搬走),红色是本来可以不动、却换了机器的键。按键范围和按哈希值范围分片,加机器时都是从现有的范围里切一段给新机器,搬的量一样;这里用哈希值的范围演示。
- 选「hash % 机器数」,从 4 台加到 5 台,对比「这次搬了几个键」和「理想最少要搬」;再加到 6 台。然后减回 5 台:白搬的又有多少?
- 换成「固定数量的分片」,24 个分片,从 4 台加到 5 台:搬走的是哪几个分片?新机器分到的为什么比「理想」略少?改成 8 个分片,一路加到 9 台会怎样?
- 换成「按范围分片」,每台 1 段,连加几台,看新机器各分到多少、「最多 / 最少一台」差多少;改成每台 16 段、256 段再试(书里说的两个默认值)。为什么段多了就匀了?
词与短语
词条按在书里出现的顺序排,按节分组;读一节之前先把这一节的过一遍,读的时候就不用停。斜体小字是原句里连续的几个词,在电子书里搜它就能找到那一句。标着「每个字都认识」的,是字面意思会把你带偏的。行话(rebalancing、hot spot 这类)不在这里,在上面的术语表里。
§0 A distributed database typically distributes…
§1 Pros and Cons of Sharding
§2 Sharding for Multitenancy
§3 Sharding of Key-Value Data
§3.1 Sharding by Key Range
§3.2 Sharding by Hash of Key
§3.3 Skewed Workloads and Relieving Hot Spots
§3.4 Operations: Automatic Versus Manual Rebalancing
§4 Request Routing
§5 Sharding and Secondary Indexes
§5.1 Local Secondary Indexes
§5.2 Global Secondary Indexes
§6 Summary
下面是读完再看的:你的预测对不对、几道换成你来选的判断题、值得细读的句子和理解检查。先看了,读书时就只是在找答案。
读完:回到你的预测
按 §3.2 的道理算,答案是第四项。书里只说「大部分键都要搬」,具体比例可以自己算:一个键留在原地,要求它的哈希值除以 4 和除以 5 的余数相同,也就是除以 20 的余数落在 0 到 3,哈希均匀时这只占五分之一,所以约五分之四的键要换机器。新机器其实只需要接手五分之一,多出来的搬动全是老机器之间互相倒腾。「已有的键不用搬」也不成立:读的时候用的是新公式,旧键不搬就找不到了。想一想:如果从 4 台直接加到 8 台,要搬的比例是多少?这一次有没有白搬的键?
答案是第三项(§5.1)。本地索引只知道本分片里有哪些订单,而订单是按订单号分的,同一个骑手的订单散在所有分片里,所以要把查询发给全部 16 个分片再合并结果。书里说这种读法代价高,还容易放大尾延迟(第 2 章讲过:等的分片越多,碰上一个慢的概率越大)。只要一部分结果时可以只问一个分片,但「今天的全部订单」不行。想一想:如果骑手 ID 上建的是按骑手 ID 分片的全局索引,这次查询要问几个分片?写一条新订单又要改几处?
判断题:换成你来选
场景都是本站编的。先选,再看解释;解释里写了对应的节号,拿不准就回原书看那一节。
场景:一家网约车公司把行程轨迹点存进按键范围分片的数据库,键是上报时间。司机端每 3 秒报一次位置,监控显示永远只有一台机器的写入打满,其余几台很闲。最常见的查询是「某一趟行程的全部轨迹点」。
按时间排序时,新数据的键总比已有的大,全落在最后一段;拆开以后新写入照样只进后一半,加机器也一样。把行程 ID 放在前面,同时进行中的大量行程把写入摊到各个分片,同一趟行程的点仍然按时间挨在一起,查一趟行程还是一次范围扫描。对整个键算哈希也能摊匀写入,但一趟行程的点会被打散到所有分片,最常见的查询反而变慢了。
场景:一个团队要上线一个用固定分片数的分布式数据库。现在 3 台机器,预计两三年里最多涨到 40 台左右,数据量会涨几十倍。分片数建库时就要定,以后改要重新分片,很难不停机。
机器数不能超过分片数,3 个直接堵死了扩容。40 个虽然够数,但 3 台时只能分成 14、13、13,到 40 台时每台只有一个分片,再要调整就无处可调。480 这样约数多的数,在 3、4、5、6、8、10、12、…、40 台时都能分得匀,每个分片也不会太小。一百万个分片每个都太小,书里说分片太小,管理开销就太大。
场景:一个票务系统按场次 ID 哈希分片。一场热门演唱会开票的那一分钟,这一场的库存计数每秒被扣几万次,它所在的那台机器 CPU 打满,其余机器很闲。
一个键不管用哪种分片方案都只落在一个分片上,加机器、换哈希算法都改变不了这一点;只读副本分担的是读,扣库存是写。把一个热键拆成 100 个键,写就分散到不同分片。代价是要知道「还剩多少」得把 100 份加起来,某一份扣完了还要去别的份里找;系统也要记住哪些键被拆过,开票的热度过去后再合回来。
场景:一个集群开着「节点失联就自动把它的分片迁走」。某天一台机器因为一次很长的垃圾回收停顿卡了二十秒,系统判定它挂了,开始往其他机器迁分片。迁移的流量让另外两台也变慢,接着它们也被判定失联……
这正是书里说的自动再平衡和自动故障检测叠在一起的危险:慢不等于死,误判之后搬数据又给别的机器加了负载,一台拖垮一台。缩短超时只会误判更多;固定分片数让搬的单位更整齐,但要搬的数据一点没少;带宽更大能缓一缓,判断错误的根子还在。书里提到的折中是让人参与再平衡,比如系统生成建议的分配、管理员确认后才生效。
场景:一个物流平台的运单表按运单号分成 64 个分片,每个分片在「收件人手机号」上有自己的本地索引。客服每秒按手机号查运单几千次,一个手机号通常只对应几张单;运单每秒新增几百条。现在每个分片的 CPU 都被这类查询占满。
本地索引的每次查询都要发给全部 64 个分片,分片加倍只会让每次查询多问一倍的分片,总吞吐上不去。全局索引按手机号分片,一次查询只问一个索引分片,几张运单也只碰几个数据分片;读远多于写、又只有一个条件,正是全局索引合适的情况。代价是每次写要多改一个索引分片,像 DynamoDB 那样异步更新时,刚建的运单可能晚一点才查得到。索引全放一台机器会成为新的瓶颈;加副本能分担一些,但每次查询仍要问遍所有分片。
和本站其他书对照
值得停下来的句子
sharding is a heavyweight solution that is mostly relevant at large scale.— §1。本站译:分片是一种重型方案,大多只在规模很大时才用得上。
这句话给全章定了调。后面讲的每一种切法、每一种搬法、每一种索引,都是在为分片带来的复杂付账:要选分区键,跨分片的写要分布式事务,二级索引要么读贵、要么写贵。作者紧接着提醒,如今一台机器能做的事很多。读后面的每一节时,可以回来问一句:这份复杂,是为多大的规模付的?
adding more shards lets you store more data, but it doesn’t increase your query throughput if every shard has to process every query anyway.— §5.1。本站译:多加分片能多存数据,但如果每个分片反正都要处理每一个查询,查询吞吐并不会提高。
分片能扩展的,只是「一个请求只碰一个分片」的那部分负载。一类查询如果要扇出到所有分片,它就没有被分摊,只是被重复了 N 遍。所以选分区键,其实是在决定哪些查询能随机器数扩展、哪些不能;全局索引就是为某一类查询另选一个分区键。
检查一下理解
1.(§1)读的量太大和写的量太大,书里分别建议怎么扩展?
读太多不一定要分片,用复制把读分给多个从库就行(第 6 章的读扩展)。数据量或写入量超出一台机器时,才需要分片。
2.(§1)为什么有的系统在一台机器上也分片?
每个 CPU 核跑一个单线程进程,各管一个分片,这样能用满多核,也能照顾到 NUMA 架构里内存离哪个 CPU 近。书里举的是 Redis、VoltDB、FoundationDB。
3.(§2)按租户分片,什么情况下这条路走不通?
某个租户大到一台机器放不下,就得在它内部再分片;租户又多又小时,一个租户一个分片开销太大,合在一起又要处理租户长大后的迁移;需要跨租户关联数据的功能会变成跨分片的 join。
4.(§3.1)按键范围分片时,边界为什么不能简单地按字母平均切?
数据本身分布不均,平均切会让一些分片比别的大得多。边界要跟着数据定:管理员手工定,或者分片长到一定大小时自动拆开。
5.(§3.2)为什么不能直接用编程语言自带的对象哈希函数来分片?
有些语言(书里举了 Java 和 Ruby)里,同一个键在不同进程中算出的哈希值可能不同。分片要求每个节点、每个客户端对同一个键算出同一个结果。
6.(§3.2)固定分片数的方案里,加一台机器时什么变、什么不变?
分片的个数和键到分片的对应都不变,变的只是分片放在哪台机器,搬的是整个分片。搬的过程要花时间,期间读写仍按旧的分配进行。
7.(§3.3)给热键加上随机后缀以后,读为什么没有变轻松?
写被分散到了 100 个键上,但读要把这 100 个键都读一遍再合并,每个子键上的读量并没有减少。而且只该对少数热键这么做,所以还要另外记住哪些键被拆了。
8.(§5.2)全局索引查一个条件只要读一个索引分片,为什么取回整条记录时还要访问多个分片?
索引分片里存的只是记录 ID 的列表;记录本身按主键分片,这些 ID 散在各个数据分片上,要一个个去取。
用一句话说这一章(写给一个没读过的朋友):
精彩回顾
一句话:分片要在「分得匀、搬得少、查得快」之间取舍,而一台机器扛得住时,最好的分片方案是不分。
分片是重型方案:要选分区键,跨分片的写要分布式事务,二级索引变难。如今一台机器能扛的量很大。
§1按范围分片时,新数据的键总是最大,全挤进最后一个分片。在前面加上实体 ID,写入就摊开了。
§3.1hash % N 从 4 台变 5 台,约五分之四的键换机器,而新机器只需要五分之一。除数一变,余数全乱。
§3.2键 → 分片永远不变,分片 → 机器可以改。加机器只搬整个分片;代价是分片数要一开始定好。
§3.2热键再怎么换分片方案都只在一个分片上。加两位随机数把写分散,读就要把一百份合起来。
§3.3自动再平衡遇上自动故障检测:把慢节点当死节点,搬数据又压垮别的节点,一路连锁下去。
§3.4本地索引写一处、读要问遍所有分片;全局索引读一处、写要改好几处。
§5