Replication Controller: scale up і scale down¶
Written by:
Igor Gorovyy
DevOps Engineer Lead & Senior Solutions Architect
У попередній частині ми побачили 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
