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

Replication Controller: scale up і scale down

Replication Controller: scale up і scale down

Written by:

Igor Gorovyy
DevOps Engineer Lead & Senior Solutions Architect

LinkedIn


У попередній частині ми побачили reconciliation loop. Тепер подивимось, як ReplicationController створює поди для Deployment і керує їх кількістю.

Створення поду з Deployment

func (rc *ReplicationController) createPodForDeployment(
    dep *Deployment, index int) {
    pod := &Pod{
        Kind: "Pod",
        Metadata: ObjectMeta{
            Name: fmt.Sprintf("%s-%d",
                dep.Metadata.Name, index),
            Namespace: dep.Metadata.Namespace,
            UID:       generateUID(),
            Labels:    mergeLabels(
                dep.Spec.Template.Metadata.Labels,
                dep.Spec.Selector),
            CreatedAt: time.Now(),
        },
        Spec: dep.Spec.Template.Spec,
        Status: PodStatus{Phase: PodPending},
    }

    rc.store.CreatePod(pod)

    rc.store.RecordEvent(Event{
        Type:    "Normal",
        Reason:  "Created",
        Message: fmt.Sprintf("Created pod %s for deployment %s",
            pod.Metadata.Name, dep.Metadata.Name),
        Object: "deployment/" + dep.Metadata.Name,
    })

    // Планування
    go rc.scheduler.SchedulePod(pod)
}

Зверни увагу на іменування: web-0, web-1, web-2. Як у StatefulSet в Kubernetes, тільки простіше.

Labels поду - це merge з template і selector deployment. Це зв'язує поди з deployment.

Зв'язок через labels

graph TB
    DEP["Deployment: web<br/>selector: app=web<br/>replicas: 3"]

    DEP --> P0["Pod: web-0<br/>labels: app=web"]
    DEP --> P1["Pod: web-1<br/>labels: app=web"]
    DEP --> P2["Pod: web-2<br/>labels: app=web"]

Deployment не зберігає список своїх подів. Він знаходить їх через label selector: "всі поди з label app=web в моєму namespace - це мої".

func mergeLabels(sets ...map[string]string) map[string]string {
    result := make(map[string]string)
    for _, s := range sets {
        for k, v := range s { result[k] = v }
    }
    return result
}

Scale up

Якщо подів менше ніж spec.replicas:

if current < desired {
    for i := 0; i < desired-current; i++ {
        rc.createPodForDeployment(dep, current+i)
    }
    rc.logger.Printf(
        "replication controller: scaled up %s from %d to %d",
        dep.Metadata.Name, current, desired)
}

Нові поди створюються з індексом current+i. Scheduler призначить їх на ноди автоматично.

Scale down

Якщо подів більше ніж потрібно:

if current > desired {
    for i := 0; i < current-desired; i++ {
        pod := matchingPods[len(matchingPods)-1-i]
        rc.store.DeletePod(
            pod.Metadata.Namespace, pod.Metadata.Name)
    }
}

Видаляємо з кінця списку. У справжньому Kubernetes є складніша логіка: спочатку видаляються поди в стані Failed, потім Pending, потім з найменшим часом роботи.

Оновлення статусу Deployment

ready := 0
for _, pod := range matchingPods {
    if pod.Status.Phase == PodRunning { ready++ }
}
dep.Status.Replicas = len(matchingPods)
dep.Status.ReadyReplicas = ready
dep.Status.AvailableReplicas = ready
rc.store.UpdateDeployment(dep)

ReadyReplicas - скільки подів реально Running. sheepctl get deployments покаже щось типу 3/3 Ready.

Весь потік від sheepctl до контейнера

sequenceDiagram
    participant U as sheepctl
    participant API as API Server
    participant RC as ReplicationController
    participant S as Scheduler
    participant A as Agent
    participant R as Sheep Runtime

    U->>API: POST /api/v1/deployments<br/>{replicas: 3}
    API->>API: Save to Store
    RC->>RC: reconcile() кожні 5с
    RC->>API: Бачить: 0 подів, потрібно 3
    RC->>API: CreatePod(web-0)
    RC->>API: CreatePod(web-1)
    RC->>API: CreatePod(web-2)
    S->>S: reconcile() кожні 2с
    S->>API: Pod web-0 → node-1
    A->>A: reconcile() кожні 3с
    A->>R: Create + Start container
    A->>API: Pod web-0: Running

Все асинхронно. Кожен компонент працює у своєму темпі і читає стан із Store.

Що ми пропустили

