Benchmarks du cache PostgreSQL : 3.1.0

Ces mesures comparent pg_local_cache RESP MGET, Valkey et SQL préparé sur deux VM reliées par un réseau privé. Tous les résultats utilisent le build final 3.1.0 c431bcc. Les médianes des essais répétés sont indiquées pour les matrices de lecture et les essais d’écriture ; les deux cas épinglés à un cœur avec Valkey io-threads=1 ont deux répétitions, les autres cas épinglés en ont cinq. Les essais mixtes, les sondes de 120 secondes et le soak d’une heure sont des essais uniques. Ces essais ne garantissent pas une capacité en production.

Résultats du build final

Lectures par paire de vCPU serveur épinglée

Le client Go/pgx utilisait 256 clients et une clé par MGET. Chaque cas a duré 20 secondes. Les deux cas épinglés à une paire avec Valkey io-threads=1 ont été répétés deux fois ; les autres cas épinglés, cinq fois. PostgreSQL et Valkey étaient épinglés à N paires de vCPU (2N vCPU). Valkey utilisait io-threads=2N et io-threads=1 ; SQL préparé utilisait les mêmes clés.

Paires de vCPU Clés pg_local_cache req/s (p99 ms, vCPU) Valkey io-threads=2N (req/s, p99 ms, vCPU) Valkey io-threads=1 (req/s, p99 ms, vCPU) SQL préparé (req/s, p99 ms, vCPU)
1 Chaudes 209,696 (2.46, 1.99) 91,478 (3.24, 2.00) 180,141 (2.59, 0.85) 23,200 (24.90, 2.00)
1 100k aléatoires 195,950 (2.65, 2.00) 88,672 (3.38, 2.00) 175,853 (2.65, 0.92) 22,433 (25.95, 2.00)
2 Chaudes 336,691 (2.02, 3.78) 266,340 (1.69, 4.00) 162,230 (2.85, 0.66) 46,386 (16.38, 4.00)
2 100k aléatoires 326,125 (1.98, 3.83) 267,826 (1.72, 4.00) 160,220 (2.92, 0.69) 45,211 (17.56, 4.00)
4 Chaudes 360,968 (2.33, 5.24) 357,161 (2.20, 7.95) 160,633 (2.85, 0.66) 88,604 (6.75, 8.00)
4 100k aléatoires 353,941 (2.33, 5.27) 351,344 (2.20, 7.95) 159,414 (2.92, 0.68) 86,790 (6.88, 8.00)

Sur un cœur, Valkey mono-thread était sa meilleure configuration. Sur deux cœurs, io-threads=4 était le meilleur réglage ; p99 de Valkey était plus faible malgré un débit inférieur. Sur quatre cœurs, pg_local_cache et io-threads=8 atteignaient presque le même débit limité par le client, avec moins de CPU serveur pour pg_local_cache.

Raw runs: Valkey io-threads=2N, Valkey io-threads=1.

Lectures avec les 16 vCPU disponibles

Ces essais n’étaient pas épinglés et ont été répétés trois fois. Dans les cas limités par le client, le nœud de charge utilisait environ 7–13 cœurs CPU. Avec Valkey io-threads=8, les écarts observés entre médianes MGET 16/64 allaient d’environ −2,15 % à +1,26 % ; les plages ne se chevauchaient pour aucune des quatre comparaisons à 64 clés. Ces essais ne démontrent pas une équivalence statistique. Avec io-threads=1, MGET 16 aléatoire à 256 clients a mesuré 116 130 contre 97 489 req/s (19,1 % de plus pour pg_local_cache). Les grands MGET étaient limités par le client et le réseau ; aucun résultat io-threads=1 n’est disponible pour 64 clés.

