写点什么

怎样让 API 快速且轻松地提取所有数据?

  • 2021-07-21
  • 本文字数:3166 字

    阅读完需:约 10 分钟

怎样让API快速且轻松地提取所有数据?

我上周在 Twitter 上发起了一个关于 API 端点的讨论。相比一次返回 100 个结果,并要求客户端对所有页面进行分页以检索所有数据的 API,这些流式传输大量数据的端点可以作为替代方案:


假设这种流式传输端点有了高效的实现,那么提供流式 HTTP API 端点(例如一次性提供 100,000 个 JSON 对象,而不是要求用户在超过 1000 个请求中每次分页 100 个对象)有任何意想不到的缺陷吗?——Simon Willison(@simonw),2021 年 6 月 17 日


我收到了很多很棒的回复。我试过在推文上把这些想法浓缩进一个,但我也会在这里将它们综合成一些见解。

批量导出数据


我花在 API 上的时间越多(尤其是处理DatasetteDogsheep项目时),我就越意识到自己最喜欢的 API 应该可以让你尽可能快速、轻松地提取所有数据。


API 一般可以通过三种方式提供这种功能:


  • 单击“导出所有内容”按钮,然后等待一段时间,等它显示包含可下载 zip 文件链接的电子邮件。这并不是真正的 API,主要因为用户通常很难甚至不可能自动执行最初的“点击”动作,但这总比没有好。谷歌的Takeout是这种模式的一个著名实现。

  • 提供一个 JSON API,允许用户对他们的数据进行分页。这是一种非常常见的模式,尽管它可能会遇到许多困难:例如,如果对原始数据分页时,有人又添加了新数据,会发生什么情况?另外,出于性能原因,某些系统也只允许访问前 N 页。

  • 提供一个你可以点击的单一 HTTP 端点,该端点将一次性返回你的所有数据(可能是数十或数百 MB 大小)。


我今天想要谈论的是最后一个选项。

高效地流式传输数据


过去,大多数 Web 工程师会很快否定用一个 API 端点流式输出无限数量行的这种想法。HTTP 请求是应该尽快处理的!处理请求所花费的时间但凡超过几秒钟都是一个危险信号,这表明我们应该重新考虑某些事情才是。


Web 堆栈中的几乎所有内容都针对快速处理小请求进行了优化。但在过去十年中,这一趋势出现了一些变化:Node.js 让异步 Web 服务器变得司空见惯,WebSockets 教会了我们如何处理长时间运行的连接,并且在 Python 世界中,asyncio 和 ASGI 为使用较少量内存和 CPU 处理长时间运行的请求提供了坚实的基础。


我在这个领域做了几年的实验。


Datasette 能使用ASGI技巧将表(或过滤表)中的所有行流式传输为 CSV,可能会返回数百 MB 的数据。


Django SQL Dashboard 可以将 SQL 查询的完整结果导出为 CSV 或 TSV,这次使用的是 Django 的StreamingHttpResponse(它确实会占用一个完整的 worker 进程,但如果你将其限制为只有一定数量的身份验证用户可用,那也没什么问题)。


VIAL用来实现流式响应,以提供“从管理员导出”功能。它还有一个受 API 密钥保护的搜索 API,可以用 JSON 或 GeoJSON输出所有匹配行。

实现说明


实现这种模式时需要注意的关键是内存使用:如果你的服务器在需要为一个导出请求提供服务时都需要缓冲 100MB 以上的数据,你就会遇到麻烦。


某些导出格式比其他格式更适合流式传输。CSV 和 TSV 非常容易流式传输,换行分隔的 JSON 也是如此。


常规 JSON 需要更谨慎的对待:你可以输出一个[字符,然后以逗号后缀在一个流中输出每一行,再跳过最后一行的逗号并输出一个]。这样做需要提前查看(一次循环两个)来验证你还没有到达终点。


或者……Martin De Wulf 指出你可以输出第一行,然后输出每行的时候带上一个前面的逗号——这完全避免了“一次迭代两个”的问题。


下一个挑战是高效地循环遍历所有数据库结果,但不要先将它们全部拉入内存。


PostgreSQL(和 psycopg2 Python 模块)提供了服务端游标,这意味着你可以通过代码流式传输结果,而无需一次全部加载它们。我把它们用在了 Django SQL仪表板中。


