Tracing Distribuito in un architettura a microservizi con Sleuth e Zipkin [2]

  ICT, Rassegna Stampa
image_pdfimage_print

italiancoders vs microservizi episodio 2

Ciao ragazzi,

benvenuti al secondo articolo dedicato al mondo dei microservizi. Sotto trovate il link al primo articolo dove abbiamo esplorato le basi di Kubernetes  andando a realizzare una semplice applicazione a microservizi su Kubernetes.

Oggi parleremo invece  di tracing distribuito.

Esempio di Architettura a microservizi su Kubernetes [PARTE1]

tracing distribuito: perche e’ necessario ?

Immaginiamo di lavorare in un architettura distribuita.  Supponiamo che il cliente si stia lamentando della lentezza dell API orderDetails per recuperare il dettaglio di un ordine. Supponiamo che tale API e’ gestita dal microservizio Order che a sua volta invoca il microservizio Customer e Item per andare a recuperare i dati necessari per creare la risposta. Non finisce qua… magari a sua volta il microservizio Customer invoca il microservizio Company per andare a recuperare altri dati  necessari del Customer per rispondere alla richiesta del microservizio Order. In questo scenario, che vi assicuro non e’  affatto raro nel mondo delle architetture distribuite, abbiamo ben due problemi da risolvere per non impazzire quando si deve analizzare una issue:

  1. Anche se si fa uso  di  appositi  componenti che permettono di aggregare e leggere i log di differenti servizi ( vedi ELK stack che analizzeremo in uno dei prossimi articoli), rimane difficile collegare le Trace di differenti Microservizi che competono/cooperano per gestire la medesima richiesta.
  2. Se una richiesta viene distribuita in sotto richieste come nel’ esempio sopra riportato, e’  difficile monitorare e identificare  quale sotto-richiesta e’  lenta e quindi causa di ritardi nella risposta finale.

Sleuth e Zipkin nascono proprio per risolvere questi problemi.

sleuth

Sleuth e’  una libreria di Spring Cloud che permette di integrare  il sistema di logging con il pattern Distributed tracing . Sleuth in maniera totalmente transparente

  • assegna un identificativo univoco detto Trace Id  per ogni richiesta esterna del client
  • assegna un identificativo detto Span Id per ogni sotto-richiesta gestita da un altro servizio che entra in gioco per soddisfare la richiesta originale del client
  • integra tutte le trace di logging dei nostri microservizi con la coppia <Trace Id, Span Id>

La seguente immagine, di un esempio trovato in rete, può chiarivi quanto detto.

Come puoi vedere dal diagramma sopra, il Trace-Id rimane lo stesso per l’intero flusso della richiesta, ma gli Span Id sono diversi in ogni passaggio.

zipkin

Zipkin è un sistema distribuito open source che fornisce meccanismi per inviare, ricevere, archiviare e visualizzare i dettagli delle trace e relative span. Questo aiuta il team a correlare le attività tra i vari servizi e ottenere una comprensione molto più approfondita di ciò che sta accadendo nei nostri servizi. Zipkin fornisce una dashboard per analizzare le trace nel dettaglio.

Ad esempio puoi andare a interrogare la GUI di Zipkin per Trace Id, andando a leggere i tempi di ogni span id e quindi sotto-richiesta. Altrimenti, è possibile eseguire query in base ad attributi come servizio, nome dell’operazione, tag e durata. Alcuni dati interessanti verranno riepilogati, come la percentuale di tempo trascorso in un servizio e se le operazioni sono fallite o meno.

integriamo il nostro esempio con sleuth e zipkin

Andiamo a integrare l’ esempio che abbiamo sviluppato per l articolo precedente disponibile sul nostro REPO. Per chi non l’ avesse letto si tratta di un esempio di API per creare Post e Commenti.

Di seguito gli step da eseguire sui progetti post-ws e comment-ws

AGGIUNGIAMO SLEUTH

Per aggiungere Sleuth basta includere la dipendenza

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-sleuth-zipkin</artifactId>
</dependency>

e aggiungere le properties nell’application.properties

spring.zipkin.base-url= http://${ZIPKIN_IP}:${ZIPKING_PORT}
spring.zipkin.sender.type=web
spring.sleuth.sampler.probability=1