Clés Clés MGET Clients pg_local_cache (req/s, p99 ms, vCPU) Valkey io-threads=8 (req/s, p99 ms, vCPU) Valkey io-threads=1 (req/s, p99 ms, vCPU) SQL préparé (req/s, p99 ms, vCPU)
Chaudes 1 64 217,724 (0.70, 2.89) 206,634 (0.70, 7.85) 167,929 (0.71, 0.66) 132,433 (0.96, 11.32)
Chaudes 1 256 372,683 (2.39, 4.21) 365,947 (2.33, 7.92) 161,455 (2.92, 0.68) 173,571 (4.26, 13.14)
Chaudes 16 64 82,858 (3.38, 3.13) 83,098 (3.38, 6.41) 79,179 (3.38, 0.69) 73,512 (2.05, 8.78)
Chaudes 16 256 122,496 (9.83, 4.07) 122,817 (9.83, 7.42) 111,790 (8.06, 0.83) 100,966 (6.36, 11.49)
Chaudes 64 64 30,241 (8.78, 3.40) 30,845 (9.04, 3.57) — 27,790 (6.88, 9.05)
Chaudes 64 256 39,531 (43.52, 4.26) 39,995 (47.71, 5.66) — 34,519 (30.67, 12.23)
100k aléatoires 1 64 215,310 (0.70, 2.89) 205,081 (0.70, 7.85) 162,778 (0.73, 0.67) 131,563 (0.97, 11.34)
100k aléatoires 1 256 366,860 (2.39, 4.31) 360,894 (2.33, 7.92) 158,444 (2.98, 0.68) 169,884 (4.26, 13.19)
100k aléatoires 16 64 79,505 (3.51, 3.45) 79,556 (3.44, 6.68) 74,270 (3.38, 0.79) 69,262 (2.13, 9.12)
100k aléatoires 16 256 116,130 (10.35, 4.42) 114,683 (9.83, 7.53) 97,489 (7.67, 0.95) 91,572 (7.01, 11.96)
100k aléatoires 64 64 29,000 (8.78, 3.81) 29,464 (9.04, 4.61) — 26,169 (7.14, 9.39)
100k aléatoires 64 256 37,356 (43.52, 4.93) 38,175 (40.37, 7.58) — 32,756 (31.20, 13.03)

Pour des clés aléatoires, MGET 64 et 256 clients, p99 de pg_local_cache était de 43,52 ms contre 40,37 ms pour Valkey. SQL préparé avait un débit nettement inférieur dans les essais épinglés à clé unique ; les résultats varient selon la taille du lot et la latence.

Raw runs: Valkey io-threads=8, Valkey io-threads=1.

Surcoût des écritures

Trois répétitions de 15 secondes comparent les transactions sur table simple, celles via pg_local_cache et les transactions simples suivies de Valkey DEL. Les pourcentages comparent au cas correspondant sur table simple.

Opération synchronous_commit Clients Table simple tx/s (DB µs/tx) pg_local_cache tx/s (DB µs/tx, écart) Table simple + Valkey DEL tx/s (DB µs/tx, écart)
Update on 32 46,747 (109) 45,065 (119, −3.6%) 33,234 (204, −28.9%)
Update on 64 63,479 (108) 61,027 (121, −3.9%) 37,659 (238, −40.7%)
Update off 32 119,156 (75) 110,987 (87, −6.9%) 53,374 (158, −55.2%)
Update off 64 148,358 (73) 136,194 (83, −8.2%) 53,853 (189, −63.7%)
Insert on 32 48,831 (97) 47,020 (105, −3.7%) 34,863 (184, −28.6%)
Insert on 64 65,688 (97) 63,011 (108, −4.1%) 39,642 (218, −39.7%)
Insert off 32 123,679 (63) 117,868 (72, −4.7%) 58,168 (135, −53.0%)
Insert off 64 138,454 (68) 130,805 (77, −5.5%) 58,361 (165, −57.8%)

Pour UPDATE, pg_local_cache était 3,6–8,2 % sous les écritures simples ; le surcoût INSERT était de 3,7–5,5 %. UPDATE simple plus Valkey DEL était inférieur de 28,9–63,7 % au débit UPDATE simple ; pour INSERT, l’écart était de 28,6–57,8 %.

Raw runs: write overhead.

Lectures pendant les écritures

Chaque cas a été exécuté une fois pendant 30 secondes, avec 64 lecteurs sur 60k lignes ; les écrivains utilisaient synchronous_commit=off. Valkey utilisait cache-aside (écriture SQL puis DEL) avec io-threads=8. « Périmées » désigne le contrôle sentinelle final ; n/a signifie qu’aucune entrée de cache n’est inspectable.

