Label Selectors: як зв'язати ресурси без foreign keys¶
Written by:
Igor Gorovyy
DevOps Engineer Lead & Senior Solutions Architect
У SQL ти зв'язуєш таблиці через foreign keys. У Kubernetes зв'язок між ресурсами - через labels. Deployment не має поля "мої поди". Він має selector: "всі поди з labels app=web". Service не знає конкретних подів. Він знає selector. Це фундаментальне рішення, яке впливає на всю архітектуру.
Labels - це просто map¶
type ObjectMeta struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
UID string `json:"uid"`
Labels map[string]string `json:"labels,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
Labels - map[string]string. Ключ і значення - довільні рядки. Жодної схеми, жодної валідації. Конвенція - через домовленість: app=web, tier=frontend, version=v2.
matchLabels - 8 рядків, які зв'язують все¶
func matchLabels(nodeLabels, selector map[string]string) bool {
if len(selector) == 0 {
return true // пустий selector збігається з будь-чим
}
for k, v := range selector {
if nodeLabels[k] != v {
return false
}
}
return true
}
Логіка проста: кожен ключ selector повинен бути в labels ресурсу з тим самим значенням. Це AND-логіка: app=web AND tier=frontend означає обидва повинні збігатися.
Хто з ким зв'язаний¶
graph TB
DEP["Deployment: web<br/>selector: app=web, tier=frontend"]
SVC["Service: web-svc<br/>selector: app=web"]
P1["Pod: web-0<br/>labels: app=web, tier=frontend"]
P2["Pod: web-1<br/>labels: app=web, tier=frontend"]
P3["Pod: debug-pod<br/>labels: app=web, tier=debug"]
P4["Pod: api-0<br/>labels: app=api"]
DEP -->|"match<br/>(app=web AND tier=frontend)"| P1
DEP -->|"match"| P2
DEP -.->|"no match<br/>(tier != frontend)"| P3
SVC -->|"match (app=web)"| P1
SVC -->|"match"| P2
SVC -->|"match"| P3
SVC -.->|"no match"| P4
Зверни увагу: Service з selector app=web збирає три поди (web-0, web-1 і debug-pod), а Deployment з selector app=web, tier=frontend - тільки два (web-0 і web-1). Різна кількість ключів = різна вибірка.
Три типи зв'язків через labels¶
Deployment -> Pods¶
ReplicationController знаходить поди Deployment'а через selector:
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
// scale up або down...
}
Deployment не зберігає список подів. Кожен reconcile він шукає їх заново.
Service -> Pods¶
ServiceController будує endpoints з Running подів, що збігаються:
func (sc *ServiceController) reconcileService(svc *Service) {
pods, _ := sc.store.ListPods(svc.Metadata.Namespace)
var endpoints []string
for _, pod := range pods {
if matchLabels(pod.Metadata.Labels, svc.Spec.Selector) &&
pod.Status.Phase == PodRunning &&
pod.Status.PodIP != "" {
for _, port := range svc.Spec.Ports {
endpoints = append(endpoints,
fmt.Sprintf("%s:%d",
pod.Status.PodIP, port.TargetPort))
}
}
}
svc.Status.Endpoints = endpoints
sc.store.UpdateService(svc)
}
Три умови: labels збігаються, під Running, є IP. Якщо під впав - він автоматично зникає з endpoints на наступному reconcile.
Pod -> Node (nodeSelector)¶
Scheduler перевіряє, чи labels ноди збігаються з nodeSelector поду:
func (s *Scheduler) filterNodes(nodes []*Node,
pod *Pod) []*Node {
var feasible []*Node
for _, node := range nodes {
// ...
if !matchLabels(node.Metadata.Labels,
pod.Spec.NodeSelector) {
continue
}
feasible = append(feasible, node)
}
return feasible
}
Под з nodeSelector: {gpu: "true"} потрапить тільки на ноду з label gpu=true.
Як labels потрапляють на под¶
Коли ReplicationController створює под для Deployment'а, він копіює labels з template і додає labels з selector:
func (rc *ReplicationController) createPodForDeployment(
dep *Deployment, index int) {
pod := &Pod{
Metadata: ObjectMeta{
Name: fmt.Sprintf("%s-%d", dep.Metadata.Name, index),
Labels: mergeLabels(
dep.Spec.Template.Metadata.Labels,
dep.Spec.Selector),
},
Spec: dep.Spec.Template.Spec,
}
rc.store.CreatePod(pod)
}
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
}
Чому не foreign keys¶
| Foreign keys | Labels | |
|---|---|---|
| Створення поду | Оновити Deployment.pods[] | Нічого, labels вже є |
| Перестворення поду (новий UID) | Оновити foreign key | Labels ті самі, зв'язок зберігається |
| Видалення Deployment | CASCADE DELETE подів | Контролер видалить при reconcile |
| Динамічне додавання | INSERT в зв'язкову таблицю | Додай label на под |
| Query | JOIN + WHERE | Лінійний перебір + matchLabels |
Labels простіші для динамічних систем, де ресурси постійно створюються і видаляються.
Нюанс¶
Label matching - повний перебір. Кожен reconcile контролер проходить по всіх подах в namespace і перевіряє labels кожного. Для 10 подів це миттєво. Для 10,000 - вже помітно.
В Kubernetes це вирішено через informers і кеші: при старті контролер завантажує всі поди в пам'ять, потім отримує тільки зміни через Watch API. У Shepherd ми кожні 5 секунд читаємо всі поди з BoltDB. Для навчального проекту - нормально.
Другий момент - немає валідації labels. Нічого не заважає поставити app=web на под, який не має стосунку до web deployment'у. В Kubernetes Admission Webhooks можуть це перевіряти.
Третій - selectors можуть перетинатися. Якщо два Deployments мають selector app=web, обидва "усиновлять" одні й ті самі поди і почнуть воювати за кількість реплік. Наш matchLabels цього не помічає. В Kubernetes для цього у Deployment є selector плюс унікальний pod-template-hash на ReplicaSet.
Цікаві факти¶
- У "великому" Kubernetes selectors бувають двох видів: equality-based (
app=web) і set-based (tier in (frontend, cache),env != prod). НашmatchLabelsреалізує лише перший, найпростіший. - Ключ label у Kubernetes може мати опціональний префікс-домен (
example.com/team), а значення обмежене 63 символами і має відповідати DNS-подібному формату. "Довільний рядок" — це спрощення, яке працює тільки в навчальному проекті. - Labels і annotations виглядають однаково (обидва
map[string]string), але призначення різне: за labels можна шукати й селектити, annotations — лише сховище метаданих, по них селектити не можна. - Той самий механізм selector лежить в основі майже всього: Services, NetworkPolicies, PodAffinity, navіть
kubectl get pods -l app=web. Один примітив — десятки застосувань.
Що я зрозумів, поки розбирався з темою¶
Поки писав ці вісім рядків matchLabels, до мене дійшло, чому Kubernetes свідомо відмовився від foreign keys. Не тому, що labels "елегантніші" — а тому, що под у цій системі ефемерний. Він народжується і вмирає десятки разів на день, щоразу з новим UID. Будь-яке посилання за ID протухло б миттєво. Labels же переживають перестворення: новий под з тими самими labels автоматично знову "свій". Loose coupling тут не стиль — це єдиний спосіб звʼязати те, що постійно зникає.
Що можна покращити¶
- Додати set-based операції (
in,notin,exists) — це перетворює пласкі labels на повноцінну мову запитів. - Збудувати інвертований індекс
label -> []podUIDзамість лінійного перебору. Тоді вибірка за selector стає O(збігів), а не O(всіх подів). - Ввести перевірку перетину selectors на створення Deployment/Service і відхиляти конфліктні — щоб два контролери не билися за одні поди.
- Додати валідацію формату ключів і значень (довжина, дозволені символи), як це робить Kubernetes, щоб ловити помилки до запису в стор.
Спробуй сам¶
# Подивись labels подів:
sheepctl describe pod web-0 | grep -A5 labels
# Створи service з тим самим selector:
sheepctl apply -f - <<'EOF'
{"kind":"Service","metadata":{"name":"test-svc"},"spec":{"selector":{"app":"web"},"ports":[{"port":80,"target_port":8080}]}}
EOF
sheepctl get services # побачиш endpoints з Running подів
Далі - чому под завжди створюється як Pending і що це дає для reliability.
Ресурси¶
- Labels and Selectors — семантика та синтаксис
- Well-known labels, annotations and taints — стандартні ключі
Вихідний код циклу: github.com/igorgorovoy/sheep-shepherd-meadow
Попередня: Desired vs Actual State
