Lyon, le 2 octobre 2026

CloudNativePG ou comment embarquer un éléphant sur un porte-conteneurs !

Il faut s’attendre à de nombreux changements lorsqu’une instance est déployée dans Kubernetes. L’une d’entre elles concerne la gestion de la mémoire et plus spécifiquement le mécanisme d’éviction en cas de surconsommation mémoire. Regardons cela de plus près !

moteur

Good old days

L’installation sur serveur physique ou machine virtuelle était, jusqu’à peu, la méthode privilégiée pour déployer PostgreSQL. Dans la majorité des cas, les machines utilisées étaient, et sont encore, dédiées à nos bases de données.

Une machine = une instance PostgreSQL. Le principe KISS à l’état pur.

Sur ces environnements dédiés, il est communément admis que la désactivation de la surallocation mémoire (paramètres vm.overcommit_memory et vm.overcommit_ratio) est une bonne pratique. La documentation officielle de PostgreSQL explique pourquoi. Pour les détails, voir notre base de connaissance.

Et maintenant ?

Les besoins évoluent et justifient parfois le déploiement de centaines, voire de milliers d’instances. Un environnement conteneurisé, et à fortiori un environnement Kubernetes, peut être le socle de la stack technique pour répondre à ce besoin.

Mais les règles du jeu changent :

  • La désactivation de l’overcommit memory n’est pas systématique, voire impossible (par exemple le mode CIS Hardening de RKE2 et le fichier rke2-cis-sysctl.conf);
  • Votre hébergeur ne vous laissera pas accéder aux Nodes pour faire de telles configurations ;
  • D’autres mécanismes du noyau rentrent en jeu (cgroups) ;
  • Vos bases de données peuvent cohabiter avec des Pods applicatifs ;
  • …

Tout cela pour dire qu’il faut revoir notre copie et adapter nos pratiques avec les possibilités offertes par Kubernetes.

QoS des Pods

Dans Kubernetes, il existe le concept de QoS. À chaque Pod est associée une classe de service qui, selon la configuration en ressources, modifie les règles de déploiement du Pod et est utilisée en cas d’éviction de Pods. L’idée de cet article n’est pas de détailler ce concept. Pour cela, nous vous renvoyons à la documentation officielle.

Ce qu’il faut savoir, c’est qu’il en existe trois qui sont attribuées selon les requests et limits que vous définissez sur la ressource Pod.

  • Best Effort ;
  • Burstable ;
  • et Guaranteed.

Selon la QoS, des ajustements sont faits au niveau des processus et la politique d’éviction change.

Surconsommation et OOM Killer

Sur un système Linux, si un processus consomme trop de mémoire, l’OOM Killer peut être appelé pour chercher à en libérer afin d’éviter de saturer le système. Le meilleur moyen de soulager le système est d’arrêter un programme en cours de fonctionnement… un arrêt que l’on pourrait qualifier de « brutal ».

L’OOM Killer sélectionne le processus à cibler en fonction d’un score calculé par le noyau. Pour faire simple, le processus avec le score le plus élevé est évincé. Il existe des moyens pour ajuster le score d’un processus en particulier.

L’OOM Killer n’a pas connaissance du traitement effectué par tel ou tel processus. Concrètement, avec PostgreSQL, il faut à tout prix éviter un kill du service postmaster, donc le rendre moins susceptible à être ciblé.

Trois Clusters, trois QoS et des comportements différents

Déployons trois Clusters avec des QoS différentes (dépend des requests et limits) et voyons quels sont les changements visibles. Notez qu’ici, la désactivation de l’overcommit n’a pas été faite.

  • cluster-besteffort :
    • Pas de section resources ;
  • cluster-burstable :
      requests:
        memory: "300Mi"
        cpu: "50m"
    
  • cluster-guaranteed :
      requests:
        memory: "2Gi"
        cpu: "0.2"
      limits:
        memory: "2Gi"
        cpu: "0.2"
    

Regardons notamment les paramètres oom_score et oom_score_adj des processus PostgreSQL postmaster de chaque Cluster.

Cluster oom_score oom_score_adj
cluster-besteffort 1333 1000
cluster-burstable 1321 1000
cluster-guaranteed 3 -997

Pour information, voici une manière de trouver le pid du postmaster :

kubectl exec -it cluster-besteffort-1 -c postgres -- cat /var/lib/postgresql/data/pgdata/>postmaster.pid | head -n1
36

