Aller au contenu

Huge pages

À propos

LicensePostgreSQL
CompatibilitéLinux

Résumé

Recommandation

Dalibo recommande la mise en place des huge pages de 2 Mo sur tout serveur Linux bien doté en RAM ou avec de nombreux processus clients.

Le gain en performance est généralement modeste mais réel, il peut y avoir des économies de mémoire, et la stabilité est meilleure.

Nous préconisons d’utiliser des pages dynamiques pour couvrir toute la mémoire partagée (en général ¼ de la RAM), via le paramètre noyau vm.nr_overcommit_hugepages et de passer le paramètre PostgreSQL huge_pages à on.

Les Transparent Huge Pages sont par contre à désactiver.

Attention

Il faut vérifier la bonne utilisation des huge pages, et en tenir compte lors du calcul de l’overcommit. Prévoir une marge en cas de changement de paramétrage de l’instance.

Utilité du paramétrage des huge pages

Adressage, MMU et TLB

Les systèmes d’exploitation modernes utilisent de la mémoire « virtuelle » : les adresses connues d’un programme ne sont pas celles réelles. Le processeur convertit les adresses connues par le processus en adresses physiques (une page et une adresse dans la bonne barrette mémoire) au moyen de son MMU (Memory Management Unit).

Le MMU enregistre chacune de ces correspondances dans un cache matériel spécifique (un par core), nommée TLB (Translation Lookaside Buffer).

Le TLB dépend du processeur et possède plusieurs niveaux. Pour être le plus rapide possible, il est petit. Pour chaque processus, une table de pagination (PTE), écrite et maintenue en RAM, vient compléter le TLB.

Quand une correspondance n’est pas trouvée directement dans le TLB, on parle de TLB miss. Ce n’est bien sûr pas idéal pour les performances, selon que l’on doit chercher dans la table de pagination (qui vit en RAM, mais dont une partie peut se trouver dans les caches L1, L2 ou L3 du processeur) ou sur disque (swap ou système de fichiers). Le hit ratio du TLB est donc important.

À chaque context switch, le TLB en cours est vidé (TLB flush), et la table de pagination du nouveau processus chargée. Au final, cela fonctionne bien s’il n’y a pas trop de RAM, ou peu de processus.

Or, les bases de données sont particulières, avec des allocations parfois en gigaoctets. Or, 1 Go correspond à 262 144 pages de 4 ko. L’appel d’un bloc précis du cache de PostgreSQL a donc une chance infime de se trouver dans le TLB en place, et il faut aller chercher l’adresse mémoire en-dehors. De plus, les bases de données génèrent énormément de context switches menant à autant de TLB flush.

Rôle des huge pages

L’idée est de ne plus avoir de table de correspondance par page de 4 ko, mais par 2 Mo voire 1 Go (valeurs sur processeurs Intel), pour réduire les TLB flush et économiser l’essentiel de la mémoire des tables de pagination (voir Gains de performance).

Avec des pages de 2 Mo, il ne faut plus que 512 entrées pour couvrir 1 Go au lieu de 262 144 : la table de pagination plus petite entraîne un bien meilleur hit ratio sur le TLB. C’est encore mieux avec des pages de 1 Go. Le premier avantage des huge pages est donc cette amélioration des performances du TLB.

D’autre part, on peut économiser de la mémoire. En effet, avec beaucoup de processus de longue durée de vie, même dormants, comme les nombreux backends de PostgreSQL, l’addition de toutes les tables de pagination peut représenter une belle quantité. Elle est calculée dans /proc/meminfo :

PageTables:      1193040 kB
Une telle valeur de 1 Go, rien que pour la pagination, peut être radicalement réduite grâce aux huge pages.

Enfin, les huge pages ne sont pas « swappables ». Comme les shared buffers ne doivent jamais être swappés (c’est un cache disque !), c’est un avantage. Le swap n’utilise pas de huge pages.

Ce qui suit se concentrera sur les huge pages de 2 Mo. Celles à 1 Go n’ont pas d’intérêt supplémentaire en performance et sont moins flexibles.

Prérequis matériels

  • Pour l’architecture x86_64, le processeur doit afficher les flags pse (pages à 2 Mo) et pdpe1gb (pages à 1 Go). Ce devrait être le cas de tout serveur installé dans la dernière décennie.
