1.1 什么是分片?

如果你坚持谷歌,原来所谓的分区和所谓的分片之间有一个相当模糊的界限。大家随便叫什么就叫什么。有些人区分水平分区和分片。其他人说分片是某种水平分区。

我没有找到一个单一的术语标准可以得到创始人的批准和 ISO 的认证。个人内心的信念大概是这样的:平均划分就是按照任意的方式“把基数切成块”。

  • 垂直分区- 按列。例如,有一个巨大的表,其中包含 60 列中的数十亿条记录。我们没有保留一个这样的巨型表,而是保留了至少 60 个每个有 20 亿条记录的巨型表——这不是列基,而是垂直分区(作为术语的一个例子)。
  • 水平分区- 我们逐行切割,可能在服务器内部。

这里的尴尬时刻是水平分区和分片之间的细微差别。我可以被切成碎片,但我不能确定地告诉你它是什么。感觉分片和水平分区是一回事。

一般来说,分片是指数据库中的大表或文档、对象的专业集合,如果你没有数据库,而是文档存储,则完全按对象切分。也就是从20亿个物体中,不管大小,都选出一块。每个对象里面的对象本身并没有被切割成碎片,我们也没有将它们分列放置,即我们将它们分批放置在不同的地方。

存在细微的术语差异。比如相对来说,Postgres的开发者可以说水平分区就是主表分出来的所有表都在同一个schema中,而在不同的机器上,这已经是sharding了。

一般意义上,不拘泥于特定数据库和特定数据管理系统的术语,有一种感觉,分片就是逐行/逐文档切片等等——仅此而已。

我强调典型。从某种意义上说,我们所做的这一切不仅仅是为了将 20 亿个文档切割成 20 个表,每个表都更易于管理,而是为了将其分布在许多核心、许多磁盘或许多不同的物理或虚拟服务器上。

1.2 划分不可分割的

据了解,我们这样做是为了让每个分片——每条数据——都被复制多次。但真的,没有。

INSERT INTO docs00 
SELECT * FROM documents WHERE (id%16)=0 
... 
 
INSERT INTO docs15 
SELECT * FROM documents WHERE (id%16)=15 

事实上,如果你做这样的数据切片,并且从你勇敢的笔记本电脑上 MySQL 上的一个巨大的 SQL 表,你将生成 16 个小表,而不会超出一台笔记本电脑,不是一个单一的模式,不是一个单一的数据库,等等. 等等。- 就是这样,你已经有了分片。

这导致以下结果:

  • 带宽增加。
  • 延迟不会改变,也就是说,在这种情况下,可以说每个工人或消费者都有自己的延迟。几乎在同一时间为不同的请求提供服务。
  • 或者两者兼而有之,还有高可用性(复制)。

为什么要带宽?有时,我们可能会有如此大量的数据不适合 - 不清楚在哪里,但它们不适合 - 在 1 {kernel | 磁盘 | 服务器 | ...}。只是资源不够,仅此而已。为了使用这个大型数据集,您需要对其进行切割。

为什么会有延迟?在一个内核上,扫描一个 20 亿行的表比在 20 个内核上并行扫描 20 个表慢 20 倍。单个资源上的数据处理速度太慢。

为什么要高可用?或者我们切割数据以便同时进行这两项操作,同时为每个分片制作多个副本——复制确保高可用性。

1.3 一个简单的例子“如何手工完成”

条件分片可以使用测试表test.documents切出32个文档,并从该表生成16个测试表,每个测试表大约2个文档test.docs00,01,02,...,15。

INSERT INTO docs00 
SELECT * FROM documents WHERE (id%16)=0 
... 
 
INSERT INTO docs15 
SELECT * FROM documents WHERE (id%16)=15 

为什么?因为先验我们不知道id是如何分布的,如果从1到32(含),那么每个正好有2个文档,否则没有。

我们在这里做为什么。我们制作完16张表后,就可以“抓取”我们需要的16张了。不管我们命中了什么,我们都可以并行化这些资源。例如,如果没有足够的磁盘空间,将这些表分解到单独的磁盘上是有意义的。

不幸的是,这一切都不是免费的。我怀疑在规范 SQL 标准的情况下(我已经很久没有重新阅读 SQL 标准了,也许它已经很久没有更新了),没有官方的标准化语法可以对任何 SQL 服务器说:“亲爱的SQL服务器,给我做32个分片,分4个盘。但是在单独的实现中,通常有一个特定的语法来做基本相同的事情。PostgreSQL 有分区机制,MySQL 有 MariaDB,Oracle 很可能早就做了这一切。

