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

Cobertura de solicitudes RPC: reduce la latencia de cola entre endpoints

Aprende cómo la cobertura de solicitudes reduce la latencia p99 en llamadas JSON-RPC de solo lectura al competir solicitudes duplicadas entre endpoints independientes y cancelar la perdedora.

TL;DR

La cobertura de solicitudes envía la misma llamada JSON-RPC de solo lectura a dos o más endpoints independientes en paralelo tras un breve retardo, toma la primera respuesta exitosa y cancela el resto. Esto reduce la latencia de cola p99 que domina la lentitud visible para el usuario en cargas de trabajo con fan-out, mientras que la mediana permanece sin cambios. La cobertura difiere de los reintentos (recuperación secuencial tras un fallo) y de la conmutación por error (enrutamiento a un endpoint saludable). Debe restringirse a métodos de lectura idempotentes, y duplica el peso de la solicitud, por lo que intercambia volumen adicional por reducción de cola. Este artículo explica el mecanismo, proporciona una implementación ejecutable en TypeScript y muestra cómo medir la caída de p99 frente a tus propios endpoints.

Por qué la latencia de cola domina la lentitud visible para el usuario en el fan-out de RPC

Una sola carga de página o simulación de transacción a menudo desencadena docenas de lecturas JSON-RPC: verificaciones de saldo, consultas de nonce, llamadas a contratos y consultas de bloques. Si cada llamada tiene una latencia mediana de 50 ms pero un p99 de 800 ms, la carga de página mediana parece bien, pero aproximadamente una de cada cien llamadas se atasca durante casi un segundo. Cuando haces fan-out a 20 llamadas, la probabilidad de que al menos una alcance la cola aumenta drásticamente, por lo que la experiencia visible para el usuario se rige por la llamada más lenta, no por el promedio.

Este es el clásico problema de cola a escala descrito en el trabajo fundamental de Google sobre latencia de cola y en la documentación de diseño de 'Request Hedging' de gRPC. La mediana no es la métrica que los usuarios sienten; el p99 sí lo es. Reducir la mediana de 50 ms a 40 ms apenas se nota, pero reducir el p99 de 800 ms a 200 ms transforma la capacidad de respuesta percibida.

Para RPC de blockchain específicamente, la latencia de cola proviene de muchas fuentes: pausas de recolección de basura, picos en el mempool, E/S de disco en nodos de archivo, fluctuación de red y limitación de velocidad del proveedor. Estas son transitorias e independientes entre proveedores, que es exactamente la condición que hace efectiva la cobertura. Para un tratamiento más amplio de las causas de latencia, consulta Latencia RPC: causas, medición y soluciones.

  • La latencia mediana oculta el 1% de llamadas más lentas que los usuarios realmente notan.
  • El fan-out multiplica la probabilidad de cola: 20 llamadas con p99 cada una producen una probabilidad mucho mayor de una respuesta lenta.
  • Causas transitorias independientes entre proveedores son la precondición para que la cobertura ayude.

Cobertura vs reintentos vs conmutación por error: tabla de decisión

Estas tres técnicas a menudo se confunden, pero resuelven problemas diferentes y se componen de manera distinta. Los reintentos son secuenciales: esperas un fallo o tiempo de espera, luego intentas de nuevo, lo que agrega latencia igual al tiempo de espera. La conmutación por error es enrutamiento: envías a un endpoint saludable según verificaciones de estado, pero aún esperas la respuesta de ese endpoint. La cobertura es paralela: envías duplicados antes de saber si el primero será lento, y tomas el primer éxito.

La siguiente tabla resume cuándo aplica cada una. Usa cobertura solo para lecturas idempotentes donde la ejecución duplicada es inofensiva. Usa reintentos para errores transitorios en cualquier método, pero ten cuidado con escrituras no idempotentes. Usa conmutación por error para salud de endpoints y enrutamiento regional. En la práctica, los clientes de producción combinan las tres: la conmutación por error elige el primario, la cobertura compite con un secundario para lecturas, y los reintentos manejan errores explícitos.

  • Cobertura: duplicado paralelo tras un retardo; reduce p99; duplica el peso de la solicitud; solo lectura.
  • Reintentos: secuencial tras fallo; se recupera de errores; agrega latencia de tiempo de espera; cualquier método con clave de idempotencia.
  • Conmutación por error: enrutamiento a endpoint saludable; maneja caídas; no reduce la cola de un endpoint lento pero activo.

