Garbage Collection в Kubernetes: основы

57e0a6a455d6b966227fa272fdfdd60c.png

Привет, Хабр! Сегодня мы рассмотрим механизмы garbage collection в Kubernetes: как удалять orphaned pods, утилизировать устаревшие данные и управлять томами.

Garbage Collection в Kubernetes — это автоматизированный процесс очистки неиспользуемых ресурсов, который предотвращает засорение кластера «мусором». Без GC кластер может превратиться в лабиринт забытых подов, устаревших ConfigMaps и ненужных томов, что очевидно приведет к снижению производительности и увеличению затрат.

Основные компоненты Garbage Collection в Kubernetes

Kubernetes GC заботится о трех основных вещах:

  1. Orphaned Pods: поды без контроллеров.

  2. Stale Data: устаревшие данные, которые больше не нужны.

  3. Volumes: ненужные тома, занимающие место.

Orphaned Pod

Orphaned Pods — это поды, которые утратили своих контроллеров, будь то ReplicaSet, Deployment или другой контроллер. Например, если контроллер внезапно упал или был удален, его поды остаются как «бездомные».

Напишем скрипт, который найдет и удалит orphaned pods. Будем использовать kubectl, jq и немного bash.

#!/bin/bash

# Проверяем, установлен ли jq
if ! command -v jq &> /dev/null; then
    echo "jq не установлен. Пожалуйста, установите jq для выполнения скрипта."
    exit 1
fi

# Получаем все поды в кластере
echo "Получаем список всех подов в кластере..."
PODS=$(kubectl get pods --all-namespaces -o json)

# Фильтруем поды без ownerReferences
echo "Фильтруем orphaned pods..."
ORPHANED_PODS=$(echo "$PODS" | jq -r '.items[] | select(.metadata.ownerReferences | length == 0) | "\(.metadata.namespace) \(.metadata.name)"')

# Проверяем, есть ли orphaned pods
if [ -z "$ORPHANED_PODS" ]; then
    echo "Orphaned pods не найдены. Отличная работа!"
    exit 0
fi

# Удаляем каждый orphaned pod
echo "Начинаем удаление orphaned pods..."
while read -r namespace pod; do
    echo "Удаляем orphaned pod: $namespace/$pod"
    kubectl delete pod "$pod" -n "$namespace" --grace-period=30 --timeout=30s || {
        echo "Не удалось удалить pod: $namespace/$pod"
    }
done <<< "$ORPHANED_PODS"

echo "Очистка orphaned pods завершена."

Теперь автоматизируем этот процесс, настроив CronJob, который будет выполнять скрипт каждый час.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: cleanup-orphaned-pods
spec:
  schedule: "0 * * * *" # Каждый час
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: gc-cleanup-sa
          containers:
          - name: cleanup
            image: bitnami/kubectl:latest
            command:
              - /bin/bash
              - -c
              - |
                # Ваш скрипт здесь
                PODS=$(kubectl get pods --all-namespaces -o json)
                ORPHANED_PODS=$(echo $PODS | jq -r '.items[] | select(.metadata.ownerReferences | length == 0) | "\(.metadata.namespace) \(.metadata.name)"')
                if [ -z "$ORPHANED_PODS" ]; then
                    echo "Orphaned pods не найдены. Отличная работа!"
                    exit 0
                fi
                while read -r namespace pod; do
                    echo "Удаляем orphaned pod: $namespace/$pod"
                    kubectl delete pod "$pod" -n "$namespace" --grace-period=30 --timeout=30s
                done <<< "$ORPHANED_PODS"
          restartPolicy: OnFailure

Чтобы CronJob мог удалять поды, необходимо настроить соответствующие права доступа.

---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: gc-cleanup-sa
  namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: gc-cleanup-role
  namespace: default
