Control plane
Il filtro sui namespace applicativi non si applica: qui si guarda esattamente ciò che altrove viene escluso.
Precedenza assoluta: se qualcosa in questa pagina è rosso, chiudilo prima di indagare sulle applicazioni. Un etcd lento produce sintomi applicativi in ogni namespace contemporaneamente, e ognuno di quei sintomi sembra un problema a sé.
| Triage | |
| API server · etcd | |
| Cluster operator | |
| Scheduler e controller manager | |
| Monitoring stack | il monitoraggio del monitoraggio |
| Comandi · Alert |
Triage
T1. Error rate dell’API server
# richieste al secondosum(rate(apiserver_request_total{code=~"5.."}[5m]))Deve essere ~0. Se non lo è, tutto il resto delle metriche è inaffidabile.
T2. Latenza fsync di etcd
# secondihistogram_quantile(0.99, sum by (le, instance) (rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m])))T3. Cluster operator degradati
# elenco: ogni serie restituita soddisfa la condizionecluster_operator_conditions{condition="Degraded"} == 1T4. Membri etcd
# numero di seriecount(etcd_server_has_leader)# 1 = il membro ha un leader, 0 = quorum a rischioetcd_server_has_leader == 0Devono essere tre e tutti con leader. Uno senza leader significa quorum a rischio.
T5. Target di scrape irraggiungibili
# numero di seriecount by (job) (up == 0)API server
Error rate scomposto
# richieste al secondosum by (code, verb, resource) ( rate(apiserver_request_total{code=~"5.."}[5m])) > 0| Codice | Causa tipica |
|---|---|
503 | un webhook di ammissione non risponde |
504 | etcd lento |
500 | bug, certificati, panic del controller |
429 | rate limiting di priority & fairness: sovraccarico |
Latenza p99 per verb
# secondihistogram_quantile(0.99, sum by (le, verb) ( rate(apiserver_request_duration_seconds_bucket{ verb!~"CONNECT|WATCH", subresource!~"log|exec|portforward|attach|proxy" }[5m]) ))L’esclusione di WATCH e CONNECT è obbligatoria: sono connessioni long-lived e la loro durata non è latenza. Includerle produce numeri assurdi.
SLO di riferimento: GET/POST/PUT/DELETE sotto 1 s, LIST sotto 5 s (le LIST su risorse numerose sono naturalmente lente).
Latenza per risorsa
# seconditopk(10, histogram_quantile(0.99, sum by (le, resource, verb) ( rate(apiserver_request_duration_seconds_bucket{ verb!~"CONNECT|WATCH", subresource!~"log|exec|portforward|attach|proxy" }[5m]) ) ))Identifica quale risorsa sta rallentando l’API. Se è una CRD, l’operator che la gestisce ne ha create troppe o il suo webhook è lento.
Chi bombarda l’API
# richieste al secondotopk(20, sum by (resource, verb, client) ( rate(apiserver_request_total[5m]) ))Il label client porta lo user-agent. È la query che identifica l’operator che polla ogni secondo invece di usare le watch — causa frequente di degrado del control plane senza nessuna causa apparente.
# richieste al secondotopk(10, sum by (client) (rate(apiserver_request_total[5m])))Richieste in volo
# numero di richieste in corsosum by (request_kind) (apiserver_current_inflight_requests)Se si avvicina a max-requests-inflight (default 400 mutating, 800 read-only), l’API server rifiuta con 429 e i client fanno backoff.
Priority & fairness
# eventi al secondosum by (priority_level, flow_schema) ( rate(apiserver_flowcontrol_rejected_requests_total[5m])) > 0# numero di richieste accodateapiserver_flowcontrol_current_inqueue_requests > 0APF sta scartando o accodando richieste per proteggere l’API server: sintomo di sovraccarico, non causa.
Webhook di ammissione
# secondihistogram_quantile(0.99, sum by (le, name, type) ( rate(apiserver_admission_webhook_admission_duration_seconds_bucket[5m]) ))# rifiuti al secondosum by (name, rejected) ( rate(apiserver_admission_webhook_rejection_count[5m])) > 0Un webhook lento rallenta ogni creazione di risorsa che intercetta. Se ha failurePolicy: Fail e smette di rispondere, blocca completamente la creazione di quelle risorse — sintomo: i deploy si fermano e oc apply va in timeout.
oc get validatingwebhookconfigurationoc get mutatingwebhookconfigurationCertificati in scadenza
# giornimin(kube_certificatesigningrequest_cert_not_after - time()) / 86400# secondiapiserver_client_certificate_expiration_seconds_bucket{le="604800"} > 0Su cluster air-gapped con rotazione manuale è una verifica da schedulare.
oc get csr | grep -i pendingoc -n openshift-kube-apiserver-operator get secret -o json | jq -r '.items[] | select(.metadata.annotations."auth.openshift.io/certificate-not-after") | "\(.metadata.name)\t\(.metadata.annotations."auth.openshift.io/certificate-not-after")"'Consumo dell’API server
# GiBsum by (pod) ( container_memory_working_set_bytes{ namespace="openshift-kube-apiserver", container="kube-apiserver" }) / 1024^3# core CPUsum by (pod) ( rate(container_cpu_usage_seconds_total{ namespace="openshift-kube-apiserver", container="kube-apiserver" }[5m]))Un kube-apiserver che consuma sempre più memoria sta accumulando watch: spesso un operator che ne apre e non ne chiude.
etcd
etcd è il database del cluster. Ogni sua lentezza si propaga a tutto.
Latenza di fsync WAL
# secondihistogram_quantile(0.99, sum by (le, instance) (rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m])))| Valore | Lettura |
|---|---|
< 0.010 | sano (soglia Red Hat: 10 ms) |
0.010 – 0.025 | degrado: il cluster inizia a sembrare lento |
> 0.025 | grave: rischio di perdita del quorum |
È la metrica singola più importante del control plane. Su cluster VMware la causa è quasi sempre il datastore condiviso: etcd vuole un disco dedicato a bassa latenza, e va detto a chi gestisce la farm.
Latenza di commit sul backend
# secondihistogram_quantile(0.99, sum by (le, instance) (rate(etcd_disk_backend_commit_duration_seconds_bucket[5m])))Soglia: 0.025.
Cambi di leader
# conteggio nell'ultima oraincrease(etcd_server_leader_changes_seen_total[1h])Deve essere 0. Ogni cambio è un’interruzione di scrittura di qualche secondo su tutto il cluster. Cause: latenza di rete tra i master, un master sotto stress, disco lento.
Membri e quorum
# 1 = il membro ha un leader, 0 = quorum a rischioetcd_server_has_leader# numero di seriecount(etcd_server_has_leader == 1)# 1 = questo membro è il leaderetcd_server_is_leaderCon tre membri, il quorum è due: perderne due significa cluster read-only.
Proposte
# conteggio nell'ultima oraincrease(etcd_server_proposals_failed_total[1h]) > 0# numero di proposte non ancora applicateetcd_server_proposals_pendingProposte pendenti in crescita = etcd non tiene il ritmo delle scritture.
# eventi al secondorate(etcd_server_proposals_committed_total[5m])Dimensione del database
# byteetcd_mvcc_db_total_size_in_bytes / 1024^3# GiBetcd_mvcc_db_total_size_in_use_in_bytes / 1024^3# MiB(etcd_mvcc_db_total_size_in_bytes - etcd_mvcc_db_total_size_in_use_in_bytes) / 1024^2La differenza è frammentazione: spazio allocato ma non in uso, recuperabile con defrag. Sopra i 500 MiB vale la pena farlo.
Quota di default su OCP: 8 GiB. Al raggiungimento etcd va in NOSPACE e il cluster diventa read-only: niente deploy, niente scale, niente scheduling.
# Stato dei membrioc -n openshift-etcd exec etcd-<master-0> -c etcdctl -- etcdctl endpoint status -w table --cluster
# Saluteoc -n openshift-etcd exec etcd-<master-0> -c etcdctl -- etcdctl endpoint health --cluster
# Defrag, un membro alla volta, MAI in parallelooc -n openshift-etcd exec etcd-<master-0> -c etcdctl -- etcdctl defrag --command-timeout=60sIl defrag blocca il membro su cui gira per la durata dell’operazione: eseguirlo su tutti i master insieme fa perdere il quorum.
Su OCP 4.9+ esiste anche il defrag automatico gestito dall’operator:
oc -n openshift-etcd-operator logs -l app=etcd-operator --tail=100 | grep -i defragLatenza di rete tra i peer
# secondihistogram_quantile(0.99, sum by (le, To) (rate(etcd_network_peer_round_trip_time_seconds_bucket[5m])))Sopra 0.050 tra due master il quorum è a rischio. Su cluster stretched tra due sale è la metrica da tenere sotto controllo.
Traffico tra peer
# MiBrate(etcd_network_peer_sent_bytes_total[5m]) / 1024^2# MiBrate(etcd_network_peer_received_bytes_total[5m]) / 1024^2Numero di oggetti
# conteggiotopk(15, apiserver_storage_objects)Identifica cosa sta gonfiando etcd. Colpevoli tipici: events, secrets accumulati da operator, pods in stato Failed mai eliminati, CRD generate da un controller in loop, Job non ripuliti.
# conteggioapiserver_storage_objects{resource="events"}# conteggioapiserver_storage_objects{resource=~"secrets|configmaps"}Watcher attivi
# numero di watch apertetopk(15, sum by (resource) (apiserver_longrunning_requests{verb="WATCH"}))Un numero di watch che cresce senza sosta indica un client che le apre e non le chiude.
Disco di etcd
# percentuale (0-100)100 * ( 1 - ( node_filesystem_avail_bytes{mountpoint=~"/var/lib/etcd|/var/lib"} / node_filesystem_size_bytes{mountpoint=~"/var/lib/etcd|/var/lib"} ))# secondi per secondo (frazione di tempo)rate(node_disk_write_time_seconds_total{instance=~".*master.*"}[5m])/rate(node_disk_writes_completed_total{instance=~".*master.*"}[5m])Cluster operator
Degraded
# elenco: ogni serie restituita soddisfa la condizionecluster_operator_conditions{condition="Degraded"} == 1Non Available
# elenco: ogni serie restituita soddisfa la condizionecluster_operator_conditions{condition="Available"} == 0Progressing da troppo tempo
# elenco: ogni serie restituita soddisfa la condizionecluster_operator_conditions{condition="Progressing"} == 1Fuori da un upgrade, un CO che resta Progressing oltre 15 minuti è bloccato.
Versioni
# numero di seriecount by (version) (cluster_operator_conditions{condition="Available"})Durante un upgrade convivono più versioni. Se dopo la fine ci sono ancora versioni miste, l’upgrade non è realmente completo.
# 1 = operator attivo, 0 = non attivocluster_operator_upStato del cluster version operator
# 1 se la condizione è veracluster_version{type="completed"}# numero di aggiornamenti disponibilicluster_version_available_updates# 1 = capability abilitatacluster_version_capability{name!=""}oc get cooc get clusterversionoc describe co <nome> | sed -n '/Conditions:/,/Extension:/p'oc adm upgradeScheduler e controller manager
Latenza di scheduling
# secondihistogram_quantile(0.99, sum by (le) (rate(scheduler_e2e_scheduling_duration_seconds_bucket[5m])))# secondihistogram_quantile(0.99, sum by (le) (rate(scheduler_scheduling_attempt_duration_seconds_bucket[5m])))Su cluster grandi con molti vincoli (affinity, taint, topology spread) lo scheduling diventa costoso e i pod restano Pending più a lungo di quanto sembri giustificato.
Tentativi falliti
# eventi al secondosum by (result) (rate(scheduler_schedule_attempts_total[5m]))Il label result distingue scheduled, unschedulable, error. Un tasso alto di unschedulable significa cluster pieno o vincoli impossibili.
Coda dello scheduler
# numero di pod in coda allo schedulerscheduler_pending_podsController manager
# eventi al secondorate(workqueue_adds_total{job="kube-controller-manager"}[5m])# numero di elementi in codaworkqueue_depth{job="kube-controller-manager"}# secondihistogram_quantile(0.99, sum by (le, name) (rate(workqueue_queue_duration_seconds_bucket{job="kube-controller-manager"}[5m])))Una workqueue che cresce significa che un controller non riesce a smaltire il lavoro: le riconciliazioni si accumulano e le modifiche agli oggetti impiegano sempre più tempo a produrre effetti.
Leader election
# conteggio nell'ultima oraincrease(leader_election_master_status[1h])# elenco: ogni serie restituita soddisfa la condizioneleader_election_master_status == 0Cambi frequenti di leader tra i componenti del control plane indicano instabilità di rete o master sotto stress.
Monitoring stack
Il monitoraggio che smette di funzionare è il modo peggiore di scoprire un problema.
Target irraggiungibili
# elenco: ogni serie restituita soddisfa la condizioneup == 0# numero di seriecount by (job, namespace) (up == 0)Prometheus vicino al limite di memoria
# percentuale 0-100100 * ( container_memory_working_set_bytes{ namespace="openshift-monitoring", pod=~"prometheus-k8s-.*", container="prometheus" } / kube_pod_container_resource_limits{ namespace="openshift-monitoring", pod=~"prometheus-k8s-.*", container="prometheus", resource="memory" })Prometheus in OOM perde i dati non ancora scritti su disco. Su cluster con molte serie è un evento ricorrente.
Serie temporali attive
# numero di serie temporali attiveprometheus_tsdb_head_series# eventi al secondorate(prometheus_tsdb_head_series_created_total[5m])Un tasso di creazione alto e costante significa churn: serie che nascono e muoiono continuamente, tipicamente perché un label contiene qualcosa di variabile (nome del pod in una metrica applicativa, ID di richiesta, timestamp).
Cardinalità per metrica
# numero di serietopk(20, count by (__name__) ({__name__=~".+"}))Query molto costosa: lanciala una volta, mai in dashboard. Identifica le metriche con più serie, che sono la causa di un Prometheus che consuma troppa memoria.
Alternativa più leggera, via API:
oc -n openshift-monitoring exec -c prometheus prometheus-k8s-0 -- \ curl -s http://localhost:9090/api/v1/status/tsdb | jq '.data.seriesCountByMetricName[:20]'oc -n openshift-monitoring exec -c prometheus prometheus-k8s-0 -- \ curl -s http://localhost:9090/api/v1/status/tsdb | jq '.data.labelValueCountByLabelName[:20]'La seconda mostra quali label hanno più valori distinti: è lì che si trova il colpevole della cardinalità.
Campioni scartati
# eventi al secondosum by (job) (rate(prometheus_target_scrapes_exceeded_sample_limit_total[5m])) > 0Un target che supera il sampleLimit viene scartato interamente: non perdi qualche serie, perdi tutto quel target.
# eventi al secondosum by (job) (rate(prometheus_target_scrapes_sample_duplicate_timestamp_total[5m])) > 0Durata degli scrape
# seconditopk(20, scrape_duration_seconds)Uno scrape che dura più dell’intervallo produce buchi nei dati.
# secondiscrape_duration_seconds > 10Valutazione delle regole
# secondiprometheus_rule_group_last_duration_seconds/prometheus_rule_group_interval_secondsSopra 1 le regole impiegano più del loro intervallo per essere valutate: gli alert arrivano in ritardo o non arrivano.
# seconditopk(10, prometheus_rule_group_last_duration_seconds)Identifica il gruppo di regole più lento — di solito quello con una subquery lunga dentro un alert.
Storage di Prometheus
# GiBprometheus_tsdb_storage_blocks_bytes / 1024^3# percentuale (0-100)100 * ( 1 - kubelet_volume_stats_available_bytes{namespace="openshift-monitoring"} / kubelet_volume_stats_capacity_bytes{namespace="openshift-monitoring"})Alertmanager
# contatore cumulativo di notifiche fallitealertmanager_notifications_failed_total# eventi al secondorate(alertmanager_notifications_failed_total[5m]) > 0# numero di alert noti ad Alertmanageralertmanager_alertsSe le notifiche falliscono, gli alert scattano e nessuno lo sa. Su cluster air-gapped la causa tipica è il proxy o il certificato del receiver.
oc -n openshift-monitoring logs -l alertmanager=main -c alertmanager --tail=100 | grep -i "error\|failed"Alert attivi
# elenco degli alert attivi (valore sempre 1)ALERTS{alertstate="firing"}# numero di seriecount by (severity) (ALERTS{alertstate="firing"})# numero di seriecount by (alertname) (ALERTS{alertstate="firing"})Utile per capire se un alert sta facendo rumore: se lo stesso alertname è firing su decine di target da settimane, o va corretto o va rimosso.
User workload monitoring
# elenco: ogni serie restituita soddisfa la condizioneup{namespace="openshift-user-workload-monitoring"} == 0# numero di seriecount by (namespace) ( up{job!~"kubelet|node-exporter|apiserver", namespace!~"openshift-.*|kube-.*"})Quanti target applicativi sono effettivamente monitorati per namespace.
Comandi
# Stato generaleoc get cooc get clusterversionoc get nodesoc adm upgrade
# etcdoc -n openshift-etcd get podsoc -n openshift-etcd exec etcd-<master-0> -c etcdctl -- etcdctl endpoint status -w table --clusteroc -n openshift-etcd exec etcd-<master-0> -c etcdctl -- etcdctl endpoint health --clusteroc -n openshift-etcd exec etcd-<master-0> -c etcdctl -- etcdctl member list -w table
# Cosa occupa etcdoc -n openshift-etcd exec etcd-<master-0> -c etcdctl -- \ etcdctl get / --prefix --keys-only | sed -E 's|/([^/]+)/([^/]+)/.*|\1/\2|' | sort | uniq -c | sort -rn | head -20
# API serveroc -n openshift-kube-apiserver get podsoc -n openshift-kube-apiserver logs -l app=openshift-kube-apiserver --tail=100 | grep -i error
# Webhook che possono bloccare tuttooc get validatingwebhookconfiguration -o custom-columns=NAME:.metadata.name,POLICY:'.webhooks[*].failurePolicy'oc get mutatingwebhookconfiguration -o custom-columns=NAME:.metadata.name,POLICY:'.webhooks[*].failurePolicy'
# Eventi accumulatioc get events -A --no-headers | wc -l
# Monitoringoc -n openshift-monitoring get podsoc -n openshift-monitoring exec -c prometheus prometheus-k8s-0 -- \ curl -s http://localhost:9090/api/v1/status/tsdb | jq '.data.seriesCountByMetricName[:15]'oc -n openshift-monitoring exec -c prometheus prometheus-k8s-0 -- \ promtool query instant http://localhost:9090 'count(up == 0)'
# Alert attivioc -n openshift-monitoring exec -c prometheus prometheus-k8s-0 -- \ promtool query instant http://localhost:9090 'ALERTS{alertstate="firing"}'Alert
apiVersion: monitoring.coreos.com/v1kind: PrometheusRulemetadata: name: control-plane-custom namespace: openshift-monitoring labels: role: alert-rulesspec: groups: - name: controlplane.etcd rules: - alert: EtcdFsyncSlow expr: | histogram_quantile(0.99, sum by (le, instance) (rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m])) ) > 0.025 for: 10m labels: severity: critical annotations: summary: "etcd {{ $labels.instance }}: fsync p99 a {{ $value | printf \"%.3f\" }}s" description: "Oltre la soglia Red Hat di 25ms. Rischio di perdita del quorum. Causa tipica: latenza del datastore. Escalation infrastruttura."
- alert: EtcdDatabaseNearQuota expr: etcd_mvcc_db_total_size_in_bytes > 7 * 1024^3 for: 15m labels: severity: critical annotations: summary: "etcd {{ $labels.instance }}: DB a {{ $value | humanize1024 }}B" description: "Vicino alla quota di 8GiB. Al raggiungimento il cluster diventa read-only. Verificare frammentazione e valutare un defrag (un membro alla volta)."
- alert: EtcdFragmentationHigh expr: | (etcd_mvcc_db_total_size_in_bytes - etcd_mvcc_db_total_size_in_use_in_bytes) > 1024^3 for: 6h labels: severity: info annotations: summary: "etcd {{ $labels.instance }}: {{ $value | humanize1024 }}B di frammentazione" description: "Recuperabile con defrag. Eseguire un membro alla volta, mai in parallelo."
- name: controlplane.apiserver rules: - alert: APIServerAdmissionWebhookSlow expr: | histogram_quantile(0.99, sum by (le, name) (rate(apiserver_admission_webhook_admission_duration_seconds_bucket[5m])) ) > 1 for: 10m labels: severity: warning annotations: summary: "Webhook {{ $labels.name }}: p99 a {{ $value | printf \"%.2f\" }}s" description: "Rallenta ogni creazione di risorsa che intercetta. Con failurePolicy Fail può bloccarle del tutto."
- alert: APIServerFlowControlRejecting expr: | sum by (priority_level) (rate(apiserver_flowcontrol_rejected_requests_total[5m])) > 0 for: 10m labels: severity: warning annotations: summary: "APF sta rifiutando richieste su {{ $labels.priority_level }}" description: "L'API server si sta proteggendo dal sovraccarico. Identificare il client con il rate più alto."
- name: controlplane.monitoring rules: - alert: PrometheusMemoryNearLimit expr: | container_memory_working_set_bytes{ namespace="openshift-monitoring", pod=~"prometheus-k8s-.*", container="prometheus" } / kube_pod_container_resource_limits{ namespace="openshift-monitoring", pod=~"prometheus-k8s-.*", container="prometheus", resource="memory" } > 0.85 for: 15m labels: severity: warning annotations: summary: "Prometheus all'{{ $value | humanizePercentage }} del memory limit" description: "Un OOM di Prometheus perde i dati non ancora persistiti. Verificare la cardinalità."
- alert: PrometheusSeriesChurnHigh expr: rate(prometheus_tsdb_head_series_created_total[10m]) > 100 for: 30m labels: severity: warning annotations: summary: "Prometheus: {{ $value | printf \"%.0f\" }} nuove serie/s" description: "Churn elevato: un label con valori variabili (pod name, request id, timestamp) in una metrica applicativa."
- alert: PrometheusRuleEvaluationSlow expr: | prometheus_rule_group_last_duration_seconds / prometheus_rule_group_interval_seconds > 0.8 for: 15m labels: severity: warning annotations: summary: "Gruppo di regole {{ $labels.rule_group }} lento" description: "Valutazione oltre l'80% dell'intervallo. Gli alert arriveranno in ritardo. Cercare subquery lunghe."
- alert: AlertmanagerNotificationsFailing expr: rate(alertmanager_notifications_failed_total[5m]) > 0 for: 10m labels: severity: critical annotations: summary: "Alertmanager: notifiche fallite verso {{ $labels.integration }}" description: "Gli alert scattano e nessuno li riceve. Su cluster air-gapped verificare proxy e certificati."
- alert: ScrapeTargetsDown expr: count by (job) (up == 0) > 0 for: 15m labels: severity: warning annotations: summary: "{{ $value }} target del job {{ $labels.job }} irraggiungibili"etcdHighFsyncDurations, etcdMembersDown, KubeAPIErrorBudgetBurn e ClusterOperatorDegraded esistono già nativamente. Verifica prima di duplicare:
oc -n openshift-monitoring get prometheusrule -o yaml | grep -E "alert: (etcd|KubeAPI|ClusterOperator)"Soglie di riferimento
| Metrica | Sano | Critico |
|---|---|---|
| API 5xx rate | ~0 | > 0.1/s |
| API p99 (escl. LIST) | < 1 s | > 3 s |
| Webhook p99 | < 0.1 s | > 1 s |
| etcd fsync p99 | < 10 ms | > 25 ms |
| etcd backend commit p99 | < 25 ms | > 50 ms |
| etcd leader changes / h | 0 | > 0 |
| etcd DB size | < 4 GiB | > 7 GiB |
| etcd peer RTT p99 | < 20 ms | > 50 ms |
| Cluster operator Degraded | 0 | > 0 |
up == 0 | 0 | > 0 |
| Prometheus mem / limit | < 70% | > 85% |
| Rule eval / interval | < 50% | > 80% |