Модуль 12 / 19
StatefulSet, DaemonSet, Job и CronJob
Содержание
1. Цели и результат модуля
Вы научитесь выбирать controller по требуемой гарантии: стабильная identity, один Pod на Node, конечная задача или расписание.
- StatefulSet даёт ordinal identity и упорядоченные операции
- DaemonSet согласует Pod с подходящими Nodes
- Job отслеживает успешные completions
- CronJob создаёт Jobs по расписанию
- ни один controller сам по себе не делает приложение HA
2. Теория: controller выбирают по семантике
Deployment подходит взаимозаменяемым replicas. StatefulSet нужен, когда важны стабильные имена или порядок. DaemonSet моделирует node-local функцию. Job имеет конечный результат, CronJob — расписание создания Jobs. Выбор по привычке создаёт неверные гарантии lifecycle.
3. StatefulSet: identity и порядок
Pod получает имя <set>-<ordinal>, стабильный network identity через headless Service и предсказуемые PVC names при volumeClaimTemplates. Удалённый web-0 создаётся снова как web-0, но с новым UID.
StatefulSet не реплицирует данные и не гарантирует кворум. Без persistent volume данные контейнера всё равно исчезают. OrderedReady облегчает некоторые последовательные системы, но может блокировать progress; Parallel меняет ordering guarantees.
4. DaemonSet: node-local workload
DaemonSet controller создаёт Pod на каждом подходящем Node с учётом selector, affinity, taints/tolerations. Типичные случаи: log agent, CNI, storage plugin, node exporter. Количество Pods равно eligible Nodes, а не фиксированному replicas.
На одноузловом стенде ожидается один Pod. В HA-курсе тот же manifest покажет различие между control-plane и worker placement.
5. Job: completion semantics
Job управляет Pods до нужного числа успешных completions. parallelism ограничивает одновременную работу; backoffLimit ограничивает повторы после failures; activeDeadlineSeconds ограничивает общее время. restartPolicy для Job — Never или OnFailure.
Задача должна учитывать at-least-once execution: даже при одном completion код обязан быть идемпотентным либо дедуплицировать side effects.
6. CronJob: расписание, а не точный таймер
CronJob создаёт Jobs по cron schedule. controller loop, downtime и clock skew означают, что запуск не является real-time сигналом. timeZone задавайте явно. concurrencyPolicy Allow, Forbid или Replace управляет пересечением Jobs, но не заменяет locking внутри приложения.
startingDeadlineSeconds ограничивает допустимое опоздание; suspend приостанавливает новые запуски; history limits чистят старые Jobs, но не являются log retention.
7. Методические указания
- Формулируйте нужную lifecycle guarantee до kind
- Не называйте StatefulSet базой данных высокой доступности
- Проверяйте ownerReferences и status controller
- Для DaemonSet считайте eligible Nodes
- Делайте Jobs идемпотентными
- Задавайте timeZone CronJob
- Не используйте CronJob history как архив логов
- Читайте Job conditions, failed Pods и Events вместе
8. Лабораторная работа: четыре controller
Создайте headless Service, StatefulSet, DaemonSet, Job и CronJob. Повторный apply после первого прохода нужен для документов namespaced после создания Namespace.
mkdir -p /tmp/k8s-m12
cat > /tmp/k8s-m12/workloads.yaml <<'EOF'
apiVersion: v1
kind: Namespace
metadata: {name: k8s-m12}
---
apiVersion: v1
kind: Service
metadata: {name: peers, namespace: k8s-m12}
spec:
clusterIP: None
selector: {app: stateful-web}
ports: [{name: http, port: 80}]
---
apiVersion: apps/v1
kind: StatefulSet
metadata: {name: web, namespace: k8s-m12}
spec:
serviceName: peers
replicas: 2
selector: {matchLabels: {app: stateful-web}}
template:
metadata: {labels: {app: stateful-web}}
spec:
containers:
- name: nginx
image: nginx:1.29-alpine
command: ["sh", "-c"]
args: ["printf 'pod=%s\n' "$HOSTNAME" > /usr/share/nginx/html/index.html; exec nginx -g 'daemon off;'"]
ports: [{name: http, containerPort: 80}]
readinessProbe: {httpGet: {path: /, port: http}, periodSeconds: 2}
---
apiVersion: apps/v1
kind: DaemonSet
metadata: {name: node-agent, namespace: k8s-m12}
spec:
selector: {matchLabels: {app: node-agent}}
template:
metadata: {labels: {app: node-agent}}
spec:
containers:
- name: agent
image: busybox:1.37
command: ["sh", "-c", "echo node=$NODE_NAME; sleep 3600"]
env: [{name: NODE_NAME, valueFrom: {fieldRef: {fieldPath: spec.nodeName}}}]
---
apiVersion: batch/v1
kind: Job
metadata: {name: batch, namespace: k8s-m12}
spec:
completions: 3
parallelism: 2
backoffLimit: 2
template:
spec:
restartPolicy: Never
containers:
- {name: task, image: busybox:1.37, command: ["sh", "-c", "echo completed-by=$HOSTNAME; sleep 2"]}
---
apiVersion: batch/v1
kind: CronJob
metadata: {name: report, namespace: k8s-m12}
spec:
schedule: "*/5 * * * *"
timeZone: Etc/UTC
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 2
failedJobsHistoryLimit: 1
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- {name: report, image: busybox:1.37, command: ["sh", "-c", "date -Iseconds; echo report=$HOSTNAME"]}
EOF
kubectl apply -f /tmp/k8s-m12/workloads.yaml || true
kubectl apply --dry-run=server -f /tmp/k8s-m12/workloads.yaml
kubectl apply -f /tmp/k8s-m12/workloads.yaml9. Лабораторная работа: stable identity
Проверьте DNS web-0/web-1. После удаления web-0 имя сохраняется, UID меняется: identity controller стабильна, object instance — новая.
kubectl -n k8s-m12 rollout status statefulset/web --timeout=180s
kubectl -n k8s-m12 get statefulset,pod -l app=stateful-web -o wide
kubectl -n k8s-m12 run dns-client --image=busybox:1.37 --restart=Never --command -- sleep 3600
kubectl -n k8s-m12 wait --for=condition=Ready pod/dns-client --timeout=120s
kubectl -n k8s-m12 exec dns-client -- nslookup web-0.peers
kubectl -n k8s-m12 exec dns-client -- nslookup web-1.peers
old_uid=$(kubectl -n k8s-m12 get pod web-0 -o jsonpath='{.metadata.uid}')
kubectl -n k8s-m12 delete pod web-0 --wait=true
kubectl -n k8s-m12 wait --for=condition=Ready pod/web-0 --timeout=120s
kubectl -n k8s-m12 get pod web-0 -o jsonpath='name={.metadata.name}{" uid="}{.metadata.uid}{"\n"}'
printf 'old uid=%s\n' "$old_uid"10. Лабораторная работа: DaemonSet, Job и CronJob
Сопоставьте один agent Pod с одним Node. Затем исследуйте 3 completions при parallelism 2 и создайте немедленный Job из CronJob template без ожидания расписания.
kubectl -n k8s-m12 rollout status daemonset/node-agent --timeout=120s
kubectl get nodes -o wide
kubectl -n k8s-m12 get daemonset node-agent
kubectl -n k8s-m12 get pods -l app=node-agent -o custom-columns=POD:.metadata.name,NODE:.spec.nodeName,OWNER:.metadata.ownerReferences[0].kind
kubectl -n k8s-m12 logs -l app=node-agent --prefixkubectl -n k8s-m12 wait --for=condition=Complete job/batch --timeout=120s
kubectl -n k8s-m12 get job batch
kubectl -n k8s-m12 get pods -l job-name=batch
kubectl -n k8s-m12 logs job/batch --prefix
kubectl -n k8s-m12 create job report-now --from=cronjob/report
kubectl -n k8s-m12 wait --for=condition=Complete job/report-now --timeout=120s
kubectl -n k8s-m12 logs job/report-now
kubectl -n k8s-m12 get cronjob report -o yaml11. Диагностика: исчерпанный backoff
Задача всегда возвращает 42. Job создаёт failed Pods до исчерпания backoffLimit и получает Failed condition. Логи одного Pod недостаточны: проверяйте все attempts, Job status и Events.
cat > /tmp/k8s-m12/failing-job.yaml <<'EOF'
apiVersion: batch/v1
kind: Job
metadata: {name: failing, namespace: k8s-m12}
spec:
backoffLimit: 2
template:
spec:
restartPolicy: Never
containers:
- {name: task, image: busybox:1.37, command: ["sh", "-c", "echo deliberate failure >&2; exit 42"]}
EOF
kubectl apply --dry-run=server -f /tmp/k8s-m12/failing-job.yaml
kubectl apply -f /tmp/k8s-m12/failing-job.yaml
kubectl -n k8s-m12 wait --for=condition=Failed job/failing --timeout=120s
kubectl -n k8s-m12 get job failing -o wide
kubectl -n k8s-m12 get pods -l job-name=failing
kubectl -n k8s-m12 logs job/failing --prefix
kubectl -n k8s-m12 describe job failing
kubectl -n k8s-m12 get events --sort-by=.metadata.creationTimestampПорядок: controller spec/status → owned Pods → scheduling/image/startup → container terminated reason/exit → retry policy/deadline → application idempotency.
12. Контроль, критерии освоения и cleanup
- Когда Deployment лучше StatefulSet?
- Что стабильно после замены web-0?
- StatefulSet реплицирует данные?
- От чего зависит число DaemonSet Pods?
- Что считают completions и parallelism?
- Почему Job должен быть идемпотентным?
- Что ограничивает backoffLimit?
- Гарантирует ли CronJob запуск ровно один раз?
- Зачем timeZone?
- Чем Forbid отличается от application lock?
kubectl delete namespace k8s-m12 --wait=true
rm -rf /tmp/k8s-m12
kubectl get namespace k8s-m12 2>&1 || trueМодуль освоен, если вы обосновываете выбор каждого controller и диагностируете failed Job от condition до конкретного exit code.