AWS
内部原理
ISR

在独立模式下,Next.js 会在构建过程中预生成 ISR 缓存。运行时,NextServer 期望这些缓存在服务器本地可用。当服务器运行在单台 Web 服务器机器上时,这种方式能有效地在所有请求间共享缓存。但在 Lambda 环境中,缓存需要集中存储在所有服务器 Lambda 函数实例都能访问的位置,而 S3 正是作为这个集中存储位置。

为实现这一机制:

  • ISR 缓存文件会被排除在 server-function 打包文件之外,转而上传到缓存桶中
  • 通过在 next.config.js 中配置 incrementalCacheHandlerPath (opens in a new tab) 字段,替换默认的缓存处理器为自定义缓存处理器
  • 自定义缓存处理器负责管理 S3 上的缓存文件,处理读写操作
  • 由于我们使用 FIFO 队列,如果需要同时处理多个重新验证请求,就需要不同的消息组 ID。我们基于路由路径为每个重新验证请求生成消息组 ID,确保同一路由的重新验证请求只会被处理一次。您可以通过 MAX_REVALIDATE_CONCURRENCY 环境变量控制同时处理的重新验证请求数量,默认值为 10
  • revalidation-function 会从队列中轮询消息,并向路由发送带有 x-prerender-revalidate 请求头的 HEAD 请求
  • server-function 接收到 HEAD 请求后会重新验证缓存
  • 标签(tags)在 DynamoDB 表中采用不同的处理方式。我们使用单独的表来存储每个路由的标签,自定义缓存处理器在更新缓存时会同步更新表中的标签

ISR(增量静态再生)请求的生命周期(针对过期页面)

  1. Cloudfront 接收到一个页面请求。假设该页面在 Cloudfront 中已过期。
  2. Cloudfront 在后台将请求转发给 server-function,但仍返回缓存的版本。
  3. server-function 检查 S3 缓存。如果页面已过期,它会将过期响应返回给 Cloudfront,同时向重新验证队列发送消息以触发后台重新验证。它还会将 cache-control 标头更改为 s-maxage=2, stale-while-revalidate=2592000
  4. 2 秒后,同一个页面收到新的请求。Cloudfront 将缓存版本返回给用户,并将请求转发给 server-function
  5. 如果重新验证已完成,server-function 将更新缓存并将更新后的响应发送回 Cloudfront。后续请求将获得更新后的版本。否则,我们将回到第 3 步。

标签(Tags)

标签存储在 dynamodb 表中。 表中有 3 个字段:tagpathrevalidatedAttag 字段是分区键,path 是排序键。

我们使用名为 revalidate 的索引,其中 path 作为分区键,revalidatedAt 作为排序键。

每个标签都有多个路径,每个子路径也被视为一个标签。例如,如果我们有一个标签 tag1,其路径为 /a/b/c,那么我们还有标签 /a/a/layout/a/page/a/b/a/b/layout/a/b/page/a/b/c/layout/a/b/c/page

当调用 revalidateTag 时,我们会更新与该标签关联的每个路径和子路径的 revalidatedAt 值。

当我们检查页面是否过期时,我们会检查每条记录的 revalidatedAt 值和 S3 缓存对象的 LastModified。如果 revalidatedAt 大于 LastModified,我们认为该页面已过期。

成本说明

⚠️

请注意 fetch 缓存使用的是 S3 服务。Next.js 中默认情况下 fetch 会被缓存,即使是 SSR 请求也会写入 S3。这可能导致大量 S3 请求并产生较高费用。您可以通过在 fetch 选项中设置 cacheno-store 来禁用 fetch 缓存。另请参阅此解决方案

对于未被 Cloudfront 缓存的 ISR 和 SSG 请求,每次请求都会调用 get 方法,而每次重新验证时都会调用 set 方法。如果 fetch 请求未将 cache 选项设为 no-store,这些方法也可能被调用。

部署时也会产生一些成本,因为需要将缓存上传到 S3 并将标签上传到 DynamoDB。

以下示例假设我们在 us-east-1 区域有一个重新验证间隔为 5 分钟的应用路由,且该路由持续有流量访问(如果没有流量,则只需支付存储费用)。

S3 成本
  • 每次缓存 get 请求至少会产生 1 次 GetObject 操作
  GetObject 成本 - 8,640 次请求 * 每 1,000 次请求 $0.0004 = $0.003456
  总成本 - 每月每条路由 $0.003456
  • 每次缓存 set 请求会产生 1 次 S3 PutObject 操作
  PutObject 成本 - 8,640 次请求 * 每 1,000 次请求 $0.005 = $0.0432
  总成本 - 每月每条路由 $0.0432

您可以根据实际使用情况和 S3 定价 (opens in a new tab) 计算具体成本。

DynamoDB 成本分析

以下示例分析假设某路由包含 2 个标签,每个标签有 10 条路径和子路径,且该路由有持续稳定的访问量。

  • 每次 revalidateTag 请求会产生:
    • 1 次 DynamoDB Query 操作
    • 每个关联路径执行 1 次 PutItem 操作(通过 BatchWriteItem 批量写入,每批最多 25 条)
  假设每 5 分钟执行 1 次重新验证:
  Query 成本 - 8,640 次请求 * 每百万次读取 $0.25 = $0.00216
  BatchWriteItem 成本 - 86,400 次请求 * 每百万次写入 $0.25 = $0.0216
  每月总成本 - 每个标签重新验证 $0.04536
  • 每次 get 请求会产生 1 次 DynamoDB Query 操作
  Query 成本 - 8,640 次请求 * 每百万次读取 $0.25 = $0.00216
  每月总成本 - 每个路由 $0.00216
  • 每次 set 请求会产生:
    • 1 次 DynamoDB Query 操作
    • 为路径关联的每个未存在于 DynamoDB 的标签执行 1 次 PutItem 操作(通过 BatchWriteItem 批量写入,每批最多 25 条)
  Query 成本 - 8,640 次请求 * 每百万次读取 $0.25 = $0.00216
  每月总成本 - 每个路由 $0.00216

您可以根据实际使用情况和 DynamoDB 定价 (opens in a new tab) 计算总成本。