December 28, 2025

Я удалил 70 процентов нашего YAML-файла Kubernetes. Задержка снизилась.

Это перевод оригинальной статьи I Deleted 70 Percent of Our Kubernetes YAML. Latency Dropped.

Подписывайтесь на телеграм-канал usr_bin, где я публикую много полезного по Linux, в том числе ссылки на статьи в этом блоге.

Наш API был медленным. Не сломанным — просто медленным.

Таким медленным, из-за которого пользователи обновляют страницу. Таким, из-за которого телефон кажется старым. У нас была задержка 200 мс на эндпоинтах, которые должны были отвечать за 40 мс. Каждая оптимизация, которую мы пробовали, делала всё только хуже. Затем я удалил большую часть нашей конфигурации Kubernetes. Задержка снизилась до 35 мс.

Проблема, о которой никто не говорит

У нас было 47 YAML-файлов. Service mesh, сайдкары, init-контейнеры, лимиты ресурсов с точностью до трёх знаков после запятой. Мы внедрили все рекомендации из прочитанных статей. Наш кластер выглядел как образцовый пример из учебника.

И он умирал под собственным весом.

Переломный момент наступил во вторник. Простой POST-запрос для создания записи пользователя занимал 340 мс. Фактическая запись в базу данных? 8 мс. Остальное — накладные расходы. Накладные расходы Kubernetes. Инфраструктура, которую мы создали для ускорения работы, на самом деле замедляла её.

Я открыл наши конфигурационные файлы и начал читать. Читать по-настоящему. Не бегло просматривая их в поисках ошибок, а пытаясь понять, что на самом деле делает каждая строка.

Большая часть этого не принесла результата.

Что мы запускали

Вот как обычно выглядела процедура развертывания:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-service
spec:
  replicas: 5
  template:
    spec:
      initContainers:
      - name: wait-db
        image: busybox
        command: ['sh', '-c', 'until nc -z db 5432; do sleep 2; done']
      - name: migrate
        image: api:latest
        command: ['npm', 'run', 'migrate']
      containers:
      - name: api
        image: api:latest
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
      - name: istio-proxy
        image: istio/proxyv2
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"

Два инициализирующих контейнера. Ограничения на ресурсы для всего. Сайдкар-контейнер service mesh. Проверка работоспособности каждые пять секунд.

У нас было пять реплик. Каждый pod запускал три контейнера. Пятнадцать контейнеров.

Архитектура до

┌─────────────────────────────────────────────────┐
│  Load Balancer                                  │
└─────────────┬───────────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────────┐
│  Istio Ingress Gateway                          │
│  (adds 40ms)                                    │
└─────────────┬───────────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────────┐
│  Service Mesh (Envoy Proxy)                     │
│  - mTLS handshake                               │
│  - Circuit breaking                             │
│  - Retry logic                                  │
│  - Telemetry collection                         │
│  (adds 60ms)                                    │
└─────────────┬───────────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────────┐
│  Pod                                            │
│  ┌──────────────────────────────────────────┐  │
│  │ Istio Sidecar (128Mi memory)             │  │
│  └──────────────────────────────────────────┘  │
│  ┌──────────────────────────────────────────┐  │
│  │ App Container                            │  │
│  │ - Readiness check every 5s               │  │
│  │ - Liveness check every 10s               │  │
│  └──────────────────────────────────────────┘  │
│  Total: 384Mi memory per pod                   │
└─────────────────────────────────────────────────┘

Что я удалил

Во-первых, service mesh. От неё отказались. Мы не использовали распределенную систему из 50 микросервисов. У нас было восемь сервисов. Все в одном кластере. В одной сети. Нам не нужен был mTLS между сервисами, которые и так находились за брандмауэром.

Во-вторых, контейнеры инициализации. Скрипт ожидания базы данных решал проблему, которой у нас не было. Политика перезапуска Kubernetes уже справлялась с этим. Под перезапускался до тех пор, пока база данных не будет готова. Нам не нужен был замкнутый цикл, выполняющий ту же самую задачу.

