Головна / Статті

Реальний приклад самовідновлення Kubernetes: три збої, сигнали від SigNoz та схвалені способи усунення.

Самовідновлюваний Kubernetes з OpenTelemetry та SigNoz: цикл збоїв, вимкнення через нестачу пам’яті та різкий зріст кількості токенів на Kind, що показує кожен сигнал, та схвалені AI-рішення.

Видавець: REACTAPP.TOP11 хв читання2499 слів

Джерела: Self-Healing Kubernetes: Wiring OpenTelemetry, SigNoz, and a Groq-Powered Remediation Agent (PART 7)

У демо-версії є кілька рухомих елементів. Локальний кластер Kind працює як хост для SigNoz, який обробляє трейси та журнали, а також для Python-додатку, троє кінцевих точок якого навмисно нефункціональні. Агент для усунення проблем перевіряє наявність аномалій та запитує у Groq діагноз, а панель керування на базі React дозволяє оператору схвалити чи відхилити запропоноване рішення перед тим, як щось буде запущено. Коли все це створено, найцікавіше — спостерігати за його роботою. Цей тест імітує кожну несправність у режимі реального часу, аналізує дані, які SigNoz записує для кожного інциденту, та вказує, що потрібно змінити, перш ніж хтось зможе довіряти цій конфігурації поза межами ноутбука.

Підтвердіть, що стек працює

Перед тим, як буде імітована будь-яка несправність, має працювати п’ять елементів. Перша перевірка — це стан кожного pod у обох просторах імен.

# All pods should show Running
kubectl get pods -n observability
kubectl get pods -n apps

Для порт-форвардингу SigNoz, сервісу підсумовування та панелі керування кожному потрібна власна вкладка терміналу, а останній команд у блоку відстежує журнали агента.

# Port-forwards (run each in its own tab if not already open)
# Tab A: kubectl port-forward -n observability svc/signoz 8080:8080
# Tab B: kubectl port-forward -n apps svc/summarizer 8000:80
# Tab C: kubectl port-forward -n apps svc/approval-dashboard 9000:9000
# Tab D: kubectl port-forward -n apps svc/approval-frontend-svc 3000:3000# Confirm the agent is polling
kubectl logs -n apps deployment/remediation-agent --tail=5

Нещодавні повідомлення про стабільну роботу у цьому виводі означають, що агент у гарному стані. Контейнер, який не працює, слід спочатку перевірити за допомогою kubectl describe та виправити. Залиште відкритими дві вкладки браузера: SigNoz на порту 8080 та панель керування на порту 3000.

Сценарій один: CrashLoopBackOff

Встановлення змінної середовища KILL_ME у конфігурації summarizer та подальше звернення до його точки краху призводить до завершення процесу під час обробки запиту.

kubectl set env deployment/summarizer -n apps KILL_ME=true
curl -s http://localhost:8000/crash

Виклик curl може нічого не повернути або застрягти, оскільки процес завершується під час виконання. Спостереження за подами показує, що лічильник перезапусків зростає, а статус змінюється між Running та CrashLoopBackOff, причому два-три цикли вже достатні.

kubectl get pods -n apps -w

У SigNoz. Огляд сервісу підсумовування показує, що рівень помилок стрімко піднімається до 100% як тільки под починає зламуватися, тоді як частота запитів знижується до нуля, коли под не працює між перезапусками. Послідовність «трафік, потім нічого, знову трафік» є візуальною ознакою циклу збоїв. Процес розгортання виглядає інакше — із чітким переходом та без сплеску помилок, а ситуація з розділенням мережі також відрізняється: рівень помилок зростає, поки под залишається у стані «Running».

Вкладка «Журнали», відфільтрована на сервіс підсумовування, містить критичну строку, яку додаток вивів безпосередньо перед зупинкою.

CRITICAL summarizer — KILL_ME is true, exiting process

У цьому журналі міститься ідентифікатор сліду запиту, який його спричинив, тож один клік переносить користувача від рядка журналу до самого сліду та до конкретного HTTP-запиту, який завершив процес.

Агент реагує. Через 30 секунд після першої збою запускається цикл опитування. Він фіксує рівень помилок 1,0, що перевищує поріг у 0,10, об’єднує сигнали та викликає Groq. Журнали агента відображають цю послідовність дій.

kubectl logs -n apps deployment/remediation-agent --tail=20 -f
WARNING  agent — crash loop signal: error_rate=1.00
INFO     agent — anomaly detected: crash_loop — calling Groq for diagnosis
INFO     agent — diagnosis: type=crash_loop severity=critical confidence=0.91
           fix=kubectl set env deployment/summarizer -n apps KILL_ME-