$ grep -oE '(pse|pdpe1gb)' /proc/cpuinfo |sort -u
pdpe1gb
pse

Paramétrage PostgreSQL

PostgreSQL possède ce paramètre depuis la version 9.4 :

huge_pages=on|off|try
  • La valeur par défaut est try : PostgreSQL tente d’allouer des huge pages pour toute sa mémoire partagée (principalement les shared_buffers). En cas d’échec, même s’il ne manque que quelques pages, PostgreSQL se rabat sur la taille de page par défaut pour toute sa mémoire partagée (il n’y a pas de message dans les traces !). C’est la valeur à préférer avec des huge pages dynamiques.

  • on exige la présence de toutes les huge pages nécessaires. En cas d’échec, PostgreSQL ne démarre pas avec ce message :

    FATAL:  could not map anonymous shared memory: Cannot allocate memory
    HINT:  This error usually means that PostgreSQL's request for a shared memory segment exceeded available memory, swap space, or huge pages. To reduce the request size (currently 1635778560 bytes), reduce PostgreSQL's shared memory usage, perhaps by reducing shared_buffers or max_connections.
    
    C’est la valeur à préférer avec des huge pages statiques.

  • off exige des pages normales, ce qui fonctionne toujours s’il reste de la mémoire.

Paramétrage Linux

Calcul du nombre de pages

Le nombre de pages pour la mémoire partagée de PostgreSQL dépend de divers paramètres : shared_buffers bien sûr, mais aussi max_connections et d’autres. Le nombre de pages nécessaires peut donc changer suite à un simple petit changement de configuration.

Attention

En conséquence, il faut calculer ce nombre de pages une fois la configuration de PostgreSQL faite, et/ou prévoir une marge.

Il faut aussi tenir compte des huge pages que d’autres logiciels sur le serveur nécessiteraient (l’idéal étant un serveur dédié à PostgreSQL).

Par défaut, PostgreSQL utilise la taille par défaut des huge pages du système (voir le paramètre huge_page_size à partir de PostgreSQL 14).

Avec PostgreSQL 15 et supérieur

PostgreSQL indique lui-même le nombre de pages voulues (mais pas forcément encore utilisées !) avec shared_memory_size_in_huge_pages :

postgres=# SHOW shared_buffers ;
 shared_buffers 
----------------
 16GB

postgres=# SHOW shared_memory_size ;
 shared_memory_size 
--------------------
 17253MB

postgres=# SHOW shared_memory_size_in_huge_pages ;
 shared_memory_size_in_huge_pages 
----------------------------------
 8627
Il faut donc ici 8627 pages de 2 Mo. -1 indiquerait que les huge pages ne sont pas supportées ou que l’on n’est pas sous Linux.

Avec PostgreSQL 14 et précédents

La documentation officielle donne une méthode de calcul à partir d’un pmap <PID du postmaster> en ko, divisée en pages de 2 Mo (2048 ko).

Exemple de calcul du nombre de pages à partir de pmap

$ head -1 $PGDATA/postmaster.pid
1468782
$ pmap 1468782 | awk '/rw-s/ && /zero/ {print $2}'
17666696K
D’où 17666696 / 2048 = 8626,3 pages, soit le 8627 indiqué plus haut.

Plus grossièrement, on peut aussi partir de la taille des shared buffers et appliquer une sécurité de 10 % pour tenir compte des autres segments de mémoire partagée. Il vaut mieux prévoir un petit excès, car une modification de max_connections ou certains autres paramètres peut agrandir la mémoire partagée. Si les pages sont dynamiques (voir plus bas) l’excès de pages ne sera pas pour autant gaspillé.

Exemple de calcul à partir de shared_buffers

16 Go de shared buffers donnent 1,1×(16×1024×1024)/(2×1024) = 9011 pages de 2 Mo (un peu plus que ci-dessus).

Évidemment une augmentation de shared_buffers doit amener à revoir ce calcul.

Pages statiques

Définir le nombre de pages statiques voulues dans /etc/sysctl.conf ou sous /etc/sysctl.d/ (ou utiliser sysctl -w) :

vm.nr_hugepages=8627
vm.nr_overcommit_hugepages=0
  • Les pages statiques bloquent immédiatement la mémoire.
  • Elles sont la seule méthode si l’on tient à des pages de 1 Go.

