几年前,在为参谋工程师采访 Nelson Elhage 时,他提到“估计”是一项特别有价值的技能。我没有过多考虑将估计作为一种技能的想法,但他的评论让我想起了我做过的最好的架构面试之一,候选人能够通过询问一些问题来显着缩小可能的解决方案详细信息(据我所知:每秒查询次数、预期行数和必要列数)。
从历史上看,我从来没有特别擅长估计磁盘空间,所以我决定花几个小时来研究这项技能,这已经变成了这些笔记,我希望这些笔记也能对其他希望改进估计的人有所帮助。我将首先分享一些估计磁盘空间的有用规则,然后汇总一个 SQLite3 片段来演示验证估计值。
千字节(1024 字节)和千字节(1000 字节)
估计大小时的第一个混淆点是千字节是 1024 字节还是 1000 字节。 “正确答案”是“千字节”(kB)是 1000 字节,“千字节”(KB)是 1024 字节。这种区别在其他单位中继续存在,例如兆字节 (MB, 1000^2) 与兆字节 (MiB, 1024^2) 以及千兆字节 (GB, 1000^3) 与千兆字节 (GiB, 1024^3)。
这里的三个关键点是:(1) 了解千字节和千字节单位,(2) 认识到与您通信的人可能不熟悉千字节单位系列,以及 (3) 使您的方法适应他们的内容’熟悉。在现实生活中,技术正确没有任何意义。
调整 UTF-8、整数等的大小
虽然有许多数据类型值得熟悉,但最常见的是:
-
UTF-8字符以 1-4 个字节编码。您是否应该估计 1、2、3 或 4 个字节的空间使用量取决于您正在编码的内容。例如,如果您只编码 ASCII 值,那么 1 个字节就足够了。最重要的是能够解释你的理由。例如,说您采用最保守的情况,每个字符使用 4 个字节是合理的。
使用 UTF-8 大小作为基线,您可以估计字符串、varchar、文本或其他类似字段的大小。这些字段的精确开销绝对会有所不同,具体取决于用于存储数据的特定技术
-
整数取决于您需要存储的最大值,但一个好的基线是 4 个字节。如果您想更精确,请找出整数的最大大小,然后您可以从那里通过计算来确定表示它所需的千字节数
maxRepresentableIntegerInNBytes(N) = 2 ^ (N bytes * 8 bits) - 1
例如,2个字节可表示的最大数是
2 ^ (2 bytes * 8 bits) -1 = 65535
-
浮点数通常存储在 4 个字节中
-
布尔值通常表示为 1 字节整数(例如在 MySQL 中)
-
枚举通常表示为 2 字节整数(例如在 MySQL 中)
-
日期时间通常以 5 个字节表示(例如在 MySQL 中)
配备这些规则后,让我们在估算数据库大小时进行一次练习。想象一下,我们的数据库中有 10,000 人。我们跟踪每个人的年龄和姓名。平均姓名长度为 25 个字符,我们希望支持的最大年龄为 125。这需要多少空间?
bytesPerName = 25 characters * 4 bytes = 100 bytes bytesPerAge = 1 byte # because 2^(1 bytes * 8bits) = 255 bytesPerRow = 100 bytes + 1 byte totalBytes = 101 bytes * 10,000 rows totalKiloBytes = (101 * 10000) / 1000 # 1,100 kB totalMegaBytes = (101 * 10000) / (1000 * 1000) # 1.1 MB
因此,大约需要 1.1 MB 来存储它。或者,这是 0.96 MiB,通过以下方式计算:
(101 * 10000) / (1024 * 1024) # 0.96 MiB
您现在可以估计数据集的生成大小。
索引、复制等
存储数据的理论成本与在数据库中存储数据的实际成本之间存在差距。您可能正在使用基于副本的工具,例如 MongoDB 或 Cassandra,它存储每条数据的三个副本。您可能正在使用存储每条数据的两个副本的主从复制系统。存储影响在这里很容易计算(分别是基本成本的 3 倍或 2 倍)。
索引提供了额外存储和减少查询时间之间的经典权衡,但它们将占用多少存储成本取决于索引本身的细节。作为调整索引大小的一种简单方法,确定索引中包含的列的大小,将其乘以索引的行数,然后将该总数添加到数据本身的基础存储成本中。如果创建一个包含每个字段的索引,那么大致估计两倍的总存储成本。
根据使用的特定数据库,还会有其他功能占用空间,真正了解它们对大小的影响将需要更深入地了解特定数据库。实现这一目标的最佳方法是直接使用该数据库验证大小。
使用 SQLite3 进行验证
好消息是验证数据大小相对容易,我们现在将使用 Python 和 SQLite3 来完成。我们将首先重新创建上述估计的 10,000 行,每行包含 25 个字符的名称和年龄。
import uuid import random import sqlite3 def generate_users(path, rows): db = sqlite3.connect(path) cursor = db.cursor() cursor.execute("drop table if exists users") cursor.execute("create table users (name, age)") for i in range(rows): name = str(uuid.uuid4())[:25] age = random.randint(0, 125) cursor.execute("insert into users values (?, ?)", (name, age)) db.commit() db.close() if __name__ == "__main__": generate_users("users.db", 10000)
之前我们估计它为 0.96 MiB,但运行这个脚本我发现它只有 344 KiB,只是预期空间的三分之一多一点。稍微调试一下我们的计算,我们可以看到我们假设每个字符 4 个字节,但是我们生成的名称(截断的UUID4s )都是 ascii 字符,所以实际上每个字符是 1 个字节。让我们基于此重新估计值:
bytesPerName = 25 characters * 1 byte = 25 bytes bytesPerAge = 1 byte bytesPerRow = 26 bytes totalKibiBytes = (26 * 10,000) / 1024 # 245 KIB
好吧,假设有一些开销,那就相当接近了,当然有。例如,SQLite3 透明地创建一个“rowid”列用作主键,它是一个 64 位整数,需要 4 个字节来表示。如果我们将这 4 个字节添加到我们之前估计的每行 26 个字节,那么我们得到的估计大小为 293 KiB,这与我们的估计非常接近。
去估计尺寸
估计数据的大小是一项相对简单的技能,它 (a) 很容易永远学不会, (b) 一旦你学会了它就非常有用。它在构建系统、通过调试复杂的分布式系统问题进行推理以及在面试中讨论架构问题时很有用。磁盘空间估计可以帮助您回答的一些有用区别:
- 它可以放在内存中吗?
- 它可以安装在一台带有 SSD 的服务器上吗?
- 这些数据是否需要在许多服务器上进行分片?多少?
- 这个索引可以放在一台服务器上吗?
- 如果没有,您将如何正确分区索引?
- 等等等等
尽管将它们用于某些时间,但我仍然感到惊讶的是,这种技术可以在多大程度上正确地限制您的解决方案空间。