Диагностика и устранение неполадок LCM¶
KeyStack LCM развёртывается как Kubernetes-кластер на базе k0s, а его компоненты (GitLab, NetBox, Nexus3, Vault, Garage и другие) работают как поды в этом кластере. Данный раздел описывает типовые неполадки, которые возникают при установке и эксплуатации LCM, и порядок их диагностики. Все команды выполняются на одном из LCM-узлов от имени непривилегированного пользователя (например, kolla), которому предоставлено право sudo.
Перед детальным разбором конкретного компонента всегда начинайте с общей проверки состояния кластера — она показывает, ограничена ли проблема одним подом, одним узлом или затрагивает весь кластер.
Общая проверка состояния кластера¶
Проверьте состояние узлов. Все LCM-узлы должны быть в статусе
Ready:$ kubectl get nodes -o wide
Проверьте поды, которые не находятся в штатном состоянии (
RunningилиCompleted):$ kubectl get pods -A | grep -vE 'Running|Completed'
Проверьте состояние самого k0s и встроенного containerd на узле:
$ sudo k0s status $ sudo systemctl status k0scontroller
Проверьте список Helm-релизов — все компоненты LCM должны быть в статусе
deployed:$ helm list -A
Поды, которые требуют внимания, как правило, находятся в одном из состояний: Pending, ImagePullBackOff, CreateContainerError, Init:CreateContainerError, CrashLoopBackOff или Error. Каждое из них рассматривается ниже.
Поды не стартуют: образы из airgap-бандла не распакованы¶
Это типовая неполадка в закрытом контуре после установки или обновления компонента с крупными образами (например, GitLab).
Симптомы
Поды зависают в статусе
CreateContainerErrorилиInit:CreateContainerErrorи не выходят из него.В событиях пода присутствует ошибка распаковки слоя образа:
$ kubectl describe pod <pod> -n <namespace>
Причина
При установке в закрытом контуре образы поставляются единым airgap-бандлом. k0s импортирует бандл в content store containerd, но не распаковывает образы в снапшоттер overlayfs. Распаковка выполняется лениво — при первом создании контейнера. Для небольших образов она укладывается в таймаут kubelet runtimeRequestTimeout (по умолчанию 2 минуты), а для крупных образов (около 1 ГиБ и больше) — нет, особенно на медленных дисках. Запрос отменяется по таймауту и повторяется бесконечно.
Примечание
Состояние «распакован / не распакован» хранится отдельно на каждом узле. Под, не запустившийся на одном узле, может нормально стартовать на другом, где образ уже был распакован.
Диагностика
Проверьте, какие образы импортированы, но не распакованы (последний столбец вывода — признак распаковки, true или false):
$ sudo k0s ctr -n k8s.io images check | awk '$NF=="false"{print $1}'
Решение
Распакуйте образы заранее, вне таймаута kubelet. Команда k0s ctr не содержит подкоманды unpack, поэтому используйте монтирование образа (images mount), которое подготавливает полную цепочку снапшотов, с последующим размонтированием:
for ref in $(sudo k0s ctr -n k8s.io images check | awk '$NF=="false"{print $1}'); do
dir=$(mktemp -d)
sudo k0s ctr -n k8s.io images mount --snapshotter overlayfs "$ref" "$dir" \
&& sudo k0s ctr -n k8s.io images unmount "$dir"
rmdir "$dir"
done
Выполните этот скрипт на каждом LCM-узле. Базовые слои переиспользуются между образами одного приложения, поэтому время занимает только распаковка первого крупного образа; остальные распаковываются за секунды. После распаковки kubelet при очередной попытке создаст контейнер мгновенно — удалять поды вручную не требуется.
Важно
Чтобы неполадка не повторялась, распаковывайте новые крупные образы заранее тем же способом сразу после импорта airgap-бандла — до первого запуска подов.
Поды не стартуют: ошибка получения образа¶
Симптомы
Под в статусе ImagePullBackOff или ErrImagePull; в событиях — ошибка обращения к реестру.
Диагностика и решение
Посмотрите точную причину в событиях пода:
$ kubectl describe pod <pod> -n <namespace>
Убедитесь, что образ присутствует в content store узла, на котором планируется под:
$ sudo k0s ctr -n k8s.io images ls | grep <имя-образа>
Если образа нет, проверьте доступность реестра Nexus с узла (по DNS-имени должен определяться IP-адрес, порт должен отвечать):
$ getent hosts docker.<lcm_name>.vm.lab.itkey.com $ curl -sk -o /dev/null -w '%{http_code}\n' https://docker.<lcm_name>.vm.lab.itkey.com/v2/
Ответ
401означает, что реестр доступен и требует аутентификации — это штатное поведение. Если запрос завершается таймаутом или ошибкой DNS, проверьте сетевую связность и записи DNS из раздела Установка KeyStack LCM.
Поды в CrashLoopBackOff: ожидание зависимостей¶
Если контейнер стартует, но завершается с ошибкой (статус CrashLoopBackOff), причина чаще всего во внешней зависимости — базе данных, Redis или ещё не выполненных миграциях.
Изучите логи завершившегося контейнера:
$ kubectl logs <pod> -n <namespace> -c <container> --previous
Для GitLab и NetBox init-контейнер
dependenciesожидает готовности базы данных. Сообщение видаDatabase has not been initialized yetилиThere are N pending migrationsозначает, что не выполнены миграции БД — см. GitLab: база данных не инициализирована.Сообщения об ошибке подключения к Redis или PostgreSQL означают недоступность соответствующего сервиса. Проверьте состояние его подов:
$ kubectl get pods -n <namespace> | grep -E 'redis|cnpg|postgres'
GitLab: база данных не инициализирована¶
Симптомы
Поды gitlab-webservice и gitlab-sidekiq не выходят из init-фазы, а задача миграций завершилась неуспешно.
Диагностика
Проверьте состояние задачи миграций:
$ kubectl get jobs -n lcm-gitlab | grep migrations
Задача в статусе Failed с событием DeadlineExceeded означает, что под миграций не успел отработать в отведённое время — как правило, из-за нераспакованного образа (см. Поды не стартуют: образы из airgap-бандла не распакованы).
Решение
Сначала устраните первопричину — распакуйте образ GitLab на узлах (Поды не стартуют: образы из airgap-бандла не распакованы).
Повторно запустите миграции. Корректнее всего сделать это повторным применением конфигурации GitLab через helmfile, которое пересоздаёт задачу миграций:
$ helmfile -e <режим> -l name=gitlab apply
где
<режим>—multi-nodeилиsingle-node.Дождитесь завершения задачи миграций и убедитесь, что она перешла в статус
Complete:$ kubectl get jobs -n lcm-gitlab | grep migrations
После инициализации базы данных init-контейнеры gitlab-webservice и gitlab-sidekiq пройдут проверку зависимостей, и поды перейдут в статус Running.
Service mesh: поды не получают sidecar или mTLS-трафик блокируется¶
Компоненты LCM работают в service mesh на базе Istio (режим sidecar). Неполадки mesh проявляются как недоступность сервиса при формально «здоровых» подах.
Проверьте, что в поде поднят sidecar-контейнер
istio-proxy(в колонкеREADYучитывается и он):$ kubectl get pod <pod> -n <namespace> -o jsonpath='{.spec.containers[*].name}'
Проверьте конфигурацию mesh с помощью
istioctl:$ istioctl proxy-status $ istioctl analyze -n <namespace>
Если трафик между сервисами блокируется, проверьте политики авторизации и аутентификации в namespace:
$ kubectl get peerauthentication,authorizationpolicy -n <namespace>
Примечание
Прямое взаимодействие подов разных namespace запрещено политиками безопасности; межпроектный трафик должен проходить через egress- и ingress-gateway. Это штатное поведение, а не неполадка.
Хранилище: Ceph и производительность дисков¶
Проверьте состояние кластера Ceph. Фаза Ready и здоровье HEALTH_OK или HEALTH_WARN считаются рабочими:
$ kubectl get cephclusters.ceph.rook.io -n ceph-cluster
$ kubectl get pod -l app.kubernetes.io/name=ceph-osd -n ceph-cluster
Если OSD-поды отсутствуют сразу после установки, дождитесь сборки кластера — в среднем она занимает около 15 минут и зависит от скорости дисков.
Убедитесь, что NFS-сервер доступен по адресу из параметра nfs_server_ip:
$ showmount -e <lcm-01-ip-address>
Медленные диски — частая причина каскадных проблем: затягивается распаковка образов, растёт задержка etcd, дольше выполняются миграции. Синхронная запись особенно чувствительна на потребительских SSD без защиты от потери питания (PLP). Оцените задержку синхронной записи на диске под каталогом данных k0s:
$ sudo fio --name=fsync --filename=/var/lib/k0s/fiotest --rw=write --bs=4k \
--size=256M --fdatasync=1 --ioengine=sync --runtime=20 --time_based
$ sudo rm -f /var/lib/k0s/fiotest
Ориентир для etcd — 99-й перцентиль fdatasync не выше ~10 мс. Существенно большие значения указывают на то, что диск является узким местом.
Предупреждение «too many PGs per OSD»¶
Состояние HEALTH_WARN с сообщением too many PGs per OSD (<N> > max <M>) означает, что среднее число плейсмент-групп (PG) на один OSD превышает мягкий порог mon_max_pg_per_osd (по умолчанию 250). Среднее считается так:
Число PG для каждого пула выбирает автоматический масштабировщик Ceph (PG autoscaler включён для всех пулов) — отдельной настройки в инсталляторе нет. Основной вклад обычно даёт пул объектного хранилища ceph-objectstore.rgw.buckets.data: он помечен как bulk и получает увеличенное число PG даже будучи пустым.
Само по себе предупреждение не блокирует ввод-вывод, восстановление и перебалансировку — порог mon_max_pg_per_osd ограничивает только создание новых пулов и PG. Данные остаются доступны.
Важно
При выводе OSD из эксплуатации (замена узла или диска) те же PG распределяются на меньшее число OSD, поэтому значение «PG на OSD» временно вырастает и предупреждение может усилиться. Это ожидаемо и не препятствует замене. Чтобы предупреждение не мешало во время обслуживания, можно заранее временно поднять порог:
$ kubectl -n ceph-cluster exec deploy/rook-ceph-tools -- ceph config set global mon_max_pg_per_osd 400
Откат — ceph config rm global mon_max_pg_per_osd. Не меняйте pg_num пулов и флаг bulk во время обслуживания: это вызывает слияние и перебалансировку PG.
Ceph не создаёт OSD: диск не подготовлен¶
При повторном использовании диска (после замены узла, переустановки или сбоя OSD) оператор Rook не создаёт на нём OSD, если на диске сохранились остатки прежней разметки: таблица разделов, метаданные LVM или сигнатуры BlueStore от предыдущего OSD. Диск необходимо привести к «чистому» состоянию.
Диагностика
Проверьте, есть ли на диске разделы, точки монтирования и сигнатуры файловых систем:
$ lsblk <диск>
$ sudo blkid -p <диск>
Диск, пригодный для OSD, не должен иметь разделов, точек монтирования и активных «держателей» (holders) — устройств device-mapper от LVM или прежнего OSD.
Решение
Затрите диск и перезапустите оператор Rook, чтобы он пересканировал устройства и создал OSD на чистом диске.
Предупреждение
Затирание безвозвратно уничтожает все данные на диске. Перед запуском убедитесь, что:
диск не несёт корневую файловую систему;
диск (и его разделы) не смонтированы;
у диска нет активных «держателей» — если OSD ещё зарегистрирован в Ceph, сначала удалите его (
ceph osd purge), иначе устройство занято.
Приведённый ниже скрипт выполняет эти проверки и отказывается работать с системным, смонтированным или занятым диском. Метаданные LVM затираются на смещениях, кратных степеням десяти ГБ, в пределах фактического размера диска:
#!/usr/bin/env bash
# Затирает диск до состояния, пригодного для адаптации Rook/ceph-volume под OSD.
# Использование: sudo ./zap_rook_disk.sh /dev/sdX [--yes]
set -euo pipefail
DD_CHUNK_KB=200 # объём, затираемый на каждом смещении (как в upstream Rook)
die() { echo "ERROR: $*" >&2; exit 1; }
[[ $# -ge 1 ]] || die "usage: $0 /dev/sdX [--yes]"
DISK=$1
ASSUME_YES=${2:-}
[[ $EUID -eq 0 ]] || die "must run as root (use sudo)"
[[ -b $DISK ]] || die "$DISK is not a block device"
# Отказ для диска с корневой ФС.
root_src=$(findmnt -n -o SOURCE / || true)
root_disk="/dev/$(lsblk -no PKNAME "$root_src" 2>/dev/null | head -n1)"
[[ $DISK != "$root_disk" ]] || die "$DISK carries the root filesystem — refusing"
# Отказ, если диск (или его раздел) смонтирован.
if lsblk -nro MOUNTPOINT "$DISK" | grep -q .; then
die "$DISK (or a partition) is mounted — unmount first"
fi
# Отказ, если у диска есть активные держатели (LVM/OSD).
if [[ -n "$(ls -A "/sys/block/$(basename "$DISK")/holders" 2>/dev/null)" ]]; then
die "$DISK has active holders (LVM/OSD?) — purge the OSD before zapping"
fi
size_bytes=$(blockdev --getsize64 "$DISK")
size_gib=$((size_bytes / 1024 ** 3))
echo "Target : $DISK"
echo "Size : ${size_gib} GiB (${size_bytes} bytes)"
if [[ $ASSUME_YES != "--yes" ]]; then
read -r -p "This PERMANENTLY ERASES $DISK. Type the device name to confirm: " ans
[[ $ans == "$DISK" ]] || die "confirmation mismatch — aborting"
fi
sgdisk --zap-all "$DISK"
# Затирание метаданных LVM на смещениях 0, 1G, 10G, 100G, ... в пределах размера диска.
dd if=/dev/zero of="$DISK" bs=1K count="$DD_CHUNK_KB" oflag=direct,dsync seek=0
offset_gb=1
while true; do
offset_bytes=$((offset_gb * 1024 ** 3))
((offset_bytes + DD_CHUNK_KB * 1024 > size_bytes)) && break
echo " - ${offset_gb} GB"
dd if=/dev/zero of="$DISK" bs=1K count="$DD_CHUNK_KB" oflag=direct,dsync \
seek=$((offset_gb * 1024 ** 2))
offset_gb=$((offset_gb * 10))
done
# На SSD дополнительно используем blkdiscard (на устройствах без discard — пропускается).
blkdiscard "$DISK" 2>/dev/null || echo "blkdiscard unsupported — relying on dd wipes"
# Удаление остаточных device-mapper артефактов Ceph.
shopt -s nullglob
for dm in /dev/mapper/ceph-*; do dmsetup remove "$dm" || true; done
shopt -u nullglob
rm -rf /dev/ceph-* /dev/mapper/ceph--*
partprobe "$DISK"
blkid -p "$DISK" || echo " (no signatures — disk is clean)"
После затирания перезапустите оператор Rook — он пересканирует устройства и создаст OSD на чистом диске:
$ kubectl -n rook-ceph-system rollout restart deploy/rook-ceph-operator
Скрипт запускается непосредственно на узле с целевым диском и отказывается затирать системный, смонтированный или занятый (с активными «держателями» LVM/OSD) диск.
Высокая нагрузка: шторм контроллеров отчётов Kyverno¶
Симптомы
Аномально высокий load average (десятки–сотни) при низкой полезной загрузке CPU и заметном ожидании ввода-вывода (wa в top). Память при этом почти не используется. В выводе top верхние строки занимают процессы Kyverno background-controller и reports-controller, потребляющие десятки ядер каждый, а следом — kube-apiserver и etcd:
Происходящее — самоподдерживающийся цикл, а не полезная работа. Контроллеры непрерывно отправляют запросы к kube-apiserver, но API-сервер и etcd не справляются с потоком: запросы не укладываются в отведённое время, Kyverno начинает повторять их снова и снова, а затем — сам себя ограничивать. Параллельно контроллер перестаёт удерживать роль лидера. При двух репликах (replicas: 2, окружение multi-node) роль начинает переходить от одной реплики к другой: каждая, получив её, заново запрашивает полный список всех ресурсов кластера — и нагрузка только растёт. Промежуточные объекты EphemeralReport накапливаются, поскольку контроллерам не хватает ресурсов для их слияния в PolicyReport.
Диагностика
Убедитесь, что реальных отчётов немного, а промежуточные
EphemeralReportпродолжают накапливаться. Тысячиephemeralreportsпри единицах или десяткахpolicyreports— признак шторма:$ kubectl get policyreports -A --no-headers | wc -l $ kubectl get clusterpolicyreports --no-headers | wc -l $ kubectl get ephemeralreports.reports.kyverno.io -A --no-headers | wc -l $ kubectl get clusterephemeralreports.reports.kyverno.io --no-headers | wc -l
Проверьте логи контроллеров. На шторм указывают повторяющиеся сообщения о задержке запросов (
Waited before sending request), ошибках соединения с API-сервером (retryable error) и потере роли лидера (Error retrieving lease lock):$ kubectl -n kyverno logs deploy/kyverno-reports-controller --tail=50 \ | grep -iE 'lease|requeue|error|Waited' $ kubectl -n kyverno logs deploy/kyverno-background-controller --tail=50 \ | grep -iE 'lease|requeue|error|Waited'
Решение
Следующие шаги не затрагивают Admission-контроллер — проверка политик при создании ресурсов продолжит работать. Все действия обратимы.
Остановите фоновые контроллеры:
$ kubectl -n kyverno scale deploy kyverno-reports-controller --replicas=0 $ kubectl -n kyverno scale deploy kyverno-background-controller --replicas=0
В течение одной–двух минут
load averageи ожидание ввода-вывода должны заметно снизиться, что подтвердит причину проблемы.Удалите накопившиеся промежуточные отчёты:
$ kubectl delete ephemeralreports.reports.kyverno.io -A --all $ kubectl delete clusterephemeralreports.reports.kyverno.io --all
Запустите контроллеры по одной реплике — это исключит переход роли лидера между репликами как возможную причину шторма. Убедитесь, что роль удерживается стабильно, а
ephemeralreportsне растут лавинообразно:$ kubectl -n kyverno scale deploy kyverno-reports-controller --replicas=1 $ kubectl get ephemeralreports.reports.kyverno.io -A --no-headers | wc -l $ kubectl -n kyverno scale deploy kyverno-background-controller --replicas=1
Важно
Корневая причина почти всегда — медленный диск под etcd: при высокой задержке записи API-сервер не успевает обслуживать запросы Kyverno, и цикл возобновляется. Параллельно с восстановлением проверьте задержку диска и состояние etcd (см. Предупреждение «too many PGs per OSD» и раздел о производительности дисков выше). Пока диск остаётся узким местом, держать две реплики для этих контроллеров не рекомендуется.
Чтобы снизить базовую нагрузку:
для политик, которым не нужно фоновое сканирование, задайте
background: false;сузьте область действия правил
matchвClusterPolicy;если отчёты
PolicyReportне требуются для соответствия регуляторным требованиям — отключитеreports-controller.
Разрешение имён: CoreDNS¶
Сбои разрешения имён внутри кластера проявляются как ошибки i/o timeout или no such host в логах подов.
Проверьте, что поды CoreDNS работают и у сервиса есть эндпоинты:
$ kubectl get pods -n kube-system -l k8s-app=kube-dns -o wide $ kubectl get endpointslices -n kube-system -l k8s-app=kube-dns
Проверьте разрешение внутреннего имени из работающего пода:
$ kubectl exec -n <namespace> <pod> -c <container> -- getent hosts kubernetes.default.svc.cluster.local