From 87a4ece6f4c1780eecbb7a773d5a67313bf897a5 Mon Sep 17 00:00:00 2001 From: chemavx Date: Wed, 5 Aug 2026 16:14:40 +0000 Subject: [PATCH] =?UTF-8?q?backup-system:=20barrido=20semanal=20de=20im?= =?UTF-8?q?=C3=A1genes=20containerd=20por=20nodo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit El worker llegó al 80,9% de disco: 223 imágenes para ~20 pods, 62 GB en containerd. Cada build de CI publica un tag y nadie recoge los viejos — 97 tags de researchowl y 105 de polymarket-bot, decomisada en julio. El GC de kubelet no basta: solo entra al 85% de imagefs, o sea cuando ya no queda sitio. Un prune manual dejó el worker en 23% (54 GB) y el master en 15% (58 GB). Un CronJob por nodo porque un CronJob programa un solo pod; el nodo se fija con nodeSelector. Domingos 06:30 y 07:15, el hueco entre backups. El script hace varias pasadas a propósito: borrar snapshots satura a containerd y las llamadas siguientes dan DeadlineExceeded. No es fallo, es cola. Se reintenta mirando la salida, no el código de retorno, porque crictl devuelve 0 aunque haya borrados fallidos. Co-Authored-By: Claude Opus 5 --- .../configmap-image-prune-script.yaml | 65 +++++++++++ backup-system/cronjob-image-prune.yaml | 110 ++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 backup-system/configmap-image-prune-script.yaml create mode 100644 backup-system/cronjob-image-prune.yaml diff --git a/backup-system/configmap-image-prune-script.yaml b/backup-system/configmap-image-prune-script.yaml new file mode 100644 index 0000000..44fc4f6 --- /dev/null +++ b/backup-system/configmap-image-prune-script.yaml @@ -0,0 +1,65 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: image-prune-script + namespace: backup-system +data: + image-prune.sh: | + #!/bin/sh + # Barrido semanal de imágenes de containerd huérfanas, un nodo por job. + # Motivo: cada build de CI publica un tag nuevo y el nodo se queda con todos + # los viejos. El 2026-08-05 el worker llegó al 80,9% de disco con 223 imágenes + # para ~20 pods (97 tags de researchowl, 105 de polymarket-bot, app decomisada + # en julio). El prune dejó el nodo en 23%: 54 GB en imágenes muertas. + # + # El recolector de kubelet no basta: solo entra al 85% de imagefs, o sea que + # solo actúa cuando ya casi no queda sitio, y en ráfaga. + # + # chroot al host porque crictl no está en la imagen: vive en el binario de k3s + # del nodo, y el socket de containerd solo se ve desde el namespace del host. + set -u + + K3S=/usr/local/bin/k3s + PASADAS=4 + + log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; } + disco() { chroot /host df -h / | tail -1 | awk '{print $3" usados, "$4" libres ("$5")"}'; } + imagenes() { chroot /host $K3S crictl images 2>/dev/null | tail -n +2 | wc -l; } + + if [ ! -x "/host$K3S" ]; then + log "ERROR: no encuentro $K3S en el host" + exit 1 + fi + + log "antes: $(disco), $(imagenes) imágenes" + + # Varias pasadas a propósito: borrar snapshots satura a containerd y las + # llamadas siguientes se caen con DeadlineExceeded. No es un fallo real, es + # cola — reintentando termina. Pasó en la limpieza manual del 2026-08-05: + # la primera pasada soltó 11 GB de los 54 y el resto salió en las siguientes. + # No fiamos el reintento del código de salida (crictl devuelve 0 aunque haya + # borrados fallidos): miramos si escupió errores de RemoveImage. + limpio=0 + for i in $(seq 1 $PASADAS); do + salida=$(chroot /host $K3S crictl rmi --prune 2>&1) + if echo "$salida" | grep -q "RemoveImage.*failed"; then + fallos=$(echo "$salida" | grep -c "RemoveImage.*failed") + log "pasada $i: $fallos borrados en cola, reintento en 30s" + sleep 30 + else + log "pasada $i: limpia" + limpio=1 + break + fi + done + + log "después: $(disco), $(imagenes) imágenes" + + if [ "$limpio" = 0 ]; then + # Sale 1 para que se vea en Grafana/Telegram: liberó algo, pero quedaron + # borrados sin cerrar tras $PASADAS intentos y conviene mirar el nodo. + log "ERROR: quedaron borrados sin completar tras $PASADAS pasadas" + exit 1 + fi + + log "OK" diff --git a/backup-system/cronjob-image-prune.yaml b/backup-system/cronjob-image-prune.yaml new file mode 100644 index 0000000..adf3b47 --- /dev/null +++ b/backup-system/cronjob-image-prune.yaml @@ -0,0 +1,110 @@ +# Un CronJob por nodo a propósito: un CronJob programa UN pod, así que para +# barrer los dos nodos hacen falta dos objetos fijados con nodeSelector. Un +# DaemonSet no vale (no tiene calendario) y dejarlo suelto lo mandaría siempre +# al mismo sitio. Si se añade un nodo al cluster, hay que añadirlo aquí. +# +# Domingos a las 06:30 y 07:15, escalonados. Es el hueco libre: los backups +# diarios ocupan 02:00, 03:00, 03:30, 04:00 y 05:00, y n8n-workflow-monitor y +# renovate caen cada 6 h en punto (00, 06, 12, 18). El prune castiga el disco +# y no interesa que coincida con nada que esté copiando datos. +apiVersion: batch/v1 +kind: CronJob +metadata: + name: image-prune-n97 + namespace: backup-system +spec: + schedule: "30 6 * * 0" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 1 + jobTemplate: + spec: + # Sin reintentos: si una pasada se queda a medias, el barrido de la semana + # siguiente recoge lo que quedó. Nada urgente que reintentar en caliente. + backoffLimit: 0 + activeDeadlineSeconds: 2400 + template: + spec: + restartPolicy: Never + automountServiceAccountToken: false + nodeSelector: + kubernetes.io/hostname: chemavx-n97 + containers: + - name: image-prune + image: busybox:1.38.0 + command: ["/bin/sh", "/scripts/image-prune.sh"] + securityContext: + # privileged + runAsUser 0: el script hace chroot al host para usar + # el crictl del binario de k3s y su socket de containerd. + privileged: true + runAsUser: 0 + resources: + requests: + cpu: 100m + memory: 64Mi + limits: + cpu: "1" + memory: 256Mi + volumeMounts: + - name: host-root + mountPath: /host + - name: scripts + mountPath: /scripts + volumes: + - name: host-root + hostPath: + path: / + type: Directory + - name: scripts + configMap: + name: image-prune-script + defaultMode: 0755 +--- +apiVersion: batch/v1 +kind: CronJob +metadata: + name: image-prune-k8 + namespace: backup-system +spec: + schedule: "15 7 * * 0" + concurrencyPolicy: Forbid + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 1 + jobTemplate: + spec: + backoffLimit: 0 + activeDeadlineSeconds: 2400 + template: + spec: + restartPolicy: Never + automountServiceAccountToken: false + nodeSelector: + kubernetes.io/hostname: chemavx-k8 + containers: + - name: image-prune + image: busybox:1.38.0 + command: ["/bin/sh", "/scripts/image-prune.sh"] + securityContext: + privileged: true + runAsUser: 0 + resources: + requests: + cpu: 100m + memory: 64Mi + limits: + cpu: "1" + memory: 256Mi + volumeMounts: + - name: host-root + mountPath: /host + - name: scripts + mountPath: /scripts + volumes: + - name: host-root + hostPath: + path: / + type: Directory + - name: scripts + configMap: + name: image-prune-script + defaultMode: 0755