Logo
Nuevos usuarios de RPC: 35% de descuento el primer mesVer oferta
OnFinality Learn
Solución de problemas de RPC13 min de lectura

Consistencia de RPC multi-endpoint: retraso de la cabeza y lecturas conflictivas

Por qué la misma consulta JSON-RPC devuelve datos diferentes desde distintos endpoints, y la disciplina de fijación, monotonicidad y quórum que hace reproducibles las lecturas multi-endpoint.

TL;DR

Cada endpoint RPC es un nodo independiente con su propia visión de la cabeza de la cadena, por lo que una lectura 'latest' contra el endpoint A y una lectura 'latest' contra el endpoint B responden a dos preguntas diferentes siempre que sus cabezas difieran. El retraso de la cabeza es un comportamiento normal de gossip y sincronización, no una caída, y por eso las comprobaciones de liveness pasan mientras los datos están obsoletos. La solución es una disciplina de lectura: resolver la cabeza una vez, fijar las lecturas críticas para la corrección a una altura o hash exactos, exigir monotonicidad en tu propio pipeline y tratar una cabeza que retrocede como un endpoint retrasado, a menos que haya cambiado el hash a la misma altura. Este artículo cubre la semántica de las etiquetas de bloque, el procedimiento de reconciliación, un método reproducible de medición del retraso de la cabeza y una tabla de decisión para cuando dos endpoints discrepan.

Por qué la misma consulta devuelve datos diferentes desde distintos endpoints

Un endpoint JSON-RPC no es una base de datos compartida. Es un nodo (o un grupo de nodos balanceados) con su propio conjunto de pares, su propio estado de sincronización y su propia visión local de la cabeza de la cadena. En cualquier instante, el endpoint A puede estar en la altura N mientras que el endpoint B está en N-2, porque el gossip de bloques, el modo de sincronización y la salud de las réplicas difieren por nodo. Una lectura latest contra A y una lectura latest contra B, por tanto, responden a dos preguntas diferentes, aunque los bytes de la solicitud sean idénticos.

Esta es la causa raíz de los dos informes de error multi-endpoint más comunes: 'mi aplicación muestra saldos inconsistentes' y 'el indexador vio un bloque que la API no'. Ninguno es una caída del proveedor. Son la consecuencia esperada de pedirle un valor a un objetivo en movimiento. El artículo sobre enrutamiento de failover RPC multirregión señala este peligro en torno a la cabeza de la cadena; este artículo enseña el procedimiento de reconciliación que lo resuelve.

El cambio mental importante es que la consistencia es una propiedad de tu patrón de lectura, no del endpoint. Si nunca fijas una altura, ningún proveedor puede hacer que tus lecturas coincidan, porque la coincidencia nunca se solicitó.

  • El endpoint A en la altura N y el endpoint B en N-2 están ambos sanos; simplemente están en puntos diferentes de la misma cadena.
  • Una lectura latest es una solicitud de 'lo que este nodo cree actualmente que es la punta', que es local al nodo y varía en el tiempo.
  • Una lectura por número o por hash es una solicitud de un objeto específico e inmutable, que es independiente del endpoint una vez que ese bloque existe en todas partes.

Las etiquetas de bloque cambian la pregunta, no solo la frescura

El parámetro de bloque JSON-RPC de Ethereum acepta latest, safe, finalized, earliest, pending y números o hashes de bloque explícitos. No son diales de frescura sobre la misma respuesta; seleccionan objetos diferentes con garantías diferentes. La documentación de la API JSON-RPC de ethereum.org define el conjunto de parámetros, y la especificación de las APIs de ejecución es la referencia autorizada para el comportamiento de los métodos.

latest es la cabeza local del nodo: rápida, pero no determinista entre endpoints. safe es la cabeza votada por supermayoría introducida tras la Merge, típicamente alrededor de dos épocas por detrás de la punta. finalized es la cabeza irreversible, aún más atrás. pending es la visión local del nodo de las transacciones aún no incluidas, que es la menos portable de todas porque depende del mempool de ese nodo. earliest es el bloque génesis.

Las lecturas por número y por hash son deterministas. Una vez que un bloque existe en todos los endpoints que consultas, eth_getBlockByNumber a esa altura devuelve el mismo bloque, y eth_getBalance a esa altura devuelve el mismo saldo. Esta es la única forma de lectura que garantiza acuerdo entre endpoints, y por eso fijar es la disciplina central.

  • latest — cabeza local, rápida, específica del endpoint, puede retroceder en un nodo retrasado.
  • safe — cabeza votada por supermayoría, documentada como aproximadamente dos épocas por detrás en Ethereum tras la Merge; el comportamiento varía por red.
  • finalized — cabeza irreversible, mayor retraso, garantía más fuerte.
  • pending — visión local del mempool, no reproducible entre endpoints.
  • Por número / por hash — determinista e independiente del endpoint una vez que el bloque está presente en todas partes.