Attention

Le noyau crée immédiatement les pages qu’il peut, mais il n’y arrive pas toujours, et il n’y a alors pas d’erreur ! (voir plus bas pour les vérifications)

À l’inverse, en cas de réduction du nombre de pages, les pages déjà utilisées par des applications ne sont pas libérées avant l’arrêt de ces applications.

Pages dynamiques

De la même manière :

vm.nr_overcommit_hugepages=8627
vm.nr_hugepages=0

Si le nombre de pages demandées ici est plus grand que ce que réserve ensuite PostgreSQL, l’excès reste en pages standard disponibles pour l’utilisation normale. (Il vaut tout de même mieux éviter de grands écarts en raison des calculs d’overcommit.)

Pages statiques ou dynamiques ?

Certes, il est techniquement possible de définir un mélange de pages statiques et dynamiques, qui pourront être utilisées simultanément. Mais on ne veut généralement qu’un type ou l’autre.

Pour éviter toute erreur, il vaut donc mieux définir systématiquement les deux paramètres, en en mettant un à zéro :

vm.nr_hugepages=0
vm.nr_overcommit_hugepages=4627

Recommandation

Dalibo recommande les pages dynamiques de 2 Mo :

  • C’est la méthode la plus simple.
  • Elle permet de prévoir une marge en cas de changement de configuration sans gaspiller de mémoire.
  • Elle permet de positionner huge_pages=try : si des huge pages dynamiques manquent, PostgreSQL démarre tout de même sans les utiliser, aucune huge page ne sera utilisée, la mémoire ne sera pas gaspillée (l’impact de ce mode légèrement dégradé est généralement faible).
  • À l’inverse, les pages statiques imposent huge_pages=on : s’il manque des huge pages statiques, PostgreSQL ne les utilise pas, mais elles bloquent tout de même le quart de la RAM qu’on aurait voulu octroyer pour les shared buffers, ce qui n’est pas tolérable. Il vaut mieux alors que PostgreSQL ne démarre même pas.

Pages de 1 Go

Des huge pages de 1 Go sont forcément statiques. Elles n’ont peut-être un intérêt que pour les plus grosses configurations mémoire.

Sur x86_64, il faut démarrer Linux avec ce paramétrage :

default_hugepagesz=1G  hugepagesz=1G  hugepages=9
(à ajouter généralement dans /etc/default/grub, ligne GRUB_CMDLINE_LINUX, mais la procédure exacte dépend de la distribution et de sa version, voire de la configuration du fournisseur cloud ; ne pas oublier de réinstaller grub avant de redémarrer).

Définir le nombre de pages (ci-dessous 9) permet de parer, dès le démarrage du système, à toute fragmentation mémoire. L’allocation statique via vm.nr_hugepages reste modifiable de la manière habituelle, contrairement à l’allocation dynamique avec vm.nr_overcommit_hugepages.

PostgreSQL en tient compte dans le nombre de pages qu’il préconise :

postgres=# SHOW shared_buffers ;
 shared_buffers 
----------------
 8GB

postgres=# SHOW shared_memory_size_in_huge_pages ;
 shared_memory_size_in_huge_pages 
----------------------------------
 9

Après que PostgreSQL a démarré et a rempli ses shared buffers, ceux-ci utilisent bien les huge pages :

$ grep Huge /proc/meminfo 
AnonHugePages:      2048 kB
ShmemHugePages:        0 kB
FileHugePages:         0 kB
HugePages_Total:       9
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:    1048576 kB
Hugetlb:         9437184 kB

Il est possible de mélanger des pages de taille différente mais ça n’a pas d’intérêt dans le cas de PostgreSQL.

Désactivation des Transparent Huge Pages

Quand le noyau Linux détecte une allocation contiguë de mémoire, il peut la convertir en transparent huge pages (THP) automatiquement.

Les THP ont mauvaise réputation dans le monde des bases de données, car des latences1 importantes ont pu être observées. Cette mauvaise réputation n’a peut-être plus lieu d’être grâce aux nombreuses améliorations à ce sujet dans les versions récentes du noyau Linux. Par prudence, et avant qu’un nouveau consensus se dégage dans la communauté PostgreSQL, nous préférons toutefois nous en tenir à notre recommandation de toujours, qui est de désactiver celles-ci, en particulier pour les noyaux linux précédant la version 4.112.

