lenec ru

← все посты

Cost optimization в Kubernetes: resource requests/limits, vertical/horizontal scaling, spot instances

18K

Ваше Node.js приложение работает отлично на локалхосте, но в Kubernetes начинаются проблемы: поды рестартятся без причины, пользователи получают 502 во время деплоя, load balancer продолжает слать трафик на умирающие поды. Проблема не в коде — проблема в отсутствии правильных health checks. Kubernetes не умеет читать мысли: он не знает, жив ли ваш процесс, готов ли он принимать трафик, и когда нужно перестать слать запросы. Разбираемся, как настроить liveness, readiness, и startup probes, чтобы ваше приложение корректно интегрировалось с оркестратором.

Зачем нужны health checks

Health checks — это контракт между вашим приложением и платформой. Kubernetes постоянно спрашивает: «Ты жив? Ты готов работать?» Ваше приложение отвечает через HTTP-эндпоинты. На основе этих ответов Kubernetes принимает решения:

  • Автоматический restart: если liveness probe падает, Kubernetes убивает под и запускает новый. Это спасает от deadlock'ов, утечек памяти, и зависших процессов
  • Load balancer routing: если readiness probe падает, Kubernetes убирает под из Service endpoints. Load balancer перестаёт слать трафик на этот под, но под продолжает работать
  • Rolling updates: во время деплоя Kubernetes ждёт, пока новые поды станут ready, прежде чем убивать старые. Без readiness probe деплой превращается в downtime

Без health checks Kubernetes работает вслепую. Он видит, что процесс запущен (PID существует), но не знает, может ли процесс обрабатывать запросы. Результат: трафик идёт на поды, которые ещё не подключились к базе, или на поды, которые уже получили SIGTERM и завершают работу.

Liveness probe: проверка что процесс жив

Liveness probe отвечает на вопрос: «Нужно ли рестартить этот под?» Если liveness probe падает несколько раз подряд, Kubernetes убивает контейнер и запускает новый.

Что должен проверять liveness:

  • Event loop отвечает (процесс не завис)
  • HTTP-сервер может обработать запрос
  • Нет deadlock'а или критической ошибки, требующей рестарта

Что НЕ должен проверять liveness:

  • Подключение к базе данных
  • Доступность внешних API
  • Состояние зависимостей

Критическое правило: liveness probe никогда не должен проверять внешние зависимости. Если ваша база упала, и liveness probe падает из-за этого, Kubernetes рестартит все поды. Новые поды тоже не могут подключиться к базе — они тоже рестартятся. Вы получаете restart loop поверх инцидента с базой, и нулевую доступность вместо деградированного сервиса.

Реализация /health/live endpoint

const express = require('express');
const app = express();

// Liveness probe — максимально простой
app.get('/health/live', (req, res) => {
  // Если мы дошли до этой строки, event loop работает
  res.status(200).json({ status: 'alive', uptime: process.uptime() });
});

const server = app.listen(3000);

Этот эндпоинт не делает I/O операций. Он просто проверяет, что Node.js может обработать HTTP-запрос. Если event loop завис, запрос не дойдёт до обработчика, и Kubernetes получит timeout.

Kubernetes manifest для liveness probe

livenessProbe:
  httpGet:
    path: /health/live
    port: 3000
  initialDelaySeconds: 15      # Ждём 15 секунд после старта
  periodSeconds: 10            # Проверяем каждые 10 секунд
  timeoutSeconds: 5            # Таймаут запроса 5 секунд
  failureThreshold: 3          # 3 неудачи подряд = рестарт

Параметр failureThreshold: 3 означает, что под должен падать 30 секунд подряд (3 × 10s), прежде чем Kubernetes его убьёт. Это защищает от рестартов при кратковременных спайках нагрузки.

Readiness probe: проверка готовности принимать трафик

Readiness probe отвечает на вопрос: «Может ли этот под обрабатывать запросы прямо сейчас?» Если readiness probe падает, Kubernetes убирает под из Service endpoints, но не рестартит его.

Что должен проверять readiness:

  • База данных подключена и отвечает
  • Redis/кеш доступен
  • Критичные зависимости готовы
  • Приложение не в процессе graceful shutdown

Readiness probe может быть строгим. Если база данных не отвечает за 2 секунды — это effectively недоступность, и под не должен получать трафик.

