Aller au contenu principal
IngénierieMar 28, 2026

Deep EVM #23 : Débogage de performance — Quand les lectures base de données tuent votre latence

OS
Open Soft Team

Engineering Team

Le symptôme

Votre tableau de bord de monitoring s’allume en rouge. Les temps de réponse passent de 50 ms à 114 secondes. Les erreurs 500 cascadent sur tous les endpoints. Quelque chose étouffe le système, et ce n’est pas un goulot d’étranglement de ressources évident. Cet article trace le problème jusqu’à l’amplification de lectures en base de données.

Amplification de lectures : le problème

L’amplification de lectures se produit quand une opération logique unique déclenche un nombre disproportionné de lectures physiques en base de données.

Exemple concret : un indexeur blockchain qui met à jour les soldes des comptes après chaque bloc. L’approche naïve :

// O(N) — lit TOUS les comptes à chaque bloc
async fn update_balances(db: &Database, block: &Block) {
    let all_accounts = db.get_all_accounts().await?; // 500K comptes

    for tx in &block.transactions {
        // Mettre à jour le solde
        let account = all_accounts.get(&tx.from);
        // ...
    }
}

Avec 500 000 comptes et des blocs toutes les 12 secondes, cela génère des millions de lectures par minute. La base de données ne peut pas suivre.

Diagnostic

1. Métriques de la base de données

-- PostgreSQL : identifier les requêtes lentes
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

2. Traçage distribué

#[instrument(skip(db))]
async fn process_block(db: &Database, block: &Block) -> Result<()> {
    let span = tracing::info_span!("process_block", block_number = block.number);
    let _guard = span.enter();

    // Chaque opération DB est tracée
    let accounts = db.get_affected_accounts(block).instrument(span.clone()).await?;
    // ...
}

Le traçage révèle que 95 % du temps est passé dans les lectures de base de données.

La solution : O(N) -> O(affecté)

Au lieu de lire tous les comptes, lisez uniquement ceux affectés par les transactions du bloc :

// O(affecté) — lit seulement les comptes modifiés
async fn update_balances(db: &Database, block: &Block) {
    // Collecter les adresses uniques du bloc
    let affected: HashSet<Address> = block.transactions.iter()
        .flat_map(|tx| vec![tx.from, tx.to])
        .collect();

    // Lire seulement les comptes affectés
    let accounts = db.get_accounts_batch(&affected).await?; // ~200 comptes

    for tx in &block.transactions {
        let account = accounts.get(&tx.from);
        // ...
    }
}

Passage de 500 000 lectures à environ 200. Facteur d’amélioration : 2500x.

Le pattern CacheDB

Pour les systèmes qui lisent fréquemment les mêmes données, une couche de cache in-memory élimine les lectures répétées :

struct CacheDB {
    inner: Arc<dyn Database>,
    cache: DashMap<Address, Account>,
}

impl CacheDB {
    async fn get_account(&self, addr: &Address) -> Result<Account> {
        // Vérifier le cache d'abord
        if let Some(account) = self.cache.get(addr) {
            return Ok(account.clone());
        }

        // Miss — lire depuis la base
        let account = self.inner.get_account(addr).await?;
        self.cache.insert(*addr, account.clone());
        Ok(account)
    }

    fn invalidate_block(&self, block: &Block) {
        // Invalider uniquement les comptes modifiés
        for tx in &block.transactions {
            self.cache.remove(&tx.from);
            self.cache.remove(&tx.to);
        }
    }
}

Résultats

MétriqueAvantAprèsAmélioration
Lectures/bloc500K~2002500x
Latence P99114s45ms2533x
Erreurs 500~30%0%Éliminées
CPU base de données95%12%8x

Conclusion

L’amplification de lectures est un problème insidieux — le système fonctionne parfaitement avec peu de données et s’effondre à l’échelle. La solution est toujours la même : passez de O(N) à O(affecté), ajoutez une couche de cache avec invalidation ciblée, et instrumentez votre code avec du traçage distribué pour détecter les régressions tôt.