Spring Boot + Kubernetes — Scalabilità con Horizontal Pod Autoscaler (HPA)

Andrea Scanzani
Architetture Digitali
8 min readNov 27, 2020
Spring Boot Kubernetes HPA

In questo articolo andiamo a vedere come effettuare un deploy di un Microservizio realizzato con Spring Boot su un cluster Kubernetes; successivamente andremo a configurare il meccanismo per rendere il nostro Microservizio scalabile in base alle necessità di utilizzo (in questo caso sul consumo della CPU), questa tecnica si chiama Horizontal Pod Autoscaler (HPA).

Tutto il sorgente Java e le configurazioni Kubernetes sono disponibili al seguente Git repos:

Realizzazione Microservizio con Spring Boot

Spring Boot Kubernetes HPA

Come prima cosa andiamo a creare il nostro progetto con Maven, di seguito riportiamo il pom.xml utile a soddisfare le dipendenza di Spring Boot:

pom.xml

Il nostro pom.xml è minimale in quanto andiamo ad utilizzare solo due dipendenze di Spring Boot:

  1. spring-boot-starter-web → Per creazione di un Servizio REST
  2. spring-boot-starter-actuator → Per esporre i nostri endpoint di Health, Liveness e Readiness

Per configurare l’actuator e dirgli che vogliamo esporre anche i Servizi Probe per Kubernetes, creiamo il file ‘application.properties’ e inseriamo la seguente riga:

management.health.probes.enabled=true

Procediamo con la scrittura di un Servizio REST minimale, con un solo Endpoint di tipo GET:

Java Code

Nel controller abbiamo aggiunto una logica per incrementare l’uso della CPU (per simulare delle logiche computazionali di un’applicazione reale).

Ora il nostro Microservizio è pronto e possiamo eseguirlo dalla linea di comando:

mvn clean spring-boot:run

La Spring Boot Application ora espone il servizio:

La nosta applicazione ora è pronta per essere Conteinerizzata, quindi andremo a creare la nostra immagine Docker. Per crearla abbiamo necessita di avere il file Dockerfile per comporre i Layer dell’immagine.

Questo il nostro Dockerfile:

Dockerfile

L’istruzione FROM indica quale immagine andiamo ad utilizzare come Runtime della nostra Spring Boot App, poi con ARG e COPY andiamo a inserire il nostro JAR creato e l’istruzione ENTRYPOINT indica come il container dovrà startare l’applicazione.

Nota Bene: Stiamo utilizzando l’immagine balenalib/armv7hf-alpine-openjdk:8 in quanto la nostra immagine girerà sul Raspberry Pi, in quanto abbiamo già pronto l’ambiente Docker + Kubernetes (K3s) eseguito in questo Articolo. Se la vostra immagine deve girare su una architettura diversa dalla ARMv7 32Bit (Raspberry Pi) è necessario utilizzare un altra immagine nell’instruzione FROM.

Adesso è tutto pronto per buildare la nostra immagine Docker:

mvn clean packagedocker build -t spring-boot/hello-app .

Al termine del processo troviamo la nostra immagine nel registri locale di Docker:

docker image ls

E avremo il seguente output:

Docker images
Docker images

Kubernetes Deploy

In questo esempio noi stiamo utilizza l’instanza di Kubernetes che gira su K3s, che ha già integrato e configurato Traefik (un Edge Router Cloud Native); se utilizzate altri prodotti come Microk8s o Minikube dovrete assicurarvi di avere Traefik installato e configurato, cosi da esporre il nostro Servizio tramite Traefik. In alternativa dovrete effettuare un kubectl port-forwarding.

Creiamo il nostro file di deployment per Kubernetes:

hello-app-deployment.yaml