Stack Distribution des clés Writer Lectures/s p99 lecture ms Taux de hit Écritures/s Entrées périmées
pg_local_cache Uniform Aucun 214,508 0.71 1.000 0 0
pg_local_cache Uniform Update 10,000/s 195,384 0.86 0.948 10,000 0
pg_local_cache Uniform Update 30,000/s 161,919 1.04 0.837 29,999 0
pg_local_cache Uniform Update unlimited 82,640 1.69 0.432 111,013 0
pg_local_cache Zipf Aucun 216,297 0.70 1.000 0 0
pg_local_cache Zipf Update 10,000/s 198,454 0.86 0.964 10,000 0
pg_local_cache Zipf Update 30,000/s 168,875 1.01 0.923 30,000 0
pg_local_cache Zipf Update unlimited 96,297 1.46 0.834 113,505 0
Valkey cache-aside Uniform Aucun 223,537 0.61 1.000 0 0
Valkey cache-aside Uniform Update 10,000/s 156,983 1.92 0.936 10,000 0
Valkey cache-aside Uniform Update 30,000/s 42,294 7.14 0.577 29,998 0
Valkey cache-aside Uniform Update unlimited 29,692 8.78 0.444 38,481 1
Valkey cache-aside Zipf Aucun 222,575 0.63 0.999 0 0
Valkey cache-aside Zipf Update 10,000/s 168,911 1.65 0.960 10,000 0
Valkey cache-aside Zipf Update 30,000/s 75,642 4.78 0.887 29,999 0
Valkey cache-aside Zipf Update unlimited 48,993 6.88 0.852 40,735 0
Prepared SQL Uniform Aucun 129,714 0.97 — 0 n/a
Prepared SQL Uniform Update 10,000/s 121,567 1.13 — 10,000 n/a
Prepared SQL Uniform Update 30,000/s 106,949 1.33 — 30,000 n/a
Prepared SQL Uniform Update unlimited 77,574 2.02 — 93,955 n/a
Prepared SQL Zipf Aucun 129,937 0.97 — 0 n/a
Prepared SQL Zipf Update 10,000/s 122,182 1.13 — 10,000 n/a
Prepared SQL Zipf Update 30,000/s 107,852 1.33 — 29,999 n/a
Prepared SQL Zipf Update unlimited 78,601 2.08 — 95,199 n/a
pg_local_cache Uniform Insert unlimited 117,531 1.20 1.000 114,066 0
Valkey cache-aside Uniform Insert unlimited 70,302 4.92 1.000 46,622 0
Prepared SQL Uniform Insert unlimited 84,294 1.98 — 95,094 n/a

pg_local_cache a terminé chaque essai mixte listé sans entrée périmée. Valkey n’a laissé qu’une entrée sentinelle périmée dans un seul cas UPDATE : clés Uniform avec un taux de mise à jour illimité. À 30k UPDATE/s avec des clés Uniform, les débits étaient de 161 919/s pour pg_local_cache, 42 294/s pour Valkey cache-aside et 106 949/s pour SQL préparé.

Raw runs: mixed reads and writes.

Sondes de cohérence de 120 secondes

Chaque sonde a été exécutée une fois avec 64 lecteurs et 64 écrivains UPDATE sans limite, puis une vérification complète des entrées périmées. Valkey utilisait io-threads=8.

Stack Distribution des clés Lectures/s p99 lecture ms Écritures/s Entrées périmées Note
pg_local_cache Uniform 82,565 1.687552 108,580 0 —
pg_local_cache Zipf 0 n/a 101,797 0 Voir la note ci-dessous.
Valkey cache-aside Uniform 30,210 8.781824 36,508 2 —
Valkey cache-aside Zipf 28,568 8.781824 35,789 1 —

Le lecteur Zipf de pg_local_cache a échoué au contrôle d’équivalence au démarrage pendant les écritures concurrentes (problème du harness) et a indiqué zéro lecture. Le contrôle des entrées périmées a tout de même été exécuté et en a trouvé zéro.

Raw runs: 120-second stale probes.

Soak d’une heure

