Mise à l'échelle de PostgreSQL pour prendre en charge 800 millions d'utilisateurs ChatGPT, par Bohan ZhangPostgreSQL, également connu sous le nom de Postgres, est un système de gestion de base de données relationnelle (SGBDR) libre et open source qui met l'accent sur l'extensibilité et la conformité SQL. PostgreSQL offre des transactions avec des propriétés d'atomicité, de cohérence, d'isolation et de durabilité (ACID), des vues automatiquement actualisables, des vues matérialisées, des déclencheurs, des clés étrangères et des procédures stockées. Il est pris en charge par tous les principaux systèmes d'exploitation, notamment Windows, Linux, macOS, FreeBSD et OpenBSD, et gère toute une gamme de charges de travail, des machines individuelles aux entrepôts de données, aux lacs de données ou aux services web avec de nombreux utilisateurs simultanés.
ChatGPT est un chatbot génératif basé sur l'intelligence artificielle développé par OpenAI. Il a été lancé en novembre 2022. Il utilise des transformateurs génératifs pré-entraînés (GPT), tels que GPT-5, pour générer du texte, de la parole et des images en réponse aux demandes des utilisateurs. OpenAI exploite le service selon un modèle freemium. Les utilisateurs peuvent interagir avec ChatGPT par le biais de requêtes textuelles, audio et visuelles. Le service a gagné 100 millions d'utilisateurs en deux mois, ce qui en fait l'application logicielle grand public à la croissance la plus rapide de l'histoire. Le site web de ChatGPT figure parmi les 5 sites web les plus visités au monde.
Depuis des années, PostgreSQL est l'un des systèmes de données les plus critiques et les plus discrets qui alimentent des produits phares tels que ChatGPT et l'API d'OpenAI. À mesure que notre base d'utilisateurs s'est rapidement développée, les exigences imposées à nos bases de données ont également augmenté de manière exponentielle. Au cours de l'année dernière, notre charge PostgreSQL a été multipliée par plus de 10 et continue d'augmenter rapidement.
Nos efforts pour faire évoluer notre infrastructure de production afin de soutenir cette croissance ont révélé une nouvelle perspective : PostgreSQL peut être mis à l'échelle pour prendre en charge de manière fiable des charges de travail beaucoup plus importantes 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 primaire et près de 50 répliques en lecture réparties dans plusieurs régions du monde. Voici comment nous avons évolué PostgreSQL chez OpenAI afin de 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 au cours de ce processus.
Les failles de notre conception initiale
Après le lancement de ChatGPT, le trafic a augmenté à un rythme sans précédent. Pour y faire face, nous avons rapidement mis en œuvre des optimisations approfondies au niveau des couches applicatives et de la base de données PostgreSQL, augmenté la taille des instances et ajouté des répliques en lecture. Cette architecture nous a bien servi pendant longtemps. Grâce à des améliorations continues, elle continue d'offrir une marge de manœuvre 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, mais la mise en œuvre pratique n'est pas simple. Nous avons constaté plusieurs SEV causés par une surcharge de Postgres, qui suivent souvent le même schéma : un problème en amont provoque une augmentation soudaine de la charge de la base de données, comme des échecs de cache généralisés dus à une défaillance de la couche de mise en cache, une augmentation des jointures multivoies coûteuses saturant le CPU ou une tempête d'écritures due au lancement d'une nouvelle fonctionnalité. À mesure que l'utilisation des ressources augmente, la latence des requêtes augmente et les demandes commencent à expirer. Les nouvelles tentatives amplifient alors encore la charge, déclenchant un cercle vicieux susceptible de dégrader l'ensemble des services ChatGPT et API.
Bien que PostgreSQL s'adapte bien à nos charges de travail à forte intensité de lecture, nous rencontrons encore des difficultés pendant les périodes de trafic d'écriture élevé. Cela est principalement dû à la mise en œuvre du contrôle de concurrence multiversion (MVCC) de PostgreSQL, 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 des charges d'écriture importantes, cela entraîne une amplification significative de l'écriture. Cela augmente également l'amplification de la lecture, car les requêtes doivent parcourir plusieurs versions de tuples (tuples morts) pour récupérer la dernière. Le MVCC introduit des défis supplémentaires tels que le gonflement des tables et des index, l'augmentation des frais généraux de maintenance des index et le réglage complexe de l'autovacuum. (Vous trouverez une analyse approfondie de ces questions dans un blog que j'ai rédigé avec le professeur Andy Pavlo de l'université Carnegie Mellon, intitulé « The Part of PostgreSQL We Hate the Most » (La partie de PostgreSQL que nous détestons le plus), cité dans la page Wikipédia consacrée à PostgreSQL.
Faire évoluer PostgreSQL à des millions de QPS
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 également plus l'ajout de nouvelles tables au déploiement PostgreSQL actuel. Les nouvelles charges de travail sont par défaut transférées vers les systèmes fragmentés.
Même si notre infrastructure a évolué, PostgreSQL est resté non fragmenté, avec une seule instance principale servant toutes les écritures. La raison principale est que la fragmentation des charges de travail des applications existantes serait très complexe et prendrait beaucoup de temps, nécessitant des modifications sur des centaines de points de terminaison d'applications et pouvant prendre des mois, voire des années. Comme nos charges de travail sont principalement lourdes en lecture et que nous avons mis en œuvre des optimisations importantes, l'architecture actuelle offre encore une marge suffisante pour soutenir la croissance continue du trafic. Bien que nous n'excluions pas le partitionnement de PostgreSQL à l'avenir, ce n'est pas une priorité à court terme étant donné que nous disposons d'une marge de manœuvre suffisante pour notre croissance actuelle et future.
Dans les sections suivantes, nous aborderons les défis auxquels nous avons été confrontés et les optimisations importantes que nous avons mises en œuvre pour y faire face et prévenir de futures pannes, en poussant PostgreSQL à ses limites et en le faisant évoluer à des millions de requêtes par seconde (QPS).
Réduire la charge sur le primaire
Défi : avec un seul rédacteur, une configuration à primaire unique ne peut pas faire évoluer les écritures. Les pics d'écriture importants peuvent rapidement surcharger le primaire et avoir un impact sur des services tels que ChatGPT et notre API.
Solution : nous minimisons autant que possible la charge sur le primaire, tant en lecture qu'en écriture, afin de nous assurer qu'il dispose d'une capacité suffisante pour gérer les pics d'écriture. Le trafic de lecture est déchargé vers des répliques dans la mesure du possible. Cependant, certaines requêtes de lecture doivent rester sur le primaire car elles font partie de transactions d'écriture. Pour celles-ci, nous nous efforçons de garantir leur efficacité et d'éviter les requêtes lentes. Pour le trafic d'écriture, nous avons migré les charges de travail fragmentables et à forte intensité d'écriture vers des systèmes fragmentés tels qu'Azure CosmosDB. Les charges de travail plus difficiles à fragmenter mais qui génèrent néanmoins un volume d'écriture élevé prennent plus de temps à migrer, et ce processus est toujours en cours. Nous avons également optimisé de manière agressive nos applications afin de réduire la charge d'écriture. Par exemple, nous avons corrigé les bogues d'application qui provoquaient des écritures redondantes et avons introduit des écritures différées, lorsque cela était approprié, afin de lisser les pics de trafic. De plus, lors du remplissage des champs des tables, nous appliquons des limites de débit strictes afin d'éviter une pression excessive sur l'écriture.
Optimisation des requêtes
Défi : nous avons identifié plusieurs requêtes coûteuses dans PostgreSQL. Dans le passé, les pics de volume soudains de ces requêtes consommaient d'importantes quantités de CPU, ralentissant à la fois ChatGPT et les requêtes API.
Solution : quelques requêtes coûteuses, telles que celles qui joignent plusieurs tables, peuvent dégrader considérablement, voire paralyser l'ensemble du service. Nous devons optimiser en permanence les requêtes PostgreSQL afin de garantir leur efficacité et d'é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, dont les pics étaient responsables de SEV de haute gravité dans le passé. Nous devons éviter autant que possible les jointures complexes entre plusieurs tables. Si des jointures sont nécessaires, nous avons appris à envisager de décomposer la requête et de déplacer la logique de jointure complexe vers la couche application. 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 code 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'elles ne bloquent l'autovacuum.
Atténuation du point de défaillance unique
Défi : si une réplique en lecture tombe en panne, le trafic peut toujours être acheminé vers d'autres répliques. Cependant, le fait de s'appuyer sur un seul rédacteur signifie qu'il existe un point de défaillance unique : s'il tombe en panne, l'ensemble du service est affecté.
Solution : la plupart des requêtes critiques ne concernent que des requêtes de lecture. Pour atténuer le point de défaillance unique dans le primaire, nous avons déchargé ces lectures du rédacteur vers les répliques, garantissant ainsi que ces requêtes 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 ; il ne s'agit plus d'un SEV0 puisque les lectures restent disponibles.
Pour atténuer les pannes du serveur principal, nous l'exécutons en mode haute disponibilité (HA) avec une réplique de secours active, une réplique synchronisée en continu qui est toujours prête à prendre le relais pour traiter le trafic. Si le serveur principal tombe en panne ou doit être mis hors ligne pour maintenance, nous pouvons rapidement promouvoir la réplique de secours afin de minimiser le temps d'indisponibilité. L'équipe Azure PostgreSQL a accompli un travail considérable pour garantir la sécurité et la fiabilité de ces basculements, même en cas de charge très élevée. Pour gérer les pannes de répliques en lecture, nous déployons plusieurs répliques dans chaque région avec une marge de capacité suffisante, afin de garantir qu'une seule panne de réplique n'entraîne pas une panne régionale.
Isolation des charges de travail
Défi : nous sommes souvent confrontés à des situations où certaines requêtes consomment une quantité disproportionnée de ressources sur les instances PostgreSQL. Cela peut entraîner une dégradation des performances des autres charges de travail exécutées sur les mêmes instances. Par exemple, le lancement d'une nouvelle fonctionnalité peut introduire des requêtes inefficaces qui consomment beaucoup de CPU PostgreSQL, ralentissant ainsi les requêtes pour d'autres fonctionnalités critiques.
Solution : pour atténuer le problème du « voisin bruyant », nous isolons les charges de travail sur des instances dédiées afin de garantir que les pics soudains de requêtes gourmandes en ressources n'aient pas d'impact sur le reste du trafic. Plus précisément, nous divisons les requêtes en niveaux de priorité faible et élevée, puis les acheminons vers des instances distinctes. De cette façon, même si une charge de travail à faible priorité devient gourmande en ressources, elle ne dégradera pas les performances des requêtes à haute priorité. Nous appliquons la même stratégie à différents produits et services, afin que l'activité d'un produit n'affecte pas les performances ou la fiabilité d'un autre.
Mise en commun des connexions
Défi : chaque instance a une limite maximale de connexions (5 000 dans Azure PostgreSQL). Il est facile d'épuiser les connexions ou d'accumuler trop de connexions inactives. Nous avons déjà connu des incidents causés par des tempêtes de connexions qui ont épuisé toutes les connexions disponibles.
Solution : nous avons déployé PgBouncer comme couche proxy pour mettre en pool les connexions à la base de données. Son exécution en mode de mise en pool des instructions ou des transactions nous permet de réutiliser efficacement les connexions, ce qui réduit considérablement le nombre de connexions client actives. Cela réduit également la latence de configuration des connexions : dans nos benchmarks, le temps de connexion moyen est passé de 50 millisecondes (ms) à 5 ms. Les connexions et les requêtes interrégionales peuvent être coûteuses, c'est pourquoi nous regroupons le proxy, les clients et les répliques dans la même région afin de minimiser la charge réseau et le temps d'utilisation des connexions. De plus, PgBouncer doit être configuré avec soin. Des paramètres tels que les délais d'inactivité sont essentiels pour éviter l'épuisement des connexions.
Chaque réplique en lecture dispose de son propre déploiement Kubernetes exécutant plusieurs pods PgBouncer. Nous exécutons plusieurs déploiements Kubernetes derrière le même service Kubernetes, qui équilibre la charge du trafic entre les pods.
Mise en cache
Défi : une augmentation soudaine des échecs de cache peut déclencher une surge de lectures sur la base de données PostgreSQL, saturant le CPU et ralentissant les requêtes des utilisateurs.
Solution : pour réduire la pression de lecture sur PostgreSQL, nous utilisons une couche de mise en cache pour traiter la plupart du trafic de lecture. Cependant, lorsque les taux de réussite du cache chutent de manière inattendue, la vague de cache misses peut envoyer un volume important de requêtes directement à PostgreSQL. Cette augmentation soudaine des lectures de la base de données consomme des ressources importantes, ce qui ralentit le service. Pour éviter la surcharge lors des tempêtes de cache manqué, nous mettons en œuvre un mécanisme de verrouillage (et de location) du cache afin que seul un lecteur qui manque une clé particulière récupère les données de PostgreSQL. Lorsque plusieurs requêtes manquent la même clé de cache, une seule requête acquiert le verrou et procède à la récupération des données et au remplissage du cache. Toutes les autres requêtes attendent que le cache soit mis à jour plutôt que d'atteindre PostgreSQL en même temps. Cela réduit considérablement les lectures redondantes de la base de données et protège le système contre les pics de charge en cascade.
Mise à l'échelle des répliques de lecture
Défi : le primaire transmet les données du journal d'écriture préalable (WAL) à chaque réplique de lecture. À mesure que le nombre de répliques augmente, le primaire doit envoyer le WAL à davantage d'instances, ce qui augmente la pression sur la bande passante du réseau et le CPU. Cela entraîne un décalage plus important et plus instable des répliques, ce qui rend le système plus difficile à mettre à l'échelle de manière fiable.
Solution : nous exploitons près de 50 répliques de lecture dans plusieurs régions géographiques afin de minimiser la latence. Cependant, avec l'architecture actuelle, le primaire doit diffuser le WAL vers chaque réplique. Bien qu'il s'adapte actuellement bien aux très grands types d'instances et à une bande passante réseau élevée, nous ne pouvons pas continuer à ajouter indéfiniment des répliques sans finir par surcharger le primaire. 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 d'évoluer vers plus d'une centaine de répliques sans surcharger le serveur principal. Cependant, elle introduit également une complexité opérationnelle supplémentaire, en particulier en ce qui concerne la gestion des basculements. Cette fonctionnalité est encore en phase de test ; nous nous assurerons qu'elle est robuste et qu'elle peut basculer en toute sécurité avant de la déployer en production.
Limite de débit
Défi : une augmentation soudaine du trafic sur des points de terminaison spécifiques, une recrudescence de requêtes coûteuses ou une avalanche de tentatives de reconnexion peuvent rapidement épuiser les ressources critiques telles que le CPU, les E/S et les connexions, ce qui entraîne 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êtes) afin d'éviter que des pics de trafic soudains ne saturent les instances de base de données et ne déclenchent des défaillances en cascade. Il est également essentiel d'éviter des intervalles de réessai trop courts, qui peuvent déclencher des tempêtes de réessais. Nous avons également amélioré la couche ORM afin de prendre en charge la limitation du débit et, si nécessaire, de bloquer complètement certains résumés de requêtes. Cette forme ciblée de délestage permet une récupération rapide après des pics soudains de requêtes coûteuses.
Gestion des schémas
Défi : même une petite modification du schéma, telle que la modification du type d'une colonne, peut déclencher une réécriture complète de la table. Nous appliquons donc les modifications de schéma avec prudence, en les limitant à des opérations légères et en évitant celles qui réécrivent des tables entières.
Solution : seules les modifications légères du schéma sont autorisées, 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'expiration strict de 5 secondes pour les modifications de schéma. La création et la suppression simultanées d'index sont autorisées. Les modifications de schéma sont limitées aux tables existantes. Si une nouvelle fonctionnalité nécessite des tables supplémentaires, celles-ci doivent se trouver dans des systèmes fragmentés alternatifs tels qu'Azure CosmosDB plutôt que PostgreSQL. Lors du remplissage d'un champ de table, nous appliquons des limites de débit strictes afin d'éviter les pics d'écriture. Bien que ce processus puisse parfois prendre plus d'une semaine, il garantit la stabilité et évite tout impact sur la production.
Résultats et perspectives
Cet effort démontre qu'avec une conception et des optimisations appropriées, Azure PostgreSQL peut être mis à l'échelle pour gérer les charges de travail de production les plus importantes. PostgreSQL gère des millions de QPS pour les charges de travail à forte intensité de 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 en lecture, tout en maintenant le décalage de réplication proche de zéro, en conservant des lectures à faible latence dans les régions géographiquement distribuées et en créant une marge de capacité suffisante pour soutenir la croissance future.
Cette mise à l'échelle fonctionne tout en minimisant la latence et en améliorant la fiabilité. Nous offrons en permanence une latence côté client de l'ordre de quelques millisecondes (p99) et une disponibilité de 99,999 % en production. Au cours des 12 derniers mois, nous n'avons connu qu'un seul incident SEV-0 PostgreSQL (il s'est produit lors du lancement viral de ChatGPT ImageGen, lorsque le trafic d'écriture a soudainement été multiplié par plus de 10, plus de 100 millions de nouveaux utilisateurs s'étant 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 fragmentables à forte intensité d'écriture vers nos systèmes fragmentés tels que CosmosDB. Les charges de travail restantes à forte intensité d'écriture sont plus difficiles à fragmenter, mais nous les migrons également de manière active afin de décharger davantage le système principal PostgreSQL. Nous travaillons également avec Azure pour permettre la réplication en cascade afin de pouvoir évoluer en toute sécurité vers un nombre nettement plus important de répliques en lecture.
À l'avenir, nous continuerons à explorer d'autres approches pour évoluer davantage, notamment PostgreSQL fragmenté ou d'autres systèmes distribués, à mesure que les besoins de notre infrastructure continuent de croître.
Source : "Scaling PostgreSQL to power 800 million ChatGPT users"
Et vous ?
Pensez-vous que ce rapport est crédible ou pertinent ?
Quel est votre avis sur le sujet ?Voir aussi :
Le chatbot IA ChatGPT d'OpenAI était hors service, les utilisateurs ont signalé une panne mondiale, les conversations ont disparu. La panne était due à « une erreur de configuration du routage »
Redéfinir la base de données pour l'IA : pourquoi MongoDB a acquis Voyage AI, par Dev Ittycheria, PDG de MongoDB
Apprendre à raisonner avec le nouveau LLM OpenAI o1 formé avec l'apprentissage par renforcement pour effectuer des raisonnements complexes, car o1 réfléchit avant de répondre
Vous avez lu gratuitement 473 articles depuis plus d'un an.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.