backup-system: barrido semanal de imágenes containerd por nodo
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user