La création de files d'attente de messages et de microservices fiables dans des bases de données cloud distribuées telles que Spanner présente des défis uniques. Ce document décrit les concepts de base et les modèles de conception utilisés pour exécuter le traitement "exactement une fois" et l'accusé de réception "au maximum une fois" dans les files d'attente Spanner.
Concepts fondamentaux et dilemme de l'idempotence
L'idempotence signifie que, quel que soit le nombre de fois où une opération est exécutée, le résultat est toujours le même. Lorsque vous créez des files d'attente de messages ou des systèmes de distribution de tâches, il existe trois méthodes de distribution principales qui traitent l'idempotence :
- At-least-once: : les messages sont garantis d'être distribués et traités. En cas de défaillance du réseau, un message peut être distribué et traité plusieurs fois.
- At-most-once: : les messages sont distribués au maximum une fois. Le traitement des doublons est empêché, mais en cas d'échec, le message peut être perdu ou supprimé.
- Exactement une fois : chaque message est traité exactement une fois, sans être perdu ni dupliqué.
Les files d'attente Spanner fournissent automatiquement une distribution au moins une fois et un accusé de réception au plus une fois. Toutefois, vous pouvez utiliser les modèles de conception et les exemples décrits dans les sections suivantes pour réduire de manière programmatique le traitement et la rediffusion des doublons.
Problème d'état de commit inconnu
Dans une base de données à une seule machine, les transactions réussissent ou échouent. Dans une base de données cloud distribuée, les transactions peuvent être perdues lorsque les données sont répliquées dans plusieurs centres de données physiques.
Lorsque votre application traite un message et valide un accusé de réception tel que DELETE FROM MessageQueue WHERE message_id = '123' ASSERT_ROWS_MODIFIED 1, la requête transite par plusieurs sauts de réseau :
[Your App] <--(gRPC)--> [Spanner API] <--(Google network)--> [Spanner Storage]
Lorsque le leader du stockage Spanner reçoit la validation, il la synchronise sur les répliques de stockage. Une fois le consensus atteint et les données écrites sur le disque, la transaction est validée dans la base de données.
Toutefois, une défaillance réseau temporaire (par exemple, un redémarrage du proxy ou une déconnexion du réseau) peut se produire après l'enregistrement des données sur le disque, mais avant que la confirmation de réussite n'atteigne votre application. Dans ce cas, votre application reçoit une erreur de délai d'attente du réseau (DEADLINE_EXCEEDED ou UNAVAILABLE).
Comme la connexion a été interrompue hors bande, votre application ne peut pas savoir si elle a été interrompue avant ou après la réussite de la validation. C'est ce qu'on appelle le problème d'état de commit inconnu.
Pourquoi les nouvelles tentatives automatiques peuvent être dangereuses
Si votre application intercepte une erreur de délai d'attente du réseau et réessaie exactement la même transaction sans déduplication, elle risque d'exécuter la logique métier deux fois. Par exemple, si la transaction impliquait de débiter la carte de crédit d'un client ou d'expédier un article, une nouvelle tentative pour une transaction réussie (mais non confirmée) entraîne un débit ou une expédition en double.
Pour protéger vos utilisateurs, les bibliothèques clientes officielles Google Cloud ne relancent jamais automatiquement les transactions ayant échoué en raison d'erreurs de commit inconnues. Ils génèrent une erreur explicite vous informant que le résultat de la transaction est inconnu, laissant à votre code d'application le soin de gérer la nouvelle tentative de manière sécurisée à l'aide de modèles d'idempotence.
Stratégies concrètes pour le traitement de type "exactement une fois"
Pour réessayer les transactions de manière sécurisée et réduire le traitement en double et la nouvelle distribution des messages, implémentez l'un des principaux modèles de conception suivants.
Affirmer les lignes modifiées
Si toute votre logique métier (comme la mise à jour d'autres tables) et l'accusé de réception du message de la file d'attente se produisent dans une seule transaction Spanner, vous pouvez utiliser ASSERT_ROWS_MODIFIED dans votre instruction DELETE. Il s'agit de la stratégie la plus simple pour le traitement "exactement une fois", car elle ne nécessite pas de table de déduplication distincte.
En ajoutant ASSERT_ROWS_MODIFIED 1 à votre instruction, l'accusé de réception ne réussit que si un seul message a été supprimé. Si une défaillance du réseau se produit et que votre application relance la transaction, le message n'existe plus dans la file d'attente. La nouvelle tentative supprime 0 ligne et l'instruction échoue avec OUT_OF_RANGE.
L'erreur OUT_OF_RANGE est permanente : la ligne a déjà disparu. Par conséquent, la réexécution de l'instruction modifiera toujours 0 ligne et échouera à nouveau. Il s'agit également d'une erreur au niveau de l'instruction : seule l'instruction DELETE échoue. La transaction reste ouverte, et toutes les écritures que vous avez déjà effectuées sont toujours mises en mémoire tampon et seront validées si vous continuez. Spanner n'annule ni ne restaure la transaction pour vous. Pour bénéficier de la protection exactement une fois prévue, vous devez vous assurer que la transaction est annulée :
- Laissez l'erreur se propager : si vous utilisez une API basée sur un runner (comme
ReadWriteTransactionen Go), laissez l'erreurOUT_OF_RANGEse propager hors de votre fonction de transaction. La bibliothèque cliente abandonnera la transaction et la restaurera, ce qui garantira qu'aucune écriture de logique métier n'est validée. - Ne pas intercepter et continuer : n'interceptez jamais l'erreur d'assertion à l'intérieur de la fonction de transaction et ne continuez pas. Si vous le faites, Spanner validera quand même vos autres instructions, ce qui entraînera la mise à jour exacte en double que ce modèle est censé empêcher.
- Annulations manuelles : si vous gérez les transactions manuellement (par exemple, avec
ReadWriteStmtBasedTransactionen Go ou les API REST/gRPC), vous devez appeler explicitement la méthode d'annulation correspondante lorsque l'assertion échoue.
Boîte d'envoi transactionnelle
Si votre logique métier ne peut pas se produire dans une seule transaction Spanner (comme les workflows en plusieurs étapes ou les mises à jour couvrant plusieurs systèmes), ou si le traitement des messages nécessite d'appeler des services externes (comme l'envoi d'un SMS ou le traitement d'un paiement), vous ne pouvez pas coupler de manière atomique la logique métier avec l'accusé de réception de la file d'attente.
Dans ces scénarios, n'appelez jamais d'API externes ni n'exécutez d'opérations non transactionnelles directement dans un bloc de transaction Spanner. Si Spanner relance ou abandonne la transaction, votre application peut exécuter cette logique métier plusieurs fois.
Votre application doit implémenter sa propre stratégie d'idempotence, mais vous pouvez utiliser les modèles suivants pour réduire le travail en double :
Opérations rapides (qui se terminent dans le délai de bail par défaut de 10 secondes) : exécutez la logique métier ou l'appel d'API externe lorsque le message est reçu. Accusez réception du message uniquement une fois l'opération réussie. Si l'opération échoue ou si le nœud de calcul plante avant d'accuser réception, n'accusez pas réception du message. Le bail expire et Spanner renvoie automatiquement le message pour une nouvelle tentative.
Opérations de longue durée (plus de 10 secondes) : accusez réception du message lorsqu'il est reçu, puis, dans la même transaction, renvoyez un nouveau message programmé pour une diffusion ultérieure (en utilisant un délai de diffusion supérieur au temps d'exécution estimé). Vous pouvez également étendre périodiquement le bail du message à l'aide du TVF
RENEWLEASE_QUEUE_NAME(). Lorsque la logique métier ou l'appel externe réussissent, confirmez le message nouvellement mis en file d'attente ou prolongé.
Idempotence des sessions multiplexées
Les sessions Spanner peuvent être multiplexées. Les sessions multiplexées suivent les états des transactions en mémoire dans les sessions partagées. Cela protège contre les défaillances réseau côté client en permettant aux bibliothèques clientes de se reconnecter et de résoudre plus facilement les états d'engagement inconnus.
Toutefois, les sessions multiplexées ne peuvent pas éliminer les résultats d'engagement inconnus si un serveur frontend plante. Si le serveur spécifique hébergeant la table des transactions de votre session redémarre immédiatement après un commit, l'état en mémoire est perdu, ce qui renvoie une erreur d'issue de transaction inconnue lors de la reconnexion. Pour faire face à ce risque, implémentez les schémas d'idempotence sur cette page.
Étapes suivantes
- Découvrez comment utiliser les files d'attente Spanner, y compris les bonnes pratiques et la surveillance.
- Découvrez d'autres scénarios et exemples de files d'attente Spanner.
- Configurez le contrôle des accès avec le contrôle des accès précis pour les files d'attente.