rules:
- apiGroups: [""]
  resources: ["pods", "configmaps", "persistentvolumes"]
  verbs: ["get", "list", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: gc-cleanup-rb
  namespace: default
subjects:
- kind: ServiceAccount
  name: gc-cleanup-sa
  namespace: default
roleRef:
  kind: Role
  name: gc-cleanup-role
  apiGroup: rbac.authorization.k8s.io

Утилизация Stale Data

Stale Data — это данные, которые больше не используются, но продолжают занимать место. Это могут быть старые ConfigMaps, Secrets, логи или временные файлы.

Создадим скрипт для удаления ConfigMaps старше 30 дней.

#!/bin/bash

# Проверяем, установлен ли jq
if ! command -v jq &> /dev/null
then
    echo "jq не установлен. Пожалуйста, установите jq для выполнения скрипта."
    exit 1
fi

# Устанавливаем пороговое время (30 дней назад)
THRESHOLD=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ)
echo "Удаляем ConfigMaps, созданные до $THRESHOLD..."

# Получаем все ConfigMaps
CONFIGMAPS=$(kubectl get configmaps --all-namespaces -o json)

# Фильтруем ConfigMaps, созданные раньше порога
OLD_CONFIGMAPS=$(echo $CONFIGMAPS | jq -r --arg THRESHOLD "$THRESHOLD" '.items[] | select(.metadata.creationTimestamp < $THRESHOLD) | "\(.metadata.namespace) \(.metadata.name)"')

# Проверяем, есть ли старые ConfigMaps
if [ -z "$OLD_CONFIGMAPS" ]; then
    echo "Старые ConfigMaps не найдены. Отличная работа!"
    exit 0
fi

# Удаляем старые ConfigMaps с подтверждением
while read -r namespace configmap; do
    echo "Удаляем ConfigMap: $namespace/$configmap"
    kubectl delete configmap "$configmap" -n "$namespace" --grace-period=30 --timeout=30s
    if [ $? -ne 0 ]; then
        echo "Не удалось удалить ConfigMap: $namespace/$configmap"
    else
        echo "ConfigMap $namespace/$configmap успешно удален."
    fi
done <<< "$OLD_CONFIGMAPS"

echo "Очистка старых ConfigMaps завершена."

Настроим CronJob для автоматической очистки ConfigMaps.

apiVersion: batch/v1
kind: CronJob
metadata:
  name: cleanup-configmaps
spec:
  schedule: "0 2 * * 0" # Каждое воскресенье в 02:00
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: gc-cleanup-sa
          containers:
          - name: cleanup
            image: bitnami/kubectl:latest
            command:
              - /bin/bash
              - -c
              - |
                THRESHOLD=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ)
                CONFIGMAPS=$(kubectl get configmaps --all-namespaces -o json)
                OLD_CONFIGMAPS=$(echo $CONFIGMAPS | jq -r --arg THRESHOLD "$THRESHOLD" '.items[] | select(.metadata.creationTimestamp < $THRESHOLD) | "\(.metadata.namespace) \(.metadata.name)"')
                if [ -z "$OLD_CONFIGMAPS" ]; then
                    echo "Старые ConfigMaps не найдены. Отличная работа!"
                    exit 0
                fi
                while read -r namespace configmap; do
                    echo "Удаляем ConfigMap: $namespace/$configmap"
                    kubectl delete configmap "$configmap" -n "$namespace" --grace-period=30 --timeout=30s
                done <<< "$OLD_CONFIGMAPS"
          restartPolicy: OnFailure

Управление Volumes

Volumes могут стать настоящей головной болью, если не управлять ими должным образом. Старые PersistentVolumes и PersistentVolumeClaims, которые больше не нужны, продолжают занимать место и ресурсы.

Настроим StorageClass с reclaimPolicy: Delete, чтобы при удалении PVC автоматически удалялся связанный PV и физический том.

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp2
reclaimPolicy: Delete

Однако иногда бывает нужно вручную очищать неиспользуемые тома. Для этого создадим скрипт:

#!/bin/bash

# Проверяем, установлен ли jq
if ! command -v jq &> /dev/null
then
    echo "jq не установлен. Пожалуйста, установите jq для выполнения скрипта."
    exit 1
fi

echo "Получаем список всех PVs..."
PVS=$(kubectl get pv -o json)

# Находим PV без связанных PVC
UNBOUND_PVS=$(echo $PVS | jq -r '.items[] | select(.spec.claimRef == null) | .metadata.name')

