23 KiB
| title | date | draft | description | tags | categories | series | series_order | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Блог на Hugo в K3s: часть 5 - что делать когда всё внезапно сломалось | 2026-02-17 | false | Сайт работал вчера, а сегодня 503. Алгоритм диагностики Kubernetes проблем за 5 минут - от DNS до пода, без паники и танцев с бубном. |
|
|
|
5 |
В части 4 мы разобрались с Git workflow. Всё работает: пушишь в dev - видишь на тестовом окружении, мержишь в main - публикуется на production.
А потом в один прекрасный день открываешь свой сайт и видишь 503 Service Temporarily Unavailable.
Вчера же все работало! Ты ничего не менял. Что произошло?
Добро пожаловать в мир эксплуатации Kubernetes, где проблемы тоже возникают и требуют системного подхода без паники.
Эта статья - алгоритм диагностики от DNS до пода. Проходишь по шагам сверху вниз, находишь проблему за 5 минут. Не гадаешь, не тыкаешь наугад - работаешь по системе.
Анатомия HTTP запроса в K3s
Прежде чем искать проблему, нужно понять путь запроса от браузера до nginx:
Браузер
↓ DNS запрос
DNS сервер (провайдер или Cloudflare)
↓ Возвращает IP адрес
Роутер/Файрвол (OPNsense, MikroTik)
↓ Port Forward 443 → K3s node
MetalLB LoadBalancer
↓ External IP
Traefik Ingress Controller
↓ IngressRoute matching
Kubernetes Service
↓ Endpoint selection
Pod (Nginx контейнер)
↓ Volume mount
NFS хранилище
Проблема может быть на любом из этих уровней. Секрет эффективной диагностики - проверять снаружи внутрь, последовательно исключая рабочие компоненты.
Когда тыкаешь наугад, проверяя сначала поды, потом DNS, потом снова поды - тратишь время. Когда идёшь по алгоритму - находишь проблему за минуты.
Шаг 1: DNS - доходит ли домен до твоего IP
Первым делом проверяем что домен резолвится в правильный IP. Без этого дальше проверять бессмысленно - браузер просто не знает куда направлять запрос.
Проверка
# Проверяем DNS резолв (используй свой домен)
dig blog.example.com +short
# Альтернатива если dig не установлен
nslookup blog.example.com
Ожидаемый результат
77.37.XXX.XXX
Должен вернуться твой публичный IP адрес (тот который прописан в A-записи у DNS провайдера).
Что может пойти не так
| Симптом | Причина | Как проверить | Решение |
|---|---|---|---|
| Возвращается старый IP | DNS кеш не обновился | dig blog.example.com @8.8.8.8 |
Подожди TTL (обычно 300-3600 сек) |
NXDOMAIN ошибка |
Домен не делегирован | Проверь NS записи у регистратора | Настрой DNS правильно |
Возвращается 127.0.0.1 |
Локальный override | cat /etc/hosts | grep blog |
Удали строку из /etc/hosts |
| Возвращается несколько IP | Round-robin DNS | Проверь все ли IP твои | Удали лишние A-записи |
Если DNS правильный - идём дальше.
Шаг 2: Внешний доступ - доходит ли запрос до сервера
Теперь проверяем что запрос физически доходит до сервера. DNS может быть правильным, но файрвол может блокировать трафик.
Проверка
# Пробуем подключиться извне (важно - НЕ из локальной сети!)
curl -v https://blog.example.com 2>&1 | head -30
Важно: Запускай эту команду с внешнего сервера или используй мобильный интернет. Тест из локальной сети ничего не докажет - можешь обходить файрвол.
Ситуация А: Connection refused или timeout
curl: (7) Failed to connect to blog.example.com port 443: Connection refused
Запрос вообще не дошёл до сервера. Проблема на сетевом уровне.
Возможные причины:
1. Порт 443 закрыт на файрволе/роутере
Проверь Port Forward правила на OPNsense/MikroTik. Должно быть:
WAN:443 → 192.168.X.X:443 (IP любой K3s ноды)
2. MetalLB не назначил External IP для Traefik
# Проверяем MetalLB
kubectl get svc -n traefik traefik
# Ожидаемый результат
NAME TYPE EXTERNAL-IP PORT(S)
traefik LoadBalancer 192.168.X.X 80:30080/TCP,443:30443/TCP
Если EXTERNAL-IP показывает <pending> - MetalLB не работает или пул IP адресов не настроен.
3. Traefik под не запущен
# Проверяем что Traefik работает
kubectl get pods -n traefik
# Должны быть все Running
NAME READY STATUS
traefik-xxxxxxxxxx-xxxxx 1/1 Running
Ситуация Б: TLS handshake прошёл, но 503
< HTTP/2 503
< content-type: text/plain; charset=utf-8
< content-length: 20
no available server
Отлично - наша ситуация! Traefik работает, SSL сертификат отдаёт, но дальше запрос упирается в стену.
Сообщение no available server означает что Traefik нашёл роутер, но не нашёл живой бэкенд за ним.
Проблема внутри кластера. Идём глубже.
Ситуация В: SSL certificate problem
curl: (60) SSL certificate problem: unable to get local issuer certificate
Сертификат невалидный или не выпущен.
# Проверяем Certificate объект (используй свой namespace)
kubectl get certificate -n blog
# Должно быть READY=True
NAME READY SECRET AGE
blog-tls True blog-tls 2d
Если READY=False - cert-manager не смог выпустить сертификат.
# Смотрим что пошло не так
kubectl describe certificate blog-tls -n blog
# Ищем секцию Events внизу вывода - там описание проблемы
Главное: Запрос доходит до Traefik
# Проверка (с ВНЕШНЕГО сервера!)
curl -I https://blog.example.com
# Ожидаемый результат (любой из двух)
HTTP/2 200 # Всё работает
HTTP/2 503 # Traefik работает, но бэкенд недоступен
# Если connection refused/timeout - проблема в сети (см. выше)
Шаг 3: Traefik - правильно ли маршрутизируется трафик
Traefik получил запрос на твой домен. Что он с ним делает? Смотрим логи.
Проверка логов Traefik
# Смотрим последние 50 строк логов Traefik
kubectl logs -n traefik deployment/traefik --tail=50
# Фильтруем только свой домен (убираем шум от других сервисов)
kubectl logs -n traefik deployment/traefik --tail=100 | grep blog.example
Что искать в логах
Нормальный запрос:
{
"request": "GET / HTTP/2.0",
"status": 200,
"size": 8994,
"router": "blog-blog-https-xxxxx@kubernetescrd",
"service": "blog-nginx-blog@kubernetescrd",
"backend": "http://10.42.2.40:80",
"duration": 12
}
Ключевые поля:
- router: Traefik нашёл нужный IngressRoute (
blog-blog-https) - backend: IP пода nginx куда проксируется запрос (
10.42.2.40:80) - status: HTTP код ответа от nginx (
200= всё хорошо)
Проблемный запрос:
{
"request": "GET / HTTP/2.0",
"status": 503,
"router": "blog-blog-https-xxxxx@kubernetescrd",
"error": "no available server"
}
Traefik нашёл роутер, но поле backend отсутствует - под недоступен или не существует.
Проверяем список IngressRoute
# Смотрим все IngressRoute в кластере
kubectl get ingressroute -A
# Фильтруем только свой домен
kubectl get ingressroute -A | grep blog.example
Важный момент: Если один и тот же домен прописан в двух разных IngressRoute из разных namespace - Traefik будет балансировать между ними.
Например:
NAMESPACE NAME AGE
blog blog-https 10d # СТАРЫЙ namespace
blog-new blog-https 2d # НОВЫЙ namespace
Оба IngressRoute имеют match: Host('blog.example.com'). Traefik видит оба, честно балансирует трафик 50/50.
Если один из бэкендов мёртв - половина запросов уходит в пустоту. 503 через раз.
Решение: Удалить старый IngressRoute:
# Удаляем дубль из старого namespace
kubectl delete ingressroute blog-https blog-http -n blog
Проверяем синтаксис match
Traefik очень требователен к синтаксису. Частая ошибка - забыть backticks или скобки.
Неправильно:
match: Host(blog.example.com) # Нет backticks
match: Host `blog.example.com` # Нет скобок вокруг Host
match: Host("blog.example.com") # Двойные кавычки вместо backticks
Правильно:
match: Host(`blog.example.com`)
Проверяем:
# Смотрим манифест IngressRoute
kubectl get ingressroute blog-https -n blog -o yaml | grep match:
# Должно быть со скобками и backticks
match: Host(`blog.example.com`)
Главное: Traefik нашёл роутер
# Проверка
kubectl logs -n traefik deployment/traefik --tail=50 | grep blog.example
# Ожидаемый результат - есть строки с "router": "blog-blog-https"
# Если router не найден - проблема в IngressRoute match синтаксисе
Шаг 4: Service - видит ли он поды
Traefik нашёл роутер, проксирует трафик на Service. Но Service может не видеть поды если selector неправильный.
Проверка endpoints
# Смотрим endpoints для Service (используй своё имя Service)
kubectl get endpoints nginx -n blog
# Ожидаемый результат - НЕ пустой список IP
NAME ENDPOINTS
nginx 10.42.0.44:80,10.42.2.40:80
Если видишь <none> - Service не нашёл ни одного пода. Две возможные причины.
Причина 1: Selector не совпадает с labels
# Смотрим selector у Service
kubectl get svc nginx -n blog -o yaml | grep -A3 "selector:"
# Вывод
selector:
app: nginx
# Смотрим labels у подов
kubectl get pods -n blog --show-labels | grep nginx
# Вывод
nginx-xxxxxxxxxx-xxxxx 1/1 Running app=nginx-old
Видишь проблему? Service ищет app: nginx, а под помечен app: nginx-old. Не совпадает.
Решение: Исправить Deployment или Service чтобы labels совпадали.
Причина 2: Поды не Running
# Смотрим статус подов
kubectl get pods -n blog
# Видим
NAME READY STATUS
nginx-xxxxxxxxxx-xxxxx 0/1 CreateContainerError
Под существует, но не работает. Service правильно не включает его в endpoints. Идём в следующий шаг - разбираемся почему под не запускается.
Главное: Service видит поды
# Проверка
kubectl get endpoints nginx -n blog
# Ожидаемый результат - НЕ пустой
nginx 10.42.0.44:80,10.42.2.40:80
# Если <none> - проблема в селекторах или поды не Running
Шаг 5: Pod - что происходит внутри контейнера
Самый глубокий уровень. Под не запускается или падает в цикле перезапусков.
Проверка статуса подов
# Смотрим все поды в namespace
kubectl get pods -n blog
# Фильтруем только nginx
kubectl get pods -n blog | grep nginx
Возможные статусы проблем:
CreateContainerError
Контейнер вообще не может стартануть. Обычно проблема с volumes или образом.
# Смотрим детали пода (используй своё имя пода)
kubectl describe pod nginx-xxxxxxxxxx-xxxxx -n blog | tail -30
Ищем секцию Events внизу вывода. Там будет описание проблемы:
Пример 1: PVC не примонтировался
Events:
Warning FailedMount MountVolume.SetUp failed for volume "blog-public-pvc":
mount failed: mount.nfs: Connection timed out
NFS хранилище недоступно. Возможные причины:
- NFS сервер выключен или перезагружается
- Неправильный IP или путь в PersistentVolume
- Файрвол блокирует NFS трафик (порт 2049)
Пример 2: Образ не скачался
Events:
Warning Failed Failed to pull image "nginx:latest": rpc error: code = Unknown
Контейнер не может скачать образ. Обычно это означает что imagePullPolicy: Never, а образ не импортирован на ноду.
# Проверяем что образ есть на ноде (используй IP своей worker ноды)
ssh user@192.168.X.X "sudo k3s crictl images | grep nginx"
Если образа нет - импортируй его через k3s ctr images import.
Пример 3: ConfigMap не найден
Events:
Warning FailedMount ConfigMap "nginx-config" not found
Deployment ссылается на несуществующий ConfigMap.
# Проверяем что ConfigMap существует
kubectl get configmap -n blog | grep nginx-config
Если нет - создай или исправь имя в Deployment.
CrashLoopBackOff
Контейнер запускается, но сразу падает. Смотрим логи предыдущего запуска:
# Логи последнего упавшего контейнера
kubectl logs nginx-xxxxxxxxxx-xxxxx -n blog --previous
Пример: Nginx падает из-за неправильного конфига
nginx: [emerg] unexpected "}" in /etc/nginx/nginx.conf:15
nginx: configuration file /etc/nginx/nginx.conf test failed
Синтаксическая ошибка в nginx.conf. Проверяем ConfigMap:
# Смотрим содержимое конфига
kubectl get configmap nginx-config -n blog -o yaml
Находим ошибку, исправляем, применяем. Под перезапустится автоматически.
Главное: Под работает
# Проверка
kubectl get pods -n blog | grep nginx
# Ожидаемый результат - все Running
nginx-xxxxxxxxxx-xxxxx 1/1 Running 0 2d
# Если не Running - смотри troubleshooting выше
Шаг 6: Контент - есть ли файлы для отдачи
Под работает, Service видит его, Traefik проксирует трафик. Но сайт отдаёт 404 Not Found или пустую страницу.
Проблема: Hugo Builder не записал файлы на NFS или записал не туда.
Проверка
# Заходим в под nginx (используй своё имя пода)
kubectl exec -it nginx-xxxxxxxxxx-xxxxx -n blog -- sh
# Внутри пода смотрим что примонтировалось
ls -la /usr/share/nginx/html/
# Должен быть index.html и папки posts, tags, etc
Если директория пустая - Hugo Builder не сработал. Проверяем его логи:
# Логи Hugo Builder
kubectl logs -n blog deployment/hugo-builder-prod --tail=50
Ищем строку Build successful! и список созданных файлов. Если её нет:
- Webhook не сработал - проверь настройки webhook в Gitea
- Hugo упал с ошибкой - читай логи выше, смотри на что ругается
- Собрал в другую директорию - проверь переменную
OUTPUT_DIRв build.sh
Главное: Контент на месте
# Проверка (используй своё имя пода)
kubectl exec -it nginx-xxxxxxxxxx-xxxxx -n blog -- ls /usr/share/nginx/html/ | head -5
# Ожидаемый результат
index.html
posts/
tags/
categories/
# Если пусто - Hugo Builder не отработал (см. выше)
Быстрый чеклист для любой проблемы
Сохрани эту последовательность - она работает для 95% проблем:
[ ] DNS: dig домен → правильный IP?
[ ] Сеть: curl -v https://домен → доходит до Traefik?
[ ] MetalLB: kubectl get svc -n traefik → External IP назначен?
[ ] Traefik: kubectl get pods -n traefik → Running?
[ ] IngressRoute: kubectl get ingressroute -A | grep домен → нет дублей?
[ ] Match синтаксис: Host(`домен`) со скобками и backticks?
[ ] Endpoints: kubectl get endpoints -n namespace → не пустые?
[ ] Selector: labels подов совпадают с selector Service?
[ ] Pods: kubectl get pods -n namespace → все Running?
[ ] PVC: kubectl get pvc -n namespace → все Bound?
[ ] Контент: kubectl exec ls /usr/share/nginx/html → файлы есть?
Проходишь по списку сверху вниз. Останавливаешься на первом [ ] где что-то не так. Чинишь. Проверяешь снова.
Не прыгай хаотично между уровнями. Алгоритм экономит время.
Реальный пример: 503 через раз
Мой сайт отдавал 503 примерно в 50% запросов. Половина запросов работала, половина нет.
Прошёл по алгоритму:
- ✅ DNS - правильный IP
- ✅ Сеть - Traefik отвечает
- ✅ MetalLB - External IP назначен
- ✅ Traefik - поды Running
- ❌ IngressRoute - два роутера на один домен
kubectl get ingressroute -A | grep oakazanin
NAMESPACE NAME
blog blog-https # СТАРЫЙ, бэкенд в CreateContainerError
oakazanin blog-https # НОВЫЙ, работает
Traefik видел два роутера, честно балансировал трафик 50/50. Каждый второй запрос улетал в мёртвый blog/nginx.
Диагноз поставлен за 3 минуты. Лечение - одна команда:
# Удаляем дубль из старого namespace
kubectl delete ingressroute blog-https blog-http -n blog
Мораль: всегда чисти за собой. Старые namespace с нерабочими сервисами - источник неочевидных проблем.
Откат и cleanup
Если в процессе диагностики что-то сломал ещё больше - откатываемся:
# Восстанавливаем предыдущую версию манифеста
kubectl apply -f nginx-deployment.yaml
# Перезапускаем поды принудительно
kubectl rollout restart deployment/nginx -n blog
# Смотрим что изменения применились
kubectl rollout status deployment/nginx -n blog
Золотое правило: Перед экспериментами делай бэкапы манифестов:
# Экспортируем текущее состояние с датой
kubectl get deployment,service,ingressroute -n blog -o yaml > backup-$(date +%Y%m%d).yaml
Что дальше
Ты умеешь диагностировать проблемы. Но лучше их вообще не создавать.
В следующей части покажу как правильно мигрировать сервисы между namespace - без даунтайма, дублей IngressRoute и других сюрпризов которые приводят к 503.
Разберём реальный пример: переносим Gitea между namespace с NFS данными, получаем новый SSL за 32 секунды, и удаляем старый namespace навсегда.
Стек этой части:
- Traefik 2.11 IngressRoute
- Kubernetes 1.30 (K3s)
- kubectl CLI
- curl для внешних проверок
- dig для DNS диагностики