El retraso de la cabeza es un comportamiento normal, no una caída

Un nodo que está dos bloques por detrás no está fallando una comprobación de salud. Sigue aceptando conexiones, sigue respondiendo a eth_blockNumber y sigue devolviendo datos válidos para cada altura que tiene. Liveness y frescura son propiedades diferentes, y la mayoría del monitoreo las confunde. El artículo sobre detectar un nodo RPC por detrás de la punta de la cadena cubre la detección de retraso de punta en un solo nodo; el caso multi-endpoint añade una segunda dimensión, porque debes decidir en cuál de varias cabezas confiar.

Las causas comunes del retraso de la cabeza incluyen el retraso en la propagación del gossip, un nodo que aún se está poniendo al día tras un reinicio, un nodo que podó estado y está volviendo a obtenerlo, y una réplica retrasada situada detrás de un balanceador de carga. En un pool de failover, una solicitud puede aterrizar en cualquiera de estos, por lo que dos llamadas consecutivas del mismo cliente pueden llegar a dos cabezas diferentes.

La consecuencia práctica: una sonda de liveness que comprueba HTTP 200 y un eth_blockNumber no nulo pasará en un nodo que está materialmente retrasado. La frescura debe medirse por separado, y medirse por endpoint.

  • Liveness: el endpoint responde. Frescura: la cabeza del endpoint está cerca de la cabeza de la red.
  • Una réplica retrasada detrás de un balanceador de carga mezcla silenciosamente respuestas obsoletas y frescas.
  • El failover que enruta solo por liveness puede enrutar a un nodo obsoleto.

La disciplina de reconciliación: fijar, imponer monotonicidad, comparar lo comparable

Tres reglas hacen reproducibles las lecturas multi-endpoint. Primero, fija una altura para las lecturas críticas para la corrección: resuelve latest una vez, captura el número y el hash del bloque, y luego consulta cada fuente a esa altura o hash exactos. Segundo, exige monotonicidad en tu propio pipeline: nunca aceptes una cabeza que retroceda, y trata una cabeza que retrocede como un endpoint retrasado en lugar de una reorganización, a menos que haya cambiado el hash a la misma altura. Tercero, lee la misma entidad desde el mismo compromiso fijado al comparar fuentes.

La regla de monotonicidad merece énfasis porque es el detector de reorganizaciones más barato que tienes. Si el endpoint A informa la altura 1000 y luego informa 998, casi siempre es A retrasado o A reiniciado, no la cadena reorganizándose. Una reorganización real se manifiesta como la misma altura con un hash diferente. Distinguir estos dos casos evita una gran clase de falsas alarmas de reorganización.

Fijar también hace manejables el almacenamiento en caché y la idempotencia. Una lectura con clave (method, params, blockHash) es segura de cachear y segura de reintentar, porque el objeto subyacente no puede cambiar. Una lectura con clave latest no es ninguna de las dos cosas.

  • Resuelve la cabeza una vez y luego fija cada lectura crítica para la corrección a esa altura o hash.
  • Rechaza las cabezas que retroceden; solo trata un cambio de hash a la misma altura como una reorganización.
  • Compara fuentes en el mismo compromiso fijado, nunca latest contra latest.
  • Usa como clave de cachés y reintentos el hash fijado, no la etiqueta.

Read-your-writes: una transacción visible en un endpoint pero no en otro

Cuando difundes una transacción al endpoint A, entra en el mempool de A. El endpoint B tiene un mempool diferente y puede no ver la transacción durante algún tiempo, si es que la ve. Una eth_getTransactionByHash posterior contra B puede devolver null aunque la transacción sea perfectamente válida y ya sea conocida por A. Este es el peligro de read-your-writes en un pool multi-endpoint.

El patrón correcto es seguir la transacción por hash en el endpoint que la aceptó, o sondear todos los endpoints hasta que un quórum la vea, antes de declarar éxito. Declarar éxito con la aceptación de un solo endpoint es una fuente común de informes de 'la transacción desapareció', porque la siguiente solicitud puede enrutarse a un endpoint diferente.

Para indexadores y trabajos de reconciliación, el mismo principio se aplica a granularidad de bloque. El enfoque de reconciliación de indexadores EVM bloque a bloque de recorrer una secuencia canónica de alturas y comparar hashes es la forma duradera de detectar divergencias entre un indexador y una API.

  • La difusión y las lecturas de seguimiento deben apuntar al mismo endpoint, o usar un quórum.
  • Un eth_getTransactionByHash nulo en un endpoint no es prueba de que la transacción falló.
  • La visibilidad por quórum antes de declarar éxito elimina la carrera de read-your-writes.