Ce qui ressort de ce tableau est que Kubernetes modifie les valeurs du oom_score_adj des processus (ici postmaster, mais ce serait pareil pour d’autres processus présents dans le Pod) en fonction de la QoS du Pod. Cela a pour conséquence de modifier le oom_score qui lui est calculé à partir de plusieurs paramètres (voir la formule) ajustée par oom_score_adj.

Ainsi, si ces trois Clusters sont sur un même Node Kubernetes et qu’une pression au niveau de la mémoire a lieu, le noyau fait appel à l’OOM Killer pour retrouver de la mémoire. Ce dernier cible donc en priorité un processus issu de cluster-besteffort ou cluster-burstable.

Là encore, il n’y a pas moyen de savoir ce que fait vraiment le processus ciblé et s’il est préférable d’arrêter tel ou tel processus parmi les cibles potentielles.

Avec PostgreSQL, on préférait que des processus enfants (par exemple un backend client) soient ciblés plutôt que postmaster. C’est possible avec la variable d’environnement PG_OOM_ADJUST_VALUE.

CloudNativePG en rajoute une couche

Ainsi, lors de la création d’un Cluster CloudNativePG qui a une QoS positionnée à guaranteed, l’opérateur effectue une opération supplémentaire en ajustant la variable d’environnement PG_OOM_ADJUST_VALUE à 0. Ceci est fait uniquement pour la QoS guaranteed.

postgres@cluster-guaranteed-1:/$ ps -aux
postgres    3749  0.0  0.0 226404 13200 ?        Ss   10:40   0:00 postgres: cluster-guaranteed: postgres postgres [local] idle
[...]
postgres    3890  0.0  0.0   6412  3768 pts/0    R+   10:48   0:00 ps -aux
postgres@cluster-guaranteed-1:/$ cat /proc/3749/oom_score_adj 
0
postgres@cluster-guaranteed-1:/$ cat /proc/3749/oom_score
666

Dans cet exemple, le backend client au pid 3749 a bien son score d’ajustement (oom_score_adj) à 0 et donc un oom_score bien plus élevé que postmaster, en l’occurrence, 666 (score généralement rencontré pour des processus consommant peu de mémoire).

OOM Killer en action

Comprendre pourquoi telle ou telle valeur est affectée et comment cela influence l’éviction de processus et de Pod est nécessaire. Mais concrètement, qu’est-ce que cela change dans ma vie de DBA, SRE ou DevOps ?

Faisons en sorte de déclencher une pression mémoire et observons ce qu’il se passe. Pour cela, exécutons l’ordre SQL de tri suivant en veillant à augmenter drastiquement la mémoire de travail utilisable. Il ne faut évidemment pas tenter ceci en production !

SET work_mem = '1000GB';
EXPLAIN (ANALYZE) SELECT i FROM generate_series (1,3e9) i ORDER BY i DESC ;

(Noter que le DBA n’a aucun moyen d’empêcher un utilisateur de faire cela !) Cet ordre est lancé dans chacun des trois Clusters via psql. Les traces sont remontées des traces noyau du système par sudo journalctl --utc -fke.

cluster-besteffort

Lors du test, le kube-controller a fait appel au OOM Killer du système car une allocation mémoire lui a été impossible.

kernel: kube-controller invoked oom-killer: gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP)…

Un peu plus loin, le process ciblé est indiqué task=postgres,pid=613477. Il consommait environ 7,8 Go de mémoire (8176716kB). L’OOM Killer global a été invoqué : global_oom; car il s’agit d’une allocation impossible. Notez également que le oom_score_adj du processus tué se trouve mentionné à la fin de la ligne (1000).

kernel: oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=cri-containerd-e51b72c22b5f0f9bd976a36b7e33183d364d6562623c0eb8d1e68ee1eaa99321.scope,mems_allowed=0,global_oom,task_memcg=/system.slice/docker-0d3ac421a88f3823ef43f68cfc… ,task=postgres,pid=613477,uid=26
kernel: Out of memory: Killed process 613477 (postgres) total-vm:9011764kB, anon-rss:8176716kB, file-rss:160kB, shmem-rss:4308kB, UID:26 pgtables:16212kB oom_score_adj:1000

Le système a donc cherché, trouvé et arrêté un processus consommateur sur le Node : Killed process 613477 (postgres).

Détail qui a son importance, le processus était « rattaché » à d’autres processus. On peut notamment le voir avec le paramètre task_memcg (tronqué dans l’exemple de trace) et are going to be killed due to memory.oom.group set. La description faite dans le message du commit est très claire. L’idée est de traiter le cgroup comme une entité unique.

