Module K4
Dalibo SCOP
26.09
10 septembre 2026
| Formation | Module K4 |
| Titre | Supervision et Troubleshooting |
| Révision | 26.09 |
| https://dali.bo/k4_pdf | |
| EPUB | https://dali.bo/k4_epub |
| HTML | https://dali.bo/k4_html |
| Slides | https://dali.bo/k4_slides |
| TP | https://dali.bo/k4_tp |
| TP (solutions) | https://dali.bo/k4_solutions |
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.
track_activities = on (défaut)pg_stat_activity affiche
archiverseq_scan, idx_scann_live_tup, n_dead_tup,
n_tup_ins, n_tup_upd,
n_tup_dellast_vacuum, last_autovacuum,
last_analyze, last_autoanalyzepg_statio_user_tablespg_stat_databasepg_stat_user_indexespg_stat_wal_receiverpg_stat_checkpointerpg_stat_database_conflictslog_destination, log_directory,
log_file_mode, log_filename, …log_min_messages
panic / fatal / log
/ error / warninglog_min_error_statement
error (ou warning)log_min_duration_statement (ex : 1s)log_statement + log_durationlog_transaction_sample_ratelog_statement_sample_rate +
log_min_duration_samplelog_connections + log_disconnectionslog_autovacuum_min_durationlog_checkpoints
time ou wal ?log_lock_waits (mini 1s)
log_line_prefix
%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%hlog_destination à csvlogCSV en JSONcheck_pg_activity
ConfigMap par défaut
cnpg-default-monitoringPod PostgreSQL
curl http://127.0.0.1:9187/metricsPod de l’opérateur
curl http://127.0.0.1:8080/metricsConfigMap cnpg-default-monitoringConfigMapspec.monitoring du Clustercnpg pour kubectl
Clusterkubectl cnpg status CLUSTER
Clusterkubectl 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)kubectl cnpg fencing on CLUSTER ID -- une instance
kubectl cnpg fencing on CLUSTER "*" -- toutes les instances
postmasterPod toujours en cours d’exécutionfencedpg_wal saturé :
max_slot_wal_keep_size dépassé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.
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.