El mecanismo de cobertura: temporizador, duplicado, primer éxito, cancelación

La solicitud cubierta canónica, como se describe en la documentación de 'Request Hedging' de gRPC, funciona de la siguiente manera. El cliente envía la solicitud al endpoint A inmediatamente. Arma un temporizador configurado con un retardo derivado de la latencia p95 observada de ese método. Si la respuesta llega antes de que se dispare el temporizador, el cliente la devuelve y no se envía ningún duplicado. Si el temporizador se dispara primero, el cliente envía la misma solicitud al endpoint B. La primera respuesta exitosa gana; la otra solicitud en vuelo se cancela mediante una señal de aborto.

El retardo es crítico. Si es demasiado bajo, ambas solicitudes se disparan casi simultáneamente y pagas el doble de costo en casi todas las llamadas. Si es demasiado alto, la cobertura rara vez se dispara y el p99 permanece alto. Un punto de partida común es el p95 de la distribución de latencia del método, medido en una ventana móvil. También puedes limitar el número de coberturas pendientes por método para acotar el costo.

La cancelación debe ser cooperativa. En Node.js, AbortController propaga una señal de aborto a fetch, que cierra el socket subyacente. Para JSON-RPC sobre HTTP, esto es seguro para lecturas porque el servidor aún puede procesar la solicitud, pero el cliente deja de esperar. Nunca cubras un método de escritura como eth_sendRawTransaction, porque ambos duplicados podrían ser aceptados y causar un doble envío.

La semántica de esperar antes de duplicar sigue el modelo de hedging del lado del cliente descrito en la guía de request hedging de gRPC, una referencia primaria útil para el diseño de temporizador y cancelación aunque el transporte sea HTTP JSON-RPC simple y no gRPC.

  • Envía a A inmediatamente; arma un temporizador con retardo ~p95.
  • Al dispararse el temporizador, envía duplicado a B; resuelve con el primer éxito.
  • Cancela el perdedor con AbortController; limita las coberturas pendientes.
  • Restringe a lecturas idempotentes: eth_call, eth_getBalance, getLatestBlockhash, getAccountInfo.

Consecuencias en el presupuesto de solicitudes: por qué la cobertura duplica el peso del método

Cada solicitud cubierta consume peso de solicitud adicional en tu plan de proveedor. Si cubres el 10% de las llamadas, tu volumen efectivo de solicitudes aumenta un 10%; si cubres agresivamente con un temporizador bajo, puede acercarse al 100% de sobrecarga. Esto importa porque la mayoría de los proveedores RPC facturan por unidades de cómputo o recuento de solicitudes, y los límites de velocidad se aplican por método. Consulta Precios de RPC para saber cómo se calcula típicamente el peso.

El impacto en el presupuesto es específico del método. Un eth_call pesado con un límite de gas grande puede costar muchas más unidades que un eth_blockNumber ligero. Cubrir la llamada pesada duplica un costo grande; cubrir la llamada ligera duplica uno pequeño. Prioriza la cobertura para métodos que son tanto frecuentes como propensos a la cola, y evita cubrir métodos que ya son rápidos o baratos.

También considera la respuesta 429. Si un proveedor devuelve 429 Too Many Requests, eso es una señal para retroceder, no para cubrir. Cubrir un 429 duplica la violación del límite de velocidad y puede empeorar la limitación. Trata el 429 como un error no cubrible y enrútalo a lógica de conmutación por error o retroceso. Para estrategias de agrupación que reducen el recuento de llamadas, consulta Solicitudes por lotes JSON-RPC y mejores prácticas.

  • La cobertura agrega volumen de solicitudes proporcional a la tasa de cobertura; presupuesta en consecuencia.
  • Los métodos pesados cuestan más por duplicado; cubre selectivamente.
  • Nunca cubras un 429; retrocede o conmuta por error en su lugar.

