本文深入探讨了如何评估和比较区块链RPC提供商的性能,强调了数据新鲜度的重要性,并提供了一个测试套件,用于衡量响应时间、区块高度和计算新鲜度得分。通过分析实际数据,文章揭示了仅仅依靠区块高度来判断RPC提供商的优劣可能会产生误导,而应该综合考虑响应速度和数据捕获时间,以确保应用程序能够及时获取最新的区块链状态。
如果你一直在构建与区块链交互的应用程序,你可能想知道哪个RPC提供商能给你带来最佳性能。在本指南中,我们将演示如何测试RPC提供商,并介绍一些基本概念,以便更好地理解RPC请求。在本指南结束时,你将能够在不同的RPC提供商上运行实验,以了解哪个提供商提供最佳性能,并更好地理解如何比较不同的提供商。
如果你想使用现有工具比较RPC提供商,请查看我们的比较页面,该页面将从你的浏览器进行实时基准测试,并提供实时结果。
你正在为你的应用程序比较RPC提供商。一个提供商始终如一地返回比竞争对手提前3-4个区块的区块高度。另一个提供商在50ms内响应,而“更好”的那个需要400ms。你应该选择哪一个?
如果你选择了当时返回较高区块号的提供商,你将犯一个错误。表面上看起来能给你最新数据的提供商,实际上可能为你提供的对你的应用程序来说不太有用的信息。
这并不明显。它需要理解RPC请求期间实际发生的情况,以及该时序如何影响你收到的数据。让我们调查一下为什么在这种情况下,“较慢”的提供商提供了更有用的数据。
查看我们的比较页面,直接从你的浏览器对提供商进行基准测试。
在评估RPC提供商时,有两个指标在进行性能比较时可能很有用:
这两个指标都很重要。问题是当它们冲突时。
让我们看一下从两个Arbitrum RPC提供商那里获取的真实数据,测试时间为一个小时。在Arbitrum生成约14,400个区块期间,我们向每个提供商发出了约60个并发请求。
Arbitrum大约每250毫秒创建一个新区块,大约每秒4次。记住这个数字。

在整个测试过程中,绿色持续返回比黄色提前2-4个区块的区块号。如果我们将分析停在这里,绿色看起来显然是赢家。
现在,让我们看一下每个提供商的响应时间。

绿色提供商给出了最新的区块数据,但其响应时间很慢且不一致(有时很快),有时需要长达6秒。黄色提供商不是最新的,但它更快,通常在200-400ms左右。
根据你的应用程序的需求,你可能会选择不同的方式。如果获取最新数据最重要,你可能仍然更喜欢黄色提供商。
这些结果显示了p95响应时间,这意味着只有最慢的5%的请求才那么慢。让我们接下来检查中位数时间,以查看典型速度。

黄色在中位数时以40-50ms的速度提供响应。绿色需要200-400ms。这是5-10倍的速度差异。
现在我们有冲突的信号:
那么,哪个提供商实际上给你更新鲜的数据?我们很快就会发现。
当你的应用程序发出RPC请求时,在你收到数据之前会发生几个步骤:
这里的见解是:区块链状态由提供商在步骤4期间捕获,在TLS握手完成后,但在第一个字节发送回给你之前。 你收到的状态反映了在那一特定时刻的区块链,而不是你发出请求或完成下载响应的时间。
现在让我们看一下两个提供商的请求生命周期中每个步骤所花费的时间分解。绿色提供商在左侧,黄色在右侧:

请注意,请求生命周期从下到上流动:DNS → TCP → TLS → 首字节 → 下载时间。这些加起来就是总响应时间。
看看发生了什么,绿色提供商仅完成TLS握手就需要大约200ms,甚至在它开始寻找当前区块高度之前。同时,黄色提供商在不到100ms的时间内完成其整个请求。
这意味着:
让我们放大以更清楚地看到这一点:

这揭示了一个悖论:尽管绿色返回了更高的区块号,但它并没有准确地表示你发出请求时的区块链状态。当数据到达你时,相对于你请求时,它已经过时了。
这揭示了绿色方法的三个问题。首先,它的滞后意味着如果你的应用程序需要来自区块N的信息,但绿色从N-1跳到N+2,你将丢失该数据。其次,即使区块号更高,数据在你收到时可能已经过时。第三,你的应用程序假设数据代表“现在”,但网络已经继续前进。
通过相对于请求时间更快地传递数据,黄色提供的数据对实时应用程序更有用。该数据更准确地反映了你请求时的网络状态。
这使我们想到了数据鲜度的概念,这是一个捕捉评估RPC提供商时真正重要的指标。
数据鲜度是指RPC提供商提供新区块链状态的速度。它不仅仅是获得最高的区块号,也不仅仅是快速的响应时间。而是指以下各项之间的关系:
这对于像Arbitrum、Solana和Base这样的高吞吐量区块链至关重要,在这些区块链中,区块产生得非常快。当区块时间为250毫秒但你的响应时间为500毫秒时,你从根本上无法实时跟上区块链。在获得一个响应所花费的时间内,已经创建了两个区块,这意味着你不断落后并丢失数据。
为了量化数据鲜度,我们可以创建一个“延迟鲜度分数”,将响应时间与区块链的平均区块时间进行比较:
Freshness Score = 1 - (latency / avgBlockTime)
该公式为你提供了一个介于 1.0(即时响应,例如,理论最大值)和负值(比区块生产慢)之间的标准化分数。得分为 0 意味着你的响应时间与平均区块时间完全匹配。
这在实践中意味着什么?如果你的得分高于 0,则你获取区块的速度快于它们产生的速度,并且你有时间在下一个区块到达之前处理每个区块。得分越高,你拥有的喘息空间就越多。精确到 0,你几乎没有跟上,没有多余的时间。低于 0,你从根本上落后于区块链,产生区块的速度快于你检索它们的速度。
现在让我们使用中位数延迟将此鲜度分数应用于我们的绿色与黄色比较:

差异是:
当我们使用 p95 延迟(考虑较慢的异常请求)计算鲜度分数时,情况会变得更加清晰:

两个提供商在 p95 时的表现都较差,但绿色的负分更为明显。即使对于最慢的请求,黄色也会降至 -1 以下,这表明即使是更快的提供商也可能在网络拥塞或高负载期间遇到困难。
结论:黄色返回较低的区块号,正是因为它可以更快地提供数据并更早地捕获请求生命周期中的区块链状态。同时,绿色返回较高的区块号,因为它需要很长时间才能完成连接设置(TLS 握手等),以至于当它最终捕获区块链状态时,已经产生了更多区块。尽管区块号较高,但对于需要实时数据的应用程序而言,黄色客观上是更好的提供商。它提供的数据准确地代表了你请求时的区块链状态,并且速度足够快,可以使你的应用程序跟上区块生产。
现在让我们构建一个工具来测试和比较你自己的RPC提供商。我们将创建一个脚本来测量响应时间、区块高度并计算鲜度分数。
首先,创建一个新的Node.js项目并安装所需的依赖项:
mkdir rpc-comparison
cd rpc-comparison
npm init -y
npm install ethers
创建一个名为 compare-rpc.js 的新文件:
const { ethers } = require('ethers');
// Configuration
const PROVIDERS = {
provider1: 'YOUR_FIRST_RPC_URL',
provider2: 'YOUR_SECOND_RPC_URL'
};
const CHAIN_BLOCK_TIME = 250; // milliseconds (adjust for your chain)
const TEST_DURATION = 60; // minutes
const REQUEST_INTERVAL = 60; // seconds between requests
// Store results
const results = {
provider1: { blockHeights: [], latencies: [], timestamps: [] },
provider2: { blockHeights: [], latencies: [], timestamps: [] }
};
// Measure request with detailed timing
async function measureRequest(providerUrl) {
const startTime = Date.now();
try {
const provider = new ethers.JsonRpcProvider(providerUrl);
const blockNumber = await provider.getBlockNumber();
const endTime = Date.now();
return {
blockNumber,
latency: endTime - startTime,
timestamp: startTime
};
} catch (error) {
console.error('Request failed:', error.message);
return null;
}
}
// Run comparison test
async function runTest() {
console.log('Starting RPC provider comparison...\n');
const testStart = Date.now();
const testEnd = testStart + (TEST_DURATION * 60 * 1000);
while (Date.now() < testEnd) {
// Make concurrent requests to both providers
const [result1, result2] = await Promise.all([\
measureRequest(PROVIDERS.provider1),\
measureRequest(PROVIDERS.provider2)\
]);
// Store results
if (result1) {
results.provider1.blockHeights.push(result1.blockNumber);
results.provider1.latencies.push(result1.latency);
results.provider1.timestamps.push(result1.timestamp);
}
if (result2) {
results.provider2.blockHeights.push(result2.blockNumber);
results.provider2.latencies.push(result2.latency);
results.provider2.timestamps.push(result2.timestamp);
}
console.log(`Provider 1: Block ${result1?.blockNumber}, Latency ${result1?.latency}ms`);
console.log(`Provider 2: Block ${result2?.blockNumber}, Latency ${result2?.latency}ms\n`);
// Wait before next request
await new Promise(resolve => setTimeout(resolve, REQUEST_INTERVAL * 1000));
}
analyzeResults();
}
// Calculate statistics
function calculateStats(values) {
const sorted = [...values].sort((a, b) => a - b);
const median = sorted[Math.floor(sorted.length / 2)];
const p95 = sorted[Math.floor(sorted.length * 0.95)];
const avg = values.reduce((a, b) => a + b, 0) / values.length;
return { median, p95, avg };
}
// Calculate freshness score
function calculateFreshnessScore(latency) {
return 1 - (latency / CHAIN_BLOCK_TIME);
}
// Analyze and display results
function analyzeResults() {
console.log('\n=== Analysis Results ===\n');
Object.keys(results).forEach(providerName => {
const data = results[providerName];
const latencyStats = calculateStats(data.latencies);
console.log(`${providerName.toUpperCase()}:`);
console.log(` Total Requests: ${data.latencies.length}`);
console.log(` Latency (median): ${latencyStats.median.toFixed(2)}ms`);
console.log(` Latency (p95): ${latencyStats.p95.toFixed(2)}ms`);
console.log(` Latency (avg): ${latencyStats.avg.toFixed(2)}ms`);
const medianFreshness = calculateFreshnessScore(latencyStats.median);
const p95Freshness = calculateFreshnessScore(latencyStats.p95);
console.log(` Freshness Score (median): ${medianFreshness.toFixed(3)}`);
console.log(` Freshness Score (p95): ${p95Freshness.toFixed(3)}`);
console.log();
});
}
// Run the test
runTest().catch(console.error);
更新脚本中的配置变量:
YOUR_FIRST_RPC_URL 和 YOUR_SECOND_RPC_URL 替换为实际的 RPC 端点CHAIN_BLOCK_TIME(对于Arbitrum为250ms,对于Ethereum为~12000ms,对于Solana为~400ms)TEST_DURATION 和 REQUEST_INTERVAL然后运行:
node compare-rpc.js
该脚本将输出实时比较,并在最后提供摘要分析。
Provider 1: Block 386487336, Latency 164ms
Provider 2: Block 386487339, Latency 261ms
Provider 1: Block 386487358, Latency 204ms
Provider 2: Block 386487360, Latency 301ms
Provider 1: Block 386487378, Latency 263ms
Provider 2: Block 386487381, Latency 262ms
Provider 1: Block 386487400, Latency 200ms
Provider 2: Block 386487402, Latency 312ms
Provider 1: Block 386487420, Latency 254ms
Provider 2: Block 386487424, Latency 323ms
Provider 1: Block 386487443, Latency 169ms
Provider 2: Block 386487445, Latency 298ms
Provider 1: Block 386487463, Latency 241ms
Provider 2: Block 386487466, Latency 269ms
=== Analysis Results ===
PROVIDER1:
Total Requests: 27
Latency (median): 227.00ms
Latency (p95): 368.00ms
Latency (avg): 243.19ms
Freshness Score (median): 0.092
Freshness Score (p95): -0.472
PROVIDER2:
Total Requests: 27
Latency (median): 316.00ms
Latency (p95): 589.00ms
Latency (avg): 356.07ms
Freshness Score (median): -0.264
Freshness Score (p95): -1.356
从上面的数据来看,提供商 2 始终返回更高的区块号(提前 2-4 个区块),这最初看起来更好,它看起来具有“更鲜”的数据。
但是 提供商 1 明显更快(中位数快约 113 毫秒,p95 快约 221 毫秒),这是关键的见解。提供商 2 返回更高的区块号,因为其较慢的响应时间意味着它在请求生命周期中稍后捕获区块链状态。同时,提供商 1 更早地捕获状态(更快的 TLS/连接),但更快地将其传递给你。即使区块号较低,数据也能更准确地表示你发出请求时的链状态。
根据你的数据鲜度要求,这里有一些策略:
对于需要最新数据的应用程序:
注意: 本指南中讨论的延迟鲜度框架适用于基于 HTTP 的 RPC 请求。WebSocket 订阅的工作方式不同,它会在产生新区块时将数据推送到客户端,从而消除了请求/响应周期及其相关的延迟问题。
对于高吞吐量应用程序:
一般最佳实践:
了解数据鲜度有助于你在选择RPC提供商时做出更好的决策。返回更高区块号的提供商不一定更好,重要的是相对于区块的产生时间,你能够以多快的速度和可靠性访问区块链数据。 通过测量延迟鲜度分数以及传统指标,你可以全面了解提供商的性能,并可以为你的应用程序的特定需求选择最佳选择。
如有任何疑问,请加入我们的Discord,或通过Twitter与我们联系。
如果你有任何反馈或对新主题的要求,请告诉我们。我们很乐意听到你的声音。
- 原文链接: quicknode.com/guides/inf...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!