La quantité de RAM soumise aux THP est visible dans /proc/meminfo :

AnonHugePages:   1595392 kB
Dans l’idéal, elle est de 0 ko. La désactivation du mécanisme se fait ainsi :

# sous root
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

Pour que ce paramétrage soit encore présent après reboot, il y a plusieurs manières.

On peut ajouter ceci dans /etc/crontab :

@reboot  root  echo never > /sys/kernel/mm/transparent_hugepage/enabled
@reboot  root  echo never > /sys/kernel/mm/transparent_hugepage/defrag

ou dans /etc/rc.local, ou dans un timer systemd ;

ou encore, dans la configuration de grub, en ajoutant ceci à la ligne de commande de Linux :

transparent_hugepage=never
(à ajouter généralement dans /etc/default/grub, ligne GRUB_CMDLINE_LINUX, mais la procédure exacte dépend de la distribution et de sa version, voire de la configuration du fournisseur cloud ; ne pas oublier de réinstaller grub avant de redémarrer) ;

ou encore en utilisant un outil comme tuned (sur distributions dérivées de Red Hat).

Contrôler l’utilisation des huge pages

La mémoire se fragmente vite, et le noyau échoue parfois à trouver les pages nécessaires. Il faut vérifier qu’il a réussi.

  • Les valeurs en cours des deux paramètres précédents sont visibles dans /proc/sys/vm/nr_hugepages et /proc/sys/vm/nr_overcommit_hugepages (ou avec sysctl -a). Elles peuvent différer de ce qui a été demandé.

  • /proc/meminfo est plus complet :

    $ cat /proc/meminfo | grep -i huge
    AnonHugePages:         0 kB
    ShmemHugePages:        0 kB
    FileHugePages:         0 kB
    HugePages_Total:    5309
    HugePages_Free:     3378
    HugePages_Rsvd:     3378
    HugePages_Surp:     3309
    Hugepagesize:       2048 kB
    Hugetlb:        10872832 kB
    

Signification (d’après la documentation de Linux) :

Ligne de /proc/meminfo Utilité
AnonHugePages Quantité de Transparent Huge Pages pour les allocations mémoire ; devrait être à 0
ShmemHugePages Quantité de Transparent Huge Pages pour la mémoire partagée; devrait être à 0
FileHugePages Quantité de Transparent Huge Pages pour les mappings de fichiers; devrait être à 0
HugePages_Total Nombre total de pages (dynamiques utilisées + statiques)
HugePages_Rsvd Pages réservées mais non encore allouées par l’application, qu’elle a la garantie de pouvoir allouer le moment venu (1)
HugePages_Surp (« surplus ») pages dépassant /proc/sys/vm/nr_hugepages, le maximum étant /proc/sys/vm/nr_overcommit_hugepages (en pratique : le nombre de pages dynamiques utilisées)
Hugepagesize Taille par défaut des huge pages sur le système
Hugetlb Taille mémoire totale consommée par les huge pages de toute taille

(1) HugePages_Free et HugePages_Rsvd peuvent être élevés au démarrage de PostgreSQL, quand celui-ci a réservé les pages mais n’a pas encore rempli les shared buffers.

En cas d’échec de création des pages :

  • répéter l’ordre sysctl plusieurs fois
  • hugeadm peut aider (voir plus bas)
  • rebooter après avoir renseigné la configuration dans /etc/sysctl.d/ est la solution la plus radicale et efficace.

Outils

Libération de pages

Si la mémoire est fragmentée, les huge pages demandées ne seront pas forcément disponibles. Pour libérer la place avant de retenter leur création, diverses solutions complémentaires peuvent être testées, comme :

  • purge du cache OS :
    echo 3 > /proc/sys/vm/drop_caches
    
  • arrêt provisoire de toutes les applications ou presque
  • demande d’une défragmentation (par expérience pas très efficace) :
    echo 1 >/proc/sys/vm/compact_memory
    
  • utilisation de hugeadm (voir ci-dessous)
  • en désespoir de cause, un reboot.

hugeadm