不过,服务端游标让我感到有些紧张,因为它们似乎很可能会占用数据库本身的资源。所以我在这里考虑的另一种技术是键集分页


键集分页(keyset pagination)适用于所有按唯一列排序的数据,尤其适合主键(或其他索引列)。使用如下查询检索每一页数据:


select * from items order by id limit 21
复制代码


注意limit 21——如果我们要检索 20 个项目的页面,我们这里要求的就是 21,因为这样我们就可以使用最后一个返回的项目来判断是否有下一页。然后对于后续页面,取第 20 个 id 值并要求大于该值的内容:


select * from items where id > 20 limit 21
复制代码


这些查询都可以快速响应(因为它针对有序索引)并使用了可预测的固定内存量。使用键集分页,我们可以遍历一个任意大的数据表,一次流式传输一页,而不会耗尽任何资源。


而且由于每个查询都是小而快的,我们也不必担心庞大的查询会占用数据库资源。

会出什么问题?


我真的很喜欢这些模式。它们还没有在我面前暴露出来什么问题,尽管我还没有将它们部署到什么真正大规模的环境里。所以我在 Twitter问了问大家,想知道应该留心什么样的问题。


根据 Twitter 讨论,以下是这种方法面临的一些挑战。

挑战:重启服务器


如果流需要很长时间才能完成,那么推出更新就会成为一个问题。你不想中断下载,但也不想一直等待它完成才能关闭服务器。——Adam Lowry(@robotadam),2021 年 6 月 17 日


这种意见出现了几次,这是我没有考虑过的。如果你的部署过程涉及重新启动服务器的操作(很难想象完全不需要重启的情况),那么在执行这一操作时需要考虑长时间运行的连接。如果有用户正在一个 500MB 的流中走过了一半路程,你可以截断他们的连接或等待他们完成。

挑战:如何返回错误


如果你正在流式传输一个响应,你会从一个 HTTP 200 代码开始……但是如果中途发生错误,可能是在通过数据库分页时发生错误会怎样?


你已经开始发送这个请求,因此你不能将状态代码更改为 500。相反,你需要向正在生成的流写入某种错误。


如果你正在提供一个巨大的 JSON 文档,你至少可以让该 JSON 变得无效,这应该能向你的客户端表明出现了某种问题。


像 CSV 这样的格式处理起来更难。你如何让用户知道他们的 CSV 数据是不完整的呢?


如果某人的连接断开怎么办——他们肯定会注意到他们丢失了某些东西呢,还是会认为被截断的文件就是所有数据呢?

挑战:可恢复的下载


如果用户通过你的 API 进行分页,他们可以免费获得可恢复性:如果出现问题,他们可以从他们获取的最后一页重新开始。


但恢复单个流就要困难得多。


HTTP 范围机制可用于提供针对大文件的可恢复下载,但它仅在你提前生成整个文件时才有效。


有一种 API 的设计方法可以用来支持这一点,前提是流中的数据处于可预测的顺序(如果你使用键集分页则必须如此,如上所述)。


让触发下载的端点采用一个可选的?since=参数,如下所示:


GET /stream-everything?since=b24ou34[    {"id": "m442ecc", "name": "..."},    {"id": "c663qo2", "name": "..."},    {"id": "z434hh3", "name": "..."},]
复制代码


这里b24ou34是一个标识符——它可以是一个故意不透明的令牌,但它需要作为响应的一部分提供。


如果用户由于任何原因断开连接,他们可以传递他们成功检索到的最后一个 ID 来从上次中断的地方开始:


GET /stream-everything?since=z434hh3
复制代码


这还需要客户端应用程序具备某种程度的智能反馈,但它是一个相当简单的模式,既可以在服务器上实现,也能作为客户端实现。

最简单的解决方案:从云存储生成和返回


实现这种 API 的最健壮的方法似乎是技术上最让人觉得无聊的:分离一个后台任务,让它生成大型响应并将其推送到云存储(S3 或 GCS),然后将用户重定向到一个签名 URL 来下载生成的文件。


这种方法很容易扩展,为用户提供了带有内容长度标头的完整文件(甚至可以恢复下载,因为 S3 和 GCS 支持范围标头),用户很清楚这些文件是可下载的。它还避免了由长连接引起的服务器重启问题。


这就是 Mixpanel 处理其导出功能的方式,这也是 Sean Coates 在尝试为 AWS Lambda/APIGate 响应大小限制寻找解决方法时想到的方案


如果你的目标是为用户提供强大、可靠的数据批量导出机制,那么导出到云存储可能是最佳选项。


但是,流式动态响应是一个非常巧妙的技巧,我计划继续探索它们!


原文链接:


https://simonwillison.net/2021/Jun/25/streaming-large-api-responses/

2021-07-21 16:493282
用户头像
王强 技术是文明进步的力量

发布了 831 篇内容, 共 439.9 次阅读, 收获喜欢 1752 次。

关注

评论

发布
暂无评论
发现更多内容

【深度挖掘RocketMQ底层源码】「底层源码挖掘系列」透彻剖析贯穿RocketMQ的消费者端的运行调度的流程(Pull模式)

洛神灬殇

RocketMQ 消费原理 运行机制 源码实现

自动化测试概况和认知

自动化测试 测试工具

Python json中一直搞不清的load、loads、dump、dumps、eval

Python json 字符串

一文搞懂秒杀系统,欢迎参与开源,提交PR,提高竞争力。早日上岸,升职加薪。

王中阳Go

Go golang 架构 高并发 秒杀

toFixed和Math.round既不是四舍五入也不是银行家舍入法

咖啡教室

Zebec生态持续深度布局,ZBC通证月内翻倍或只是开始

股市老人

用reduce高阶函数组装查询表单分隔字符数据

咖啡教室

磁盘有限,Docker 垃圾很多怎么办

newbe36524

C# Docker Kubernetes

Python写入csv出现空白行,如何解决?

Python csv 数据读写

云原生 + AI 时代已至,大数据底座何去何从?

Kyligence

hadoop 云原生

fl studio21中文版免费的音乐编曲制作软件

茶色酒

FL Studio21

全国独家线下面授 | 北京大规模敏捷LeSS认证5月18-20日开班

ShineScrum

less 大规模敏捷

MySql基础-笔记12 -重复数据处理、SQL注入、导入导出数据

MySQL 数据库

推荐系统[三]:粗排算法常用模型汇总(集合选择和精准预估),技术发展历史(向量內积,Wide&Deep等模型)以及前沿技术

汀丶人工智能

推荐系统 推荐算法 搜索系统

软件测试 | 0经验拿下大厂年薪30万offer,我的面试求职之路(含面试题)

测吧(北京)科技有限公司

测试

降本提效 | AIRIOT设备运维管理解决方案

AIRIOT

物联网 设备运维

Zebec官方辟谣“我们与Protradex没有任何关系”

鳄鱼视界

好用的录屏工具值得免费拥有

穿过生命散发芬芳

录屏工具

WebUI自动化中截图的使用

Python 自动化测试 unittest 截图

【FAQ】获取Push Token失败,如何进行排查?

HarmonyOS SDK

HMS Core

从人工测量转向计算机视觉,基于PaddleSeg实现自动测量心胸比

飞桨PaddlePaddle

深度学习 开发者 开发工具 飞桨

2023年Web安全最详细学习路线指南,从入门到入职(含书籍、工具包)【建议收藏】

网络安全学海

黑客 网络安全 信息安全 渗透测试 WEB安全

模块七作业

张贺

架构训练营

CleanMyMac2024免费版系统清理优化软件

茶色酒

CleanMyMac X CleanMyMac2024

秒懂算法 | 子集树模型——0-1背包问题的回溯算法及动态规划改进

TiAmo

算法 回溯算法 动态回溯算法

阿里云ECS TOP性能提升超20%!KeenTune助力倚天+Alinux3达成开机即用的全栈性能调优 | 龙蜥技术

OpenAnolis小助手

ECS 龙蜥社区 KeenTune 云场景 全栈性能调优

ABBYY FineReader16永久版图片文字识别软件

茶色酒

ABBYY FineReader16

3 个加强理解TypeScript 的面试问题

devpoint

JavaScript typescript ES6 前端面试

LeetCode题解:633. 平方数之和,枚举,JavaScript,详细注释

Lee Chen

JavaScript 算法 LeetCode

这个只要三步就能实现ins图片下载的方法!我直接就是一个疯狂点赞的大动作!

frank

ins图片下载

怎样让API快速且轻松地提取所有数据?_语言 & 开发_Simon Willison_InfoQ精选文章