INFO     incident_poster — incident posted: id=<uuid> type=crash_loop

Схвалення. Панель керування відображає картку приблизно за десять секунд, і запропоноване рішення є правильним, тому оператор натискає «Схвалити». Бекенд перевіряє, чи команда починається з kubectl set env та не містить заблокованих термінів, а потім її виконує. Картка оновлюється, щоб підтвердити схвалення та зміну середовища.

✓ Approved by operator
deployment.apps/summarizer env updated

У режимі спостереження в капсулі наступна спроба запуску вдається, цикл збоїв закінчується, а рівень помилок SigNoz знову стає нульовим. Перед наступним сценарієм швидка перевірка підтверджує, що змінна більше не існує.

# The approve action already removed KILL_ME, but confirm:
kubectl get deployment summarizer -n apps -o jsonpath='{.spec.template.spec.containers[0].env}' | jq

KILL_ME не повинен з’являтися у результаті.

Сценарій два: OOMKill

Цикл запитів POST до кінцевої точки витоку поступово збільшує об’єм бака в пам’яті.

for i in {1..30}; do
  curl -s -X POST http://localhost:8000/leak | jq -c '{leaked_mb,chunks}'
  sleep 0.5
done

Об’єм пам’яті в поді зростає, і приблизно на 25-му виклику под вмирає посеред циклу, після чого curl починає повертати помилки підключення.

У SigNoz. Цього разу частота помилок не зростає поступово. Завершення роботи через нестачу пам’яті відбувається раптово, і додаток ніколи не повертає відповіді про помилку. Натомість частота запитів різко падає: спочатку значний спад трафіку, потім перерва, а після перезапуску поду трафік знову з’являється.

У журналах подій міститься справжня інформація. Після фільтрації для підсумовувача перед перервою з’являються попередження про зростання розміру бака.

WARNING summarizer — leak endpoint hit, bucket now holds 10 MB
WARNING summarizer — leak endpoint hit, bucket now holds 20 MB
...
WARNING summarizer — leak endpoint hit, bucket now holds 240 MB

After the last warning comes silence, with no exception, no stack trace and no graceful shutdown message, because the kernel's OOM killer gives the process no chance to log. A run of climbing warnings followed by nothing is the pattern worth recognizing.

The cluster confirms the cause from its side.

kubectl describe pod -n apps -l app=summarizer | grep -A 5 "Last State"
Last State:     Terminated
  Reason:       OOMKilled
  Exit Code:    137

Код вихіду 137 є унікальним позначенням ядра — це SIGKILL (сигнал 9) плюс 128. Він дозволяє відрізнити вбивство процесу через нестачу пам’яті від збою додатку, який завершується з кодом 1, а також від невдачі перевірки життєздатності, яка також дає код 137, але з іншою рядком причини у результаті опису.

Агент реагує. Його перевірка на нестачу пам’яті шукає в індексі журналів SigNoz рядок OOMKilled протягом останніх десяти хвилин. У разі знаходження він об’єднує зразки журналів та подію перезапуску поду в одне повідомлення, після чого надходить діагноз.

type=oom_kill severity=critical confidence=0.88
fix=kubectl set resources deployment/summarizer -n apps \
  --containers=summarizer --limits=memory=512Mi

Пропоноване рішення подвоює ліміт пам’яті. Це розумний короткостроковий захід, адже він дає час на впровадження виправлення до програми. Після схвалення бекенд встановлює ліміт у 512 МБ, Kubernetes здійснює поступове перезапускання, і под повертається з достатньою кількістю ресурсів.

Сама витікання пам’яті залишається у коді. Ця зміна лише підвищує верхню межу, а справжній інцидент вимагатиме виправлення коду, яке буде періодично усувати це витікання. Завданням агента є зупинити втрати ресурсів, тоді як пошук первинної причини залишається завданням людини.

Сценарій третій: затримка та різкий стрибок кількості токенів

Короткий цикл викликів до дорогого кінцевого пункту призводить до третьої несправності.

for i in {1..5}; do
  curl -s -X POST http://localhost:8000/expensive \
    | jq '{output_tokens, length_chars}'
  sleep 2
done

Ніщо не збивається з роботи та ніщо не завершується, і под залишається у стані Running. Однак час відповідей становить від 8 до 15 секунд, а кількість токенів наближається до максимального значення у 2 048.

У SigNoz. Саме у таких „тихих“ збоях проявляється корисність інструментів для спостереження. Лінія затримки p95, яка під час звичайного трафіку залишається близькою до 1,5 секунди, різко піднімається під час виконання дорогих викликів; форма змін є різкою, а не поступовою, оскільки кожен виклик досягає максимального ліміту. Сортування треків за тривалістю виводить ці виклики на вершину — їхній час тривання становить від 8 до 15 секунд.

