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

T

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 ( vediDistributed 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.

     

    conclusioni

    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.

A proposito di me

Dario Frongillo

Uno degli admin di Italiancoders e dell iniziativa devtalks.
Dario Frongillo è un software engineer e architect, specializzato in Web API, Middleware e Backend in ambito cloud native. Attualmente lavora presso NTT DATA, realtà di consulenza internazionale.
E' membro e contributor in diverse community italiane per developers; Nel 2017 fonda italiancoders.it, una community di blogger italiani che divulga articoli, video e contenuti per developers.

Gli articoli più letti

Articoli recenti

Commenti recenti