# Проверяем, есть ли незакрепленные PV
if [ -z "$UNBOUND_PVS" ]; then
    echo "Незакрепленные PV не найдены. Отличная работа!"
    exit 0
fi

# Удаляем незакрепленные PV
for pv in $UNBOUND_PVS; do
    echo "Удаляем незакрепленный PV: $pv"
    kubectl delete pv "$pv" --grace-period=30 --timeout=30s
    if [ $? -ne 0 ]; then
        echo "Не удалось удалить PV: $pv"
    else
        echo "PV $pv успешно удален."
    fi
done

echo "Очистка незакрепленных PV завершена."

Настроим CronJob для автоматической очистки PV

apiVersion: batch/v1
kind: CronJob
metadata:
  name: cleanup-unbound-pvs
spec:
  schedule: "0 3 * * *" # Каждый день в 03:00
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: gc-cleanup-sa
          containers:
          - name: cleanup
            image: bitnami/kubectl:latest
            command:
              - /bin/bash
              - -c
              - |
                PVS=$(kubectl get pv -o json)
                UNBOUND_PVS=$(echo $PVS | jq -r '.items[] | select(.spec.claimRef == null) | .metadata.name')
                if [ -z "$UNBOUND_PVS" ]; then
                    echo "Незакрепленные PV не найдены. Отличная работа!"
                    exit 0
                fi
                for pv in $UNBOUND_PVS; do
                    echo "Удаляем незакрепленный PV: $pv"
                    kubectl delete pv "$pv" --grace-period=30 --timeout=30s
                done
          restartPolicy: OnFailure

OwnerReferences и Finalizers

Теперь рассмотрим как Kubernetes решает, что удалять, а что оставлять. Основные механизмы здесь — это ownerReferences(использовали в коде выше) и finalizers.

OwnerReferences

ownerReferences — это механизм, позволяющий Kubernetes отслеживать зависимости между объектами. Например, ReplicaSet владеет подами, а Deployment владеет ReplicaSet. Если объект-родитель удаляется, Kubernetes GC автоматически удалит все объекты-потомки.

Связывание Pod с ReplicaSet через ownerReferences:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: frontend
spec:
  replicas: 3
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: frontend
        image: nginx
---
apiVersion: v1
kind: Pod
metadata:
  name: frontend-pod
  ownerReferences:
  - apiVersion: apps/v1
    kind: ReplicaSet
    name: frontend
    uid: 
spec:
  containers:
  - name: frontend
    image: nginx

Когда ReplicaSet frontend удаляется, Kubernetes автоматом удалит все поды, связанные с ним через ownerReferences.

Finalizers

finalizers — это способ задержать удаление объекта до выполнения определённых действий

Предположим, есть Custom Resource, и хочется выполнить некоторые действия перед его удалением:

apiVersion: mydomain.com/v1
kind: MyResource
metadata:
  name: my-resource
  finalizers:
  - mydomain.com/finalizer
spec:
  # спецификация ресурса

Когда вы удаляете этот ресурс, Kubernetes сначала добавляет его в статус deletionTimestamp, но не удаляет до тех пор, пока все finalizers не будут удалены. Контроллер должен отследить это и выполнить необходимые действия:

func (r *MyResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    var resource myv1.MyResource
    if err := r.Get(ctx, req.NamespacedName, &resource); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    if !resource.ObjectMeta.DeletionTimestamp.IsZero() {
        // Выполняем очистку
        if err := cleanup(resource); err != nil {
            return ctrl.Result{}, err
        }
        // Удаляем finalizer
        resource.ObjectMeta.Finalizers = remove(resource.ObjectMeta.Finalizers, "mydomain.com/finalizer")
        if err := r.Update(ctx, &resource); err != nil {
            return ctrl.Result{}, err
        }
        return ctrl.Result{}, nil
    }

    // Основная логика контроллера
    return ctrl.Result{}, nil
}

Как оптимизировать все это дело

Garbage Collection может быть ресурсоемким, если не оптимизировать.

Reclaim Policies

reclaimPolicy определяет, что делать с PV при удалении PVC. Есть два варианта:

  • Delete: автоматически удаляет физический том при удалении PVC.

  • Retain: оставляет физический том, чтобы его можно было повторно использовать.