hugeadm est packagé pour les principales distributions, mais pas installé par défaut :

  • Debian 10/11/12 : paquet libhugetlbfs-bin (éventuellement hugepages sur Ubuntu avant la version 20.04)
  • CentOS/Rocky Linux 7/8 : paquet libhugetlbfs-utils
  • RHEL/Rocky Linux 9 : le package contenant l’outil n’est plus disponible.

Exemple :

$ sudo hugeadm --pool-list
      Size  Minimum  Current  Maximum  Default
   2097152     2000     6985    10000        *
1073741824        0        0        0

On y voit un minimum de 2000 pages statiques (vm.nr_hugepages), une valeur maximale de 10 000 (vm.nr_hugepages + vm.nr_overcommit_hugepages), et 6985 pages réellement affectées.

La commande suivante tente de monter vm.nr_hugepages à 20 000 malgré la fragmentation. L’outil retente tant qu’il arrive à trouver des pages. Comme on le voit, il n’est pas garanti qu’il y arrive.

$ sudo hugeadm -v --pool-pages-min DEFAULT:20000 --hard

hugeadm:INFO: page_size<DEFAULT> adjust<20000> counter<0>
hugeadm:INFO: 9000, 20000 -> 20000, 20000
hugeadm:INFO: setting HUGEPAGES_TOTAL to 20000
hugeadm:INFO: Retrying allocation HUGEPAGES_TOTAL to 20000 current 18895
hugeadm:INFO: Retrying allocation HUGEPAGES_TOTAL to 20000 current 18895
hugeadm:INFO: Retrying allocation HUGEPAGES_TOTAL to 20000 current 18899
hugeadm:INFO: Retrying allocation HUGEPAGES_TOTAL to 20000 current 18899
hugeadm:INFO: Retrying allocation HUGEPAGES_TOTAL to 20000 current 18899
hugeadm:INFO: Retrying allocation HUGEPAGES_TOTAL to 20000 current 18899
hugeadm:INFO: Retrying allocation HUGEPAGES_TOTAL to 20000 current 18899
hugeadm:INFO: Retrying allocation HUGEPAGES_TOTAL to 20000 current 18899
hugeadm:WARNING: failed to set pool minimum to 20000 became 18899
hugeadm:INFO: setting HUGEPAGES_OC to 1101

$ sudo hugeadm --pool-list

      Size  Minimum  Current  Maximum  Default
   2097152    18899    18899    20000        *
1073741824        0        0        0    

Quels processus utilisent les huge pages ?

Noter qu’attribuer des huge pages à un processus précis n’a guère de sens car il s’agit de mémoire partagée entre les différents processus de PostgreSQL.

La commande suivante va chercher la mention des huge pages dans chaque entrée de processus /proc/<PID>, et affiche les processus correspondants. On peut voir deux instances PostgreSQL et un serveur Apache. (S’il n’y a aucune huge page, la commande ps tombera en erreur)

