当节点未在同步时,eth_syncing 返回 false;当节点正在同步时,返回包含 startingBlock、currentBlock 和 highestBlock 的对象。但 false 并不能证明节点可用或已到达网络头部。许多客户端在仍在修复状态或同步到已知头部时也会报告 false,因此仅基于 eth_syncing 构建的健康检查并不可靠。可靠的断言会结合 eth_chainId 和 net_version 确认链身份,然后在一段时间窗口内从候选节点和独立参考节点采样 eth_blockNumber,通过比较差值来测量头部滞后。本文提供一个可运行的 Node.js 检查器和结果表,帮助你验证自己的端点。
eth_syncing 的返回值约定
以太坊 JSON-RPC 规范 定义 eth_syncing 在节点未同步时返回 false,否则返回描述同步进度的对象。该对象通常包含 startingBlock、currentBlock 和 highestBlock,但确切的字段集合及其准确性因客户端和版本而异。这是文档化的行为,而非缺陷,这意味着任何解析器都必须处理两种截然不同的形态:布尔值和对象。
将 false 视为健康的朴素客户端,实际上并未对节点所服务的链状态做出任何断言。节点可能已完全同步,也可能远远落后但当前并未处于同步循环中。返回值是状态标志,而非健康结论。对于生产系统,你需要知道节点相对于网络头部的位置,而不仅仅是它是否正在主动同步。
JSON-RPC 2.0 规范 规定了请求和响应信封,包括错误处理,但并未定义 eth_syncing 的语义。以太坊 JSON-RPC 规范是该方法的返回约定的权威来源。请始终查阅执行客户端的文档,了解它实际返回的字段,因为各客户端存在差异。
- false:节点报告未在同步;并不意味着它已到达网络头部。
- 对象:节点报告同步进度;字段因客户端和版本而异。
- 常见字段:startingBlock、currentBlock、highestBlock(可能缺失或过时)。
- 解析必须同时处理布尔值和对象形态,且不能抛出异常。
为什么节点落后时 eth_syncing 仍可能报告 false
Snap 同步和检查点同步允许节点在完全验证历史状态之前就进入可运行状态。在状态修复期间,节点可能报告 false,因为它认为自己已同步到它所知道的头部,即使它仍在追赶网络头部。这是有文档记录且长期被报告的客户端行为,在公开的问题跟踪器和客户端文档中可见。
各客户端对“已同步”的定义也不同。有些客户端一旦拥有最新区块头就报告 false,即使状态不完整。另一些客户端在距离头部很近时可能报告 false。结果是,仅凭 eth_syncing 无法区分一个完全追上的节点和一个仅仅未处于主动同步循环中的节点。
对于生产 RPC 端点,风险在于虚假的信心。负载均衡器可能将请求路由到一个报告 false 但落后数分钟甚至数小时的后端。你的应用随后会在无错误的情况下读取到过时数据。唯一可靠的方法是直接测量头部滞后,使用 eth_blockNumber 与独立参考节点进行比较。
- Snap/检查点同步:节点可能在完全状态验证之前就可运行。
- 状态修复:节点可能在仍在追赶时报告 false。
- 客户端对“已同步”的定义各不相同;不要假设一致性。
- 假阴性是基于 eth_syncing 的健康检查的主要故障模式。
高度比较前的链身份检查
在比较区块高度之前,确认节点位于预期的链上。使用 eth_chainId 获取链 ID,使用 net_version 获取网络 ID。这些调用开销很低,应成为每次健康检查的一部分。位于错误链上的节点会有不同的区块高度,并可能产生误导性的滞后测量结果。
对于以太坊主网,链 ID 为 1。对于测试网,则有所不同。如果你的应用期望主网,而节点位于测试网,它会报告低得多的区块号并显得滞后。先检查链身份可以防止误报,并确保你的参考端点位于同一条链上。
RPC 端点指南(RPC Assistant) 介绍了如何选择和验证端点。关于 OnFinality 特定的网络详情,请参阅以太坊网络页面。
- eth_chainId 返回链 ID(例如主网为 1)。
- net_version 返回网络 ID(通常与链 ID 相同)。
- 始终在同一条链上比较候选节点和参考节点。
- 链 ID 不匹配会使任何高度比较失效。
用 eth_blockNumber 差值测量头部滞后
头部滞后是候选节点的最新区块号与网络头部之间的差值。要可靠地测量它,请在一段短时间窗口内从候选节点和至少两个独立参考端点采样 eth_blockNumber。然后比较差值:位于顶端的节点与参考节点以相同速率推进;滞后的节点以相同速率推进但存在固定偏移;停滞的节点不推进。
单次比较是不够的,因为区块生产具有突发性。一次性差异只是噪声。使用包含多个样本的窗口,并计算中位数差值而非最大值。中位数能稳定信号并减少异常值的影响。对于生产检查,30–60 秒内 5–10 个样本的窗口通常足够。
检测 RPC 节点头部滞后和过时响应 一文更详细地介绍了客户端侧检测。关于监控和告警,请参阅 RPC 节点监控、指标和告警。
- 在一段窗口内采样候选节点和参考节点(例如 5–10 个样本)。
- 在每个样本处计算差值:候选高度减去参考高度。
- 使用中位数差值作为滞后估计值。
- 停滞的节点在样本间显示零推进。
- 滞后的节点显示一致的偏移。
- 健康的节点显示接近零的差值(在一两个区块以内)。
标签语义:latest、safe 和 finalized
对于单个客户端而言,latest、safe 和 finalized 这些标签来自不同的地方,客户端可能正确地提供其中一个,而另一个却是过时的。latest 指的是节点所知道的最新区块,它可能不是网络头部。safe 和 finalized 指的是共识层检查点,可能落后于 latest。健康检查应说明它探测的是哪个标签。
对于生产系统,在候选节点和参考节点之间比较 latest 是衡量头部滞后最直接的方法。然而,如果你的应用依赖 finalized 数据,你还应验证 finalized 区块正在推进。如果节点未收到共识更新,它可能在 latest 上处于头部,但在 finalized 上滞后。
记录你的健康检查使用哪个标签,并确保你的参考端点支持相同的标签。在候选节点和参考节点之间混用标签会产生误导性的滞后数字。
- latest:节点已知的最新区块;可能不是网络头部。
- safe:被共识认为安全的近期区块;可能落后于 latest。
- finalized:被认为最终的区块;至少落后 latest 两个 epoch。
- 在健康检查中说明标签,并一致地使用它。
远程 RPC 端点的运维限制
你未自行运营的 RPC 端点可能在多个后端之间进行负载均衡。连续两次探测可能不会命中同一个节点。这使得逐次探测比较成为唯一安全的模式:将候选节点的响应与参考节点在同一时刻的响应进行比较,而不是假设一个长期基线。来自较早探测的基线可能反映的是不同的后端。
如果你运营自己的节点,可以维护一个基线,但仍应在一段窗口内采样,以应对突发性的区块生产。对于第三方端点,始终将每次探测视为独立的。API 服务 和 RPC 定价 页面描述了 OnFinality 的产品,但该测量方法适用于任何提供商。
负载均衡还意味着单个端点 URL 在连续调用时可能返回不同的高度。你的检查器不应假设探测之间具有单调性。相反,应在相同的样本索引处比较候选节点和参考节点。
- 远程端点可能经过负载均衡;连续探测可能命中不同的后端。
- 在同一时刻比较候选节点和参考节点,而不是与历史基线比较。
- 对于自运营节点,可以使用基线,但仍应在一段窗口内采样。
- 在负载均衡的端点上,不要假设探测之间的区块高度是单调的。
用于同步断言的可运行 Node.js 检查器
以下 Node.js 脚本在一段窗口内轮询一个候选端点和两个参考端点,计算中位数滞后,并打印结论。它使用原生 fetch API(Node.js 18+)。请将占位 URL 替换为你自己的端点。为完整起见,该脚本同时处理 eth_syncing 的布尔值和对象响应,但结论基于 eth_blockNumber 差值。
使用以下命令运行:node sync-check.js。该脚本输出样本表和最终结论。使用结果填写下一节中的表格。
const CANDIDATE = 'https://your-candidate-rpc';
const REF1 = 'https://reference-1-rpc';
const REF2 = 'https://reference-2-rpc';
const SAMPLES = 7;
const INTERVAL_MS = 5000;
async function rpc(url, method, params = []) {
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
});
const json = await res.json();
if (json.error) throw new Error(json.error.message);
return json.result;
}
async function getBlockNumber(url) {
const hex = await rpc(url, 'eth_blockNumber');
return parseInt(hex, 16);
}
async function getChainId(url) {
return rpc(url, 'eth_chainId');
}
function median(arr) {
const sorted = [...arr].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
}
(async () => {
const chainId = await getChainId(CANDIDATE);
console.log('Candidate chainId:', chainId);
const refChainId = await getChainId(REF1);
if (chainId !== refChainId) {
console.error('Chain ID mismatch. Aborting.');
process.exit(1);
}
const rows = [];
for (let i = 0; i < SAMPLES; i++) {
const [c, r1, r2] = await Promise.all([
getBlockNumber(CANDIDATE),
getBlockNumber(REF1),
getBlockNumber(REF2)
]);
const refMedian = median([r1, r2]);
const lag = c - refMedian;
rows.push({ sample: i + 1, candidate: c, ref1: r1, ref2: r2, refMedian, lag });
console.log(`Sample ${i + 1}: candidate=${c} ref1=${r1} ref2=${r2} refMedian=${refMedian} lag=${lag}`);
if (i < SAMPLES - 1) await new Promise(r => setTimeout(r, INTERVAL_MS));
}
const lags = rows.map(r => r.lag);
const medianLag = median(lags);
const maxLag = Math.max(...lags);
const minLag = Math.min(...lags);
const candidateAdvance = rows[rows.length - 1].candidate - rows[0].candidate;
const refAdvance = rows[rows.length - 1].refMedian - rows[0].refMedian;
console.log('\n--- Summary ---');
console.log(`Median lag: ${medianLag}`);
console.log(`Min lag: ${minLag}, Max lag: ${maxLag}`);
console.log(`Candidate advance: ${candidateAdvance}, Reference advance: ${refAdvance}`);
let verdict = 'UNKNOWN';
if (candidateAdvance === 0 && refAdvance > 0) verdict = 'STALLED';
else if (medianLag > 5) verdict = 'LAGGING';
else if (Math.abs(medianLag) <= 2) verdict = 'HEALTHY';
else verdict = 'DEGRADED';
console.log(`Verdict: ${verdict}`);
})();用于记录你自己测量结果的结果表
使用下表记录你自己的测量结果。针对你的候选端点和两个参考端点运行检查器,然后填写各列。中位数滞后和结论列应根据你的样本计算得出。此表是模板;不要将示例值视为真实测量结果。
填写表格后,比较你在不同时段和不同端点上的结果。健康的端点应显示接近零的中位数滞后和一致的推进。滞后的端点会显示正的中位数滞后。停滞的端点会显示零推进。
- 样本:探测的序号。
- 候选:来自你端点的区块号。
- 参考 1 / 参考 2:来自独立端点的区块号。
- 参考中位数:两个参考高度的中位数。
- 滞后:候选减去参考中位数。
- 结论:根据你的阈值判定为 HEALTHY、DEGRADED、LAGGING 或 STALLED。
局限性与权衡
此方法测量的是头部滞后,而非完整的同步状态。节点可能在 latest 上处于头部,但仍在修复状态或缺少历史数据。对于需要归档数据的应用,你必须单独验证历史数据可用性,如以太坊归档节点和历史 RPC 所述。
参考端点并非绝对可靠。如果两个参考节点都落后或位于不同的分叉上,你的滞后测量将是错误的。至少使用两个独立参考节点,对于关键系统可考虑第三个。该方法还假设区块生产正在进行;在没有近期区块的测试网上,滞后测量可能毫无意义。
该检查器使用 eth_blockNumber,它返回最新区块号。它不验证该区块是否为规范链,也不验证状态是否可用。要获得完整的健康检查,请将此方法与 eth_syncing 解析、链 ID 验证和应用层读取测试结合使用。OnFinality Learn 中心 有关于监控和检测的相关指南。
- 测量头部滞后,而非完整同步或状态可用性。
- 需要至少两个独立参考节点。
- 假设链上区块生产活跃。
- 不验证规范性和历史数据。
- 结合应用层读取测试以实现全面覆盖。
排查常见的同步断言故障
如果你的检查器报告较大的滞后,但节点在其他工具中看起来健康,请验证所有端点是否位于同一条链上。链 ID 不匹配会产生一致的偏移。还要检查你是否在比较相同的标签;如果候选节点使用 latest 而参考节点使用 finalized,滞后会很大且属于预期情况。
如果检查器报告 STALLED,请确认参考端点正在推进。如果参考节点也停滞,链可能已停止或你的参考节点可能已宕机。如果只有候选节点停滞,该节点可能已与对等节点断开连接或遇到共识问题。检查节点的对等节点数量和日志。
如果检查器报告 HEALTHY,但你的应用看到过时数据,问题可能是状态修复或归档数据可用性,而非头部滞后。使用 eth_getBlockByNumber 查询近期区块以验证状态,并考虑使用专用的归档端点。Base OP-Stack 节点同步状态 指南涵盖了 OP-Stack 链的类似概念。
- 链 ID 不匹配:在所有端点上验证 eth_chainId。
- 标签不匹配:确保候选节点和参考节点使用相同的标签。
- 参考节点停滞:检查参考节点健康状况和链状态。
- 候选节点停滞:检查对等节点数量和节点日志。
- 头部健康但数据过时:检查状态修复和归档可用性。
生产环境同步监控的后续步骤
将检查器集成到你的监控管道中。按计划运行它(例如每分钟一次),并在中位数滞后超过阈值时告警。存储结果以跟踪长期趋势。结合 eth_syncing 解析以获得完整图景,但不要仅依赖 eth_syncing。
关于 OnFinality 特定的端点,请参阅以太坊网络页面 和 RPC 定价。API 服务 提供托管的 RPC 端点,你可以用此方法进行验证。更多指南,请访问 OnFinality Learn 中心。
请记住,eth_syncing 的确切字段集合和准确性因客户端和版本而异。始终针对你的特定客户端和版本进行测试。此处描述的方法与客户端无关,并依赖广泛支持的 eth_blockNumber。
- 按计划运行检查器,并在中位数滞后阈值上告警。
- 存储结果以进行趋势分析。
- 结合 eth_syncing 解析和链 ID 检查。
- 针对你的特定客户端和版本进行测试。
- 查阅监控和检测的相关指南。