Я удалил 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, где будет еще больше полезной информации.