我们如何将无服务器 API 提升 300 倍

2025-06-05

我们如何将无服务器 API 提升 300 倍

这是一个关于我们如何让微服务响应速度提高 3️⃣0️⃣0️⃣ 倍的故事。是的,你没看错:快了 3️⃣0️⃣0️⃣ 倍!🤯

性能改进

我们将响应时间从平均约 20 秒缩短至约 60 毫秒。为了不欺骗您的眼睛,我使用相同的比例:20,000 毫秒60 毫秒

“等等”,你可能会说,“你们把加载时间缩短了300倍,这真是让我印象深刻,但你们到底做了什么,导致响应时间这么糟糕?”

违背承诺还是不切实际的期望?

我们一直在大量使用AWS DynamoDB作为epilot平台,主要因为𝟸原因

  1. 它是无服务器的 -> 我们相信尽可能做操作工作,但事实并非如此
  2. 使用 AWS 无服务器技术构建应用程序时,入门很容易。

当然,我们现在知道 DynamoDB 并不像承诺的那样简单。它的灵活性和易于上手的优势或许很有吸引力,但用 DynamoDB 作为存储解决方案来设计微服务,并不适合胆小的人😮‍💨。尤其如果你有SQL密集型的背景,可以灵活地查询几乎所有内容。

   SELECT * FROM TABLE WHERE <insert clause here> 
Enter fullscreen mode Exit fullscreen mode

使用 DynamoDB,您必须以这样的方式设计您的分区键排序键全局本地二级索引,以便所有搜索模式都得到最佳处理。

提前了解所有搜索模式乍一看可能听起来微不足道:

  • 查找某个作者的所有书籍
  • 查找在某个时间范围内出版的所有书籍
  • 通过关键词查找书籍,

但在动态环境中,例如初创公司,事情可能会从一周转变为另一周🗓️,至少可以说这是一项相当具有挑战性的努力。

但现在,回到性能故事。

发生了两件重要的事情,导致 API 响应时间如此下降:

  1. 在事先不知道搜索模式的情况下设计表格。
  2. 将每个记录数据增加 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}

事实证明,这个过滤器最终给我们带来了麻烦。为什么

DynamoDB 1 MB 限制

首先,所有这些{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"
    }
]
 }
}
Enter fullscreen mode Exit fullscreen mode

虽然将这些任务存储在 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",
    }
  ]
 }
}
Enter fullscreen mode Exit fullscreen mode

从 ElasticSearch迁移所有任务数据,结合过滤表达式定时炸弹,导致时间显着下降:从 1-3 秒下降到平均约 20 秒。💣

工作流程持续增加

解决方案

经过快速调查,发现了问题:

查询工作流,其中 PK=:tenantId且包含(#contexts、:contextId)

dbClient.query({
  {
    // ...
    FilterExpression: "contains(#contexts, :context)",
    ExpressionAttributeValues: {
      ":context": {
          "id":"id-1"
      }
   }
}).promise()
Enter fullscreen mode Exit fullscreen mode

问题摆在眼前,是时候实施解决方案了。但这一次,我们需要一个不会在未来对我们造成不利影响的解决方案:更好的表格设计和更完善的搜索模式支持。

在我们的案例中,这意味着在表中存储更多数据。虽然这听起来可能违反直觉,但它确实帮助 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 倍⚡️。

  1. 根据上下文查找工作流 ID
dbClient.query({
  {
    // ...
    KeyConditionExpression: `PK=:tenantId AND begins_with(SK, :ctx)`,
    ExpressionAttributeValues: {
      ':tenantId': tenantId,
      ':ctx': `CTX#id`
    }
}).promise()
Enter fullscreen mode Exit fullscreen mode

这将返回特定上下文 ID 的工作流 ID 列表。

  const ids = [{PK: tenantId, SK: wfId1}, {PK: tenantId, SK: wfId2}, ...]
Enter fullscreen mode Exit fullscreen mode
  1. 通过 ID 批量获取工作流 (*)
dbClient.batchGet({
  {
     RequestItems: {
        [TABLE_NAME]: {
          Keys: [{PK: tenantId, SK: wfId1}, {PK: tenantId, SK: wfId2}, ...]
        }
      }
}).promise()

Enter fullscreen mode Exit fullscreen mode

(*)- 根据AWS 文档,批量获取查询必须限制为 100 条记录

结论

我们吸取了教训。我们一路走来很艰难。但无论如何,我们做到了。📝

虽然 DynamoDB 听起来在无模式灵活性方面很棒,但最终,您必须做好功课并了解您计划支持的所有搜索模式。

仅仅依靠过滤表达式来拯救你的一天是行不通的。

良好的设计是应用程序成功的关键🗝️!

文章来源:https://dev.to/epilot/how-we-improved-our-serverless-api-300x-3o27
PREV
您的代码听起来怎么样?
NEXT
Git 命令介绍