Requisitos de selección de proveedores para una cobertura efectiva

La cobertura solo funciona si los endpoints son genuinamente independientes. Si ambos endpoints comparten el mismo proveedor, región o infraestructura upstream, un único evento de caída o congestión ralentizará ambos, y la cobertura no aportará ningún beneficio. La documentación de gRPC enfatiza que la cobertura asume dominios de fallo independientes. Para RPC de blockchain, esto significa usar diferentes proveedores o al menos diferentes regiones y tipos de nodo.

Al seleccionar endpoints, verifica que no estén detrás del mismo balanceador de carga o borde de CDN. Una verificación rápida es comparar la IP del servidor o el emisor del certificado TLS entre endpoints. Si coinciden, los endpoints pueden compartir destino. Para obtener orientación sobre cómo evaluar proveedores, consulta Cómo elegir un proveedor RPC (RPC Assistant).

El servicio API de OnFinality proporciona endpoints multirregión, pero aún debes confirmar la independencia si mezclas proveedores. El objetivo es que una ralentización transitoria en un endpoint no afecte al otro. Si estás construyendo sobre Ethereum, consulta Endpoints de la red Ethereum para ver las opciones disponibles.

  • Usa diferentes proveedores o regiones para garantizar dominios de fallo independientes.
  • Verifica la IP del servidor y el emisor TLS para detectar infraestructura compartida.
  • Evita cubrir contra endpoints redundantes de un solo proveedor si comparten destino.

Ejemplo ejecutable en TypeScript: cobertura a N con AbortController

El siguiente ejemplo de Node.js implementa un cliente JSON-RPC con cobertura. Envía la solicitud al primer endpoint, arma un temporizador con un retardo p95 configurable, luego envía duplicados a endpoints adicionales. La primera respuesta exitosa resuelve la promesa; todas las demás solicitudes en vuelo se abortan. También limita el número de coberturas pendientes por llamada.

El código usa la API fetch nativa y AbortController, disponibles en Node.js 18+. Asume métodos de solo lectura y no reintenta en 429. Puedes adaptar la lista de endpoints y el retardo a tu entorno. El ejemplo registra la latencia por intento para que puedas derivar el temporizador p95 empíricamente.

import { performance } from 'node:perf_hooks';

interface HedgeOptions {
  endpoints: string[];
  method: string;
  params: any[];
  p95DelayMs: number;
  maxHedges?: number;
}

async function hedgedRpc({ endpoints, method, params, p95DelayMs, maxHedges = 2 }: HedgeOptions): Promise<any> {
  const controllers: AbortController[] = [];
  const attempts: Promise<any>[] = [];
  let settled = false;

  const send = (url: string, index: number) => {
    const controller = new AbortController();
    controllers.push(controller);
    const start = performance.now();
    return fetch(url, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ jsonrpc: '2.0', id: index, method, params }),
      signal: controller.signal,
    })
      .then(async (res) => {
        if (res.status === 429) throw new Error('rate-limited');
        const json = await res.json();
        if (json.error) throw new Error(json.error.message);
        const elapsed = performance.now() - start;
        console.log(`endpoint ${index} succeeded in ${elapsed.toFixed(1)} ms`);
        return json.result;
      })
      .catch((err) => {
        if (err.name === 'AbortError') return new Promise(() => {});
        throw err;
      });
  };

  // Send to first endpoint immediately
  attempts.push(send(endpoints[0], 0));

  // Arm timer for hedge
  const timer = setTimeout(() => {
    if (settled) return;
    const remaining = Math.min(maxHedges, endpoints.length - 1);
    for (let i = 1; i <= remaining; i++) {
      attempts.push(send(endpoints[i], i));
    }
  }, p95DelayMs);

  try {
    const result = await Promise.race(attempts);
    settled = true;
    clearTimeout(timer);
    controllers.forEach((c) => c.abort());
    return result;
  } catch (err) {
    clearTimeout(timer);
    throw err;
  }
}

