Logo
新用户订阅 RPC,首月享 6.5 折优惠查看优惠
OnFinality Learn
RPC 故障排查阅读约 12 分钟

Sui RPC 速率限制与计算单元:在生产环境中管理成本与 429 错误

了解 Sui RPC 速率限制和计算单元计费机制,如何通过分页和成本优化查询避免 429 错误,以及如何在 Sui TypeScript SDK 中处理退避重试。

TL;DR

Sui RPC 网关根据每个方法的估算计算单元对请求进行计量,采用滚动窗口和成本头。像 queryTransactionBlocks 和 multiGetCoins 这样的重查询占主导成本。本文解释了该机制,提供了可运行的 TypeScript SDK 示例,用于分页和成本优化,并讨论了使用退避处理 429 错误,以及向 GraphQL 的演进转变。

直接回答:关于 Sui RPC 速率限制你需要了解什么

Sui RPC 速率限制不仅仅是每秒固定请求数。Sui 网关为每个方法估算计算单元成本,您的使用量会按照滚动窗口进行计量。像 queryTransactionBlocksmultiGetCoins 这样的重方法比 getObject 这样的轻量调用消耗更多的单元。当您超过分配的单元时,您会收到 HTTP 429 响应。为了管理成本并避免生产环境中的 429 错误,您必须优化查询:使用分页,避免深度垂直分页,并优先使用 GraphQL,因为它正成为推荐的接口。

本文解释了计算单元计费机制,展示了如何使用 Sui TypeScript SDK 编写成本高效的查询,并提供了处理 429 错误的退避策略。我们还涵盖了 Sui 基金会公布的公共 RPC 速率限制变更,以及从传统 JSON-RPC 到 GraphQL 的持续过渡。

Sui RPC 计算单元如何工作

Sui 网关为每个 RPC 方法分配一个估算的计算单元成本。此估算反映了满足请求所需的服务器端工作量。例如,queryTransactionBlocks 使用较大的页面大小可能会扫描许多交易,而 multiGetCoins 在一次调用中获取多个币对象。确切的单元值没有在固定表中公开记录,但 Sui 关于 RPC 最佳实践的文档指出某些方法更昂贵,并建议使用分页来限制负载。

网关使用滚动窗口强制执行速率限制。不是简单的每秒计数器,而是为您分配一个在滑动时间窗口(例如,每分钟或每小时)内的计算单元预算。每个请求从您当前的预算中扣除其估算成本。当预算耗尽时,网关返回 429,并带有 Retry-After 头或类似指示。响应头包括 x-sui-rpc-units(或类似)以显示请求的成本,使您能够监控消耗。

需要注意的是,这些单元值是估算值,不是精确测量值。实际负载可能因网络状态和返回数据的大小而异。因此,将单元值视为优化查询的指南,而不是精确的计费表。

  • 重方法:queryTransactionBlocks, queryEvents, multiGetTransactionBlocks, multiGetCoins
  • 轻方法:getObject, getBalance, getChainIdentifier
  • 滚动窗口:滑动时间段内的计算单元预算
  • 头:x-sui-rpc-units(或类似)指示每个请求的成本

分页:垂直与水平

分页是控制计算单元消耗的主要工具。像 queryTransactionBlocksqueryEvents 这样的 Sui RPC 方法返回一个 nextCursor,您可以使用它来获取下一页。有两种分页策略:垂直和水平。

垂直分页意味着通过设置高 limit(例如,1000)在单个请求中获取大量项目。这在往返次数方面效率高,但在计算单元方面可能昂贵,因为网关必须处理和序列化大型负载。水平分页意味着使用较小的 limit(例如,50)并发出多个请求来分页浏览数据。这将负载分散到多个请求中,每个请求的单元成本较低,并且通常被 Sui 文档推荐以避免超时和速率限制。

Sui 关于 RPC 最佳实践的文档明确建议使用分页并避免大的页面大小。例如,在查询交易块时,使用 50 或更小的 limit,并始终跟随 nextCursor 直到它返回 null。这种方法降低了每个请求的峰值计算单元消耗,使您的使用更加可预测。

可运行示例:使用 Sui TypeScript SDK 进行成本优化查询

下面是一个使用 Sui TypeScript SDK 的自包含示例。它演示了如何使用水平分页查询交易块,如何使用 multiGetCoins 获取多个币,以及如何使用指数退避处理 429 错误。该示例使用公共 Sui 测试网端点,但您可以将其替换为您自己的 RPC URL,来自 OnFinality 的 Sui 网络页面

代码首先创建一个 SuiClient,带有一个自定义的 fetch 包装器,用于拦截 429 响应并使用退避重试。然后它定义了一个函数,以每页 50 条的方式查询交易块,打印摘要和可用的计算单元头。最后,它演示了使用币对象 ID 列表的 multiGetCoins

import { SuiClient, getFullnodeUrl } from '@mysten/sui/client';

// Custom fetch wrapper to handle 429 with exponential backoff
async function fetchWithRetry(url: string, options: any, retries = 3): Promise<Response> {
  let attempt = 0;
  while (attempt <= retries) {
    const response = await fetch(url, options);
    if (response.status === 429 && attempt < retries) {
      const retryAfter = response.headers.get('retry-after');
      const delayMs = retryAfter ? parseInt(retryAfter) * 1000 : Math.pow(2, attempt) * 1000;
      console.log(`429 received, retrying in ${delayMs}ms`);
      await new Promise(resolve => setTimeout(resolve, delayMs));
      attempt++;
      continue;
    }
    return response;
  }
  throw new Error('Exhausted retries due to 429');
}