Medir el retraso de la cabeza por endpoint de forma reproducible

Para medir el retraso de la cabeza sin adivinar, muestrea eth_blockNumber (o getSlot en endpoints estilo Solana) contra varios endpoints con un temporizador, registra la marca de tiempo y la altura devuelta, y calcula la distribución de deltas por endpoint en relación con la altura máxima observada en cada muestra. Registra también el hash del bloque a una altura fija entre endpoints para detectar divergencia, no solo retraso.

Ejecuta el muestreador el tiempo suficiente para capturar la variación normal, y registra el entorno: red, URLs de los endpoints, intervalo de muestreo y muestras totales. No compares números de ejecuciones diferentes o redes diferentes. La tabla de abajo es una plantilla para rellenar con tus propias mediciones; los valores son tuyos para producir, no benchmarks publicados.

Para endpoints estilo Solana, la señal de frescura equivalente es la altura de slot y el hash de slot; se aplica el mismo método de distribución de deltas. Consulta la página de la red Solana para el contexto de endpoints.

// head-lag sampler: run against several endpoints on a timer
// node sampler.js
const ENDPOINTS = [
  'https://endpoint-a.example/rpc',
  'https://endpoint-b.example/rpc',
  'https://endpoint-c.example/rpc'
];
const INTERVAL_MS = 2000;
const SAMPLES = 60;

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 sampleOnce() {
  const t = Date.now();
  const heights = await Promise.all(
    ENDPOINTS.map(async (url) => {
      try {
        const hex = await rpc(url, 'eth_blockNumber');
        return { url, height: parseInt(hex, 16), ok: true };
      } catch (e) {
        return { url, height: null, ok: false, error: String(e) };
      }
    })
  );
  const max = Math.max(...heights.filter(h => h.ok).map(h => h.height));
  return { t, heights, max };
}

(async () => {
  const rows = [];
  for (let i = 0; i < SAMPLES; i++) {
    rows.push(await sampleOnce());
    await new Promise(r => setTimeout(r, INTERVAL_MS));
  }
  for (const ep of ENDPOINTS) {
    const deltas = rows
      .map(r => r.heights.find(h => h.url === ep))
      .filter(h => h && h.ok)
      .map(h => r => 0); // placeholder, replaced below
    const d = rows
      .map(r => {
        const h = r.heights.find(x => x.url === ep);
        return h && h.ok ? r.max - h.height : null;
      })
      .filter(x => x !== null);
    d.sort((a, b) => a - b);
    const p50 = d[Math.floor(d.length * 0.5)] ?? null;
    const p95 = d[Math.floor(d.length * 0.95)] ?? null;
    console.log(ep, 'samples=', d.length, 'p50=', p50, 'p95=', p95, 'max=', d[d.length - 1]);
  }
})();

Tabla de resultados: rellénala con tus propias mediciones de endpoints

Usa la salida del muestreador para rellenar una tabla como la de abajo. Registra una fila por endpoint y mantén fijos los parámetros de ejecución para que las filas sean comparables. El objetivo no es producir un número universal, sino caracterizar tu propio pool: qué endpoints están consistentemente retrasados y por cuánto.

Junto con el delta de altura, registra la concordancia de hash a una altura fija. Dos endpoints pueden informar la misma altura con hashes diferentes solo durante una ventana de reorganización; fuera de esa ventana, una discrepancia de hash a la misma altura indica un problema que vale la pena investigar.

  • URL del endpoint — la URL exacta muestreada.
  • Muestras — número de muestras exitosas.
  • Delta p50 — mediana de bloques por detrás de la altura máxima observada.
  • Delta p95 — percentil 95 de bloques por detrás.
  • Delta máximo — peor retraso observado.
  • Concordancia de hash a altura fija — coincidencias / discrepancias entre endpoints.

Tabla de decisión: dos endpoints discrepan, ¿en cuál confías?

Cuando dos endpoints devuelven datos diferentes para la misma consulta lógica, clasifica la discrepancia antes de reaccionar. La clasificación determina la acción correcta, y la mayoría de las discrepancias son retraso, no reorganización.

