Pipeline Goldfields, par SeanMac (Wikimedia Commons)
Si vous essayez d'optimiser les performances de votre application basée sur PostgreSQL, vous vous concentrez probablement sur les outils habituels :EXPLAIN (BUFFERS, ANALYZE) , pg_stat_statements , auto_explain , log_statement_min_duration , etc.Vous cherchez peut-être un conflit de verrouillage avec log_lock_waits , surveiller les performances de vos points de contrôle, etc.
Mais avez-vous pensé à la latence du réseau ? ? Les joueurs connaissent la latence du réseau, mais pensiez-vous que cela importait pour votre serveur d'applications ?
La latence est importante
Les latences typiques du réseau client/serveur aller-retour peuvent aller de 0,01 ms (localhost) à ~ 0,5 ms d'un réseau commuté, 5 ms de WiFi, 20 ms d'ADSL, 300 ms de routage intercontinental, et même plus pour des choses comme les liaisons satellite et WWAN .
Un SELECT trivial peut prendre de l'ordre de 0,1 ms pour s'exécuter côté serveur. Un INSERT trivial peut prendre 0,5 ms.
Chaque fois que votre application exécute une requête, elle doit attendre que le serveur réponde avec succès/échec et éventuellement un ensemble de résultats, des métadonnées de requête, etc. Cela entraîne au moins un délai d'aller-retour sur le réseau.
Lorsque vous travaillez avec de petites requêtes simples, la latence du réseau peut être importante par rapport au temps d'exécution de vos requêtes si votre base de données n'est pas sur le même hôte que votre application.
De nombreuses applications, en particulier les ORM, sont très susceptibles d'exécuter
De même, si vous remplissez la base de données à partir d'un ORM, vous faites probablement des centaines de milliers de triviaux INSERT s… et attendre après chacun que le serveur confirme que tout va bien.
Il est facile d'essayer de se concentrer sur le temps d'exécution des requêtes et d'essayer de l'optimiser, mais vous ne pouvez pas faire grand-chose avec un trivial INSERT INTO ...VALUES ... . Supprimez quelques index et contraintes, assurez-vous qu'ils sont regroupés dans une transaction, et vous avez pratiquement terminé.
Et si on se débarrassait de toutes les attentes du réseau ? Même sur un réseau local, ils commencent à s'accumuler sur des milliers de requêtes.
COPIER
Une façon d'éviter la latence est d'utiliser COPY . Pour utiliser le support COPY de PostgreSQL, votre application ou votre pilote doit produire un ensemble de lignes de type CSV et les diffuser sur le serveur dans une séquence continue. Ou le serveur peut être invité à envoyer à votre application un flux de type CSV.
Dans tous les cas, l'application ne peut pas entrelacer une copie avec d'autres requêtes, et les insertions de copie doivent être chargées directement dans une table de destination. Une approche courante consiste à COPIER dans une table temporaire, puis à partir de là faites un INSERT INTO ... SELECT ... , MISE À JOUR ... DE .... , SUPPRIMER DE... EN UTILISANT... , etc pour utiliser les données copiées pour modifier les tables principales en une seule opération.
C'est pratique si vous écrivez directement votre propre SQL, mais de nombreux frameworks d'application et ORM ne le prennent pas en charge, et il ne peut remplacer directement que le simple INSERT . Votre application, framework ou pilote client doit gérer la conversion pour la représentation spéciale requise par COPY , rechercher toutes les métadonnées de type requises, etc.
(Pilotes notables qui font soutenir COPIER incluent libpq, PgJDBC, psycopg2 et la gemme Pg… mais pas nécessairement les frameworks et les ORM construits dessus.)
PgJDBC – mode batch
Le pilote JDBC de PostgreSQL a une solution à ce problème. Il s'appuie sur le support présent sur les serveurs PostgreSQL depuis la version 8.4 et sur les fonctionnalités de batch de l'API JDBC pour envoyer un batch de requêtes au serveur, puis n'attendez qu'une seule fois pour avoir la confirmation que tout le lot s'est bien déroulé.
Eh bien, en théorie. En réalité, certains défis de mise en œuvre limitent cela, de sorte que les lots ne peuvent être effectués que par tranches de quelques centaines de requêtes au mieux. Le pilote ne peut également exécuter que des requêtes qui renvoient des lignes de résultats par lots s'il peut déterminer à l'avance la taille des résultats. Malgré ces limitations, l'utilisation de Statement.executeBatch() peut offrir une amélioration considérable des performances aux applications qui effectuent des tâches telles que le chargement de données en masse d'instances de bases de données distantes.
Comme il s'agit d'une API standard, elle peut être utilisée par des applications qui fonctionnent sur plusieurs moteurs de base de données. Hibernate, par exemple, peut utiliser le batch JDBC bien qu'il ne le fasse pas par défaut.
libpq et traitement par lots
La plupart (tous?) Les autres pilotes PostgreSQL ne prennent pas en charge le traitement par lots. PgJDBC implémente le protocole PostgreSQL de manière totalement indépendante, alors que la plupart des autres pilotes utilisent en interne la bibliothèque C libpq qui est fourni avec PostgreSQL.
libpq ne prend pas en charge le traitement par lots. Il dispose d'une API asynchrone non bloquante, mais le client ne peut toujours avoir qu'une seule requête "en vol" à la fois. Il doit attendre que les résultats de cette requête soient reçus avant de pouvoir en envoyer une autre.
Le serveur PostgreSQL prend en charge le traitement par lots très bien, et PgJDBC l'utilise déjà. J'ai donc écrit un support batch pour libpq et l'a soumis comme candidat pour la prochaine version de PostgreSQL. Comme cela ne change que le client, s'il est accepté, cela accélérera toujours les choses lors de la connexion à des serveurs plus anciens.
Je serais vraiment intéressé par les commentaires des auteurs et des utilisateurs avancés de libpq pilotes clients et développeurs de libpq -applications basées sur. Le correctif s'applique bien au-dessus de PostgreSQL 9.6beta1 si vous voulez l'essayer. La documentation est détaillée et il existe un exemple de programme complet.
Performances
Je pensais qu'un service de base de données hébergé comme RDS ou Heroku Postgres serait un bon exemple de l'utilité de ce type de fonctionnalité. En particulier, y accéder depuis nos propres réseaux montre vraiment à quel point la latence peut faire mal.
À une latence réseau d'environ 320 ms :
- 500 insertions sans traitement par lots :
167,0 s - 500 insertions avec traitement par lots :
1,2 s
… qui est plus de 120 fois plus rapide.
Vous n'exécuterez généralement pas votre application sur un lien intercontinental entre le serveur d'application et la base de données, mais cela sert à mettre en évidence l'impact de la latence. Même sur un socket unix vers localhost, j'ai constaté une amélioration des performances de plus de 50 % pour 10 000 insertions.
Regroupement dans les applications existantes
Il n'est malheureusement pas possible d'activer automatiquement le batching pour les applications existantes. Les applications doivent utiliser une interface légèrement différente dans laquelle elles envoient une série de requêtes et ne demandent ensuite que les résultats.
Il devrait être assez simple d'adapter les applications qui utilisent déjà l'interface libpq asynchrone, surtout si elles utilisent le mode non bloquant et un select() /sondage() /epoll() /WaitForMultipleObjectsEx boucle. Applications qui utilisent la libpq synchrone les interfaces nécessiteront plus de modifications.
Regroupement dans d'autres pilotes clients
De même, les pilotes clients, les frameworks et les ORM auront généralement besoin de modifications d'interface et internes pour permettre l'utilisation du traitement par lots. S'ils utilisent déjà une boucle d'événement et des E/S non bloquantes, ils devraient être assez simples à modifier.
J'aimerais voir les utilisateurs de Python, Ruby, etc. pouvoir accéder à cette fonctionnalité, donc je suis curieux de voir qui est intéressé. Imaginez être capable de faire ceci :
import psycopg2 conn = psycopg2.connect(...) cur = conn.cursor() # this is just an idea, this code does not work with psycopg2: futures = [ cur.async_execute(sql) for sql in my_queries ] for future in futures: result = future.result # waits if result not ready yet ... process the result ... conn.commit()
L'exécution par lots asynchrone n'a pas besoin d'être compliquée au niveau du client.
COPIER est le plus rapide
Là où les clients pratiques devraient toujours privilégier COPIER . Voici quelques résultats de mon ordinateur portable :
inserting 1000000 rows batched, unbatched and with COPY batch insert elapsed: 23.715315s sequential insert elapsed: 36.150162s COPY elapsed: 1.743593s Done.
Le traitement par lots du travail offre une amélioration des performances étonnamment importante, même sur une connexion socket Unix locale…. mais COPIER laisse les deux approches d'insertion individuelles loin derrière elle dans la poussière.
Utilisez COPIER .
L'image
L'image de ce poste est du pipeline Goldfields Water Supply Scheme de Mundaring Weir près de Perth en Australie occidentale jusqu'aux champs aurifères intérieurs (déserts). Il est pertinent car il a fallu si longtemps pour terminer et a fait l'objet de critiques si intenses que son concepteur et principal promoteur, C. Y. O'Connor, s'est suicidé 12 mois avant sa mise en service. Dans la région, les gens disent souvent (à tort) qu'il est mort