26 settembre 2017

Scaling orizzontale di microservizi SpringBoot con kubernetes su Azure

In questo tutorial proveremo a creare un'immagine docker a partire da un microservizio SpringBoot. L'immagine sarà poi eseguita in un Azure Container Service orchestrato da Kubernetes che configureremo per fare autoscaling. Applicando un carico di stress apprezzeremo come l'orchestratore manderà in esecuzione altre istanze del microservizio per far fronte alle richieste.

Requisiti

docker, account Azure, account dockerHub, azure CLI, kubectl, immagine docker

Il servizio SpringBoot

Prima di tutto abbiamo bisogno di un microservizio da eseguire e dockerizzare. Se non ne avete uno a disposizione è possibile crearlo in pochi semplici passi utilizzando SpringBoot. Si può fare riferimento a questa guida prima di procedere ai passi successivi.
https://software.oreilly.com/ideas/microservices-for-java-developers/page/2/spring-boot-for-microservices?log-out
Possiamo seguire il tutorial fino a completare il paragrafo "Add the HTTP Endpoints" e saltare il resto. Sarà sufficiente avere un servizio esposto che risponda correttamente all'indirizzo /api/hola.
Aggiungiamo invece un nuovo entry point che ci servirà per stressare il servizio
 @RequestMapping(method = RequestMethod.GET, value = "/stress/{var}", produces = "text/plain")
 public String stress(@PathVariable("var") String var) throws UnknownHostException {

      try {
       
       fatt(Integer.parseInt(var));
              
  } catch (NumberFormatException e) {
   e.printStackTrace();
   return "ERRORE: input numerico richiesto!";
  }
      
      return "delayed for " + var + " cycles";
 }
La funzione fatt(int x) è una semplice implementazione del calcolo del fattoriale appesantita ulteriormente da una sequenza di calcoli trigonometrici.
  private int fatt(int x) {
      int i;
      int f=1;

      for(i=1; i<=x; i=i+1) {
        f=f*i;

        //incrementa il tempo di elaborazione        

        Math.tan(Math.atan( Math.tan(Math.atan( Math.tan(Math.atan( 28564 ))) )));
      }

      return f;
 }
Lo scopo è ovviamente quello di generare un carico computazionale perché sarà poi questa la metrica che utilizzeremo per configurare l'autoscaling.
Per stressare il servizio sarà sufficiente passare un input sufficientemente grande all'indirizzo /api/stress/x (ad esempio 9128811).

Creazione e distribuzione del microservizio su dockerhub

Creazione dell'immagine

Per poter creare l'immagine docker abbiamo bisogno di scrivere il dockerfile che andrà letto ed eseguito in fase di build. Supponendo di eseguire la compilazione attraverso un tool di automation mi dovrò solo preoccupare di indicare una eventuale immagine di partenza, le porte esposte e il comando di pacchettizzazione:
#FROM java:8 FROM frolvlad/alpine-oraclejdk8

#workdirWORKDIR /code

# Prepare by downloading dependencies
ADD target /code/target

EXPOSE 8080
CMD ["java", "-jar", "target/hola-springboot-1.0.jar"]
Altrimenti si può specificare puntualmente come comporre l'immagine e i comandi da eseguire per la compilazione:
#FROM java:8 FROM frolvlad/alpine-oraclejdk8

