Jump to content
View in the app

A better way to browse. Learn more.

T.M.I IThub

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.

Kubernetes в продакшне: 18 месяцев, 47 инцидентов и одно просветление

(0 reviews)

Прежде чем начать, хочу сказать одну важную вещь: я люблю Kubernetes. Искренне. Как любят сложного человека — за глубину, за то, что никогда не знаешь чего ожидать, за то, что каждый день чему-то учишься. И одновременно хочется иногда взять его и... ну, вы понимаете.

Восемнадцать месяцев в продакшне с k8s. Сорок семь инцидентов в PagerDuty. Из них тридцать один — "это мы сами виноваты". Остальные шестнадцать — "это k8s виноват, но мы неправильно его настроили". Итого: сорок семь раз мы были виноваты сами. Добро пожаловать в правду.


Начало: эйфория

Мы переехали на k8s с bare-metal + ansible + systemd. По тем временам это был большой шаг. Наконец-то: декларативная конфигурация, автомасштабирование, rolling updates без даунтайма, самолечение. Красота!

Первая неделя прошла в написании манифестов. Я писал их как поэт — вдохновенно, не жалея строк. YAML цвёл. Deployments, Services, ConfigMaps, Secrets, Ingress — всё было прекрасно.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: php-app
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: php-fpm
          image: myapp:latest  # ← вот здесь уже первая мина
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"

image: myapp:latest — это первая из ошибок, которая позже стоила нам часа даунтайма. Тег latest — это зло. Это не версия, это "бог знает что". При следующем деплое k8s может подтянуть другой образ на разные ноды, и у вас будет кластер, где половина подов — старая версия, а половина — новая, и они несовместимы. Всегда используйте immutable теги: git sha, semver, timestamp. Всегда.


Инцидент #7: OOMKiller приходит ночью

Наше PHP приложение — CI4 с тяжёлой бизнес-логикой. PHP-FPM воркеры потребляют память по-разному в зависимости от endpoint'а. Лёгкий API — 40MB. Тяжёлый отчёт — 380MB. Мы выставили limits.memory: 512Mi и думали, что этого хватит.

Ночью запустился cron-джоб, который генерировал ежемесячные отчёты. Каждый воркер под отчёт — ~380MB. PHP-FPM с 8 воркерами = 3GB. Лимит пода — 512MB.

OOMKilled
Exit Code: 137

Под убит. k8s перезапускает. Новый под опять запускает отчёт. Опять OOMKilled. Restart loop. Кейс называется CrashLoopBackOff, и выглядит он в логах примерно так:

pod/php-app-7d9f8c-xk2p9  0/1  CrashLoopBackOff  14  47m

Четырнадцать рестартов за 47 минут. k8s упорно пытался поднять под, PHP упорно пытался сожрать память, OOMKiller упорно его убивал. Это было похоже на зомби-апокалипсис в миниатюре.

Решение: разделение воркеров

Мы разделили PHP-FPM на два пула:

; /etc/php-fpm.d/www.conf — общий пул
[www]
pm = dynamic
pm.max_children = 20
pm.max_requests = 500

; /etc/php-fpm.d/heavy.conf — пул для тяжёлых операций
[heavy]
pm = ondemand
pm.max_children = 4
pm.max_requests = 50
pm.process_idle_timeout = 10s

И создали отдельный Deployment для heavy-операций с увеличенными лимитами:

# heavy-deployment.yaml
resources:
  requests:
    memory: "512Mi"
    cpu: "500m"
  limits:
    memory: "2Gi"
    cpu: "2000m"

Nginx роутит по prefix:

location /api/reports/ {
    fastcgi_pass heavy-php-fpm:9001;
}

location / {
    fastcgi_pass www-php-fpm:9000;
}

Элегантно? Не очень. Работает? Да.


Инцидент #19: Liveness probe убивает прод

Это был шедевр. Мы настроили liveness probe — k8s регулярно проверяет, жив ли под, и если нет — убивает и перезапускает:

livenessProbe:
  httpGet:
    path: /health
    port: 80
  initialDelaySeconds: 30
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3

Endpoint /health возвращал 200 и JSON со статусами всех зависимостей: Redis, MySQL, очередь. Казалось бы — отлично.

И вот в один прекрасный день MySQL реплика начала лагать (проблема с дисками). Запросы к реплике стали занимать 15-20 секунд. Наш /health endpoint проверял реплику, таймаут probe — 5 секунд. Итог:

Liveness probe failed: Get "http://10.244.2.5/health": context deadline exceeded