При відкритті повільного треку в графіку полум’я видно майже весь час, що проходить у межах одного під-спану, як час виконання вихідного HTTP-запиту до api.groq.com. Спан для самого обробника кінцевої точки є дуже коротким, оскільки обробник FastAPI майже нічого не робить, а основну частину часу займає обмін даними з LLM. Довжина вмісту відповіді цього спану значно більша, ніж у звичайного запиту на узагальнення — це кількість токенів вихідних даних, які передаються у вигляді байтів. Плоска лінія, підйом, знову плоска лінія після завершення циклу — ось характеристики неправильно налаштованої кінцевої точки, а не проблеми інфраструктури.

Агент реагує. Перевірка затримки порівнює значення p95 за маршрутами. Дорогий маршрут показує значення близько 11 000 мс, що перевищує поріг у 5 000 мс, і агент об’єднує ці дані для аналізу.

{
  "check": "latency_spike",
  "spikes": { "/expensive": 11240.0 },
  "p95_ms_by_route": {
    "/summarize": 1820.0,
    "/expensive": 11240.0,
    "/healthz": 4.0
  }
}

Groq читає його та ставить точний діагноз.

type=token_spike severity=high confidence=0.85
root_cause: The /expensive endpoint has no reasonable max_tokens budget
            and a prompt that invites long outputs, causing p95 latency
            to spike to 11s versus 1.8s on the normal /summarize path.
fix=kubectl set env deployment/summarizer -n apps MAX_TOKENS_EXPENSIVE=256

Пропоноване рішення полягає у встановленні змінної середовища, яку додаток може читати для обмеження кількості токенів у цьому кінцевому пункті. Після натискання «Approve» ця змінна встановлюється. Під час реального розгортання додаток також буде читати значення MAX_TOKENS_EXPENSIVE з середовища. Агент пропонує зміну конфігурації, а не зміну коду, що є правильним підходом для виправлення на рівні kubectl.

Три збої протягом одного періоду часу

