背景
偶尔在推特上看到关于使用 aws 的 s3 存储 sqllite 搭配 serverless 实现数据仓库的帖子。感觉很有趣,故而把一些有效的信息做一下收集。
// 来源
https://news.ycombinator.com/item?id=31487706
https://bryson3gps.wordpress.com/2021/04/01/a-quick-look-at-s3-read-speeds-and-python-lambda-functions/
https://twitter.com/rclmenezes/status/1529278214346051584?s=21
回到我在 2016 年的旧工作,我们通过 Postgres、SQLite 和 Lambda 构建了一个廉价的本土数据仓库。基本上,它是这样工作的:
- 我们所有的数据都保存在 S3 上的压缩 SQLite DB 中。
- 收到查询后,Postgres 将使用我们构建的自定义外部数据包装器。
- 此 FDW 会将查询转发到 Web 服务。
- 此 Web 服务将为每个 SQLite 文件启动一个 lambda。每个 lambda 都会获取文件、查询文件并将结果返回给 Web 服务。
- 此 Web 服务将根据需要重新发出 lambda,并将结果返回给 FDW。
- Postgres(托管在内存优化的 EC2 实例上)将聚合。
这简直是魔法。计算 + 存储分离,成本基本为零,性能优于 Redshift 和 Vertica。我们所有的数据都是时间序列数据,因此非常容易分区。
而且,它也比雅典娜便宜得多。在 Athena 上,我们的查询成本约为 5 美元 / TB(今天没有改变!),因此对于大多数查询来说,它很容易超过 100 美元,而且我们每小时运行数千个查询。
直到今天,我仍然认为 DW 不可避免的开源解决方案可能看起来像这样。将您的数据作为 SQLite 或 DuckDB 插入存储桶,弹出 Postgres 扩展,创建 FDW,然后 `terraform apply` lambdas + api 网关。非时间序列数据会更难,但您可能可以制作一些存储其他分区的东西。
</td>
我们做类似的事情,但是:- 我们现在使用 R2 代替 S3。
- 我们使用 DuckDB+CSV/Parquet 代替 Postgres+Sqlite3。
- 我们使用 AWS AppRunner 而不是 Lambda(考虑将其移至 Fly.io 或 Workers)。
即使我们使用 Clickhouse/Timescale/Redshift/Elasticsearch 的速度较慢,它也能出色地处理各种分析工作负载。
巫师 4 天前 | | [–]
您在生产环境中使用 DuckDB 的体验如何?这是一个相对较新的项目。它的可靠性如何?
无知 3 天前 | | | [–]
对于我们的规模和请求模式(易于分区 / 0.1 qps),没有大问题,但我使用的 JavaScript 绑定(与它们的wasm 绑定不同)还有很多不足之处。值得称赞的是,它们似乎拥有一流的 CPP 和 Python 绑定,甚至支持专为跨语言 / 跨进程构建的高效内存映射箭头格式,此外,它们还具有一流的内存表示类似熊猫的数据框。诚然 DuckDB 正在不断开发中,但它还没有原生的跨版本导出 / 导入功能(因为它的开发人员声称 DuckDB 还没有成熟到稳定它的磁盘格式)。
我还关注 https://h2oai.github.io/db-benchmark/ 至于箭头支持的查询引擎,尤其是 Pola.rs 和 DataFusion 听起来对我来说最令人兴奋。
DataBrick 的 delta.io 是如何开发的还有待观察(对于更大的数据仓库可能会派上用场)。
3 天前 | | [–]
我对此进行了调查,但看到吞吐量变化很大,有时只有 20 MB / 秒。即使全吞吐量,我认为 s3 单键性能最高可达约 130 MB / 秒。您是如何在合理的时间内将这些巨大的 s3 blob 放入 lambda 中的?
5 小时前 | | [–]
* 使用更大的 lambda,您可以获得更可预测的性能,2GB RAM lambda 应该可以为您带来 ~ 90MB/s [0]* 假设您的解析速度比从 S3 读取的速度快(对于大多数工作负载来说都是如此?)读取吞吐量是您的瓶颈。
* 设置目标查询时间,例如 1s。这意味着要在 1 秒内完成查询,S3 上的每条记录必须为 90MB 或更小。
* 对数据进行分区,使 S3 上的每条记录小于 90 MB。
* 忘了提一下,您还可以从 S3 进行并行读取,具体取决于您的数据格式 / 解析速度,也可能需要研究一下。
这在某种程度上是一个简化的指南(例如,对于某些工作负载,合并数据需要时间,我们在这里不包括在内)但应该足够好开始了。
[0] - https://bryson3gps.wordpress.com/2021/04/01/a-quick-look-at-...
西蒙 3 天前 | | [–]
您在这里使用的 SQLite 数据库文件有多大?我一直在考虑构建将 SQLite 存储在 S3 中并将它们拉到 lambda 进行查询的系统,但我担心它基于数据库文件大小的可行性以及执行提取需要多长时间。
老实说,我没有考虑过压缩它们,但这显然是一个巨大的胜利。
西蒙 3 天前 | | [–]
看起来您已经在这里回答了我的问题:https ://news.ycombinator.com/item?id=31487825
巫师 4 天前 | | [–]
您是否有一篇博文或类似的文章来详细介绍此架构?我会非常有兴趣阅读它!
西蒙 3 天前 | | [–]
只是让你知道我在推特上发布了一个链接,它在 Twitter 上引起了相当多的关注:https ://twitter.com/simonw/status/1529134311806410752
dfinninger 3 天前 | | [–]
这听起来与 Trino 的(以及扩展的 Athena 的)架构非常相似。SQLite -> parquet(用于列而不是行存储) Lambda -> Worker Tasks FDW -> Connector Postgres Aggregation -> Worker Stage
我们在具有自动缩放功能的 Kubernetes (EKS) 中运行它,所以它的工作方式有点像 lambda。
notmattbark 3 天前 | | [–]
快速提问,你在 LinkedIn 上吗?请按我的方式发送
乔万迪克 4 天前 | [–]
好奇:每个压缩的 s3 sqlite db 有多大?
temuze 4 天前 | [-]
抱歉,好久不见,忘记了:(我们必须在文件太大(这会很慢)和文件太小(太多 lambdas 无法启动)之间取得平衡
我_认为_它们每个约为 10 GB,但我可能会相差一个数量级。
temuze 3 天前 | | [-]
哎呀我完全错了:https://twitter.com/rclmenezes/status/1529278214346051584?s=...
</td>
本文由 简悦 SimpRead 转码, 原文地址 bryson3gps.wordpress.com
前几天我正在和一位同事讨论异步文件哈希操作……
前几天,我与一位同事就异步文件散列操作进行了交谈,该操作触发了上传到 S3 存储桶的新对象。有一次我们在谈论吞吐量。该设计具有将 S3 事件发送到 SQS 队列进行处理的通知配置。这意味着在第一分钟,我们有五个 Lambda 函数,每个函数一次处理一个文件(批量大小为 1:这是一个实现决策,为了本文,我们不会讨论更大的批量大小),并且然后在第二分钟 65,三分之二 125,依此类推。
本次讨论的餐巾纸数学假设平均文件大小为 1 GB,理想的吞吐量为 100 MBps。在每个文件 10 秒、每个 Lambda 函数每分钟 6 个文件的情况下,我们可以预期在扩展期间第一分钟处理 30 个文件,第二分钟处理 380 个文件,第三分钟处理 730 个文件。我们当前的实现允许我们在 3 分钟内对 1,170 个文件(诚然在高端)1 GB 文件进行哈希处理。
也就是说,_如果_我们实际获得 100 MBps。
我们实际上可以期待什么?
我下午剩下的时间专注于找出这段代码的实际吞吐量是多少。我将服务的 Lambda 函数剥离为关键项目,并预加载了一个 S3 存储桶,其中包含四个测试文件,这些文件的大小是我们通常期望进入系统的大小:100 MB、500 MB、1 GB 和 5 GB。
这是代码:
import hashlib
import time
import boto3
s3 = boto3.resource("s3")
BUCKET = "my-bucket"
KEYS = [
"test100mb.file",
"test500mb.file",
"test1gb.file",
"test5gb.file",
]
CHUNK_SIZE = 20000000
def lambda_handler(event, context):
print("Starting...")
for key in KEYS:
stream_file(key)
print("Complete")
def stream_file(key):
start_time = time.time()
hash_digest = hashlib.sha1()
s3_object = s3.Object(bucket_name=BUCKET, key=key).get()
for chunk in read_in_chunks(s3_object):
hash_digest.update(chunk)
ellapsed_time = time.time() - start_time
print(
f"File {key} - SHA1 {hash_digest.hexdigest()} - Total Seconds: {round(ellapsed_time, 2)}"
)
def read_in_chunks(s3_object: dict):
"""A generator that iterates over an S3 object in 10 MB chunks."""
stream = s3_object["Body"]._raw_stream
while True:
data = stream.read(CHUNK_SIZE)
if not data:
break
yield data
现在是测试部分。我需要看看这段代码不仅在不同的内存设置下是如何执行的(记住:内存设置还为我们的函数分配 CPU 和网络 IO),还需要查看将从 S3 为每个对象流式传输的块大小。上面的代码使用块大小将 S3 对象的 X 字节流式传输到内存中,然后用它更新哈希摘要,并在移动到下一个块之前丢弃。这使得我们的实际内存利用率非常低。事实上,即使执行时间不是很长,上述散列操作也适用于 Lambda 函数的 128 MB 内存。
在这一点上,我必须告诉大家,我在更改内存和块设置后单击控制台中的“Invoke”,像一个古老的野蛮人一样进行了测试。如果您要进行性能测试,我建议您查看AWS Lambda Power Tuning项目。这非常棒。
下表是我作为这项工作的一部分记录的数据。需要注意的几件事会限制此数据并使其不完整:
- 我在每个配置中只执行了两次运行。这是一个非常有限的数据集,执行之间明显存在影响时间的环境差异。如果我获得更大的数据集,这些可能会被拉平,并且异常值会下降。
- 一次只执行一次运行,我没有明确的迹象表明不同文件的大量并行读取操作是否会影响读取速度。我的假设是_否定_的,但这是一个假设。
- 虽然可以对这个工作流程进行多线程处理,并可能在更高的内存设置下对其进行多处理,但我认为这样做不会增加代码复杂性。此外,跨线程拆分下载可能不会提高 S3 的读取速度,因为现在有多个流竞争带宽。
我们将在这张桌子的另一边了解我的思考过程。
文件大小 (MB) | 使用的内存 | 第一次运行(秒) | 第二轮 | 平均速度 (MBps) |
128 MB 内存/1 MB 块大小 | ||||
100 | 82 | 4.91 | 7.49 | 16.13 |
500 | 82 | 27.26 | 30.74 | 17.24 |
1000 | 82 | 55.7 | 74.82 | 15.32 |
5000 | 82 | 329.68 | 329.68 | 15.17 |
128 MB 内存/10 MB 块大小 | ||||
100 | 108 | 5.18 | 7.64 | 15.60 |
500 | 108 | 21.26 | 24.96 | 21.64 |
1000 | 108 | 43.32 | 49.82 | 21.47 |
5000 | 108 | 217.84 | 240.16 | 21.83 |
256 MB 内存/10 MB 块大小 | ||||
100 | 108 | 2.65 | 2.51 | 38.76 |
500 | 108 | 9.8 | 10.8 | 48.54 |
1000 | 108 | 20.32 | 21.48 | 47.85 |
5000 | 108 | 118.7 | 99.08 | 45.92 |
256 MB 内存/20 MB 块大小 | ||||
100 | 138 | 2.64 | 2.4 | 39.68 |
500 | 138 | 9.74 | 9.92 | 50.86 |
1000 | 138 | 19.58 | 19.92 | 50.63 |
5000 | 138 | 99.42 | 97.44 | 50.80 |
256 MB 内存 / 50 MB 块大小 | ||||
100 | 245 | 3.55 | 2.71 | 31.95 |
500 | 245 | 12.68 | 12.52 | 39.68 |
1000 | 245 | 25.54 | 25.4 | 39.26 |
5000 | 245 | 128.06 | 127.5 | 39.13 |
512 MB 内存 / 20 MB 块大小 | ||||
100 | 137 | 1.5 | 1.16 | 75.19 |
500 | 137 | 5.38 | 5.34 | 93.28 |
1000 | 137 | 13.76 | 13.8 | 72.57 |
5000 | 137 | 69.74 | 69.74 | 71.69 |
512 MB 内存 / 50 MB 块大小 | ||||
100 | 245 | 2.07 | 1.73 | 52.63 |
500 | 245 | 6.74 | 6.52 | 75.41 |
1000 | 245 | 13.66 | 14.3 | 71.53 |
5000 | 245 | 68.78 | 70.21 | 71.95 |
1024 MB 内存 / 20 MB 块大小 | ||||
100 | 137 | 1.2 | 1.1 | 86.96 |
500 | 137 | 6.6 | 5.29 | 84.10 |
1000 | 137 | 14.57 | 13.93 | 70.18 |
5000 | 137 | 72.76 | 69.57 | 70.26 |
1024 MB 内存 / 50 MB 块大小 | ||||
100 | 246 | 1.33 | 1.21 | 78.74 |
500 | 246 | 6.52 | 6.53 | 76.63 |
1000 | 246 | 14.46 | 14.61 | 68.80 |
5000 | 246 | 72.65 | 72.69 | 68.80 |
2048 MB 内存/20 MB 块大小 | ||||
100 | 138 | 1.09 | 1.06 | 93.02 |
500 | 138 | 5.33 | 5.35 | 93.63 |
1000 | 138 | 13.89 | 13.91 | 71.94 |
5000 | 138 | 69.69 | 69.56 | 71.81 |
我从默认的 128 MB 和 1 MB 块的拼写错误开始(我以为我已经写了10000000 )。较小的块大小意味着我们向 S3 发出了更多的请求,因此将其增加到 10 MB 是提高性能的简单方法。在更高的块大小下,我们现在接近利用所有可用内存,我们无法再次增加它。
我认为应该说任何部署 Lambda 函数的人都应该将其内存设置默认为 256 MB 以启动无论如何。无论您在做什么,性能的飞跃都是显而易见的,而且按每毫秒计费,没有理由不去追求它。
有了额外的内存开销,我决定看看如果我将块大小增加 5 倍会发生什么。在极限之内,我的表现实际上下降了。将块大小降低到 20 MB 揭示了一个最佳点(有人在这里提供帮助,但我知道我听说在 AWS 中的其他几个地方使用 20 MB 数字进行分块/内存缓存),我们现在可以一致性从 S3 读取约 50 MBps。
在 512 MB 内存和 20 MB 块大小时,我们已经达到了跨对象大小的最佳设置。70+ MBps 基线,方差高达 90+ MBps。
如果我要进行更密集的性能测试,我会专注于这里。将内存增加到 1024 MB 和 2048 MB 可以提高 < 1 GB 对象的读取速度,但不能提高 ≥ 1 GB 对象的读取速度。我仍然以 512 MB 和 1024 MB 测试了 50 MB 块,但它再次导致性能下降。
查看 < 1 GB 文件的速度增加可能很诱人,并说该函数应该以该速度运行以更快地刻录那些文件,但在我们的上下文中,时间差异是微不足道的,2048 MB 为 1.06 秒,而我们的“最佳” 512 MB 用于 100 MB 对象。
我这样说是因为这个系统不需要处理不断的、大量的对象进入我们的存储桶。在每月周期的某些时间,入口将不一致且尖峰。现在,如果我期待大量进入并且以更恒定的速率进入,我可能会发现增加是有道理的。每小时约 3,400 个 100 MB 对象与约 2,400 个是一种非常不同的测量方法。
我希望你们都喜欢这个小小的旅程。也许有一天我会回到它并通过一些适当的性能调整分析。