Essai unique : MGET 16 sur 100k clés aléatoires, 64 clients, plus 20 000 UPDATE/s. RssAnon des workers RESP était échantillonné chaque minute.

Raw runs: one-hour soak.

Environnement et méthode

Selon lscpu, chaque VM avait 16 vCPU en 2 sockets × 4 cœurs par socket × 2 threads par cœur (topologie visible de la VM), 62 GB et Debian 13 avec le noyau 6.12.111+deb13-amd64. La VM base exécutait PostgreSQL 16.15 (max_connections=1000, shared_buffers=1048576) et pg_local_cache 3.1.0 c431bcc (activé, memory_budget_mb=1024, workers=8). La table contenait 100 000 lignes JSON de 166 octets en moyenne. Valkey 8.1.1 utilisait io-threads=8 pendant les essais d’écriture, mixtes et de sondes de cohérence. De la VM de charge vers la VM base, iperf3 a mesuré 10,4 Gbit/s et ping a indiqué un RTT min/moy/max de 0,090/0,143/0,871 ms. Le client Go/pgx tournait sur la seconde VM. Les lectures duraient 20 secondes ; écritures 15 secondes, essais mixtes 30 secondes, sondes 120 secondes et soak 3 600 secondes.

La matrice épinglée utilisait 256 clients ; chaque cas a eu cinq exécutions sauf les deux cas épinglés à une paire avec Valkey io-threads=1, qui en ont eu deux. La matrice complète et les essais d’écriture en ont eu trois ; les essais mixtes, sondes et soak, une seule. Les interruptions NIC n’étaient pas épinglées. Tous les tableaux utilisent le build final c431bcc. Le débit est en requêtes/s sauf mention tx/s ; la latence est p99 et le CPU en vCPU serveur.

Environnement et JSONL bruts : environnement, preuve de l’environnement, lectures épinglées, lectures épinglées Valkey io-threads=1, lectures tous cœurs, tous cœurs Valkey io-threads=1, surcoût des écritures, lectures pendant les écritures, sondes 120 s et soak d’une heure.

Reproduire avec bench/

Il faut Python 3 et des alias SSH vers les VM base de données et charge. La VM base nécessite PostgreSQL 16, Valkey, mpstat, sar, ip et systemd ; la VM de charge doit avoir le client Go compilé dans /root/bench-client. Voir bench/README.md et le client Go/pgx.

Matrice de lecture par défaut : 20 s, trois répétitions, 1/16/64 clés, 16/64/256 clients, ensemble chaud et espace de 100k clés :

python3 bench/run_bench.py reads.jsonl

Exemple avec une paire de vCPU épinglée, 256 clients et cinq répétitions ; remplacez 0-1 par les ID CPU logiques d’une paire de vCPU :

PGLC_SERVER_CPUS=0-1 REPEATS=5 BATCHES=1 CLIENTS=256 KEY_SPACES=0,100000 python3 bench/run_bench.py pinned.jsonl

Le runner applique AllowedCPUs à PostgreSQL et Valkey puis retire la limite. Il enregistre CPU serveur, latence, RX/TX réseau et CPU client. Configurez io-threads de Valkey avant l’essai.

Lancer les tests d’écriture et la charge mixte de 30 s :

REPEATS=3 WRITE_SECONDS=15 WRITE_CLIENTS=32,64 python3 bench/write_bench.py writes.jsonl
REPEATS=1 MIXED_SECONDS=30 MIXED_RATES=none,10000,30000,unlimited MIXED_KEY_DISTS=uniform,zipf python3 bench/write_bench.py mixed.jsonl

Les scripts ajoutent des lignes JSONL et conservent les échantillons terminés en cas d’erreur. Le test d’écriture mesure le CPU base par transaction ; le test mixte enregistre aussi latence, hits/misses, débit des écritures et vérification finale des sentinelles.

Résultats historiques 2.x sur MacBook

Ces anciens JSON sont des mesures historiques sur Apple M3 Max avec PostgreSQL 16 ; environnement et topologie client diffèrent des deux VM ci-dessus. Ils incluent l’API SQL mget de 2.x, supprimée en 3.0.0. Voir la documentation 2.0.4 pour cette API ; les lectures en cache actuelles utilisent RESP MGET.

Mis à jour .