Andiamo a introdurre le nuove properties:

  • spring.zipkin.base-url: la url di Zipkin utilizzata per inviare i dettagli delle trace
  • spring.zipkin.sender.type: Possiamo inviare le trace tramite richieste http o utilizzando un service bus. In questo momento ho utilizzato la properties web perche’ nel nostro esempio non abbiamo ancora introdotto un service bus. Vi consiglio in produzione di appoggiarsi ad un service bus per evitare una single point of failure.
  • spring.sleuth.sampler.probability: Di default, Spring Cloud Sleuth imposta tutti gli span su non esportabili. Ciò significa che le trace vengono visualizzate nei log della nostra applicazione ma non in zipkin. Questa property accetta un intero tra 0 e 1 che rappresenta la   probabilità’ che uno span venga esportato su Zipkin. Nel nostro esempio abbiamo messo 100%.

Per testare Sleuth, ho inserito una Trace sul controller di Post-ws che gestisce l endpoint GET /posts/{id}/comments

@RequestMapping(value = "/posts/{id}/comments", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<List<Comment>> findCommentOfPost(@PathVariable Long id) throws Exception { log(); log.info("before to call Comment-ws"); this.postService.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Post not found")); List<Comment> comments = null; try { comments = commentServiceClient.getComments(id); } catch (FeignException exc) { throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, exc.getMessage()); } return ResponseEntity.ok(comments);
}

e un altra Trace sul controller  di Comment-ws che gestisce l endpoint invocato dalla sotto richiesta di Post-ws per recuperare i Commenti di un Post.

@RequestMapping(value = "/comments", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<List<Comment>> insertComment(@RequestParam(value = "postId", required = true) final Long postId, @RequestHeader Map<String, String> headers ) throws Exception { log(); log.info("Find Comments"); List<Comment> comments = commentService.findAllByPostId(postId); return ResponseEntity.ok(comments);
}

Andiamo a lanciare i due microservizi e invochiamo una HTTP  GET /posts/{id}/comments.

Se andiamo a leggere i log del servizio post-ws troviamo trace di questo tipo

2020-05-27 18:17:45.009 INFO [post-ws,9570512ba0c92b56,9570512ba0c92b56,true] 18560 --- [nio-8080-exec-3] i.i.p.controller.post.PostController : before to call Comment-ws

In maniera del tutto transparente Sleuth ha integrato la mia trace con le seguenti info

[${spring.application.name}, Trace ID, Span ID, exportable?]

A questo punto se vado a leggere i log del servizio comment-ws trovero delle trace contenente 9570512ba0c92b56 il trace id della richiesta. Ad esempio

2020-05-27 18:21:54.209 INFO [comment-ws,9570512ba0c92b56,98f78102c5b753bc,true] 40032 --- [nio-8081-exec-1] i.i.c.controller.CommentController : Find Comments

Utilizzando tool piu’ evoluti come ELK stack sara’ possibile leggere i log di una richiesta utente piu’ facilmente andando a correlare i diversi log per Trace Id

AGGIUNGIAMO zipkin

Per installare Zipkin ci sono diversi modi, vedi il seguente LINK.

Il modo più’ immediato e’ quello di tirare su Zipkin con Docker

docker run -d -p 9411:9411 openzipkin/zipkin

Una volta running Zipkin potro’ accedere alla sua dashbaord all url: http://localhost:9411/zipkin/

e analizzare le varie richieste client identificate da un Trace Id

e potendo andare in dettaglio sul singolo Trace-id per controllare quanto tempo ha impiegato l’ intera richiesta ma sopratutto quanto tempo ha impiegato il singolo span e quindi la  singola sotto richiesta

ad esempio nello screen sopra si evince che la richiesta ha impiegato in totale 512.286 ms e che lo span figlio 98f78102c5b753bc ha impiegato 475.514 ms. Quindi il grosso del tempo speso e’ impiegato nel microservizio dei commenti.

installiamo SU K8s sleuth/zipkin

In questa sezione andremo a integrare il cluster K8s creato nell articolo precedente andando ad utilizzare le nuove immagine dei servizi post-ws e comment-ws che utilizzano Sleuth come mostrato nelle sezioni precedenti. Andremo infine ad installare anche il servizio Zipkin.

La nuova versione del codice dei nostri due microservizi li trovate sul nuovo REPO.

Aggiorniamo i deployment file dei due microservizi utilizzando la nuova immagine docker e passando due environment variables che rappresentano l ip e porta di Zipkin

apiVersion: apps/v1
kind: Deployment
metadata: name: post-ws-deployment
spec: replicas: 3 selector: matchLabels: component: post-ws template: metadata: labels: component: post-ws spec: containers: - name: post-ws image: dariofrongillo/post-ws:2.0.0 ports: - containerPort: 8080 env: - name: MYSQL_ROOT_USERNAME_POST_DB value: root - name: MYSQL_ROOT_PASSWORD_POST_DB valueFrom: secretKeyRef: name: dbpassword key: MYSQL_ROOT_PASSWORD_POST_DB - name: MYSQL_HOST_POST_DB value: mysql-post-cluster-ip-service - name: MYSQL_PORT_POST_DB value: '3306' - name: COMMENT_WS_PORT value: '8081' - name: COMMENT_WS_IP value: comment-ws-cluster-ip-service - name: SPRING_PROFILE value: docker - name: ZIPKIN_IP value: zipkin-cluster-ip-service - name: ZIPKING_PORT value: '9411'
apiVersion: apps/v1
kind: Deployment
metadata: name: comment-ws-deployment
spec: replicas: 4 selector: matchLabels: component: comment-ws template: metadata: labels: component: comment-ws spec: containers: - name: comment-ws image: dariofrongillo/comment-ws:2.0.1 ports: - containerPort: 8081 env: - name: MYSQL_ROOT_USERNAME_COMMENT_DB value: root - name: MYSQL_ROOT_PASSWORD_COMMENT_DB valueFrom: secretKeyRef: name: dbpassword key: MYSQL_ROOT_PASSWORD_COMMENT_DB - name: MYSQL_HOST_COMMENT_DB value: mysql-comment-cluster-ip-service - name: MYSQL_PORT_COMMENT_DB value: '3306' - name: SPRING_PROFILE value: docker - name: ZIPKIN_IP value: zipkin-cluster-ip-service - name: ZIPKING_PORT value: '9411'

Andiamo infine a creare il deployment del nuovo pod dove girera’ zipkin e il relativo servizio.

apiVersion: apps/v1
kind: Deployment
metadata: name: zipkin-ws-deployment
spec: replicas: 3 selector: matchLabels: component: zipkin template: metadata: labels: component: zipkin spec: containers: - name: zipkin image: openzipkin/zipkin ports: - containerPort: 9411 env: # note: in-memory storage holds all data in memory, purging older data upon a span limit. # you should use a proper storage in production environments - name: STORAGE_TYPE value: mem
apiVersion: v1
kind: Service
metadata: name: zipkin-cluster-ip-service
spec: type: NodePort selector: component: zipkin ports: - port: 9411 targetPort: 9411 nodePort: 32766

Due note:

  • essendo un articolo a fine didattico ho configurato zipkin per usare il database in memory. Potete configurare in produzione zipkin per utilizzare un database reale.
  • ho creato il servizio zipkin-cluster-ip-service, uno speciale servizio che permette di raggiungere fuori dal cluster tale servizio alla porta riportata nel campo nodePort. Tale configurazione va utilizzata solo in ambito di sviluppo. In ottica di produzione dovete configurare il flusso che passi da Ingress come abbiamo fatto per gli endpoint REST.

Una volta apportate tali modifiche ai deployment file e applicati a K8s con kubectl potrete visualizzare la dashboard all url <ipcluster>:32766/zipkin… nel mio caso http:localhost:32766/zipkin.

Il tracing distribuito deve essere un  MUST in un architettura distribuita altrimenti diventa un impresa debuggare, correlare i log di diversi microservizi che collaborano per soddisfare una richiesta client ma sopratutto individuare le latenze.

Prossimo articolo introdurremo ELK stack nella architettura del nostro esempio in modo di centralizzare la lettura di log di diversi servizi.

https://italiancoders.it/tracing-distribuito-in-un-architettura-a-microservizi-con-sleuth-e-zipkin-2/