我们如何将无服务器 API 提升 300 倍
这是一个关于我们如何让微服务响应速度提高 3️⃣0️⃣0️⃣ 倍的故事。是的,你没看错:快了 3️⃣0️⃣0️⃣ 倍!🤯
我们将响应时间从平均约 20 秒缩短至约 60 毫秒。为了不欺骗您的眼睛,我使用相同的比例:20,000 毫秒到60 毫秒。
“等等”,你可能会说,“你们把加载时间缩短了300倍,这真是让我印象深刻,但你们到底做了什么,导致响应时间这么糟糕?”
违背承诺还是不切实际的期望?
我们一直在大量使用AWS DynamoDB作为epilot平台,主要因为𝟸原因:
- 它是无服务器的 -> 我们相信尽可能少做操作工作,但事实并非如此
- 使用 AWS 无服务器技术构建应用程序时,入门很容易。
当然,我们现在知道 DynamoDB 并不像承诺的那样简单。它的灵活性和易于上手的优势或许很有吸引力,但用 DynamoDB 作为存储解决方案来设计微服务,并不适合胆小的人😮💨。尤其如果你有SQL密集型的背景,可以灵活地查询几乎所有内容。
SELECT * FROM TABLE WHERE <insert clause here>
使用 DynamoDB,您必须以这样的方式设计您的分区键、排序键、全局和本地二级索引,以便所有搜索模式都得到最佳处理。
提前了解所有搜索模式乍一看可能听起来微不足道:
- 查找某个作者的所有书籍
- 查找在某个时间范围内出版的所有书籍
- 通过关键词查找书籍,
但在动态环境中,例如初创公司,事情可能会从一周转变为另一周🗓️,至少可以说这是一项相当具有挑战性的努力。
但现在,回到性能故事。
发生了两件重要的事情,导致 API 响应时间如此下降:
- 在事先不知道搜索模式的情况下设计表格。
- 将每个记录数据增加 10 倍。
1. 在事先不知道搜索模式的情况下设计表格
就像老人们常说的“回到过去”,当我们开始构建微服务时,开发人员意识到我们必须支持大量的搜索模式。
由于epilot是一个多租户平台,租户之间的数据清晰划分,我们决定采用 DynamoDB 作为存储解决方案,将租户 ID作为PartitionKey,将资源 ID作为SortKey。通过这个相当简单的设置,我们确信可以轻松查询资源:
- 通过 ID 查找资源:
query resources where PK=:tenantId & SK=:resourceId
- 一次查找多个资源:
batchGet resources PK=:tenantId & SK=:resourceId
- 查询租户的资源:
query resources where PK=:tenantId AND {flexible_attributes_filtering}
事实证明,这个过滤器最终给我们带来了麻烦。为什么?
首先,所有这些{flexible_attributes_filtering}都会在通过PK=:tenantId进行初始查询后由 DynamoDB 解析,大小限制为1 MB。这意味着,DynamoDB 将首先按 PartitionKey 匹配表项(最大大小限制为 1 MB),然后才会应用过滤表达式进一步筛选返回的数据。
顺便说一下,Alex Debrie 是这个家伙的忠实粉丝,你应该去看看他,他有一篇很好的文章,更深入地解释了这个陷阱。
引用那位大佬的话:“过滤表达式拯救不了你糟糕的 DynamoDB 表设计!”哦天哪,他说得对!✔️
但这并没有被证明是致命的☠️,直到它与原因 2 结合起来。
2.将每条记录的数据增加10倍
我们的资源称为工作流,存储有关每个特定启动工作流的数据(例如:名称、启动时间、上下文数据、状态、分配的用户等),但也存储一些称为任务的数据的引用,这些数据由 ElasticSearch 持久化和索引,以便进行更灵活的搜索。
{
"workflow": {
"name": "Wallbox",
"started_at": "2023-08-07T07:19:55.695Z",
"completed_at": "2023-08-07T07:19:55.695Z",
"status": "IN_PROGRESS",
"assignees": ["123", "456"],
"contexts": [{"id": "id1", "name": ""}, {"id": "id2", "name": ""}],
"tasks": [
{
"id": "id-1"
},
{
"id": "id-2"
},
{
"id": "id-3"
}
]
}
}
虽然将这些任务存储在 ElasticSearch 中,帮助我们在平台中支持相当灵活的任务概览仪表板,但企业后来决定放弃对该功能的支持,并用更好的仪表板 2.0 替换。
由于不再需要ElasticSearch ,我们决定将有关任务的完整数据迁移到 DynamoDB,以避免数据分散在 2 个存储解决方案中。
{
"workflow": {
"name": "Wallbox",
"started_at": "2023-08-07T07:19:55.695Z",
"completed_at": "2023-08-07T07:19:55.695Z",
"status": "IN_PROGRESS",
"assignees": ["123", "456"],
"contexts": [{"id": "id1", "name": ""}, {"id": "id2", "name": ""}],
"tasks": [
{
"id": "id-1",
"name": "Buy",
"started_at": "2023-08-05T07:19:55.695Z",
"completed_at": "2023-08-06T07:19:55.695Z",
"assignees": ["23"],
"dueDate" : "2023-09-12T07:19:55.695Z",
"status": "COMPLETED",
},
{
"id": "id-2",
"name": "Validate",
"started_at": "2023-08-05T07:19:55.695Z",
"assignees": ["73"],
"dueDate" : "2023-09-12T07:19:55.695Z",
"status": "IN_PROGRESS",
},
{
"id": "id-3",
"name": "Ship",
"started_at": "2023-08-05T07:19:55.695Z",
"assignees": [],
"dueDate" : "2023-09-12T07:19:55.695Z",
"status": "TO_DO",
}
]
}
}
从 ElasticSearch迁移所有任务数据,结合过滤表达式定时炸弹,导致时间显着下降:从 1-3 秒下降到平均约 20 秒。💣
解决方案
经过快速调查,发现了问题:
查询工作流,其中 PK=:tenantId且包含(#contexts、:contextId)
dbClient.query({
{
// ...
FilterExpression: "contains(#contexts, :context)",
ExpressionAttributeValues: {
":context": {
"id":"id-1"
}
}
}).promise()
问题摆在眼前,是时候实施解决方案了。但这一次,我们需要一个不会在未来对我们造成不利影响的解决方案:更好的表格设计和更完善的搜索模式支持。
在我们的案例中,这意味着在表中存储更多数据。虽然这听起来可能违反直觉,但它确实帮助 DynamoDB 更高效地解决查询。
而在原始设计中,单个工作流仅保留 1 条表记录
PK | SK | 属性 |
---|---|---|
租户ID | WF#wf1 | 姓名、状态、受让人、任务 |
租户ID | WF#wf2 | 姓名、状态、受让人、任务 |
为工作流程的每个上下文添加额外的记录,
PK | SK | 属性 |
---|---|---|
租户ID | WF#wf1 | 姓名、状态、受让人、任务 |
租户ID | CTX#ctx1 | 工作坊 |
租户ID | CTX#ctx2 | wf1 |
租户ID | CTX#ctx3 | wf1 |
租户ID | WF#wf2 | 姓名、状态、受让人、任务 |
租户ID | CTX#ctx1 | wf2 |
租户ID | CTX#ctx7 | wf2 |
帮助解决通过上下文查询查找工作流程的问题,速度提高了10-20 倍⚡️。
- 根据上下文查找工作流 ID
dbClient.query({
{
// ...
KeyConditionExpression: `PK=:tenantId AND begins_with(SK, :ctx)`,
ExpressionAttributeValues: {
':tenantId': tenantId,
':ctx': `CTX#id`
}
}).promise()
这将返回特定上下文 ID 的工作流 ID 列表。
const ids = [{PK: tenantId, SK: wfId1}, {PK: tenantId, SK: wfId2}, ...]
- 通过 ID 批量获取工作流 (*)
dbClient.batchGet({
{
RequestItems: {
[TABLE_NAME]: {
Keys: [{PK: tenantId, SK: wfId1}, {PK: tenantId, SK: wfId2}, ...]
}
}
}).promise()
(*)- 根据AWS 文档,批量获取查询必须限制为 100 条记录
结论
我们吸取了教训。我们一路走来很艰难。但无论如何,我们做到了。📝
虽然 DynamoDB 听起来在无模式灵活性方面很棒,但最终,您必须做好功课并了解您计划支持的所有搜索模式。
仅仅依靠过滤表达式来拯救你的一天是行不通的。
良好的设计是应用程序成功的关键🗝️!
文章来源:https://dev.to/epilot/how-we-improved-our-serverless-api-300x-3o27