Reconciliation Loop: серце Shepherd¶
Written by:
Igor Gorovyy
DevOps Engineer Lead & Senior Solutions Architect
Reconciliation loop — серце будь-якого оркестратора. Ідея одна: ти описуєш бажаний стан, а контролери постійно порівнюють його з реальним і виправляють різницю. Уся архітектура Kubernetes побудована саме на ній — і ми в Shepherd відтворюємо ту саму модель, беручи Kubernetes за взірець для нашої реалізації.
Паттерн¶
graph TD
A["Observe<br/>прочитати поточний стан"] --> B["Compare<br/>порівняти з бажаним"]
B --> C{"Є різниця?"}
C -->|Так| D["Act<br/>виправити"]
C -->|Ні| E["Sleep<br/>почекати"]
D --> E
E --> A
У Shepherd кожен контролер працює за цим паттерном. Тікер прокидається кожні кілька секунд, контролер дивиться на стан у Store і виправляє, що не збігається.
ReplicationController¶
Найяскравіший приклад. Deployment каже "хочу 3 репліки", контролер рахує скільки є і додає або видаляє:
func (rc *ReplicationController) Run(stopCh <-chan struct{}) {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-stopCh:
return
case <-ticker.C:
rc.reconcile()
}
}
}
func (rc *ReplicationController) reconcile() {
deployments, _ := rc.store.ListDeployments("")
for _, dep := range deployments {
rc.reconcileDeployment(dep)
}
}
func (rc *ReplicationController) reconcileDeployment(
dep *Deployment) {
allPods, _ := rc.store.ListPods(dep.Metadata.Namespace)
var matchingPods []*Pod
for _, pod := range allPods {
if matchLabels(pod.Metadata.Labels, dep.Spec.Selector) {
matchingPods = append(matchingPods, pod)
}
}
current := len(matchingPods)
desired := dep.Spec.Replicas
if current < desired {
// Scale up
for i := 0; i < desired-current; i++ {
rc.createPodForDeployment(dep, current+i)
}
} else if current > desired {
// Scale down - видаляємо з кінця
for i := 0; i < current-desired; i++ {
pod := matchingPods[len(matchingPods)-1-i]
rc.store.DeletePod(
pod.Metadata.Namespace, pod.Metadata.Name)
}
}
// Оновлюємо статус deployment
ready := 0
for _, pod := range matchingPods {
if pod.Status.Phase == PodRunning { ready++ }
}
dep.Status.Replicas = len(matchingPods)
dep.Status.ReadyReplicas = ready
rc.store.UpdateDeployment(dep)
}
Чому це працює надійно¶
Reconciliation loop самовідновлюється. Якщо: - Под впав - контролер побачить, що реплік менше ніж потрібно, і створить нову - Ноду видалили - поди стануть Failed, контролер створить нові, scheduler призначить їх на іншу ноду - Хтось вручну видалив под - те саме, контролер відновить
Нікого не цікавить, чому стан змінився. Контролер просто бачить різницю і виправляє.
Три контролери в Shepherd¶
| Контролер | Інтервал | Що робить |
|---|---|---|
| ReplicationController | 5с | Кількість подів = spec.replicas |
| ServiceController | 5с | Endpoints сервісу = Running поди |
| NodeController | 10с | Heartbeat timeout = NotReady |
Всі три працюють паралельно як goroutines з одним stopCh каналом для shutdown.
Ідемпотентність¶
Кожен reconcile повинен бути ідемпотентним. Виклик двічі поспіль дає той самий результат. Це важливо, бо контролер прокидається регулярно і завжди обробляє весь стан, а не тільки зміни.
Де можна наступити¶
Reconciliation кожні 5 секунд означає затримку до 5 секунд. Для швидкого scale-up це повільно. Kubernetes поєднує event-driven (watch) і periodic (resync) для балансу швидкості і надійності.
- Якщо один reconcile падає з паніки в goroutine — цикл просто зупиняється, а решта системи нічого не помічає. Реальні контролери загортають кожну ітерацію в recover і метрику помилок.
- Два контролери, що працюють з однаковими подами, можуть "перетягувати ковдру": один створює, інший видаляє. У Kubernetes це вирішують owner references і єдиний контролер-власник на ресурс.
💡 Цікаві факти¶
- Сам термін "reconcile" і модель level-triggered (а не edge-triggered) Kubernetes свідомо запозичив з мережевого обладнання: маршрутизатори давно працюють за принципом "звіряйся з бажаним станом, а не реагуй на окрему подію". Тому навіть якщо контролер пропустив подію — наступний цикл усе одно все виправить.
- У Kubernetes майже жоден контролер не звертається до API напряму на кожній ітерації — між ними стоїть informer з локальним кешем і чергою з дедуплікацією. Наш прямий
ListPodsна кожному тіку — саме те, що informer прибирає. controller-runtime(фреймворк, на якому будують більшість операторів) зводить весь контролер до однієї функціїReconcile(req) (Result, error)— повернешrequeueі він сам поставить тебе в чергу знову. Те саме «observe-compare-act», тільки за тебе крутить чергу фреймворк.- Idempotency тут не побажання, а вимога виживання: контролер обробляє весь стан на кожному тіку, тож неідемпотентна дія накопичувала б ефект кожні 5 секунд.
Що я зрозумів, поки розбирався з темою¶
Довго не вкладалося в голові, чому контролер не реагує на події напряму — здавалося марнуванням: навіщо щоразу перечитувати весь стан, якщо змінився один под? Аж поки сам не зловив баг, де пропущена подія залишила систему в неузгодженому стані назавжди. Тоді дійшло: level-triggered цикл стійкий саме тому, що йому байдуже, скільки подій він проґавив — він завжди дивиться на повну картину. Едж-тригер швидший, але одна загублена подія — і ти розсинхронізований назавжди.
Що можна покращити¶
- Додати recover у кожну ітерацію reconcile, щоб паніка в одному контролері не вбивала весь цикл.
- Замінити фіксований тікер на чергу з backoff: при помилці перепланувати з експоненційною затримкою, а не чекати наступного тіку.
- Зробити кроком до event-driven: watch на Store, що будить контролер одразу при зміні, а periodic resync лишити як страхувальну сітку.
- Додати метрики тривалості reconcile і кількості помилок — без них непомітно, що цикл відстає.
Спробуй сам¶
# Створи deployment з 3 репліками:
sheepctl apply -f - <<'EOF'
{"kind":"Deployment","metadata":{"name":"web"},"spec":{"replicas":3,"selector":{"app":"web"},"template":{"metadata":{"labels":{"app":"web"}},"spec":{"containers":[{"name":"web","image":"minimal"}]}}}}
EOF
sheepctl get pods # 3 поди з'являться
# Видали один под вручну:
sheepctl delete pod web-0
sleep 10 && sheepctl get pods # контролер відновить!
Reconciliation працює. Далі - ReplicationController детальніше: scale up і scale down.
Ресурси¶
- Kubernetes controllers — офіційний controller pattern
- Borg, Omega and Kubernetes — уроки декларативних систем
- Patterns of Distributed Systems — каталог Мартіна Фаулера
Вихідний код циклу: github.com/igorgorovoy/sheep-shepherd-meadow
Попередня: Scheduler