然而,如果我们在没有数据库支持的情况下在标准框架内手动完成,那么我们有条件地为数据访问的复杂性付出代价。以前有一个简单的 SELECT * FROM documents WHERE id=123,现在是 16 x SELECT * FROM docsXX。如果我们尝试通过密钥获取记录,那就太好了。如果我们试图获得早期的记录范围,那就更有趣了。现在(我强调,如果我们是傻瓜,并保持在标准的框架内),这 16 个 SELECT * FROM 的结果将必须在应用程序中组合。

您可以期待什么性能变化?

  • 直观地-线性。
  • 理论上 - 次线性,因为阿姆达尔定律。
  • 实际上,也许几乎是线性的,也许不是。

事实上,正确的答案是未知的。通过巧妙地应用分片技术,您可以实现应用程序性能的显着超线性下降,即使是 DBA 也会拿着一把红热的扑克牌跑来跑去。

让我们看看这是如何实现的。很明显,只是将设置设置为 PostgreSQL shards=16,然后它自己起飞,这并不有趣。让我们考虑一下如何确保将分片速度减慢 16 倍 32 倍——从如何不这样做的角度来看,这很有趣。

我们加速或减速的尝试总是会遇到经典 - 古老的 Amdahl 法则,它说任何请求都没有完美的并行化,总会有一些一致的部分。

1.4 阿姆达尔定律

总有一个序列化的部分。

总有一部分查询执行是并行化的,总有一部分没有并行化。即使在您看来是一个完美的并行查询,至少您要从每个分片接收的行中发送给客户端的结果行的集合始终存在,并且始终是顺序的。

总有一些一致的部分。它可以很小,在一般背景下完全不可见,也可以很大,因此会强烈影响并行化,但它始终存在。

此外,它的影响正在发生变化并且可以显着增长,例如,如果我们削减我们的表 - 让我们提高赌注 - 从 64 条记录变成 16 个表的 4 条记录,这部分将会改变。当然,从如此巨大的数据量来看,我们正在使用手机和 2 MHz 86 处理器,我们没有足够的文件可以同时打开。显然,有了这样的输入,我们一次打开一个文件。

  • 它是Total = Serial + Parallel。例如,并行是数据库内部的所有工作,串行是将结果发送到客户端。
  • 成为Total2 = Serial + Parallel/N + Xserial。比如整体ORDER BY时,Xserial>0。

通过这个简单的例子,我试图证明一些 Xserial 出现了。除了总是有一个序列化部分这一事实以及我们试图并行处理数据这一事实之外,还有一个额外的部分来提供这种数据切片。粗略地说,我们可能需要:

  • 在数据库的内部字典中找到这 16​​ 个表;
  • 打开文件;
  • 分配内存;
  • 取消分配内存;
  • 合并结果;
  • 核之间同步。

一些不同步的效果仍然出现。它们可以微不足道,占据总时间的十亿分之一,但它们始终不为零,始终存在。在他们的帮助下,我们可以在分片后显着降低性能。

这是关于阿姆达尔定律的标准图片。这里重要的是,理想情况下应该是直的并且线性增长的线会进入渐近线。但由于互联网上的图表难以阅读,在我看来,我制作了更多带有数字的可视化表格。

假设我们有一些只占用 5% 的请求处理序列化部分:serial = 0.05 = 1 / 20

直觉上,序列化部分似乎只占用请求处理的 1/20,如果我们将 20 个内核的请求处理并行化,它将变得大约 20,在最坏的情况下快 18 倍。

其实数学是个没心没肺的东西

墙 = 0.05 + 0.95/核心数,加速比 = 1 / (0.05 + 0.95/核心数)

事实证明,如果仔细计算,序列化部分为 5%,加速比为 10 倍(10.3),比理论理想值提高了 51%。

8芯 = 5.9 = 74%
10芯 = 6.9 = 69%
20芯 = 10.3 = 51%
40核 = 13.6 = 34%
128核 = 17.4 = 14%

将 20 个内核(如果你愿意,也可以是 20 个磁盘)用于过去处理的任务,我们在理论上什至永远不会获得超过 20 倍的加速,但在实践中 - 更少。而且,随着并行数的增加,效率低下也大大增加。

当只剩下 1% 的串行化工作,99% 被并行化时,加速比值有所提高:

8芯 = 7.5 = 93%
16芯 = 13.9 = 87%
32核 = 24.4 = 76%
64核 = 39.3 = 61%

对于一个完美的热核查询,自然需要几个小时才能完成,而准备工作和结果的组装只需要很少的时间 (serial = 0.001),我们已经可以看到良好的效率:

8芯 = 7.94 = 99%
16芯 = 15.76 = 99%
32核 = 31.04 = 97%
64核 = 60.20 = 94%

请注意,我们永远不会看到 100%。在特别好的情况下,您可以看到,例如,99.999%,但不完全是 100%。