$ ps -hq $(echo $(sudo grep huge /proc/*/numa_maps 2>/dev/null|cut -f 3 -d'/'|sort -n) |sed 's/ /,/g')

 3612 ?        S      0:29 /usr/lib/postgresql/9.6/bin/postgres -D /var/lib/postgresql/9.6/infra -c config_file=/etc/postgresql/9.6/infra/postgresql.conf
 4111 ?        Ss     0:19 postgres: 9.6/infra: checkpointer process   
 4112 ?        Ss     0:00 postgres: 9.6/infra: writer process   
 4113 ?        Ss     0:08 postgres: 9.6/infra: wal writer process   
 4114 ?        Ss     0:03 postgres: 9.6/infra: autovacuum launcher process   
 4173 ?        Ss     0:00 postgres: 9.6/infra: temboard temboard [local] idle
 5935 ?        Ss     0:59 postgres: 9.6/infra: icinga2 icinga2 127.0.0.1(54732) idle in transaction
24995 ?        Ss     0:00 /usr/sbin/apache2 -k start
26776 ?        Ss     0:01 /usr/lib/postgresql/12/bin/postgres -D /var/lib/postgresql/hdd/12/huge -c config_file=/etc/postgresql/12/huge/postgresql.conf
26841 ?        Ss     0:00 postgres: 12/huge: checkpointer   
26842 ?        Ss     0:00 postgres: 12/huge: background writer   
26843 ?        Ss     0:00 postgres: 12/huge: walwriter   
26844 ?        Ss     0:00 postgres: 12/huge: autovacuum launcher   
26847 ?        Ss     0:00 postgres: 12/huge: logical replication launcher   
28360 ?        Rs     0:02 postgres: 9.6/infra: postgres opm [local] SELECT
32708 ?        Sl     0:00 /usr/sbin/apache2 -k start
32709 ?        S      0:01 /usr/sbin/apache2 -k start
32710 ?        S      0:00 /usr/sbin/apache2 -k start
32711 ?        S      0:00 /usr/sbin/apache2 -k start
32712 ?        S      0:00 /usr/sbin/apache2 -k start
32713 ?        S      0:00 /usr/sbin/apache2 -k start
32714 ?        S      0:00 /usr/sbin/apache2 -k start

Les huge pages sont-elles bien utilisées ?

Globalement

Il faut vérifier que les huge pages sont bien utilisées. Ce n’est pas si facile car :

  • Linux ne tombe pas en erreur s’il n’arrive pas à les allouer
  • par défaut, PostgreSQL ne tombe pas en erreur s’il ne les trouve pas, sauf à changer huge_pages à on
  • les valeurs affichées diffèrent selon que les shared buffers sont remplis ou non.

Les outils sont surtout /proc/meminfo, éventuellement hugeadm ou numastat (paquet numactl) pour une vision un peu plus lisible. free -m est un complément utile.

Sans huge pages

Si les huge pages ne sont pas configurées, et si les shared buffers (1,5 Go ici) sont pleins, la mémoire consommée par ceux-ci s’affiche dans used et shared :

$ free -m
               total        used        free      shared  buff/cache   available
Mem:            5925        1738          46        1551        5769        4186
Swap:              0           0           0

Avec des pages statiques

On suppose une machine de 6 Go de RAM, avec 1,5 Go de shared buffers et ces paramètres :

vm.nr_overcommit_hugepages=0
vm.nr_hugepages=800

PostgreSQL arrêté : les pages sont déjà considérées comme utilisées, même si elles sont libres :

sudo grep Huge /proc/meminfo
AnonHugePages:         0 kB
ShmemHugePages:        0 kB
FileHugePages:         0 kB
HugePages_Total:     800
HugePages_Free:      800
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
Hugetlb:         1638400 kB
et Linux considère la mémoire comme utilisée :

$ free -m
               total        used        free      shared  buff/cache   available
Mem:            5925        1798         272           2        3947        4126
Swap:              0           0           0

PostgreSQL démarré, on voit qu’il a bien réservé ses pages, mais elles sont presque toutes libres :

$ sudo grep Huge /proc/meminfo
AnonHugePages:         0 kB
ShmemHugePages:        0 kB
FileHugePages:         0 kB
HugePages_Total:     800
HugePages_Free:      771
HugePages_Rsvd:      751
HugePages_Surp:        0
Hugepagesize:       2048 kB
Hugetlb:         1638400 kB
$ free -m
               total        used        free      shared  buff/cache   available
Mem:            5925        1808         257           3        3953        4116
Swap:              0           0           0

Une fois les shared buffers pleins (pg_prewarm permet d’accélérer cela au besoin), il n’y a presque plus de pages libres :

$ sudo grep Huge /proc/meminfo
AnonHugePages:         0 kB
ShmemHugePages:        0 kB
FileHugePages:         0 kB
HugePages_Total:     800
HugePages_Free:       22
HugePages_Rsvd:        2
HugePages_Surp:        0
Hugepagesize:       2048 kB
Hugetlb:         1638400 kB
$ free -m
               total        used        free      shared  buff/cache   available
Mem:            5925        1790          41           3        4175        4135
Swap:              0           0           0

Noter que le champ shared est presque nul, la mémoire partagée étant classée dans la mémoire utilisée.

Avec des pages dynamiques

On suppose une machine de 6 Go de RAM, 1,5 Go de shared buffers et ces paramètres :

vm.nr_hugepages=0
vm.nr_overcommit_hugepages=800

PostgreSQL arrêté, aucune page n’est réservée ou allouée :

$ sudo grep Huge /proc/meminfo
AnonHugePages:         0 kB
ShmemHugePages:        0 kB
FileHugePages:         0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
Hugetlb:               0 kB
PostgreSQL démarré, les pages nécessaires sont allouées et réservées mais la plupart restent inutilisées :
$ sudo grep Huge /proc/meminfo
AnonHugePages:         0 kB
ShmemHugePages:        0 kB
FileHugePages:         0 kB
HugePages_Total:     780
HugePages_Free:      751
HugePages_Rsvd:      751
HugePages_Surp:      780
Hugepagesize:       2048 kB
Hugetlb:         1597440 kB
Après passage de max_connections de 100 à 800 et redémarrage de PostgreSQL, le nombre de pages réservées est plus élevé :
$ sudo grep Huge /proc/meminfo
AnonHugePages:         0 kB
ShmemHugePages:        0 kB
FileHugePages:         0 kB
HugePages_Total:     794
HugePages_Free:      755
HugePages_Rsvd:      755
HugePages_Surp:      794
Hugepagesize:       2048 kB
Hugetlb:         1626112 kB

Une fois les shared buffers pleins (pg_prewarm permet d’accélérer cela au besoin), il n’y a presque plus de pages libres :

sudo grep Huge /proc/meminfo
AnonHugePages:         0 kB
ShmemHugePages:        0 kB
FileHugePages:         0 kB
HugePages_Total:     794
HugePages_Free:        6
HugePages_Rsvd:        6
HugePages_Surp:      794
Hugepagesize:       2048 kB
Hugetlb:         1626112 kB

Par processus

Dans /proc/<PID>/status, la ligne HugetlbPages indique la quantité de RAM en huge pages que le processus utilise (forcément de la mémoire partagée ici).

Exemple de valeurs HugetlbPages dans /proc/PID/status

On constate que seuls certains processus clients ont « touché » aux 1,5 Go de mémoire partagée.

for p in  $(pgrep postgres) ; do cat /proc/$p/cmdline ; printf '\n'; grep HugetlbPages /proc/$p/status ; done
/usr/lib/postgresql/15/bin/postgres-D/var/lib/postgresql/15/main-cconfig_file=/etc/postgresql/15/main/postgresql.conf
HugetlbPages:      59392 kB
postgres: 15/main: checkpointer 
HugetlbPages:      38912 kB
postgres: 15/main: background writer 
HugetlbPages:    1253376 kB
postgres: 15/main: walwriter 
HugetlbPages:      24576 kB
postgres: 15/main: autovacuum launcher 
HugetlbPages:      43008 kB
postgres: 15/main: logical replication launcher 
HugetlbPages:      12288 kB
postgres: 15/main: postgres postgres [local] idle
HugetlbPages:    1581056 kB                   <------ ce processus client avait exécuté un pg_prewarm sur toute la base
postgres: 15/main: autovacuum worker postgres 
HugetlbPages:    1574912 kB                   <------ autovacuum a parcouru tous les shared buffers

Si la ligne HugetlbPages n’est pas présente dans le fichier status, cette commande calcule le volume de Huge pages utilisé (en Mo) :

sudo grep -B 11 'KernelPageSize:     2048 kB' /proc/$(sudo head -1 $PGDATA/postmaster.pid)/smaps | grep "^Size:" | awk 'BEGIN{sum=0}{sum+=$2}END{print sum/1024}'
1560

Gains de performance

Les huge pages apportent un gain en performances pures mesurable mais modeste (quelques pour cent dans les configurations avec beaucoup de RAM et de clients). Les accès aux TLB sont en effet déjà très optimisés.

Le support Dalibo a cependant déjà vu des impacts plus notables, comme décrit dans cet article de Julien Rouhaud : Diagnostique de lenteurs inattendues.

L’article montre aussi un gain sur la taille de la PTE (la table de pagination) et ce sur chaque processus.

La somme des taille des tables de pagination se voit dans /proc/meminfo :

PageTables:      1193040 kB

Si la session dure et parcoure tout le cache de PostgreSQL, la PTE peut monter à 16 Mo par processus (avec des pages normales de 4 ko pour une mémoire partagée de 8 Go). Avec des huge pages de 2 Mo, la PTE tombe à environ 100 ko par processus. Avec des centaines ou milliers de sessions et sans pooling, l’économie de mémoire justifie l’utilisation des huge pages.

Comparaison des PTE d’un processus avec des pages de 4 ko ou 2 Mo

Sur un processus donné, la PTE se trouve ainsi :

$ grep VmPTE /proc/2124/status
VmPTE:      3216 kB

Voici un processus ayant accédé aux 8 Go de shared buffers, d’abord avec des pages de 4 ko :

$ sudo cat /proc/11472/status 
Name:   postgres
Umask:  0077
State:  R (running)
Tgid:   11472
Ngid:   0
Pid:    11472
PPid:   9059
TracerPid:  0
Uid:    1000    1000    1000    1000
Gid:    1000    1000    1000    1000
FDSize: 128
Groups: 7 50 90 91 92 93 96 100 1000 1001 
NStgid: 11472
NSpid:  11472
NSpgid: 11472
NSsid:  11472
VmPeak:  8449256 kB
VmSize:  8448776 kB
VmLck:         0 kB
VmPin:         0 kB
VmHWM:   8352340 kB
VmRSS:   8352340 kB
RssAnon:        8532 kB
RssFile:        7752 kB
RssShmem:    8336056 kB <-- 8 Go de shared buffers: shared memory « normale »
VmData:     9188 kB
VmStk:       136 kB
VmExe:      6204 kB
VmLib:      7376 kB
VmPTE:     16420 kB   <-------- 1 Mo de RAM utilisée, juste par la table de pagination
VmPMD:        48 kB
VmSwap:        0 kB
HugetlbPages:          0 kB
Threads:    1
SigQ:   0/63677
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000001301800
SigCgt: 0000000184006287
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
Seccomp:    0
Cpus_allowed:   f
Cpus_allowed_list:  0-3
Mems_allowed:   00000000,00000001
Mems_allowed_list:  0
voluntary_ctxt_switches:    4786
nonvoluntary_ctxt_switches: 11157

La même, avec des pages de 2 Mo :

cat /proc/11957/status 
Name:   postgres
Umask:  0077
State:  R (running)
Tgid:   11957
Ngid:   0
Pid:    11957
PPid:   11934
TracerPid:  0
Uid:    1000    1000    1000    1000
Gid:    1000    1000    1000    1000
FDSize: 64
Groups: 7 50 90 91 92 93 96 100 1000 1001 
NStgid: 11957
NSpid:  11957
NSpgid: 11957
NSsid:  11957
VmPeak:  8449808 kB
VmSize:  8449800 kB
VmLck:         0 kB
VmPin:         0 kB
VmHWM:     14908 kB
VmRSS:     14908 kB
RssAnon:        7868 kB
RssFile:        7040 kB
RssShmem:          0 kB
VmData:     8188 kB
VmStk:       136 kB
VmExe:      6204 kB
VmLib:      7376 kB
VmPTE:        92 kB <---------- 92 ko de pages utilisées par la PTE
VmPMD:        44 kB
VmSwap:        0 kB
HugetlbPages:    8495104 kB <-- 8 Go de shared buffers, en huge pages cette fois-ci
Threads:    1
SigQ:   0/63677
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000001301800
SigCgt: 0000000184006287
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
Seccomp:    0
Cpus_allowed:   f
Cpus_allowed_list:  0-3
Mems_allowed:   00000000,00000001
Mems_allowed_list:  0
voluntary_ctxt_switches:    5091

Impact sur les limites d’allocation de la mémoire

Le paramétrage de l’overcommit est fortement conseillé sur un serveur PostgreSQL pour sa stabilité.

L’utilisation de huge pages complexifie le calcul de vm.overcommit_ratio ou vm.overcommit_kbytes : les huge pages doivent être déduites de la mémoire « disponible » pour le calcul du CommitLimit. Voir le détail sur la page dédiée au calcul de l’overcommit avec les huge pages, et les exemples.

Aller plus loin


  1. voir notamment le dernier paragraphe de la documentation du paramètre huge_page

  2. à partir de cette version, il est possible de positionner /sys/kernel/mm/transparent_hugepage/defrag à la valeur defer ou madvise+defer, ce qui supprime théoriquement le plus gros des problèmes de latences qui avaient été observées jusqu’alors. Il reste toutefois possiblement des problèmes liés aux mécanismes de maintenance de ces huge pages lorsque la pression mémoire est importante, notamment la refragmentation de celles-ci en cas de swapping.