# Здравствуйте, HR и SRE команды Mindbox!
# Меня зовут Дьяченко Юрий, и это тестовое задание на вакансию DevOps-инженер/SRE.
# Первый момент: нашему приложению нужен неймспейс.
# Если мы конечно не хотим, чтобы оно находилось в default (а мы не хотим).
# В целом лучше назвать его как стенд, например dev или qa,
# но мне кажется более подходящим держать всё в абстрактном виде, поэтому app-namespace.
apiVersion: v1
kind: Namespace
metadata:
name: app-namespace
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-deployment
namespace: app-namespace
labels:
app: app
spec:
# На счёт количества реплик.
# Я попробовал рассмотреть 5 и 4 реплик и выбрать лучший вариант.
# Вот аргументы за 5 подов:
# Да, нагрузочное тестирование показало, что 4 пода справляются с пиковой нагрузкой,
# но 5 мне кажется более правильным решением по следующим причинам:
# 1. это тестирование, то есть реальная нагрузка может быть и больше, в таком случае лучше иметь запас;
# 2. так как у нас пять нод, в целях обеспечения отказоустойчивости целесообразно было бы разнести по поду на каждую ноду,
# ведь, если упадёт одна нода при 5 репликах, это не повлияет на общую работоспособность приложения,
# а если у нас пик, и реплик изначально 4, и отказывает узел?
#
# Но контраргументировав их, я всё же пришёл к 4:
# 1. нагрузочное тестирование нужно не просто так, и если известно, что 4 пода выдерживают пик, то ещё один под
# это пустая трата ресурсов. Этот под при крайней необходимости и так будет поднят.
# 2. 4 пода прекрасно разнесутся на 5 нод, ещё и место свободное оставят на всякий случай. Такой вариант экономичнее.
replicas: 4
# На счёт обновлений.
# На самом деле такие значения получатся по умолчанию, но мне кажется лучше держать их в явном виде и не в процентах.
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
affinity:
# Помните про идею разнести по поду на ноду? Вот реализация :)
# Я думаю всё таки это правило должно быть рекомендательным, нежели обязательным,
# потому что не хочется чтобы какие-то поды висели в Pending.
# Они всё равно будут размещены на доступных узлах, даже если одна или даже две ноды будут недоступны.
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- app
topologyKey: "kubernetes.io/hostname"
# И добавлю ещё нестрогое правило размещения по зонам (ведь кластер у нас мультизональный).
# Это повысит отказоустойчивость.
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- app
topologyKey: "topology.kubernetes.io/zone"
containers:
- name: app
# Думал поставить app:latest, но так лучше не делать, потому что это ведёт к непредвиденному поведению.
image: app:1.0.0
# Далее про ресурсы.
# Сначала я поставил реквесты 0.1 CPU и 128M, а лимиты 1 CPU и 256M. Это было плохое решение.
#
# Обо всём по порядку, начнём с CPU:
# В первом решении я фактически на авось тыкнул пальцем в небо, поставив лимит 1.
# Затем, когда задумался об экономии ресурсов и использовании hpa, понял, что лимит мне
# в этом не помогает, а наборот - мешает, ограничивая потребление. Мы не можем знать лимит.
# Подбирать его — неблагодарное дело,
# особенно когда в задании "на первые запросы приложению требуется значительно больше ресурсов CPU",
# а значительно много — это сколько? Можно прогадать.
# Подумав, я пришёл к выводу, что гораздо более правильным вариантом будет отказ от лимита вовсе
# и управлять CPU через реквест и HPA.
# Появилась другая проблема - выставление реквеста: нужно такое значение реквеста,
# которое будет экономичным и обеспечит корректную работу HPA, то есть правильное количество подов при нужной нагрузке.
# После попыток решить это математически, я перешёл к подбору и остановился на 0.2
#
# Теперь о memory:
# С реквестом понятно, он практически указан в задании - 128Mi, а лимит я снова взял с размахом - 256Mi,
# написав, что это убережёт нас от нагрузки. Но это слишком много, от нагрузки не убережёт, а ресурсов потратит.
# Я буквально позволил убивать под, расходующий аномально много, только когда он израсходует в два раза больше.
# Реквест и лимит на пямять нужно держать как можно ближе к друг другу, в идеале - равными.
# В задании "всегда “ровно” в районе 128M memory". Я бы всё-таки уточнил, а сколько это - "в районе".
# Потому что, если в среднем потребление памяти не выходит за 128, тогда реквест и лимит можно поставить одинаково 128.
# Если выходит, но не сильно, то подойдёт 128 и 150. Я поставлю ровно.
resources:
requests:
cpu: "0.2"
memory: "128Mi"
limits:
memory: "128Mi"
# Пригодится проверка на то, готово ли реально приложение принимать трафик.
# Зная, что ему для старта требуется 5-10 секунд ставим первую пробу на 10, а остальные с интервалом в 5.
readinessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 10
periodSeconds: 5
# В целом задержка на ливинесс пробу больше, чтобы сначал убрать трафик от пода, если он завалит рединес пробу,
# а потом перезапустить его, если он завалит ещё и ливинесс пробу, и провести пробы заново.
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 20
periodSeconds: 20
ports:
- containerPort: 80
# Не хватает усиленной экономии ресурсов.
# Ночью нам все поды не нужны, поэтому можно сократить их количесвтво до 3,
# а в моменты пиков нагрузки поднять до 5 при необходимости.
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: app-hpa
namespace: app-namespace
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: app-deployment
# Тут спорно, можно minReplicas поставить и поменьше, зависит от того насколько сильно падает нагрузка.
# В задании "ночью запросов на порядки меньше", то есть в теории можно 2 или даже 1, это очень хорошо сэкономит ресурсы,
# но я поставлю 3 на всякий случай.
minReplicas: 3
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 80
# Используем Service ClusterIP с Ingress вместо простого Service LoadBalancer.
# Почему это лучше?
# - Service скрыт внутри кластера и доступ к нему не прямой, а опосредованный, что повышает безопасноcть
# - Ingress поддерживает много вариантов маршрутизации и балансировки
# - Ingress даёт возможность работы с множеством сервисов
---
apiVersion: v1
kind: Service
metadata:
name: app-service
namespace: app-namespace
labels:
app: app
spec:
type: ClusterIP
selector:
app: app
ports:
- protocol: TCP
port: 80
targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
namespace: app-namespace
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: app.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app-service
port:
number: 80
# Почему нет VPA и PDB?
# Они кажутся мне избыточными по нескольким причинам:
# - нагрузкой на cpu занимается hpa, значит на vpa остаётся память (использование их вместе на одном ресурсе ведёт к непредвиденому поведению)
# - но по заданию память ровная
# - pdb ничем не поможет при незапланированных сбоях, а ограничение количества недоступных подов при плановых обновлениях указано явно
# Итоги:
# 1. отказоустойчивость обеспечена распределеним реплик на разные ноды и зоны (по возможности);
# 2. оптимальное потребление ресурсов днём/ночью и первые/последующие запросы реализовано с помощью реквестов и hpa;
# 3. сбои и преждевременная работа с трафиком минимизированы с помощью проверок;
# 4. доступ к приложению через ClusterIP + Ingress, это повысит безопасность, качество маршрутизации и балансировки.
# Спасибо за прочтение!YurDuiachenko/mindbox-sre
Folders and files
| Name | Name | Last commit date | ||
|---|---|---|---|---|