Formation CNPG1
Dalibo SCOP
26.09
10 septembre 2026
| Formation | Formation CNPG1 |
| Titre | PostgreSQL & Kubernetes avec CloudNativePG |
| Révision | 26.09 |
| ISBN | N/A |
| https://dali.bo/cnpg1_pdf | |
| EPUB | https://dali.bo/cnpg1_epub |
| HTML | https://dali.bo/cnpg1_html |
| Slides | https://dali.bo/cnpg1_slides |
Cette formation est sous licence CC-BY-NC-SA. Vous êtes libre de la redistribuer et/ou modifier aux conditions suivantes :
PostgreSQL® Postgres® et le logo Slonik sont des marques déposées par PostgreSQL Community Association of Canada.
Ce document ne couvre que les versions supportées de PostgreSQL au moment de sa rédaction, soit les versions 14 à 18.
Quelle version utiliser ?
De M.m à M.m+n :
jsonbpg_stat_progress_basebackup,
pg_stat_progress_analyzepg_stat_progress_copy, pg_stat_wal,
pg_lock.waitstart, query_id…MERGEDISTINCT parallélisablepublic n’est plus accessible en écriture à tousDISTINCT…)pg_hba.confpg_stat_io…VACUUMIN et CTEJSON_TABLE…pg_basebackup +
pg_combinebackup)pg_createsubscriber)pg_dump --filterCOPY peut rejeter des lignes,
MERGE)Au premier semestre 2025 :
Une seule version officielle de PostgreSQL existe, celle publiée par le PGDG. Elle est communautaire.
Il existe de nombreuses versions dérivées.
Entre de nombreux autres :
StatefulSetPod, Service,
Persistent Volume, …A chart is a collection of files that describe a related set of Kubernetes resources.
controller : le cerveau de l’histoire<-> État actuel
kubectl, …Tout ceci sera détaillé au fur et à mesure des modules, n’ayez crainte !
Pod CloudNativePG
ghcr.io/cloudnative-pg/cloudnative-pg:1.30.0Pod PostgreSQL
ghcr.io/cloudnative-pg/postgresqlminimal ou standard avec OS
18.1-minimal-trixieN’hésitez pas, c’est le moment !
Quelques explications concernant Kubernetes
Pod : un ou plusieurs conteneurs applicatifsService : permet d’accéder durablement à un ou
plusieurs PodsDeployment, Secret,
Configmap, …Pods)Custom Resource Definitions (extension de l’API
Kubernetes)Se trouvent dans le handout HTML
kubectl get pod -n cnpg-system
NAME READY STATUS
cnpg-controller-manager-65bfdb64c9-nztfj 1/1 Running
ClusterSecret et ROLE)appapp, streaming_replicapg_hba, pg_identSecrets : postgresql-app (contient le mot de passe du
rôle app)
kubectl describe secrets postgresql-appServices : postgresql-r, postgresql-ro,
postgresql-rwPod(s) : où est déployé PostgreSQLspec.bootstrap.initdb du fichier YAML du
Clusterinitdb est utiliséeCustom Resource Definition (v1.30)Cluster PostgreSQLSecret pour le mot de passedatabaseRoleReclaimPolicypostgresql.confpg_hba.confpg_ident.confALTER SYSTEM désactivé
allow_alter_system à false (v17+)primaryUpdateStrategy)kubectlkubectl cnpg --helpCluster, une instance
spécifiquestatuspsqlpromotebackuplogsreloadrestart%h du paramètre log_line_prefixPodlog_* non modifiablesMise en place facilitée
instances: N1 primaire et N-1 secondairesStreaming Replication
Slot de réplication créé par défaut
Asynchrone par défaut
Pod en conservant les volumescnpgpostmasterN’hésitez pas, c’est le moment !
La version en ligne des solutions de ces TP est disponible sur https://dali.bo/k1_solutions.
But : Prendre en main le cluster Kubernetes.
Se connecter à la machine qui vous est dédiée.
ssh root@A.B.C.D
Trouver la version de l’utilitaire
kubectl.
kubectl version
kubectl version
Client Version: v1.34.3
Kustomize Version: v5.7.1
Server Version: v1.34.0
L’utilitaire kubectl vous permet d’interagir avec le
cluster Kubernetes déployé.
Lister les nœuds du cluster Kubernetes.
kubectl get nodes
NAME STATUS ROLES AGE VERSION
kind-control-plane Ready control-plane 135m v1.34.0
kind-worker Ready <none> 134m v1.34.0
kind-worker2 Ready <none> 134m v1.34.0
Cet utilitaire sait avec quel cluster Kubernetes interagir grâce au
fichier ~/.kube/config qui se trouve dans le répertoire de
votre utilisateur.
But : Installer l’opérateur dans le cluster Kubernetes ainsi que des modules complémentaires.
Il existe plusieurs méthodes pour installer l’opérateur : soit en
appliquant directement les fichiers YAML soit en utilisant le
Helm Chart fourni par le projet. Pour cet atelier, nous
utiliserons la première méthode, plus simple et rapide.
Installer la version 1.30.0 de l’opérateur avec la commande
kubectl apply -f:
kubectl apply --server-side -f \
https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.30/releases/cnpg-1.30.0.yaml
namespace/cnpg-system serverside-applied
customresourcedefinition.apiextensions.k8s.io/backups.postgresql.cnpg.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/clusterimagecatalogs.postgresql.cnpg.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/clusters.postgresql.cnpg.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/imagecatalogs.postgresql.cnpg.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/poolers.postgresql.cnpg.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/scheduledbackups.postgresql.cnpg.io serverside-applied
serviceaccount/cnpg-manager serverside-applied
clusterrole.rbac.authorization.k8s.io/cnpg-manager serverside-applied
clusterrolebinding.rbac.authorization.k8s.io/cnpg-manager-rolebinding serverside-applied
configmap/cnpg-default-monitoring serverside-applied
service/cnpg-webhook-service serverside-applied
deployment.apps/cnpg-controller-manager serverside-applied
mutatingwebhookconfiguration.admissionregistration.k8s.io/cnpg-mutating-webhook-configuration serverside-applied
validatingwebhookconfiguration.admissionregistration.k8s.io/cnpg-validating-webhook-configuration serverside-applied
Les fichiers seront récupérés depuis internet et appliqués sur votre
cluster Kubernetes. Pour rappel, l’outil kubectl sait avec
quel cluster Kubernetes interagir grâce au fichier
kubeconfig.
Ces fichiers là contiennent la définition de différents ressources :
Namespace;CustomResourceDefinition pour les différentes
ressources que l’opérateur va gérer (Backup,
Cluster, …)ServiceAccount,
unClusterRoleBinding et surtout un déploiement du
controller CloudNativePG.Par défaut, le Controller, cerveau de l’opérateur, sera
déployé dans le Namespace cnpg-system, créé lors
de l’installation du l’opérateur. Ce controller n’est ni plus ni moins
qu’une application. On peut voir le controller avec la
commande kubectl get pods et en indiquant le bon
Namespace :
Lister les
Podsprésents dans lenamespacecnpg-system.
kubectl get pods -n cnpg-system
NAME READY STATUS RESTARTS AGE
cnpg-controller-manager-7fc549dc69-xq7gq 1/1 Running 0 11s
Retrouver la liste des nouvelles ressources Kubernetes disponibles grâce à l’opérateur CloudNativePG.
kubectl api-resources --api-group postgresql.cnpg.io
backups postgresql.cnpg.io/v1 true Backup
clusterimagecatalogs postgresql.cnpg.io/v1 false ClusterImageCatalog
clusters postgresql.cnpg.io/v1 true Cluster
imagecatalogs postgresql.cnpg.io/v1 true ImageCatalog
poolers postgresql.cnpg.io/v1 true Pooler
scheduledbackups postgresql.cnpg.io/v1 true ScheduledBackup
But : Déployer un cluster PostgreSQL mono-instance, s’y connecter et suivre les traces de l’opérateur et de l’instance.
Voici un exemple de fichier YAML très simple qui permet de déployer une instance PostgreSQL en version 17.5 avec 5 Go de volume associés au
PGDATAet 5 autres aux journaux de transactions.
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgresql-demo
spec:
imageName: ghcr.io/cloudnative-pg/postgresql:17.5-standard-bookworm
instances: 1
storage:
size: 5Gi
walStorage:
size: 5Gi
affinity:
enablePodAntiAffinity: true
topologyKey: kubernetes.io/hostname
podAntiAffinityType: preferred
resources:
requests:
memory: "256Mi"
cpu: "0.5"
limits:
memory: "512Mi"
cpu: "1"Quelques informations supplémentaires sur le contenu de ce fichier :
apiVersion : La version de l’API de Kubernetes est
utilisée ;kind : Le type d’objet créé ;metadata : Des informations pour identifier l’objet
;spec : La définition de l’objet en question (“l’état
désiré”) ;imageName : Le nom de l’image utilisée ;instances : Le nombre d’instances voulues (sera
toujours 1 primaire + le reste en secondaire(s) ;)storage : Les informations sur le stockage souhaité
pour le PGDATA ;walStorage : Les informations sur le stockage souhaité
pour les WALs ;affinity : Indique où et comment seront déployées les
instances de ce Cluster ;resources : L’indication de requests et
limits sur la RAM et CPU.Créer le fichier
postgresql-demo.yamldans lehome directorydedaliboet copier le contenu YAML ci-dessus :
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgresql-demo
spec:
imageName: ghcr.io/cloudnative-pg/postgresql:17.5-standard-bookworm
instances: 1
storage:
size: 5Gi
walStorage:
size: 5Gi
affinity:
enablePodAntiAffinity: true
topologyKey: kubernetes.io/hostname
podAntiAffinityType: preferred
resources:
requests:
memory: "256Mi"
cpu: "0.5"
limits:
memory: "512Mi"
cpu: "1"
Dans un autre terminal sur la VM, suivre les traces du
controlleravec la commandekubectl logs -f -n cnpg-system <POD>et l’utilitairejq. Pour retrouver le nom duPoddu controlleur, vous pouvez utiliserkubectl get pod -A.
kubectl logs -f -n cnpg-system cnpg-controller-manager-7fc549dc69-8v8xw | jq
Retourner dans l’ancienne session SSH et créer l’instance PostgreSQL à partir du fichier
~/postgresql-demo.yamlaveckubectl. En parallèle regarder ce qu’il se passe dans les traces ducontroller:
kubectl apply -f ~/postgresql-demo.yaml
cluster.postgresql.cnpg.io/postgresql-demo created
Une instance PostgreSQL est désormais en train d’être déployée par l’opérateur. Vous avez décrit ce que vous souhaitiez avoir, l’opérateur fait le reste.
Plusieurs choses se passent lorsque vous appliquez ce fichier avec
kubectl. Tout d’abord l’opérateur va déployer un premier
Pod appelé
<clusterName>-1-initdb-<random>.
initdb devrait vous faire penser à la la commande à
exécuter lorsque vous devez créer une instance manuellement par
exemple.
kubectl get pods --watch
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
postgresql-demo-1-initdb-5pndc 0/1 Pending 0 2s <none> <none> <none> <none>
Ce Pod là se repose sur une image qui doit être
téléchargée. C’est pour cela que vous devez avoir autorisé l’accès vers
internet (ou votre dépôt local d’images) à votre cluster. Lorsque
celle-ci est récupérée, le Pod est « amorcé » et les
conteneurs d’initialisation sont déployés, comme on peut le voir avec
cette seconde remontée. Ici il existe un conteneur d’initialisation mais
aucun n’est terminé.
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
postgresql-demo-1-initdb-5pndc 0/1 Init:0/1 0 13s <none> k8s-demo <none> <none>
Au fur et à mesure, le Pod passe par d’autres états…
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
postgresql-demo-1-initdb-5pndc 0/1 PodInitializing 0 24s 10.244.228.68 k8s-demo <none> <none>
… jusqu’à l’état Running. À cette étape-ci, le
Pod va notamment initialiser l’instance avec la création de
l’arborescence du PGDATA.
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
postgresql-demo-1-initdb-5pndc 1/1 Running 0 35s 10.244.228.68 k8s-demo <none> <none>Enfin, lorsque cette étape est terminée, l’opérateur CloudNativePG
déploie un autre Pod qui cette fois-ci ne porte plus le mot
initdb. Un numéro est ajouté à la fin du nom. Une adresse
IP est attribuée à ce Pod (IP privée RFC 1918) :
kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
postgresql-demo-1 1/1 Running 0 10m 10.244.228.69 k8s-demo <none> <none>
Votre Pod est prêt et donc votre instance aussi !
Se connecter à l’instance et vérifier la version de celle-ci. Vous pouvez utiliser
kubectl exec […]comme ceci :
kubectl exec -it postgresql-demo-1 -c postgres -- psql
ou, via le plugin :
kubectl cnpg psql postgresql-demo
psql (17.5 (Debian 17.5-1.pgdg120+1))
Type "help" for help.
postgres=# select version()\gx
-[ RECORD 1 ]----------------------------------------------------------------------------------------------------------------
version | PostgreSQL 17.5 (Debian 17.5-1.pgdg120+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
La commande kubectl exec -it permet d’exécuter un
programme au sein du Pod. L’outil psql étant
présent dans l’image, cela est possible. Essayez avec vim,
qui lui n’est pas présent dans l’image, un message d’erreur
apparaîtra.
Si besoin, pour quitter psql, vous pouvez utiliser
control+d, \q ou exit.
postgres=# \q
Suivre les traces de l’instance avec la commande
kubectl logs -f postgresql-demo-1.
kubectl logs -f postgresql-demo-1
{"level":"info","ts":"2025-12-11T14:23:56.028300944Z","logger":"postgres","msg":"record","logging_pod":"postgresql-demo-1","record":{"log_time":"2025-12-11 14:23:56.027 UTC","process_id":"30","session_id":"693ad3fb.1e","session_line_num":"5","session_start_time":"2025-12-11 14:23:55 UTC","transaction_id":"0","error_severity":"LOG","sql_state_code":"00000","message":"listening on Unix socket \"/controller/run/.s.PGSQL.5432\"","backend_type":"postmaster","query_id":"0"}}
{"level":"info","ts":"2025-12-11T14:23:56.075466089Z","logger":"postgres","msg":"record","logging_pod":"postgresql-demo-1","record":{"log_time":"2025-12-11 14:23:56.075 UTC","process_id":"36","session_id":"693ad3fc.24","session_line_num":"1","session_start_time":"2025-12-11 14:23:56 UTC","transaction_id":"0","error_severity":"LOG","sql_state_code":"00000","message":"database system was shut down at 2025-12-11 14:23:48 UTC","backend_type":"startup","query_id":"0"}}
{"level":"info","ts":"2025-12-11T14:23:56.108020409Z","logger":"postgres","msg":"record","logging_pod":"postgresql-demo-1","record":{"log_time":"2025-12-11 14:23:56.107 UTC","process_id":"30","session_id":"693ad3fb.1e","session_line_num":"6","session_start_time":"2025-12-11 14:23:55 UTC","transaction_id":"0","error_severity":"LOG","sql_state_code":"00000","message":"database system is ready to accept connections","backend_type":"postmaster","query_id":"0"}}Les logs de l’instance sont récupérés au format JSON, et sont en
l’état, peu exploitables. Pour les lire plus facilement, vous pouvez
utiliser l’outil jq.
kubectl logs -f postgresql-demo-1 | jq
[…]
{
"level": "info",
"ts": "2025-09-16T11:58:11.525416928Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "postgresql-demo-1",
"record": {
"log_time": "2025-09-16 11:58:11.525 UTC",
"process_id": "22",
"session_id": "68c950d3.16",
"session_line_num": "6",
"session_start_time": "2025-09-16 11:58:11 UTC",
"transaction_id": "0",
"error_severity": "LOG",
"sql_state_code": "00000",
"message": "database system is ready to accept connections",
"backend_type": "postmaster",
"query_id": "0"
}
}But : Découvrir les éléments automatiquement créés par l’opérateur.
Avec quelques lignes de YAML et peu de commandes, une instance PostgreSQL est déployée et accessible. De nombreuses choses sont créées automatiquement pour nous. Voyons de quoi il s’agit.
Bases de données
Retrouver la liste des bases de données dans l’instance déployée. La meta-commande
\lde psql vous permet de récupérer la liste des bases.
kubectl exec -it postgresql-demo-1 -- psql -c '\l'
ou
kubectl cnpg psql postgresql-demo -- -c '\l'
Defaulted container "postgres" out of: postgres, bootstrap-controller (init)
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+---------+-------+--------+-----------+-----------------------
app | app | UTF8 | libc | C | C | | |
postgres | postgres | UTF8 | libc | C | C | | |
template0 | postgres | UTF8 | libc | C | C | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | C | C | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
(4 rows)
Par défaut, une base de données app est créée dans
l’instance PostgreSQL.
Rôles et Secret
Retrouver la liste des rôles dans l’instance déployée. La méta-commande psql
\dupeut vous aider.
La méta-commande psql \du vous permet de récupérer la
liste des rôles.
kubectl exec -it postgresql-demo-1 -- psql -c "\du"
ou
kubectl cnpg psql postgresql-demo -- -c '\du'
List of roles
Role name | Attributes
-------------------+------------------------------------------------------------
app |
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS
streaming_replica | Replication
Par défaut deux rôles sont créés : app et
streaming_replica. Dans la liste des bases de données, on
peut d’ailleurs voir que le rôle app est propriétaire de la
base app.
Se déconnecter de l’instance.
postgres=# \q
Se connecter à la base de données
appavec le rôleapp. Une erreur devrait vous être retournée.
kubectl exec -it postgresql-demo-1 -- psql -U app
ou
kubectl cnpg psql postgresql-demo -- -U app
psql: error: connection to server on socket "/controller/run/.s.PGSQL.5432" failed: FATAL: Peer authentication failed for user "app"
command terminated with exit code 2
Se connecter à la base de données
appavec le rôleappet en passant par la stack TCP/IP.
L’authentification du rôle app avec la méthode
peer ne peut pas se faire. Mais alors comment se connecter
avec app ? En sachant que listen_addresses est
positionné à * par défaut, une solution pour tester la
connexion est de passer par la stack TCP/IP classique en
utilisant l’option -h de psql.
kubectl exec -it postgresql-demo-1 -- psql -U app -h 127.0.0.1
Defaulted container "postgres" out of: postgres, bootstrap-controller (init)
Password for user app:
Il faut comprendre que le 127.0.0.1 fait référence à
l’adresse localhost du Pod. On demande à psql,
via kubectl, de se connecter sur l’interface
localhost du Pod… mais il nous faut le mot de
passe de app… où le trouver ?
CloudNativePG crée automatiquement un Secret qui
contient des informations de connexion, notamment le mot de passe de
app.
Récupérer la liste des
Secretsdu cluster aveckubectl get secrets.
kubectl get secrets
NAME TYPE DATA AGE
postgresql-demo-app kubernetes.io/basic-auth 11 24m
postgresql-demo-ca Opaque 2 24m
postgresql-demo-replication kubernetes.io/tls 2 24m
postgresql-demo-server kubernetes.io/tls 2 24m
Récupérer le mot de passe présent dans le
Secretpostgresql-demo-app. N’oubliez pas qu’il est encodé en BASE64, il faut décoder le contenu obtenu.
kubectl get secret postgresql-demo-app -o json | jq '.data.password'
"VFdyejRQbmY1RWMwVjFjUHlqYkdFZnI5RG52WE5YaXN0NUhIaFZkOENwSkpKOEthVkVLUkNxUGwweTRzaGlVbw=="
Ou bien sans jq, avec une commande kubectl
un peu plus poussée :
kubectl get secret postgresql-demo-app --no-headers -o custom-columns=Passwd:.data.password
VFdyejRQbmY1RWMwVjFjUHlqYkdFZnI5RG52WE5YaXN0NUhIaFZkOENwSkpKOEthVkVLUkNxUGwweTRzaGlVbw==
Le résultat est encodé en BASE64. Il faut donc le décoder avec la commande suivante :
kubectl get secret postgresql-demo-app --no-headers -o custom-columns=Passwd:.data.password | base64 -d
TWrz4Pnf5Ec0V1cPyjbGEfr9DnvXNXist5HHhVd8CpJJJ8KaVEKRCqPl0y4shiUo
Se connecter à l’instance en utilisant le mot de passe.
kubectl exec -it postgresql-demo-1 -- psql -U app -h 127.0.0.1
Defaulted container "postgres" out of: postgres, bootstrap-controller (init)
Password for user app:
psql (17.5 (Debian 17.5-1.pgdg120+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off, ALPN: postgresql)
Type "help" for help.
app=>
L’astuce d’utiliser kubectl et psql avec
l’option -h permet à des administrateurs de se connecter,
mais cela n’est pas envisageable pour des applications. Les applications
doivent passer par les objets Services.
Services
Un Service est une couche d’abstraction qui permet
d’accéder à un ensemble de Pods spécifiques. L’association
Service - Pods se fait via des
labels. Un label est une étiquette, un tag,
apposée à une ressource.
Retrouver la liste des
Servicesdans le cluster Kubernetes.
Comme toutes les autres ressources Kubernetes, vous pouvez récupérer
les objets Services avec get.
kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 6h24m
postgresql-demo-r ClusterIP 10.105.219.134 <none> 5432/TCP 6h11m
postgresql-demo-ro ClusterIP 10.105.155.153 <none> 5432/TCP 6h11m
postgresql-demo-rw ClusterIP 10.105.191.44 <none> 5432/TCP 6h11m
À chaque cluster PostgreSQL déployé, trois services sont créés :
postgresql-demo-rw qui est en lecture/écriture;postgresql-demo-ro qui sont en lecture seule;postgresql-demo-r.Retrouver les labels définis sur le
Podde votre instance :
kubectl get pod postgresql-demo-1 --show-labels
NAME READY STATUS RESTARTS AGE LABELS
postgresql-demo-1 1/1 Running 0 26h cnpg.io/cluster=postgresql-demo,cnpg.io/instanceName=postgresql-demo-1,cnpg.io/instanceRole=primary,cnpg.io/podRole=instance,role=primary
Retrouver la description du
Servicepostgresql-demo-roet retrouver la partieSelectorqui indique à quel(s)Pod(s) sera associé ceService.
kubectl describe service postgresql-demo-ro
Name: postgresql-demo-ro
Namespace: default
Labels: cnpg.io/cluster=postgresql-demo
Annotations: cnpg.io/operatorVersion: 1.28.1
Selector: cnpg.io/cluster=postgresql-demo,cnpg.io/instanceRole=replica
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.123.203
IPs: 10.96.123.203
Port: postgres 5432/TCP
TargetPort: 5432/TCP
Endpoints:
Session Affinity: None
Internal Traffic Policy: Cluster
Events: <none>
Lorsqu’une bascule a lieu, les labels des Pods
sont mis à jour et l’association Service - Pod
est automatiquement adaptée.
De ce fait, si vos applications utilisent bien le nom du
Service dans les informations de connexion, elles seront
automatiquement redirigées vers la nouvelle instance primaire par
exemple.
Il existe plusieurs méthodes pour créer un rôle dans une instance.
L’ordre SQL CREATE ROLE … peut évidemment être utilisé,
mais pour cet exemple, nous allons passer par la méthode déclarative et
demander à l’opérateur de faire en sorte que le rôle soit présent dans
l’instance.
Créer un fichier
~/roles.yaml.
touch ~/roles.yaml
Créer un rôle
dbaayant les droitsSUPERUSERdans l’instance. Cela peut se faire grâce à la ressourceDatabaseRole.
Par exemple, le fichier ~/roles.yaml ressemblerait à
:
---
apiVersion: postgresql.cnpg.io/v1
kind: DatabaseRole
metadata:
name: dba
spec:
cluster:
name: postgresql-demo
name: dba
comment: "Utilisateur Support"
login: true
superuser: true
databaseRoleReclaimPolicy: deleteAppliquer la modification avec
kubectl apply -f ~/roles.yaml.
kubectl apply -f ~/roles.yaml
databaserole.postgresql.cnpg.io/dba created
Vérifier que le rôle a bien été créé.
kubectl exec -it postgresql-demo-1 -- psql -c '\du'
Defaulted container "postgres" out of: postgres, bootstrap-controller (init)
List of roles
Role name | Attributes
-----------------------+----------------------------------------
--------------------
app |
cnpg_metrics_exporter |
dba | Superuser
postgres | Superuser, Create role, Create DB, Repl
ication, Bypass RLS
streaming_replica | Replication
Le rôle est bien créé mais il n’a actuellement pas de mot de passe configuré.
Si vous souhaitez en ajouter un, vous pouvez le faire de plusieurs manières :
ALTER ROLE … SET PASSWORD …
;\password <user> (la préférer à
ALTER ROLE…);Secret.C’est cette dernière méthode que nous allons suivre. Pour cela, le
mot de passe n’est jamais passé en clair dans le fichier YAML. Il est en
fait nécessaire de créer un objet Secret qui contiendra ce
mot de passe encodé en BASE64 ainsi que le nom du rôle. C’est ce
Secret là qui sera utilisé dans le fichier YAML.
Encoder le nom du rôle (
dba) en BASE64.
printf "dba" | base64
ZGJh
Encoder le mot de passe (
ilovemydba) en BASE64.
Vous pouvez ajouter un espace avant echo pour que la
commande n’apparaisse pas dans l’historique de l’utilisateur
dalibo.
printf "ilovemydba" | base64
aWxvdmVteWRiYQ==
Créer un fichier
~/secret.yamlavec le contenu suivant puis créer leSecret.
apiVersion: v1
data:
username: ZGJh
password: aWxvdmVteWRiYQ==
kind: Secret
metadata:
name: secret-password-dba
labels:
cnpg.io/reload: "true"
type: kubernetes.io/basic-authkubectl apply -f ~/secret.yaml
secret/secret-password-dba created
Ajouter ce mot de passe à la définition du rôle
dbadans le fichier~/roles.yaml, dans le champpasswordSecret.
---
apiVersion: postgresql.cnpg.io/v1
kind: DatabaseRole
metadata:
name: dba
spec:
cluster:
name: postgresql-demo
name: dba
comment: "Utilisateur Support"
login: true
superuser: true
databaseRoleReclaimPolicy: delete
passwordSecret:
name: secret-password-dbaAppliquer les modifications.
kubectl apply -f ~/roles.yaml
databaserole.postgresql.cnpg.io/dba configured
Se connecter avec ce nouveau rôle à la base
postgres.
Le rôle dba peut désormais se connecter avec son super
mot de passe. Par exemple :
kubectl exec -it postgresql-demo-1 -c postgres -- psql -d postgres -U dba -h 127.0.0.1
Defaulted container "postgres" out of: postgres, bootstrap-controller (init)
Password for user dba:
psql (17.0 (Debian 17.0-1.pgdg110+1))
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off, ALPN: postgresql)
Type "help" for help.
postgres=#
Créer une base de données
db1dans l’instancepostgresql-demo. Le propriétaire de cette base doit être le rôleapp. Pour cela, créer un fichierdb1.yamlcontenant la définition d’une ressourceDatabase.
Voici la déclaration d’une telle base de données.
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
name: db1
spec:
name: db1
owner: app
cluster:
name: postgresql-demoCréer la nouvelle ressource avec
kubectl apply -f ~/db1.yaml.
kubectl apply -f db1.yaml
database.postgresql.cnpg.io/db1 created
Vérifier la présence de cette base de données dans l’instance.
Plusieurs possibilités. En voici une :
kubectl cnpg psql postgresql-demo -- -c "\l"
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+---------+-------+--------+-----------+-----------------------
app | app | UTF8 | libc | C | C | | |
db1 | app | UTF8 | libc | C | C | | |
postgres | postgres | UTF8 | libc | C | C | | |
template0 | postgres | UTF8 | libc | C | C | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | C | C | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
(5 rows)
But : Déployer une instance secondaire dans le
cluster postgresql-demo.
Notre instance actuellement déployée ne possède pas de secondaire.
L’ajout de secondaire se fait facilement en modifiant le paramètre
instances dans la section spec de notre
fichier yaml.
Déployer un secondaire à votre instance en modifiant le paramètre
instancesà 2.
kubectl apply -f ~/postgresql-demo.yaml
Un second Pod va être déployé.
kubectl get pod | grep demo
postgresql-demo-1 1/1 Running 0 73m
postgresql-demo-2-join-h2vvw 0/1 Init:0/1 0 38s
kubectl get pod | grep demo
postgresql-demo-1 1/1 Running 0 73m
postgresql-demo-2-join-h2vvw 0/1 PodInitializing 0 58s
kubectl get pod | grep demo
postgresql-demo-1 1/1 Running 0 73m
postgresql-demo-2 1/1 Running 0 31s
Et voilà, un secondaire a été créé ! L’opérateur CloudNativePG
s’assure de tout configurer : ajout du paramètre
primary_conninfo, création du fichier
standby.signal, mise à jour de pg_hba.conf,
slot de réplication, etc. Le secondaire se connecte alors au
primaire en utilisant la réplication physique native de PostgreSQL
(Streaming Replication).
Regardons la configuration qui est mise en place par défaut.
Se connecter avec
psqlau secondaire nouvellement créé.
kubectl exec -it postgresql-demo-2 -c postgres -- psql
Récupérer le contenu du paramètre
primary_conninfo.
postgres=# \x
Expanded display is on.
postgres=# SHOW primary_conninfo ;
-[ RECORD 1 ]----+---------------------
primary_conninfo | host=postgresql-demo-rw user=streaming_replica […]
La sortie a été mise en forme pour plus de lisibilité.
host=postgresql-demo-rw
user=streaming_replica
port=5432
sslkey=/controller/certificates/streaming_replica.key
sslcert=/controller/certificates/streaming_replica.crt
sslrootcert=/controller/certificates/server-ca.crt
application_name=postgresql-demo-2
sslmode=verify-caLe secondaire utilise le Service
postgresql-demo-rw pour accéder à l’instance primaire. Vous
comprendrez qu’une résolution DNS interne au cluster Kubernetes
doit se faire pour retrouver l’adresse IP associée. La réplication
utilise l’utilisateur dédié streaming_replica créé par
CloudNativePG lors du déploiement de la première instance.
L’authentification se fait par certificat. Le paramètre
application_name permet d’indiquer un nom d’application
dans les informations liée la connexion.
Se connecter avec
psqlau primaire.
kubectl exec -it postgresql-demo-1 -c postgres -- psql
ou, via le plugin :
kubectl cnpg psql postgresql-demo
Récupérer le contenu de la table
pg_stat_replication.
postgres=# SELECT * FROM pg_stat_replication\gx
-[ RECORD 1 ]----+------------------------------
pid | 16026
usesysid | 16386
usename | streaming_replica
application_name | postgresql-demo-2
client_addr | 10.244.1.11
client_hostname |
client_port | 54458
backend_start | 2025-12-12 07:13:15.829844+00
backend_xmin |
state | streaming
sent_lsn | 0/B000130
write_lsn | 0/B000130
flush_lsn | 0/B000130
replay_lsn | 0/B000130
write_lag | 00:00:00.000806
flush_lag | 00:00:00.013548
replay_lag | 00:00:00.014192
sync_priority | 0
sync_state | async
reply_time | 2025-12-12 07:17:57.892685+00
Par défaut, c’est une réplication asynchrone qui est créée.
Récupérer le contenu de la table
pg_replication_slots.
postgres=# SELECT * FROM pg_replication_slots \gx
-[ RECORD 1 ]-------+------------------------
slot_name | _cnpg_postgresql_demo_2
plugin |
slot_type | physical
datoid |
database |
temporary | f
active | t
active_pid | 16026
xmin |
catalog_xmin |
restart_lsn | 0/C000060
confirmed_flush_lsn |
wal_status | reserved
safe_wal_size |
two_phase | f
inactive_since |
conflicting |
invalidation_reason |
failover | f
synced | f
Par défaut, CloudNativePG crée automatiquement un slot de réplication pour sécuriser la réplication. Son nom permet de savoir facilement à quoi il correspond.
Le slot de réplication garantit au secondaire que son primaire ne recyclera pas les journaux de transactions dont il aura encore besoin. Le secondaire peut donc prendre un retard conséquent sans risque de décrochage. Attention à l’accumulation des WALs qu’il peut y avoir sur le primaire en cas de retard ou de problème (coupure réseau, crash secondaire, etc).
Sur le primaire, créer une table dans la base
app.
Sur le primaire :
postgres=# \c app
app=# CREATE TABLE ma_table (i int);
CREATE TABLE
Vérifier qu’elle se trouve également sur le secondaire.
Sur le secondaire :
postgres=# \c app
You are now connected to database "app" as user "postgres".
app=# \dt
List of relations
Schema | Name | Type | Owner
--------+----------+-------+----------
public | ma_table | table | postgres
(1 row)
La réplication fonctionne !
L’opérateur CloudNativePG veille à déployer les instances PostgreSQL sur des nœuds différents afin de garantir la disponibilité. Cela permet de réduire les risques liés à un incident en s’assurant que toutes les instances ne sont pas affectées simultanément. Cette configuration permet également la répartition de la charge entre plusieurs nœuds pour des opérations de lecture.
Trouver le nom du nœud où est déployée chaque instance.
kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
postgresql-demo-1 1/1 Running 0 16h 10.244.2.11 kind-worker2 <none> <none>
postgresql-demo-2 1/1 Running 0 8m34s 10.244.1.11 kind-worker <none> <none>
Cette répartition est possible grâce notamment à la configuration de
l’affinité et anti-affinité entre Pod ajoutée au YAML du
Cluster.
But : Découvrir des fonctionnalités plus complexes.
Il existe plusieurs méthodes pour mettre en place une réplication synchrone. Le but ici n’est pas de les évoquer ni de les comparer, mais simplement de voir le principe de la configuration.
Pour l’exemple, nous mettrons en place la méthode par
Quorum.
Modifier la configuration de l’instance en rajoutant le bloc suivant à votre fichier
~/postgresql-restored-demo.yaml.
kubectl apply -f postgresql.yaml
cluster.postgresql.cnpg.io/postgresql-demo configured
Vérifier que la réplication est synchrone en regardant le champ
sync_statede la vuepg_stat_replicationde l’instance primaire.
postgres=# select * from pg_stat_replication\gx
-[ RECORD 1 ]----+------------------------------
pid | 1210
usesysid | 16386
usename | streaming_replica
application_name | postgresql-restored-demo-1
client_addr | 10.244.3.29
client_hostname |
client_port | 51518
backend_start | 2025-12-15 18:29:04.335147+00
backend_xmin |
state | streaming
sent_lsn | 0/D000000
write_lsn | 0/D000000
flush_lsn | 0/D000000
replay_lsn | 0/D000000
write_lag |
flush_lag |
replay_lag |
sync_priority | 1
sync_state | quorum
reply_time | 2025-12-15 18:52:30.849357+00
sync_state est bien à quorum.
Plus d’informations sur la documentation(https://cloudnative-pg.io/docs/current/replication#synchronous-replication.
Pour voir comment une application peut se connecter à une instance, nous allons déployer pgAdmin dans le cluster Kubernetes.
Ouvrir une nouvelle session SSH. Passer en tant qu’utilisateur
dalibo.
Créer le fichier
~/pgadmin.yamlavec le contenu suivant et le déployer dans le cluster Kubernetes.
apiVersion: apps/v1
kind: Deployment
metadata:
name: pgadmin
spec:
replicas: 1
selector:
matchLabels:
app: pgadmin
template:
metadata:
labels:
app: pgadmin
spec:
containers:
- name: pgadmin
image: dpage/pgadmin4
ports:
- containerPort: 80
env:
- name: PGADMIN_DEFAULT_EMAIL
value: admin@example.com
- name: PGADMIN_DEFAULT_PASSWORD
value: adminkubectl apply -f ~/pgadmin.yaml
deployment.apps/pgadmin created
Récupérer le nom du
PodpgAdmin déployé, et lancer la commande suivante :
kubectl port-forward --address 0.0.0.0 pgadmin-*****-***** 8888:80
Cette commande permet de forwarder le trafic entrant sur le
port TCP 8888 de la machine vers le port 80 du Pod pgAdmin,
rendant ainsi accessible l’application. Cette méthode reste valide pour
des démonstrations, n’allez pas mettre ça en production :).
Accéder à l’interface de pgAdmin via votre navigateur
http://adresseippublique:8888et connectez vous à l’interface (admin@example.com / admin).
L’adresse IP publique de la machine peut être retrouvée avec la commande suivante :
ip -f inet addr show ens2
2: ens2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
altname enp0s2
inet 51.158.67.253/32 metric 100 scope global dynamic ens2
valid_lft 842sec preferred_lft 842sec
Créer une nouvelle connexion avec les informations suivantes :
postgresql-demo (Onglet General) ;postgresql-demo-rw (Onglet
Connection) ;5432 ;app;Secret;Save.Créer une nouvelle connexion avec cette fois-ci
postgresql-demo-rocomme paramètreHost name/addresset créer une tableCREATE TABLE ma_table (i int);.
Cette commande ne pourra pas être exécutée comme le
Service renvoie sur une instance secondaire qui est
nécessairement en lecture seule. Le message d’erreur sera :
ERROR: cannot execute CREATE TABLE in a read-only transaction
SQL state: 25006
Vous avez maintenant l’application pgAdmin déployée dans Kubernetes
qui a accès à l’instance postgresql-demo. L’exemple
ci-dessus montre comment une application peut accéder à une base de
données en utilisant la ressource Service prévue à cet
effet. Application et base se trouvent toutes deux dans Kubernetes.
Accéder à l’instance PostgreSQL depuis une application externe (déployée
ailleurs) est plus complexe, demande le déploiement d’autres ressources…
mais ne sera pas traité ici.
Ce module se consacre à la configuration des ressources, que ce soit
au niveau de Kubernetes et des Pods de nos
Clusters mais également sur la configuration PostgreSQL qui
est indispensable à faire. L’opérateur fait un certain nombre
d’opérations pour nous, mais on verra qu’il reste encore de nombreuses
choses à faire.
Quelques paramètres PostgreSQL basiques seront présentés et nous verrons comment l’opérateur configure par défaut les instances. Il existe plus de 300 paramètres pour le SGBD PostgreSQL, tout ne sera pas abordé dans cette formation. Nos autres formations sont là pour ça.
PodCluster et de
l’opérateurPodAvant de prendre le temps d’explorer la configuration des
Cluster et de PostgreSQL, un petit détour est nécessaire
sur les notions de base de l’attribution de ressources dans Kubernetes.
Ces bases étant posées, nous pourrons nous attarder sur les implications
que cela peut avoir sur un Cluster et notamment concernant
la Quality of Service Class d’un Pod.
Il est évident que PostgreSQL doit également être correctement configuré. Cela fera l’objet de la dernière partie de ce module.
D’abord sur Kubernetes
Dans Kubernetes, les Pods qui vont exécuter une charge
applicative peuvent se voir attribuer des ressources. Ces ressources
sont appliquées par le container runtime et, in fine,
par le kernel du nœud. Sur des nœuds Linux, c’est le mécanisme
de cgroup qui entre en jeu. Cette configuration est
totalement optionnelle. Vous pouvez tout à fait déployer une application
sans aucune attribution de ressources.
Prenons l’exemple des ressources RAM et CPU qui sont les principales à configurer. Le schéma montre la différence entre les deux catégories de ressources :
requests ;limits.Dans l’exemple du schéma, 50 millicores de CPU
sont demandés (requests) ainsi que 4 Go de RAM.
Le plan de contrôle va chercher un Node qui est en
capacité de répondre à cette demande de ressources. Ce sont les
quantités requises pour qu’un Pod puisse être déployé
(d’autres éléments que les ressources disponibles peuvent entrer en jeu
sur le déploiement ou non d’un Pod, mais ils ne seront pas
abordés dans cette formation). Lorsque le plan de contrôle a trouvé un
Node, le déploiement du Pod va se faire.
Les limits quant à elles fixent une quantité maximale
qu’un Pod peut consommer à un instant. Pour la partie CPU,
le kernel va tout simplement limiter la consommation selon ce
qui aura été renseigné. On parle de CPU throttling. Pour la
partie RAM, le kernel va faire appel au OOM Killer
pour faire respecter la limite. Potentiellement donc, Kubernetes peut
évincer le Pod s’il consomme trop de ressources du
nœud.
Il est possible de trouver les capacités disponibles sur un
Node dans sa
description kubectl describe nodes <NODE>, plus
particulièrement dans la partie Allocable.
Allocable:
cpu: 12
ephemeral-storage: 486903968Ki
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 16052384Ki
pods: 110Les Huge Pages sont un type ressource moins connu, mais pour autant très intéressant à connaître dans le cas de PostgreSQL. Nous avons une page dédiée sur ce sujet dans notre base de connaissances.
L’utilisation des Huge Pages permet d’avoir moins de blocs adressables à quantité de mémoire équivalente, ceci permet d’améliorer l’efficacité du cache utilisé par le processeur pour gérer les adresses de ces blocs mémoires.
Les Huge Pages présentent deux avantages :
L’activation des Huge Pages se fait en modifiant le
paramètre noyau vm.nr_overcommit_hugepages du
Node. Elle sera donc prise en compte pour tous les
Pods déployés sur votre Node. Cela suppose
qu’un accès aux serveurs soit possible. Sur des instances avec un
shared_buffers élevé, et de nombreux clients, l’économie de
mémoire peut être importante.
spec.resources d’un objet Cluster
requests et limitsPod
instance-managerNous retrouvons, dans la définition d’un Cluster, une
section resources qui permet de renseigner, comme vu juste
avant, les quantités de RAM, de CPU et de Huge Pages qui seront
attribuées aux Pods du Cluster.
Il n’est pas possible d’avoir des allocations de ressources différentes entre une instance primaire et une instance secondaire. Ceci est plutôt une bonne chose. On s’assure en effet qu’en cas de bascule sur un secondaire, il sera en capacité de soutenir la charge qu’il y avait sur l’ancien primaire.
Aussi, il faut bien avoir en tête que ces ressources là, sont
attribués au Pod, et pas uniquement à PostgreSQL. Tout ce
qui existerait en plus dans le Pod peut consommer des
ressources, et notamment l’instance-manager qui est
installé dans le Pod via le mécanisme
d’init-container (voir notre article de
blog à ce sujet).
Si vous créez et utilisez des images personnalisées, en rajoutant des
outils qui vous sont propres, soyez attentifs à leur consommation. De la
même manière qu’il est préconisé de dédier des machines virtuelles à
PostgreSQL, le Pod devrait être dédié à PostgreSQL. Si des
outils supplémentaires doivent être utilisés, l’utilisation de
sidecar container semble plus approprié. Voir la documentation
officielle sur ce sujet.
Voici un extrait d’un fichier YAML définissant un
Cluster configuré avec 2 Go de RAM et 0.2 CPU. La
définition des ressources se fait dans la partie spec du
Cluster.
La modification de l’un ou l’autre des paramètres dans la définition
du Cluster déclenchera une recréation du ou des
Pods. Attention à ne pas le faire en production ! Prévoir
des créneaux de maintenance est primordial.
Assez logiquement, les valeurs positionnées dans
requests ne peuvent pas être plus hautes que celles de
limits. Si une telle modification venait à être faite, une
erreur sera remontée lors de l’application de la modification. Par
exemple :
clusters.postgresql.cnpg.io "cluster-example" was not valid:
* spec.resources.requests.memory: Invalid value: "1000Mi": Memory request is greater than the limit
Pod dans le Namespace
cnpg-system Limits:
cpu: 100m
memory: 200Mi
Requests:
cpu: 100m
memory: 100Mi
Par défaut, une configuration des ressources est faite pour le ou les
Pods controller de CloudNativePG. Les ressources
demandées sont plutôt faibles.
Il est possible de retrouver les ressources attribuées au
controller (ou tout autre Pod) avec la commande
kubectl suivante et l’utilitaire jq. D’autres
méthodes existent pour retrouver cette information (avec
kubectl describe par exemple).
kubectl get pods -n cnpg-system cnpg-controller-manager-84d498b97-vr4vb -o json | jq .spec.containers[].resources
{
"limits": {
"cpu": "100m",
"memory": "200Mi"
},
"requests": {
"cpu": "100m",
"memory": "100Mi"
}
}
Le Pod controller est géré par une ressource
Deployment. Si un changement de ressources doit être fait,
il faudra le faire dans le Deployment
cnpg-controller-manager certainement présent dans le
Namespace cnpg-system. Là aussi, pour chaque
modification, un nouveau Pod sera créé.
PodDans Kubernetes, les ressources attribuées à un Pod
définissent automatiquement une Quality of Service. Selon les
requests et limits choisis pour la RAM et le CPU, une
des trois QoS va être automatiquement attribuée au
Pod.
Cette QoS est d’autant plus d’importante pour des
applications de type SGBD comme PostgreSQL. Kubernetes utilise les
classes de QoS pour prendre des décisions sur l’éviction de
Pods si les ressources du Node viennent à
manquer.
Guaranteed : Les paramètres
requests et limits en RAM et CPU doivent être définis
pour tous les conteneurs du Pod. Les valeurs de
requests et limits doivent être les mêmes. Exemple
d’attribution dans la définition d’un Cluster.
Cette QoS là permet de limiter les risques d’éviction du
Pod. Il ne seront ciblés qu’en dernier recours. Les
processus du Pod se voient attribuer un
oom_score_adj. Dans le cas Guaranteed, la valeur
de ce paramètre sera de -997. Aussi si des
Pods doivent être évincés à cause d’une sur-utilisation des
ressources du Node, notre Cluster PostgreSQL
sera un des dernières à être ciblé.
La version 1.27 de CloudNativePG va même encore plus loin en
positionnant le paramètre PG_OOM_ADJUST_VALUE à 0 pour le
postmaster. Ainsi, ce sera le seul processus du
Pod à conserver la valeur de -997. Les
processus enfant auront quand à eux un oom_score_adj à 0.
De cette manière, si l’OOM killer entre en jeu, il ciblera
encore moins le postmaster.
Les ressources demandées seront garanties durant toute la durée de
vie du Pod. Pour cette raison et les ajustements faits au
niveau de oom_score_adj, nous préconisons d’utiliser la
QoS Guaranteed pour les Pods embarquant
PostgreSQL.
Pod.Si les ressources demandées ne sont pas disponibles sur le
Node, le Pod ne pourra pas être créé. Dans le
cas d’éviction de Pod, les Pods avec cette
QoS seront ciblés après les Pods en Best
Effort.
Pod en question pourra utiliser les ressources dans la
limite de ce qu’il reste de disponible sur le Node. les
Pod avec QoS seront les premiers ciblés en cas
d’éviction. Cette QoS ne doit pas être utilisée pour des
instances PostgreSQL en production.Puis sur PostgreSQL
ClusterSi la configuration « système » des ressources (RAM, CPU et Huge Pages) est primordiale, il reste à configurer PostgreSQL pour qu’il soit adapté à celle-ci.
Pour le bon fonctionnement des instances avec l’opérateur, certains
paramètres ne peuvent pas être modifiés. Il s’agit des
Fixed Parameters comme mentionné plus tôt dans la
formation.
L’opérateur ne reconfigure pas automatiquement vos instances PostgreSQL pour vous (sauf cas bien précis ou d’évènements particuliers comme une bascule automatique).
Gardez en tête que c’est à vous de le faire, notamment pour avoir une bonne adéquation avec les ressources « système ». Retenez aussi que PostgreSQL est très conservateur dans ses paramètres par défaut. Autrement dit, sur des infrastructures modernes, une reconfiguration systématique des paramètres doit être faite.
Aussi, maintenant que PostgreSQL est déployé avec l’opérateur, tout
doit se faire (autant que possible) dans la définition du
Cluster, cette ressource étant la source de vérité de ce
qui doit exister dans Kubernetes. Modifier la configuration avec des
ordres SQL reste faisable.
Quelques paramètres seront passés en revue. Nos autres formations, notamment la DBA2 ou PERF1 apporteront de nombreux éléments à ce sujet.
initdb (section
boostrap)Tous les paramètres PostgreSQL ont des valeurs par défaut qui résultent d’un choix des développeurs de PostgreSQL et de la communauté en fonction des nouveautés, des pratiques, des aspects de sécurité, etc…
La modification de leur valeur peut se faire à plusieurs endroits.
Typiquement, initdb permet de modifier des paramètres que
l’on pourrait qualifier de « structurels » pour l’instance. À titre
d’exemple :
checksums ;CloudNativePG modifie également certaines valeurs de certains paramètres en plus des Fixed Parameters dont on parlera juste après.
Pour donner quelques exemples :
allow_alter_systemmax_parallel_workersmax_worker_processeswal_keep_sizewal_levelwal_receiver_timeoutLa modification de ces paramètres peut avoir du sens. Mais garder en tête que l’opérateur modifie par défaut un ensemble de paramètres, qui parfois, peuvent ne pas être adaptés à votre besoin.
Sur une instance vierge, la requête suivante vous permet de les retrouver :
L’ajout d’intermédiaire comme CloudNativePG implique certains choix de configuration. Il est donc bienvenu de connaître ce que cela implique et de connaître toutes les strates possibles de configuration, sans oublier vos propres scripts.
Impossible à modifier
Can't set fixed configuration parameterAssurer la gestion de l’instance par l’opérateur
Des paramètres peu modifiés en général
Dans le cas d’un déploiement avec CloudNativePG, certains paramètres ne sont pas modifiables. L’opérateur vous empêchera de modifier tel ou tel paramètre. Si vous essayez, un message d’erreur vous sera remonté. Par exemple :
kubectl apply -f postgresql-config.yaml
The Cluster "postgresql-config" is invalid: spec.postgresql.parameters.archive_command: Invalid value: "/bin/true": Can't set fixed configuration parameter
La liste des paramètres fixés se trouve dans la documentation.
L’objectif est simple : s’assurer que la configuration des instances PostgreSQL soit toujours adaptée à une utilisation avec l’opérateur. Cela concerne des paramètres globaux de l’instance qui jouent un rôle dans la gestion des traces, la gestion du recovery, de la réplication, ou encore l’emplacement des dossiers de données. Sur des instances installées sur des machines virtuelles, ces paramètres ne sont généralement modifiés qu’une seule fois puis ne sont plus touchés une fois l’instance en production.
Nous l’avons vu CloudNativePG propose, ou impose, une configuration pour certains paramètres. Mais il laisse aussi la possibilité de les modifier. Si certains changements découlent des retours de vos développeurs ou utilisateurs (requêtes lentes, timeout de transaction, etc), un bon nombre de paramètres peuvent être ajustés en amont.
Le choix de la locale peut être fait dans la partie
spec.bootstrap.initdb qui ne sera appliquée que lors de la
création de l’instance. Par exemple, pour avoir par défaut des bases
avec le paramètre Collate à fr_FR.utf8, il
faut utliser la configuration suivante dans la définition de votre
Cluster.
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+------------+-------+--------+-----------+-----------------------
app | app | UTF8 | libc | fr_FR.utf8 | C | | |
postgres | postgres | UTF8 | libc | fr_FR.utf8 | C | | |
template0 | postgres | UTF8 | libc | fr_FR.utf8 | C | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | fr_FR.utf8 | C | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
(4 rows)
Cette étape est primordiale pour que vos instances soient adaptées à
vos applications. Il est préférable de se poser les questions avant de
déployer une instance plutôt que de devoir revenir plus tard sur des
configurations qui nécessiteront des redémarrages et donc des arrêts de
production, voire même la reconstruction de votre
Cluster.
Certaines reconfigurations doivent se faire selon l’infrastructure et
les ressources attribuées à votre Cluster mais également
selon vos besoins.
shared_bufferswork_memmaintenance_work_memshared_buffers permet de configurer la taille du cache
disque de PostgreSQL. Chaque fois qu’un utilisateur veut extraire des
données d’une table (par une requête SELECT) ou modifier
les données d’une table (par exemple avec une requête
UPDATE), PostgreSQL doit d’abord lire les lignes impliquées
et les mettre dans son cache disque. Cette lecture prend du temps. Si
ces lignes sont déjà dans le cache, l’opération de lecture n’est plus
utile, ce qui permet de renvoyer plus rapidement les données à
l’utilisateur.
Ce cache est en mémoire partagée, et donc commun à tous les processus PostgreSQL. Généralement, il faut lui donner une grande taille, tout en conservant malgré tout la majorité de la mémoire pour le cache disque du système, à priori plus efficace pour de grosses quantités de données. Le configurer à 25% de la RAM en première intention est généralement un bon point de départ.
Les processus de PostgreSQL ont accès à la mémoire partagée, définie
principalement par shared_buffers, mais ils ont aussi leur
mémoire propre. Cette mémoire n’est utilisable que par le processus
l’ayant allouée.
Le paramètre le plus important est work_mem, qui définit
la taille maximale de la mémoire de travail que peut utiliser un
processus dans un nœud de requête, en particulier pour des tris
(ORDER BY), certaines agrégations (par hash),
certaines jointures (hash join notamment), des déduplications
(DISTINCT), des CTE matérialisées
(WITH … AS …)…
Autre paramètre capital, maintenance_work_mem définit la
mémoire utilisable pour les opérations de maintenance lourdes :
VACUUM, CREATE INDEX, REINDEX,
ajouts de clé étrangère…
Cette mémoire liée au processus est rendue immédiatement après la fin de l’ordre concerné.
maintenance_work_mem peut être monté de 256 Mo à 1 Go,
voire plus sur les machines récentes, car il concerne des opérations
lourdes (indexation, nettoyage des index par VACUUM…).
Leurs consommations de RAM s’additionnent, mais en pratique, ces
opérations sont rarement exécutées plusieurs fois simultanément.
Monter au-delà de 1 Go n’a d’intérêt que pour la création ou la réindexation de très gros index.
Configurer work_mem est plus compliqué.
Si work_mem est trop bas, beaucoup d’opérations ne
s’effectueront pas en RAM. Par exemple, si une jointure par hachage
impose d’utiliser 100 Mo en mémoire, mais que work_mem vaut
10 Mo, PostgreSQL écrira des dizaines de mégaoctets sur disque à chaque
appel de la jointure. Par contre, si work_mem vaut 120 Mo,
aucune écriture n’aura lieu sur disque, ce qui accélérera généralement
la requête et réduira les I/O.
Trop de fichiers temporaires peuvent ralentir les opérations, voire
saturer le disque. Supervisez leur présence. Mais il est illusoire de
vouloir éviter tous les fichiers temporaires des grosses requêtes en
montant work_mem à une valeur déraisonnable.
wal_leveleffective_io_concurrencyLe paramètre wal_level fixe le comportement à adopter.
Comme son nom l’indique, il permet de préciser le niveau d’informations
que l’on souhaite avoir dans les journaux. Il connaît trois
valeurs :
replica est adapté à l’archivage ou la
réplication, en plus de la sécurisation contre les arrêts brutaux. C’est
le niveau par défaut. L’optimisation évoquée plus haut n’est pas
possible.minimal n’offre que la protection contre les
arrêts brutaux, mais ne permet ni réplication ni sauvegarde PITR. Ce
niveau ne sert plus guère qu’aux environnements ni archivés, ni
répliqués, pour réduire la quantité de journaux générés, comme dans
l’optimisation ci-dessus.logical est le plus complet et doit être
activé pour l’utilisation du décodage logique, notamment pour utiliser
la réplication logique. Il n’est ni nécessaire pour la sauvegarde PITR
ou la réplication physique, ni incompatible.Dans le cadre d’une utilisation avec CloudNativePG, et pour pouvoir
tirer profit de la réplication physique mise en place automatiquement,
ce paramètre doit être positionné à minima à replica.
Par défaut, la valeur positionnée par l’opérateur est
logical. C’est une valeur adaptée pour des besoins
spécifiques de réplication logique. Dans 90%, voire même 95% des
configurations rencontrées lors de nos audits ou sur notre support, un
paramétrage à replica est suffisant. Le redescendre semble
être une bonne chose et permet notamment de réduire la volumétrie
globale des journaux de transactions, que ce soit localement ou sur le
système d’archivage. C’est la conclusion d’un article sur notre blog.
effective_io_concurrency a pour but d’indiquer le nombre
d’opérations disques possibles en même temps pour un client
(prefetch). Il n’a d’intérêt que si un nœud Bitmap
Scan a été choisi. Cela n’arrive qu’avec un certain nombre de
lignes à récupérer, et est favorisé par une valeur importante de
effective_cache_size et un peu de corrélation physique dans
la table.
La valeur d’effective_io_concurrency n’influe pas sur le
choix du plan, mais sa valeur peut notablement accélérer l’exécution du
Bitmap Heap Scan. Le temps de lecture peut fréquemment être
divisé par 3 ou plus.
Les valeurs possibles d’effective_io_concurrency vont de
0 à 1000. En principe, sur un disque magnétique seul, la valeur 1 ou 0
peut convenir. Avec du SSD, et encore plus du NVMe, il est possible de
monter à plusieurs centaines, étant donné la rapidité de ce type de
disque. Trouver la bonne valeur dépend de divers paramètres liés aux
caractéristiques exactes des disques et de leur paramétrage noyau. Le
read ahead du noyau intervient également. Le comportement de
PostgreSQL sur ce point change aussi avec les versions. De plus, à
partir d’un certain nombre de blocs, les I/O peuvent simplement
saturer.
effective_cache_sizerandom_page_costeffective_cache_size :
Il permet d’indiquer la taille totale du cache disque disponible pour
une requête. Pour le configurer, il faut prendre en compte le cache de
PostgreSQL (shared_buffers) et celui du système
d’exploitation. Ce n’est donc pas une mémoire que PostgreSQL va allouer,
mais plutôt une simple indication de ce qui est disponible. Le
planificateur se base sur ce paramètre pour évaluer les chances de
trouver des pages de données en mémoire. Une valeur plus importante aura
tendance à faire en sorte que le planificateur privilégie l’utilisation
des index, alors qu’une valeur plus petite aura l’effet inverse.
Généralement, on positionne effective_cache_size à ⅔ de
la mémoire dédiée à une instance PostgreSQL, voire ¾.
random_page_cost :
Le paramètre random_page_cost permet de faire
appréhender au planificateur le fait qu’une lecture aléatoire (autrement
dit avec déplacement de la tête de lecture) est autrement plus coûteuse
qu’une lecture séquentielle. Par défaut, random_page_cost
vaut 4, la lecture aléatoire d’un bloc donné a donc un coût 4 fois plus
important que s’il était lu au sein d’une lecture séquentielle avec de
nombreux autres blocs. Ce n’est qu’une estimation, qui n’a pas à voir
directement avec la vitesse des disques et prend aussi en compte l’effet
du cache, et elle est plutôt adaptée aux disques magnétiques.
Si random_page_cost est revu à la baisse, les parcours
aléatoires deviennent moins coûteux et, par conséquent, les parcours
d’index sont plus facilement sélectionnés. Avec des disques magnétiques
rapides, il ne faut pas hésiter à descendre un peu cette valeur (entre 2
et 3 par exemple). Si les données tiennent entièrement en cache ou sont
stockées sur des disques SSD directement attachés, on descend souvent à
1,1. Il ne faut pas descendre en-dessous de seq_page_cost
qui définit le coût pour une lecture séquentielle, et vaut 1,0. À
l’inverse, un stockage réseau sur des SSD mais avec une grosse latence
pourrait justifier de remonter la valeur de
random_page_cost.
max_parallel_workersmax_worker_processesLe nombre maximum de processus utilisables pour un nœud d’exécution
dépend de la valeur du paramètre
max_parallel_workers_per_gather (à 2 par défaut). Ils ne
seront lancés que si la requête le nécessite.
Si plusieurs processus veulent paralléliser l’exécution de leur
requête au même moment, le nombre total de workers parallèles
simultanés ne pourra pas dépasser la valeur du paramètre
max_parallel_workers.
Ce nombre ne peut lui-même dépasser la valeur du paramètre
max_worker_processes, nombre de processus
d’arrière-plan.
Les paramètres max_worker_processes et
max_parallel_workers sont positionnés à 32 par l’opérateur.
Si ces paramètres sont trop hauts par rapport aux ressources associées
au Pod, il y a un risque que les grosses requêtes saturent
les CPU au détriment des plus petites et d’autres processus.
Si vos instances ne sont pas appelées à être solidement
dimensionnées, c’est à dire que les request et
limits en CPU ne dépassent pas quelques unités, les réduire
serait une bonne chose car le risque est que les parallel
workers soient lancés, mais qu’en réalité il y ait une contention
sur le CPU du Node.
Après la phase de déploiement, il est indispensable de procéder à une
phase de configuration de vos Clusters. Cette configuration
doit se faire à plusieurs niveaux, notamment « système » avec les
ressources RAM / CPU qui influencent directement la QoS de vos
Pods mais également au niveau de PostgreSQL.
L’opérateur ne reconfigure pas vos instances selon les ressources
attribuées et, sur certains points, l’opérateur positionne des
paramètres qui peuvent ne pas être adaptés à vos besoins
(wal_level, max_parallel_processes). Le parti
pris suivi par les développeurs peut se résumer à « qui peut le plus,
peut le moins ». En positionnant ces paramètres assez «haut », ils font
en sorte que PostgreSQL puisse répondre à certaines demandes sans devoir
redémarrer les instances.
N’hésitez pas, c’est le moment !
La version en ligne des solutions de ces TP est disponible sur https://dali.bo/k2_solutions.
But : Configurer l’instance PostgreSQL.
Installer une instance PostgreSQL ne suffit pas. Il faut en plus la
configurer. Habituellement, la configuration se fait dans le fichier
postgresql.conf et nécessite soit un rechargement, soit un
redémarrage de l’instance selon le paramètre modifié. Nous allons voir
comment le faire sur notre instance postgresql-demo-1.
Créer le fichier
~/postgresql-config.yamlavec le contenu suivant :
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgresql-config
spec:
instances: 2
storage:
size: 5Gi
walStorage:
size: 5Gi
affinity:
enablePodAntiAffinity: true
topologyKey: kubernetes.io/hostname
podAntiAffinityType: preferred
resources:
requests:
memory: "1024Mi"
cpu: "1"
limits:
memory: "1024Mi"
cpu: "1"Créer ce
Clusteravec la commandekubectl apply -f.
Utiliser la documentation https://cloudnative-pg.io/docs/current/ pour trouver comment modifier des paramètres PostgreSQL.
Ajuster la configuration du
Clusteren ajoutant le paramètreshared_buffers. Positionnez-le à 25% de la mémoirerequest.
Dans une autre console, suivre les traces du
Clusteret de l’instance avec
kubectl cnpg logs cluster cluster-config -f | kubectl cnpg logs pretty.
Utiliser
kubectl apply -f ~/postgresql-config.yamlpour appliquer les modifications. Observer ce qui se passe sur leCluster.
Modifier le paramètre
work_memen le passant à 8 Mo et réappliquer la définition YAML aveckubectl apply -f ~/postgresql-config.yaml. Observer ce qui se passe sur leCluster.
Vérifier que la modification a bien été prise en compte en vous connectant à une des deux instances et en utilisant
show work_memdans le promptpsql.
Dans PostgreSQL, il est possible de modifier des paramètres avec
l’ordre SQL ALTER.
Se connecter à l’instance primaire.
Modifier le paramètre
transaction_timeoutde la basepostgresen le positionnant à 10 secondes.
Vérifier la modification avec la meta-commande
\drdsqui retourne les configurations spécifiques de chaque base de données
Modifier un second paramètre avec l’ordre SQL
ALTER SYSTEM SET. Par exemple, positionner le paramètreidle_in_transaction_session_timeoutà 10 minutes.
Retrouver la valeur du paramètre
allow_alter_system.
But : Configurer l’instance PostgreSQL.
Installer une instance PostgreSQL ne suffit pas. Il faut en plus la
configurer. Habituellement, la configuration se fait dans le fichier
postgresql.conf et nécessite soit un rechargement, soit un
redémarrage de l’instance selon le paramètre modifié. Nous allons voir
comment le faire sur notre instance postgresql-demo-1.
Créer le fichier
~/postgresql-config.yamlavec le contenu suivant :
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgresql-config
spec:
instances: 2
storage:
size: 5Gi
walStorage:
size: 5Gi
affinity:
enablePodAntiAffinity: true
topologyKey: kubernetes.io/hostname
podAntiAffinityType: preferred
resources:
requests:
memory: "1024Mi"
cpu: "1"
limits:
memory: "1024Mi"
cpu: "1"Créer ce
Clusteravec la commandekubectl apply -f.
kubectl apply -f ~/postgresql-config.yaml
cluster.postgresql.cnpg.io/postgresql-config created
Utiliser la documentation https://cloudnative-pg.io/docs/current/ pour trouver comment modifier des paramètres PostgreSQL.
La section The
postgresql section est celle qui nous intéresse. On va prendre
exemple sur l’extrait donné pour configurer notre Cluster.
Une section postgresql existe, dans laquelle une liste de
paramètres parameters peut être renseignée.
À noter, tous les paramètres doivent être passés sous forme de string, même si dans la configuration PostgreSQL, ils correspondrent à des entiers, ou flottants.
Ajuster la configuration du
Clusteren ajoutant le paramètreshared_buffers. Positionnez-le à 25% de la mémoirerequest.
La description YAML devient donc :
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgresql-config
spec:
instances: 2
storage:
size: 5Gi
walStorage:
size: 5Gi
affinity:
enablePodAntiAffinity: true
topologyKey: kubernetes.io/hostname
podAntiAffinityType: preferred
resources:
requests:
memory: "1024Mi"
cpu: "1"
limits:
memory: "1024Mi"
cpu: "1"
postgresql:
parameters:
shared_buffers: "256MB"Dans une autre console, suivre les traces du
Clusteret de l’instance avec
kubectl cnpg logs cluster postgresql-config -f | kubectl cnpg logs pretty.
Utiliser
kubectl apply -f ~/postgresql-config.yamlpour appliquer les modifications. Observer ce qui se passe sur leClusteravec les traces.
Lorsque la modification est appliquée, l’opérateur va procéder à
certaines opérations. Tout d’abord, il comprend que le paramètre modifié
nécessite un redémarrage des instances pour la prise en compte de la
nouvelle valeur. Effectivement, shared_buffers est un
paramètre qui touche à la mémoire partagée de PostgreSQL. Elle ne peut
être changée à chaud.
L’opérateur initie donc un redémarrage du Cluster (i.e
de toutes les instances). Dans les traces, on peut voir que c’est
d’abord l’instance postgresql-config-2 qui redémarre en
premier. C’est une instance secondaire.
2026-04-16T07:41:10.347 INFO postgresql-config-2 instance-manager Cluster has become unhealthy
2026-04-16T07:41:10.707 INFO postgresql-config-2 instance-manager Received termination signal
2026-04-16T07:41:10.707 INFO postgresql-config-2 instance-manager Requesting smart shutdown of the PostgreSQL instance
2026-04-16T07:41:10.712 INFO postgresql-config-2 pg_ctl pg_ctl: server is running (PID: 28)…
[…]
2026-04-16T07:41:10.791 INFO postgresql-config-2 postgres database system is shut down
Quelques secondes après, elle redémarre et accepte de nouveau des connexions en lecture seule.
[…]
2026-04-16T07:41:15.554 ERROR postgresql-config-2 postgres the database system is starting up
2026-04-16T07:41:15.665 INFO postgresql-config-2 postgres entering standby mode
2026-04-16T07:41:15.675 INFO postgresql-config-2 postgres redo starts at 0/4059E18
2026-04-16T07:41:16.018 INFO postgresql-config-2 postgres consistent recovery state reached at 0/6000000
2026-04-16T07:41:16.018 INFO postgresql-config-2 postgres database system is ready to accept read-only connections
2026-04-16T07:41:16.223 INFO postgresql-config-2 postgres started streaming WAL from primary at 0/6000000 on timeline 1
Si une autre instance secondaire existait, elle aurait été redémarrée. Ici, comme il n’y en a qu’une, il passe à l’instance primaire qui se voit être également arrêtée :
2026-04-16T07:41:23.704 INFO postgresql-config-1 instance-manager Received request for postgres
2026-04-16T07:41:23.705 INFO postgresql-config-1 instance-manager Requesting smart shutdown of the PostgreSQL instance
2026-04-16T07:41:23.744 INFO postgresql-config-1 pg_ctl pg_ctl: server is running (PID: 28)…
[…]
2026-04-16T07:41:23.869 INFO postgresql-config-1 instance-manager Shutting down instance
Puis redémarrée :
2026-04-16T07:41:24.316 INFO postgresql-config-1 instance-manager postmaster started
[…]
2026-04-16T07:41:24.403 INFO postgresql-config-1 postgres starting PostgreSQL 18.3 (Debian 18.3-1.pgdg13+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14…
2026-04-16T07:41:24.403 INFO postgresql-config-1 postgres listening on IPv4 address "0.0.0.0", port 5432
2026-04-16T07:41:24.403 INFO postgresql-config-1 postgres listening on IPv6 address "::", port 5432
2026-04-16T07:41:24.408 INFO postgresql-config-1 postgres listening on Unix socket "/controller/run/.s.PGSQL.5432"
2026-04-16T07:41:24.422 INFO postgresql-config-1 postgres database system was shut down at 2026-04-16 07:41:24 UTC
2026-04-16T07:41:24.436 INFO postgresql-config-1 postgres database system is ready to accept connections
Il faut donc comprendre que, par défaut, une modification d’un paramètre nécessitant un redémarrage déclenchera aussitôt le redémarrage. Attention donc au moment où vous apportez des modifications. Nous verrons par la suite comment avoir la main sur le moment du redémarrage avec la notion de stratégie de mise à jour.
Modifier le paramètre
work_memen le passant à 8 Mo et réappliquer la définition YAML aveckubectl apply -f ~/postgresql-config.yaml. Observer ce qui se passe sur leCluster.
tail postgresql-config.yaml
requests:
memory: "512Mi"
cpu: "1"
limits:
memory: "512Mi"
cpu: "1"
postgresql:
parameters:
shared_buffers: "256MB"
work_mem: "8MB"
kubectl apply -f ~/postgresql-config.yaml
cluster.postgresql.cnpg.io/postgresql-config configured
Là encore, les traces nous aident à comprendre ce qui se passe. Les deux instances sont rechargées à chaud.
2026-04-16T07:53:36.281 INFO postgresql-config-2 instance-manager Installed configuration file
2026-04-16T07:53:36.282 INFO postgresql-config-1 instance-manager Installed configuration file
2026-04-16T07:53:36.383 INFO postgresql-config-1 instance-manager reloading the instance
2026-04-16T07:53:36.384 INFO postgresql-config-1 instance-manager Requesting configuration reload
2026-04-16T07:53:36.386 INFO postgresql-config-1 pg_ctl server signaled
2026-04-16T07:53:36.386 INFO postgresql-config-1 postgres received SIGHUP, reloading configuration files
2026-04-16T07:53:36.388 INFO postgresql-config-1 postgres parameter "work_mem" changed to "8MB"
2026-04-16T07:53:36.388 INFO postgresql-config-1 postgres parameter "cnpg.config_sha256" changed to "44cf902cab017fd0aa7e0ed149e1d779778870c5283b1dcc4af61b992…
2026-04-16T07:53:36.389 INFO postgresql-config-2 instance-manager reloading the instance
2026-04-16T07:53:36.389 INFO postgresql-config-2 instance-manager Requesting configuration reload
2026-04-16T07:53:36.401 INFO postgresql-config-2 pg_ctl server signaled
2026-04-16T07:53:36.401 INFO postgresql-config-2 postgres received SIGHUP, reloading configuration files
2026-04-16T07:53:36.404 INFO postgresql-config-2 postgres parameter "work_mem" changed to "8MB"
2026-04-16T07:53:36.404 INFO postgresql-config-2 postgres parameter "cnpg.config_sha256" changed to "44cf902cab017fd0aa7e0ed149e1d779778870c5283b1dcc4af61b992…
Vérifier que la modification a bien été prise en compte en vous connectant à une des deux instances et en utilisant
show work_memdans le promptpsql.
kubectl cnpg psql postgresql-config -- -c 'show work_mem';
work_mem
----------
8MB
(1 row)
Certains paramètres PostgreSQL ne sont pas modifiables. C’est le parti pris des développeurs de CloudNativePG. La liste se trouve dans la documentation du projet.
Dans PostgreSQL, il est possible de modifier des paramètres avec
l’ordre SQL ALTER.
Se connecter à l’instance primaire.
Par exemple :
kubectl cnpg psql postgresql-config
psql (18.3 (Debian 18.3-1.pgdg13+1))
Type "help" for help.
postgres=#
Modifier le paramètre
transaction_timeoutde la basepostgresen le positionnant à 10 secondes.
ALTER DATABASE permet de modifier des paramètres spécifiquement pour la base ciblée. Tous les paramètres ne peuvent pas être modifiés de cette manière.
Vérifier la modification avec la meta-commande
\drdsqui retourne les configurations spécifiques de chaque base de données
postgres=# \drds
List of settings
Role | Database | Settings
------+----------+-------------------------
| postgres | transaction_timeout=10s
(1 row)
La configuration est bien modifiée. Désormais toutes les transactions qui s’effectueront sur cette basse profiteront de ce paramétrage.
Modifier un second paramètre avec l’ordre SQL
ALTER SYSTEM SET. Par exemple, positionner le paramètreidle_in_transaction_session_timeoutà 10 minutes.
postgres=# ALTER SYSTEM SET idle_in_transaction_session_timeout = '10m';
ERROR: ALTER SYSTEM is not allowed in this environmentLe message d’erreur est très explicite mais pour le moins étonnant.
Il faut savoir que depuis la version 17 de PostgreSQL, le paramètre
allow_alter_system existe et permet, à la discrétion des
administrateurs, d’autoriser ou non les ordres
ALTER SYSTEM.
Retrouver la valeur du paramètre
allow_alter_system.
postgres=# show allow_alter_system ;
allow_alter_system
--------------------
off
(1 row)
Par défaut, ce paramètre est à on. Vous l’aurez compris,
CloudNativePG passe ce paramètre à off pour interdire
les ordres ALTER SYSTEM et forcer la modification des
paramètres PostgreSQL à se faire de manière déclarative.
Il reste néanmoins possible de modifier ce paramètre dans la YAML du
Cluster avec le champ
.spec.postgresql.enableAlterSystem. Même si cela est
possible, il est préférable de se forcer à utiliser la définition YAML
du Cluster pour modifier la configuration globale des
instances, notamment :
Cluster ;ClustersLe présent module va vous présenter les différentes méthodes pour sauvegarder nos instances PostgreSQL. Si la mise en place de sauvegardes est essentielle, elles doivent suivre une politique de sauvegarde bien définie. Il en sera question en fin de module.
VolumeSnapshotCloudNativePG propose et supporte de nouveaux mécanismes pour sauvegarder vos instances. Avant de découvrir en quoi elles consistent, prenons le temps de rappeler quels sont les différents types de sauvegarde dans PostgreSQL.
Connaître les différences entre sauvegardes logiques et physiques est essentiel, tout comme la différence entre sauvegarde à chaud et à froid.
Nous verrons ensuite comment CloudNativePG met en œuvre, ou non, ces types de sauvegardes et quels sont les pré-requis.
COMMITLa journalisation, sous PostgreSQL, permet de garantir l’intégrité des fichiers, et la durabilité des opérations :
COMMIT) est
écrite physiquement, et un arrêt brutal immédiatement ne va pas la faire
disparaître (excepté la perte de tous les disques de stockage et
réplicas, bien sûr).Pour cela, le mécanisme est relativement simple : toute modification affectant un fichier sera d’abord écrite dans le journal. Les modifications affectant les vrais fichiers de données ne sont écrites qu’en mémoire, dans les shared buffers.
Les écritures dans le journal, bien que synchrones, sont relativement
performantes, car elles sont séquentielles (moins de déplacement de
têtes pour les disques magnétiques). Il n’y a que le fichier en cours à
synchroniser à chaque COMMIT.
Ce n’est généralement que bien plus tard que les modifications seront
écrites de façon asynchrone, soit par un processus recherchant un buffer
libre, soit par le background writer, soit par le
checkpointer. Ce dernier processus sait étaler la charge en
écriture dans les fichiers de données sur plusieurs minutes, et dans
l’idéal il est seul à s’en charger.
Il existe plusieurs configurations et astuces pour arbitrer entre le
niveau de durabilité des données exigé et les contraintes de
performances : secondaire en réplication synchrone pour une sécurité
maximale, désactivation partielle du mécanisme d’enregistrement
synchrone des journaux dans une session, tables de travail non
journalisées (unlogged), regroupement des insertions pour
réduire l’impact des COMMIT… Aucune ne remet en cause
l’intégrité définie dans les modèle de données.
Essentiellement :
pg_wal/ : journaux de transactions
archive_status00000002 00000142 000000FFpg_xact/ : état des transactionsRappelons que les journaux de transaction sont des fichiers de 16 Mo
par défaut, stockés dans PGDATA/pg_wal, dont les noms
comportent le numéro de timeline, un numéro de journal de 4 Go
et un numéro de segment, en hexadécimal.
$ ls -l
total 2359320
…
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 00000002000001420000007C
-rw------- 1 postgres postgres 33554432 Mar 26 16:28 00000002000001420000007D
…
-rw------- 1 postgres postgres 33554432 Mar 26 16:25 000000020000014300000023
-rw------- 1 postgres postgres 33554432 Mar 26 16:25 000000020000014300000024
drwx------ 2 postgres postgres 16384 Mar 26 16:28 archive_statusLe sous-répertoire archive_status est lié à
l’archivage.
D’autres plus petits répertoires comme pg_xact, qui
contient les statuts des transactions passées, ou
pg_commit_ts, pg_multixact,
pg_serial, pg_snapshots,
pg_subtrans ou encore pg_twophase sont
également impliqués.
Tous ces répertoires sont critiques, gérés par PostgreSQL, et ne doivent pas être modifiés !
Les journaux de transactions sont nécessaires pour les sauvegardes de types PITR.
Choisir celle qui convient à votre besoin.
Il existe deux types de sauvegardes dans PostgreSQL : les sauvegardes dites logiques et celles dites physiques. Chacune d’entre elle a ses spécificités et chacune d’entre elles répond à un besoin spécifique.
Ces deux types de sauvegardes sont, la plupart du temps, complémentaires.
La sauvegarde logique nécessite que le serveur soit en cours d’exécution. Un outil se connecte à la base et récupère la déclaration des différents objets ainsi que les données des tables.
La technique alors utilisée permet de s’assurer de la cohérence des
données : lors de la sauvegarde, l’outil ne voit pas les modifications
faites par les autres utilisateurs. Pour cela, quand il se connecte à la
base à sauvegarder, il commence une transaction pour que sa vision des
enregistrements de l’ensemble des tables soit cohérente. Cela empêche le
recyclage des enregistrements par VACUUM pour les
enregistrements dont il pourrait avoir besoin. Par conséquent, que la
sauvegarde dure 10 minutes ou 10 heures, le résultat correspondra au
contenu de la base telle qu’elle était au début de la transaction.
Des verrous sont placés sur chaque table, mais leur niveau est très
faible (Access Share). Il visent juste à éviter la suppression
des tables pendant la sauvegarde, ou la modification de leur structure.
Les opérations habituelles sont toutes permises en lecture ou écriture,
sauf quand elles réclament un verrou très invasif, comme
TRUNCATE, VACUUM FULL ou certains
LOCK TABLE. Les verrous ne sont relâchés qu’à la fin de la
sauvegarde.
Par ailleurs, pour assurer une vision cohérente de la base durant toute la durée de son export, cette transaction de longue durée est de type REPEATABLE READ, et non de type READ COMMITED, celui utilisé par défaut.
pg_dumppg_dumpallIl existe deux outils pour la sauvegarde logique dans la distribution officielle de PostgreSQL :
pg_dump, pour sauvegarder uniquement des bases de
l’instance, complètement ou partiellement, avec de nombreuses options et
formats ;pg_dumpall pour sauvegarder toutes les définitions et
données des bases en un seul script SQL, ainsi que les objets globaux
(rôles, tablespaces).Pour la restauration :
psql exécute les ordres SQL contenus dans des
dumps (sauvegardes) au format texte ;pg_restore traite uniquement les dumps au
format binaire, et produit le SQL qui permet de restaurer les
données.Il est important de bien comprendre que ces outils n’échappent pas au fonctionnement client-serveur de PostgreSQL. Ils « dialoguent » avec l’instance PostgreSQL uniquement en SQL, aussi bien pour la sauvegarde que la restauration.
Comme ce type d’outil n’a besoin que d’une connexion standard à la
base de données, il peut se connecter en local comme à distance. Cela
implique qu’il doive aussi respecter les autorisations de connexion
configurées dans le fichier pg_hba.conf.
L’export ne concerne que les données des utilisateurs : les tables
systèmes ne sont jamais concernées, il n’y a pas de risque de les
écraser lors d’un import. En effet, les schémas systèmes
pg_catalog et information_schema et leurs
objets sont gérés uniquement par PostgreSQL. Vous n’êtes d’ailleurs pas
censé modifier leur contenu, ni y ajouter ou y effacer quoi que ce
soit !
La configuration du serveur (fichiers postgresql.conf,
pg_hba.conf…) n’est jamais incluse et doit être sauvegardée
à part. Un export logique ne concerne que des données et structures.
Les extensions ne posent pas de problème non plus : la sauvegarde contiendra une mention de l’extension, et les données des éventuelles tables gérées par cette extension. Mais à la restauration, il faudra que les binaires de l’extension soient installés sur le système cible.
Toutes les données d’une instance PostgreSQL se trouvent dans des fichiers. Donc sauvegarder les fichiers permet de sauvegarder une instance. Cependant, cela ne peut pas se faire aussi simplement que ça.
Lorsque PostgreSQL est en cours d’exécution, il modifie certains fichiers du fait de l’activité des utilisateurs ou des processus (internes ou non) de maintenances diverses.
Pour garantir la cohérence des données, la seule solution serait de sauvegarder tous les fichiers lorsque l’instance est arrêtée (sauvegarde à froid), ce qui n’est concrètement envisageable que dans très peu de cas. Alors comment faire ?
La solution est d’effectuer des sauvegardes physiques à chaud et en continu en se reposant sur le mécanisme de PITR. PITR est l’acronyme de Point In Time Recovery, autrement dit restauration à un point dans le temps.
C’est une sauvegarde à chaud et surtout en continu. Là où une
sauvegarde logique du type pg_dump se fait par exemple une
fois toutes les 24h, la sauvegarde PITR se fait en continu grâce à
l’archivage des journaux de transactions. De ce fait, ce type de
sauvegarde diminue très fortement la fenêtre de perte de données.
Bien qu’elle se fasse à chaud, la sauvegarde reste cohérente grâce aux journaux de transactions qui contiennent toutes les modifications opérées sur les fichiers pendant la durée de la sauvegarde.
Ce type de sauvegarde est possible avec des outils spécifiques de
l’écosystème PostgreSQL comme pg_basebackup
(contrib), Barman ou encore pgBackRest.
La cohérence est garantie grâce aux WAL
Backup et
ScheduledBackupspec.backup d’un objet
ClusterL’opérateur sait gérer pour vous des sauvegardes dites physiques.
C’est le seul type de sauvegarde que vous allez pouvoir déclencher via
l’opérateur. Autrement dit, il existe des nouvelles ressources dans
Kubernetes, appelées Backup et ScheduledBackup
qui correspondent à une sauvegarde physique d’un Cluster
PostgreSQL.
Les sauvegardes logiques (faites avec pg_dump ou
pg_dumpall par exemple) pourront toujours se faire avec ces
outils dès lors que votre instance est accessible. À date, ce type de
sauvegarde n’est pas déclenchable déclarativement via CloudNativePG.
Plusieurs méthodes de sauvegarde physiques existent. Quelle que soit
la méthode, la configuration se fait dans l’objet Cluster
correspondant à vos instances, par exemple, quel plugin doit
être utilisé.
Certains paramètres peuvent également être renseignés dans les objets
Backup ou ScheduledBackup. C’est ce que nous
allons découvrir par la suite.
Aussi, vous pourrez effectuer ces sauvegardes à partir des instances secondaires pour décharger les instances primaires, et ce, quelle que soit la méthode choisie. Des points d’attention sont à avoir notamment lors de l’exécution de sauvegarde à froid.
La première méthode consiste à effectuer les sauvegardes sur un stockage objet de type S3, Azure Blob Storage ou Google Cloud Storage.
Cette méthode se repose sur un plugin de sauvegarde indépendant qui doit être installé dans le cluster Kubernetes. À l’heure actuelle, le seul plugin proposé est le plugin Barman Cloud. D’autres initiatives on vu le jour, notamment au sein de Dalibo, pour proposer pgBackRest comme alternative de plugin.
Le plugin est généralement utilisé pour l’archivage des
journaux de transactions (isWALArchiver). Couplé avec
l’archivage des journaux de transactions, vous obtenez une sauvegarde
PITR fonctionnelle pour votre instance.
ObjectStore
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
name: scaleway-store
spec:
configuration:
destinationPath: "s3://<bucket>/<folder>/"
endpointURL: "https://s3.<region>.scw.cloud"
s3Credentials:
accessKeyId:
name: scaleway-api-secret
key: ACCESS_KEY_ID
secretAccessKey:
name: scaleway-api-secret
key: ACCESS_SECRET_KEY
region:
name: scaleway-api-secret
key: ACCESS_REGIONCette ressource est fournie par Barman Cloud Plugin. Elle représente un emplacement de stockage objet. Trois providers sont supportés : Amazon S3, Microsoft Azure Blob Storage ou encore Google Cloud Storage. Des services compatibles S3 peuvent être également utilisés comme MinIO ou encore Scaleway Object Storage.
D’un fournisseur de stockage à un autre, les champs
destinationPath et endpointURL peuvent
changer. En plus de la connaissance du point d’entrée de votre solution
de stockage, vous devez renseigner les informations de connexion dans la
section s3Credentials.
Ces informations là doivent être enregistrées dans un objet
Secret (objet Kubernetes). Dans l’exemple, le nom de ce
Secret est scaleway-api-secret. Voici ce à
quoi il pourrait ressembler.
apiVersion: v1
kind: Secret
metadata:
name: scaleway-api-secret
type: Opaque
data:
ACCESS_KEY_ID: bWEgY2xlIGQgYWNjZXMK
ACCESS_REGION: ZnItcGFy
ACCESS_SECRET_KEY: bW9uIHNlY3JldCBiaWVuIGdhcmRlCg==Les valeurs de chacun des trois champs data doivent être encodées en BASE64.
spec.backup.method: volumeSnapshotStorageClassContainer Storage InterfaceLa seconde méthode utilise quant à elle un mécanisme propre à l’API
de Kubernetes, le Volume Snapshot. C’est une fonctionnalité
qui doit être supportée par la Storage Class (et donc
in fine par le CSI) avec laquelle les volumes de
votre instance ont été créés.
Cette méthode va créer un objet Volume Snapshot dans
votre cluster Kubernetes qui contiendra un instantané du
Persistent Volume ciblé. Cette sauvegarde sera donc locale
à votre système de stockage et non plus envoyée sur un stockage objet.
Un snapshot sera créé pour chaque volume de votre instance
(storage et walStorage).
Des mécanismes plus complexes comme les sauvegardes incrémentales ou
différentielles sont possibles si la Storage Class le
permet.
Couplé avec l’archivage des journaux de transactions, vous obtenez une sauvegarde PITR fonctionnelle pour votre instance. Les journaux de transaction sont quant à eux stockés sur un stockage objet et nécessite toujours l’utilisation du plugin d’archivage.
Custom Resource DefinitionVoici un exemple d’un objet Backup qui, une fois créé
dans le cluster Kubernetes, déclenchera une sauvegarde physique
du Cluster nommé postgresql. Si le
champ methode n’est pas mentionné, la sauvegarde se fera
sur un stockage objet.
Il existe d’autres paramètres de configuration, comme :
target : qui indique à partir de quelle instance doit
être faite la sauvegarde ;
prefer-standby : pour demander à la faire depuis un
secondaire ;primary : pour demander à la faire depuis le
primaire.method : permet de définir par quel moyen la sauvegarde
physique doit être faite ;
volumeSnapshot : en se basant sur la fonctionnalité de
Volume Snapshot. Votre CSI doit supporter
cette fonctionnalité pour utiliser cette méthode ;plugin : si la sauvegarde se fait à partir d’un plugin
autre que vous aurez déployé au préalable. Fonctionnalité encore en
test.Pour les sauvegardes qui utilisent la méthode plugin,
les informations sur l’emplacement de stockage seront reprises de la
configuration de l’objet spécifique du plugin. Selon le
plugin utilisé, il devrait exister deux dossiers, un contenant
les journaux archivés, un contenant les sauvegardes.
Pour les sauvegardes qui utilisent la méthode
volumeSnapshot, la configuration sera récupérée depuis
spec.backup.volumeSnapshot.
Par défaut, les sauvegardes se font à chaud. Seule la méthode par
Volume Snapshot permet de faire des sauvegardes à froid
(i.e instance arrêtée).
Custom Resource DefinitionBackup créée à chaque exécutionIl existe une deuxième ressource appelée ScheduledBackup
qui, comme son nom l’indique, permet de déclencher une sauvegarde
régulièrement. L’exemple donné correspond au déclenchement d’une
sauvegarde quotidienne à 20h00:00.
Quelques paramètres de configuration sont spécifiques à cet objet, comme :
schedule : définit le moment où la sauvegarde sera
déclenchée. Il y a bien six arguments, correspondant aux secondes,
minutes, heures, jours du mois, mois et jour de la semaine ;backupOwnerReference : indique à quelle ressource sera
rattachée cette sauvegarde ;
self : au ScheduledBackup qui a déclenché
cette sauvegarde ;cluster : au Cluster mentionné ;none: à aucune ressource.D’autres paramètres comme method ou target
peuvent être renseignés dans un ScheduledBackup.
Le paramètre backupOwnerReference a un impact sur la
manière dont sont conservés les objets Kubernetes Backup.
Dans l’exemple ci-dessus, si l’objet ScheduledBackup
masauvegardequotidienne est supprimé, tous les objets
Backup qui auraient été créés par la sauvegarde planifiée
seront supprimés. Dans le cas où backupOwnerReference est
configuré à cluster, les objets Backup seront
supprimés si l’objet Cluster est supprimé. À
none ils seront tout le temps conservés. Notez bien qu’il
s’agit bien des objets Kubernetes. Les sauvegardes qui se trouvent sur
le stockage S3 par exemple, ne seront pas supprimées.
Il est tout à fait possible de combiner ces deux types de déclenchements (manuel ou régulier) de sauvegardes.
archive_mode à on)archive_command :
/controller/manager wal-archive …isWALArchiver)L’archivage des journaux de transaction est fortement conseillé lorsque qu’il est question de sauvegarde physique car il permet notamment la mise en place de sauvegardes dites PITR (voir notre module I2).
CloudNativePG supporte ce mécanisme par défaut. L’archivage des
journaux se fait via l’intermédiaire d’un plugin. Le choix du
plugin reste libre. L’opérateur va même automatiquement activé
ce mécanisme pour que vous n’ayez pas à le faire plus tard
(archive_mode à on).
Si aucun plugin de sauvegarde n’est mentionné pour archiver les
journaux, aucun archivage qui ne sera fait. Le processus
archiver sera bien démarré et exécutera la commande de
archive_commande, mais renverra toujours un code retour
valide à cette demande d’archivage.
L’archive ne peut se faire que sur un stockage de type objet (type S3, Google Cloud Storage, Azure Blob Storage ou MinIO). C’est le cas quelque soit la méthode de sauvegarde utilisée.
Il est nécessaire de créer une ressource ObjectStore au
sein du cluster Kubernetes qui sera réutilisée plus tard par
les objets Cluster PostgreSQL.
Pour configurer l’archivage sur un Cluster, il est
demandé de renseigner spec.plugins avec le nom du
plugin, s’il a la capacité d’archiver des journaux
(isWALArchiver) et l’ObjectStore sur lequel
seront envoyés les journaux. Par exemple :
spec.bootstrap.recovery
barmanObjectStorevolumeSnapshotsLa première chose à noter est qu’une restauration d’une instance avec CloudNativePG se fera toujours par la création d’une nouvelle instance. Autrement dit, il n’est pas possible de faire une restauration sur une instance déjà déployée. La restauration in-place n’est pas possible.
CloudNativePG se base sur une sauvegarde physique pour créer cette
nouvelle instance. Le paramètre de configuration
spec.bootstrap.recovery permet de configurer cette
restauration. Lorsque ce paramétre est utilisée dans le fichier
YAML, CloudNativePG comprend qu’il doit créer
(bootstrap) une instance à partir d’une sauvegarde.
Comme l’archive_command, le paramètre
restore_command est déjà positionné par l’opérateur et
utilise l’instance-manager. Cette fois-ci c’est la commande
wal-restore qui est utilisée.
/controller/manager wal-restore --log-destination /controller/log/postgres.json %f %p
La source utilisée pour la restauration peut être de nature
différente selon la méthode (method) de la sauvegarde.
bootstrap.recovery.source associé à
externalClusters. La partie barmanObjectStore
contiendra alors toutes les informations du stockage objets où se trouve
la sauvegarde. Par exemple : spec:
[…]
bootstrap:
recovery:
source: clusterBackup
externalClusters:
- name: clusterBackup
barmanObjectStore:
[…]Volume Snapshot, vous devrez
utiliserbootstrap.recovery.volumeSnapshots.storage. Dans le cas où
le snapshot a été fait depuis une instance ayant deux espaces de
stockage (storage et walStorage) vous devez
également le renseigner.Quelques éléments supplémentaires sont à prendre en compte. Tout
d’abord, CloudNativePG part du principe que la base
app existe dans la sauvegarde et qu’elle a pour
propriétaire app. Si vous utilisez d’autres noms, vous
devez le renseigner dans la partie recovery. Aussi, si vous
souhaitez conserver un mot de passe en particulier pour le rôle par
défaut, vous devez renseigner le Secret à utiliser.
Autrement, CloudNativePG se chargera d’en générer un aléatoirement.
Par défaut, la sauvegarde se fera en rejouant l’intégralité des
journaux de transactions disponibles sur la dernière
timeline. Il est possible de faire une restauration de type
Point In Time Recovery en renseignant le champs
bootstrap.recovery.recoveryTarget. Par exemple :
D’autres cibles de restauration existent, comme :
targetTime : l’horodatage auquel vous souhaitez
restaurer votre instance ;targetXID : l’ID de transaction jusqu’auquel vous
souhaitez restaurer votre instance ;targetName : le nom du point de restauration que vous
aurez créé au préalable avec
pg_create_restore_point() ;targetLSN : La position dans les journaux de
transactions à laquelle arrêter la restauration ;targetImmediate : Indique si la restauration doit
s’arrêter dès qu’un point de consistance est atteint.N’hésitez pas, c’est le moment !
La version en ligne des solutions de ces TP est disponible sur https://dali.bo/k3_solutions.
But : Mettre en place une sauvegarde PITR sur un stockage S3 (archivage et sauvegarde complète).
Comme vous le savez certainement, il existe le concept de sauvegarde physique PITR comme mécanisme de sauvegarde d’une instance. Pour mettre en place cela, il est d’abord nécessaire de faire une sauvegarde physique de l’arborescence de l’instance. Ceci peut être fait à chaud. Le second élément essentiel est l’archivage des journaux de transactions (WAL) qui seront rejoués après une restauration pour rétablir un état cohérent.
En déployant une instance avec CloudNativePG, la seule solution de
sauvegarde PITR utilisable est Barman Cloud. Très connu
dans l’écosystème PostgreSQL, cet outil nous permet de faire la
sauvegarde physique et l’archivage des WALs. Les commandes passées pour
la mettre en place le seront de manière automatique mais une
configuration doit être rajoutée dans le fichier YAML de notre
Cluster.
La page de documentation du projet Barman Cloud CNPG-I plugin peut vous aider.
La mise en place d’une solution de sauvegarde nécessite, depuis la version 1.26, l’installation d’un plugin dédié aux sauvegardes. Le projet CloudNativePG met à disposition le plugin Barman Cloud CNPG-I.
Nous allons donc l’installer sur notre cluster Kubernetes. Aussi,
l’outil cert-manager doit être présent dans le cluster
Kubernetes. Il est utilisé pour la génération de certificats pour la
communication entre l’opérateur et le plugin de sauvegarde.
Installer
cert-manageravec la commande suivante.
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.20.2/cert-manager.yaml
Patienter quelques minutes, le temps que
cert-managers’installe, puis installer le plugin avec la commande suivante.
kubectl apply -f https://github.com/cloudnative-pg/plugin-barman-cloud/releases/download/v0.14.0/manifest.yaml
Vérifier que le plugin est bien installé dans le
Namespacecnpg-system.
Créer le fichier
~/s3-creds.yamlavec le contenu suivant.
Les paramètres ACCESS_* doivent contenir l’information
encodée en BASE64. Si vous utilisez la commande echo,
n’oubliez pas l’option -n qui empêche la prise en compte du
saut de ligne. Les informations vous seront données par le
formateur.
---
apiVersion: v1
kind: Secret
metadata:
name: s3-creds
type: Opaque
data:
ACCESS_KEY_ID: CHANGEME
ACCESS_REGION: ZnItcGFy
ACCESS_SECRET_KEY: CHANGEMECréer le
Secretdans votre cluster Kubernetes avec la commandekubectl apply -f ~/s3-creds.yaml.
Pour cette partie du TP, nous allons créer une autre instance PostgreSQL (
postgresql-with-backup-demo), donc un objet de typeClusteret un nouveau nom.
Créer le fichier
~/postgresql-with-backup-demo.yamlavec le contenu suivant.
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgresql-with-backup-demo
spec:
imageName: ghcr.io/cloudnative-pg/postgresql:17.5-standard-bookworm
instances: 1
storage:
size: 5Gi
walStorage:
size: 5Gi
postgresql:
parameters:
shared_buffers: '256MB'
max_connections: '10'
work_mem: "8MB"
archive_timeout: "20min"
resources:
requests:
memory: "256Mi"
cpu: "0.5"
limits:
memory: "512Mi"
cpu: "1"
plugins:
- name: barman-cloud.cloudnative-pg.io
isWALArchiver: true
parameters:
barmanObjectName: objectstore-demo La partie backup indique quel plugin de
sauvegarde utilisé, ici Barman Cloud. Il est également
indiqué que c’est ce plugin qui sera en charge de l’archivage
des journaux de transactions grâce au paramètre
isWALArchiver à true.
Créer le fichier
~/objectstore-demo.yamlpour créer l’ObjectStorequi sera utilisé par le nouveauCluster. N’oubliez pas de modifier CHANGEME dans ledestinationPathen gardant bien le dernier/(mettre quelque chose de reconnaissable et unique).
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
name: objectstore-demo
spec:
configuration:
destinationPath: "s3://demo-cnpg/CHANGEME/"
endpointURL: "https://s3.fr-par.scw.cloud"
s3Credentials:
accessKeyId:
name: s3-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: s3-creds
key: ACCESS_SECRET_KEY
region:
name: s3-creds
key: ACCESS_REGION
wal:
compression: gzipCréer l’
ObjectStoreavec la commande :
kubectl apply -f ~/objectstore-demo.yaml
Créer le nouveau
ClusterPostgreSQL avec la commande :
kubectl apply -f ~/postgresql-with-backup-demo.yaml
Vérifier que l’archivage se passe correctement directement dans la vue
pg_stat_archiver.
Nous demander de vous montrer, sur l’interface Scaleway, le
Bucketet le dossier que vous avez utilisé.
C’est un super point de départ. Mais pour le moment, il n’est pas possible de faire quelconque restauration comme il nous manque une sauvegarde complète de l’instance.
Créer le fichier
~/letsbackup.yamlavec le contenu suivant :
apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata:
name: first-backup
spec:
cluster:
name: postgresql-with-backup-demo
method: plugin
pluginConfiguration:
name: barman-cloud.cloudnative-pg.ioCréer cette ressource avec
kubectl.
Vérifier le statut de l’objet
Backup.
Chercher dans les traces du
Podune preuve que la sauvegarde complète s’est bien déroulée.
Nous demander de vous montrer, sur l’interface Scaleway, le
Bucketet le dossier que vous avez utilisé.
Se connecter à l’instance. Créer une table et insérer quelques données.
Forcer la création d’un nouveau journal de transactions avec
SELECT pg_switch_wal();.
But : Restaurer notre instance depuis la sauvegarde PITR existante.
Les restaurations se font obligatoirement dans une nouvelle instance
PostgreSQL. Le principe de restauration in-place n’est donc pas
possible. Attention donc si vous souhaitez conserver le nom du
Cluster vous devrez détruire le précédent
Cluster qui porterait ce nom.
Maintenant qu’une instance est déployée et qu’une sauvegarde a été faite, attardons-nous sur les manières qui existent pour restaurer une instance.
Aussi, c’est l’occasion de faire un petit rappel ! N’oubliez pas de tester vos procédures de restauration fréquemment !
Simuler un crash. Détruire l’instance
postgresql-with-backup-demo(Nous sommes bien évidemment ici dans un exercice de destruction maîtrisé par des professionnels).
Créer un nouveau fichier
~/postgresql-restored-demo.yamlavec le contenu suivant. L’idée est de créer une nouvelle instancepostgresql-restored-demoet d’indiquer avec la sectionbootstrapqu’elle doit démarrer à partir d’une sauvegarde.
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgresql-restored-demo
spec:
imageName: ghcr.io/cloudnative-pg/postgresql:17.5-standard-bookworm
instances: 1
storage:
size: 5Gi
walStorage:
size: 5Gi
postgresql:
parameters:
shared_buffers: '256MB'
max_connections: '10'
work_mem: '8MB'
archive_timeout: '20min'
resources:
requests:
memory: "256Mi"
cpu: "0.5"
limits:
memory: "512Mi"
cpu: "1"
bootstrap:
recovery:
source: source
externalClusters:
- name: source
plugin:
name: barman-cloud.cloudnative-pg.io
parameters:
barmanObjectName: objectstore-demo
serverName: postgresql-with-backup-demo
plugins:
- name: barman-cloud.cloudnative-pg.io
isWALArchiver: true
parameters:
barmanObjectName: objectstore-demo # réutilisation du bucket S3Créer votre nouvelle instance avec
kubectl apply -f ~/postgresql-restored-demo.yaml.
Lorsque l’instance est prête, s’y connecter et vérifier que les données s’y trouvent bien.
Supprimer les instances
postgresql-demoqui ne vont plus nous servir par la suite.
But : Découvrir des fonctionnalités plus complexes.
Il est possible de programmer des sauvegardes régulières avec la
ressource ScheduledBackup.
Voici un exemple de définition qui permet de déclencher une
sauvegarde appelée backup-every-day tous les jours à 16h00
pour le cluster postgresql-restored-demo :
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: backup-every-day
spec:
schedule: "0 42 18 * * *"
backupOwnerReference: self
cluster:
name: postgresql-restored-demo
method: plugin
pluginConfiguration:
name: barman-cloud.cloudnative-pg.ioAttention, l’option schedule prend bien six paramètres
(le premier étant les secondes), contrairement au CronJob
dans Kubernetes ou aux lignes de /etc/crontab qui n’en
prennent que cinq.
Créer le fichier
~/backup-every-day.yamlavec le contenu ci-dessus en modifiant l’heure d’exécution pour que la sauvegarde s’exécute dans 5 à 10 minutes.
Créer l’objet
ScheduledBackupaveckubectl.
Suivez les traces de l’opérateur avec
kubectl logs -n cnpg-system cnpg-controller-manager-6b9f78f594-hd5qq | grep backup | jq. Vous devriez voir le déclenchement de la sauvegarde.
But : Mettre en place une sauvegarde PITR sur un stockage S3 (archivage et sauvegarde complète).
Comme vous le savez certainement, il existe le concept de sauvegarde physique PITR comme mécanisme de sauvegarde d’une instance. Pour mettre en place cela, il est d’abord nécessaire de faire une sauvegarde physique de l’arborescence de l’instance. Ceci peut être fait à chaud. Le second élément essentiel est l’archivage des journaux de transactions (WAL) qui seront rejoués après une restauration pour rétablir un état cohérent.
En déployant une instance avec CloudNativePG, la seule solution de
sauvegarde PITR utilisable est Barman Cloud. Très connu
dans l’écosystème PostgreSQL, cet outil nous permet de faire la
sauvegarde physique et l’archivage des WALs. Les commandes passées pour
la mettre en place le seront de manière automatique mais une
configuration doit être rajoutée dans le fichier YAML de définition.
La mise en place d’une solution de sauvegarde nécessite, depuis la version 1.26, l’installation d’un plugin dédié aux sauvegardes. Le projet CloudNativePG met à disposition le plugin Barman Cloud CNPG-I.
Nous allons donc l’installer sur notre cluster Kubernetes. Aussi,
l’outil cert-manager doit être présent dans le cluster
Kubernetes. Il est utilisé pour la génération de certificats pour la
communication entre l’opérateur et le plugin de sauvegarde.
Installer
cert-manageravec la commande suivante.
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.19.2/cert-manager.yaml
Patienter quelques minutes, le temps que
cert-managers’installe, puis installer le plugin avec la commande suivante.
kubectl apply -f https://github.com/cloudnative-pg/plugin-barman-cloud/releases/download/v0.14.0/manifest.yaml
Vérifier que le plugin est bien installé dans le
Namespacecnpg-system.
kubectl get pod -n cnpg-system
NAME READY STATUS RESTARTS AGE
barman-cloud-6858cdc47f-gr2bh 1/1 Running 0 3m32s
cnpg-controller-manager-7b7fcf5cf6-8rmj9 1/1 Running 0 9m18s
Créer le fichier
~/s3-creds.yamlavec le contenu suivant.
Les paramètres ACCESS_* doivent contenir l’information
encodée en BASE64. Si vous utilisez la commande echo,
n’oubliez l’option -n qui empêche la prise en compte du
saut de ligne. Les informations vous seront données par le
formateur.
---
apiVersion: v1
kind: Secret
metadata:
name: s3-creds
type: Opaque
data:
ACCESS_KEY_ID: CHANGEME
ACCESS_REGION: ZnItcGFy
ACCESS_SECRET_KEY: CHANGEMECréer le
Secretdans votre cluster Kubernetes avec la commandekubectl apply -f ~/s3-creds.yaml.
kubectl apply -f s3-creds.yaml
secret/s3-creds created
Ce Secret contient les informations de la clé API qui
permettra de s’authentifier au Bucket S3 et de déposer les
WAL et les sauvegardes.
Pour cette partie du TP, nous allons créer une autre instance PostgreSQL (
postgresql-with-backup-demo), donc un objet de typeClusteret un nouveau nom.
Créer le fichier
~/postgresql-with-backup-demo.yamlavec le contenu suivant.
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgresql-with-backup-demo
spec:
imageName: ghcr.io/cloudnative-pg/postgresql:17.5-standard-bookworm
instances: 1
storage:
size: 5Gi
walStorage:
size: 5Gi
postgresql:
parameters:
shared_buffers: '256MB'
max_connections: '10'
work_mem: "8MB"
archive_timeout: "20min"
resources:
requests:
memory: "256Mi"
cpu: "0.5"
limits:
memory: "512Mi"
cpu: "1"
plugins:
- name: barman-cloud.cloudnative-pg.io
isWALArchiver: true
parameters:
barmanObjectName: objectstore-demo La partie backup indique quel plugin de
sauvegarde utilisé, ici Barman Cloud. Il est également
indiqué que c’est ce plugin qui sera en charge de l’archivage
des journaux de transactions grâce au paramètre
isWALArchiver à true.
Créer le fichier
~/objectstore-demo.yamlpour créer l’ObjectStorequi sera utilisé par le nouveauCluster. N’oubliez pas de modifier CHANGEME dans ledestinationPathen gardant bien le dernier/(mettre quelque chose de reconnaissable et unique).
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
name: objectstore-demo
spec:
configuration:
destinationPath: "s3://demo-cnpg/CHANGEME/"
endpointURL: "https://s3.fr-par.scw.cloud"
s3Credentials:
accessKeyId:
name: s3-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: s3-creds
key: ACCESS_SECRET_KEY
region:
name: s3-creds
key: ACCESS_REGION
wal:
compression: gzipLa configuration des paramètres endpointURL et
destinationPath devra être adaptée selon votre fournisseur
de stockage S3. Les paramètres ci-dessus fonctionnent bien avec
Scaleway. Faites vraiment attention, vous risquerez de perdre beaucoup
de temps… vraiment :-).
Créer l’
ObjectStoreavec la commande :
kubectl apply -f ~/objectstore-demo.yaml
Créer le nouveau
ClusterPostgreSQL avec la commande :
kubectl apply -f ~/postgresql-with-backup-demo.yaml
Vérifier que l’archivage se passe correctement directement dans la vue
pg_stat_archiver.
kubectl cnpg psql postgresql-with-backup-demo
postgres=# \x
Expanded display is on.
postgres=# SELECT * FROM pg_stat_archiver ;
-[ RECORD 1 ]------+------------------------------
archived_count | 2
last_archived_wal | 000000010000000000000002
last_archived_time | 2025-12-12 10:22:11.071834+00
failed_count | 0
last_failed_wal |
last_failed_time |
stats_reset | 2025-12-12 10:20:09.349145+00
Nous demander de vous montrer, sur l’interface Scaleway, le
Bucketet le dossier que vous avez utilisé.
Pour ce TP, nous sommes passés par la solution
Object Storage de Scaleway compatible S3. Voici un exemple
de ce qu’il sera créé dans le Bucket.
Le dossier pierrick est bien créé dans le
Bucket.
On y retrouve dedans un dossier avec le nom du cluster PostgreSQL…
…qui contient lui-même un dossier wals.
Les journaux (WAL) sont enregistrés dans des dossiers qui reprennent
la timeline de l’instance.
Et enfin, dans ce dernier dossier, se trouvent les journaux de transaction compressés.
C’est un super point de départ. Mais pour le moment, il n’est pas possible de faire quelconque restauration comme il nous manque une sauvegarde complète de l’instance.
Créer le fichier
~/letsbackup.yamlavec le contenu suivant :
apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata:
name: first-backup
spec:
cluster:
name: postgresql-with-backup-demo
method: plugin
pluginConfiguration:
name: barman-cloud.cloudnative-pg.ioIl faut donner un nom à cet objet Backup et le nom du
cluster PostgreSQL que l’on souhaite sauvegarder ainsi qu’avec quelle
méthode de sauvegarde cela va être fait, ici via le plugin
Barman Cloud.
Créer cette ressource avec avec
kubectl.
kubectl apply -f ~/letsbackup.yaml
backup.postgresql.cnpg.io/first-backup created
Vérifier le statut de l’objet
Backup:
kubectl get backup
NAME AGE CLUSTER METHOD PHASE ERROR
first-backup 14s postgresql-with-backup-demo plugin started
Chercher dans les traces du
Podune preuve que la sauvegarde complète s’est bien déroulée.
kubectl logs postgresql-with-backup-demo-1 | grep completed | jq
{
"level": "info",
"ts": "2025-12-12T10:29:49.602829931Z",
"msg": "Backup completed",
"pluginConfiguration": {
"name": "barman-cloud.cloudnative-pg.io"
},
"backupName": "first-backup",
"backupNamespace": "default",
"logging_pod": "postgresql-with-backup-demo-1"
}Au niveau de l’interface Scaleway, un nouveau dossier
base est apparu à côté de wals.
Il contient toutes les sauvegardes faites jusqu’à présent.
La sauvegarde physique se trouve dans ce dossier et comporte un
fichier d’informations et une archive tar.
Incroyable ! Nous avons une sauvegarde et un archivage des WALs qui semblent se dérouler correctement. Mais, qu’est ce qu’il se cache derrière cela ?
La première chose que nous pouvons chercher à savoir par exemple, est
quel outil est utilisé pour archiver les journaux. Le paramètre
archive_command nous donne un début de réponse.
kubectl exec -it postgresql-with-backup-demo-1 -- psql -c "SHOW archive_command"
archive_command
------------------------------------------------------------------------------------
/controller/manager wal-archive --log-destination /controller/log/postgres.json %p
(1 row)
Un outil appelé manager présent dans le conteneur est
utilisé avec l’option wal-archive suivie de plusieurs
paramètres. %p est un placeholders qui permet
d’indiquer le WAL courant.
Se connecter à l’instance. Créer une table et insérer quelques données.
kubectl exec -it postgresql-with-backup-demo-1 -- psql
ou, via le plugin :
kubectl cnpg psql postgresql-with-backup-demo
Ne pas oublier d’exécuter l’ordre CHECKPOINT qui
permettra de forcer la synchronisation des données sur disque et la
création d’un point de cohérence sans attendre l’expiration de
checkpoint_timeout.
Forcer la création d’un nouveau journal de transactions avec
SELECT pg_switch_wal();.
postgres=# SELECT pg_switch_wal();
pg_switch_wal
---------------
1/FFCFCAA8
(1 row)
Il devrait apparaitre dans votre Bucket S3.
But : Restaurer notre instance depuis la sauvegarde PITR existante.
Les restaurations se font obligatoirement dans une nouvelle instance
PostgreSQL. Le principe de restauration in-place n’est donc pas
possible. Attention donc si vous souhaitez conserver le nom du
Cluster vous devrez détruire le précédent
Cluster qui porterait ce nom.
Maintenant qu’une instance est déployée et qu’une sauvegarde a été faite, attardons-nous sur les manières qui existent pour restaurer une instance.
Aussi, c’est l’occasion de faire un petit rappel ! N’oubliez pas de tester vos procédures de restauration fréquemment !
Simuler un crash. Détruire l’instance
postgresql-with-backup-demo(Nous sommes bien évidemment ici dans un exercice de destruction maîtrisé par des professionnels).
kubectl delete -f ~/postgresql-with-backup-demo.yaml
Créer un nouveau fichier
~/postgresql-restored-demo.yamlavec le contenu suivant. L’idée est de créer une nouvelle instancepostgresql-restored-demoet d’indiquer avec la sectionbootstrapqu’elle doit démarrer à partir d’une sauvegarde.
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgresql-restored-demo
spec:
imageName: ghcr.io/cloudnative-pg/postgresql:17.5-standard-bookworm
instances: 1
storage:
size: 5Gi
walStorage:
size: 5Gi
postgresql:
parameters:
shared_buffers: '256MB'
max_connections: '10'
work_mem: '8MB'
archive_timeout: '20min'
resources:
requests:
memory: "256Mi"
cpu: "0.5"
limits:
memory: "512Mi"
cpu: "1"
bootstrap:
recovery:
source: source
externalClusters:
- name: source
plugin:
name: barman-cloud.cloudnative-pg.io
parameters:
barmanObjectName: objectstore-demo
serverName: postgresql-with-backup-demo
plugins:
- name: barman-cloud.cloudnative-pg.io
isWALArchiver: true
parameters:
barmanObjectName: objectstore-demo # réutilisation du bucket S3L’emplacement de la sauvegarde est indiqué dans l’attribut
source de la section bootstrap.recovery. Il
fait référence à un externalClusters qui contient les
informations de l’ObjectStore utilisé et présent dans le
cluster Kuberetes.
Créer votre nouvelle instance avec
kubectl apply -f ~/postgresql-restored-demo.yaml.
kubectl apply -f ~/postgresql-restored-demo.yaml
Lorsque l’instance est prête, s’y connecter et vérifier que les données s’y trouvent bien.
kubectl exec -it postgresql-restored-demo-1 -c postgres -- psql -c "select count(*) from t1;"
count
-------
100
(1 row)
La méthode que nous venons de suivre, suppose que vous ayez accès à
l’objet ObjectStore créé dans le cluster
Kubernetes. Mais qu’en est-il si c’est tout le cluster
Kubernetes qui est en panne et doit être recréé ?
Dans ce cas-là, l’objet ObjectStore n’existe plus. Soit
vous le recréez, soit vous renseignez directement les informations de
connexion au stockage S3.
Du côté du Bucket S3, un nouveau dossier est
automatiquement créé avec le nom du nouvel objet Cluster.
Cela est dû à la partie plugins dans laquelle nous avons
indiqué de réutiliser l’ObjectStore précédent.
Dans cet exemple, la restauration s’est faite sur le même
cluster Kubernetes. Dans le cas où vous devez la faire
ailleurs, n’oubliez pas de recréer le Secret qui contient
les informations de l’API Key nécessaire à l’accès au stockage S3.
Supprimer les instances
postgresql-demoqui ne vont plus nous servir par la suite.
kubectl delete -f postgresql-demo.yaml
Il ne doit rester que le cluster PostgreSQL
postgresql-restored-demo.
kubectl get pod
NAME READY STATUS RESTARTS AGE
postgresql-restored-demo-1 1/1 Running 2 (26m ago) 2d15h
But : Découvrir des fonctionnalités plus complexes.
Il est possible de programmer des sauvegardes régulières avec la
ressource ScheduledBackup.
Voici un exemple de définition qui permet de déclencher une
sauvegarde appelée backup-every-day tous les jours à 16h00
pour le cluster postgresql-restored-demo :
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: backup-every-day
spec:
schedule: "0 42 18 * * *"
backupOwnerReference: self
cluster:
name: postgresql-restored-demo
method: plugin
pluginConfiguration:
name: barman-cloud.cloudnative-pg.ioAttention, l’option schedule prend bien six paramètres
(le premier étant les secondes), contrairement au CronJob
dans Kubernetes ou aux lignes de /etc/crontab qui en prenne
que cinq.
Créer le fichier
~/backup-every-day.yamlavec le contenu ci-dessus en modifiant l’heure d’exécution pour que la sauvegarde s’exécute dans 5 à 10 minutes.
Créer l’objet
ScheduledBackupaveckubectl.
kubectl apply -f ~/backup-every-day.yaml
scheduledbackup.postgresql.cnpg.io/backup-every-day created
Suivez les traces de l’opérateur avec
kubectl logs -n cnpg-system cnpg-controller-manager-6b9f78f594-hd5qq | grep backup | jq. Vous devriez voir le déclenchement de la sauvegarde.
kubectl logs -n cnpg-system cnpg-controller-manager-6b9f78f594-hd5qq | grep backup | jq
{
"level": "info",
"ts": "2025-12-15T18:42:00.095050921Z",
"msg": "Starting backup",
"controller": "backup",
"controllerGroup": "postgresql.cnpg.io",
"controllerKind": "Backup",
"Backup": {
"name": "backup-every-day-20251215184200",
"namespace": "default"
},
"namespace": "default",
"name": "backup-every-day-20251215184200",
"reconcileID": "65490b4b-415b-41d2-be86-b1bab33de9b4",
"cluster": "postgresql-restored-demo",
"pod": "postgresql-restored-demo-1"
}Vous verrez alors la sauvegarde sur votre emplacement de stockage S3.
Photo de l’incendie du datacenter OVHcloud à Strasbourg du 10 mars 2021 fournie gracieusement par l’ITBR67.
Cet incendie a provoqué de nombreux arrêts et pertes de données dans toute la France et ailleurs.
Afin d’assurer la sécurité des données, il est nécessaire de faire des sauvegardes régulières.
Ces sauvegardes vont servir, en cas de problème, à restaurer les bases de données dans un état le plus proche possible du moment où le problème est survenu.
Cependant, le jour où une restauration sera nécessaire, il est possible que la personne qui a mis en place les sauvegardes ne soit pas présente. C’est pour cela qu’il est essentiel d’écrire et de maintenir un document qui indique la mise en place de la sauvegarde et qui détaille comment restaurer une sauvegarde.
En effet, suivant les besoins, les outils pour sauvegarder, le contenu de la sauvegarde, sa fréquence ne seront pas les mêmes.
Par exemple, il n’est pas toujours nécessaire de tout sauvegarder. Une base de données peut contenir des données de travail, temporaires et/ou faciles à reconstruire, stockées dans des tables standards. Il est également possible d’avoir une base dédiée pour stocker ce genre d’objets. Pour diminuer le temps de sauvegarde (et du coup de restauration), il est possible de sauvegarder partiellement son serveur pour ne conserver que les données importantes.
La fréquence peut aussi varier. Un utilisateur peut disposer d’un serveur PostgreSQL pour un entrepôt de données, serveur qu’il n’alimente qu’une fois par semaine. Dans ce cas, il est inutile de sauvegarder tous les jours. Une sauvegarde après chaque alimentation (donc chaque semaine) est suffisante. En fait, il faut déterminer la fréquence de sauvegarde des données selon :
Le support de sauvegarde est lui aussi très important. Il est possible de sauvegarder les données sur un disque réseau (à travers SMB/CIFS ou NFS), sur des disques locaux dédiés, sur des bandes ou tout autre support adapté. Dans tous les cas, il est fortement déconseillé de stocker les sauvegardes sur les disques utilisés par la base de données.
Ce document doit aussi indiquer comment effectuer la restauration. Si la sauvegarde est composée de plusieurs fichiers, l’ordre de restauration des fichiers peut être essentiel. De plus, savoir où se trouvent les sauvegardes permet de gagner un temps important, qui évitera une immobilisation trop longue.
De même, vérifier la restauration des sauvegardes de façon régulière est une précaution très utile.
L’objectif essentiel de la sauvegarde est la sécurisation des données. Autrement dit, l’utilisateur cherche à se protéger d’une panne matérielle ou d’une erreur humaine (un utilisateur qui supprimerait des données essentielles). La sauvegarde permet de restaurer les données perdues. Mais ce n’est pas le seul objectif d’une sauvegarde.
Une sauvegarde peut aussi servir à dupliquer une base de données sur un serveur de développement, de test ou de préproduction. Elle permet aussi d’archiver des tables. Cela se voit par exemple dans le cadre des tables partitionnées, où l’archivage de la table la plus ancienne permet ensuite sa suppression de la base pour gagner en espace disque.
Un autre cas d’utilisation de la sauvegarde est la mise à jour majeure de versions PostgreSQL. Il s’agit de la solution historique de mise à jour (export/import). Historique, mais pas obsolète.
pg_basebackupÀ ces différents objectifs vont correspondre différentes approches de la sauvegarde.
La sauvegarde logique permet de créer un fichier texte de commandes SQL ou un fichier binaire contenant le schéma et les données de la base de données, à chaud (sans arrêt de l’activité de la base). C’est une méthode très sûre, mais souvent trop longue.
La sauvegarde au niveau système de fichiers permet de conserver une image cohérente de l’intégralité des répertoires de données. Pour garantir cette cohérence, elle n’est simplement possible qu’à froid (base arrêtée), ce qui est rarement possible de nos jours.
La sauvegarde à chaud des fichiers est cependant possible avec
quelques précautions, notamment l’archivage des journaux. PostgreSQL
fournit un outil avec quelques limitations mais très simple pour cela :
pg_basebackup.
Une autre alternative est le snapshot de baie (avec les mêmes précautions, ou avec des outils comme Veeam ou Commvault…), ce qui est très intéressant avec les grosses bases de données.
Les méthodes précédentes ne donnent qu’une image à un moment donné de la base. Les sauvegardes PITR sont une évolution de la sauvegarde physique, où la restauration peut se faire à n’importe quel moment du passé.
Suivant les prérequis et les limitations de chaque méthode, il est fort possible qu’une seule de ces solutions soit utilisable. Par exemple :
Rien n’interdit d’utiliser plusieurs méthodes à la fois pour différents besoins.
La politique de sauvegarde découle du :
Le RPO et RTO sont deux concepts déterminants dans le choix des politiques de sauvegardes.
La RPO (ou PDMA) est la perte de données maximale admissible, ou quantité de données que l’on peut tolérer de perdre lors d’un sinistre majeur, souvent exprimée en heures ou minutes.
Pour un système mis à jour épisodiquement ou avec des données non critiques, ou facilement récupérables, le RPO peut être important (par exemple une journée). Peuvent alors s’envisager des solutions comme :
Dans beaucoup de cas, la perte de données admissible est très faible (heures, quelques minutes), voire nulle. Il faudra s’orienter vers des solutions de type :
La RTO (ou DMIA) est la durée maximale d’interruption du service.
Dans beaucoup de cas, les utilisateurs peuvent tolérer une indisponibilité de plusieurs heures, voire jours. La durée de reprise du service n’est alors pas critique, on peut utiliser des solutions simples comme :
Si elle est plus courte, le service doit très vite remonter. Cela nécessite des procédures avec un minimum d’acteurs et de manipulation :
Plus le besoin en RTO/RPO sera court, plus les solutions seront complexes à mettre en œuvre — et chères. Inversement, pour des données non critiques, un RTO/RPO long permet d’utiliser des solutions simples et peu coûteuses.
Les moyens nécessaires pour la mise en place, le maintien et l’intégration de la sauvegarde dans le SI ont un coût financier qui apporte une contrainte supplémentaire sur la politique de sauvegarde.
Du point de vue matériel, il faut disposer principalement d’un volume de stockage qui peut devenir conséquent. Cela dépend de la volumétrie à sauvegarder, il faut considérer les besoins suivants :
Avec une rétention d’une sauvegarde unique, il est bon de prévoir 3 fois la taille de la base ou de l’instance. Pour une faible volumétrie, cela ne pose pas de problèmes, mais quand la volumétrie devient de l’ordre du téraoctet, les coûts augmentent significativement.
L’autre poste de coût est la mise en place de la sauvegarde. Une équipe de DBA peut tout à fait décider de créer ses propres scripts de sauvegarde et restauration, pour diverses raisons, notamment :
Enfin, le dernier poste de coût est la maintenance, à la fois des scripts et par le test régulier de la restauration.
Comme pour n’importe quelle procédure, il est impératif de documenter la politique de sauvegarde, les procédures de sauvegarde et de restauration ainsi que les scripts.
Au strict minimum, la documentation doit permettre à un DBA non familier de l’environnement de comprendre la sauvegarde, retrouver les fichiers et restaurer les données le cas échéant, le plus rapidement possible et sans laisser de doute. En effet, en cas d’avarie nécessitant une restauration, le service aux utilisateurs finaux est généralement coupé, ce qui génère un climat de pression propice aux erreurs qui ne fait qu’empirer la situation.
L’idéal est de réviser la documentation régulièrement en accompagnant ces révisions de tests de restauration : avoir un ordre de grandeur de la durée d’une restauration est primordial. On demandera toujours au DBA qui restaure une base ou une instance combien de temps cela va prendre.
L’un des points les plus importants à prendre en compte est l’endroit où sont stockés les fichiers des sauvegardes. Laisser les sauvegardes sur la même machine n’est pas suffisant : si une défaillance matérielle se produit, les sauvegardes peuvent être perdues en même temps que l’instance sauvegardée, rendant ainsi la restauration impossible.
Il est conseillé de suivre au moins la règle 3-2-1 : 3 exemplaires des données sur 2 supports physiques différents au moins, dont 1 hors site.
Les données elles-mêmes sont le premier exemplaire.
Les deux copies doivent se trouver sur des supports physiques différents (et de préférence sur un autre serveur) pour parer à la destruction du support original (notamment une perte de disques durs). La première copie peut être à proximité pour faciliter une restauration.
Des disques en RAID ne sont pas une sauvegarde ! Ils peuvent parer à
la défaillance d’un disque, pas à une fausse manipulation
(rm -rf / ou TRUNCATE malheureux). La perte de
la carte contrôleur peut entraîner la perte de toute la grappe.
Un conseil courant est de choisir des disques de séries différentes pour éviter des défaillances simultanées.
Le troisième exemplaire doit se trouver à un autre endroit, pour parer aux scénarios les plus catastrophiques (cambriolage, incendie…). Selon la criticité, le délai nécessaire pour remonter rapidement un système fonctionnel, et le budget, ce troisième exemplaire peut être une copie manuelle sur un disque externe stocké dans un coffre, ou une infrastructure répliquée complète avec sa copie des sauvegardes à l’autre bout de la ville, voire du pays.
Pour limiter la consommation d’espace disque des copies multiples, les durées de rétention peuvent différer. La dernière sauvegarde en date peut résider sur la machine, les cinq dernières sur un serveur distant, et une autre sur des bandes déposées dans un site sécurisé tous les mois.
La règle 3-2-1 a été déclinée au fil du temps en plusieurs variantes. La plus connue, la variante 3-2-1-1-0, ajoute deux règles.
Une des copies doit être « immuable », c’est-à-dire non modifiable en cas d’attaque délibérée. C’est le cas de certains services « S3 Object Lock », non modifiables au moins durant un certain temps. C’est aussi le cas d’une sauvegarde air gapped, par exemple sur un disque dur stocké dans un coffre.
Plus important encore : aucune erreur de restauration n’apparaît dans les différentes copies. Cela suppose que vous avez bien vérifié que vous avez les procédures pour restaurer vos données et qu’elles sont complètes et cohérentes.
De la même manière qu’un RAID n’est pas une sauvegarde, un serveur
répliqué n’est pas une sauvegarde. Certes, il permet de parer à
certaines pannes, mais il réplique aussi les erreurs de manipulation
(DROP TABLE).
Stocker vos données dans le cloud n’est pas une solution miracle : un datacenter peut brûler entièrement. Il faut donc bien vérifier que votre hébergeur sauvegarde les données sur un autre site. Ce n’est pas forcément suffisant : en 2024, Google Cloud a effacé par erreur tout le cloud privé (deux zones) de l’assureur australien UniSuper, qui n’a pu remonter son infrastructure que grâce à des sauvegardes hors de ce cloud.
Ne vous reposez pas sur votre fournisseur. N’hésitez pas à faire vous-même une copie de vos données, sur un autre site ou dans un deuxième cloud totalement indépendant.
Il ne faut pas non plus sous-estimer le risque d’une attaque (piratage, malveillance ou ransomware…), qui s’en prendra aussi à toute sauvegarde accessible en ligne. En 2023 l’hébergeur danois CloudNordic a perdu toutes les données de ses clients à cause d’un ransomware.
Une copie physique hors ligne est donc chaudement recommandée pour les données les plus critiques.
La sauvegarde ne concerne pas uniquement les données. Il est également fortement conseillé de sauvegarder les fichiers de configuration du serveur et les scripts d’administration. Ces scripts sont notamment ceux de maintenance pour diverses opérations (techniques ou fonctionnelles), ou ceux de sauvegarde eux-même.
Le paramétrage est parfois géré par l’outil d’industrialisation (Ansible, Docker Compose…), par un outil externe (Patroni…), ou versionné quelque part. L’idéal est copier les fichiers avec les sauvegardes. On peut parfois inclure ces scripts dans une sauvegarde au niveau système, vu que ce sont de simples fichiers.
Les principaux fichiers de PostgreSQL à prendre en compte sont :
postgresql.conf, postgresql.auto.conf,
pg_hba.conf, pg_ident.conf. Ils sont parfois
dans le PGDATA avec les données, parfois pas. Ils ont des clauses
d’inclusion pour utiliser d’autres fichiers. Cette liste n’est en aucun
cas exhaustive.
Il s’agit donc de recenser l’ensemble des fichiers et scripts nécessaires si l’on désirait recréer le serveur depuis zéro. Il faut prévoir le pire des cas, où l’infrastructure a disparu aussi.
Même si les sauvegardes se déroulent correctement, il est indispensable de tester la restauration, et de vérifier qu’elle se déroule sans erreur. Une erreur de copie lors de l’externalisation peut, par exemple, rendre la sauvegarde inutilisable.
Just that backup tapes are seen to move, or backup scripts are run for a lengthy period of time, should not be construed as verifying that data backups are properly being performed.
Que l’on voit bouger les bandes de sauvegardes, ou que les scripts de sauvegarde fonctionnent pendant une longue période, ne doit pas être interprété comme une validation que les sauvegardes sont faites.
(NASA.gov, Lessons learned #1781, https://llis.nasa.gov/lesson/1781)
Le test de restauration permet de vérifier l’ensemble de la procédure :
La rejouer régulièrement vous évitera de découvrir la procédure dans l’urgence, le stress, voire la panique, alors que vous serez harcelé par de nombreux utilisateurs ou clients bloqués. Ce stress peut vous faire faire des erreurs.
Le test permet aussi de connaître la durée de restauration, une information toujours utile. Si elle est trop importante, il faudra peut-être revoir ou optimiser la méthode de sauvegarde.
Pour se faire la main, restaurer régulièrement les bases de test ou de préproduction à partir des sauvegardes de la production est une bonne idée. Il est conseillé que développeurs et testeurs aient des données aussi proches que possible de la production à disposition.
Nous rencontrons régulièrement en clientèle des scripts de sauvegarde qui ne fonctionnent pas, et jamais testés. Vous trouverez sur Internet de nombreuses histoires de catastrophes qui auraient été évitées par un simple test. Entre mille autres :
Voir aussi :
L’écosystème de PostgreSQL offre tout le nécessaire pour effectuer des sauvegardes fiables. Le plan de sauvegarde doit être fait sérieusement, et les sauvegardes testées. Cela a un coût, mais un désastre détruisant toutes vos données sera incommensurablement plus ruineux.
Superviser une instance PostgreSQL consiste à superviser l’instance elle-même, mais aussi le système d’exploitation et le matériel. Ces deux derniers sont importants pour connaître la charge système, l’utilisation des disques ou du réseau, qui pourraient expliquer des lenteurs au niveau des bases.
PostgreSQL propose lui aussi des informations qu’il est important de surveiller pour détecter des problèmes au niveau de son utilisation. L’opérateur CloudNativePG met à disposition un certain nombre de métriques et d’outils pour les exploiter (Exporter Prometheus, _dashboard). D’autres outils complémentaires peuvent vous aider.
La supervision occasionnelle permet de répondre à un problème ponctuel là ou la surveillance automatique vous permet de suivre l’évolution de votre instance et d’être alertés, selon les outils que vous utilisez.
Ce module a pour but de montrer les outils existant dans l’éco-système PostgreSQL et CloudNativePG. Certains points d’attention seront détaillés. La supervision d’éléments système, comme la RAM ou le CPU seront également évoqués.
La supervision, et notamment le suivi des traces, nous aide en cas de problème et de recherche de solution (troubleshooting). C’est le dernier thème qui sera abordé dans ce module.
PostgreSQL embarque de nombreuses tables systèmes contenant des métriques intéressantes à surveiller. PostgreSQL fournit également des vues combinant des informations puisées dans différentes tables systèmes, ce qui simplifie le suivi de l’activité de l’instance. C’est ce que l’on peut regrouper sous le terme de “statistiques d’activité”.
Ces statistiques peuvent être récupérées avec de simples requêtes SQL. Par exemple, la requête suivante permet de récupérer le nombre de connexions par base :
Ou encore celle-ci qui permet de savoir la taille des bases.
Ces vues sont là et ces informations sont facilement récupérables. Conserver ces informations pour voir leur évolution dans le temps est essentiel. Rien n’existe dans PostgreSQL pour les historiser. Un outil supplémentaire est nécessaire.
CloudNativePG rend ces informations là facilement accessibles dans un contexte Kubernetes, avec notamment un Exporter Prometheus, nous allons le voir. Malheureusement, l’opérateur ne propose, lui non plus, aucun mécanisme de conservation. C’est à vous et à vos équipes DevOps de déployer et maintenir cette autre solution.
Parcourons ensemble quelques vues importantes de PostgreSQL.
track_activities = on (défaut)pg_stat_activity affiche
pg_stat_activity est une des vues les plus utilisées et
est souvent le point de départ d’une recherche. Elle donne la liste des
processus en cours sur l’instance, en incluant entre autres :
pid) ;application_name ;SELECT datname, pid, usename, application_name,
backend_start, state, backend_type, query
FROM pg_stat_activity \gx-[ RECORD 1 ]----+-------------------------------------------------------------
datname | ¤
pid | 26378
usename | ¤
application_name |
backend_start | 2019-10-24 18:25:28.236776+02
state | ¤
backend_type | autovacuum launcher
query |
-[ RECORD 2 ]----+-------------------------------------------------------------
datname | ¤
pid | 26380
usename | postgres
application_name |
backend_start | 2019-10-24 18:25:28.238157+02
state | ¤
backend_type | logical replication launcher
query |
-[ RECORD 3 ]----+-------------------------------------------------------------
datname | pgbench
pid | 22324
usename | test_performance
application_name | pgbench
backend_start | 2019-10-28 10:26:51.167611+01
state | active
backend_type | client backend
query | UPDATE pgbench_accounts SET abalance = abalance + -3810 WHERE…
-[ RECORD 4 ]----+-------------------------------------------------------------
datname | postgres
pid | 22429
usename | postgres
application_name | psql
backend_start | 2019-10-28 10:27:09.599426+01
state | active
backend_type | client backend
query | select datname, pid, usename, application_name, backend_start…
-[ RECORD 5 ]----+-------------------------------------------------------------
datname | pgbench
pid | 22325
usename | test_performance
application_name | pgbench
backend_start | 2019-10-28 10:26:51.172585+01
state | active
backend_type | client backend
query | UPDATE pgbench_accounts SET abalance = abalance + 4360 WHERE…
-[ RECORD 6 ]----+-------------------------------------------------------------
datname | pgbench
pid | 22326
usename | test_performance
application_name | pgbench
backend_start | 2019-10-28 10:26:51.178514+01
state | active
backend_type | client backend
query | UPDATE pgbench_accounts SET abalance = abalance + 2865 WHERE…
-[ RECORD 7 ]----+-------------------------------------------------------------
datname | ¤
pid | 26376
usename | ¤
application_name |
backend_start | 2019-10-24 18:25:28.235574+02
state | ¤
backend_type | background writer
query |
-[ RECORD 8 ]----+-------------------------------------------------------------
datname | ¤
pid | 26375
usename | ¤
application_name |
backend_start | 2019-10-24 18:25:28.235064+02
state | ¤
backend_type | checkpointer
query |
-[ RECORD 9 ]----+-------------------------------------------------------------
datname | ¤
pid | 26377
usename | ¤
application_name |
backend_start | 2019-10-24 18:25:28.236239+02
state | ¤
backend_type | walwriter
query |Les textes des requêtes sont tronqués à 1024 caractères : c’est un
problème courant. Il est conseillé de monter le paramètre
track_activity_query_size à plusieurs kilooctets.
Cette vue fournit aussi les wait events, qui indiquent ce
qu’une session est en train d’attendre. Cela peut être très divers et
inclut la levée d’un verrou sur un objet, celle d’un verrou interne, la
fin d’une entrée-sortie… L’absence de wait event indique que la
requête s’exécute. À noter qu’une session avec un wait event
peut rester en statut active.
Les détails sur les champs wait_event_type (type
d’événement en attente) et wait_event (nom de l’événement
en attente) sont disponibles dans le tableau des événements
d’attente. de la documentation.
À partir de PostgreSQL 17, la vue pg_wait_events peut
être directement jointe à pg_stat_activity, et son champ
description évite d’aller voir la documentation :
SELECT datname, application_name, pid,
wait_event_type, wait_event, query, w.description
FROM pg_stat_activity a
LEFT OUTER JOIN pg_wait_events w
ON (a.wait_event_type = w.type AND a.wait_event = w.name)
WHERE backend_type='client backend'
AND wait_event IS NOT NULL
ORDER BY wait_event DESC LIMIT 4 \gx-[ RECORD 1 ]----+-------------------------------------------------------------
datname | pgbench_20000_hdd
application_name | pgbench
pid | 786146
wait_event_type | LWLock
wait_event | WALWrite
query | UPDATE pgbench_accounts SET abalance = abalance + 4055 WHERE…
description | Waiting for WAL buffers to be written to disk
-[ RECORD 2 ]----+-------------------------------------------------------------
datname | pgbench_20000_hdd
application_name | pgbench
pid | 786190
wait_event_type | IO
wait_event | WalSync
query | UPDATE pgbench_accounts SET abalance = abalance + -1859 WHERE…
description | Waiting for a WAL file to reach durable storage
-[ RECORD 3 ]----+-------------------------------------------------------------
datname | pgbench_20000_hdd
application_name | pgbench
pid | 786145
wait_event_type | IO
wait_event | DataFileRead
query | UPDATE pgbench_accounts SET abalance = abalance + 3553 WHERE…
description | Waiting for a read from a relation data file
-[ RECORD 4 ]----+-------------------------------------------------------------
datname | pgbench_20000_hdd
application_name | pgbench
pid | 786143
wait_event_type | IO
wait_event | DataFileRead
query | UPDATE pgbench_accounts SET abalance = abalance + 1929 WHERE…
description | Waiting for a read from a relation data fileLe processus de la ligne 2 attend une synchronisation sur disque du journal de transaction (WAL), et les deux suivants une lecture d’un fichier de données.
Pour entrer dans le détail des champs liés aux connexions :
backend_type est le type de processus : on filtrera
généralement sur client backend, mais on y trouvera aussi
des processus de tâche de fond comme checkpointer,
walwriter, autovacuum launcher et autres
processus de PostgreSQL, ou encore des workers lancés par des
extensions ;datname est le nom de la base à laquelle la session est
connectée, et datid est son identifiant (OID) ;pid est le processus du backend, c’est-à-dire
du processus PostgreSQL chargé de discuter avec le client, qui durera le
temps de la session (sauf parallélisation) ;usename est le nom de l’utilisateur connecté, et
usesysid est son OID dans pg_roles ;application_name est un nom facultatif, et il est
recommandé que l’application cliente le renseigne autant que possible
avec SET application_name TO 'nom_outil_client' ;client_addr est l’adresse IP du client connecté
(NULL si connexion sur socket Unix), et
client_hostname est le nom associé à cette IP, renseigné
uniquement si log_hostname a été passé à on
(cela peut ralentir les connexions à cause de la résolution DNS) ;client_port est le numéro de port sur lequel le client
est connecté, toujours s’il s’agit d’une connexion IP.Une requête parallélisée occupe plusieurs processus, et apparaîtra
sur plusieurs lignes de pid différents. Le champ
leader_pid indique le processus principal. Les autres
processus disparaîtront dès la requête terminée.
Pour les champs liés aux durées de session, transactions et requêtes :
backend_start est le timestamp de l’établissement de la
session ;xact_start est le timestamp de début de la
transaction ;query_start est le timestamp de début de la requête en
cours, ou de la dernière requête exécutée ;status vaut soit active, soit
idle (la session ne fait rien) soit
idle in transaction (en attente pendant une transaction) ;
backend_xid est l’identifiant de la transaction en
cours, s’il y en a une ;backend_xmin est l’horizon des transactions visibles,
et dépend aussi des autres transactions en cours. Rappelons qu’une session durablement en statut
idle in transaction bloque le fonctionnement de
l’autovacuum car backend_xmin est bloqué. Cela peut mener à
des tables fragmentées et du gaspillage de place disque.
pg_stat_activity contient un champ
query_id, c’est-à-dire un identifiant de requête normalisée
(dépouillée des valeurs de paramètres). Il faut que le paramètre
compute_query_id soit à on ou
auto (le défaut, et alors une extension peut l’activer). Ce
champ est utile pour retrouver une requête dans la vue de l’extension
pg_stat_statements, par exemple.
Certains champs de cette vue ne sont renseignés que si le paramètre
track_activities est à on (valeur par défaut,
qu’il est conseillé de laisser ainsi).
À noter qu’il ne faut pas interroger pg_stat_activity au
sein d’une transaction, son contenu pourrait sembler figé.
archiverIl est essentiel de vérifier que le processus d’archivage fonctionne correctement. Dans un contexte de déploiement avec CloudNativePG, un problème peut survenir à plusieurs niveaux :
archiver lui-même ;La vue pg_stat_archiver permet de connaître le nombre de
journaux de transactions correctement archivés
(archived_count), ou ayant eu un problème lors de leur
archivage (failed_count).
Les informations horodatage last_archived_time et
last_archived_time sont très pratiques, notamment pour
trouver la cause d’une saturation d’espace par exemple.
select * from pg_stat_archiver \gx
-[ RECORD 1 ]------+------------------------------
archived_count | 7
last_archived_wal | 000000010000000000000007
last_archived_time | 2026-03-20 13:15:02.288457+00
failed_count | 0
last_failed_wal |
last_failed_time |
stats_reset | 2026-03-12 08:46:22.391185+00
Les données de cette vue sont essentielles à suivre.
seq_scan, idx_scann_live_tup, n_dead_tup,
n_tup_ins, n_tup_upd,
n_tup_dellast_vacuum, last_autovacuum,
last_analyze, last_autoanalyzePostgreSQL intègre une vue permettant de suivre l’utilisation des
tables, notamment avec les compteurs d’utilisation seq_scan
et idx_scan qui indiquent, respectivement, combien de fois
la table a été parcourue avec une lecture séquentielle ou via
l’utilisation d’un index.
Des statistiques sur le contenu sont également présentes avec par
exemple des informations sur le nombre de lignes vivantes
(n_live_tup) et le nombre de lignes mortes
(n_dead_tup). Ces informations sont notamment utilisées par
le processus autovacuum launcher pour déclencher ou non un
nettoyage des lignes (VACUUM) ainsi que le calcul des
statistiques (ANALYZE).
Ces dernières opérations sont d’ailleurs tracées dans d’autres
colonnes : last_vacuum, last_autovacuum,
last_analyze, last_autoanalyze. Elles
contiennent l’horodatage du dernier passe de l’opération, qu’elle soit
manuelle ou automatique.
Le suivi des indicateurs peut vous aider à identifier des tables qui n’auraient pas leurs statistiques à jour ou les raisons d’une fragmentation trop importante.
Les statistiques sont collectées dans la table
pg_statistic. La vue pg_stats affiche le
contenu de cette table système de façon plus accessible.
Les statistiques sont collectées sur :
Le recueil des statistiques s’effectue quand on lance un ordre
ANALYZE sur une table, ou que l’autovacuum le
lance de son propre chef.
Les statistiques sont calculées sur un échantillon égal à 300 fois le
paramètre STATISTICS de la colonne (ou, s’il n’est pas
précisé, du paramètre default_statistics_target, 100 par
défaut).
La vue pg_stats affiche les statistiques
collectées :
\d pg_stats
View "pg_catalog.pg_stats"
Column | Type | Collation | Nullable | Default
------------------------+----------+-----------+----------+---------
schemaname | name | | |
tablename | name | | |
attname | name | | |
inherited | boolean | | |
null_frac | real | | |
avg_width | integer | | |
n_distinct | real | | |
most_common_vals | anyarray | | |
most_common_freqs | real[] | | |
histogram_bounds | anyarray | | |
correlation | real | | |
most_common_elems | anyarray | | |
most_common_elem_freqs | real[] | | |
elem_count_histogram | real[] | | |inherited : la statistique concerne-t-elle un objet
utilisant l’héritage (table parente, dont héritent plusieurs tables)
;null_frac : fraction d’enregistrements dont la colonne
vaut NULL ;avg_width : taille moyenne de cet attribut dans
l’échantillon collecté ;n_distinct : si positif, c’est le nombre de valeurs
distinctes ; si négatif, c’est la fraction de valeurs distinctes pour
cette colonne dans la table. Il est possible de forcer la valeur de ce
champ s’il est constaté que la collecte des statistiques le calcule mal.
Par exemple, pour indiquer à l’optimiseur que chaque valeur apparaît
statistiquement deux fois :most_common_vals et most_common_freqs :
les valeurs les plus fréquentes de la table, et leur fréquence. Le
nombre de valeurs collectées est au maximum celui indiqué par le
paramètre STATISTICS de la colonne, ou à défaut par
default_statistics_target. Le défaut de 100 échantillons
sur 30 000 lignes peut être modifié comme ci-après (sachant que le temps
de planification augmente exponentiellement avec ce paramètre, et qu’il
vaut mieux ne pas dépasser la valeur 1000) :histogram_bounds : les limites d’histogramme sur la
colonne. Les histogrammes permettent d’évaluer la sélectivité d’un
filtre par rapport à sa valeur précise. Ils permettent par exemple à
l’optimiseur de déterminer que 4,3 % des enregistrements d’une colonne
noms commencent par un A, ou 0,2 % par AL. Le principe est
de regrouper les enregistrements triés dans des groupes de tailles
approximativement identiques, et de stocker les limites de ces groupes
(on ignore les most_common_vals, pour lesquelles il y a
déjà une mesure plus précise). Le nombre d’histogram_bounds
est calculé de la même façon que les most_common_vals
;correlation : le facteur de corrélation statistique
entre l’ordre physique et l’ordre logique des enregistrements de la
colonne. Il vaudra par exemple 1 si les enregistrements
sont physiquement stockés dans l’ordre croissant, -1 si ils
sont dans l’ordre décroissant, ou 0 si ils sont totalement
aléatoirement répartis. Ceci sert à affiner le coût d’accès aux
enregistrements ;most_common_elems et
most_common_elems_freqs : les valeurs les plus fréquentes
si la colonne est un tableau (NULL dans les autres cas), et leur
fréquence. Le nombre de valeurs collectées est au maximum celui indiqué
par le paramètre STATISTICS de la colonne, ou à défaut par
default_statistics_target ;elem_count_histogram : les limites d’histogramme sur la
colonne si elle est de type tableau.Parfois, il est intéressant de calculer des statistiques sur un
ensemble de colonnes ou d’expressions. Dans ce cas, il faut créer un
objet statistique en indiquant les colonnes et/ou expressions à traiter
et le type de statistiques à calculer (voir la documentation de
CREATE STATISTICS).
pg_statio_user_tablespg_stat_databasepg_stat_user_indexespg_stat_wal_receiverpg_stat_checkpointerpg_stat_database_conflictsIl existe de très nombreuses vues qui permettent de retrouver des
informations sur à peut-prêt tous les composants de PostgreSQL, que ce
soit des conflits de réplication entre une instance primaire et un
secondaire avec pg_stat_database_conflicts, sur la
réception des journaux de transactions avec
pg_stat_wal_receiver, et bien d’autres encore.
Ayez en tête que l’informations que vous cherchez est très probablement présentes au sein de PostgreSQL.
Les traces d’une instance PostgreSQL sont par défaut peu fournies mais peuvent devenir, lorsque bien configurées, une vrai mine d’or. Bien suivies et bien exploitées elles vous permettront de trouver différents éléments, comme :
Voyons quels sont les paramètres de configuration qui sont essentiels à connaître et à configurer !
log_destination, log_directory,
log_file_mode, log_filename, …Nous l’avons vu dans le module K1 de cette formation, la gestion des traces est déléguée à l’opérateur. Les paramètres qui gèrent les traces (emplacement, format JSON, etc) sont fixés par l’opérateur. Cependant aucun paramètre PostgreSQL modifiant le contenu des traces n’est modifié. C’est bien à nous, à vous, de le faire.
log_min_messages
panic / fatal / log
/ error / warninglog_min_error_statement
error (ou warning)log_min_messages est le paramètre à configurer pour
avoir plus ou moins de traces. Par défaut, PostgreSQL enregistre tous
les messages de niveau panic, fatal,
log, error et warning. Cela peut
sembler beaucoup mais, dans les faits, c’est assez discret. Cependant,
il est possible de descendre le niveau ou de l’augmenter.
log_min_error_statement indique à partir de quel niveau
la requête est elle-aussi tracée. Par défaut, la requête n’est tracée
que si une erreur est détectée. Généralement, ce paramètre n’est pas
modifié, sauf dans un cas précis. Les messages d’avertissement (niveau
warning) n’indiquent pas la requête qui a généré
l’affichage du message. Cela est assez important, notamment dans le
cadre de l’utilisation d’antislash dans les chaînes de caractères. On
verra donc parfois un abaissement au niveau warning pour
cette raison.
log_min_duration_statement (ex : 1s)log_statement + log_durationlog_transaction_sample_ratelog_statement_sample_rate +
log_min_duration_samplePour repérer les problèmes de performances, il est intéressant de pouvoir tracer les requêtes et leur durée d’exécution. PostgreSQL propose deux solutions à cela.
log_statement & log_duration :
La première solution disponible concerne les paramètres
log_statement et log_duration. Le premier
permet de tracer toute requête exécutée si la requête correspond au
filtre indiqué par le paramètre :
none : aucune requête n’est tracée ;ddl : seules les requêtes DDL (autrement dit de
changement de structure) sont tracées ;mod : seules les requêtes de changement de structure et
de données sont tracées ;all : toutes les requêtes sont tracées.Le paramètre log_duration est un simple booléen. S’il
vaut true ou on, chaque requête exécutée
envoie en plus un message dans les traces indiquant la durée d’exécution
de la requête. Évidemment, il vaut mieux alors configurer
log_statement à all, ou il sera impossible de
dire à quelles requêtes les temps correspondent.
Donc pour tracer toutes les requêtes et leur durée d’exécution, une solution serait de réaliser la configuration suivante :
Une requête générera deux entrées dans les traces. Par exemple, pour
la simple requête SELECT 1 ;, nous obtenons ces deux
records JSON. Au passage, notons le réel surcoût en terme de
lignes entre une trace PostgreSQL pure et une trace PostgreSQL gérée par
CloudNativePG.
{
"level": "info",
"ts": "2026-05-28T09:17:48.181909992Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "postgresql-prod-1",
"record": {
"log_time": "2026-05-28 09:17:48.181 UTC",
"user_name": "postgres",
"database_name": "postgres",
"process_id": "2875",
"connection_from": "[local]",
"session_id": "6a18081e.b3b",
"session_line_num": "1",
"command_tag": "idle",
"session_start_time": "2026-05-28 09:17:18 UTC",
"virtual_transaction_id": "43/17",
"transaction_id": "0",
"error_severity": "LOG",
"sql_state_code": "00000",
"message": "statement: select 1;",
"application_name": "psql",
"backend_type": "client backend",
"query_id": "0"
}
}
{
"level": "info",
"ts": "2026-05-28T09:17:48.182290205Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "postgresql-prod-1",
"record": {
"log_time": "2026-05-28 09:17:48.182 UTC",
"user_name": "postgres",
"database_name": "postgres",
"process_id": "2875",
"connection_from": "[local]",
"session_id": "6a18081e.b3b",
"session_line_num": "2",
"command_tag": "SELECT",
"session_start_time": "2026-05-28 09:17:18 UTC",
"virtual_transaction_id": "43/0",
"transaction_id": "0",
"error_severity": "LOG",
"sql_state_code": "00000",
"message": "duration: 0.869 ms",
"application_name": "psql",
"backend_type": "client backend",
"query_id": "0"
}
}Cette méthode n’est pas recommandée car elle va générer des lignes
pour toutes les requêtes. Or, le système de Readiness Probe
utilise l’utilitaire pg_isready qui va se connecter très
fréquemment pour s’assurer que le Pod est toujours en
vie.
log_min_duration_statement :
Il est préférable de désactiver ces deux paramètres et de configurer
log_min_duration_statement. Son but est d’abord de cibler
les requêtes lentes, par exemple celles qui prennent plus de deux
secondes à s’exécuter :
La requête et la durée d’exécution seront alors tracées dans le champ
message :
{
"level": "info",
"ts": "2026-05-28T09:25:01.497261444Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "postgresql-prod-1",
"record": {
"log_time": "2026-05-28 09:25:01.496 UTC",
"user_name": "postgres",
"database_name": "postgres",
"process_id": "2875",
"connection_from": "[local]",
"session_id": "6a18081e.b3b",
"session_line_num": "3",
"command_tag": "SELECT",
"session_start_time": "2026-05-28 09:17:18 UTC",
"virtual_transaction_id": "43/0",
"transaction_id": "0",
"error_severity": "LOG",
"sql_state_code": "00000",
"message": "duration: 3003.653 ms statement: SELECT pg_sleep(3) ;",
"application_name": "psql",
"backend_type": "client backend",
"query_id": "0"
}
}En plus de la trace par log_min_duration_statement, rien
n’interdit de tracer des requêtes sensibles, notamment le DDL :
Échantillonnage :
Quelle que soit la méthode, tracer toutes les requêtes peut poser problème pour de simples raisons de volumétrie du fichier de traces. Même s’il est possible de configurer finement la durée à partir de laquelle une requête est tracée, il faut bien comprendre que plus la durée minimale est importante, plus la vision des performances est partielle. Passeront ainsi « sous le radar » des requêtes relativement rapides mais très nombreuses qui, ensemble, peuvent représenter l’essentiel de la charge.
Cela étant dit, laisser 0 en permanence n’est pas recommandé. Il est préférable de configurer ce paramètre à une valeur plus importante en temps normal pour détecter seulement les requêtes longues et, lorsqu’un audit de la plateforme est nécessaire, passer temporairement ce paramètre à une valeur très basse (0 étant le mieux).
Une nouvelle fonctionnalité a donc été ajoutée : tracer une certaine proportion des requêtes ou des transactions.
log_transaction_sample_rate indique une proportion de
transactions à tracer. Par exemple, en le configurant à
0.01, toutes les requêtes d’un centième des transactions,
choisies au hasard, seront tracées.
De manière similaire, log_statement_sample_rate indique
la proportion de requêtes à tracer, parmi celles durant
plus d’une certaine durée, à indiquer dans
log_min_duration_sample :
Évidemment, une requête dépassant la durée de
log_min_duration_statement sera toujours tracée.
log_connections + log_disconnectionslog_autovacuum_min_durationlog_checkpoints
time ou wal ?log_lock_waits (mini 1s)
En dehors des erreurs et des durées des requêtes, il est aussi
possible de tracer certaines activités ou comportements. Le paramétrage
par défaut est peu bavard, et beaucoup de paramètres sont à
off. Il est généralement conseillé d’activer tous ceux qui
suivent.
Dates de (dé)connexion :
log_connections et son pendant
log_disconnections, avec la valeur on,
permettent de suivre qui se (dé)connecte, depuis où, et durant combien
de temps.
Exemple de traces d’une connexion sur une instance en version 18. Les
quatre étapes de connexions sont visibles
(connection received,
connection authenticated,
connection authorized, connection received)
:
{
"level": "info",
"ts": "2026-05-28T09:37:53.554333667Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "postgresql-prod-1",
"record": {
"log_time": "2026-05-28 09:37:53.554 UTC",
"process_id": "3346",
"connection_from": "10.244.0.8:57010",
"session_id": "6a180cf1.d12",
"session_line_num": "1",
"session_start_time": "2026-05-28 09:37:53 UTC",
"transaction_id": "0",
"error_severity": "LOG",
"sql_state_code": "00000",
"message": "connection received: host=10.244.0.8 port=57010",
"backend_type": "not initialized",
"query_id": "0"
}
}
{
"level": "info",
"ts": "2026-05-28T09:37:53.576991821Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "postgresql-prod-1",
"record": {
"log_time": "2026-05-28 09:37:53.576 UTC",
"user_name": "admin",
"database_name": "postgres",
"process_id": "3346",
"connection_from": "10.244.0.8:57010",
"session_id": "6a180cf1.d12",
"session_line_num": "2",
"command_tag": "authentication",
"session_start_time": "2026-05-28 09:37:53 UTC",
"virtual_transaction_id": "86/27",
"transaction_id": "0",
"error_severity": "LOG",
"sql_state_code": "00000",
"message": "connection authenticated: identity=\"admin\" method=scram-sha-256 (/var/lib/postgresql/data/pgdata/pg_hba.conf:26)",
"backend_type": "client backend",
"query_id": "0"
}
}
{
"level": "info",
"ts": "2026-05-28T09:37:53.577086783Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "postgresql-prod-1",
"record": {
"log_time": "2026-05-28 09:37:53.576 UTC",
"user_name": "admin",
"database_name": "postgres",
"process_id": "3346",
"connection_from": "10.244.0.8:57010",
"session_id": "6a180cf1.d12",
"session_line_num": "3",
"command_tag": "authentication",
"session_start_time": "2026-05-28 09:37:53 UTC",
"virtual_transaction_id": "86/27",
"transaction_id": "0",
"error_severity": "LOG",
"sql_state_code": "00000",
"message": "connection authorized: user=admin database=postgres application_name=psql SSL enabled (protocol=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384, bits=256)",
"backend_type": "client backend",
"query_id": "0"
}
}
{
"level": "info",
"ts": "2026-05-28T09:37:54.315218272Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "postgresql-prod-1",
"record": {
"log_time": "2026-05-28 09:37:54.314 UTC",
"process_id": "3348",
"connection_from": "[local]",
"session_id": "6a180cf2.d14",
"session_line_num": "1",
"session_start_time": "2026-05-28 09:37:54 UTC",
"transaction_id": "0",
"error_severity": "LOG",
"sql_state_code": "00000",
"message": "connection received: host=[local]",
"backend_type": "not initialized",
"query_id": "0"
}
}Et la trace de déconnexion de cette même session :
{
"level": "info",
"ts": "2026-05-28T09:37:55.493956778Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "postgresql-prod-1",
"record": {
"log_time": "2026-05-28 09:37:55.493 UTC",
"user_name": "admin",
"database_name": "postgres",
"process_id": "3346",
"connection_from": "10.244.0.8:57010",
"session_id": "6a180cf1.d12",
"session_line_num": "4",
"command_tag": "idle",
"session_start_time": "2026-05-28 09:37:53 UTC",
"transaction_id": "0",
"error_severity": "LOG",
"sql_state_code": "00000",
"message": "disconnection: session time: 0:00:01.940 user=admin database=postgres host=10.244.0.8 port=57010",
"application_name": "psql",
"backend_type": "client backend",
"query_id": "0"
}
}Depuis PostgreSQL 18, log_connections connaît d’autres
valeurs que on suivant les phases de la connexion à tracer
:
receipt pour noter la réception de la demande de
connexion ;authentication pour l’authentification (utilisateur
original et non l’utilisateur connu de PostgreSQL qui peut
différer) ;authorization pour l’autorisation (avant finalisation
du_backend_) ;setup_durations (nouveauté PostgreSQL 18) pour la durée
de tout le processus.Pour la compatibilité, la valeur on est équivalente aux
trois premiers paramètres ensemble. Il est conseillé de tout
tracer :
mais pour réduire le volume de traces, on peut se limiter à certaines valeurs :
Durée des autovacuums :
log_autovacuum_min_duration équivaut à
log_min_duration_statement, mais pour le démon
autovacuum. Le but est de tracer son activité, au-delà d’une
certaine durée, pour vérifier qu’il passe suffisamment fréquemment et
rapidement, et sur quelles tables.
Checkpoints :
Un checkpoint est l’opération périodique qui nettoie le cache de
PostgreSQL, synchronise sur disque les fichiers de la base, invalide les
slots de réplication inutilisés et recycle les journaux de transaction.
log_checkpoints = on trace son début, sa fin, et quelques
statistiques :
{
"level": "info",
"ts": "2026-05-28T09:53:42.858079202Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "postgresql-prod-1",
"record": {
"log_time": "2026-05-28 09:53:42.857 UTC",
"process_id": "50",
"session_id": "6a17e96c.32",
"session_line_num": "19",
"session_start_time": "2026-05-28 07:06:20 UTC",
"transaction_id": "0",
"error_severity": "LOG",
"sql_state_code": "00000",
"message": "checkpoint starting: immediate force wait",
"backend_type": "checkpointer",
"query_id": "0"
}
}
{
"level": "info",
"ts": "2026-05-28T09:53:42.873303902Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "postgresql-prod-1",
"record": {
"log_time": "2026-05-28 09:53:42.872 UTC",
"process_id": "50",
"session_id": "6a17e96c.32",
"session_line_num": "20",
"session_start_time": "2026-05-28 07:06:20 UTC",
"transaction_id": "0",
"error_severity": "LOG",
"sql_state_code": "00000",
"message": "checkpoint complete: wrote 22 buffers (0.1%), wrote 1 SLRU buffers; 0 WAL file(s) added, 0 removed, 1 recycled; write=0.004 s, sync=0.004 s, total=0.016 s; sync files=17, longest=0.002 s, average=0.001 s; distance=16402 kB, estimate=67968 kB; lsn=0/3501CE78, redo lsn=0/3501CE20",
"backend_type": "checkpointer",
"query_id": "0"
}
}Le message indique aussi le nombre de blocs écrits sur disque, le nombre de journaux de transactions ajoutés, supprimés et recyclés. Il est rare que des journaux soient ajoutés, ils sont plutôt recyclés. Des journaux sont supprimés quand il y a eu une très grosse activité qui a généré plus de journaux que d’habitude. Les statistiques incluent aussi la durée des écritures, de la synchronisation sur disque, la durée totale, etc…
Le plus important est de pouvoir vérifier que l’écriture des
checkpoints est généralement régulière (par défaut toutes les 5
minutes), comme l’indique ici checkpoint starting: time.
Une mention de checkpoint starting: wal indique un
déclenchement forcé par l’écriture de nombreux journaux lors d’une
grosse activité.
Repérer les attentes sur verrous :
log_lock_waits à on permet de tracer les
attentes de verrous (par exemple, un UPDATE bloqué par un
autre UPDATE, un SELECT bloqué par
TRUNCATE ou un VACUUM FULL, etc…) Lorsque
l’attente dépasse la durée indiquée par le paramètre
deadlock_timeout (1 seconde par défaut), un message
d’information est enregistré, comme dans cet exemple :
{
"level": "info",
"ts": "2026-05-28T09:59:53.813973716Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "postgresql-prod-1",
"record": {
"log_time": "2026-05-28 09:59:53.813 UTC",
"user_name": "postgres",
"database_name": "postgres",
"process_id": "3608",
"connection_from": "[local]",
"session_id": "6a181046.e18",
"session_line_num": "10",
"command_tag": "DROP TABLE waiting",
"session_start_time": "2026-05-28 09:52:06 UTC",
"virtual_transaction_id": "97/49",
"transaction_id": "863",
"error_severity": "LOG",
"sql_state_code": "00000",
"message": "process 3608 still waiting for AccessExclusiveLock on relation 16431 of database 5 after 1000.181 ms",
"detail": "Process holding the lock: 3661. Wait queue: 3608.",
"query": "drop table t1;",
"application_name": "psql",
"backend_type": "client backend",
"query_id": "0"
}
}Ici, un DROP TABLE attend depuis 1 seconde de pouvoir
poser un verrou exclusif sur une relation. L’opération n’est pas
interrompue, et, en général, finira par s’exécuter, avec le retard lié à
ce verrou.
Plus ce type de message apparaît dans les traces, plus des contentions ont lieu sur certains objets, ce qui peut diminuer fortement les performances. Ces messages peuvent permettre d’analyser la cause première d’une accumulation de verrous, à condition que les requêtes soient tracées.
log_temp_files : à activer !Quand PostgreSQL ne peut effectuer une opération en mémoire, il le fait sur disque dans un fichier temporaire, ce qui est beaucoup plus lent qu’en mémoire, même avec un SSD.
Typiquement, les fichier temporaires apparaissent lors de tris de
données (ORDER BY), certains agrégats, les déduplications
(DISTINCT), les jointures par hachage (hash join),
les CTE matérialisées (WITH … AS …)…, quand la valeur du
paramètre work_mem est insuffisante pour travailler en
mémoire. Le passage par des fichiers temporaires n’est pas forcément
gênant pour de grosses requêtes ponctuelles. Ils sont parfois
inévitables quand on brasse beaucoup de données. Cependant, des fichiers
temporaires trop fréquents et trop gros peuvent avoir un impact sur la
performance du système. Dans le pire des cas, ils peuvent saturer les
I/O, voire le disque de l’instance.
Être averti lors de la création de ce type de fichiers peut être
intéressant, mais ils sont parfois trop fréquents pour que ce soit
réaliste. Il est préférable de faire analyser après coup un fichier de
traces pour savoir combien de fichiers temporaires ont été créés, et de
quelles tailles. Cela peut mener à vérifier les requêtes exécutées, les
optimiser, vérifier la configuration, réviser la valeur de
work_mem…
Le paramètre log_temp_files à 0 permet de tracer toutes
les créations de fichiers temporaires.
Pour le même tri, il peut y avoir de nombreux fichiers temporaires. De plus, la requête est aussi tracée, et si elle est longue et fréquente, le volume de traces peut être conséquent.
log_line_prefix
%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%hlog_destination à csvlogCSV en JSONLe paramètre log_line_prefix permet d’ajouter un préfixe
à une trace. Vous l’avez peut être déjà modifié sur des instances
installées sur machine virtuelle. Le défaut ('%m [%p] ',
soit horodatage et numéro de processus) est généralement insuffisant. On
conseille généralement de le modifier avec la valeur présentée dans le
slide.
Il est à noter que ce paramètre n’est PAS pris en compte lorsque
log_destination de Postgresql est positionné à
csvlog (ou jsonlog). Ces destinations n’ont
pas besoin de préfixe car leur structure est fixe et contient toutes les
informations nécessaires. Or le paramètre log_line_prefix
fait partie de la liste des paramètres fixés par CloudNativePG (voir https://cloudnative-pg.io/docs/current/postgresql_conf#fixed-parameters).
Sa valeur est à csvlog. PostgreSQL exporte les traces dans
ce format qui seront par la suite récupérées et transformées au format
JSON par l’opérateur.
check_pg_activity
Une autre manière de surveiller vos instances est d’utiliser des outils ou scripts externes aux instances. Ils se basent sur les informations contenues dans l’instance pour nous alerter sur tel ou tel évènement.
Ces vérifications peuvent notamment être très précises (vont plus loin que les métriques de base remontées par CloudNativePG) ou alors adaptées au métier utilisant la base si l’on définit nos propres requêtes.
Pour n’en citer qu’un, le script de monitoring
check_pgactivity permet d’intégrer la supervision de bases
de données PostgreSQL dans un système de supervision piloté par un outil
tel que Nagios. Dès lors que votre instance est accessible, ce script
peut être utilisé. C’est dans le cadre de sa R&D que Dalibo a conçu
check_pgactivity.
Pour les besoins les plus simples, le script peut être utilisé de façon autonome, sans nécessité d’installer toute l’infrastructure d’un outil comme Nagios, Icinga ou Grafana.
La supervision d’un serveur PostgreSQL passe par la surveillance de
sa disponibilité, des indicateurs sur son activité, l’identification des
besoins de maintenance, et le suivi de la réplication le cas échéant.
Ci-dessous figurent les sondes check_pgactivity à mettre en
place sur ces différents aspects. Le site du projet contient toute la
documentation de chaque sonde.
Disponibilité :
connection : réalise un test de connexion pour vérifier
que le serveur est accessible ;backends : compte le nombre de connexions au serveur
comparé au paramètre max_connections ;backends_status : permet d’obtenir des statistiques
plus précises sur l’état des connexions clientes et d’être alerté
lorsqu’un certain nombre de connexions clientes sont dans un état donné
(waiting, idle in transaction…) ;uptime : détecte un redémarrage du serveur ou du
rechargement de la configuration.Vacuum :
autovacuum : suit le fonctionnement de l’autovacuum et
des tâches en cours (VACUUM, ANALYZE,
FREEZE…) ;table_bloat : vérifie le volume de données « mortes »
et la fragmentation des tables ;btree_bloat : vérifie le volume de données « mortes »
et la fragmentation des index - par rapport à
check_postgres, le calcul est séparé entre tables et
index ;last_analyze : vérifie si le dernier analyze (relevé
des statistiques relatives aux calculs des plans d’exécution) est trop
ancien ;last_vacuum : vérifie si le dernier vacuum (relevé des
espaces réutilisables dans les tables) est trop ancien.Activité :
locks : permet d’obtenir des statistiques plus
détaillées sur les verrous obtenus et tient notamment compte des
spécificités des predicate locks du niveau d’isolation
SERIALIZABLE ;wal_files : compte le nombre de segments du journal de
transaction présents dans le répertoire pg_wal ;longest_query : permet d’être alerté si une requête est
en cours d’exécution depuis plus d’un certain temps ;oldest_xact : permet d’être alerté si une transaction
est ouverte depuis un certain temps sans être utilisée ;oldest_2pc : calcule l’âge de la plus ancienne
transaction préparée (two-phase commit transaction) ;oldest_xmin : repère la plus ancienne transaction de
chaque base, et ce à quoi elle est liée (requête, slot…) ;bgwriter : permet de collecter des données de
performance des différents processus d’écritures de PostgreSQL ;hit_ratio : calcule le hit ratio (utilisation
du cache de PostgreSQL) ;commit_ratio : calcule la proportion de
COMMIT et ROLLBACK ;checksum_errors : détecte l’apparition d’erreurs de
sommes de contrôle (à partir de PostgreSQL 12) ;database_size : suit la volumétrie des bases et leurs
variations ;max_freeze_age : calcule l’âge des plus vieilles lignes
stockées dans chaque base pour suivre le bon passage des
VACUUM FREEZE ;stat_snapshot_age : calcule l’âge des statistiques
d’activité pour repérer un blocage du collecteur ;temp_files : suivi des fichiers temporaires.Configuration :
configuration : permet de vérifier que les principaux
paramètres mémoire n’ont pas leur valeur par défaut ;minor_version : détecte les instances n’ayant pas la
dernière version mineure ;settings : repère un changement des paramètres ;invalid_indexes : repérer tout index invalide ;pgdata_permission : vérifie les droits sur
PGDATA pour éviter un blocage au redémarrage ;table_unlogged : remonte le nombre de tables
unlogged ;extensions_versions : détecte les extensions à mettre à
jour.Réplication & archivage :
archiver : compte le nombre de segments du journal de
transaction en attente d’archivage ;archive_folder : vérifie qu’il n’y a pas de journal
manquant dans les archives de sauvegarde PITR ;hot_standby_delta : calcule le délai de réplication
entre un serveur primaire et un serveur secondaire ;is_master / is_hot_standby : vérifie que
l’instance est bien démarrée en lecture/écriture, ou une instance
secondaire ;is_replay_paused : vérifie si la réplication est en
pause ;replication_slots : calcule la volumétrie conservée
pour chaque slot de réplication.Sauvegarde physique et logique :
backup_label_age : calcule l’âge du fichier
backup_label (sauvegardes PITR exclusives) ;pg_dump_backup : contrôle l’âge et la variation de
taille des sauvegardes logiques.ConfigMap par défaut
cnpg-default-monitoringCloudNativePG propose plusieurs outils vous permettant de suivre ce
qu’il se passe sur vos instances, avec notamment, un exporter
Prometheus. Un ensemble de métriques sont prédéfinies et sont
exploitables dès la création des instances. Elles peuvent être
retrouvées dans le ConfigMap
cnpg-default-monitoring.
Un mécanisme de métriques personnalisées est disponible pour nous permettre d’étendre ces relevés. En plus des métriques sur les instances PostgreSQL, des métriques de l’opérateur sont également disponibles.
Un dashboard Grafana est disponible et permet d’exploiter les données remontées par l’exporter. Voyons cela plus en détails.
Pod PostgreSQL
curl http://127.0.0.1:9187/metricsPod de l’opérateur
curl http://127.0.0.1:8080/metricsConfigMap cnpg-default-monitoringUn ensemble de métriques PostgreSQL est automatiquement exposé sur le
port 9187 du Pod PostgreSQL. C’est un
exporter compatible avec Prometheus. Elles sont récupérables
sur le point de terminaison /metrics.
Toute les métriques prédéfinies peuvent être retrouvées dans la
ressource ConfigMap qui est nommée par défaut
cnpg-default-monitoring. La commande suivante vous permet
de retrouver sa définition.
kubectl get configmaps cnpg-default-monitoring -o yaml
D’autres métriques peuvent être présentes, c’est notamment le cas pour des métriques concernant les sauvegardes ou encore le suivi de consommation d’une application Golang. Rappelez vous que l’’opérateur est écrit en Golang. Les métriques avec :
cnpg_* concernent des informations de
PostgreSQL ;go_* concernent des informations
applicatives de Golang.L’export de métriques de l’opérateur est également présent, cette
fois-ci sur le port 8080 du Pod de
l’opérateur. Les métriques relevées peuvent aider lors d’une recherche
de bug ou de diagnostique avancé, mais très peu dans le quotidien.
Les requêtes exécutées sur les instances PostgreSQL le sont avec le
ROLE pg_monitor. Elles ciblent la base de
données indiquée par la méthode bootstrap (donc la base
app par défaut). Cela peut être surchargé par l’option
target_databases pour des métriques maisons.
Elles sont exécutées à chaque fois qu’une requête sur
/metrics est faite. Pour éviter que des exécutions trop
fréquentes des requêtes ne soit faites, un système de cache est mis en
place. Par défaut, le résultat est mis en cache pendant 30 secondes. Ce
temps peut être modifié par le paramètre
cluster.spec.monitoring.metricsQueriesTTL. Autrement dit,
la fréquence de récupération via votre système de monitoring ne peut
pas, par défaut, avoir un suivi plus précis que 30 secondes.
ConfigMapspec.monitoring du ClusterSelon vos besoins, il est possible que ce jeu de métriques ne soit
pas suffisant. Vous avez la possibilité de rajouter vos propres
métriques personnalisées, qui seront exposées via l’Exporter
Prometheus. Pour cela, il est nécessaire de créer une resource
ConfigMap qui contiendra la définition de la métrique ainsi
que la requête à exécuter.
Voici un exemple simpliste qui permet de remonter le nombre de lignes
d’une table foo grâce à count(*) :
---
apiVersion: v1
kind: ConfigMap
metadata:
name: monitoring-count-ma-table
labels:
cnpg.io/reload: ""
data:
custom-queries: |
count-foo:
query: "SELECT count(*) FROM foo"
metrics:
- count:
usage: "GAUGE"
description: "Number of rows of foo"À noter que le label cnpg.io/reload: "" permet
de prendre en compte les nouvelles requêtes de manière automatique.
Ce ConfigMap doit ensuite être mentionné dans la partie
spec.monitoring de votre Cluster pour que les
métriques personnalisées soit elles aussi exportées sur le point de
terminaison /metrics.
[…]
monitoring:
customQueriesConfigMap:
- name: monitoring-count-ma-table # ConfigMap
key: custom-queriesComme indiqué plus haut, les requêtes vont être exécutées dès lors
que /metrics est accédé. Attention donc à vos requêtes
personnalisées qui, si elles sont trop compliquées et mettent du temps à
s’exécuter, risquent d’impacter durablement l’instance, et ce même avec
le système de cache.
Ces métriques doivent être collectées. C’est à vous ou à votre équipe en charge de l’exploitation de Kubernetes de mettre en place une solution de collecte et de stockage.
La documentation du projet propose dans la partie Quick Start un exemple de déploiement de la stack Prometheus - Grafana.
Si vous utiliser Prometheus, une resource PodMonitor
doit être créée pour lui indiquer quel Cluster doit être
pris en compte dans la collecte.
Une fois toutes ces métriques collectées, il vous restera à les exploiter.
Dans l’écosystème Kubernetes, l’outil Grafana est très réputé. CloudNativePG et la communauté maintiennent un dashboard Grafana, rendant l’exploitation des métriques aisées.
De nombreux graphiques existent déjà. Ils concernent soit des métriques systèmes, soit des métriques PostgreSQL. Ces derniers sont alimentés par les valeurs récupérées de l’Exporter Prometheus vu précédemment. Toutes les métriques remontées ne sont pas présentées dans des graphiques. À vous de les rajouter.
Le troubleshooting est une étape que l’on apprécie guère devoir faire. Pourtant, il est bien nécessaire de connaître quelques astuces pour y parvenir, que ce soit sur PostgreSQL ou CloudNativePG.
Ces quelques slides se veulent être une introduction à ce sujet, tant les problèmes peuvent être variés. Couvrir l’intégralité des thèmes est impossible. Ceci est d’autant plus vrai que le monde Kubernetes apporte lui aussi sa dose de complexité.
cnpg pour kubectl
ClusterLe plugin cnpg pour kubectl
devrait être installé sur les postes des administrateurs à qui revient
la gestion des instances PostgreSQL.
Il intègre un ensemble de commandes permettant de récupérer des
informations sur les Clusters ainsi que de lancer des
opérations de maintenance.
Il est mis à jour à chaque nouvelle version de l’opérateur.
kubectl cnpg status CLUSTER
ClusterLa première commande à connaître est status. Elle donne
un aperçu du Cluster d’instances ciblé. Par exemple :
kubectl cnpg status postgresql-prod
La section Cluster Summary indique notamment quelle est
l’instance primaire (Primary instance) et depuis quand elle
l’est (Primary promotion time), leur état de santé
(Status) ou encore à quel LSN se trouve l’instance primaire
(Current Write LSN).
Cluster Summary
Name default/postgresql-prod
System ID: 7644933667360784417
PostgreSQL Image: ghcr.io/cloudnative-pg/postgresql:18.3-system-trixie
Primary instance: postgresql-prod-1
Primary promotion time: 2026-05-28 13:27:14 +0000 UTC (50s)
Status: Cluster in healthy state
Instances: 2
Ready instances: 2
Size: 96M
Current Write LSN: 0/4000060 (Timeline: 1 - WAL File: 000000010000000000000004)
La section Backup donne des informations sur la
sauvegarde et l’archivage qui serait en place. Si ce n’est pas le cas,
le message suivant est indiqué :
Continuous Backup not configured
La section Streaming Replication status donne beaucoup
d’informations et indique notamment quel est l’état de la réplication
avec la colonne State. Cette valeur est récupérée depuis la
vue pg_stat_wal_receiver
du secondaire. Les colonnes Lag sont quant a elle retrouvées
depuis la vue pg_stat_replication
de l’instance primaire. Ici il n’est question que de la Streaming
Replication.
Streaming Replication status
Replication Slots Enabled
Name Sent LSN Write LSN Flush LSN Replay LSN Write Lag Flush Lag Replay Lag State Sync State Sync Priority Replication Slot
---- -------- --------- --------- ---------- --------- --------- ---------- ----- ---------- ------------- ----------------
postgresql-prod-2 0/4000060 0/4000060 0/4000060 0/4000060 00:00:00 00:00:00 00:00:00 streaming async 0 active
Enfin, la dernière partie concerne les instances elles mêmes. Elle
mélange des informations PostgreSQL, (colonne Current LSN),
et des informations Kubernetes (colonnes QoS et
Node).
Instances status
Name Current LSN Replication role Status QoS Manager Version Node
---- ----------- ---------------- ------ --- --------------- ----
postgresql-prod-1 0/4000060 Primary OK BestEffort 1.29.1 kind-control-plane
postgresql-prod-2 0/4000060 Standby (async) OK BestEffort 1.29.1 kind-control-plane
kubectl cnpg report cluster CLUSTER --logs -f report.zip
kubectl cnpg report operator -n NAMESPACE --logs -f report_cnpg.zip
.zip) d’un Cluster ou de
l’opérateur
Pods (logs)La collecte d’informations est une étape importante pour mener un
diagnostic. Le plugin intègre la commande report
qui permet de générer des archives contenant la définition des
ressources Kubernetes de l’objet ciblé (Cluster PostgreSQL
ou opérateur) ainsi que les traces des Pods associés.
kubectl cnpg report cluster postgresql-prod --logs -f report.zip
unzip report.zip
Archive: report.zip
creating: report_cluster_postgresql-prod_20260529_072650/
creating: report_cluster_postgresql-prod_20260529_072650/manifests/
inflating: report_cluster_postgresql-prod_20260529_072650/manifests/cluster.yaml
inflating: report_cluster_postgresql-prod_20260529_072650/manifests/cluster-pods.yaml
inflating: report_cluster_postgresql-prod_20260529_072650/manifests/cluster-jobs.yaml
inflating: report_cluster_postgresql-prod_20260529_072650/manifests/events.yaml
inflating: report_cluster_postgresql-prod_20260529_072650/manifests/cluster-pvcs.yaml
creating: report_cluster_postgresql-prod_20260529_072650/logs/
inflating: report_cluster_postgresql-prod_20260529_072650/logs/postgresql-prod-1-postgres.jsonl
inflating: report_cluster_postgresql-prod_20260529_072650/logs/postgresql-prod-3-postgres.jsonl
creating: report_cluster_postgresql-prod_20260529_072650/job-logs/
kubectl cnpg fencing on CLUSTER ID -- une instance
kubectl cnpg fencing on CLUSTER "*" -- toutes les instances
postmasterPod toujours en cours d’exécutionfencedIl existe un moyen d’arrêter le processus postmaster de
PostgreSQL sans pour autant arrêter les Pods. C’est le
mécanisme de fencing de CloudNativePG qui permet de faire
cela.
En arrêtant un processus postmaster, on s’assure
qu’aucune modification dans les fichiers de données PostgreSQL ne sera
faite. Cela permet de diagnostiquer des problèmes sur le
Pod ou le système de fichiers. Par conséquence, les
connexions en cours sur la ou les instances ciblées par ce
fencing seront toutes interrompues.
Si le Pod de l’instance primaire est
fenced, aucun mécanisme de failover ne sera
déclenché.
Il existe la commande inverse kubectl cnpg fencing off
pour redémarrer un postmaster arrêté dans un
Pod.
De nombreux paramètres sont modifiables, que ce soit de manière
globale (postgresql.conf), dans une session, ou encore au
sein même d’une transaction. Des surcharges peuvent également être
faites au niveau d’un rôle ou d’une base de données.
Pour s’assurer de la valeur prise par tel ou tel paramètre, la
commande SHOW est à connaître et à utiliser.
true ou falseIl vous sera peut-être donné uniquement un accès SQL aux instances.
Dans ce cas là, si vous souhaitez savoir si votre instance est une
instance primaire ou instance secondaire, la fonction
pg_is_in_recovery() vous sera utile.
Elle indique si l’instance est en mode recovery ou si elle ne l’est pas. Une instance est dans ce mode si elle est en train de rejouer des journaux de transactions. C’est donc forcément le cas d’une instance secondaire.
Attention, il existe un cas où une instance dite primaire peut être en mode recovery : lorsqu’elle redémarre et qu’elle rejoue ses journaux localement avant d’atteindre un point de consistance.
Dans divers cas (accumulation de verrous, requête particulièrement
lente, …), il peut être nécessaire d’arrêter l’exécution d’une requête,
ou forcer la déconnexion d’une session. PostgreSQL intègre différentes
fonctions pour y parvenir avec notamment
pg_cancel_backend(pid) et
pg_terminate_backend(pid, timeout).
L’utilisation de pg_terminate_backend() et
pg_cancel_backend() n’est disponible que pour les
utilisateurs appartenant au même rôle que l’utilisateur à déconnecter,
les utilisateurs membres du rôle pg_signal_backend et bien
sûr les superutilisateurs.
Dossier pg_wal
Utiles sans accès au système de fichiers
L’accès au système de fichiers n’est pas systématique dans un environnement conteneurisé, et si cela reste possible, il n’est pas forcément aisé d’y accéder. Il vous sera parfois nécessaire de trouver, lister, les fichiers présents dans tel ou tel dossier.
PostgreSQL vous permet cela avec la fonction pg_ls_dir()
en renseignant le chemin du dossier. Bien qu’intéressante, cette
fonction n’est certainement pas la plus utilisée, au contraire de
pg_ls_waldir(), qui permet de lister les fichiers présents
dans le dossier pg_wal du PGDATA.
Quand un incident survient sur une instance PostgreSQL, il y a de grandes chances que celui-ci ait déjà été rencontré par d’autres personnes. Notre expérience au support nous le confirme tous les jours. Ces schémas se répètent et les préconisations associées ne changent que rarement.
Prenons le temps d’étudier quelques cas typiques d’incidents pouvant être rencontrés, et comment y remédier.
pg_wal saturé :
Un problème couramment rencontré est la saturation du système de fichiers. PostgreSQL ne pouvant plus écrire sur disque, par mesure de précaution, l’instance est bloquée.
Il faut distinguer deux types de blocages :
Dans ce cas de figure, le système de fichiers où se trouve les
fichiers de données (PGDATA) n’a plus de place. PostgreSQL
accepte de répondre aux requêtes en lecture, mais les requêtes en
écriture sont impossibles.
Le diagnostique est assez simple : les données présentes dans les bases (tables, index, etc) n’ont cessé de grossir, et comme elles sont utiles, il faut agrandir l’espace.
postmaster est complètement arrêté.Dans ce second cas, il n’est même plus possible de se connecter à l’instance. L’arrêt du processus principal a été opéré afin de garantir qu’aucune modification ne soit faite sans qu’elle ne soit répercutée dans les journaux de transactions.
Autrement dit PostgreSQL n’a plus pu écrire dans pg_wal.
Les raisons peuvent être variées. Dans tous les cas, si le répertoire
pg_wal commence à grossir fortement, c’est que PostgreSQL
n’arrive plus à recycler ses journaux de transactions.
Plusieurs choses peuvent expliquer un mauvais recyclage :
wal_keep_size ou
max_slot_wal_keep_size;Dans l’un ou l’autre des cas présentés, il est nécessaire de trouver
de la place en augmentant la taille des Persistent Volumes.
Dans notre contexte, modifier spec.storage.size est
nécessaire.
Aussi, il est recommandé de créer deux volumes pour chaque
Pod de notre Cluster en utilisant
spec.storage et spec.walStorage, par exemple
:
max_slot_wal_keep_size dépassé{
"level": "info",
"ts": "2026-05-29T09:33:42.655361766Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "postgresql-prod-3",
"record": {
"log_time": "2026-05-29 09:33:42.655 UTC",
"process_id": "45",
"session_id": "6a195d1a.2d",
"session_line_num": "23",
"session_start_time": "2026-05-29 09:32:10 UTC",
"virtual_transaction_id": "165/0",
"transaction_id": "0",
"error_severity": "LOG",
"sql_state_code": "00000",
"message": "waiting for WAL to become available at 2/80002000",
"backend_type": "startup",
"query_id": "0"
}
}
{
"level": "info",
"ts": "2026-05-29T09:33:47.529693788Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "postgresql-prod-3",
"record": {
"log_time": "2026-05-29 09:33:47.529 UTC",
"process_id": "1191",
"session_id": "6a195d7b.4a7",
"session_line_num": "1",
"session_start_time": "2026-05-29 09:33:47 UTC",
"transaction_id": "0",
"error_severity": "FATAL",
"sql_state_code": "08P01",
"message": "could not start WAL streaming: ERROR: can no longer access replication slot \"_cnpg_postgresql_prod_3\"\nDETAIL: This replication slot has been invalidated due to \"wal_removed\".",
"backend_type": "walreceiver",
"query_id": "0"
}
}N’hésitez pas, c’est le moment !
La version en ligne des solutions de ces TP est disponible sur https://dali.bo/k4_solutions.
But : Récupérer les métriques exportées et créer des métriques personnalisées.
But : Découvrir les métriques de l’Exporter Prometheus.
Créer un fichier
~/cluster.yamlqui définit unClusteravec une seule instance dans la dernière version de PostgreSQL disponible.
Créer cette ressource.
Exposer localement le port
9187de l’Exporter Prometheus de l’instance primaire.
Récupérer toutes les métriques disponibles à l’aide de
curl -s. Ils sont accessibles sur/metrics.
Retrouver le nombre de backend connectés à l’instance.
Depuis une autre fenêtre lancer la commande suivante. Elle va ouvrir une nouvelle connexion.
Regarder comment évolue le nombre de backend connectés à l’instance.
Retrouver le nombre de fois où un ordre
CHECKPOINTa été demandé.
Vérifier que les instances du
Clustersoient bien réparties sur votre cluster Kubernetes.
But : Créer une métrique personnalisée.
Les métrique récupérées couvrent déjà un spectre très large de notre instance PostgreSQL. Vous aurez certainement besoin d’ajouter de nouvelles métriques, quelles soient liées au métier ou plus techniques pour diagnostiques des problèmes. Regardons comment faire cela.
Le but est de rajouter la métrique :
last-analyze-foo : qui renvoie la date du dernier
passage d’un ANALYZE sur la table foo;Créer d’abord la table
foodans votre instance.
kubectl cnpg psql cluster -- -c "CREATE TABLE foo (i int); INSERT INTO foo SELECT FROM generate_series (1,250);"
CREATE TABLE
INSERT 0 250
Créer le fichier
~/configmap.yamlavec le contenu suivant :
---
apiVersion: v1
kind: ConfigMap
metadata:
name: nouvelles-metriques
namespace: default
labels:
cnpg.io/reload: ""
data:
custom-queries: |
analyze-foo:
query: "SELECT last_analyze FROM pg_stat_user_tables WHERE relname = 'foo';"
metrics:
- last_analyze:
usage: "GAUGE"
description: "Last foo's ANALYZE"Puis créer cette nouvelle ressource
ConfigMap.
Ajuster la définition du
Clusteren rajoutant la partiespec.monitoringsuivante dans~/cluster.yaml:
spec
[…]
monitoring:
customQueriesConfigMap:
- name: nouvelles-metriques # nom de la ConfigMap
key: custom-queriesAppliquer cette modification au
Cluster.
Récupérer les nouvelles métriques sur
fooaveccurletgrep. Que remarquez-vous ?
Exécuter un ordre
ANALYZEsur la tablefoo.
Retrouver la valeur du
last_analyze.
But : Simuler un décrochage d’une instance secondaire, repérer les indices associés et enfin la reconstruire.
Dans ce TP, nous allons utiliser trois instances déployées pour un
même Cluster. La définition à utiliser est la suivante
:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: integration
spec:
instances: 3
storage:
size: 10Gi
postgresql:
parameters:
max_slot_wal_keep_size: '1GB'Nous reviendrons sur le paramètre max_slot_wal_keep_size
plus tard dans le TP.
Créer le fichier
cluster-integration.yamlet créer leClusterà partir de la définition précédente.
Créer la table
utilisateurset ajouter quelques lignes avec les ordres suivants :
CREATE TABLE utilisateurs( id INT GENERATED ALWAYS AS IDENTITY, nom text NOT NULL);
INSERT INTO utilisateurs(nom) SELECT 'user ' || n name FROM generate_series(1,50) n;Récupérer les informations sur les réplications en place.
Les instances souffrent elles d’un retard de réplication ?
Arrêter le service
postmasterde l’instanceintegration-3.
Retrouver la taille sur disque des journaux de transaction sur l’instance primaire.
La requête suivante peut être utilisée à cet effet :
Insérer 1 million de lignes dans la table
utilisateurs.
Relever une nouvelle fois la taille sur disque des journaux de transaction sur l’instance primaire.
Cette volumétrie de WAL n’a pas été appliquée sur l’instance
integration-3comme le processuspostmastery est arrêté. Pourquoi reste-t-elle présente sur le primaire ?
Retrouver les informations du slot de réplication
_cnpg_integration_3de l’instance primaire grâce à la vuepg_replication_slots.
Insérer cette fois-ci 10 millions de lignes dans la table
utilisateurs.
Relever une nouvelle fois la taille sur disque des journaux de transaction sur l’instance primaire.
Qu’en est-il du slot de réplication ? Des changements ont-ils eu lieu ?
Qu’est-ce que cela implique pour
integration-3?
Sortir l’instance
integration-3du fencing et regarder ses traces.
L’instance se trouve dans un état instable et est surtout
inutilisable en cas de bascule. Dans cette situation, la seule solution
envisageable est de reconstruire entièrement l’instance
integration-3.
Petite précision, c’est la seule solution envisageable dans ce
contexte précis. Avec un Cluster configuré pour archiver
ses journaux sur un emplacement tiers, le secondaire aurait pu retrouver
les journaux manquant en basculant en mode Log Shipping. Cette
bascule sur ce mode réplication est un mécanisme propre à PostgreSQL, et
non CloudNativePG.
Supprimer le
Podintegration-3. Est-ce que cela résout le souci ?
Supprimer toutes les ressources liées à l’instance
integration-3.
But : Récupérer les métriques exportées et créer des métriques personnalisées.
Créer un fichier
~/cluster.yamlqui définit unClusteravec une seule instance dans la dernière version de PostgreSQL disponible.
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster
spec:
instances: 1
storage:
size: 1GiCréer cette ressource.
kubectl apply -f ~/cluster.yaml
Exposer localement le port
9187de l’Exporter Prometheus de l’instance primaire.
kubectl port-forward pod/cluster-1 9187:9187 &
Forwarding from 127.0.0.1:9187 -> 9187
Forwarding from [::1]:9187 -> 9187
Récupérer toutes les métriques disponibles à l’aide de
curl -s. Ils sont accessibles sur/metrics.
curl -s http://localhost:9187/metrics
[…]
# HELP go_sched_gomaxprocs_threads The current runtime.GOMAXPROCS setting, or the number of operating system threads that can execute user-level Go code simultaneously. Sourced from /sched/gomaxprocs:threads.
# TYPE go_sched_gomaxprocs_threads gauge
go_sched_gomaxprocs_threads 12
# HELP go_threads Number of OS threads created.
# TYPE go_threads gauge
go_threads 18
Retrouver le nombre de backend connectés à l’instance.
La métrique qui nous intéresse est nommé
cnpg_backends_total.
curl -s http://localhost:9187/metrics | grep cnpg_backends_total
# HELP cnpg_backends_total Number of backends
# TYPE cnpg_backends_total gauge
cnpg_backends_total{application_name="cnpg_metrics_exporter",datname="app",state="active",usename="postgres"} 1
Il y aurait donc une seule connexion à notre instance.
Depuis une autre fenêtre connectez-vous à votre instance.
Le plus simple est d’utiliser la commande suivante :
kubectl cnpg psql cluster.
psql (18.1 (Debian 18.1-1.pgdg13+2))
Type "help" for help.
postgres=#
Regarder comment évolue le nombre de backend connectés à l’instance.
Vous devriez voir apparaître une nouvelle ligne avec le paramètre
application_name à psql.
curl -s http://localhost:9187/metrics | grep cnpg_backends_total
# HELP cnpg_backends_total Number of backends
# TYPE cnpg_backends_total gauge
cnpg_backends_total{application_name="cnpg_metrics_exporter",datname="app",state="active",usename="postgres"} 1
cnpg_backends_total{application_name="psql",datname="postgres",state="idle",usename="postgres"} 1
Si ce n’est pas tout de suite le cas, attendez et recommencez un tout petit peut plus tard. Comme l’indique la documentation, il y a un mécanisme de cache pour les requêtes. La durée du cache peut être modifiée.
By default, the outputs of monitoring queries are cached for thirty seconds. This is done to enhance resource efficiency and to avoid PostgreSQL to run monitoring queries every time the prometheus endpoint is scraped.
Retrouver le nombre de fois où un ordre
CHECKPOINTa été demandé.
La métrique cnpg_pg_stat_checkpointer_checkpoints_req
vous donnera cette information.
curl -s http://localhost:9187/metrics | grep cnpg_pg_stat_checkpointer_checkpoints_req
# HELP cnpg_pg_stat_checkpointer_checkpoints_req Number of requested checkpoints that have been performed
# TYPE cnpg_pg_stat_checkpointer_checkpoints_req counter
cnpg_pg_stat_checkpointer_checkpoints_req 2
Si vous avez toujours votre session en cours, faites le test de
lancer des ordres CHECKPOINT.
Vérifier que les instances du
Clustersoient bien réparties sur votre cluster Kubernetes.
La métrique cnpg_collector_nodes_used vous donnera cette
information.
curl -s http://localhost:9187/metrics | grep cnpg_collector_nodes_used
# HELP cnpg_collector_nodes_used NodesUsed represents the count of distinct nodes accommodating the instances. A value of '-1' suggests that the metric is not available. A value of '1' suggests that all instances are hosted on a single node, implying the absence of High Availability (HA). Ideally this value should match the number of instances in the cluster.
# TYPE cnpg_collector_nodes_used gauge
cnpg_collector_nodes_used 1
La description de cette métrique est assez limpide. Ici nous avons une seule instance, la valeur de 1 est donc valide. Faites le test avec plusieurs instances et regarder l’évolution de la métrique en fonction du nombre d’instances et du nombre de nœuds dans votre cluster Kubernetes.
Les métrique récupérées couvrent déjà un spectre très large de notre instance PostgreSQL. Vous aurez certainement besoin d’ajouter de nouvelles métriques, quelles soient liées au métier ou plus techniques pour diagnostiques des problèmes. Regardons comment faire cela.
Le but est de rajouter la métrique :
last-analyze-foo : qui renvoie la date du dernier
passage d’un ANALYZE sur la table foo;Créer d’abord la table
foodans votre instance.
kubectl cnpg psql cluster -- -d app -c "CREATE TABLE foo (i int); INSERT INTO foo SELECT FROM generate_series (1,250);"
CREATE TABLE
INSERT 0 250
Créer le fichier
~/configmap.yamlavec le contenu suivant :
---
apiVersion: v1
kind: ConfigMap
metadata:
name: nouvelles-metriques
namespace: default
labels:
cnpg.io/reload: ""
data:
custom-queries: |
analyze-foo:
query: "SELECT last_analyze FROM pg_stat_user_tables WHERE relname = 'foo';"
metrics:
- last_analyze:
usage: "GAUGE"
description: "Last foo's ANALYZE"Puis créer cette nouvelle ressource
ConfigMap.
kubectl apply -f ~/configmap.yaml
Ajuster la définition du
Clusteren rajoutant la partiespec.monitoringsuivante dans~/cluster.yaml:
spec
[…]
monitoring:
customQueriesConfigMap:
- name: nouvelles-metriques # ConfigMap
key: custom-queriesAppliquer cette modification au
Cluster.
kubectl apply -f ~/cluster.yaml
Récupérer les nouvelles métriques sur
fooaveccurletgrep. Que remarquez-vous ?
curl -s http://localhost:9187/metrics | grep foo
# HELP cnpg_analyze_foo_last_analyze Last foo's ANALYZE
# TYPE cnpg_analyze_foo_last_analyze gauge
cnpg_analyze_foo_last_analyze NaN
La requête s’est bien exécutée mais aucune valeur n’est remontée
(Nan).
Exécuter un ordre
ANALYZEsur la tablefoo.
kubectl cnpg psql cluster -- -d app -c "ANALYZE foo"
ANALYZE
Retrouver la valeur du
last_analyze.
curl -s http://localhost:9187/metrics | grep foo
# HELP cnpg_analyze_foo_last_analyze Last foo's ANALYZE
# TYPE cnpg_analyze_foo_last_analyze gauge
cnpg_analyze_foo_last_analyze 1.769702472e+0
But : Simuler un décrochage d’une instance secondaire, repérer les indices associés et enfin la reconstruire.
Dans ce TP, nous allons utiliser trois instances déployées pour un
même Cluster. La définition à utiliser est la suivante
:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: integration
spec:
instances: 3
storage:
size: 10Gi
postgresql:
parameters:
max_slot_wal_keep_size: '1GB'Nous reviendrons sur le paramètre max_slot_wal_keep_size
plus tard dans le TP.
Créer le fichier
cluster-integration.yamlet créer leClusterà partir de la définition précédente.
vim cluster-integration.yaml
kubectl apply -f cluster-integration.yaml
Attendez que les trois instances soient à l’état
Running.
kubectl get pod | grep integration
integration-1 1/1 Running 0 84s
integration-2 1/1 Running 0 61s
integration-3 1/1 Running 0 40s
Créer la table
utilisateurset ajouter quelques lignes avec les ordres suivants :
CREATE TABLE utilisateurs( id INT GENERATED ALWAYS AS IDENTITY, nom text NOT NULL);
INSERT INTO utilisateurs(nom) SELECT 'user ' || n name FROM generate_series(1,50) n;Vous pouvez vous connecter au primaire avec la ligne de commande suivante :
kubectl cnpg psql integration
psql (18.3 (Debian 18.3-1.pgdg13+1))
Type "help" for help.
postgres=# CREATE TABLE utilisateurs( id INT GENERATED ALWAYS AS IDENTITY, nom text NOT NULL);
INSERT INTO utilisateurs(nom) SELECT 'user ' || n name FROM generate_series(1,50) n;
CREATE TABLE
INSERT 0 50
Récupérer les informations sur les réplications en place.
Le plus simple est de récupérer ces informations avec la commande
status du plugin cnpg.
kubectl cnpg status integration
Cluster Summary
Name default/integration
System ID: 7654873992545370143
PostgreSQL Image: ghcr.io/cloudnative-pg/postgresql:18.3-system-trixie
Primary instance: integration-1
Primary promotion time: 2026-06-24 08:20:46 +0000 UTC (1m40s)
Status: Cluster in healthy state
Instances: 3
Ready instances: 3
Size: 128M
Current Write LSN: 0/602E428 (Timeline: 1 - WAL File: 000000010000000000000006)
Continuous Backup not configured
Streaming Replication status
Replication Slots Enabled
Name Sent LSN Write LSN Flush LSN Replay LSN Write Lag Flush Lag Replay Lag State Sync State Sync Priority Replication Slot
---- -------- --------- --------- ---------- --------- --------- ---------- ----- ---------- ------------- ----------------
integration-2 0/602E428 0/602E428 0/602E428 0/602E428 00:00:00 00:00:00 00:00:00 streaming async 0 active
integration-3 0/602E428 0/602E428 0/602E428 0/602E428 00:00:00 00:00:00 00:00:00 streaming async 0 active
Instances status
Name Current LSN Replication role Status QoS Manager Version Node
---- ----------- ---------------- ------ --- --------------- ----
integration-1 0/602E428 Primary OK BestEffort 1.29.1 kind-control-plane
integration-2 0/602E428 Standby (async) OK BestEffort 1.29.1 kind-control-plane
integration-3 0/602E428 Standby (async) OK BestEffort 1.29.1 kind-control-plane
Les instances souffrent elles d’un retard de réplication ?
Pas du tout. Dans la section
Streaming Replication status, la colonne
Replay Lag est à 00:00:00 pour les deux
instances.
Il est également possible de le voir avec la colonne
Current LSN de la section
Instances status.
Arrêter le service
postmasterde l’instanceintegration-3.
Cette opération peut être faite avec la commande
fencing. L’idée ici est de simuler un décrochage en
arrêtant le service. Dans un contexte plus réel, on pourrait imaginer un
problème réseau ou encore un arrêt brutal d’un Node.
kubectl cnpg fencing on integration 3
integration-3 fenced
Retrouver la taille sur disque des journaux de transaction sur l’instance primaire.
La requête suivante peut être utilisée à cet effet :
kubectl cnpg psql integration
psql (18.3 (Debian 18.3-1.pgdg13+1))
Type "help" for help.
postgres=# select pg_size_pretty(sum(size)) from pg_ls_waldir();
pg_size_pretty
----------------
96 MB
(1 row)
Insérer 1 million de lignes dans la table
utilisateurs.
INSERT 0 1000000
Relever une nouvelle fois la taille sur disque des journaux de transaction sur l’instance primaire.
postgres=# SELECT pg_size_pretty(sum(size)) FROM pg_ls_waldir();
pg_size_pretty
----------------
160 MB
(1 row)
Cette volumétrie de WAL n’a pas été appliquée sur l’instance
integration-3comme le processuspostmastery est arrêté. Pourquoi reste-t-elle présente sur le primaire ?
Le slot de réplication créé automatiquement par CloudNativePG explique cela. En effet, un slot de réplication est un objet PostgreSQL qui permet de conserver les journaux nécessaires à une réplication si celle-ci est arrêtée (panne réseau, arrêt d’un service).
Par défaut, il n’y a aucune limite de taille sur la quantité de
journaux à conserver. Dans notre exemple, le paramètre
max_slot_wal_keep_size a été positionné à 1 Go. L’instance
primaire conserve donc l’équivalent de 1 Go de journaux pour tous les
slots de réplication. Au delà de ce seuil, les journaux seront supprimés
pour éviter que l’instance primaire ne sature.
Retrouver les informations du slot de réplication
_cnpg_integration_3de l’instance primaire grâce à la vuepg_replication_slots.
postgres=# SELECT * FROM pg_replication_slots WHERE slot_name = '_cnpg_integration_3'\gx
-[ RECORD 1 ]-------+------------------------------
slot_name | _cnpg_integration_3
plugin |
slot_type | physical
datoid |
database |
temporary | f
active | f
active_pid |
xmin |
catalog_xmin |
restart_lsn | 0/602E428
confirmed_flush_lsn |
wal_status | reserved
safe_wal_size | 1014612016
two_phase | f
two_phase_at |
inactive_since | 2026-06-24 08:38:06.029758+00
conflicting |
invalidation_reason |
failover | f
synced | f
La documentation explique longuement tous les paramètres, voir https://www.postgresql.org/docs/current/view-pg-replication-slots.html. Intéressons nous à quelques paramètres seulement :
active : à false (f), cela
indique qu’il n’est pas utilisé.restart_lsn : permet de savoir à quel emplacement dans
la timeline il se trouvait lorsque le slot a été
invalidé. 0/602E428 correspond bien à la valeur du
current_lsn dans la sortie du plugin.wal_status : le mot reserved indique que
tous les WAL nécessaires à ce slot de réplication se trouvent bien
présents sur le primairesafe_wal_size : la quantité de journaux qui peut être
encore écrite sans que ce slot ne devienne inutilisable.inactive_since : depuis quand le slot est inutilisé.
Dans notre TP, il s’agit de l’horodatage du fencing de
l’instance integration-3.Insérer cette fois-ci 10 millions de lignes dans la table
utilisateurs.
postgres=# INSERT INTO utilisateurs(nom) SELECT 'user ' || n name FROM generate_series(1,10_000_000) n;
INSERT 0 10000000
Relever une nouvelle fois la taille sur disque des journaux de transaction sur l’instance primaire.
postgres=# SELECT pg_size_pretty(sum(size)) FROM pg_ls_waldir();
pg_size_pretty
----------------
1056 MB
(1 row)
Qu’en est-il du slot de réplication ? Des changements ont-ils eu lieu ?
postgres=# SELECT * FROM pg_replication_slots WHERE slot_name = '_cnpg_integration_3'\gx
-[ RECORD 1 ]-------+------------------------------
slot_name | _cnpg_integration_3
plugin |
slot_type | physical
datoid |
database |
temporary | f
active | f
active_pid |
xmin |
catalog_xmin |
restart_lsn |
confirmed_flush_lsn |
wal_status | lost
safe_wal_size |
two_phase | f
two_phase_at |
inactive_since | 2026-06-24 08:38:06.029758+00
conflicting |
invalidation_reason | wal_removed
failover | f
synced | f
Des changements ont effectivement eu lieu avec notamment le champ
restart_lsn qui n’a plus de valeur et le champ
wal_status qui est passé à lost. Cela signifie
que ce qui était nécessaire à ce slot (et donc à l’instance
integration-3) n’est plus disponible : les journaux ont été
supprimés. C’est d’ailleurs cette raison qui est indiquée dans le champ
invalidation_reason.
Pourquoi ? Les dernières insertions de ligne ont généré un volume de
journaux qui a dépassé la limite configurée. PostgreSQL a donc commencé
à supprimer les journaux les plus anciens (à commencer par celui où se
trouvait le segment 0/602E428), rendant le slot
inopérant.
Qu’est-ce que cela implique pour
integration-3?
Cette instance secondaire a décroché. Elle n’est en l’état pas capable de se reconnecter au primaire comme des journaux vont lui manquer.
Sortir l’instance
integration-3du fencing et regarder ses traces.
kubectl cnpg fencing off integration 3
kubectl logs -f integration-3 | jq
Le redémarrage de l’instance est déclenché et elle entre donc dans le mode standby.
{
"level": "info",
"ts": "2026-06-24T09:28:43.950809781Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "integration-3",
"record": {
"log_time": "2026-06-24 09:28:43.950 UTC",
"user_name": "postgres",
"database_name": "postgres",
"process_id": "239",
"connection_from": "[local]",
"session_id": "6a3ba34b.ef",
"session_line_num": "1",
"session_start_time": "2026-06-24 09:28:43 UTC",
"transaction_id": "0",
"error_severity": "FATAL",
"sql_state_code": "57P03",
"message": "the database system is starting up",
"backend_type": "client backend",
"query_id": "0"
}
}
{
"level": "info",
"ts": "2026-06-24T09:28:44.074692668Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "integration-3",
"record": {
"log_time": "2026-06-24 09:28:44.074 UTC",
"process_id": "215",
"session_id": "6a3ba34b.d7",
"session_line_num": "2",
"session_start_time": "2026-06-24 09:28:43 UTC",
"transaction_id": "0",
"error_severity": "LOG",
"sql_state_code": "00000",
"message": "entering standby mode",
"backend_type": "startup",
"query_id": "0"
}
}Un premier point de consistance est trouvé. Dans le champ
message, on retrouve bien le même segment que celui au
moment du fencing de l’instance : 0/602E428.
{
"level": "info",
"ts": "2026-06-24T09:28:44.253241733Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "integration-3",
"record": {
"log_time": "2026-06-24 09:28:44.252 UTC",
"process_id": "215",
"session_id": "6a3ba34b.d7",
"session_line_num": "4",
"session_start_time": "2026-06-24 09:28:43 UTC",
"virtual_transaction_id": "165/0",
"transaction_id": "0",
"error_severity": "LOG",
"sql_state_code": "00000",
"message": "consistent recovery state reached at 0/602E428",
"backend_type": "startup",
"query_id": "0"
}
}Automatiquement, PostgreSQL essaye de se reconnecter au primaire avec le mécanisme de réplication par flux avec l’utilisation du slot de réplication. Il se rend malheureusement compte que le slot est invalide, en donne la raison, et boucle sur des redémarrages.
{
"level": "info",
"ts": "2026-06-24T09:28:44.279240957Z",
"logger": "postgres",
"msg": "record",
"logging_pod": "integration-3",
"record": {
"log_time": "2026-06-24 09:28:44.278 UTC",
"process_id": "254",
"session_id": "6a3ba34c.fe",
"session_line_num": "1",
"session_start_time": "2026-06-24 09:28:44 UTC",
"transaction_id": "0",
"error_severity": "FATAL",
"sql_state_code": "08P01",
"message": "could not start WAL streaming: ERROR: can no longer access replication slot \"_cnpg_integration_3\"\nDETAIL: This replication slot has been invalidated due to \"wal_removed\".",
"backend_type": "walreceiver",
"query_id": "0"
}
}L’instance se trouve dans un état instable et est surtout
inutilisable en cas de bascule. Dans cette situation, la seule solution
envisageable est de reconstruire entièrement l’instance
integration-3.
Petite précision, c’est la seule solution envisageable dans ce
contexte précis. Avec un Cluster configuré pour archiver
ses journaux sur un emplacement tiers, le secondaire aurait pu retrouver
les journaux manquant en basculant en mode Log Shipping. Cette
bascule sur ce mode réplication est un mécanisme propre à PostgreSQL, et
non CloudNativePG.
Supprimer le
Podintegration-3. Est-ce que cela résout le souci ?
kubectl delete pod integration-3
La suppression du Pod ne changera rien car cette
opération n’a pas d’influence sur les données associées à ce
Pod. Le problème se situe bien au niveau des données de
integration-3 qui ne sont plus en phase avec l’instance
primaire et, de plus, le rejeu des journaux n’est plus possible car
supprimés.
Supprimer toutes les ressources liées à l’instance
integration-3.
PODNAME=integration-3
VOLNAME=$(kubectl get pv -o json | \
jq -r '.items[]|select(.spec.claimRef.name=='\"$PODNAME\"')|.metadata.name')
kubectl delete pod/$PODNAME pvc/$PODNAME pvc/$PODNAME-wal pv/$VOLNAME
La suppression de toutes les ressources va être captée par
l’opérateur et, comme le nombre d’instances (2) est maintenant différent
de la demande initiale (3), un nouveau Pod va être créé en
suivant le mécanisme classique, c’est à dire la création d’un nouveau
Pod avec une étape de join ou une copie des
données de l’instance primaire sera effectuée.
Le déploiement de PostgreSQL dans Kubernetes a des conséquences à bien avoir en tête. Celles-ci ne sont pas insurmontables. Il faut juste en avoir conscience.
La gestion des versions se voit quelques peu complexifiée avec un suivi régulier qui doit être faite avec assiduité.
Un bon alignement des versions de PostgreSQL, de l’opérateur et de Kubernetes doit être respecté, enfin si vous souhaitez travailler avec des versions supportées.
Commençons par PostgreSQL. Une nouvelle version majeure est publiée chaque année, aux alentours de septembre. On parle de Major Release. Des versions mineures, où Minor Release sont publiées tous les trimestres, sans compter les versions mineures exceptionnelles pour des correctifs de sécurité. Lorsqu’une nouvelle version est disponible, la plus ancienne est retirée du support. Un total de cinq versions sont supportées en même temps par le projet PGDG.
Le projet CloudNativePG fait en sorte de proposer les images des nouvelles versions très rapidement après la mise à disposition sur le dépôt du PGPD. Généralement, en quelques semaines, les nouvelles images sont disponibles.
Notez que le projet CloudNativePG ne crée et propose des images que pour les versions de PostgreSQL supportées par le PGDG. Autrement dit, si vous souhaitez déployer une version 13 de PostgreSQL en 2026, l’opérateur ne vous le permettra pas.
CloudNativePG connaît un cycle de release assez rapide. Une nouvelle version majeure de l’opérateur est publiée tous les 6 mois. Il n’y a que deux versions majeures supportées en même temps. Autrement dit, si vous souhaitez avoir votre opérateur supporté par le projet, il vous faudra le mettre à jour au moins une fois par an.
Des versions mineures sont aussi proposées tous les deux mois (environ). Ces versions mineures apportent des correctifs fonctionnels et de sécurité.
Enfin, Kubernetes doit lui aussi être mis à jour. 3 versions sont
supportées en même temps. Chaque version mineure a une durée de vie de 3
mois environ. La montée de versions des Nodes imposent que
les Pods soient évincés d’un Node et
redéployés sur un autre Node le temps de la mise à jour.
L’utilisation du mécanisme de bascule automatique facilite cette
opération.
La fréquence des différentes releases est assez élevée, et sachez que, pour chacune des interruptions, des arrêts et relances de PostgreSQL sont nécessaires. Il faut donc s’attendre à des interruptions de service, plus ou moins longs. Vos applications devraient être en capacité de retenter l’exécution de leurs requêtes si elles venaient à perdre leur connexions à la base. Cette problématique est aussi présente pour des infrastructures virtualisées ou physiques.
L’ajout d’outils intermédiaires renforce cette problématique.
Nous allons aborder les différentes montées de version que vous allez rencontrer en déployant PostgreSQL sur Kubernetes, que ce soit celles de l’opérateur, des instances ou de Kubernetes lui même.
Des notions avancées sur Kubernetes seront évoquées concernant les montées de versions comme l’opérateur les intègre.
Sources : page Wikipédia de PostgreSQL et PostgreSQL Versioning Policy
imageName du Clusterimages du ImageCatalog ou
ClusterImageCatalogUne montée de version consiste à la mise à jour des binaires de
PostgreSQL. En mode conteneur, et donc sur Kubernetes, il n’est pas
envisageable de les mettre à jour via apt ou
yum. Cela se fait en modifiant l’image utilisée dans la
définition YAML de votre Cluster PostgreSQL.
Une nouvelle image contenant la nouvelle version souhaitée sera alors
téléchargée.
Les montées de version se font de manières déclarative. On laisse
faire l’opérateur après lui avoir donné l’ordre de mettre à jour notre
Cluster. La mise à jour se fera selon le mécanisme bien
connu du Rolling Update. Il sera détaillé par la suite.
Un ordre de montée de version est donnée en modifiant le paramètre
imageName d’une ressource Cluster ou en
modifiant l’image utilisée dans les ressources ImageCatalog
ou ClusterImageCatalog. Nous prendrons le temps de
détailler ceci plus tard.
Il faut distinguer deux types de montées de version pour PostgreSQL :
Le déroulé d’une montée de version est totalement différent entre ces deux types. CloudNativePG fait en plus quelques opérations supplémentaires de manière transparente. en détails ces deux types de montées de version.
spec:
- imageName: ghcr.io/cloudnative-pg/postgresql:18.0-standard-trixie
+ imageName: ghcr.io/cloudnative-pg/postgresql:18.1-standard-trixieImageCatalog / ClusterImageCatalogLa modification du nom de l’image entraîne une montée de version de
toutes les instances du Cluster.
Cette montée de version se fera en mode Rolling Update. Les instances secondaires seront les premières à être mises à jour, une par une. L’instance primaire sera la dernière à être mise à jour.
Nous verrons un peu plus loin comment gérer la mise à jour de l’instance primaire, avec la stratégie de mise à jour.
Lors d’une mise à jour, les nouveaux binaires doivent être
téléchargés. Ici, il s’agit de la nouvelle version de l’image. L’image
va être téléchargée sur tous les nœuds où se trouvent des instances du
Cluster mis à jour. Si vous avez un Cluster
trois nœuds, elle sera téléchargée trois fois si vos instances sont
réparties sur des nœuds distincts.
pg_dump/pg_restorepg_upgradeChanger de version majeure de PostgreSQL est un peu moins trivial qu’une montée de version mineure. Des changements structurels peuvent avoir lieu dans les nouvelles versions et le passage dans une nouvelle version majeure ne peut pas se faire par une simple installation de binaires.
L’opérateur vous permet de mettre à jour vos instances facilement. Mais sachez que les différentes méthodes présentées sont, en faites, toutes possibles sur des environnements virtualisés.
Il existe trois méthodes pour y parvenir :
pg_dump / pg_restore :
pg_upgrade : Qui sera exécuté par
l’opérateur lors de la procédure de mise à jour ;Publications et
Sbscriptions. Ce sont, avant tout des concepts et objets
PostgreSQL. L’opérateur permet de les créer déclarativement. Ces objets
sont utiles pour une réplication logique.Dans la suite de la formation ces méthodes ne seront présentées que dans le contextes de CloudNativePG et les fonctionnalités qu’il propose.
Chaque méthodes a ses avantages et inconvénients. Toutes trois sont supportées par CloudNativePG (comprendre qu’il est possible de les déclenchée de manière déclarative).
Depuis la version 1.26 de l’opérateur, il est possible de faire des In-Place Major Upgrade qui permet de mettre à jour une instance sans devoir en recréer d’autres avant.
Depuis la version 1.26, il est désormais possible de changer de
version majeure d’un Cluster de manière déclarative. Comme
pour une montée de version mineure, un changement du
imageName ou de l’image utilisée ImageCatalog
/ ClusterImageCatalog déclenche la montée de version
majeure.
Cette méthode va tout d’abord arrêter tous les Pods pour
ne garder que celui de l’instance primaire. Les ressources
PV et PVC sont quant à elles conservées. La
nouvelle image avec la nouvelle version PostgreSQL va être téléchargée.
Un job d’upgrade va être déclenché et utilisera l’outil
pg_upgrade, bien connu des DBAs, pour effectuer la
migration. L’option
--link est notamment utilisée pour accélérer le
traitement.
Si tout se déroule correctement, les ressources PV et
PVC des instances secondaires seront supprimées. Deux
nouvelles instance seront alors recréées à partir de l’instance
primaire. Avec cette méthode, les noms des ressources
Kubernetes, notamment Cluster et
Services sont conservés.
Comme les données ne sont pas réécrites mais « simplement » déplacées, assurez-vous que les images utilisées se basent sur les mêmes systèmes d’exploitation. Assurez-vous également que les extensions, que vous utilisez, soient bien présentes et supportées pour la nouvelle version de PostgreSQL.
Cette méthode ne permet pas un retour arrière facile. Lancez une sauvegarde et assurez-vous de sa bonne exécution avant de lancer l’opération de mise à jour.
pg_dump/pg_restorebootstrap.initdb.importmicroservice ou monolithCluster
externalClusters dans sa définition YAMLL’idée de cette méthode est de profiter du mécanisme d’import lors de
l’étape d’initdb d’un nouveau Cluster. Cela
permet d’importer les données d’une base en version N (source), dans une
nouvelle base en version N+1 (destination). Le nouveau
Cluster est créé à partir d’une base déjà existante. Cette
méthode utilisent les outils pg_dump et
pg_restore.
L’intérêt est que cette méthode ne touche pas à l’instance source ni à la (ou les) base qui doit être importée. En cas de problème lors de l’import, vous pourrez arrêter le processus et réessayer après correction du problème. La migration peut se faire à chaud, mais aucune activité ne doit se faire sur la base source pour assurer la cohérence des données.
C’est aussi une méthode qui vous permet de migrer des bases dans Kubernetes, relativement simplement.
Un inconvénient notable est le fait que cette méthode implique la
création d’une nouvelle ressource Cluster. Ainsi, le nom du
Cluster aura changé… et donc les Services
également. Elle nécessite, relativement aux autres, beaucoup d’espace
disque. Aussi, si les données de la ou les bases sont bien rapatriées,
les objets globaux ne le seront pas forcément, tout comme la
configuration qui doit être reportée dans le
nouveau Cluster.
monolith
microservice
pg_dump -Fd
PGDATADeux méthodes d’import existent :
microservice ;monolith.La première méthode ne vous permet d’importer les données que d’une
seule base. Ses données seront importées dans la base par défaut de
l’instance (donc app). Si vous souhaitez conserver le même
nom de base, n’oubliez pas de modifier le paramètre
spec.bootstrap.initdb.database de la définition du
Cluster. Cette méthode d’import est intéressante si vous
souhaitez diviser une instance multi-bases en plusieurs instances
mono-base.
La seconde méthode vous permet d’importer autant de bases que vous le
souhaitez ainsi que des rôles PostgreSQL qui seraient existant dans
l’instance source. C’est très pratique si vous souhaitez faire une
migration complète de votre instance. Petite astuce, vous pouvez
utiliser le caractère wildcard * pour signifier
que vous voulez importer toutes les bases ou tous les rôles.
Quelque soit la méthode utilisée, l’utilitaire pg_dump
est utilisé pour récupérer les données de la base et créer un
dump dans le volume associé à la nouvelle instance. Attention
donc à l’espace de stockage lors du déclenchement de l’import.
spec.externalClusters
Pour utiliser cette méthodes, il faut renseigner la partie
spec.bootstrap.initdb.import de la ressource
Cluster et indiquer où se trouve l’instance où se trouve la
ou les bases à importer. Ceci est fait par l’utilisation du mot clé
source qui indique le nom d’un
externalCluster. Ce dernier doit être renseigné dans la
liste spec.externalClusters, en fin de fichier par
exemple.
Toutes les informations seront utilisées par l’opérateur pour
exécuter pg_dump vers cette source. Évidemment, l’accès
réseau doit être possible (ouvertures réseau,
pg_hba.conf…), le user doit avoir le droit se
connecter à la base source, etc.
Custom Resource Definitionsinstance-managerCLUSTERS_ROLLOUT_DELAYINSTANCES_ROLLOUT_DELAYLa mise à jour de l’opérateur se fait en plusieurs temps et impacte les instances PostgreSQL qu’il gère. Le principe de mise à jour est simple, il suffit d’appliquer les nouveaux manifestes de l’opérateur sur Kubernetes. Si vous avez installé l’opérateur avec Helm, mettez le à jour avec Helm. Même chose pour les autres méthodes de déploiement.
Ces nouveaux manifestes mettent à jour les
Custom Resource Definitions (comme Cluster,
ScheduledBackup, etc) et l’opérateur en tant que tel, c’est
à dire le Pod qui contient le controller.
Cette première étape terminée, la seconde va être automatiquement
déclenchée. Elle consiste à la mise à jour de tous les Pods
des instances PostgreSQL déployées. En effet, il existe dans le
Pod de l’instance, un composant appelé
instance-manager. Celui-ci est étroitement lié au
controller CloudNativePG. Ils doivent être dans la même
version. La mise à jour des instances suit le modèle de Rolling
Update que nous verrons plus tard lorsque nous évoquerons la montée de version
mineure d’une instance PostgreSQL.
Par défaut toutes les instances gérées par l’opérateur seront mises à jour en même temps. Ceci peut poser problème si vous avez de très nombreuses instances. Il est possible de répartir ces redémarrages dans le temps avec les paramètres :
CLUSTERS_ROLLOUT_DELAY : permet de définir un délai
entre le redémarrage de deux Cluster différents. Par défaut
positionné à 0 ;INSTANCES_ROLLOUT_DELAY : permet de définir un délai
entre le redémarrage de deux instances différentes qui font partie du
même Cluster. Par défaut positionné à 0 ;Ces paramètres là sont à définir dans le Configmap de
configuration de l’opérateur, à savoir le Configmap
cnpg-controller-manager-config qui doit être créé dans le
même Namespace que l’opérateur.
Il existe une méthode pour éviter ce comportement (redémarrage des
Pods). Cependant elle ne garantit pas le critère immuable
que devrait suivre un Pod. À titre d’information, voici la
méthode à suivre pour y parvenir. Il faut modifier la configuration de
l’opérateur en passant le paramètre
ENABLE_INSTANCE_MANAGER_INPLACE_UPDATES à true
dans son ConfigMap. L’image du init-container
qui contient le manager ne sera pas mis à jour dans les
Pods.
Si il y a bien une chose à retenir, c’est qu’une mise à jour de
l’opérateur déclenche la mise à jour d’un composant au sein des
Pods PostgreSQL. Opération qui nécessite un redémarrage du
Pod. Aussi,la configuration
primaryUpdateStrategy est également prise en compte dans le
cadre d’une montée de version de l’opérateur.
primaryUpdateStrategy et
primaryUpdateMethod
La mise à jour de l’instance primaire peut se faire automatiquement
ou de manière supervisée selon le paramètre de
primaryUpdateStrategy qui peut prendre deux valeurs.
unsupervised : après que toutes les instances
secondaires aient été mises à jour, l’instance primaire est
automatiquement mise à jour. C’est la valeur par défaut ;supervised : le mécanisme de montée de version attend
une opération manuelle pour effectuer la montée de version de l’instance
primaire.En mode unsupervised, le paramètre
primaryUpdateMethod imagesest pris en compte pour savoir
comment procéder à la mise à jour de l’instance primaire. Il peut lui
aussi prendre deux valeurs :
restart : l’instance primaire est redémarrée. Il faudra
attendre la fin de la mise à jour pour que le primaire soit de nouveau
accessible. C’est la valeur par défaut ;switchover : une bascule sur un secondaire est
effectuée et le primaire devient secondaire. Le nouveau primaire est
déjà accessible comme il a déjà été mis à jour.En mode supervised, vous devrez donc vous même procéder
à ce redémarrage ou à ce switchover avec le plugin
cnpg de kubectl. Par exemple pour le
switchover vous pouvez utiliser la commande suivante pour
passer l’instance postgresql-2 en tant que nouvelle
primaire :
kubectl cnpg promote postgresql 2
Si au contraire, vous souhaitez procédre à un redémarrage du primaire
vous pouvez utiliser la commande restart du plugin.
kubectl cnpg restart postgresql 1
PodsPod Disruption Budget
Les mises à jour des nœuds (ou Node) finira par
s’imposer à vous. Durant cette opération, les Pods de vos
instances PostgreSQL, seront nécessairement évincés pour être redéployés
sur d’autres nœuds (on parle de drain). Un
drain marque le nœud comme unschedulable et évince
(comprendre détruit) les Pods.
Le drain d’un nœud contenant une instance primaire
déclenchera, grâce à l’opérateur, un switchover, rendant cette
instance une secondaire. L’éviction d’une telle instance n’a donc pas de
répercussion particulière, ce n’est pas la primaire ! Le
drain du nœud pourra se poursuivre.
kubectl describe pdb cluster-18-primary
Name: cluster-18-primary
Namespace: default
Min available: 1
Selector: cnpg.io/cluster=cluster-18,cnpg.io/instanceRole=primary
Status:
Allowed disruptions: 0
Current: 1
Desired: 1
Total: 1
Events: <none>
Une ressource PodDisruptionBudget est créée par défaut
pour chaque Cluster. Elle indique qu’elle est la politique
d’éviction des Pods que Kubernetes doit respecter en cas de
drain de nœud. Elle permet d’indiquer, à Kubernetes,
combien de Pods doivent être actifs lorsqu’une perturbation
volontaire à lieu, ici la montée de version d’un nœud.
Dans l’exemple du slide, un PodDisruptionBudget
est une ressource créée par l’opérateur pour le Cluster
cluster-18. Son nom est suffixé de -primary.
On comprend que la politique va concerner l’instance primaire.
Name : est le nom de cette ressource ;Namespace : le Namespace où elle se
trouve. Le même que le Cluster ;Min available : indique combien de Pods
respectant le Selector doivent exister ;Selector : la liste des labels que doit avoir
un Pod, il faut comprendre ici :
cnpg.io/instanceRole) du
Cluster cluster-18 (cnpg.io/cluster) ;Ce PDB impose donc qu’il y ait toujours une instance
primaire pour le Cluster. L’opérateur fera en sorte que ce
soit toujours le cas.
N’hésitez pas, c’est le moment !
La version en ligne des solutions de ces TP est disponible sur https://dali.bo/k5_solutions.
But : Effectuer une montée de version mineure de PostgreSQL.
La version de PostgreSQL est indiquée dans le nom et le tag de l’image déployée. La modification de celle-ci entraîne une montée de version de l’instance. Cette montée de version peut se faire automatiquement ou de manière supervisée (appelée “manuelle” dans la documentation).
Dans une seconde session SSH, lancer la commande
watch kubectl get podspour voir ce qu’il va se passer pendant la montée de version.
Créer un
Clustercluster-productionen version 17.5 composé d’une instance.
Lorsqu’il est
Running, modifier la version de PostgreSQL de17.5à17.6et appliquer la modification aveckubectl apply.
Le paramètre primaryUpdateStrategy permet de définir la
stratégie à suivre lors d’une mise à jour de l’instance primaire. Il est
positionné par défaut à unsupervised. C’est le comportement
que nous venons de voir dans l’exemple précédent.
Positionner le paramètre
spec.primaryUpdateStrategyàsupervised, modifier la version en la passant de17.6à17.7et tenter de faire la montée de version. Que constatez vous ?
Ajouter un secondaire à votre cluster PostgreSQL en modifiant la ligne
instancesdu fichiercluster-production.yamlet en appliquant la modification.
Vérifier les versions des deux instances. Que constatez-vous ?
Faite une bascule manuelle sur l’instance secondaire.
kubectl cnpg promote cluster-production cluster-production-2
But : Effectuer une montée de version mineure de PostgreSQL.
La version de PostgreSQL est indiquée dans le nom et le tag de l’image déployée. La modification de celle-ci entraîne une montée de version de l’instance. Cette montée de version peut se faire automatiquement ou de manière supervisée (appelée “manuelle” dans la documentation).
Dans une seconde session SSH, lancer la commande
watch kubectl get podspour voir ce qu’il va se passer pendant la montée de version.
Vous devriez avoir un rafraîchissement toutes les 2 secondes.
Créer un
Clustercluster-productionen version 17.5 composé d’une instance.
vim ~/cluster-production.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-production
spec:
imageName: ghcr.io/cloudnative-pg/postgresql:17.6-standard-bookworm
instances: 2
storage:
size: 1Gikubectl apply -f ~/cluster-production.yaml
Lorsqu’il est
Running, modifier la version de PostgreSQL de17.5à17.6et appliquer la modification aveckubectl apply.
kubectl apply -f ~/cluster-production.yaml
cluster.postgresql.cnpg.io/cluster-production configured
Le Pod de l’instance secondaire en version 17.5 est
supprimé. Le status du Pod passe à l’état
Terminating.
Un nouveau Pod est créé et la phase d’initialisation est
relancée. Le status du Pod passe donc à l’état
PodInitializing.
Le temps de télécharger l’image et de le redémarrer, il passe à
l’état Running.
Lorsque l’instance secondaire est mise à jour, le Pod de
l’instance primaire est à son tour mise à jour.
Un SELECT version(); indique bien que nous sommes bien
passés en version 17.6.
kubectl cnpg psql cluster-production -- -c 'SELECT version();'
version
-----------------------------------------------------------------------------------------------------------
-----------------
PostgreSQL 17.6 (Debian 17.6-2.pgdg12+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14+deb12u1) 12.2.0, 64-bit
(1 row)
Par défaut, un Cluster PostgreSQL est configuré de telle
sorte que la montée de version se fasse automatiquement, c’est-à-dire
que l’opérateur arrête puis redémarre l’instance tout seul. Voyons
maintenant le cas d’une montée de version en mode
supervised.
Le paramètre primaryUpdateStrategy permet de définir la
stratégie à suivre lors d’une mise à jour de l’instance primaire. Il est
positionné par défaut à unsupervised. C’est le comportement
que nous venons de voir dans l’exemple précédent.
Positionner le paramètre
spec.primaryUpdateStrategyàsupervised, modifier la version en la passant de17.6à17.7et tenter de faire la montée de version. Que constatez vous ?
spec:
imageName: ghcr.io/cloudnative-pg/postgresql:17.7-standard-bookworm
instances: 1
primaryUpdateStrategy: supervisedkubectl apply -f ~/cluster-production.yaml
Aïe…
The Cluster "cluster-production" is invalid: spec.primaryUpdateStrategy: Invalid value: "supervised": supervised update strategy is not allowed for clusters with a single instance
Nous venons de découvrir une première subtilité. Ce paramètre n’est en réalité pas utilisable avec une seule instance. En effet ce paramètre permet de contrôler la manière dont est redémarrée l’instance primaire après que toutes les instances secondaires aient été mis à jour. Ici, nous n’avons qu’une primaire de déployée.
Ceci nous impose donc de déployer un secondaire.
Ajouter un secondaire à votre cluster PostgreSQL en modifiant la ligne
instancesdu fichiercluster-production.yamlet en appliquant la modification.
Un premier Pod join va être créé puis le
Pod de l’instance secondaire sera finalement déployé. Nous
nous retrouvons donc avec deux Pods reprenant le nom du
Cluster, incrémentés de 1.
kubectl get pod
NAME READY STATUS RESTARTS AGE
cluster-production-1 2/2 Running 0 6m16s
cluster-production-2-join-7ckvl 0/1 Pending 0 5s
kubectl get pod
NAME READY STATUS RESTARTS AGE
cluster-production-1 1/1 Running 0 61m
cluster-production-2 1/1 Running 0 20s
Vérifier les versions des deux instances. Que constatez-vous ?
Comme nous avons modifié la version de l’image en 17.7,
l’instance secondaire qui vient d’être déployée est bien dans cette
version. La version du primaire est quant à elle restée en
17.6, ce qui est normal comme nous sommes dans une montée
de version supervisée.
# primaire
kubectl exec -it cluster-production-1 -c postgres -- psql -c 'show server_version;'
server_version
-------------------------------
17.6 (Debian 17.6-2.pgdg12+1)
(1 row)
# secondaire
kubectl exec -it cluster-production-2 -c postgres -- psql -c 'show server_version;'
server_version
-------------------------------
17.7 (Debian 17.7-3.pgdg12+1)
(1 row)
Il nous reste donc à signifier à CloudNativePG que le primaire peut
être mis à jour à son tour. Pour cela, il faut passer par le
plugin CloudNativePG de kubectl et procéder à une
bascule sur le secondaire qui deviendra le nouveau primaire avec la
ligne de commande :
kubectl cnpg promote [cluster] [new_primary].
Faite une bascule manuelle sur l’instance secondaire.
kubectl cnpg promote cluster-production cluster-production-2
L’ancien secondaire est désormais primaire comme on peut le voir dans
la sortie de kubectl cnpg status cluster-production qui
donne l’état du cluster PostgreSQL, en particulier, avec la ligne
Primary instance: cluster-production-2, ou encore dans le
tableau.
Instances status
Name Current LSN Replication role Status QoS Manager Version Node
---- ----------- ---------------- ------ --- --------------- ----
cluster-production-2 0/D001108 Primary OK Burstable 1.30.0 kind-worker
cluster-production-1 0/D001108 Standby (async) OK Burstable 1.30.0 kind-worker2
Les deux instances sont bien en version 17.7.
kubectl exec -it cluster-production-1 -c postgres -- psql -c 'show server_version;'
server_version
-------------------------------
17.7 (Debian 17.7-3.pgdg12+1)
(1 row)
Il existe d’autres cas où le redémarrage des instances est
nécessaire. Par exemple, si un paramètre comme
max_connections ou shared_buffers a été
modifié.
Si vous faites toujours une montée de version supervisée, il vous est
possible de ne pas faire de bascule mais simplement de redémarrer
l’instance primaire avec la ligne de commande
kubectl cnpg restart [cluster] [current_primary];
But : Effectuer une montée de version majeure de PostgreSQL.
Vous l’avez vu, la version majeure de PostgreSQL est indiquée dans le tag de l’image déployée. La modification de celle-ci entraîne le déclenchement d’une In-Place Major Upgrade. Ce mécanisme se fait nécessairement sur des instances arrêtées. L’opérateur CloudNativePG les arrêtera pour vous.
Pour cet exercice, nous allons mettre à jour un Cluster
en version 16.11 vers la version 18.1.
Créer le fichier
~/postgresql-16-to-18.yamlet ajouter le contenu suivant puis appliquer le aveckubectl.
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: postgresql-test
spec:
instances: 2
imageName: ghcr.io/cloudnative-pg/postgresql:16.11-standard-bookworm
storage:
size: 1Gikubectl apply -f ~/postgresql-16-to-18.yaml
Vérifier que les deux instances soient correctement déployées.
kubectl get pod
NAME READY STATUS RESTARTS AGE
postgresql-test-1 1/1 Running 0 38s
postgresql-test-2 1/1 Running 0 20s
Créer une table et y insérer quelques lignes.
kubectl cnpg psql postgresql-test -- -c "CREATE TABLE t1 (c1 INTEGER PRIMARY KEY, c2 text)";
CREATE TABLE
kubectl cnpg psql postgresql-test -- -c "INSERT INTO t1 SELECT generate_series(1,100), generate_series(1,100)::text";
INSERT 0 100
Ouvrir un nouveau terminal et lancer la commande
kubectl get pod --watch.
Ouvrir un nouveau terminal et lancer la commande
kubectl get pv --watch.
Ouvrir un nouveau terminal et lancer la commande
kubectl get pvc --watch.
Modifier le tag du paramètre
imageNamedu fichier~/postgresql-16-to-18.yamlen le passant de16.11à18.1puis appliquer la modification aveckubectl.
kubectl apply -f ~/postgresql-16-to-18.yaml
cluster.postgresql.cnpg.io/postgresql-test configured
Observez ce qu’il se passe dans les différents terminaux que vous avez ouvert.
Les deux instances passent en Terminating
postgresql-test-1 1/1 Terminating 0 4m26s
postgresql-test-2 1/1 Terminating 0 4m8s
La montée de version est déclenchée et un nouveau Pod
apparait :
postgresql-test-1-major-upgrade-7hl8w 0/1 Init:0/2 0 0s
postgresql-test-1-major-upgrade-7hl8w 0/1 Init:1/2 0 1s
postgresql-test-1-major-upgrade-7hl8w 0/1 PodInitializing 0 2s
postgresql-test-1-major-upgrade-7hl8w 1/1 Running 0 18s
postgresql-test-1-major-upgrade-7hl8w 0/1 Completed 0 45s
Lorsque qu’il a terminé de s’exécuté, le Pod
correspondant à l’instance primaire est relancé :
postgresql-test-1 0/1 Pending 0 0s
postgresql-test-1 0/1 Init:0/1 0 0s
postgresql-test-1 0/1 PodInitializing 0 0s
postgresql-test-1 0/1 Running 0 10s
postgresql-test-1 1/1 Running 0 10s
À ce moment là, le volume (PV) qui existait et qui était rattaché à
l’ancienne instance secondaire postgresql-test-2 sont
supprimés.
pvc-23b021b2-163b-41e3-9791-721b8635bee5 1Gi RWO Delete Released default/postgresql-test-2 standard <unset> 5m3s
pvc-23b021b2-163b-41e3-9791-721b8635bee5 1Gi RWO Delete Terminating default/postgresql-test-2 standard <unset> 5m6s
Même chose pour le PVC :
postgresql-test-2 Terminating pvc-23b021b2-163b-41e3-9791-721b8635bee5 1Gi RWO standard <unset> 5m5s
Pour respecter ce qui est demandé dans la définition YAML, une
instance secondaire postgresql-test-3 est créée et va se
joindre à la nouvelle instance primaire postgresql-test-1.
Cela se fait par le déploiement d’un Pod intermédiaire
suffixé par -join-XXXXX. Il sera détruit à la fin de
l’opération.
postgresql-test-3-join-tcrbs 0/1 Pending 0 0s
postgresql-test-3-join-tcrbs 0/1 Init:0/1 0 5s
postgresql-test-3-join-tcrbs 0/1 PodInitializing 0 6s
postgresql-test-3-join-tcrbs 1/1 Running 0 7s
postgresql-test-3-join-tcrbs 0/1 Completed 0 10s
postgresql-test-3 0/1 Pending 0 0s
postgresql-test-3 0/1 Init:0/1 0 0s
postgresql-test-3 0/1 PodInitializing 0 0s
postgresql-test-3 0/1 Running 0 10s
postgresql-test-3 1/1 Running 0 10s
postgresql-test-3-join-tcrbs 0/1 Terminating 0 22s
Vérifier que les données soient toujours présentes.
kubectl cnpg psql postgresql-test -- -c "SELECT count(*) FROM t1;"
Vérifier la version de PostgreSQL.
kubectl cnpg psql postgresql-test -- -c "show server_version;"
But : Effectuer une montée de version de l’opérateur.
Nous avons déployé la version 1.30.0 de l’opérateur.
Nous allons nous intéresser à la manière de le mettre à jour.
Lorsqu’une nouvelle version de l’opérateur est déployée, un nouveau
Pod se crée. Lorsque celui-ci est prêt, l’ancien opérateur
est tout simplement supprimé. Cette mise à jour déclenche également la
mise à jour d’un composant présentant dans les Pods des
instances PostgreSQL.
Lorsqu’un Pod PostgreSQL est déployé, un
InitContainer est créé en amont et permet de récupérer du
code correspondant à l’instance-manager. Il permet de
contrôler l’instance, son cycle, ses redémarrages, etc. La version de ce
manager est étroitement liée à la version de l’opérateur.
Pour information, c’est ce processus qui va lancer PostgreSQL et qui
aura le pid 1 dans le Pod.
cat /proc/1/cmdline
/controller/manager instance run--status-port-tls--log-level=info
Attention si vous avez utilisé votre opérateur pour
déployer plusieurs instances PostgreSQL, lorsque vous mettez à jour
l’opérateur, tous les Pods seront mis à
jour en même temps (ou quasiment). Il y aura donc une coupure de service
pour chaque instance. C’est le fonctionnement par défaut.
Dans une première console, lancer la commande
watchsuivante :
watch kubectl get pod
Dans une seconde console, lancer la commande
watchsuivante :
watch kubectl get pod -n cnpg-system
Dans une autre console, appliquer les fichiers YAML correspondant à la version
1.31.0de l’opérateur.
kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.31/releases/cnpg-1.31.0.yaml
Regarder ce qu’il se passe au niveau des différents
Pods(opérateur et PostgreSQL)
Les instances PostgreSQL déployées par l’opérateur sont redémarrées lors d’une montée de version de l’opérateur. Selon la configuration des instances, une opération manuelle sera nécessaire pour mettre à jour le primaire.
Il existe une méthode pour éviter ce comportement. Cependant elle ne
garantit pas le critère immuable que devrait suivre un Pod.
Nous vous conseillons de l’utiliser qu’à des fins de tests.
À titre d’information, voici la méthode à suivre pour y parvenir.
Pour cela il faut modifier la configuration de l’opérateur en passant le
paramètre ENABLE_INSTANCE_MANAGER_INPLACE_UPDATES à
true. Cela permettra de mettre à jour le
manager sans pour autant redémarrer le Pod
complet. Cette configuration doit être faite dans un objet
ConfigMap.
Créer un
Clusteravec une seule instance dans la dernière version de PostgreSQL disponible.
Le fichier suivant vous permet cela. Créer le dans, par exemple,
~/cluster_pdb.yaml et créer la ressource avec
kubectl apply -f ~/cluster_pdb.yaml.
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-pdb
spec:
instances: 1
storage:
size: 1GiRetrouver la ressource
PodDisruptionBudgetet la décrire.
L’abréviation pdb avec kubectl get peut
être utilisée.
kubectl get pdb
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
cluster-pdb-primary 1 N/A 0 6m23s
kubectl describe pdb cluster-pdb-primary
Name: cluster-pdb-primary
Namespace: default
Min available: 1
Selector: cnpg.io/cluster=cluster-pdb,cnpg.io/instanceRole=primary
Status:
Allowed disruptions: 0
Current: 1
Desired: 1
Total: 1
Events:
Trouver le nœud sur lequel fonctionne le
Podprimaire de votre instance primaire.
kubectl get pod cluster-pdb-1 -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
cluster-pdb-1 1/1 Running 0 31s 10.244.1.15 kind-worker <none> <none>
Dans cet exemple, le nœud est kind-worker. Cela peut
être différent pour vous. Comme il n’y a qu’un Pod, il
s’agit forcément de l’instance primaire.
Effectuer un
drainde ce nœud. Que se passe t’il ?
Le drain d’un nœud peut se déclencher avec la commande
kubectl drain. Des options supplémentaires peuvent être
indiquées comme --delete-emptydir-data ou
--ignore-daemonsets.
kubectl drain kind-worker --delete-emptydir-data --ignore-daemonsets
node/kind-worker cordoned
Warning: ignoring DaemonSet-managed Pods: kube-system/kindnet-rqjqx, kube-system/kube-proxy-dv4ld
evicting pod default/cluster-pdb-1
error when evicting pods/"cluster-pdb-1" -n "default" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod default/cluster-pdb-1
error when evicting pods/"cluster-pdb-1" -n "default" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
Le nœud est marqué cordoned. Kubernetes cherche à
évincer les Pods. La politique d’éviction de notre
Cluster n’étant pas respectée
(Cannot evict pod as it would violate the pod's disruption budget.),
le drain du nœud ne peut se faire.
Arrêter le
draindu nœud.
kubectl uncordon kind-worker
Ajouter une instance secondaire à votre
Cluster. Vérifier qu’elle soit correctement démarrée avant de passer à la suite.
Attendez que le Pod de l’instance secondaire soit
marquée comme Running ou que le statut du
Cluster soit bien
Cluster in healthy state.
kubectl get cluster
NAME AGE INSTANCES READY STATUS PRIMARY
cluster-pdb 18m 2 2 Cluster in healthy state cluster-pdb-1
Trouver le nœud sur lequel fonctionne le
Podde votre instance secondaire.
kubectl get pod cluster-pdb-2 -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
cluster-pdb-2 0/1 Running 0 10s 10.244.2.17 kind-worker2 <none> <none>
Ici il s’agit du nœud kind-worker2.
Effectuer un
drainde ce nœud. Que se passe t’il ?
kubectl drain kind-worker2 --delete-emptydir-data --ignore-daemonsets
node/kind-worker2 cordoned
Warning: ignoring DaemonSet-managed Pods: kube-system/kindnet-t8c2x, kube-system/kube-proxy-rt6s9
evicting pod default/cluster-pdb-2
evicting pod cnpg-system/cnpg-controller-manager-65bfdb64c9-4rx2v
pod/cnpg-controller-manager-65bfdb64c9-4rx2v evicted
pod/cluster-pdb-2 evicted
node/kind-worker2 drained
Au passage, notons que le drain affecte tous les
Pods du nœud. Donc, dans cet exemple, il y a aussi le
Pod de l’opérateur qui se voit être évincé :
evicting pod cnpg-system/cnpg-controller-manager-65bfdb64c9-4rx2v.
Ce dernier sera recréé sur un autre nœud pouvant l’accueillir.
Le Pod du secondaire est également bien supprimé du
nœud. L’opérateur (après que celui-ci est été lui-même recréé) cherche à
le redéployer ailleurs. S’il n’existe pas de troisième nœud, le
Pod restera à l’état Pending, en attendant
qu’un nouveau nœud soit disponible.
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
cluster-pdb-1 1/1 Running 0 6m3s 10.244.1.15 kind-worker <none> <none>
cluster-pdb-2 0/1 Pending 0 3m10s <none> <none> <none> <none>
Arrêter le
draindu nœud.
kubectl uncordon kind-worker2
node/kind-worker2 uncordoned
L’instance secondaire doit redevenir opérationnelle.
Effectuer un
draindu nœud où se trouve l’instance primaire. Que se passe t’il ?
kubectl drain kind-worker --delete-emptydir-data --ignore-daemonsets
node/kind-worker cordoned
Warning: ignoring DaemonSet-managed Pods: kube-system/kindnet-rqjqx, kube-system/kube-proxy-dv4ld
evicting pod default/cluster-pdb-1
evicting pod cnpg-system/cnpg-controller-manager-65bfdb64c9-n7vgn
error when evicting pods/"cluster-pdb-1" -n "default" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
pod/cnpg-controller-manager-65bfdb64c9-n7vgn evicted
evicting pod default/cluster-pdb-1
error when evicting pods/"cluster-pdb-1" -n "default" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
evicting pod default/cluster-pdb-1
pod/cluster-pdb-1 evicted
node/kind-worker drained
Kubernetes cherche a évincer le Pod primaire. Or le
PodDisruptionBudget cluster-pdb-primary
indique qu’il doit y avoir toujours, au sein de cluster
Kubernetes, un Pod ayant le rôle de primaire. Ici ce n’est
pas le cas.
Il faut donc attendre que le Pod secondaire
cluster-pdb-2 soit promu primaire par l’opérateur. Et pour
cela, une opération de switchover est déclenchée. Dès que cette
dernière se termine, l’éviction du Pod peut se faire :
pod/cluster-pdb-1 evicted.
La commande kubectl cnpg status cluster-pdb permet de
nous rendre compte que le primaire à changé.
kubectl cnpg status cluster-pdb
Cluster Summary
Name default/cluster-pdb
System ID: 7600706097499480099
PostgreSQL Image: ghcr.io/cloudnative-pg/postgresql:18.1-system-trixie
Primary instance: cluster-pdb-2
Primary promotion time: 2026-01-29 09:09:33 +0000 UTC (40s)
Status: Waiting for the instances to become active Some instances are not yet active. Please wait.
Instances: 2
Ready instances: 1
Size: 112M
Current Write LSN: 0/6000F40 (Timeline: 2 - WAL File: 000000020000000000000006)
Continuous Backup not configured
Streaming Replication status
Not available yet
Instances status
Name Current LSN Replication role Status QoS Manager Version Node
---- ----------- ---------------- ------ --- --------------- ----
cluster-pdb-2 0/6000F40 Primary OK BestEffort 1.28.0 kind-worker2
cluster-pdb-1 - - BadRequest BestEffort -
Error(s) extracting status
-----------------------------------
failed to get status by proxying to the pod, you might lack permissions to get pods/proxy: the server rejected our request for an unknown reason (get pods https:cluster-pdb-1:8000)
À cet instant, le nœud où se trouvait le primaire est
drain. Le nœud peut donc être mis à jour.
Le Cluster n’est pas dans un état Healthy
car il attend que toutes les instances soit démarrées.
Arrêter le
draindu nœud.
kubectl uncordon kind-worker
node/kind-worker uncordoned
L’instance cluster-pdb-1 qui maintenant secondaire a été
recréée :
kubectl get pod -o wide lenovo-pro: Thu Jan 29 10:10:54 2026
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
cluster-pdb-1 1/1 Running 0 75s 10.244.1.17 kind-worker <none> <none>
cluster-pdb-2 1/1 Running 0 6m26s 10.244.2.18 kind-worker2 <none> <none>
et le Cluster a retrouvé un état
Healthy.
Instances status
Name Current LSN Replication role Status QoS Manager Version Node
---- ----------- ---------------- ------ --- --------------- ----
cluster-pdb-2 0/8000000 Primary OK BestEffort 1.28.0 kind-worker2
cluster-pdb-1 0/8000000 Standby (async) OK BestEffort 1.28.0 kind-worker
La trace se retrouve encore dans le nom de la librairie C pour les clients, la libpq.↩︎