Per chiarezza evidenzio qualche configurazione inserita nel file sopra. Abbiamo tre strutture Deployment (per la creazione del nostro Container), Service (il gestore dei Pod creati dal Deployment) e Ingress (utile per indirizzare il traffico da e verso Traefik). Utilizziamo le seguenti proprietà:

  • imagePullPolicy: IfNotPresent → Per dire a Kubernetes che se non trova l’immagine spring-boot/hello-app nei repository pubblici dove andrà a cercare l’immagine, deve provare a cercarla sul repository locale di Docker. (Attenzione K3s deve utilizzare come runtime dei Container Docker e non Containerd)
  • containerPort: 8080 → Indichiamo a Kubernetes quale porta utilizza il nostro Microservizio
  • livenessProbe e readinessProbe → Specifichiamo a Kubernetes quale endpoint deve utilizzare per utilizzare le due Probe per capire lo stato di salute della nostra applicazione
  • resources/limits/cpu → Configuriamo le risorse richieste e i limiti (in questo caso solo a livello CPU, ma possiamo specificare anche altri criteri come la Memoria). Questo è utile per non far prendere tutte le risorse ad un solo Servizio, e soprattuto per abilitare l’Autoscaler!
  • pathType: Prefix e path: “/hello-app” → Specifichiamo a Traefik per quale Prefisso Url dove reindirizzare le chiamate al nostro Service.

Il nostro file di deployment è pronto, e possiamo effettuare l’installazione con il comando:

kubectl create -f hello-app-deployment.yaml

Al termine possiamo controllare che tutti e 3 i nostri oggetti di Deploy (Deployment, Service e Ingress) siano stati installati.

> kubectl get pod
NAME READY STATUS RESTARTS AGE
hello-app-deployment-5468847d57-m4dql 1/1 Running 0 65m
> kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 4h31m
hello-app-service ClusterIP 10.43.84.106 <none> 8080/TCP 4h30m
> kubectl get ingress
NAME CLASS HOSTS ADDRESS PORTS AGE
hello-app-ingress <none> * 192.168.1.100 80 4h30m
Verifica Deploy
Verifica Deploy

Come per magia avremo il nostro Microservizio esposto tramite Traefik, richiamando la seguente url:

http://<MACCHINA>/hello-app/sayHello

Installiamo metric-server su Kubernets (K3s)

Per configurare la metrica sul quale Kubernetes si basa per permetterci la scalabilità con HPA (Horizontal Pod Autoscaler), abbiamo la necessità di installare la componente metric-server che semplifica la raccolta delle metriche dai vari oggetti deployati sul cluster Kubernetes.

Come indicato dalla documentazione ufficiale di K3s (link) è consigliata l’installazione di questo metric-server.

Per l’installazione possiamo seguire i passi descritti dalla documentazione sopra, e configurare poi gli oggetti del deploy di metric-server per abilitare la modalità Development (per bypassare la sicurezza TLS). Per questo o già modificato l’installazione del metric-server già pronta per il nostro utilizzo:

metric-server-deployment.yaml

Per l’installazione usiamo il seguente comando:

kubectl create -f metric-server-deployment.yaml

HPA — Horizontal Pod Autoscaler

HPA ridimensiona automaticamente il numero di pod in un controller di replica in base all’utilizzo della CPU (oppure altre metriche custom, come ad esempio con l’ausilio di Prometheus).

Il cluster Kubernetes regola periodicamente il numero di repliche in un controller per abbinare l’utilizzo medio della CPU osservato al target specificato.

Kubernetes HPA
Kubernetes HPA

Ora configuriamo l’API che si occupa di HPA, andiamo a creare il nostro file di configurazione, dando indicazione della soglia target:

hello-app-hpa-deployment.yaml

Nel file specifichiamo:

  • name: hello-app-deployment → Il nostro servizio creato in precedenza
  • minReplicas: 1 e maxReplicas: 5 → Diamo indicazione a Kubernetes di quanto può scalare il nostro Servizio.
  • name: cpu e targetAverageUtilization: 70 → La soglia (CPU) per la quale è richiesta la creazione di un’altro POD del nostro Servizio, abilitando delle repliche aggiuntive.

Installiamo la configurazione:

kubectl create -f hello-app-hpa-deployment.yaml

Ora la nostra configurazione è pronta, quando la nostra Spring Boot Application raggiungerà la soglia di utilizzo CPU del 70%, Kubernetes automaticamente andrà a replicare, quindi scalare, il nostro Servizio in maniera automatica. Ovviamente quando non sarà più necessario, le repliche create al momento saranno killate da Kubernetes.