Misma altura y mismo hash significa que los endpoints coinciden; la diferencia anterior fue casi con certeza un artefacto de temporización entre dos lecturas latest. Misma altura y hash diferente significa una ventana de reorganización: espera a finalized antes de actuar sobre cualquiera de las dos. Alturas diferentes significa retraso: confía en la cabeza más alta solo si es un descendiente válido de la más baja, lo que puedes verificar recorriendo los hashes de los padres.

  • Misma altura, mismo hash — coinciden; vuelve a leer a la altura fijada para confirmar.
  • Misma altura, hash diferente — reorganización; espera a la finalidad antes de confirmar.
  • Altura diferente, la más alta es descendiente — retraso; la cabeza más alta es la visión más fresca.
  • Altura diferente, la más alta no es descendiente — investiga; esto no es un simple retraso.

Solución de problemas: síntomas y el patrón de lectura que los corrige

La mayoría de las quejas de consistencia multi-endpoint se corresponden con un pequeño conjunto de errores en el patrón de lectura. La solución suele ser cambiar lo que pides, no qué proveedor usas.

Si los saldos parpadean entre dos valores, estás leyendo latest entre endpoints; fija una altura. Si un indexador ve un bloque que la API no, la API está retrasada; compara a una altura fijada y trata la cabeza más alta como más fresca. Si una transacción parece desaparecer, la estás leyendo en un endpoint diferente del que la aceptó; sigue por hash en el endpoint que la aceptó o usa un quórum. Si una cabeza retrocede, trátala como un endpoint retrasado a menos que haya cambiado el hash a la misma altura.

  • Valores parpadeantes — fija la altura y vuelve a leer.
  • Indexador por delante de la API — retraso esperado; reconcilia a una altura fijada.
  • Transacción 'desaparecida' — read-your-writes entre endpoints; sigue por hash o quórum.
  • La cabeza retrocedió — endpoint retrasado, no una reorganización, a menos que haya cambiado el hash a la misma altura.
  • El failover enrutó a un nodo obsoleto — añade frescura a tu señal de enrutamiento, no solo liveness.

Limitaciones y compensaciones: frescura, latencia y el coste del quórum

Fijar una altura cambia frescura por determinismo. Una lectura fijada es reproducible y cacheable, pero por definición no es la punta. Para lecturas críticas para la corrección esa es la compensación correcta; para la frescura de la interfaz de usuario puede que no lo sea. Elige por lectura, no por aplicación.

safe y finalized introducen latencia por diseño. En Ethereum tras la Merge, safe está documentado como aproximadamente dos épocas por detrás de la cabeza y finalized aún más atrás; el comportamiento varía por red, así que trátalos como documentado / varía según proveedor y red en lugar de constantes fijas. Una lectura por quórum cuesta más solicitudes que una lectura única, pero es la única forma de hacer fiable una lectura multi-endpoint cuando no puedes fijar.

Nada de esto elimina la necesidad de monitoreo. La frescura debe medirse continuamente, y el enrutamiento debería considerarla. El artículo sobre monitoreo y failover de nodos RPC cubre el lado operativo; la guía de endpoints RPC multicadena cubre la selección de endpoints entre redes.

  • Lecturas fijadas: deterministas, cacheables, no son la punta.
  • safe / finalized: garantías más fuertes, latencia añadida, variación por red.
  • Lecturas por quórum: mayor coste de solicitudes, la única lectura multi-endpoint fiable cuando fijar es imposible.
  • La frescura debe monitorearse y alimentar el enrutamiento, no asumirse a partir del liveness.

Próximos pasos: aplica la disciplina a tu propio pool

Empieza ejecutando el muestreador contra tus endpoints reales y rellenando la tabla de resultados. Eso te da una línea base de cuánto retraso exhibe tu pool y qué endpoints están consistentemente retrasados. Luego cambia tu patrón de lectura: fija alturas para las lecturas críticas para la corrección, impón monotonicidad en tu pipeline y sigue las transacciones en el endpoint que las aceptó.

Si estás evaluando proveedores, compáralos por su comportamiento de frescura y sus garantías de consistencia, no solo por liveness. Las páginas del servicio de API y de precios de RPC describen la superficie del servicio, y el hub de aprendizaje de OnFinality recopila los artículos relacionados de fiabilidad. Para una visión más amplia de selección de endpoints, consulta la guía de endpoints RPC multicadena.

El objetivo no es eliminar el retraso de la cabeza, que es normal, sino hacer que tus lecturas sean reproducibles a pesar de él. Fija, impón monotonicidad y compara lo comparable.

  • Mide tu propio pool antes de cambiar nada.
  • Fija alturas para las lecturas críticas para la corrección.
  • Impón monotonicidad y clasifica las discrepancias antes de reaccionar.
  • Sigue las transacciones en el endpoint que las aceptó o usa un quórum.

Nunca te preocupes por la infraestructura nuevamente

OnFinality elimina la carga pesada de DevOps para que puedas construir de forma más inteligente y rápida.

Comenzar