Настроим Reclaim Policy:

apiVersion: storage.k8s.io/v1
kind: PersistentVolume
metadata:
  name: pv-example
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: standard
  awsElasticBlockStore:
    volumeID: 
    fsType: ext4

Юзаем Delete для временных или легко восстанавливаемых данных, и Retain для критически важных данных. Главное — понимать, что именно нужно и как долго хотите хранить данные.

TTL Controllers

TTL Controllers позволяют автоматически удалять объекты через заданное время жизни.

Настроим TTL для Jobs:

apiVersion: batch/v1
kind: Job
metadata:
  name: example-job
  ttlSecondsAfterFinished: 3600 # Удаление через 1 час
spec:
  template:
    spec:
      containers:
      - name: job
        image: busybox
        command: ["sleep", "60"]
      restartPolicy: Never

Так кластер не будет засоряться старыми Jobs после их выполнения.

Параллельная очистка

Если у вас большой кластер, разбивайте задачи очистки на параллельные части. Например, можно использовать несколько CronJobs для разных типов ресурсов.

Разделение очистки ConfigMaps и PVCs:

# CronJob для ConfigMaps
apiVersion: batch/v1
kind: CronJob
metadata:
  name: cleanup-configmaps
spec:
  schedule: "0 2 * * 0"
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: gc-cleanup-sa
          containers:
          - name: cleanup
            image: bitnami/kubectl:latest
            command:
              - /bin/bash
              - -c
              - |
                THRESHOLD=$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ)
                CONFIGMAPS=$(kubectl get configmaps --all-namespaces -o json)
                OLD_CONFIGMAPS=$(echo $CONFIGMAPS | jq -r --arg THRESHOLD "$THRESHOLD" '.items[] | select(.metadata.creationTimestamp < $THRESHOLD) | "\(.metadata.namespace) \(.metadata.name)"')
                if [ -z "$OLD_CONFIGMAPS" ]; then
                    echo "Старые ConfigMaps не найдены. Отличная работа!"
                    exit 0
                fi
                while read -r namespace configmap; do
                    echo "Удаляем ConfigMap: $namespace/$configmap"
                    kubectl delete configmap "$configmap" -n "$namespace" --grace-period=30 --timeout=30s
                done <<< "$OLD_CONFIGMAPS"
          restartPolicy: OnFailure

# CronJob для PVCs
apiVersion: batch/v1
kind: CronJob
metadata:
  name: cleanup-unbound-pvs
spec:
  schedule: "0 3 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: gc-cleanup-sa
          containers:
          - name: cleanup
            image: bitnami/kubectl:latest
            command:
              - /bin/bash
              - -c
              - |
                PVS=$(kubectl get pv -o json)
                UNBOUND_PVS=$(echo $PVS | jq -r '.items[] | select(.spec.claimRef == null) | .metadata.name')
                if [ -z "$UNBOUND_PVS" ]; then
                    echo "Незакрепленные PV не найдены. Отличная работа!"
                    exit 0
                fi
                for pv in $UNBOUND_PVS; do
                    echo "Удаляем незакрепленный PV: $pv"
                    kubectl delete pv "$pv" --grace-period=30 --timeout=30s
                done
          restartPolicy: OnFailure

Заключение

Мы узнали, как Kubernetes сам чистит свой «мусор», удаляя забытые поды, старые ConfigMaps и ненужные тома. Благодаря этим настройкам кластер будет работать быстрее и без лишних затрат.

Если хотите углубиться дальше, посмотрите на Kubernetes Operators, освоите Helm Charts или изучите инструменты мониторинга типа Prometheus и Grafana. Все это поможет сделать ваш кластер еще более надежным.

Больше актуальных навыков по IT-инфраструктуре вы можете получить в рамках практических онлайн-курсов от экспертов отрасли.

А 17 декабря пройдет открытый урок, посвящённый внутреннему устройству Kubernetes. Вы узнаете, из каких ключевых компонентов состоит Kubernetes, как они взаимодействуют между собой и как настроить их для эффективного управления контейнерными приложениями. Если интересно, записывайтесь по ссылке.

© Habrahabr.ru