警惕承诺。
在 Javascript 中,Promise.all它允许你并行执行多个 Promise 并获取结果数组。
const responses = await Promise.all([
fetch("/api/1"),
fetch("/api/2")
])
很简单。但是,如果你用 100 次 fetch 请求来执行上述操作,可能会因为一次自导自演的拒绝服务攻击而导致服务器宕机。即使你在 API 中通过速率限制来防止这种情况,随着规模的扩大,你仍然会看到大量请求失败的错误。
API 是个例外。大多数类型的外部调用根本没有速率限制的概念——例如文件系统操作、系统调用等等。
例如,在 NodeJS 中,你可以创建新的 shell 来调用计算机上的其他程序。我在我的开源 A/B 测试平台GrowthBook中就使用了这项功能来调用 Python 脚本。类似这样:
const results = await Promise.all(
metrics.map(m => callPython(m))
)
如果给定一个大型数组,上述代码会愉快地生成数百个 Python shell 并并行执行它们。我的开发机性能很强,所以在测试时我没有注意到所有 8 个 CPU 核心会在几秒钟内达到 100% 的占用率。但是,当我把代码部署到 AWS 上的 Docker 容器后,我才明显地注意到它开始频繁崩溃和重启。
解决方法是对调用添加速率限制或并发限制Promise.all。有几种方法可以实现这一点。
对于需要限制每秒调用次数的 API 调用,可以使用简单的p-throttle库:
import pThrottle from 'p-throttle'
// Limit to 2 calls per second
const throttle = pThrottle({
limit: 2,
interval: 1000
})
const responses = await Promise.all([
throttle(() => fetch("/api/1")),
throttle(() => fetch("/api/2")),
...
])
对于需要限制并行执行次数的系统调用(无论耗时多久),可以使用简单的p-limit库:
import pLimit from 'p-limit'
// Only 5 promises will run at a time
const limit = pLimit(5)
const results = await Promise.all(
metrics.map(
m => limit(() => callPython(m))
)
)
对于更高级的使用场景,您可能需要考虑使用功能齐全的作业队列,例如bree、bull或agenda。
作为开发者,我们花了很多时间担心外部攻击,却忽略了保护应用程序免受内部代码漏洞的侵害。我希望我的经历能帮助其他人避免在生产环境中遇到我曾经遇到的导致 CPU 崩溃的 bug。祝大家好运!
文章来源:https://dev.to/jdorn/beware-of-promiseall-3pph