kernel: Tasks in /system.slice/docker-0d3ac421a88f3823ef43f68cfcad6de7739069b01ff51d2ed918f8659e93702e.scope/kubelet.slice/kubelet-kubepods.slice/kubelet-kubepods-besteffort.slice/kubelet-kubepods-besteffort-pod6e7d6a9d_2946_4aa6_9c74_c866aadf17e1.slice/cri-containerd-342c7d2864fbb3eed1c4645e39db632ae0d0ec41676780b6f6af047e252ab011.scope are going to be killed due to memory.oom.group set

Ces autres processus ne sont ni plus ni moins que ceux présents dans le conteneur, autrement dit, ceux associés au même cgroup… donc le postmaster lui-même.

kernel: Out of memory: Killed process 611483 (manager) …
kernel: Out of memory: Killed process 611551 (postgres) …
kernel: Out of memory: Killed process 611552 (postgres) …
kernel: Out of memory: Killed process 611553 (postgres) …
kernel: Out of memory: Killed process 611554 (postgres) …
kernel: Out of memory: Killed process 611555 (postgres) …
kernel: Out of memory: Killed process 611556 (postgres) …
kernel: Out of memory: Killed process 611557 (postgres) …
kernel: Out of memory: Killed process 611559 (postgres) …
kernel: Out of memory: Killed process 611560 (postgres) …
kernel: Out of memory: Killed process 611561 (postgres) …
kernel: Out of memory: Killed process 611562 (postgres) …
kernel: Out of memory: Killed process 613471 (psql) …
kernel: Out of memory: Killed process 613477 (postgres) …

Ce qui explique que le Pod a été redémarré. D’un point de vue PostgreSQL, on peut dire que l’instance a connu un crash. Elle redémarre donc en mode recovery. Dans les traces de PostgreSQL, lors du redémarrage, la ligne suivante est visible.

"message": "database system was not properly shut down; automatic recovery in progress

cluster-burstable

Lors du test sur le deuxième Cluster, les mêmes observations sont faites.

  • Un processus (ici python3) sur le Node demande de la mémoire.
  • Comme l’allocation ne peut pas être faite, ce même programme invoque l’OOM Killer. Le programme python3 qui tourne sur la machine, n’est pas responsable du manque de mémoire ;
  • Le processus ciblé est le processus PostgreSQL qui exécute le tri. Il consomme environ 8 Go de RAM , trop pour les capacités de la machine ;
  • Tous les processus du cgroup sont évincés ;
  • L’instance redémarre en mode recovery puis redevient accessible.

Ceci n’est pas surprenant, puisque nous avons vu que rien ne diffère d’un point de vue mémoire et sur la configuration oom_*.

cluster-guaranteed

Pour ce dernier cas, l’OOM Killer n’est pas invoqué par n’importe quel processus, mais par un processus postgres, processus interne au conteneur où est exécuté le tri.

kernel: postgres invoked oom-killer: gfp_mask=0xcc0(GFP_KERNEL), order=0, oom_score_adj=0
kernel: CPU: 3 UID: 26 PID: 614252 Comm: postgres …

Notons le pid du processus : 614252.

On retrouve dans les traces la quantité de mémoire et la limite imposée atteint par le cgroup.

kernel: memory: usage 2097152kB, limit 2097152kB, failcnt 871

Cette limite correspond bien aux 2 Go indiqués dans la définition des resources du Cluster cluster-guaranteed. Le journal de traces remonte les processus en cours au moment de l’invocation de l’OOM Killer.

kernel: Tasks state (memory values in pages):
kernel: [  pid  ]   uid  tgid total_vm      rss rss_anon rss_file rss_shmem pgtables_bytes swapents oom_score_adj name
kernel: [ 577430]    26 577430      257        1        0        1         0    40960       10          -998 pause
kernel: [ 607179]    26 607179   347284     4631     4622        9         0   266240        0          -997 manager
kernel: [ 607224]    26 607224    56071     4675      643      428      3604   172032        0          -997 postgres
kernel: [ 607225]    26 607225    17249      640      638        2         0   118784        0             0 postgres
kernel: [ 607226]    26 607226    56104     1317      657        2       658   163840        0             0 postgres
kernel: [ 607227]    26 607227    56104      728      653        2        73   135168        0             0 postgres
kernel: [ 607228]    26 607228    56104      873      657        2       214   163840        0             0 postgres
kernel: [ 607229]    26 607229    56106     2172      669        2      1501   151552        0             0 postgres
kernel: [ 607230]    26 607230    56104     1278      655      209       414   143360        0             0 postgres
kernel: [ 607237]    26 607237    56104     1890      656      105      1129   139264        0             0 postgres
kernel: [ 607238]    26 607238    56472     1806      722      758       326   159744        0             0 postgres
kernel: [ 607239]    26 607239    56104     1021      656      264       101   131072        0             0 postgres
kernel: [ 607240]    26 607240    56472      939      716        2       221   143360        0             0 postgres
kernel: [ 614238]    26 614238     4002     1380      278     1102         0    73728        0          -997 psql
kernel: [ 614252]    26 614252   597541   514260   511082     2136      1042  4300800        0             0 postgres