// Create a SuiClient with the custom fetch
const client = new SuiClient({
  url: getFullnodeUrl('testnet'), // Replace with your own RPC URL
  fetch: fetchWithRetry as any,
});

// Horizontal pagination for queryTransactionBlocks
async function queryAllTransactionBlocks() {
  let cursor: string | null = null;
  let hasNextPage = true;
  while (hasNextPage) {
    const page = await client.queryTransactionBlocks({
      cursor,
      limit: 50, // small page size to control compute units
      order: 'descending',
    });
    console.log(`Fetched ${page.data.length} blocks, nextCursor: ${page.hasNextPage ? page.nextCursor : 'null'}`);
    // Process each block digest
    for (const block of page.data) {
      console.log(block.digest);
    }
    // Check for next page
    if (page.hasNextPage && page.nextCursor) {
      cursor = page.nextCursor;
    } else {
      hasNextPage = false;
    }
  }
}

// Example of multiGetCoins (heavy method)
async function getMultipleCoins(coinIds: string[]) {
  const coins = await client.multiGetCoins({ ids: coinIds });
  console.log(`Fetched ${coins.data.length} coins`);
  for (const coin of coins.data) {
    console.log(coin.coinObjectId, coin.balance);
  }
}

// Run the example
async function main() {
  await queryAllTransactionBlocks();
  // Replace with actual coin IDs
  await getMultipleCoins(['0x...', '0x...']);
}

main().catch(console.error);

预期结果与验证方法

当您运行该示例时,您应该看到一系列日志行,显示每页交易块。queryTransactionBlocks 调用将返回一个 nextCursor,直到所有页面耗尽。multiGetCoins 调用将返回提供的 ID 的币对象。

要验证您的查询是否成本高效,请检查响应头。Sui 网关包含一个像 x-sui-rpc-units 这样的头,指示请求消耗的计算单元。您可以在 fetch 包装器中记录此头以监控您的使用情况。例如,修改 fetchWithRetry 函数,为每个成功响应打印 response.headers.get('x-sui-rpc-units')

另外,检查 429 响应上的 Retry-After 头。网关可能提供建议的等待时间。我们的退避逻辑使用该头(如果存在),否则回退到指数退避(1 秒、2 秒、4 秒)。这是处理 HTTP API 速率限制的文档化模式。

常见故障与修复

一个常见的故障是因为在 queryTransactionBlocks 中使用了大的 limit(例如,1000)而遇到 429 错误。修复方法是将 limit 减少到 50 或更小,并使用水平分页。另一个故障是没有正确跟随 nextCursor,导致无限循环或数据丢失。始终检查 hasNextPage,并且仅在不为 null 时更新游标。

另一个问题是使用 multiGetCoins 时传入非常大的币 ID 数组。此方法很重,因为它一次获取多个对象。如果您有很多币,请考虑将 ID 分批(例如,每次调用 50 个)以降低每个请求的计算单元成本。

最后,如果您使用传统的 JSON-RPC 接口,您可能会遇到比 GraphQL 接口更严格的速率限制。Sui 基金会已宣布公共 RPC 速率限制的变更,建议迁移到 GraphQL 以获得更好的效率和更低的成本。有关详细信息,请参阅 Sui 论坛公告(注意:这是公告记录,不是基准测试)。

权衡与限制

水平分页减少了每个请求的计算单元消耗,但增加了请求数量,如果您分页浏览大型数据集,这仍然可能导致更高的总单元消耗。在请求数量和每个请求的成本之间存在权衡。您应该测试不同的页面大小,以找到适合您工作负载的最佳点。

计算单元值是估算值,可能随着 Sui 网络的发展而变化。Sui 文档是当前最佳实践的权威来源,但它没有发布固定的单元成本表。因此,您应该通过 x-sui-rpc-units 头监控您的实际使用情况,并相应调整查询。

从 JSON-RPC 到 GraphQL 的过渡正在进行中。虽然 GraphQL 更灵活且通常更高效,但它有不同的查询语法,需要学习曲线。Sui 文档提供了迁移指南,但您应该在切换生产环境之前彻底测试您的查询。

后续步骤与进一步阅读

要充分利用 Sui RPC,请首先查看 Sui RPC 最佳实践 官方文档。然后,探索 OnFinality Sui RPC 指南 获取特定于提供商的提示。如果您管理多个端点,请考虑使用 OnFinality 的 RPC 监控和故障转移 以确保高可用性。

对于生产工作负载,您可能希望使用专用的 RPC 服务,如 OnFinality 的 API 服务,以获得更高的速率限制和专用资源。查看 定价页面 了解选项。此外,通过关注 Sui 开发者论坛 了解最新的 Sui RPC 变更。

最后,随着生态系统向 GraphQL 发展,开始尝试 Sui GraphQL 接口。Sui GraphQL 文档 提供了示例和迁移指南。通过采用这些实践,您可以有效管理成本并避免 429 错误。

永远不用担心基础设施

OnFinality 消除了 DevOps 的繁重工作,让您能够更聪明、更快地构建。

开始