# Install mavenRUN wget http://ftp.fau.de/apache/maven/maven-3/3.3.9/binaries/apache-maven-3.3.9-bin.tar.gz 
RUN tar -zxvf apache-maven-3.3.9-bin.tar.gz 
RUN rm apache-maven-3.3.9-bin.tar.gz 
RUN mv apache-maven-3.3.9 /usr/lib/mvn
ENV M2_HOME=/usr/lib/mvn
ENV M2=$M2_HOME/bin 
ENV PATH $PATH:$M2_HOME:$M2RUN apk add --update bash && rm -rf /var/cache/apk/*


WORKDIR /code

# Prepare by downloading dependenciesADD pom.xml /code/pom.xml
RUN ["mvn", "dependency:resolve"]

# Adding source, compile and package into a fat jarADD src /code/src
RUN ["mvn", "package"]

ADD target /code/target

EXPOSE 8080

CMD ["java", "-jar", "target/hola-springboot-1.0.jar"]
Per una compilazione in locale ad esempio ho usato questo Dockerfile che aggiunge, rispetto al precedente, l'installazione di maven e la compilazione. Il risultato finale voluto è una immagine contenente una JVM e un jar autoconsistente output della compilazione.
Per generare l'immagine sarà sufficiente utilizzare il comando
 docker build .
Verifichiamo l'immagine creata
PS C:\Users\e.mattei> docker images
REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
emattei/firsttag              <none>              f0b3301b99c4        7 weeks ago         304 MB
busybox                       latest              c30178c5239f        7 weeks ago         1.11 MB
frolvlad/alpine-oraclejdk8    latest              8a75025e4288        2 months ago        170 MB
d4w/nsenter                   latest              9e4f13a0901e        10 months ago       83.8 kB
Notiamo che non ha alcun tag. Per poter caricare la nostra immagine su un registry abbiamo bisogno prima di assegnarle un tag
docker tag f0b3301b99c4 emattei/firsttag

Login su dockerhub

Accediamo a dockerhub per caricare l'immagine appena creata
docker login

Caricamento dell'immagine sul repository dockerhub

Con il comando push saremo in grado di caricare l'immagine sul registry
docker push emattei/firsttag
A questo punto l'immagine sarà disponibile su dockerhub. Per qualsiasi altro registry docker i passi sono analoghi.

3. Creazione del cluster kubernetes

A questo punto dobbiamo creare all'interno della sottoscrizione Azure tutte le risorse necessarie al deploy della nostra applicazione. Utilizzeremo il client az

Creazione del resource-group

Creiamo il resource group che accoglierà le risorse
az group create --name=myKubernetesResourceGroup --location=westeurope

Creazione del cluster kubernetes

All'interno del resource group appena creato creiamo il cluster kubernetes. Dobbiamo scegliere un dns e contestualmente richiediamo la generazione della chiave ssh
az acs create --orchestrator-type=kubernetes \
  --resource-group myKubernetesResourceGroup \
  --name=myKubernetesClusterName \
  --dns-prefix=myPrefix \
  --agent-count=2 \
  --generate-ssh-keys \
  --windows --admin-username myWindowsAdminName \
  --admin-password myWindowsAdminPassword

Scaricare la configurazione del cluster sul client kubectl

Per poter accedere al cluster appena creato attraverso il client kubectl dobbiamo scaricare la configurazione e le credenziali
az acs kubernetes get-credentials 

--resource-group=myKubernetesResourceGroup --name=myKubernetesClusterName
A questo punto sarà possibile accedere al cluster kubernetes dalla macchina locale. Per verificarlo basta eseguire un comando tipo
kubectl get nodes
che restituisce la lista dei nodi

Creazione del servizio autoscaled ed esposizione al pubblico

Creazione service e deployment

kubectl run kubeholahub --image=emattei/firsttag --requests=cpu=200m 

--expose --port=8080

service "kubeholahub" created
deployment "kubeholahub" created
La porta esposta dal servizio kubernetes dovrà coincidere con la porta esposta dall'immagine docker. In questo caso i microservizi realizzati sono esposti sulla 8080.

Definizione delle logice di autoscaling 

E' possibile definire il numero di pods minimo, massimo e le soglie di scalabilità attraverso varie metriche. In questo caso richiediamo di scalare non appena il carico sulla cpu raggiunga il 30%.
kubectl autoscale deployment kubeholahub --cpu-percent=30 --min=1 --max=10
deployment "kubeholahub" autoscaled

Esposizione pubblica del servizio

Per verificare quanto realizzato e procedere con i test di carico vogliamo poter raggiungere i microservizi dall'esterno e pertanto creiamo un bilanciatore di carico
kubectl expose deployment kubeholahub --type=LoadBalancer 

--name=kubeholahub-service

service "kubeholahub-service" exposed
Dopo alcuni di minuti Azure assegnerà al bilanciatore un indirizzo IP pubblico.
Sarà possibile verificare lo stato dell'assegnazione attraverso il comando
kubectl get svc

NAME                CLUSTER-IP  EXTERNAL-IP  PORT(S)     AGE
kubeholahub         1xx.xxx.xxx.xx1 <none>      8080/TCP    21m

kubeholahub-service 1xx.xxx.xxx.xx3   <pending>   8080:30885/TCP 20m



kubectl get svc

NAME                CLUSTER-IP  EXTERNAL-IP      PORT(S)     AGE
kubeholahub         1xx.xxx.xxx.xx1 <none>          8080/TCP    1h

kubeholahub-service 1xx.xxx.xxx.xx3   1xx.xxx.xxx.x10 8080:30885/TCP 1h

Prima di proseguire procediamo con alcune verifiche
Attraverso il comando kubectl get pods otteniamo la lista e lo stato dei pods sul cluster
kubectl get pods

NAME             READY  STATUS  RESTARTS AGE

kubeholahub-2216774994-47401/1   Running 0     1m
Attraverso il comando kubectl get deployments otteniamo la lista dei deployments
kubectl get deployments

NAME     DESIRED CURRENT UP-TO-DATE AVAILABLE AGE

kubeholahub 1    1    1      1     2m
Attraverso il comando kubectl get hpa possiamo vedere lo stato del horizontal autoscaling pod. Dovremmo ottenere qualcosa di simile a questo:
kubectl get hpa

NAME     REFERENCE        TARGET  CURRENT MINPODS MAXPODS AGE

kubeholahub Deployment/kubeholahub 50%   3%    1    10    1h
Se tutto è in ordine siamo in grado di raggiungere dall'esterno il microservizio esposto. Sarà sufficiente puntare all'IP assegnato da azure chiamando le API di riferimento:

http://1xx.xxx.xxx.xx0:8080/api/hola

Setup del monitoring e test di carico

Panello di monitoring 

Ora abbiamo un microservizio in esecuzione su un cluster kubernetes con autoscaling definito. Il passo successivo sarà quello di eseguire dei test di carico per stressare la cpu oltre la soglia definita e vedere come reagisce l'orchestratore.
Quindi abbiamo bisogno di monitorare lo stato dei pods.
Abbiamo almeno due opzioni: attraverso CLI con i comandi visti al passo precedente oppure attraverso un pannello di monitoring grafico che è sufficiente attivare con il seguente comando:
kubectl proxy
Il pannello sarà raggiungibile all'indirizzo http://localhost:8001/ui oppure http://localhost:8001/api/v1/namespaces/kube-system/services/kubernetes-dashboard/proxy/



In questa fase abbiamo una sola istanza ed un carico minimo sulla CPU

Setup di un test di carico

Il test di carico consisterà semplicemente in una sequenza di request verso la API appositamente esposta. Per crearlo sfrutteremo ancora docker scaricando ed eseguendo una shell linux attraverso la quale potremo realizzare ed eseguire uno script nel nostro linguaggio preferito.
In questo esempio eseguiremo ubuntu e utilizzeremo uno script bash. Abbiamo però bisogno di installare prima il comando wget, quindi ci dobbiamo preoccupare di creare un'immagine custom a partire dall'immagine ufficiale.
Creiamo un dockerfile allo scopo:
FROM ubuntu:latest
RUN  apt-get update \
  && apt-get install -y wget \
  && rm -rf /var/lib/apt/lists/*
Creiamo l'immagine e la mandiamo in esecuzione:
docker build -t ubuntuwget .docker run -it --rm ubuntuwget
al prompt sarà sufficiente definire lo script nel modo seguente:





Una volta lanciato lo script sarà possibile vedere il container scalare. Il cluster impiega tempi nell'ordine di circa un minuto per replicare la nostra immagine:







Le istanze lanciate iniziano a ridurre il carico





Aumentiamo ulteriormente il carico







In questo caso è interessante notare che il container vorrebbe mandare in esecuzione tutte le 10 istanze del microservizio che avevamo definito
kubectl autoscale deployment kubeholahub --cpu-percent=30 --min=1 

--max=10deployment "kubeholahub" autoscaled
Ma le sette già create saturano la cpu assegnata.
Terminiamo il carico e attendiamo qualche minuto per consentire lo scaling down







Il carico è tornato a 0 e quindi sono state terminate le istanze non più necessarie.

Riferimenti:

https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale-walkthrough
https://docs.microsoft.com/en-us/azure/container-service/container-service-kubernetes-windows-walkthrough