Passer à PostgreSQL avec 800 millions d'utilisateurs de ChatGPT
Par Bohan Zhang, membre du personnel technique
Depuis des années, PostgreSQL est l’un des systèmes de données les plus critiques, en coulisses, qui alimentent des produits phares tels que ChatGPT et l’API d’OpenAI. Alors que notre base d’utilisateurs croît rapidement, les demandes sur nos bases de données ont également augmenté de façon exponentielle. Au cours de l'année écoulée, notre charge PostgreSQL a été multipliée par plus de dix, et elle continue d'augmenter rapidement.
Nos efforts pour faire progresser notre infrastructure de production afin de soutenir cette croissance ont révélé un nouvel aperçu : PostgreSQL peut être mis à l’échelle de manière fiable pour prendre en charge des charges de travail beaucoup plus importantes axées sur la lecture que ce que beaucoup pensaient possible auparavant. Ce système (initialement créé par une équipe de scientifiques de l'université de Californie à Berkeley) nous a permis de prendre en charge un trafic mondial massif avec une seule instance de serveur flexible Azure PostgreSQL(s'ouvre dans une nouvelle fenêtre) principale et près de 50 répliques en lecture réparties dans plusieurs régions à travers le monde. Voici l’histoire de la façon dont nous avons fait évoluer PostgreSQL chez OpenAI pour prendre en charge des millions de requêtes par seconde pour 800 millions d’utilisateurs grâce à des optimisations rigoureuses et à une ingénierie solide; nous aborderons également les principaux enseignements que nous avons tirés en cours de route.
Après le lancement de ChatGPT, le trafic a augmenté à un rythme sans précédent. Pour le soutenir, nous avons rapidement mis en œuvre d'importantes optimisations aux niveaux de l'application et de la base de données PostgreSQL, augmenté la capacité en augmentant la taille de l'instance, et élargi la capacité en ajoutant davantage de répliques de lecture. Cette architecture nous a bien servi pendant longtemps. Grâce à des améliorations continues, elle continue d’offrir une marge suffisante pour la croissance future.
Il peut sembler surprenant qu'une architecture à primaire unique puisse répondre aux exigences d'OpenAI en termes d'échelle; cependant, la mise en œuvre pratique de cette architecture n'est pas simple. Nous avons constaté plusieurs SEVs causés par une surcharge de Postgres, et ils suivent souvent le même schéma : un problème en amont provoque un pic soudain de charge sur la base de données, comme des ratés de cache généralisés à la suite d’une défaillance de la couche de mise en cache, une hausse de jointures coûteuses à plusieurs tables saturant le CPU, ou une tempête d’écritures lors du lancement d’une nouvelle fonctionnalité. À mesure que l’utilisation des ressources augmente, la latence des requêtes s’accroît et les demandes commencent à expirer. Les nouvelles tentatives amplifient ensuite la charge, déclenchant un cercle vicieux pouvant dégrader l'ensemble des services ChatGPT et API.
Bien que PostgreSQL s'adapte bien à nos charges de travail axées sur la lecture, nous rencontrons encore des défis lors des périodes de trafic d'écriture intense. Cela est en grande partie dû à l’implémentation du contrôle de concurrence multiversion (MVCC) de PostgreSQL, ce qui le rend moins efficace pour les charges de travail à forte intensité d’écriture. Par exemple, lorsqu'une requête met à jour un tuple ou même un seul champ, la ligne entière est copiée pour créer une nouvelle version. Sous de lourdes charges d'écriture, cela entraîne une amplification significative de l'écriture. Cela augmente également l’amplification de lecture, car les requêtes doivent parcourir plusieurs versions de tuples (tuples obsolètes) pour récupérer la plus récente. MVCC introduit des défis supplémentaires, tels que le gonflement des tables et des index, l'augmentation de la surcharge de maintenance des index et le réglage complexe de l'autovacuum. (Vous pouvez trouver une analyse approfondie de ces problèmes dans un billet de blogue que j’ai écrit avec le Prof. Andy Pavlo à la Carnegie Mellon University intitulé The Part of PostgreSQL We Hate the Most(s'ouvre dans une nouvelle fenêtre), cité(s'ouvre dans une nouvelle fenêtre) dans la page Wikipédia de PostgreSQL.)
Pour atténuer ces limitations et réduire la pression d'écriture, nous avons migré, et continuons à migrer, les charges de travail fragmentables (c'est-à-dire celles qui peuvent être partitionnées horizontalement) et à forte intensité d'écriture vers des systèmes fragmentés tels qu'Azure Cosmos DB, en optimisant la logique des applications afin de minimiser les écritures inutiles. Nous n'autorisons plus l'ajout de nouvelles tables au déploiement actuel de PostgreSQL. Les nouvelles charges de travail sont par défaut attribuées aux systèmes fragmentés.
Même si notre infrastructure a évolué, PostgreSQL est resté non partitionné, avec une seule instance principale assurant toutes les écritures. La principale raison est que le partitionnement des charges de travail des applications existantes serait extrêmement complexe et long, nécessitant des modifications à des centaines d'endpoints d'application et pouvant prendre des mois, voire des années. Étant donné que nos charges de travail sont principalement axées sur la lecture et que nous avons mis en œuvre d’importantes optimisations, l’architecture actuelle offre encore une marge de manœuvre suffisante pour soutenir la croissance continue du trafic. Bien que nous n’écartions pas la possibilité de partitionner PostgreSQL à l’avenir, ce n’est pas une priorité à court terme compte tenu de la marge de manœuvre suffisante dont nous disposons pour la croissance actuelle et future.
Dans les sections suivantes, nous allons explorer les défis rencontrés et les optimisations approfondies que nous avons mises en œuvre pour les surmonter et prévenir de futures pannes, en poussant PostgreSQL à ses limites et en le faisant évoluer pour gérer des millions de requêtes par seconde (QPS).
Défi : Avec un seul rédacteur, une configuration à auteur unique ne peut pas évoluer pour gérer plus d'écritures. De fortes pointes d'écriture peuvent rapidement surcharger le serveur principal et avoir un impact sur des services tels que ChatGPT et notre API.
Solution : Nous réduisons autant que possible la charge sur le primaire, tant pour les lectures que pour les écritures, afin de nous assurer qu'il dispose d'une capacité suffisante pour gérer les pics d'écriture. Le trafic de lecture est délégué aux répliques dans la mesure du possible. Cependant, certaines requêtes de lecture doivent rester sur le serveur principal, car elles font partie de transactions d'écriture. Pour celles-ci, nous nous concentrons sur l'assurance de leur efficacité et sur l'évitement des requêtes lentes. Pour le trafic d'écriture, nous avons migré les charges de travail à forte intensité d'écriture et pouvant être fragmentées vers des systèmes fragmentés tels qu'Azure CosmosDB. Les charges de travail qui sont plus difficiles à fragmenter, mais qui génèrent néanmoins un volume élevé d'écritures, prennent plus de temps à migrer, et ce processus est toujours en cours. Nous avons également optimisé nos applications de manière intensive pour réduire la charge d'écriture; par exemple, nous avons corrigé des bogues applicatifs qui provoquaient des écritures redondantes et introduit des écritures différées, lorsque cela était approprié, pour atténuer les pics de trafic. De plus, lors du remplissage rétroactif des champs de table, nous appliquons des limites strictes pour éviter une pression d’écriture excessive.
Défi : Nous avons identifié plusieurs requêtes coûteuses dans PostgreSQL. Dans le passé, des pics soudains de volume dans ces requêtes consommaient de grandes quantités de CPU, ralentissant à la fois ChatGPT et les demandes API.
Solution : Quelques requêtes coûteuses, telles que celles qui joignent de nombreuses tables, peuvent considérablement dégrader, voire interrompre, l'ensemble du service. Nous devons optimiser en continu les requêtes PostgreSQL pour garantir leur efficacité et éviter les anti-modèles courants du traitement des transactions en ligne (OLTP). Par exemple, nous avons déjà identifié une requête extrêmement coûteuse qui joignait 12 tables, où les pics de cette requête étaient responsables de SEVs de haute gravité par le passé. Nous devrions éviter les jointures complexes entre plusieurs tables autant que possible. Si des jointures sont nécessaires, nous avons appris à envisager de décomposer la requête et à déplacer la logique complexe de jointure vers la couche applicative. Bon nombre de ces requêtes problématiques sont générées par des frameworks de mappage objet-relationnel (ORM), il est donc important d’examiner attentivement le SQL qu’ils produisent et de s’assurer qu’il se comporte comme prévu. Il est également courant de trouver des requêtes inactives de longue durée dans PostgreSQL. La configuration de délais d'expiration tels que idle_in_transaction_session_timeout est essentielle pour éviter qu'ils ne bloquent l'autovacuum.
Défi : Si une réplique de lecture tombe en panne, le trafic peut toujours être redirigé vers d'autres répliques. Cependant, s'appuyer sur un seul rédacteur signifie avoir un point de défaillance unique—si celui-ci échoue, l'ensemble du service est affecté.
Solution : La plupart des demandes les plus critiques ne concernent que des requêtes de lecture. Pour atténuer le point de défaillance unique du primaire, nous avons transféré ces lectures du nœud d'écriture vers des répliques, garantissant que ces demandes puissent continuer à être traitées même si le primaire tombe en panne. Bien que les opérations d’écriture échouent toujours, l’impact est réduit; ce n’est plus un SEV0 puisque les opérations de lecture restent disponibles.
Pour atténuer les défaillances du système principal, nous exécutons le système principal en mode haute disponibilité (HA) avec un système de secours actif, une réplique synchronisée en continu qui est toujours prête à prendre le relais pour gérer le trafic. Si le système principal tombe en panne ou doit être mis hors ligne pour maintenance, nous pouvons rapidement promouvoir le système de secours pour minimiser le temps d'arrêt. L’équipe Azure PostgreSQL a réalisé un travail important pour garantir que ces basculements restent sécurisés et fiables, même sous une charge très élevée. Pour gérer les défaillances des répliques de lecture, nous déployons plusieurs répliques dans chaque région avec une marge de capacité suffisante pour qu’une défaillance d’une seule réplique n’entraîne pas une panne régionale.
Défi : Nous rencontrons souvent des situations où certaines demandes consomment une quantité disproportionnée de ressources sur des instances PostgreSQL. Cela peut entraîner une dégradation des performances pour d'autres charges de travail s'exécutant sur les mêmes instances. Par exemple, le lancement d'une nouvelle fonctionnalité peut introduire des requêtes inefficaces qui consomment beaucoup de ressources CPU de PostgreSQL, ce qui ralentit les demandes pour d'autres fonctionnalités essentielles.
Solution : Nous exploitons près de 50 répliques de lecture dans plusieurs régions géographiques pour minimiser la latence. Cependant, avec l'architecture actuelle, le serveur principal doit transmettre le WAL à chaque réplique. Bien qu'elle s'adapte actuellement bien à des types d'instances très volumineux et à une bande passante réseau élevée, nous ne pouvons pas continuer à ajouter des répliques indéfiniment sans finir par surcharger le serveur principal. Pour remédier à cela, nous collaborons avec l'équipe Azure PostgreSQL sur la réplication en cascade(s'ouvre dans une nouvelle fenêtre), où les répliques intermédiaires relaient le WAL vers les répliques en aval. Cette approche nous permet de faire évoluer le système jusqu’à potentiellement plus d’une centaine de répliques sans surcharger l’instance principale. Cependant, cela introduit également une complexité opérationnelle supplémentaire, notamment en ce qui concerne la gestion du basculement. La fonctionnalité est encore en phase de test; nous veillerons à ce qu’elle soit robuste et qu’elle puisse basculer en toute sécurité avant de la déployer en production.
Défi : Un pic soudain de trafic sur des endpoint spécifiques, une augmentation des requêtes coûteuses ou une tempête de réessais peut rapidement épuiser des ressources critiques telles que le processeur, les entrées/sorties et les connexions, entraînant une dégradation généralisée du service.
Solution : Nous avons mis en place une limitation du débit à plusieurs niveaux — application, pool de connexions, proxy et requête — afin d’empêcher que des pics de trafic soudains ne submergent les instances de base de données et ne déclenchent des défaillances en cascade. Il est également crucial d’éviter des intervalles de réessai trop courts, ce qui peut déclencher des tempêtes de réessais. Nous avons également amélioré la couche ORM pour prendre en charge la limitation du débit et, si nécessaire, bloquer complètement certains digests de requêtes. Cette forme ciblée de délestage permet une reprise rapide après des pics soudains de requêtes coûteuses.
Défi : Même un petit changement de schéma, comme la modification du type d’une colonne, peut entraîner une réécriture complète de la table(s'ouvre dans une nouvelle fenêtre). Nous appliquons donc les changements de schéma avec prudence, en les limitant à des opérations légères et en évitant ceux qui réécrivent des tables entières.
Solution : Seules les modifications légères du schéma sont permises, telles que l’ajout ou la suppression de certaines colonnes qui ne déclenchent pas une réécriture complète de la table. Nous appliquons un délai d'attente strict de 5 secondes pour les modifications de schéma. La création et la suppression d'index simultanément sont autorisées. Les modifications du schéma sont limitées aux tables existantes. Si une nouvelle fonctionnalité nécessite des tables supplémentaires, elles doivent être dans des systèmes partitionnés alternatifs tels qu’Azure CosmosDB plutôt que PostgreSQL. Lorsque nous remplissons rétroactivement un champ de table, nous appliquons des limites strictes pour éviter les pics d'écriture. Bien que ce processus puisse parfois prendre plus d'une semaine, il assure la stabilité et évite tout impact sur la production.
Cet effort démontre qu’avec une conception et des optimisations appropriées, Azure PostgreSQL peut être mis à l’échelle pour gérer les plus grandes charges de travail en production. PostgreSQL gère des millions de QPS pour des charges de travail axées sur la lecture, alimentant les produits les plus critiques d’OpenAI, tels que ChatGPT et la plateforme API. Nous avons ajouté près de 50 répliques de lecture, tout en maintenant un décalage de réplication proche de zéro, conservé des lectures à faible latence dans l’ensemble des régions géodistribuées et constitué une marge de capacité suffisante pour soutenir la croissance future.
Cette mise à l’échelle fonctionne tout en réduisant la latence et en augmentant la fiabilité. Nous offrons systématiquement une latence côté client p99 de quelques dizaines de millisecondes et une disponibilité de 99,999 % en production. Et au cours des 12 derniers mois, nous n’avons eu qu’un seul incident SEV-0 PostgreSQL (il s’est produit lors du lancement viral(s'ouvre dans une nouvelle fenêtre) de ChatGPT ImageGen, lorsque le trafic d’écriture a soudainement augmenté de plus de 10 fois, alors que plus de 100 millions de nouveaux utilisateurs se sont inscrits en une semaine.)
Bien que nous soyons satisfaits des progrès réalisés grâce à PostgreSQL, nous continuons à repousser ses limites afin de nous assurer une marge de manœuvre suffisante pour notre croissance future. Nous avons déjà migré les charges de travail à forte intensité d’écriture pouvant être fragmentées vers nos systèmes fragmentés comme CosmosDB. Les charges de travail restantes à forte intensité d’écriture sont plus difficiles à fragmenter—nous migrons activement celles-ci également afin de décharger davantage les écritures du serveur principal PostgreSQL. Nous collaborons également avec Azure pour activer la réplication en cascade, ce qui nous permettra d'augmenter en toute sécurité le nombre de répliques de lecture.
À l’avenir, nous continuerons d’explorer d’autres approches pour accroître notre capacité de mise à l’échelle, notamment avec PostgreSQL partitionné ou d’autres systèmes distribués, à mesure que les exigences de notre infrastructure continuent de croître.
Auteur
Remerciements
Nous tenons à remercier tout particulièrement Jon Lee, Sicheng Liu, Chaomin Yu et Chenglong Hao pour leur contribution à cet article, ainsi que toute l’équipe qui a aidé à faire évoluer PostgreSQL. Nous souhaitons également remercier l’équipe Azure PostgreSQL pour leur solide partenariat.


