写点什么

怎样让 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:493305
用户头像
王强 技术是文明进步的力量

发布了 836 篇内容, 共 445.1 次阅读, 收获喜欢 1753 次。

关注

评论

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

NodeJS 5分钟 连接MySQL 增删改查 🥇

德育处主任

node.js MySQL 6月月更

如何在 Vue 项目中,通过点击 DOM 自动定位VSCode中的代码行?

vivo互联网技术

Vue 前端 vscode vite webpack

Camtasia2022全新版功能详情讲解

茶色酒

Camtasia Studio2022

【最佳实践】修改Anaconda中的Jupyter Notebook默认工作路径

迷彩

Python Anaconda Jupyter Notebook 6月月更

线程的创建方法

卢卡多多

线程池 线程安全 6月月更

攻防演练中六条安全体系建议

穿过生命散发芬芳

6月月更 攻防演练

物联网低代码平台如何查询授权信息?

AIRIOT

物联网 低代码开发

G1收集器概述

Nick

Java GC G1垃圾回收器 6月月更 Garbage-First Collector

spring4.1.8扩展实战之三:广播与监听

程序员欣宸

Java spring Spring Framework 6月月更

改造微服务的三个时机

阿泽🧸

微服务 6月月更

数组(二)

Jason199

数组 js 数组操作 6月月更

Python代码自动提取Win10内置的锁屏壁纸

宇宙之一粟

Python 6月月更

C#入门系列(十四) -- 结构体应用

陈言必行

C# 6月月更

读书笔记之:认知觉醒

甜甜的白桃

读书 书单 阅读 6月月更

Linux开发_介绍目录编程、标准文件编程、Linux系统文件接口编程、GDB调试

DS小龙哥

6月月更

Push还是Pull,这是个问题么?

MatrixOrigin

push Pull MatrixOrigin MatrixOne 数据库·

【sql语句基础】——查(select)(合并查询+连接查询)

写代码两年半

sql 查询 MySQL 数据库 数据库· 6月月更

ZooKeeper进阶(二):ZooKeeper的运行

No Silver Bullet

zookeeper 6月月更

InfoQ 极客传媒 15 周年庆征文 | Kettle实现ES到ES循环增量抽取

写程序的小王叔叔

架构 kettle ELK Stack InfoQ极客传媒15周年庆

Android ShapeableImageView使用详解,告别shape、三方库

yechaoa

android 6月月更 material design ShapeableImageView

前端uni框架学习day_2

恒山其若陋兮

前端 6月月更

文档管理系统应该具备哪些功能?

小炮

推荐的十个Flutter插件

坚果

6月月更

【Python技能树共建】scrapy 上手篇

梦想橡皮擦

Python 爬虫 Python爬虫 6月月更

leetcode 310. Minimum Height Trees 最小高度树(中等)

okokabcd

LeetCode 搜索 数据结构与算法

人人都在搞数据治理

奔向架构师

数据治理 数据管理 6月月更

Docker常用命令总结

乌龟哥哥

6月月更

【Java Web系列】Cookie的原理分析和使用细节

倔强的牛角

Java javaWeb Cookie 6月月更

MobX 获取网络数据来渲染酷炫的曲线

岛上码农

flutter ios 前端 安卓 6月月更

【涨姿势】你没用过的BadgeDrawable

yechaoa

android 6月月更 material design BadgeDrawable

Archiva 运行时提示 JAXBException 错误

HoneyMoose

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