Kubernetes
Теория: Pod в Kubernetes
Первым делом нужно зафиксировать простую вещь. Pod — это базовая и минимальная единица работы в Kubernetes. Kubernetes не запускает контейнеры по одному и не управляет ими напрямую. Он всегда оперирует Pod’ами. Если что-то запущено в кластере, значит где-то существует Pod, внутри которого уже находятся контейнеры.
Pod — это не контейнер и не виртуальная машина. Контейнер — это просто процесс с файловой системой и точкой входа, который вообще не знает, что работает в Kubernetes. Pod находится уровнем выше. Именно Pod получает IP-адрес, именно Pod планируется на ноду, именно Pod пересоздаётся при сбоях. Контейнеры внутри Pod — это уже детали реализации.
Когда Kubernetes создаёт Pod, он гарантирует, что все контейнеры внутри него окажутся на одной ноде. Они получают общее сетевое пространство имён, один IP-адрес и один набор портов. Любой контейнер внутри Pod может обратиться к другому через localhost, как если бы это были процессы на одном сервере. Для хранения данных Pod может подключать volumes, которые также будут общими для всех контейнеров внутри.
Важно сразу понять, как Kubernetes реагирует на сбои. Kubernetes не понимает бизнес-смысл приложения. Он не знает, какой контейнер главный, а какой вспомогательный. Он работает на уровне процессов и их состояния. Если контейнер должен быть запущен и контейнер упал, Kubernetes делает единственное логичное для него действие — перезапускает этот контейнер. Он не анализирует, привёл ли этот сбой к полной неработоспособности приложения или нет.
Именно из этого ограничения вырастает паттерн sidecar. Sidecar — это вспомогательный контейнер, который живёт рядом с основным в одном Pod и помогает ему выполнять задачу. Такой контейнер может принимать входящий трафик, писать логи, собирать метрики или шифровать соединения. Он не имеет самостоятельного смысла и существует только вместе с основным процессом.
Представим практическую ситуацию. В системе есть backend-приложение — обычный HTTP-сервер. Он слушает порт 8080, принимает запросы, ходит в базу данных и возвращает JSON. Пока нагрузка небольшая, его можно запустить напрямую и открыть порт наружу. Пользователи заходят, заказы создаются, всё работает.
Через некоторое время появляется новое требование. Backend больше нельзя отдавать напрямую в интернет. Перед ним нужно поставить прокси. Этот прокси должен слушать 80-й порт, писать access-логи и проксировать трафик в backend на localhost
. В системе появляется два процесса. Прокси без backend бесполезен, потому что он будет принимать HTTP-запросы и возвращать 502 Bad Gateway — проксировать просто некуда. Backend без прокси продолжает работать, но он недоступен снаружи на стандартном порту и фактически выключен из пользовательского трафика.Эти два процесса жёстко связаны. Они должны стартовать вместе, жить рядом и общаться через localhost. Именно здесь появляется Pod. Kubernetes получает инструкцию: эти процессы нельзя разносить, они должны быть размещены вместе. Когда Pod запускается, Kubernetes гарантирует, что оба контейнера окажутся на одной ноде, получат общий IP и смогут работать так, как будто это два процесса на одном сервере.
Если в таком Pod падает backend-контейнер, прокси продолжает работать и возвращает 502. Kubernetes видит лишь то, что один контейнер завершился, и перезапускает его. Он не знает, что именно backend делает приложение бесполезным. Для системы это просто процесс, который должен быть приведён в нужное состояние.
Из этого следует важное архитектурное правило. Pod — это не способ сгруппировать связанные сервисы. Pod — это способ запустить несколько процессов, которые обязаны жить вместе. Они должны находиться на одной ноде, должны общаться через localhost и должны делить сеть и файловую систему. Если эти условия не выполняются, контейнерам не место в одном Pod.
Хороший контрпример — backend и база данных. Даже если backend постоянно обращается к базе, они не должны находиться в одном Pod. У них разный жизненный цикл и разные правила масштабирования. Backend можно перезапускать и масштабировать часто. Базу данных перезапускают редко и очень осторожно. Если положить их в один Pod, они будут падать и подниматься вместе, а это почти всегда архитектурная ошибка.
Pod всегда целиком живёт на одной ноде. Его нельзя растянуть на несколько серверов. Если системе не хватает мощности, Kubernetes не увеличивает Pod. Он создаёт ещё один Pod. Поэтому масштабирование в Kubernetes — это всегда масштабирование Pod’ов. Если внутри Pod находятся прокси и backend, они всегда масштабируются вместе и никак иначе.
Pod — сущность временная. Его нельзя воспринимать как сервер. Kubernetes спокойно уничтожает Pod и создаёт новый при обновлении приложения, изменении конфигурации, масштабировании или падении ноды. Новый Pod не является продолжением старого. У него будет другой IP и другой hostname. Именно поэтому нельзя хранить состояние в файловой системе Pod без volume и нельзя полагаться на IP-адреса.
Если падает нода, Pod не исчезает мгновенно. Kubernetes сначала ждёт и считает ноду временно недоступной. По умолчанию он около пяти минут не принимает решение о пересоздании Pod’ов. Всё это время Pod может висеть в статусе Unknown или Terminating. Только после этого Kubernetes признаёт ноду мёртвой и создаёт новые Pod’ы на других нодах.
Сеть внутри Pod всегда общая. Все контейнеры внутри Pod видят друг друга через localhost. Между Pod’ами localhost не работает никогда, даже если они запущены на одной ноде. Для общения между Pod’ами используется сеть кластера, а для стабильного адреса применяется Service. Если в архитектуре фигурирует localhost, почти всегда это означает «внутри одного Pod».
Если внутри Pod падает контейнер, Pod остаётся, а контейнер перезапускается. При частых падениях получим CrashLoopBackOff. Если Pod уничтожается целиком, Kubernetes создаёт новый Pod с нуля. Это разные события, и их важно различать при диагностике проблем.
Pod без указанных ресурсов почти всегда становится источником боли. Если не задать requests и limits, Kubernetes не понимает, куда его безопасно разместить. Pod может оказаться на перегруженной ноде или съесть всю память и утянуть за собой соседей, что закончится OOMKill. Это одна из самых частых ошибок в учебных и боевых кластерах.
Без probes Kubernetes тоже остаётся слепым. Если приложение внутри контейнера зависло, Kubernetes продолжит считать его здоровым, пока процесс формально жив. Именно поэтому используются startup, liveness и readiness probes. На практике часто встречается ситуация, когда приложение запускается 30–40 секунд, probe настроен слишком агрессивно, контейнер не успевает стартовать и Pod уходит в CrashLoopBackOff.
Иногда недостаточно просто запустить контейнер. Приложение может зависеть от внешних сервисов: базы данных, очереди сообщений, кэша или файлового хранилища. Kubernetes запускает контейнеры параллельно, и нет гарантии, что база данных поднимется раньше backend-сервиса. В такой ситуации приложение стартует, пытается подключиться к базе, получает connection refused и аварийно завершается. Kubernetes перезапускает контейнер снова и снова, но проблема не в коде — проблема в порядке старта.
Для таких сценариев в Kubernetes существуют init-контейнеры. Это специальные контейнеры внутри Pod, которые запускаются раньше основных и выполняются строго последовательно. Их задача — подготовить окружение перед стартом приложения. Например, init-контейнер может выполнять команду pg_isready в цикле и ждать, пока база данных начнёт принимать подключения. Только когда он успешно завершится, Kubernetes запустит основной backend-контейнер.
Всё сводится к одному чёткому правилу. В один Pod кладут только то, что обязано жить вместе. Если компоненты имеют разный жизненный цикл, масштабируются по-разному и могут падать независимо, это разные Pod’ы. Если компоненты не имеют смысла друг без друга, стартуют вместе и общаются через localhost, это один Pod.