// Example usage
const endpoints = [
  'https://eth-mainnet.example-provider-a.com',
  'https://eth-mainnet.example-provider-b.com',
];

hedgedRpc({
  endpoints,
  method: 'eth_getBalance',
  params: ['0x0000000000000000000000000000000000000000', 'latest'],
  p95DelayMs: 150,
}).then(console.log).catch(console.error);

Derivación del temporizador p95 a partir de la latencia por solicitud

El retardo de cobertura debe basarse en tu propia latencia medida, no en una constante adivinada. Instrumenta cada llamada RPC con una marca de tiempo de inicio y fin, registra el nombre del método, el endpoint y el resultado, y calcula percentiles en una ventana móvil. El p95 de las llamadas exitosas es un temporizador inicial razonable: significa que la cobertura se dispara solo cuando el primario es más lento que el 95% de las llamadas normales.

Puedes calcular percentiles en proceso o exportar métricas a un sistema de monitoreo. Para una discusión más profunda sobre métricas y conmutación por error, consulta Monitoreo de nodos RPC, métricas y conmutación por error. Comienza con un temporizador conservador (por ejemplo, p95) y bájalo gradualmente mientras observas el volumen de solicitudes y el costo.

La siguiente tabla es una plantilla para tus propias mediciones. Complétala con datos de tus endpoints; no confíes en números genéricos. El objetivo es ver la caída de p99 después de habilitar la cobertura, mientras confirmas que la mediana y el volumen de solicitudes cambian como se espera.

  • Registra método, endpoint, hora de inicio, hora de fin y éxito/fallo para cada llamada.
  • Calcula p50, p95 y p99 en una ventana móvil (por ejemplo, 5 minutos).
  • Establece el temporizador de cobertura en el p95 de las llamadas exitosas, luego ajústalo a la baja con cautela.
  • Monitorea el volumen de solicitudes y la tasa de 429 después de habilitar la cobertura.

Tabla de resultados: medir la caída de p99 frente a tus propios endpoints

Usa la siguiente tabla para registrar tus mediciones antes y después. Ejecuta una prueba de carga controlada que emita el mismo método de lectura repetidamente (por ejemplo, 10,000 llamadas) contra tu conjunto de endpoints, primero sin cobertura y luego con cobertura habilitada. Captura p50, p95, p99 y el recuento total de solicitudes. El resultado esperado es que el p99 caiga significativamente mientras que el p50 permanezca aproximadamente igual y el recuento de solicitudes aumente.

No trates ninguna cifra de latencia específica como universal. La latencia varía según el proveedor, la región, el método y la hora del día. El valor de este ejercicio es el cambio relativo que observas en tu propio entorno. Si el p99 no mejora, verifica que tus endpoints sean verdaderamente independientes y que el temporizador de cobertura no sea demasiado alto.

Para una visión más amplia de la medición de latencia, consulta Latencia RPC: causas, medición y soluciones. Para problemas específicos de tiempos de espera, consulta Errores de tiempo de espera RPC: causas y soluciones.

  • Métrica | Sin cobertura | Con cobertura | Notas
  • Latencia p50 | completar | completar | debería ser similar
  • Latencia p95 | completar | completar | puede mejorar ligeramente
  • Latencia p99 | completar | completar | objetivo principal
  • Solicitudes totales | completar | completar | se espera un aumento
  • Tasa de 429 | completar | completar | monitorear limitación

Fallos comunes y solución de problemas

El fallo más peligroso es cubrir una escritura no idempotente. Si cubres eth_sendRawTransaction, ambos endpoints pueden aceptar la transacción, lo que resulta en un doble envío o conflicto de nonce. Restringe siempre la cobertura a métodos de solo lectura. Si necesitas confiabilidad para escrituras, usa reintentos con claves de idempotencia o un gestor de transacciones.

