Ir al contenido principal
IngenieurwesenMar 28, 2026

Deep EVM #23: Performance-Debugging — Wenn Datenbank-Lesevorgaenge Ihre Latenz toeten

OS
Open Soft Team

Engineering Team

Das Problem

Ihr Monitoring-Dashboard leuchtet rot auf. Antwortzeiten steigen von 50ms auf 114 Sekunden. 500-Fehler kaskadieren ueber alle Endpunkte. Etwas wuergt das System, und es ist kein offensichtlicher Ressourcenengpass.

Leseamplifizierung diagnostizieren

Leseamplifizierung tritt auf, wenn eine einzelne logische Abfrage mehrere physische Lesevorgaenge ausloest:

// Logische Operation: "Gib mir die Balance von Adresse X"
// Physische Operationen in MDBX/RocksDB:
// 1. Index-Lookup im B-Tree (3-4 Seiten)
// 2. Daten-Seite lesen
// 3. Wenn komprimiert: dekomprimieren
// 4. Wenn Seite nicht im Cache: Disk I/O

// Amplifizierung: 1 logischer Read -> 4-5 physische Reads

Fallstudie: MEV-Bot mit MDBX

// Problem: O(N) Lesevorgaenge bei jedem Block
async fn process_block(db: &Database, block: &Block) -> Result<()> {
    for tx in &block.transactions {
        // Fuer jede Transaktion: Balance lesen
        let balance = db.get_balance(tx.from)?; // MDBX Read
        let nonce = db.get_nonce(tx.from)?;     // MDBX Read
        
        // Problem: N Transaktionen = N * 2 Datenbank-Reads
        // Bei 300 Transaktionen = 600 Reads pro Block
    }
    Ok(())
}

Die Loesung: CacheDB-Muster

struct CacheDB {
    balance_cache: DashMap<Address, U256>,
    nonce_cache: DashMap<Address, u64>,
    underlying: Arc<Database>,
}

impl CacheDB {
    fn get_balance(&self, addr: &Address) -> Result<U256> {
        // Zuerst im Cache suchen
        if let Some(bal) = self.balance_cache.get(addr) {
            return Ok(*bal);
        }
        // Cache Miss: Datenbank abfragen und cachen
        let bal = self.underlying.get_balance(addr)?;
        self.balance_cache.insert(*addr, bal);
        Ok(bal)
    }
    
    fn apply_block(&self, changes: &StateChanges) {
        // Nur geaenderte Eintraege aktualisieren: O(betroffene)
        for (addr, new_balance) in &changes.balances {
            self.balance_cache.insert(*addr, *new_balance);
        }
    }
}

O(N) vs. O(betroffene)

Der Schluesselunterschied:

  • O(N): Bei jedem Block ALLE N relevanten Zustaende aus der Datenbank lesen
  • O(betroffene): Nur die Zustaende aktualisieren, die sich tatsaechlich geaendert haben

Bei 34 Millionen Adressen und 300 Transaktionen pro Block: O(N) = 34M Lesevorgaenge, O(betroffene) = ~500 Lesevorgaenge. Der Unterschied: 5 Groessenordnungen.

Monitoring und Messung

use tracing::{instrument, info_span};

#[instrument(skip(db))]
async fn process_block(db: &CacheDB, block: &Block) -> Result<()> {
    let span = info_span!("process_block", block_number = block.number);
    let _guard = span.enter();
    
    let cache_hits = AtomicU64::new(0);
    let cache_misses = AtomicU64::new(0);
    
    // ... Verarbeitung ...
    
    tracing::info!(
        hits = cache_hits.load(Ordering::Relaxed),
        misses = cache_misses.load(Ordering::Relaxed),
        "Cache-Statistiken fuer Block {}", block.number
    );
}

Fazit

Datenbank-Leseamplifizierung ist ein stiller Performance-Killer. Die Loesung: CacheDB als Zwischenschicht, die O(betroffene) Aktualisierungen statt O(N) Lesevorgaenge ermoeglicht. Messen Sie immer mit Tracing — raten Sie nie.