Pod Lifecycle: від Pending до Running¶
Written by:
Igor Gorovyy
DevOps Engineer Lead & Senior Solutions Architect
Под проходить через кілька компонентів, перш ніж стати Running. Кожен компонент працює незалежно і асинхронно. Ось повний шлях.
sequenceDiagram
participant U as sheepctl apply
participant API as API Server
participant Store as BoltDB
participant Sched as Scheduler
participant Agent as Agent (node-1)
participant RT as Sheep Runtime
U->>API: POST /api/v1/pods {name: web-0}
API->>Store: Save pod (Phase: Pending, NodeName: "")
API->>API: RecordEvent("Created")
API-->>Sched: go SchedulePod()
Note over Sched: Filter + Score nodes
Sched->>Store: Update pod (NodeName: node-1)
Sched->>Store: RecordEvent("Scheduled")
Note over Agent: reconcile кожні 3с
Agent->>API: GET /api/v1/pods
Agent->>Agent: Pod web-0 Pending, мій node
Agent->>RT: Create container
Agent->>RT: Start container
RT-->>Agent: PID, Network IP
Agent->>API: PUT pod (Phase: Running, IP: 10.20.0.2)
Фази поду¶
stateDiagram-v2
[*] --> Pending : API створює
Pending --> Pending : Scheduler призначає ноду
Pending --> Running : Agent запускає контейнери
Pending --> Failed : Agent не зміг запустити
Running --> Failed : Контейнер впав
Running --> Succeeded : Процес завершився з кодом 0
Pending без NodeName - чекає на Scheduler. Бувають причини: немає нод, недостатньо ресурсів, nodeSelector не збігається.
Pending з NodeName - чекає на Agent. Agent ще не встиг запустити контейнери.
Running - всі контейнери працюють. Agent перевіряє їх кожні 3 секунди.
Failed - один або більше контейнерів впали. В залежності від RestartPolicy, контролер може перестворити под.
Eventual consistency¶
Жоден компонент не чекає на інший. API Server не чекає, поки Scheduler знайде ноду. Scheduler не чекає, поки Agent запустить под. Кожен працює у своєму темпі з reconciliation loops.
Це означає, що є затримка. Від sheepctl apply до Running може пройти 5-15 секунд:
- 0с: API Server створює під
- 0-2с: Scheduler знаходить ноду (ticker 2с)
- 0-3с: Agent бачить під (ticker 3с)
- 1-5с: Контейнер запускається
Перевірка здоров'я¶
Коли під Running, Agent продовжує перевіряти:
func (a *Agent) checkPod(pod *Pod) {
for i, cs := range pod.Status.Containers {
if cs.ContainerID == "" { continue }
c, err := a.mgr.Get(cs.ContainerID)
if err != nil {
pod.Status.Containers[i].Ready = false
continue
}
pod.Status.Containers[i].Ready =
c.State == container.StateRunning
pod.Status.Containers[i].State = string(c.State)
}
allRunning := true
for _, cs := range pod.Status.Containers {
if !cs.Ready { allRunning = false; break }
}
if !allRunning && pod.Status.Phase == PodRunning {
pod.Status.Phase = PodFailed
a.updatePodStatus(pod)
}
}
Якщо контейнер зупинився, під переходить у Failed.
Чого не вистачає¶
Немає readiness/liveness probes. Під стає Running одразу після старту контейнера, навіть якщо додаток всередині ще завантажується. В Kubernetes probe-и дозволяють відрізнити "процес працює" від "додаток готовий приймати трафік".
💡 Цікаві факти¶
- Фаза поду в Kubernetes — це навмисно груба абстракція. Офіційна документація прямо попереджає: на
phaseне варто покладатись для тонкої логіки, бо вона лише агрегує стани контейнерів. Справжня деталізація живе вconditions(PodScheduled,Ready,Initialized,ContainersReady). SucceededіFailed— термінальні фази: под у них уже не повертається вRunning. Тому Job-и створюють новий под на ретрай, а не "оживляють" старий. Сам под у Kubernetes — immutable після створення майже в усьому, крім кількох полів.- Eventual consistency, на якій тримається весь шлях поду, — це та сама модель, що й у git: кожен компонент бачить власну "версію світу" і збігається з рештою згодом. Жодного глобального локу, жодної транзакції через увесь кластер.
- Те, що ми звемо Pending, у Kubernetes ще тонше: між
PendingіRunningє фаза, коли тягнеться образ. Окрема подіяPulling/Pulled— і саме тут поди найчастіше "зависають" у проді через повільний registry. - У поду є ще й
initContainers, які мають відпрацювати до кінця перед стартом основних контейнерів. Поки вони працюють, под усе щеPending— тому под може висіти вPendingхвилинами не через збій планування, а тому що init-контейнер робить повільну міграцію чи чекає на залежність. - Ті самі "5–15 секунд" пояснюють, чому
kubectl applyповертається миттєво, хоч под ще й близько не готовий.kubectlлише підтверджує, що об'єкт записано в etcd. Усе далі — планування, тягнення образу, старт — відбувається асинхронно, і саме тому окремо існує командаkubectl wait --for=condition=Ready. FailedіSucceededвиглядають симетрично, але голий под майже ніколи не доходить доSucceededна практиці: довгоживучі сервіси не завершуються з кодом 0.Succeeded— це радше природний термінальний стан Job-ів і CronJob-ів, і саме тому в завершений под не можна зайти черезkubectl exec— там уже немає процесу.- RestartPolicy (
Always/OnFailure/Never) рестартує контейнери всередині поду, а не сам под. Кожен рестарт збільшує лічильник і застосовує експоненційний backoff з межею в 5 хвилин — той сумнозвіснийCrashLoopBackOffуkubectl get pods— це стан контейнера, а не фаза поду. terminationGracePeriodSeconds(дефолт 30с) — дзеркало старту: на видалення kubelet шлеSIGTERM, чекає, потімSIGKILL. Под, що завис уTerminating, зазвичай означає застосунок, який ігноруєSIGTERM— цикл eventual consistency терпляче відлічує час, перш ніж застосувати силу.
Що я зрозумів, поки розбирався з темою¶
Найсильніше клацнуло, коли я перестав думати про lifecycle як про послідовність викликів і почав думати про нього як про набір незалежних спостерігачів за спільним станом. Ніхто нікого не "запускає" — кожен компонент дивиться на под, бачить розбіжність між бажаним і реальним, і робить один маленький крок. Pending-без-NodeName і Pending-з-NodeName — це той самий Pending, але чекають на нього зовсім різні компоненти. Доки я це не намалював, латентність "5–15 секунд" здавалась багом. Виявилось — це сума незалежних ticker-ів, і вона прогнозована.
На що звернути увагу¶
Pendingнеоднозначний: без NodeName чекає Scheduler, з NodeName — Agent. Дивлячись лише на фазу, не зрозумієш, хто саме "винен" у затримці. Завжди дивись ще й на NodeName.- Перехід
Running → Failedу нас односторонній: впав контейнер — под Failed і лишається таким. Без RestartPolicy=Always под не підніметься сам, а ReplicationController підніме лише новий, не цей. - Latency складається з незалежних ticker-ів (2с + 3с), тому у найгіршому випадку — близько 5 секунд лише на "помітити", ще до старту контейнера. Не лякайся, що
applyне дає миттєвого Running.
Що можна покращити¶
- Додати readiness/liveness probes, щоб
Runningозначало "готовий приймати трафік", а не лише "процес стартував". - Ввести фазу
ContainerCreating/Pullingміж Pending і Running, щоб бачити, де саме застряг под (планування vs тягнення образу vs старт). - Реалізувати RestartPolicy на рівні Agent — рестартувати контейнер у тому ж поді замість того, щоб одразу позначати Failed.
- Додати pod
conditionsпоряд зphase, щоб відрізняти "заплановано", "образ готовий", "контейнери готові".
Спробуй сам¶
# Створи под і слідкуй за events:
sheepctl apply -f examples/pod.json
watch -n1 sheepctl events
# Побачиш: Created → Scheduled → Running
Pod lifecycle зрозумілий. Далі - Event System: журнал всього, що відбувається в кластері.
Ресурси¶
- Pod lifecycle — фази, conditions, restart policy
- Pod termination — деталі graceful shutdown
- Pod conditions —
PodScheduled,Initialized,ContainersReady,Ready - Container probes — liveness, readiness і startup probes
- Init containers — послідовна фаза перед основними контейнерами
- Restart policy і CrashLoopBackOff — як backoff-иться рестарт контейнерів
- State pattern — патерн, що лежить в основі lifecycle
- Reconciliation / control loops — модель eventual consistency, на якій тримається весь шлях
Вихідний код циклу: github.com/igorgorovoy/sheep-shepherd-meadow
Попередня: Node Agent | Наступна: Event System
