oakazanin/content/posts/blog-part-5-debugging/index.md

635 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: "Блог на Hugo в K3s: часть 5 - что делать когда всё внезапно сломалось"
date: 2026-02-17
draft: false
description: "Алгоритм диагностики Hugo блога в K3s за 5 минут: от DNS до пода. Сайт отдаёт 503 — находим причину без паники."
tags: ["kubernetes", "k3s", "traefik", "debugging", "nginx", "troubleshooting"]
categories: ["Kubernetes", "DevOps практики"]
series: ["Блог на Hugo в K3s"]
series_order: 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. Без этого дальше проверять бессмысленно - браузер просто не знает куда направлять запрос.
### Проверка
```bash
# Проверяем 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 может быть правильным, но файрвол может блокировать трафик.
### Проверка
```bash
# Пробуем подключиться извне (важно - НЕ из локальной сети!)
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**
```bash
# Проверяем 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 под не запущен**
```bash
# Проверяем что 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
```
Сертификат невалидный или не выпущен.
```bash
# Проверяем Certificate объект (используй свой namespace)
kubectl get certificate -n blog
# Должно быть READY=True
NAME READY SECRET AGE
blog-tls True blog-tls 2d
```
Если `READY=False` - cert-manager не смог выпустить сертификат.
```bash
# Смотрим что пошло не так
kubectl describe certificate blog-tls -n blog
# Ищем секцию Events внизу вывода - там описание проблемы
```
### Главное: Запрос доходит до Traefik
```bash
# Проверка (с ВНЕШНЕГО сервера!)
curl -I https://blog.example.com
# Ожидаемый результат (любой из двух)
HTTP/2 200 # Всё работает
HTTP/2 503 # Traefik работает, но бэкенд недоступен
# Если connection refused/timeout - проблема в сети (см. выше)
```
---
## Шаг 3: Traefik - правильно ли маршрутизируется трафик
Traefik получил запрос на твой домен. Что он с ним делает? Смотрим логи.
### Проверка логов Traefik
```bash
# Смотрим последние 50 строк логов Traefik
kubectl logs -n traefik deployment/traefik --tail=50
# Фильтруем только свой домен (убираем шум от других сервисов)
kubectl logs -n traefik deployment/traefik --tail=100 | grep blog.example
```
### Что искать в логах
**Нормальный запрос:**
```json
{
"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` = всё хорошо)
**Проблемный запрос:**
```json
{
"request": "GET / HTTP/2.0",
"status": 503,
"router": "blog-blog-https-xxxxx@kubernetescrd",
"error": "no available server"
}
```
Traefik нашёл роутер, но поле `backend` отсутствует - под недоступен или не существует.
### Проверяем список IngressRoute
```bash
# Смотрим все IngressRoute в кластере
kubectl get ingressroute -A
# Фильтруем только свой домен
kubectl get ingressroute -A | grep blog.example
```
**Важный момент:** Если один и тот же домен прописан в **двух разных IngressRoute** из разных namespace - Traefik будет балансировать между ними.
Например:
```bash
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:
```bash
# Удаляем дубль из старого namespace
kubectl delete ingressroute blog-https blog-http -n blog
```
### Проверяем синтаксис match
Traefik очень требователен к синтаксису. Частая ошибка - забыть backticks или скобки.
**Неправильно:**
```yaml
match: Host(blog.example.com) # Нет backticks
match: Host `blog.example.com` # Нет скобок вокруг Host
match: Host("blog.example.com") # Двойные кавычки вместо backticks
```
**Правильно:**
```yaml
match: Host(`blog.example.com`)
```
Проверяем:
```bash
# Смотрим манифест IngressRoute
kubectl get ingressroute blog-https -n blog -o yaml | grep match:
# Должно быть со скобками и backticks
match: Host(`blog.example.com`)
```
### Главное: Traefik нашёл роутер
```bash
# Проверка
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
```bash
# Смотрим 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
```bash
# Смотрим 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
```bash
# Смотрим статус подов
kubectl get pods -n blog
# Видим
NAME READY STATUS
nginx-xxxxxxxxxx-xxxxx 0/1 CreateContainerError
```
Под существует, но не работает. Service правильно не включает его в endpoints. Идём в следующий шаг - разбираемся почему под не запускается.
### Главное: Service видит поды
```bash
# Проверка
kubectl get endpoints nginx -n blog
# Ожидаемый результат - НЕ пустой
nginx 10.42.0.44:80,10.42.2.40:80
# Если <none> - проблема в селекторах или поды не Running
```
---
## Шаг 5: Pod - что происходит внутри контейнера
Самый глубокий уровень. Под не запускается или падает в цикле перезапусков.
### Проверка статуса подов
```bash
# Смотрим все поды в namespace
kubectl get pods -n blog
# Фильтруем только nginx
kubectl get pods -n blog | grep nginx
```
**Возможные статусы проблем:**
### CreateContainerError
Контейнер вообще не может стартануть. Обычно проблема с volumes или образом.
```bash
# Смотрим детали пода (используй своё имя пода)
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`, а образ не импортирован на ноду.
```bash
# Проверяем что образ есть на ноде (используй 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.
```bash
# Проверяем что ConfigMap существует
kubectl get configmap -n blog | grep nginx-config
```
Если нет - создай или исправь имя в Deployment.
### CrashLoopBackOff
Контейнер запускается, но сразу падает. Смотрим логи **предыдущего** запуска:
```bash
# Логи последнего упавшего контейнера
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:
```bash
# Смотрим содержимое конфига
kubectl get configmap nginx-config -n blog -o yaml
```
Находим ошибку, исправляем, применяем. Под перезапустится автоматически.
### Главное: Под работает
```bash
# Проверка
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 или записал не туда.
### Проверка
```bash
# Заходим в под nginx (используй своё имя пода)
kubectl exec -it nginx-xxxxxxxxxx-xxxxx -n blog -- sh
# Внутри пода смотрим что примонтировалось
ls -la /usr/share/nginx/html/
# Должен быть index.html и папки posts, tags, etc
```
**Если директория пустая** - Hugo Builder не сработал. Проверяем его логи:
```bash
# Логи Hugo Builder
kubectl logs -n blog deployment/hugo-builder-prod --tail=50
```
Ищем строку `Build successful!` и список созданных файлов. Если её нет:
1. **Webhook не сработал** - проверь настройки webhook в Gitea
2. **Hugo упал с ошибкой** - читай логи выше, смотри на что ругается
3. **Собрал в другую директорию** - проверь переменную `OUTPUT_DIR` в build.sh
### Главное: Контент на месте
```bash
# Проверка (используй своё имя пода)
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% запросов. Половина запросов работала, половина нет.
Прошёл по алгоритму:
1. ✅ DNS - правильный IP
2. ✅ Сеть - Traefik отвечает
3. ✅ MetalLB - External IP назначен
4. ✅ Traefik - поды Running
5. ❌ IngressRoute - **два роутера на один домен**
```bash
kubectl get ingressroute -A | grep oakazanin
NAMESPACE NAME
blog blog-https # СТАРЫЙ, бэкенд в CreateContainerError
oakazanin blog-https # НОВЫЙ, работает
```
Traefik видел два роутера, честно балансировал трафик 50/50. Каждый второй запрос улетал в мёртвый `blog/nginx`.
Диагноз поставлен за 3 минуты. Лечение - одна команда:
```bash
# Удаляем дубль из старого namespace
kubectl delete ingressroute blog-https blog-http -n blog
```
Мораль: **всегда чисти за собой**. Старые namespace с нерабочими сервисами - источник неочевидных проблем.
---
## Откат и cleanup
Если в процессе диагностики что-то сломал ещё больше - откатываемся:
```bash
# Восстанавливаем предыдущую версию манифеста
kubectl apply -f nginx-deployment.yaml
# Перезапускаем поды принудительно
kubectl rollout restart deployment/nginx -n blog
# Смотрим что изменения применились
kubectl rollout status deployment/nginx -n blog
```
**Золотое правило:** Перед экспериментами делай бэкапы манифестов:
```bash
# Экспортируем текущее состояние с датой
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 диагностики