Otro error común es cubrir contra endpoints de un solo proveedor que comparten infraestructura. Si ambos endpoints pasan por el mismo balanceador de carga, un único evento de congestión ralentiza ambos, y la cobertura es inútil. Verifica la independencia comprobando las IPs del servidor y los emisores TLS.

Un temporizador configurado demasiado bajo hace que ambas solicitudes se disparen en casi todas las llamadas, duplicando el costo sin una reducción de cola significativa. Un temporizador configurado demasiado alto significa que la cobertura rara vez se dispara. Monitorea la tasa de disparo de la cobertura y ajústala. Finalmente, nunca cubras una respuesta 429; eso es una señal para retroceder, no para duplicar. Si ves 429, reduce tu tasa de solicitudes o cambia a un proveedor con límites más altos.

  • Nunca cubras eth_sendRawTransaction ni ningún método de escritura.
  • Verifica la independencia de endpoints; evita infraestructura compartida.
  • Ajusta el temporizador: demasiado bajo duplica el costo, demasiado alto pierde la cola.
  • No cubras 429; retrocede o conmuta por error en su lugar.
  • Cancelar una solicitud que ya se confirmó es inofensivo para lecturas pero peligroso para escrituras.

Limitaciones y compensaciones de la cobertura de solicitudes

La cobertura no puede corregir un nodo sistemáticamente incorrecto o retrasado. Si un endpoint devuelve consistentemente datos obsoletos o resultados incorrectos, competir contra un endpoint correcto aún puede devolver la respuesta incorrecta si la respuesta obsoleta llega primero. La cobertura reduce la varianza de latencia, no los errores de corrección. Para la corrección, usa verificaciones de altura de bloque o lecturas de quórum.

La cobertura intercambia volumen de solicitudes adicional por reducción de cola. Si tu carga de trabajo ya está dentro del presupuesto y tu p99 es aceptable, la cobertura agrega costo sin beneficio. También es inútil cuando un endpoint ya es rápido y confiable; la cobertura rara vez se disparará, y la complejidad añadida puede no justificarse.

Finalmente, la cobertura no sustituye una planificación de capacidad adecuada. Si tu proveedor te está limitando la velocidad, la cobertura empeorará el problema. Usa la cobertura como una herramienta entre muchas, junto con conmutación por error, agrupación y almacenamiento en caché. Para una visión holística, consulta el centro de aprendizaje de OnFinality.

  • No corrige datos obsoletos o incorrectos; usa quórum o verificaciones de altura.
  • Agrega volumen de solicitudes; solo vale la pena si el p99 importa y el presupuesto lo permite.
  • Inútil si un endpoint ya es rápido; agrega complejidad.
  • No es una solución para la limitación de velocidad; puede empeorar los 429.

Próximos pasos: integrar la cobertura en tu stack de RPC

Comienza instrumentando tus llamadas RPC para medir p50, p95 y p99 por método. Identifica los métodos de lectura que más contribuyen a la latencia de cola visible para el usuario. Luego implementa cobertura para esos métodos usando el ejemplo de TypeScript anterior, con un temporizador conservador y un límite de coberturas pendientes.

Ejecuta una prueba de carga controlada y completa la tabla de resultados. Compara el p99 antes y después, y monitorea el volumen de solicitudes y la tasa de 429. Si el p99 mejora sin un costo excesivo, despliega gradualmente. Si no, revisa la independencia de endpoints y el ajuste del temporizador.

Para producción, considera combinar la cobertura con conmutación por error y agrupación. Revisa Precios de RPC para entender las implicaciones de costo, y explora el servicio API para endpoints multirregión. Para la selección de proveedores, consulta Cómo elegir un proveedor RPC (RPC Assistant).

  • Instrumenta primero; cubre solo los métodos de lectura propensos a la cola.
  • Usa un temporizador p95 conservador y limita las coberturas.
  • Mide la mejora de p99 y el impacto en el costo antes del despliegue completo.
  • Combina con conmutación por error y agrupación para una estrategia completa.

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