Ir al contenido principal
IngenieríaMar 28, 2026

Deep EVM #23: Depuración de Rendimiento — Cuando las Lecturas de Base de Datos Matan Tu Latencia

OS
Open Soft Team

Engineering Team

El problema de la amplificación de lecturas

En un bot de MEV de producción, observamos latencias de 50-200ms para operaciones que deberían tardar <5ms. La causa: amplificación de lecturas de base de datos — leer N entidades cuando solo K << N eran necesarias.

Diagnóstico con tracing

El primer paso es instrumentar el código:

use tracing::{instrument, info_span};

#[instrument(skip(db), fields(pool_count))]
async fn update_reserves(db: &Database, pools: &[Address]) -> Result<()> {
    let span = info_span!("update_reserves");
    let _enter = span.enter();
    
    tracing::Span::current()
        .record("pool_count", pools.len());
    
    for pool in pools {
        let _read_span = info_span!("db_read", pool = %pool).entered();
        let reserves = db.get_reserves(pool).await?;
        // ...
    }
    
    Ok(())
}

Los traces revelaron que update_reserves leía 15,000 pools cuando solo 200 habían cambiado en el bloque.

La solución: O(afectados) en vez de O(total)

// Antes: O(N) — lee todos los pools
async fn update_all_reserves(db: &Database) -> Result<()> {
    let all_pools = db.get_all_pools().await?; // 15,000 pools!
    for pool in &all_pools {
        let reserves = fetch_reserves(pool).await?;
        db.save_reserves(pool, reserves).await?;
    }
    Ok(())
}

// Después: O(K) — solo los afectados
async fn update_changed_reserves(
    db: &Database,
    block_logs: &[Log],
) -> Result<()> {
    let affected_pools = extract_sync_events(block_logs); // ~200 pools
    for pool in &affected_pools {
        let reserves = parse_sync_event(pool);
        db.save_reserves(&pool.address, reserves).await?;
    }
    Ok(())
}

Resultado: latencia de 150ms a 3ms.

CacheDB para lecturas repetidas

Para lecturas que se repiten durante la simulación:

struct CacheDB {
    cache: DashMap<(Address, U256), U256>,
    underlying: Arc<dyn Database>,
}

impl CacheDB {
    async fn sload(&self, address: Address, slot: U256) -> Result<U256> {
        if let Some(value) = self.cache.get(&(address, slot)) {
            return Ok(*value);
        }
        
        let value = self.underlying.sload(address, slot).await?;
        self.cache.insert((address, slot), value);
        Ok(value)
    }
}

Profiling con flamegraphs

# Con perf + inferno
cargo build --release
perf record -g --call-graph dwarf target/release/mev-bot
perf script | inferno-collapse-perf | inferno-flamegraph > flamegraph.svg

El flamegraph mostró que el 70% del tiempo se gastaba en llamadas a MDBX, confirmando que la base de datos era el cuello de botella.

Lecciones aprendidas

  1. Mide antes de optimizar — tracing y flamegraphs revelan los problemas reales
  2. O(afectados) > O(total) — Solo procesa lo que cambió
  3. Cachea lecturas calientes — CacheDB elimina lecturas redundantes
  4. Batch reads — Leer múltiples valores en una sola llamada a la DB
  5. Monitoreo continuo — Las regresiones de latencia se detectan rápido con métricas

Conclusión

La amplificación de lecturas es un problema insidioso que se esconde detrás de abstracciones aparentemente simples. Instrumentación con tracing, profiling con flamegraphs, y patrones como CacheDB y procesamiento incremental son las herramientas para diagnosticar y resolver estos problemas de rendimiento.

Etiquetas