В-третьих, агрессивные ограничения ресурсов. Мы установили их, основываясь на статьях в блоге, а не на реальности. Наше приложение использовало 80 МБ памяти под нагрузкой. Мы запрашивали 256 МБ, а ограничение составляло 512 МБ. Kubernetes удерживал память, которую наше приложение никогда бы не использовало.

В-четвертых, частота проверки работоспособности. Проверка работоспособности API без сохранения состояния каждые пять секунд ничего не дает. Мы перешли на 30-секундные интервалы.

Вот что осталось:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-service
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: api
        image: api:latest
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 30

Вот и всё. Один контейнер. Реалистичные запросы на ресурсы. Разумные проверки состояния.

Архитектура после

┌─────────────────────────────────────────────────┐
│  Load Balancer                                  │
└─────────────┬───────────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────────┐
│  Kubernetes Service (ClusterIP)                 │
│  (adds 2ms)                                     │
└─────────────┬───────────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────────┐
│  Pod                                            │
│  ┌──────────────────────────────────────────┐  │
│  │ App Container                            │  │
│  │ - Readiness check every 30s              │  │
│  │ - Liveness check every 30s               │  │
│  └──────────────────────────────────────────┘  │
│  Total: 128Mi memory per pod                   │
└─────────────────────────────────────────────────┘

Результаты

До и после задержки:

Endpoint: POST /api/users
Before: 340ms (p95)
After:   48ms (p95)

Endpoint: GET /api/posts
Before: 180ms (p95)
After:   35ms (p95)
Endpoint: PUT /api/profile
Before: 290ms (p95)
After:   52ms (p95)

Объем используемой памяти на один под снизился с 384 МБ до 128 МБ. Количество контейнеров в пяти подах сократилось с 15 до 3 в трех подах. Счет за кластер снизился на 60 процентов.

Но важнее всего была задержка. Наше приложение стало быстрым. По-настоящему быстрым.

Что я узнал

Kubernetes предоставляет бесконечные возможности для настройки. Но это не значит, что вы должны ими пользоваться.

Service mesh решают реальные проблемы компаний с сотнями сервисов в нескольких кластерах. Мы не были такой компанией. Сетка добавляла 100 мс к каждому запросу и предоставляла нам функции, которые мы никогда не использовали.

Init-контейнеры полезны, когда у вас сложные зависимости при старте. У нас их не было. База данных всегда была доступна до деплоя приложения.

Ограничения ресурсов защищают кластер от неконтролируемого роста процессов. Но если устанавливать их на основе предположений, а не измерений, вы просто тратите ресурсы впустую.

Статьи о передовом опыте пишут люди, решающие разные задачи в разных масштабах. Их решения не обязательно подойдут вам.

Как это сделать самостоятельно

Начните с измерений. Установите профайлер. Узнайте, откуда на самом деле берётся задержка. Мы использовали базовые метрики Prometheus и были шокированы тем, что увидели.

Затем подвергните сомнению все ваши конфигурационные файлы. Для каждого элемента конфигурации спросите, какую проблему он решает. Если вы не можете назвать конкретную проблему, которая у вас есть, удалите его.

Тестирование проводилось на тестовой площадке. Мы внедряли эти изменения в течение двух недель, по одному сервису за раз. Каждое изменение можно было откатить.

Самое главное, помните, что сложность — это не утонченность. Простая, но работающая система лучше, чем сложная, но медленная.

Наша настройка Kubernetes теперь скучная. Она делает меньше. Она быстрее, дешевле и проще в отладке.

Иногда лучший код — это тот, который вы удалили.

Мораль: ваша инфраструктура должна служить вашему приложению, а не наоборот. Если ваши конфигурационные файлы замедляют работу, вы имеете право их удалить.

На этом все! Спасибо за внимание! Если статья была интересна, подпишитесь на телеграм-канал usr_bin, где будет еще больше полезной информации.