Polkadot RPC 超时是指通过 WebSocket 或 HTTP 向节点发出的请求在客户端的时间限制内未收到响应。本文解释了 Substrate 和 polkadot.js 中不同的超时行为、WsProvider 静默断开的原因,以及如何使用显式超时、重连处理和批处理构建弹性查询。
直接回答:什么导致 Polkadot RPC 超时?
Polkadot RPC 超时是指通过 WebSocket (wss://) 或 HTTP (https://) 向节点发出的请求在客户端的时间限制(通常为 60 秒)内未收到响应。在 Polkadot 上,这通常不是网络问题,而是节点繁忙、请求过重或客户端未正确处理重连的结果。与通用 HTTP 超时不同,Substrate 的 RPC 层具有特定行为:WebSocket 订阅可能在没有错误的情况下变得陈旧,而繁重的状态查询可能超过公共端点的时限。
最常见的情况是使用 WsProvider 的 polkadot.js ApiPromise 静默断开且永不重连,导致应用程序挂起。这是因为默认的 WsProvider 在超时时不会抛出异常;它只是发出一个 'disconnected' 事件,许多开发人员会忽略该事件。理解这些机制是构建可靠查询的第一步。
- HTTP RPC 超时很简单:如果在超时时间内没有响应,请求就会失败。
- WebSocket 订阅可能会在没有错误的情况下变得陈旧,尤其是在节点重启或连接断开时。
- 诸如 state_getPairs 或 state_getKeysPaged 之类的繁重调用可能会因速率限制或资源限制而在公共端点上超时。
- 当客户端等待的最终区块落后于最佳区块时,会发生区块完成超时。
Substrate 和 polkadot.js 处理超时的不同方式
Substrate 的 RPC 层构建在基于 HTTP 或 WebSocket 的 JSON-RPC 之上。对于 HTTP,服务器有一个默认超时(通常为 60 秒),超过该时间后返回错误。对于 WebSocket,连接是持久的,请求通过 ID 匹配。如果请求耗时过长,服务器可能不会响应,客户端的超时机制就会启动。
polkadot.js 的 WsProvider 对单个 RPC 调用内置了 60 秒的超时。当超时发生时,默认情况下它不会抛出错误;相反,它会发出一个 'disconnected' 事件并尝试重新连接。但是,如果连接丢失,提供程序可能不会自动重新订阅现有的订阅,从而导致静默陈旧。这是 polkadot.js API 文档 中记录的行为。
HTTP 和 WebSocket 超时之间的区别至关重要:HTTP 请求是一次性的,因此超时是明确的失败。WebSocket 订阅是长期存在的,因此订阅超时意味着订阅已失效,但客户端可能直到尝试使用它时才知道。
- HTTP:如果在超时时间内没有响应,请求将失败并返回错误。
- WebSocket:请求可能会挂起,并且提供程序可能会在不抛出异常的情况下断开连接。
- 订阅:chain_newHead 和 chain_subscribeFinalizedHeads 在重新连接后可能会变得陈旧。
- polkadot.js 默认超时为 60 秒,但可以配置。
为什么 WsProvider 会静默断开(以及如何捕获)
Substrate Stack Exchange 问题“为什么 WsProvider 超时错误没有被 try-catch 捕获”强调了一个常见的陷阱:将 RPC 调用包装在 try-catch 中不会捕获超时,因为错误是作为事件发出的,而不是抛出的。当连接丢失时,WsProvider 会发出 'disconnected' 事件,对于协议错误会发出 'error' 事件。如果你不监听这些事件,你的应用程序将会挂起。
要处理这个问题,你必须为提供程序附加事件监听器并实现重连策略。提供程序具有内置的自动重连,但它不会重新订阅以前的订阅。你需要在重连后手动重新订阅。
以下是一个如何监听断开和错误的最小示例:
- 监听提供程序上的 'disconnected' 和 'error' 事件。
- 使用标志来跟踪连接状态。
- 重连后,重新订阅所有活动订阅。
- 考虑使用像 @polkadot/api-contract 这样的库,它在内部处理重连。
const { ApiPromise, WsProvider } = require('@polkadot/api');
const provider = new WsProvider('wss://rpc.polkadot.io');
provider.on('disconnected', () => {
console.log('Provider disconnected');
// 设置标志以触发重新订阅
});
provider.on('error', (err) => {
console.error('Provider error:', err);
});
const api = await ApiPromise.create({ provider });
// ... 你的代码区块 RPC 限制:最终区块与最佳区块滞后
Polkadot 节点公开两个与区块相关的 RPC:chain_getHeader(最佳区块)和 chain_getFinalizedHead(最终区块)。由于最终性,最终区块落后于最佳区块几个区块。如果你的应用程序等待的最终区块远远落后,如果节点正在同步或网络拥塞,你可能会遇到超时。
公共端点通常对区块 RPC 有速率限制以防止滥用。例如,提供程序可能会限制每秒的请求数。如果超过此限制,你可能会收到超时或错误。这是许多提供程序的记录行为,但具体限制各不相同。例如,OnFinality 的 API 服务 提供具有更高限制的专用端点。
为避免超时,请根据你的用例使用适当的 RPC:如果你需要最新状态,请使用最佳区块;如果你需要最终性,请使用最终区块。此外,考虑使用订阅而不是轮询以减少请求负载。
- chain_getHeader 返回最佳区块头。
- chain_getFinalizedHead 返回最后一个最终区块头。
- 最终区块落后于最佳区块几个区块。
- 公共端点可能会对区块 RPC 进行速率限制;请查看提供程序文档。
繁重的 RPC 调用:state_getPairs 和大规模存档扫描
像 state_getPairs 或 state_getKeysPaged 这样的调用可能非常繁重,因为它们会遍历整个存储。在像 Polkadot 这样的大型链上,这可能需要几分钟,并且几乎肯定会在公共端点上超时。即使在专用节点上,此类调用也可能阻塞 RPC 线程并导致其他请求超时。
推荐的方法是使用存储 RPC(state_getStorage)进行特定键的轻量读取,并使用批处理进行多次读取。对于遍历存储,使用具有较小页面大小的 state_getKeysPaged 并增量处理页面,但要注意速率限制。
另一种技术是使用 state_queryStorage 在特定区块查询历史存储,这可能比扫描整个状态更有效。
- state_getPairs 和 state_getKeysPaged 很繁重,可能会超时。
- 使用 state_getStorage 进行单键读取。
- 使用带分页的 state_getKeysPaged 进行大规模扫描。
- 考虑使用 state_queryStorage 进行历史查询。
- 使用 JSON-RPC 批处理或 api.rpc.batch 批处理多个读取。
可运行示例:超时、重连和重新订阅循环
以下 Node.js 脚本演示了一个健壮的模式:它创建一个具有自定义超时的 ApiPromise,监听断开事件,并在重连后重新订阅 chain_newHead。它还展示了如何使用 Promise.race 处理单个调用的超时。
要运行它,请安装 @polkadot/api 和 @polkadot/util-crypto,然后使用 Node.js 执行。该脚本将记录新的区块头并优雅地处理重连。
- 安装依赖:npm install @polkadot/api @polkadot/util-crypto
- 使用以下命令运行:node script.js
- 预期输出:记录新的区块号和重连消息。
const { ApiPromise, WsProvider } = require('@polkadot/api');
const WS_URL = 'wss://rpc.polkadot.io';
const TIMEOUT = 60000; // 60 秒
async function main() {
const provider = new WsProvider(WS_URL, false); // autoConnect false 以手动控制
provider.on('disconnected', () => {
console.log('Disconnected, attempting reconnect...');
// 提供程序自动重连,但我们需要重新订阅
});
provider.on('error', (err) => {
console.error('Provider error:', err);
});
const api = await ApiPromise.create({ provider, timeout: TIMEOUT });
await provider.connect();
let unsubscribe;
const subscribe = async () => {
if (unsubscribe) await unsubscribe();
unsubscribe = await api.rpc.chain.subscribeNewHeads((header) => {
console.log(`New block #${header.number}`);
});
};
await subscribe();
// 繁重调用超时的示例
const heavyCall = api.rpc.state.getPairs('0x');
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), TIMEOUT));
try {
const result = await Promise.race([heavyCall, timeoutPromise]);
console.log('Heavy call result length:', result.length);
} catch (err) {
console.error('Heavy call failed:', err.message);
}
// 保持进程存活
process.on('SIGINT', async () => {
if (unsubscribe) await unsubscribe();
await api.disconnect();
process.exit(0);
});
}
main().catch(console.error);常见故障和修复
paritytech/polkadot-sdk 问题“[rpc] Polkadot assethub rpc's randomly stop providing...”描述了 Asset Hub 上的 RPC 端点间歇性停止响应的场景。这通常是由于节点资源耗尽或网络问题。修复涉及监控节点健康状况、实现重试和使用多个端点。
另一个常见故障是来自 Substrate Stack Exchange 的“运行 RPC 调用时出错”,这通常发生在调用超过服务器超时时。修复方法是将调用分解为更小的块或使用不同的 RPC 方法。
对于 WebSocket 订阅,一个常见故障是重连后订阅未收到更新。修复方法是在 'connected' 事件上重新订阅,如示例所示。
- 间歇性 RPC 停止:使用多个端点和故障转移。
- 繁重调用超时:使用分页或更轻量的 RPC。
- 订阅陈旧:在重连时重新订阅。
- 速率限制:遵守提供程序限制并使用批处理。
- 节点同步问题:检查节点健康状况并使用可靠的提供程序。
权衡和限制
虽然设置自定义超时和实现重连逻辑可以提高可靠性,但它增加了复杂性。较短的超时可能会导致慢速网络上的误报,而较长的超时可能会延迟错误检测。最佳超时取决于你的用例和网络条件。
批处理请求减少了往返次数,但可能会增加节点上的负载。公共端点可能对批处理大小有限制。始终检查提供程序的文档以了解具体限制。
使用专用节点或像 OnFinality 的 API 服务 这样的高级 RPC 服务可以提供更一致的性能和更高的速率限制,但需要付出成本。权衡在于可靠性和费用之间。
- 自定义超时:在误报和延迟错误之间取得平衡。
- 批处理:减少往返次数,但可能达到批处理限制。
- 公共端点与高级端点:可靠性 vs 成本。
- WebSocket 与 HTTP:WebSocket 更适合订阅,HTTP 适合一次性查询。
后续步骤:构建可靠的 Polkadot 查询
要深入了解,请探索 Polkadot WebSocket RPC 教程 了解订阅模式,并查看 Polkadot RPC 端点 获取公共和高级端点列表。如果你正在构建生产应用程序,请考虑使用 OnFinality 的 API 服务 获取专用端点,以及 RPC 定价 获取经济高效的方案。
请记住始终在真实网络条件下测试你的超时和重连逻辑。使用 OnFinality Learn 中心 获取更多关于 Polkadot 和其他链的教程。要更全面地了解 Polkadot,请参阅 Polkadot 网络概述。
- 查看 Polkadot WebSocket RPC 教程 了解订阅最佳实践。
- 在 Polkadot RPC 端点 页面上比较端点。
- 考虑通过 API 服务 使用专用端点。
- 查看 RPC 定价 获取经济高效的选项。
- 在 OnFinality Learn 中心 探索更多内容。