Database Engineering

Optimisation des chemins d'écriture dans l'Event Sourcing : Batching à haut débit et stratégies append-only pour le CQRS

L'Event Sourcing (ES) combiné à la ségrégation des responsabilités de commande et de requête (CQRS) est devenu l'ossature architecturale de nombreux systèmes à haute évolutivité. Si les avantages du côté lecture sont bien documentés, le côté écriture présente souvent un goulot d'étranglement majeur : l'écriture séquentielle des événements vers le backend de stockage. Lorsque des millions de commandes arrivent simultanément, des implémentations naïves peuvent souffrir de contention de transaction, d'escalade de verrouillage et de saturation des E/S. Dans cet article, nous explorons des stratégies avancées pour optimiser ces chemins d'écriture, en mettant l'accent sur le regroupement (batching) et les mécanismes append-only.

Le défi de la contention en écriture

Dans une implémentation standard d'Event Sourcing, chaque commande génère un ou plusieurs événements de domaine qui doivent être persistés. Si chaque événement déclenche une transaction de base de données distincte, la surcharge liée aux allers-retours réseau et à la journalisation des transactions peut réduire considérablement le débit. De plus, le contrôle de concurrence optimiste (OCC) nécessite de comparer les versions à chaque écriture. Si plusieurs commandes ciblent le même agrégat simultanément, elles peuvent échouer lors de la validation et devoir être réessayées, ce qui entraîne des problèmes de troupeau tonitruant (thundering herd).

Pour atteindre un haut débit, nous devons passer d'une insertion ligne par ligne à des opérations groupées et append-only. Cette approche minimise la surcharge de verrouillage et maximise l'efficacité des E/S.

Stratégies de regroupement à haut débit

Le regroupement des événements réduit le nombre d'allers-retours vers la base de données. Au lieu de valider les événements individuellement, l'application collecte les événements dans un tampon local et les vide par lots. Cependant, un regroupement naïf peut introduire de la latence. L'essentiel est d'équilibrer la taille du lot par rapport aux exigences en temps réel.

Un modèle courant implique un travailleur en arrière-plan qui surveille une file d'attente en mémoire. Lorsque la file d'attente atteint une certaine taille ou un seuil temporel, le travailleur valide le lot en tant qu'unité atomique unique. Cela garantit que soit tous les événements du lot sont persistés, soit aucun, maintenant ainsi la cohérence des données sans sacrifier les performances.

// Pseudo-code pour un bus d'événements à haut débit
class EventBatcher:
    def __init__(self, batch_size=100, flush_interval_ms=50):
        self.buffer = []
        self.batch_size = batch_size
        self.flush_interval = flush_interval_ms

    def append(self, event):
        self.buffer.append(event)
        if len(self.buffer) >= self.batch_size:
            self.flush()

    def flush(self):
        if not self.buffer:
            return
        
        # Début d'une seule transaction de base de données pour tout le lot
        try:
            with db.transaction():
                for event in self.buffer:
                    db.insert("events", event)
                db.commit()
        except IntegrityError:
            # Gérer les conflits de concurrence ici
            retry_buffer(self.buffer)
        finally:
            self.buffer = []

Cette stratégie réduit considérablement la surcharge du journal des transactions, car la base de données n'a besoin d'écrire qu'un seul enregistrement de commit pour des dizaines ou des centaines d'événements. Cependant, les développeurs doivent faire preuve de prudence : le regroupement augmente la surface d'impact d'une défaillance. Si un lot échoue, tous les événements de ce lot doivent être annulés, ce qui peut nécessiter une logique de réessai complexe.

Mécaniques de stockage append-only

L'Event Sourcing est intrinsèquement append-only. Contrairement aux bases de données relationnelles traditionnelles qui mettent à jour les lignes existantes (ce qui peut provoquer des splits de pages et de la fragmentation), l'ajout de nouveaux enregistrements permet des E/S séquentielles, qui sont bien plus rapides que les écritures aléatoires sur la plupart des supports de stockage.

Pour tirer parti de cette caractéristique, envisagez d'utiliser des structures de journaux append-only ou des bases de données conçues pour les écritures séquentielles, telles qu'Apache Kafka, Amazon Kinesis ou des bases de données d'événements spécialisées comme EventStoreDB. Ces systèmes sont optimisés pour un haut débit d'écriture en séparant complètement le chemin d'écriture du chemin de lecture.

Lors de la mise en œuvre de journaux append-only, assurez-vous que votre moteur de base de données prend en charge un partitionnement efficace. Le partitionnement par ID d'agrégat ou par ID de locataire garantit que les écritures d'événements liés restent localisées, réduisant davantage la contention des verrous. Par exemple, tous les événements d'un agrégat Commande unique doivent être écrits dans la même partition pour garantir une cohérence séquentielle.

Concurrence optimiste avec écritures groupées

La mise en œuvre de l'OCC dans un environnement groupé nécessite une gestion minutieuse des versions. Au lieu de vérifier la version avant chaque événement individuel, vous pouvez vérifier la version au début du lot. Si la version a changé depuis la réception de la commande, l'intégralité du lot est invalidée et réessayée.

Cette approche suppose qu'il est peu probable que l'état de l'agrégat change de manière significative pendant le temps nécessaire à l'écriture du lot. Bien qu'il s'agisse d'un compromis, cela permet une concurrence beaucoup plus élevée. Pour les systèmes nécessitant une linéarisation stricte, des mécanismes de verrouillage supplémentaires ou des numéros de séquence peuvent être nécessaires, mais pour la plupart des domaines métier, la cohérence éventuelle fournie par l'OCC groupée est acceptable.

Conclusion

L'optimisation du chemin d'écriture dans un système d'Event Sourcing est cruciale pour atteindre l'évolutivité que promet le CQRS. En combinant le regroupement à haut débit avec des stratégies de stockage append-only, les développeurs peuvent réduire considérablement la latence et augmenter le débit du système. L'essentiel réside dans l'équilibre des tailles de lot, la gestion gracieuse des conflits de concurrence et le choix de moteurs de stockage excellant dans les écritures séquentielles. À mesure que votre système grandit, ces optimisations feront la différence entre une application lente et une qui s'adapte parfaitement sous une charge lourde.

Share: