Workloads
В данной практике предлагаю испробовать работу Horizontal Pod Autoscaler в связке с StatefulSet, увеличивая потребляемые ресурсы приложения с помощью Cronjob.
Metrics server
В первую очередь для работы HPA в кластер необходимо установить metrics server, который позволит hpa контроллеру получать информацию о потребляемых ресурсах подами приложения.
$ k apply -f https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.6.3/components.yaml
$ k patch -n kube-system deployment metrics-server --type=json -p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
Application
Для проверки работы HPA предлагаю использовать масштабирование на основе потребления памяти приложением. Потребуется простое приложение, которое сможет занять память заданного размера, для этого можно использовать вызов mmap, отображая файл в оперативную память. Наше приложение будет отображать файл заданного размера в оперативную память, таким образом можно будет регулировать потребляемую память. Вы можете написать его на своем любимом языке, в качестве примера приведу простую реализацию на C:
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
if (argc < 3) {
printf("too few arguments\n");
return 1;
}
while (1) {
int fd = open(argv[1], O_RDONLY);
if (fd < 0) {
printf("error read file: %s\n", argv[1]);
sleep(strtol(argv[2], NULL, 10));
continue;
}
struct stat s;
int status = fstat(fd, &s);
char *data = mmap(0, s.st_size, PROT_READ, MAP_SHARED, fd, 0);
for (int i = 0; i < s.st_size; i++) {
char c;
c = data[i];
}
printf("size: %lld\n", s.st_size);
sleep(strtol(argv[2], NULL, 10));
munmap(data, s.st_size);
close(fd);
}
}
Данное программа перечитывает файл заданный первым аргументом с периодичностью заданной вторым аргументом и отображает в оперативную память.
Image
Для запуска приложения в kubernetes нам понадобится docker образ, для приведенной выше программы Dockerfile будет выглядеть так:
FROM alpine:3.17 as build
WORKDIR /build
RUN apk add --no-cache clang musl-dev binutils gcc
COPY main.c ./
RUN clang main.c
FROM alpine:3.17
COPY --from=build /build/a.out /app
CMD ["/app", "/tmp/data", "30"]
Таким образом для сборки образа можно выполнить следующие команды:
mkdir testapp;cd testapp
cat << EOF > main.c
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
if (argc < 3) {
printf("too few arguments\n");
return 1;
}
while (1) {
int fd = open(argv[1], O_RDONLY);
if (fd < 0) {
printf("error read file: %s\n", argv[1]);
sleep(strtol(argv[2], NULL, 10));
continue;
}
struct stat s;
int status = fstat(fd, &s);
char *data = mmap(0, s.st_size, PROT_READ, MAP_SHARED, fd, 0);
for (int i = 0; i < s.st_size; i++) {
char c;
c = data[i];
}
printf("size: %lld\n", s.st_size);
sleep(strtol(argv[2], NULL, 10));
munmap(data, s.st_size);
close(fd);
}
}
EOF
cat << EOF > Dockerfile
FROM alpine:3.17 as build
WORKDIR /build
RUN apk add --no-cache clang musl-dev binutils gcc
COPY main.c ./
RUN clang main.c
FROM alpine:3.17
COPY --from=build /build/a.out /app
CMD ["/app", "/tmp/data", "30"]
EOF
docker build -t app:test .
Появится образ app:test
, который необходимо загрузить в кластер. В утилите kind
для этого есть
команда kind load docker-image
, которая загрузит образ на все ноды кластера.
$ kind load docker-image app:test
Image: "" with ID "sha256:7966712b4b2f550489f136128f48fafd8d0224c85f6770ae4562714ce5a0c202" not yet present on node "kind-worker", loading...
Image: "" with ID "sha256:7966712b4b2f550489f136128f48fafd8d0224c85f6770ae4562714ce5a0c202" not yet present on node "kind-worker2", loading...
Image: "" with ID "sha256:7966712b4b2f550489f136128f48fafd8d0224c85f6770ae4562714ce5a0c202" not yet present on node "kind-control-plane", loading...
StatefulSet
Создадим StatefulSet с собранным образом, указав в нем:
resources.requests
- необходимые ресурсы для работы пода, также hpa будет учитывать данный параметр при расчете необходимости увеличения количества репликresources.limits
- максимально возможное потребление для подаimage: app:test
- собранный образimagePullPolicy: Never
- для того, чтобы kubelet не пытался скачать собранный локально образ из внешнего источникаvolumeClaimTemplates
с необходимыми параметрами дискового хранилища, в котором будет лежать файл, загружаемый в памятьvolumeMounts
с указанием пути для монтирования хранилища
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: app
spec:
selector:
matchLabels:
app: app
podManagementPolicy: OrderedReady
replicas: 1
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: app:test
imagePullPolicy: Never
resources:
limits:
memory: 300Mi
requests:
memory: 100Mi
volumeMounts:
- name: data
mountPath: /tmp
updateStrategy:
type: RollingUpdate
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "standard"
resources:
requests:
storage: 300Mi
Для создания можно воспользоваться командой kubectl apply
:
k apply -f - << EOF
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: app
spec:
selector:
matchLabels:
app: app
podManagementPolicy: OrderedReady
replicas: 1
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: app:test
imagePullPolicy: Never
resources:
limits:
memory: 300Mi
requests:
memory: 100Mi
volumeMounts:
- name: data
mountPath: /tmp
updateStrategy:
type: RollingUpdate
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "standard"
resources:
requests:
storage: 300Mi
EOF
После создания пода при установленном metrics server потребление ресурсов подами
можно посмотреть командой kubectl top pod
:
$ k top pod
NAME CPU(cores) MEMORY(bytes)
app-0 1m 1Mi
Horizontal Pod Autoscaler
Для конфигурации автоматического масштабирования необходимо создать ресурс HorizontalPodAutoscaler
,
где необходимо указать:
maxReplicas
- максимальное количество реплик, до которого можно масштабироватьсяminReplicas
- минимальное количество репликaverageUtilization
- средняя утилизация между всеми репликами в процентах от параметраresource.requests
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: app
spec:
maxReplicas: 5
minReplicas: 1
scaleTargetRef:
apiVersion: apps/v1
kind: StatefulSet
name: app
metrics:
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 60
Примечание
HorizontalPodAutoscaler
имеет несколько версий своей спецификации, важно указывать правильную версию
в поле apiVersion
. Управление на основе ресурса памяти есть в стабильной версии autoscaling/v2
.
Информацию по параметрам конкретной версии можно посмотреть командой kubectl explain
с указанием
опции --api-version
, например k explain --api-version='autoscaling/v2' hpa
.
Для создания можно воспользоваться командой kubectl apply
:
k apply -f - << EOF
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: app
spec:
maxReplicas: 5
minReplicas: 1
scaleTargetRef:
apiVersion: apps/v1
kind: StatefulSet
name: app
metrics:
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 60
EOF
После создания можно посмотреть текущее состояние:
$ k get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
app StatefulSet/app 1%/60% 1 5 1 4s
Как видно из вывода - текущее среднее потребление памяти составляет 1% от resource.requests
.
Cronjob
Теперь необходимо сделать механизм для постепенного увеличения потребления, для этого можно сделать небольшой скрипт, который будет создавать файл заданного размера внутри каждого пода. Сохранять состояние между запусками скрипта можно в ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: app
data:
max: "500"
size: "50"
Где size - текущий размер, который необходимо распределить между подами, max - максимальный размер.
Сам скрипт будет выглядеть так:
#!/bin/bash
set -e
NAME="app"
ADD="10"
data="$(kubectl get configmap "$NAME" -o json)"
max="$(echo "$data" | jq -rc '.data.max')"
size="$(echo "$data" | jq -rc '.data.size')"
size="$((size + ADD))"
if [[ "$size" -gt "$max" ]];then
size="$max"
fi
pods=($(kubectl get pods -l app="$NAME" -o name))
count="${#pods[@]}"
pod_size="$((size/count))"
for pod in "${pods[@]}";do
echo "$pod - $pod_size"
kubectl exec "$pod" -- /bin/sh -c \
"dd if=/dev/zero of=/tmp/data.tmp bs=1M count=$pod_size;mv /tmp/data.tmp /tmp/data"
done
kubectl patch configmap "$NAME" -p "{\"data\":{\"size\":\"$size\"}}"
Сам скрипт можно также сохранить в ConfigMap.
Для работы данного скрипта внутри кластера потребуется дать права сервисному аккаунту default, для этого необходимо создать Role и RoleBinding такого вида:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app
rules:
- apiGroups:
- ""
resources:
- pods
- pods/exec
- configmaps
verbs:
- '*'
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: app
subjects:
- kind: ServiceAccount
name: default
namespace: default
Применить данные ресурсы в кластер можно также командой kubectl apply
:
k apply -f - << EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: app
rules:
- apiGroups:
- ""
resources:
- pods
- pods/exec
- configmaps
verbs:
- '*'
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: app
subjects:
- kind: ServiceAccount
name: default
namespace: default
---
apiVersion: v1
kind: ConfigMap
metadata:
name: app
data:
max: "500"
size: "50"
EOF
k apply -f - << 'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
name: app-job-sh
data:
job.sh: |
#!/bin/bash
set -e
NAME="app"
ADD="10"
data="$(kubectl get configmap "$NAME" -o json)"
max="$(echo "$data" | jq -rc '.data.max')"
size="$(echo "$data" | jq -rc '.data.size')"
size="$((size + ADD))"
if [[ "$size" -gt "$max" ]];then
size="$max"
fi
pods=($(kubectl get pods -l app="$NAME" -o name))
count="${#pods[@]}"
pod_size="$((size/count))"
for pod in "${pods[@]}";do
echo "$pod - $pod_size"
kubectl exec "$pod" -- /bin/sh -c \
"dd if=/dev/zero of=/tmp/data.tmp bs=1M count=$pod_size;mv /tmp/data.tmp /tmp/data"
done
kubectl patch configmap "$NAME" -p "{\"data\":{\"size\":\"$size\"}}"
EOF
Для запуска скрипта с заданной периодичностью можно воспользоваться ресурсом Cronjob:
apiVersion: batch/v1
kind: CronJob
metadata:
labels:
app: app
name: app
spec:
jobTemplate:
metadata:
name: app
spec:
template:
metadata:
labels:
app: app-job
spec:
containers:
- image: alpine/k8s:1.25.8
name: app
command:
- /bin/bash
- /script/job.sh
resources: {}
volumeMounts:
- name: script
mountPath: /script
restartPolicy: OnFailure
volumes:
- name: script
configMap:
name: app-job-sh
schedule: '*/5 * * * *'
Данный Cronjob будет запускать созданный скрипт каждые 5 минут, создадим также через kubectl apply
:
k apply -f - << EOF
apiVersion: batch/v1
kind: CronJob
metadata:
labels:
app: app
name: app
spec:
jobTemplate:
metadata:
name: app
spec:
template:
metadata:
labels:
app: app-job
spec:
containers:
- image: alpine/k8s:1.25.8
name: app
command:
- /bin/bash
- /script/job.sh
resources: {}
volumeMounts:
- name: script
mountPath: /script
restartPolicy: OnFailure
volumes:
- name: script
configMap:
name: app-job-sh
schedule: '*/5 * * * *'
EOF
Scaling
После запуска скрипта из CronJob с некоторой задержкой будет отображаться новое потребление у пода:
$ k top po
NAME CPU(cores) MEMORY(bytes)
app-0 10m 64Mi
$ k get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
app StatefulSet/app 64%/60% 1 5 1 52m
Последующие запуски будут увеличивать потребление и разделять между подами, в свою очередь hpa при прохождении порога в 60% средней нагрузки между подами и нахождении за этим порогом достаточное время будет увеличивать количество реплик.
$ k top po
NAME CPU(cores) MEMORY(bytes)
app-0 19m 44Mi
app-1 7m 43Mi
$ k get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
app StatefulSet/app 44%/60% 1 5 2 61m
По итогу постепенно развернутся все поды до максимального количества, заданного в hpa:
$ k top po
NAME CPU(cores) MEMORY(bytes)
app-0 0m 58Mi
app-1 0m 59Mi
app-2 14m 58Mi
app-3 17m 57Mi
app-4 0m 57Mi
$ k get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
app StatefulSet/app 58%/60% 1 5 5 159m
Примечание
Чтобы процесс шел быстрее можно увеличить частоту запуска Job в CronJob через параметр schedule
.
После можно вернуть количество потребляемых ресурсов в ConfigMap в 0:
$ k patch cm app -p '{"data":{"size": "0"}}'
configmap/app patched
Что через некоторое время приведет к уменьшению количества реплик:
$ k get po -l app=app
NAME READY STATUS RESTARTS AGE
app-0 1/1 Running 0 3h48m
app-1 1/1 Running 0 117m
app-2 0/1 Terminating 0 87m
$ k get po -l app=app
NAME READY STATUS RESTARTS AGE
app-0 1/1 Running 0 3h49m
$ k get hpa
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
app StatefulSet/app 7%/60% 1 5 1 172m
Все примеры, используемые здесь, можно также посмотреть на github.