Реализация /health/ready endpoint

let isShuttingDown = false;

app.get('/health/ready', async (req, res) => {
  // Проверка 1: не в процессе shutdown
  if (isShuttingDown) {
    return res.status(503).json({ 
      status: 'shutting_down',
      message: 'Pod is draining connections'
    });
  }
  
  // Проверка 2: база данных
  const dbHealthy = await checkDatabase();
  
  // Проверка 3: Redis
  const cacheHealthy = await checkRedis();
  
  const allHealthy = dbHealthy && cacheHealthy;
  
  res.status(allHealthy ? 200 : 503).json({
    status: allHealthy ? 'ready' : 'not_ready',
    checks: {
      database: dbHealthy,
      cache: cacheHealthy
    }
  });
});

async function checkDatabase() {
  try {
    await db.query('SELECT 1', { timeout: 2000 });
    return true;
  } catch (err) {
    console.error('Database health check failed:', err.message);
    return false;
  }
}

async function checkRedis() {
  try {
    await redis.ping({ timeout: 2000 });
    return true;
  } catch (err) {
    console.error('Redis health check failed:', err.message);
    return false;
  }
}

Ключевой момент: всегда используйте timeout на проверках зависимостей. Если база зависла, и ваш запрос висит 30 секунд, Kubernetes решит, что под мёртв. Лучше упасть быстро с явной ошибкой.

Kubernetes manifest для readiness probe

readinessProbe:
  httpGet:
    path: /health/ready
    port: 3000
  initialDelaySeconds: 5       # Проверяем раньше, чем liveness
  periodSeconds: 5             # Проверяем чаще
  timeoutSeconds: 3            # Короче timeout
  failureThreshold: 2          # 2 неудачи = убрать из endpoints

Readiness probe более агрессивный: failureThreshold: 2 означает, что под убирается из rotation через 10 секунд (2 × 5s) после первой проблемы. Это правильно — если под не может обрабатывать запросы, трафик должен уйти на здоровые поды быстро.

Startup probe: для медленно стартующих приложений

Проблема: ваше приложение стартует 60 секунд (подключение к базе, миграции, прогрев кеша). Если вы поставите initialDelaySeconds: 60 на liveness probe, вы будете ждать минуту даже когда приложение стартует за 5 секунд. Если поставите initialDelaySeconds: 10, liveness probe убьёт под до того, как он успеет стартовать.

Решение: startup probe. Он отключает liveness и readiness проверки до тех пор, пока не пройдёт успешно хотя бы раз.

startupProbe:
  httpGet:
    path: /health/live
    port: 3000
  periodSeconds: 10
  failureThreshold: 30         # 30 × 10s = 5 минут на старт

Startup probe даёт приложению до 5 минут на инициализацию. Как только он пройдёт успешно один раз, он больше не запускается — управление переходит к liveness и readiness probes с их более агрессивными настройками.

Graceful degradation: fallback при недоступности зависимостей

Не каждая недоступная зависимость должна делать под unready. Если Redis — это кеш, и вы можете работать без него (медленнее, но работать), не убирайте под из rotation.

Статусы: ready, degraded, not_ready

app.get('/health/ready', async (req, res) => {
  if (isShuttingDown) {
    return res.status(503).json({ status: 'shutting_down' });
  }
  
  const checks = {
    database: await checkDatabase(),
    cache: await checkRedis(),
    queue: await checkQueue()
  };
  
  // Критичные зависимости
  const critical = checks.database;
  
  // Некритичные зависимости
  const nonCritical = checks.cache && checks.queue;
  
  if (!critical) {
    // База недоступна — под не может работать
    return res.status(503).json({ 
      status: 'not_ready',
      checks 
    });
  }
  
  if (!nonCritical) {
    // Кеш/очередь недоступны, но база работает — деградированный режим
    return res.status(200).json({ 
      status: 'degraded',
      checks 
    });
  }
  
  res.status(200).json({ 
    status: 'ready',
    checks 
  });
});

Статус degraded возвращает 200 (под остаётся в rotation), но сигнализирует мониторингу, что что-то не так. Вы получаете алерт, но сервис продолжает работать.

Circuit breaker для внешних API

Если вы проверяете внешний API в readiness probe, и этот API падает, все ваши поды станут unready. Лучше использовать circuit breaker:

class CircuitBreaker {
  constructor(threshold = 5, timeout = 60000) {
    this.failureCount = 0;
    this.threshold = threshold;
    this.timeout = timeout;
    this.state = 'closed'; // closed, open, half-open
    this.nextAttempt = Date.now();
  }
  
  async call(fn) {
    if (this.state === 'open') {
      if (Date.now() < this.nextAttempt) {
        throw new Error('Circuit breaker is open');
      }
      this.state = 'half-open';
    }
    
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (err) {
      this.onFailure();
      throw err;
    }
  }
  
  onSuccess() {
    this.failureCount = 0;
    this.state = 'closed';
  }
  
  onFailure() {
    this.failureCount++;
    if (this.failureCount >= this.threshold) {
      this.state = 'open';
      this.nextAttempt = Date.now() + this.timeout;
    }
  }
}

const externalApiBreaker = new CircuitBreaker();

async function checkExternalAPI() {
  try {
    await externalApiBreaker.call(() => 
      fetch('https://api.example.com/health', { timeout: 2000 })
    );
    return true;
  } catch (err) {
    // Circuit breaker открыт — не проверяем API
    return false;
  }
}

Circuit breaker открывается после 5 неудач подряд и остаётся открытым 60 секунд. В это время проверки не выполняются, и readiness probe не падает из-за недоступного внешнего API.

Интеграция с graceful shutdown

Когда Kubernetes убивает под (деплой, scale down), он отправляет SIGTERM. Ваше приложение должно:

  1. Немедленно вернуть 503 на readiness probe
  2. Перестать принимать новые соединения
  3. Дождаться завершения активных запросов
  4. Закрыть соединения с базой/Redis
  5. Выйти с кодом 0
process.on('SIGTERM', async () => {
  console.log('SIGTERM received, starting graceful shutdown');
  
  // 1. Readiness probe начнёт возвращать 503
  isShuttingDown = true;
  
  // 2. Ждём, пока load balancer обновит endpoints (5 секунд)
  await new Promise(resolve => setTimeout(resolve, 5000));
  
  // 3. Закрываем HTTP-сервер (новые соединения не принимаются)
  server.close(() => {
    console.log('HTTP server closed');
  });
  
  // 4. Ждём завершения активных запросов (макс 20 секунд)
  await new Promise(resolve => setTimeout(resolve, 20000));
  
  // 5. Закрываем зависимости
  await db.end();
  await redis.quit();
  
  console.log('Graceful shutdown complete');
  process.exit(0);
});

Kubernetes manifest должен дать достаточно времени:

spec:
  terminationGracePeriodSeconds: 60
  containers:
  - name: app
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 5"]

preStop hook добавляет 5 секунд задержки перед отправкой SIGTERM. Это даёт Kubernetes время обновить endpoints во всех узлах кластера.

Подводные камни

Liveness проверяет базу данных. Самая частая ошибка. Временная недоступность базы не должна рестартить все поды. Проверяйте базу в readiness, а не в liveness.

Readiness не меняется при shutdown. Если readiness продолжает возвращать 200 после SIGTERM, load balancer продолжает слать трафик на умирающий под. Всегда возвращайте 503, когда isShuttingDown === true.

Отсутствие timeout на проверках зависимостей. Зависший запрос к базе блокирует health check на 30+ секунд. Kubernetes решает, что под мёртв, и рестартит его. Всегда ставьте timeout 2-3 секунды.

Одинаковые настройки для liveness и readiness. Liveness должен быть терпеливым (failureThreshold: 3), readiness — агрессивным (failureThreshold: 2). Они решают разные задачи.

Игнорирование startup probe. Если ваше приложение стартует 30 секунд, а initialDelaySeconds на liveness 10 секунд, под будет убит до завершения инициализации. Используйте startup probe для медленных приложений.

Вывод

Health checks — это не формальность, а критичная часть production-ready приложения. Liveness probe проверяет, что процесс жив (без проверки зависимостей), readiness probe проверяет, что под готов принимать трафик (с проверкой зависимостей), startup probe даёт время медленным приложениям. Graceful degradation позволяет работать при частичной недоступности зависимостей, а правильный graceful shutdown гарантирует zero-downtime деплои. Настройте эти три эндпоинта, и Kubernetes будет управлять вашим приложением корректно, без restart loops и 502 ошибок во время деплоя.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.