Перейти до змісту

Pod Lifecycle: від Pending до Running

Pod Lifecycle: від Pending до Running

Written by:

Igor Gorovyy
DevOps Engineer Lead & Senior Solutions Architect

LinkedIn


Под проходить через кілька компонентів, перш ніж стати 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: журнал всього, що відбувається в кластері.

Ресурси

Вихідний код циклу: github.com/igorgorovoy/sheep-shepherd-meadow

Попередня: Node Agent | Наступна: Event System