Scale down видаляє поди з кінця списку, без урахування стану. В Kubernetes ReplicaSet спочатку видаляє Failed поди, потім Pending, потім з найменшим uptime. Наш підхід простіший, але менш розумний.

  • Іменування web-0..web-N за індексом небезпечне: видалили web-1, контролер на scale-up порахує current і знову створить под з тим самим іменем — або наступить на стейл-запис. Тому Kubernetes-ReplicaSet дає подам випадкові суфікси (web-x7k2p), а не послідовні індекси.
  • Якщо два selector'и в різних deployment перетинаються, один под може "належати" обом — і два контролери почнуть навперемінно його створювати й видаляти. Owner references у Kubernetes саме від цього й страхують.

💡 Цікаві факти

  • Послідовні імена web-0, web-1 — це насправді модель StatefulSet, а не Deployment. Звичайний Kubernetes-Deployment створює ReplicaSet з випадковим хешем у назві подів саме тому, що стабільна ідентичність потрібна лише stateful-навантаженням (бази, черги), а stateless-репліки взаємозамінні.
  • Deployment у Kubernetes не керує подами напряму — між ними є проміжний об'єкт ReplicaSet. Саме завдяки цьому шару працює rolling update: створюється новий ReplicaSet, старий поступово зменшується до нуля. Наш контролер цей рівень пропускає.
  • Зв'язок "deployment → поди" в Kubernetes тримається не лише на label selector, а й на полі ownerReferences у кожному поді. Це дає каскадне видалення (garbage collection): видаляєш deployment — його поди прибираються автоматично.
  • Порядок видалення при scale-down у Kubernetes описаний явним cost-функціоналом: Pending/Unscheduled, потім не-Ready, потім молодші поди, потім ті, що на нодах із більшою кількістю реплік — щоб рівномірно "проріджувати" кластер.
  • ReplicationController — це буквально перший контролер, який Kubernetes колись випустив, і назва нашого класу — пряма данина йому. Згодом його замінив ReplicaSet (з set-based селекторами), але kubectl досі відгукується на скорочення rc заради зворотної сумісності.
  • kubectl scale не запускає жодної особливої операції "scale" — він просто патчить поле spec.replicas через окремий сабресурс /scale. Контролер підхоплює нове число на наступному reconcile, точно як у нас. Те саме поле автоматично переписує HorizontalPodAutoscaler на основі метрик CPU/пам'яті — а контролер навіть не здогадується, що його автоскейлять.
  • replicas: 0 — цілком валідне значення і стандартний спосіб "поставити на паузу" навантаження, не видаляючи його конфіг. Scale-to-zero — це фундамент serverless-платформ на кшталт Knative і KEDA, які згортають Deployment до нуля, коли він простоює, і піднімають назад на першому ж запиті.

Що я зрозумів, поки розбирався з темою

Найнеочевиднішим виявилося те, що deployment взагалі не "володіє" подами в прямому сенсі — він їх не тримає в списку, а щоразу перезнаходить через label selector. Спершу це здавалося ненадійним: а що як хтось почепить той самий label вручну? А потім зрозумів, що саме ця слабка зв'язність і робить систему живучою — контролеру байдуже, звідки взявся под, головне щоб лейбли збігалися. Мінус у тому, що selector'и легко випадково перетнути, і тоді два контролери б'ються за ті самі поди.

Що можна покращити

  • Замінити послідовні індекси на випадкові суфікси імен, щоб scale-up після видалення в середині не конфліктував.
  • Реалізувати розумний порядок scale-down: спершу Failed, потім Pending, далі наймолодші поди.
  • Додати проміжний шар ReplicaSet, щоб з'явилася база для rolling update і відкату.
  • Ввести ownerReferences і каскадне видалення замість покладання тільки на selector.

Спробуй сам

# Scale deployment:
sheepctl scale deployment web --replicas 5
sheepctl get pods     # побачиш 5 подів
sheepctl scale deployment web --replicas 2
sheepctl get pods     # побачиш 2 поди

Масштабування працює. Далі - Service Discovery: як Service автоматично знаходить свої поди.

Ресурси

  • ReplicaSet — декларативна реплікація в Kubernetes
  • ReplicationController — оригінальний (нині legacy) контролер, на честь якого названо наш клас
  • Deployment — шарування Deployment → ReplicaSet → Pod
  • HorizontalPodAutoscaler — автоматичне масштабування, що просто переписує spec.replicas
  • Controllers — controller pattern
  • Manage deployments — операційний посібник

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

Попередня: Reconciliation Loop | Наступна: Service Discovery