При послідовному виконанні всіх трьох сценаріїв огляд у форматі резюме протягом двогодинного періоду демонструє три різні закономірності.

  • Цикл збоїв проявляється у формі зубців гірки у кількості помилок, яка досягає 100%, потім падає до нуля та знову зростає, з відповідними перервами у пропускній здатності.
  • Зупинка процесу через нестачу пам’яті проявляється у різкому спаді кількості запитів без підвищення кількості помилок, оскільки процес так і не встиг повернути повідомлення про помилку.
  • Різкий стрибок кількості токенів є чітким показником збільшення затримки p95, водночас рівень помилок залишається стабільним, оскільки под ніколи не збивався з роботи, а лише ставав повільнішим та дорожчим у використанні.
  • На одному панелі керування відображаються три способи збою із трьома розпізнаваними формами, без необхідності налаштовувати власні правила сповіщень чи аналізувати журнали вручну. Це результат використання взаємопов’язаних треків, метрик та журналів.

    Зміни, необхідні для продакшну

    Демо-версія працює на ноутбуці та ігнорує аспекти, які мають значення в інших ситуаціях.

    Замініть пам’ятний сховище інцидентів. Список інцидентів зберігається у вигляді словника Python усередині пода-бекенду, тому після перезапуску втрачаються всі дані. Використання SQLite через SQLAlchemy — це зміна, яка потребує лише однієї рядка коду та не вимагає додаткових сервісів, а Redis підходить, коли панель керування має масштабуватися через кілька реплік бекенду. Формат API залишається незмінним, змінюється лише сховище даних.

    Додайте сповіщення в Slack. Наразі хтось мусить стежити за панеллю керування, щоб дізнатися про з’явлення картки. Використання вебхука Slack у модулі прийому інцидентів на бекенді вирішує цю проблему.

    import httpx
    
    SLACK_WEBHOOK = os.environ.get("SLACK_WEBHOOK_URL")def notify_slack(card: dict):
        if not SLACK_WEBHOOK:
            return
        d = card["diagnosis"]
        httpx.post(SLACK_WEBHOOK, json={
            "text": (
                f"*{d['severity'].upper()} — {d['incident_type']}* on `{card['service']}`\n"
                f"Root cause: {d['root_cause']}\n"
                f"Proposed fix: `{d['proposed_fix']}`\n"
                f"Confidence: {int(d['confidence']*100)}%\n"
                f"<http://your-dashboard-url|Review on dashboard>"
            )
        })
    

    Виклик сповіщувача наприкінці процесу отримання даних означає, що кожен новий інцидент надсилає сигнал у канал чергового оператора, і особі, яка схвалює, більше не потрібно стежити за вкладкою браузера.

    Належним чином керуйте обліковими даними кластера. Бекенд використовує kubectl, що є прийнятним, якщо середовище вузла це налаштовує. У справжньому кластері бекенд-поду потрібен ServiceAccount із відповідними правами RBAC та Python-клієнт Kubernetes, налаштований для роботи з обліковими даними всередині кластера.

    from kubernetes import client, config
    
    config.load_incluster_config()
    apps_v1 = client.AppsV1Api()
    core_v1 = client.CoreV1Api()
    

    Кожен схвалений тип команди стає прямим викликом SDK, а не рядком у шеллі. Це більш трудомістке, але забезпечує структуроване оброблення помилок, журналування операцій на сервері API та відсутність ризиків вставки коду у шеллі.

    Запровадіть більш суворий список дозволених команд. Демо-версія приймає будь-яку команду kubectl set env. У продакшені її слід обмежити лише названими розгортаннями та просторами імен.

    ALLOWED_TARGETS = {
        "deployment/summarizer -n apps",
        "deployment/worker -n apps",
    }
    
    def validate_command(cmd: str) -> bool:
        if not any(cmd.startswith(p) for p in ALLOWED_PREFIXES):
            return False
        if any(term in cmd for term in BLOCKED_TERMS):
            return False
        if not any(target in cmd for target in ALLOWED_TARGETS):
            return False
        return True
    

    Модель не повинна мати можливості застосовувати виправлення до будь-якого простору імен чи розгортання поза списком, який їй був наданий.

    Проекти, які варто вивчити далі

    Демо-версія була створена з нуля, щоб показати, як взаємодіють окремі компоненти, тоді як у продакшні використовуватимуться вже існуючі проекти.

    • KubeStellar допомагає, коли навантаження розподілене між кількома кластерами. Він бере на себе роботу контрольної платформи, яка ускладнює усунення проблем між кластерами, наприклад, визначаючи, у якому кластері знаходиться інцидент, перш ніж пропонувати будь-яке виправлення.
    • OpenTelemetry пропонує значно більше функцій, ніж потрібно для демо. Його збирач має процесори та експортери, які можуть автоматично додавати метадані Kubernetes до спанів, тому атрибути ресурсів не потрібно додавати вручну у маніфести додатків.
  • KEDA охоплює категорію масштабування — цей механізм виправляє ситуації, коли агент іноді пропонує зміни, які людина не повинна схвалювати окремо. Якщо зміна полягає завжди у збільшенні масштабування, коли глибина черги перевищує певне значення, це має бути реалізовано через KEDA ScaledObject, а не через схвалення людини.
  • Robusta та HolmesGPT є продакшн-готовими версіями того, що було створено тут. Після ознайомлення з рівнем запитів, структурою запрошень та механізмом схвалення, їхній код стає легким для розуміння.
  • Що включає в себе весь проект

    Готова система — це кластер Kind із трьома вузлами з реалістичною топологією планування багатьох вузлів, а SigNoz розгорнутий за допомогою Helm; він обробляє сліди, метрики та журнали через OpenTelemetry Collector. Сервіс Python FastAPI автоматично оснащується інструментами під час запуску, тож кожен HTTP-запит та виклик LLM стає слідом, а кожен рядок журналу корелюється без необхідності використання коду інструментування в додатку.

    Три пошкоджені кінцеві точки спричиняють три найпоширеніші проблеми в Kubernetes: зупинку процесу, вичерпання пам’яті та погіршення якості та затримки. Агент для усунення проблем періодично перевіряє SigNoz кожні 30 секунд, застосовує перевірки порогових значень, об’єднує сигнали у структуроване запитання, запитує у ШІ діагноз та створює картку інциденту. Бекенд на FastAPI отримує ці картки, перевіряє запропоновані рішення за списком дозволених та виконує схвалені команди через kubectl. Панель керування на React додає функції схвалення та відхилення, індикатор рівня впевненості моделі, попередження для сумнівних діагнозів та журнал кожної виконаної дії.

    Усе працює локально за допомогою API безкоштовного тарифу та не коштує нічого. Код достатньо компактний — близько 600 рядків Python, одна HTML-сторінка та кілька YAML-файлів — тож кожен елемент можна зрозуміти повністю.

    Пов’язана література

  • Панель керування схваленням для агента виправлення Kubernetes: FastAPI та React — Створіть бекенд на FastAPI зі списком дозволів kubectl та панель керування схваленням у React, щоб агент виправлення на основі LLM міг усувати інциденти лише після того, як людина натисне «Схвалити».