Scaliamo automaticamente con HPA!

Ora è tutto pronto per vedere il nostro cluster Kubernetes orchestrare per noi la nostra applicazione, creando le repliche necessarie per sopperire al carico di lavoro richiesto alla nostra applicazione.

Per motivi di test, andiamo a configurare e ad abbassare i valori:

  • horizontal-pod-autoscaler-downscale-stabilization → Valore che indica quanto tempo aspettare prima di effettuare il downscale delle repliche per HPA
  • horizontal-pod-autoscaler-upscale-delay → Valore che indica quanto tempo aspettare prima di effettuare il upscale delle repliche per HPA

Sul nostro server K3s, possiamo modificare il file /etc/systemd/system/k3s.service e aggiungere alla fine i nostri parametri:

> cat /etc/systemd/system/k3s.service[Unit]
Description=Lightweight Kubernetes
Documentation=https://k3s.io
Wants=network-online.target
After=network-online.target
[Install]
WantedBy=multi-user.target
[Service]
Type=notify
EnvironmentFile=/etc/systemd/system/k3s.service.env
KillMode=process
Delegate=yes
# Having non-zero Limit*s causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
LimitNOFILE=1048576
LimitNPROC=infinity
LimitCORE=infinity
TasksMax=infinity
TimeoutStartSec=0
Restart=always
RestartSec=5s
ExecStartPre=-/sbin/modprobe br_netfilter
ExecStartPre=-/sbin/modprobe overlay
ExecStart=/usr/local/bin/k3s \
server \
'--docker' \
'horizontal-pod-autoscaler-downscale-stabilization=30s' \
'horizontal-pod-autoscaler-upscale-delay=30s'

In questo caso abbiamo inserito 30 secondi per entrambi i valori, cosi da avere evidenza subito dello scaling.

Procediamo al riavvio del servizio K3s:

systemctl stop k3ssystemctl start k3s

Possiamo ora iniziare il nostro test, lanciando il tool Hey per effettuare il Load Test della nostra Spring Boot Application.

hey -c 20 -z 60s http://<MACCHINA>/hello-app/sayHello

Ora che il nostro test è in corso possiamo monitorare la situazione del cluster Kubernetes con i seguenti comandi:

  • kubectl get hpa → Monitoriamo HPA. Fornisce l’indicazione dell’utilizzo della CPU, del Target, e del numero di Repliche attive
  • kubectl top pod → Monitoriamo le risorse utilizzate per ogni Pod (simile al comand top di linux, ma per i pod)
  • kubectl get pods → Vediamo quante repliche della nostra applicazione sono presenti nel cluster

Di seguito lo screenshot del mio Monitoring sul cluster K3s durante il Load Test:

HPA Monitoring

Come vediamo:

  1. All’inizio del test i valori in HPA erano nella soglia quindi non avevamo necessità di repliche.
  2. Il nostro Servizio ha iniziato a “soffrire” il carico di lavoro e come vediamo ha iniziato a consumare più CPU della soglia impostata.
  3. Passato il periodo di horizontal-pod-autoscaler-upscale-delay, vediamo come Kubernetes abbia creato una replica in maniera automatica. Da calcoli vediamo che il totale ha 2 Repliche perche 133% di CPU è inferiore a 70% * 2 Repliche (per creare una terza replica dovremmo aver superato il 140%).
  4. Dal comando get pods vediamo i nostri due pods in stato Running.
  5. Una volta finito il Load Test vediamo come la CPU sia tornata normale e ci siano ancora 2 Repliche attive.
  6. Passato il periodo di horizontal-pod-autoscaler-downscale-delay, vediamo come Kubernetes abbia rimosso una replica, non piu necessaria in quanto sotto la soglia, in maniera automatica.
  7. Dal comando get pods vediamo un solo pod in stato Running.

Per conclure abbiamo visto come Deployare una Spring Boot Application su Kubernetes e come abilitare un meccanismo per avere una scalabilità automatica della nostra Applicazione.

--

--

Andrea Scanzani
Architetture Digitali

IT Solution Architect and Project Leader (PMI-ACP®, PRINCE2®, TOGAF®, PSM®, ITIL®, IBM® ACE).