En dernière ligne, on retrouve le processus 614252 à l’initiative de l’appel au OOM Killer. C’est ce processus qui consomme le plus de mémoire, un peu moins de 2 Go. Au passage, notez que tous les processus enfants à postmaster ont bien hérité du paramètre PG_OOM_ADJUST_VALUE à 0.

Le kill est initié, mais cette fois-ci avec une contrainte différente constraint=CONSTRAINT_MEMCG. Celle-ci indique que l’éviction d’un processus ne peut se faire que dans le conteneur en question. Ceci est logique : quel serait l’intérêt d’arrêter des processus en dehors de ce cgroup s’il est nécessaire de retrouver de la mémoire au sein de ce cgroup ?

sept. 21 06:52:05 lenovo-pro kernel: oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=cri-containerd-683…,mems_allowed=0,oom_memcg=/system.slice/docker-0d3ac421a88f3823ef43f68cfcad6de7739069b01ff51d2ed918f8659e93702e.scope/kubelet.slice/kubelet-kubepods.slice/kubelet-kubepods-pod34c24b1b_f063_4779_b778_1a490b0cab1b.slice,task_memcg=/system.slice/docker-0d3ac…e2eaa7.scope,task=postgres,pid=614252,uid=26

Le processus 614252 est donc arrêté.

kernel: Memory cgroup out of memory: Killed process 614252 (postgres) total-vm:2390164kB, anon-rss:2044328kB, file-rss:8544kB, shmem-rss:4168kB, UID:26 pgtables:4200kB oom_score_adj:0

Notez que le début de la ligne de trace diffère d’un OOM Kill global. Ici il s’agit d’un Memory cgroup out of memory alors que, dans les deux autres exemples, il s’agissait d’un Out of memory.

Là aussi, tous les processus du cgroup sont évincés comme memory.oom.group est utilisé. L’instance redémarre en mode recovery puis redevient accessible.

Par ailleurs, il est à noter que dans cet exemple, comme dans ceux d’avant, tous les processus redémarrent, postmaster compris. En effet, la disparition brusque d’un backend est suspecte et le processus principal préfère préserver l’intégrité des données en redémarrant tout. Là aussi, toutes les connexions autres qui pouvaient exister sont coupées brutalement et devront être re-initiées après le redémarrage de l’instance.

Une différence notable est à souligner. Vous l’avez peut-être compris, mais dans le cas d’un Cluster en QoS Guaranteed, la consommation RAM n’a pas pu dépasser les 2 Go (limits.memory), là où avec les autres QoS, la limite de consommation par le processus de tri a atteint les 8 Go. La consommation de 8 Go de RAM a eu pour conséquence d’épuiser les ressources du système qui étaient de 16 Go.

Conclusion

Nous l’avons vu, le déploiement de PostgreSQL sur Kubernetes implique des changements de fonctionnement à très bas niveau, notamment sur la gestion mémoire. Le diagnostic d’un out of memory dépend également de la QoS du Pod.

Cette QoS dépend directement de la configuration des ressources allouées à celui-ci. CloudNativePG gère une préconisation des développeurs de PostgreSQL en ce qui concerne le score d’ajustement oom_score_adj pour les processus enfants pour une QoS Guaranteed.

Plusieurs choses sont à retenir :

  • Si l’overcommit_memory du Node n’est pas désactivé, l’arrêt d’un processus postgres de type backend client aura pour conséquence l’arrêt et le redémarrage de l’instance, peu importe la QoS choisie. La QoS Guaranteed limite le risque d’un arrêt brutal de postmaster.

  • Cette même QoS vous permet de garder le contrôle sur la consommation mémoire du Pod ;

  • Et enfin, vous évitez une surconsommation qui pourrait avoir des effets de bord sur le reste du système. D’ailleurs, nous l’avons vu dans les exemples, les deux premiers appels OOM Killer ont été faits par deux outils sans rapport avec PostgreSQL qui se trouvaient sur le système : python3 et kube-controller.


Des questions, des remarques ?

** Écrivez-nous !**


DALIBO

DALIBO est le spécialiste français de PostgreSQL®. Nous proposons du support, de la formation et du conseil depuis 2005.