k8s решил, что под нездоров. Убил его. Поднял новый. Новый тоже упёрся в лагающую реплику. Тоже убит. В какой-то момент k8s убивал поды быстрее, чем они успевали принять трафик.

Мы устроили собственноручный DDoS на наш прод с помощью liveness probe. Это надо уметь.

Решение:

Разделили liveness и readiness:

# Liveness: только "жив ли процесс"
livenessProbe:
  httpGet:
    path: /health/live  # Проверяет только PHP-FPM ping
    port: 80
  initialDelaySeconds: 30
  periodSeconds: 30
  timeoutSeconds: 2
  failureThreshold: 5

# Readiness: "готов ли принимать трафик"
readinessProbe:
  httpGet:
    path: /health/ready  # Проверяет все зависимости
    port: 80
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3
// CI4 Controller
class HealthController extends BaseController
{
    public function live(): ResponseInterface
    {
        // Только базовая проверка процесса
        return $this->response->setJSON(['status' => 'ok']);
    }

    public function ready(): ResponseInterface
    {
        $checks = [
            'database' => $this->checkDatabase(),
            'redis'    => $this->checkRedis(),
            'queue'    => $this->checkQueue(),
        ];

        $healthy = !in_array(false, $checks, true);

        return $this->response
            ->setStatusCode($healthy ? 200 : 503)
            ->setJSON([
                'status' => $healthy ? 'ready' : 'degraded',
                'checks' => $checks,
            ]);
    }
}

Теперь при проблемах с репликой поды переставали получать трафик (readiness failed), но не убивались (liveness — ok). Через 20 минут реплика восстановилась, readiness снова позеленела, трафик вернулся. Без единой 500-й ошибки.


Инцидент #31: HPA и смерть от масштабирования

Horizontal Pod Autoscaler — прекрасная вещь. Трафик растёт — поды добавляются. Трафик падает — поды убираются. Автоматически. Без участия человека.

Кроме случаев, когда всё идёт не так.

Мы настроили HPA по CPU:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: php-app-hpa
spec:
  minReplicas: 3
  maxReplicas: 30
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

Ждём нагрузочного теста. Трафик растёт. HPA видит CPU 85% → добавляет поды. Новые поды стартуют, прогревают OPcache, пока прогреваются — CPU у них 90% → HPA добавляет ещё поды. Те тоже прогреваются → CPU опять высокий → ещё поды.

Через 3 минуты у нас было 28 подов из максимальных 30. Они все одновременно прогревали OPcache, коннектились к MySQL, коннектились к Redis. MySQL задыхался от 28×20 = 560 новых соединений. Redis cluster не успевал. Прод лёг под весом собственного масштабирования.

Решение:

  1. Добавить cooldown и stabilization:

behavior:
  scaleUp:
    stabilizationWindowSeconds: 60
    policies:
      - type: Pods
        value: 2  # Не более 2 подов за раз
        periodSeconds: 60
  scaleDown:
    stabilizationWindowSeconds: 300
  1. Использовать custom metrics (RPS), а не CPU:

metrics:
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second
      target:
        type: AverageValue
        averageValue: "300"
  1. Добавить PHP-FPM warming в startup probe:

startupProbe:
  httpGet:
    path: /warmup
    port: 80
  failureThreshold: 30
  periodSeconds: 5

Endpoint /warmup прогревал OPcache и пул соединений. Пока startupProbe не вернула 200 — под не получал трафик и не учитывался в метриках HPA.


Просветление

После 18 месяцев и 47 инцидентов я пришёл к простому выводу: Kubernetes не упрощает жизнь. Он переносит сложность из "как запустить" в "как правильно настроить". И это совершенно другой уровень сложности — более тонкий, более коварный, и требующий понимания системы на глубоком уровне.

Ключевые правила, которые я вколотил в стену рядом с рабочим столом:

  • Никогда image:latest — только иммутабельные теги

  • Liveness ≠ Readiness — это разные вещи с разной ценой ошибки

  • Resource limits — это не потолок, это граница жизни и смерти пода

  • HPA с CPU — ловушка для PHP. Используйте RPS или custom metrics

  • Один под не значит один процесс — PHP-FPM это N воркеров

  • Chaos engineering — убивайте поды намеренно, пока это не сделает production

И последнее: k8s — это не серебряная пуля. Это мощный инструмент с огромным количеством движущихся частей. Уважайте его, изучайте, читайте исходники при необходимости. И обязательно найдите коммьюнити — на ithub.uno, на форумах, на конференциях. Потому что некоторые грабли лучше подбирать чужим лбом.

До следующего CrashLoopBackOff. 🤕


0 Comments

Recommended Comments

There are no comments to display.

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.