APIs e Microsserviços

13 nov, 2019

Microsserviços em Go com Prometheus utilizando Rabbitmq e Postgresql

Publicidade
Nas minhas aventuras de estudo na linguagem Go me deparei com a vontade de trabalhar com micro serviços com a linguagem Go visto que é uma linguagem muito divertida, simples e performática.
Ao longo dos estudos conheci o software aberto Prometheus que gerencia muito bem a parte de métricas e gerenciamento de alertas no cenário de micro serviços.
Trabalhei um pouco também com o RabbitMQ nos diversos tipos de exchange para aprender na prática o comportamento de cada uma e a utilização no cenário de micro serviços para comunicação entre os mesmos através de um sistema de mensageria.
Pensei também em algum banco de dados para persistência final após o processamento dos micro serviços e diante do cenário e estrutura escolhi o PostgreSQL, entre os motivos para uso do PostgreSQL no cenário de micro serviços estão:
  • Geralmente as empresas que adotam o cenário de micro serviços estão migrando do monolítico e com isso utilizam banco de dados relacionais.
  • PostgreSQL possui armazenamento em diversos tipos de dados.
  • 100% Open source, facilita a adoção onde não existe preocupação com licenças (escalável).
  • Facilidade no balanceamento de carga com streaming replication.

Por fim para unificar toda esta stack pensei em algum cenário simples para “codar” e simular todo o fluxo de uso destas tecnologias, um cenário de processamento de mensagens onde teríamos um padrão a ser validado, processado e inserido na base pelos micro serviços além do monitoramento através de métricas pelo prometheus.

Arquitetura:

Como regra de layout para mensagem que vamos receber,processar e armazenar criei uma validação simples apenas para estudarmos as tecnologias. Aqui temos a função que será utilizada pelos nossos slaves (workers) que receberam uma mensagem e irão validar se cada bit separado por ponto e vírgula ou dois pontos não possuem letras e são de tamanho 4:

Programa principal — gateway

Abaixo está o código Go do programa principal denominado gateway, ele será responsável por receber todas as requisições e direcionar para a fila do RabbitMQ, bem como irá possuir as métricas default e customizadas do Prometheus

Neste ponto estamos criando o main.go que irá conter os pacotes de http (promhttp), rabbitmq (amqp), custom metrics (promauto), postgresql (pq) entre outras padrões do Go, posteriormente instanciámos algumas variáveis que serão utilizadas para conexões do RabbitMQ e Postgresql, temos também a requestSendmsg que é utilizada para dizer ao nosso serviço http que estamos adicionando ao path metrics (será explicado mais a frente) uma métrica Counter (Contador) chamada main_total_request_sendmsg que será responsável pela quantidade de request do path sendmsg. Por fim iniciamos a função preparaRabbit que será responsável por iniciar a conexão com nosso RabbitMQ bem como o channel especifico e fila.

Criamos a nossa fila denominada conforme passagem do parâmetro onde a mesma terá, a mesma durável, ou seja, caso ocorra um desligamento ou reinicialização do RabbitMQ, nossa fila irá permanecer criada com suas configurações,a mesma não será exclusiva apenas para a conexão que instância, ou seja, esta acessível a todos. Depois associamos na Queue para uma exchange fanout (através do bind), onde basicamente será um direcionamento direto e eliminando a mensagem assim que consumido.

Teremos a função contWorkers que serve basicamente para registrar no PostgreSQL e retornar um numero + 2112 (portal default do master) de worker disponível para os micro serviços que utilizaram deste numero para instanciar em uma porta especifica posterior e a partir da porta default do master (2112).

Inserimos algumas informações na tabela workeruse para registrar o horário e a porta do worker que foi iniciando.

Adicionei também a função metricscustom apenas para complementar este artigo demonstrando uma outra forma de adicionar métricas para uso no Prometheus, no entanto recomendo a utilização das funções do pacote promauto. Temos a função enviaMsg que será responsável pela publicação das mensagens que serão posteriormente lidas,processadas e armazenadas pelos workers, onde instanciámos o channel do RabbitMQ realizamos uma leitura do que recebemos no Body e publicamos na fila, por fim incrementamos o Counter da nossa métrica de request. Iniciamos a função main onde criamos a variavel psqlInfo para formatar a string de conexão com nosso PostgreSQL.

Através da nossa string de conexão realizo uma abertura de conexão com o PostgreSQL, aloco recurso para fechar a conexão e realizo um ping para confirmar se o serviço esta 100%. É feito uma atribuição a variável base para utilização nas demais funções do código e inicializo os serviços http na porta 2112 com os devidos path’s.

  • /workers — responsável por devolver a próxima porta disponível para o worker.
  • /sendmsg — responsável por publicas a mensagem na fila do RabbitMQ.
  • /metrics — responsável por fornecer todas as métricas default do prometheus e métricas custom.

Até este ponto temos nosso serviço gateway pronto para execução, a partir de agora vamos entender um pouco mais sobre o prometheus antes de abordamos a criação dos micro serviços.

O que é o Prometheus?

O Prometheus é um software open-source para health check de suas aplicações, ele utiliza métricas de forma temporal (armazena os dados de forma histórica) para devolver informações de todo start do serviço, o prometheus possui uma flexibilidade muito grande na parte de consultas, além do armazenamento dos dados serem nos pares chave-valor (que contribui na performance) ele possui uma linguagem própria de consulta chamada PromQL que permite gerar gráficos,tabelas e criação de alertas.

Prometheus possui uma ótima integração com o Grafana onde existe facilmente a conexão com o prometheus e diversos dashboards de diversas aplicações feitos pela comunidade para ser utilizado entre o prometheus e Grafana.

Algumas empresas que utilizam o prometheus: Docker, DigitalOcean, SoundCloud, Ericsson.

Existe também o evento oficial do prometheus chamado Promcon e existe diversas palestras no YouTube.

Como instalar o prometheus?

Basicamente você deverá acessar a pagina https://prometheus.io/download/ e baixar a versão do prometheus server (vai estar algo como monitoring system) de acordo com seu sistema operacional. Após o download do mesmo você terá o serviço do prometheus e o arquivo de configuração do mesmo (prometheus.yml), neste arquivo você poderá definir diversas configurações do prometheus, entre as principais:

  • Scrape_interval — tempo que o serviço ira atualizar as métricas das suas aplicações.
  • scrape_configs — informações de suas aplicações que possuem métricas a serem utilizadas pelo prometheus.
  • rule_files — arquivos na linguagem PromQL para definir alertas de acordo com uma expressão PromQL especifica.

Mais na frente vamos configurar nosso prometheus.yml

Codificando os workers

Depois de termos criando nosso aplicação principal que irá servir como “gateway” para enviar as requisições a fila do RabbitMQ, agora vamos criar nosso serviço de worker que realiza a leitura a todo momento da fila do RabbitMQ, processa a mensagem e armazena no PostgreSQL.

Abaixo o código completo e no final alguns detalhes importantes:

package main


import (
    "database/sql"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "strconv"
    "strings"
    "time"


    _ "github.com/lib/pq"
    "github.com/prometheus/client_golang/prometheus/promhttp"
    "github.com/streadway/amqp"
)


var ipMaster = "http://172.18.0.3:2112"
var base *sql.DB


// Const referente as regras da mensageria
const (
    MsgTamInvalido = "Mensagem com tamanho de um dos bits invalidos"
    MsgConLetra    = "Mensagem contem um dos bits com letra(s)"
)


// const referente aos parametros de conexão com o PostgreSQL
const (
    host     = "172.18.0.2"
    port     = 5432
    user     = "postgres"
    password = "1234"
    dbname   = "microservicos"
    iprabbit = "172.18.0.4:5672"
)


func leituraFila(number int) {
    for {
        conn, channel, que := preparaRabbit("ExtressQueue")
        msgs, _ := channel.Consume(
            que.Name,
            "",
            true,
            false,
            false,
            false,
            nil,
        )


        for m := range msgs {
            mensagem := string(m.Body)


            retmsg, result := mapaMensageria(mensagem)
            log.Printf("Mensagem: %s worker: %d", retmsg, number)
            msgstatus := fmt.Sprint(retmsg + " Worker: " + strconv.Itoa(number))


            if result {
                InsertFilaSQL(mensagem, result)
            } else {
                InsertFilaSQL(msgstatus, result)
            }


        }
        conn.Close()
    }


}


// InsertFilaSQL função responsavel por inserir no PostgreSQL o resultado da mensageria
func InsertFilaSQL(mensagem string, result bool) {


    sqlStatement := `
    CREATE TABLE IF NOT EXISTS public.filarabbit
    (
        mensagem text COLLATE pg_catalog."default",
        resultado "char",
        datahora timestamp without time zone
    )
    WITH (
        OIDS = FALSE
    )
    `


    _, err := base.Exec(sqlStatement)
    if err != nil {
        panic(err)
    }


    sqlStatement =
        `
    INSERT INTO FilaRabbit (mensagem, resultado, datahora)
    VALUES ($1, $2, $3)`
    horario := time.Now()
    _, err = base.Exec(sqlStatement, mensagem, result, horario.Format("2006-01-02 15:04:05"))
    if err != nil {
        panic(err)
    }


}


func mapaMensageria(msg string) (string, bool) {
    mensagem := ""
    f := func(c rune) bool {
        return c == ':' || c == ';'
    }


    // Separa a mensageria
    msgFields := strings.FieldsFunc(msg, f)
    for i := 0; i < len(msgFields); i++ {
        if len(msgFields[i]) != 4 {
            mensagem = MsgTamInvalido
            return mensagem, false
        }
        if strings.ContainsAny(msgFields[i], "abcdefghijklmnopqrstuvxz") {
            mensagem = MsgConLetra
            return mensagem, false
        }
    }


    mensagem = strings.Join(msgFields, " ")


    return mensagem, true
}


func preparaRabbit(queue string) (*amqp.Connection, *amqp.Channel, amqp.Queue) {
    conn, _ := amqp.Dial("amqp://guest:guest@172.18.0.4:5672")
    ch, _ := conn.Channel()


    q, _ := ch.QueueDeclare(
        queue, //name string,
        true,  // durable bool,
        false, // autodelete
        false, // exclusive
        false, // nowait
        nil)   // args


    ch.QueueBind(
        q.Name,       //name string,
        "",           //key string,
        "amq.fanout", //exchange string
        false,        //noWait bool,
        nil)          //args amqp.Table


    return conn, ch, q
}


//Worker de função para leitura/escrita de fila realtime
func main() {


    var ret *http.Response
    var err error
    //Enquanto não obter conexão não segue
    for {
        ret, err = http.Get(ipMaster + "/workers")
        if err == nil {
            break
        }
        time.Sleep(5 * time.Second)
    }
    qntWorkers, err := ioutil.ReadAll(ret.Body)
    novqnt, _ := strconv.Atoi(string(qntWorkers))
    novqnt += 2112
    if err != nil {
        log.Fatalln(err)
    }


    psqlInfo := fmt.Sprintf("host=%s port=%d user=%s "+
        "password=%s dbname=%s sslmode=disable",
        host, port, user, password, dbname)


    db, err := sql.Open("postgres", psqlInfo)
    if err != nil {
        panic(err)
    }
    defer db.Close()


    err = db.Ping()
    if err != nil {
        panic(err)
    }


    base = db // Depois de conectar e validar conexão, atribui para todos da rotina utilizar.


    go leituraFila(novqnt)
    port := ":" + strconv.Itoa(novqnt)
    println(port)


    http.Handle("/metrics", promhttp.Handler())
    http.ListenAndServe(port, nil)


}

Inicialmente definimos a variável ipMaster definindo o ip default do nosso serviço master que irá retornar ao nosso worker a próxima porta para start do worker. Definimos as constante MsgTamInvalido MsgConLetra como mensagem de resposta para inconsistência na nossa pequena validação de layout da mensagem. Conforme abaixo:

Criamos a função leituraFila que irá receber como parâmetro o numero do worker e ira realizar a leitura da fila recebendo o conteúdo da fila do RabbitMQ e passando para a função mapaMensageria que vai fazer o tratamento do layout e nos retornar o resultado do layout, caso esteja correto será repassado para a função que ira armazenar no PostgreSQL a mensagem do RabbitMQ por completo, do contrario, será repassado a mensagem da inconsistência que neste caso existe apenas 2 — MsgTamInvalido ou MsgConLetra. E por fim ira finalizar a conexão com o RabbitMQ. Conforme abaixo:

A função de insertFilaSQL basicamente ira validar se a tabela filarabbit não existe na base do Postgresql, caso não exista o mesmo cria a tabela e inseri 3 informações:

  • Mensagem retornada pela função de mapaMensageria.
  • Valor lógico da validação de mapaMensageira (verdadeiro ou falso).
  • Horario atual no formato aaaa-mm-dd hh:mm:ss

Conforme abaixo:

Na parte da função mapaMensageria é criado uma função atribuída a f que retorna verdadeiro o falso de acordo com a rune passada como parâmetro esta função é criada para que possa ser utilizada na função FieldsFunc do objeto strings (padrão Go), com isso conseguimos “quebrar” nossa mensagem em uma string array de acordo com o retorno lógico da função passada por parâmetro (que no caso é ponto e vírgula ou dois pontos para verdadeiro), posteriormente fazemos a varredura desse array validando nossas duas regras de layout, uma sendo o tamanho dos bits da mensagem e outra se contem letras através da função ContainsAny do objeto strings, caso o bit esteja em uma dessas regras nossa função ja retorna falso antes mesmo de validar os demais, do contrario irá retornar a mensagem completa para que posteriormente seja inserida em nossa base do postgresql.

Temos a nossa função preparaRabbit mas já abordamos ela anteriormente e por fim temos a função main que irá buscar em nossa master através do path /workers a porta para start do serviço http e abertura de conexão com o postgresql. Paralelizamos a execução da função leiturafila e iniciamos o worker com o path de /metrics para ser utilizado no prometheus. Reparem que enquanto o master não estiver disponível eu não finalizo a execução restante do main, isso é devido a sequencia de containers que vamos subir através do docker-composer e pode ser que um worker inicie primeiro que o master.

Criando os arquivos necessários para o docker.

Primeiramente vamos ter dois dockerfile, um para o master e outro para os workers, no master (main.go) vamos obter a ultima versão do Go, criar um diretório e copiar o conteúdo da nossa aplicação para dentro deste diretório, vamos expor a porta 2112 e utilizaremos um mod feito em Go para download das dependências assim que for utilizado esse dockerfile

Mais detalhes dessa mod: (https://medium.com/@petomalina/using-go-mod-download-to-speed-up-golang-docker-builds-707591336888)

Para o dockerfile dos workers utilizamos o mesmo dockerfile com a exceção de o dockerfile estar dentro da pasta raiz do worker e não necessariamente utilizar o EXPOSE. Por fim vamos criar nosso docker-compose.yml

Temos na parte do postgresql as definições de ambiente para autenticação, executamos alguns comandos do postgresql para que o mesmo permita qual quer conexão ao postgreSQL (inclusive através do pgAdmin). Na parte do master utilizamos o nosos dockerfile criado e informações a porta 2112 além da dependência do postgresql para start. Na parte do RabbitMQ definimos que em caso de falha deve ser iniciado novamente o container sempre, utilizamos a versão de imagem do Rabbit Management para que seja possível utilizarmos o painel do RabbitMQ e por ultimo definimos nossos workers no mesmo padrão que o master:

Com isso vamos executar na raiz do projeto o docker-compose para instanciarmos os containers deixarei no modo “up” para acompanharmos de modo assistido a cada etapa

Após a execução, podemos reparar que os slaves foram iniciados e os demais serviços:

Vamos realizar um teste enviando uma mensagem através do Postman para o master e avaliar todo o fluxo:

Enviamos uma mensagem onde todo o layout esta correto, como criamos apenas 2 regras (deve conter 4 dígitos entre os bits e não possui letra) o mesmo deverá processar a mensagem por completo para o PostgreSQL:

Observem que foi aberto uma conexão e autenticada pelo usuário guest e worker denominado slave 1 recebeu e processou a mensagem, posteriormente fechou a conexão. Vamos agora validar no PostgreSQL:

Temos a mensagem completo (visto que não houve erros de layout) o retorno True e a datahora do processamento. Vamos realizar uma nova chamada mas passando algumas partes da mensagem com letra:

Com isso validamos o fluxo e objetivo da aplicação, agora vamos partir para as métricas junto ao prometheus.

Configurando o prometheus.yml e visualizando as métricas

Após feito o download do prometheus, no arquivo prometheus.yml vamos configurar nossos jobs que contem as configurações de onde estão as métricas para alimentar o prometheus.

# my global config
global:
  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
  # scrape_timeout is set to the global default (10s).
# Alertmanager configuration
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      # - alertmanager:9093
# Load rules once and periodically evaluate them according to the global 'evaluation_interval'.
rule_files:
  # - "first_rules.yml"
  # - "second_rules.yml"
# A scrape configuration containing exactly one endpoint to scrape:
# Here it's Prometheus itself.
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: 'master'
    metrics_path: /metrics
    static_configs:
    - targets: ['localhost:2112']
  - job_name: 'slave1'
    metrics_path: /metrics
    static_configs:
    - targets: ['localhost:2113']
  - job_name: 'slave2'
    metrics_path: /metrics
    static_configs:
    - targets: ['localhost:2114']

Basicamente o trecho que vamos modificar é o bloco do scrape_configs onde informares o ip do master e dos nossos slaves conforme acima.

Agora podemos iniciar nosso Prometheus, caso tenha iniciando sem erros ao acessar a url: localhost:9090 você terá uma pagina igual a imagem abaixo:

Nesta tela inicial, podemos executar comandos PromQL a partir das métricas disponíveis dos nossos “receptores” que configuramos no arquivo yml. Na aba Status->Targets poderemos ver todos os serviços e seu status (up or down)

Em nosso caso temos o master e os 2 slaves:

Voltando para a parte de alerts, vamos obter algumas métricas e visualizarmos o que o prometheus já tem a oferecer. Vamos informar a métrica process_cpu_seconds_total para visualizarmos de forma temporal a evolução do consumo de cpu em modo gráfico:

Como configuramos para a captura das informações acontecer de 15 em 15 milissegundos no arquivo yml, podemos ver que houve um crescimento no consumo de ambos os serviços justamente pela utilização dos mesmos no primeiro start do Prometheus chamando o path de métricas. O pacote do prometheus Go contem por padrão algumas métricas pertinentes a aplicações Go onde temos 2 coletores de informações:

  • Process Collector — que coleta informações básicas do host, como CPU, memória, uso do descritor de arquivo e horário de início.
  • Go Collector — que coleta informações sobre o tempo de execução da aplicação em Go, como detalhes sobre GC, número de gouroutines e threads do SO.

Um exemplo de métrica padrão do pacote é go_memstats_alloc_bytes que nos retorna a quantidade de bytes de memória que está alocada no heap para os objetos

Quantidade de Go Routines de forma temporal:

Requisições em massa

Vamos agora criar um pequeno programa para enviar diversas requisições ao nosso master com mensagens randômicas no layout e acompanhar no Prometheus através das métricas.

Criei um script onde recebemos um parâmetro de chamada direta como valor de total de requisições a serem enviadas, obtenho o horário atual para comparativo de tempo total de execução e crio uma string randômica com tamanhos de 0 a 9999 para obter todos os cenários:

Realizando a chamada conforme acima, estou submetendo o teste para envio de 10 mil requisições.

Por fim temos 1.5minutos para envio de 10 mil mensagens, vamos no PostgreSQL se foi gravado os 10 mil registros:

Agora vamos utilizar o prometheus e verificar o numero de goroutines que foram abertas, total de cpu e memória no teste realizado.

Tivemos um pico apenas no master na abertura de go routines onde muito provavelmente se trata da abertura de conexão tanto do RabbitMQ como do PostgreSQL, reparem que no inicio esse quantitativo foi equivalente quando iniciamos nossos containers, ja nos slaves não tivemos grandes variações.

Ja na parte de alocação de memória reparem que os slaves tiveram mais consumo de memória do que o master onde os slaves tem o trabalho de fazer a validação na mensagem para o layout de acordo com as regras e gravar na base.

Nossa métrica customizada acima, onde mostra o numero de requisições feitas no path /workers.

Em resumo o prometheus nos permite realizar diversas filtragens simples e avançadas abaixo deixo alguns links principais da documentação oficial do prometheus para trabalhar com operadores matemáticos e querys para uma filtragem mais avançada:

  1. https://prometheus.io/docs/prometheus/latest/querying/examples/
  2. https://prometheus.io/docs/prometheus/latest/querying/operators/
  3. https://prometheus.io/docs/prometheus/latest/querying/functions/

Portanto o intuito aqui foi demonstrar um pouco de cada stack em um cenário simples e pratico e como podemos utilizar o prometheus para monitorar todo o ambiente e aplicações de acordo com sua evolução, bem como reforçar o aprendizado de micro serviço, Go e Docker. Acredito que utilizando o conceito de gRpc ao invés de APIs e utilizar o zeroMQ ao invés do RabbitMQ poderíamos ter uma melhor performance, no entanto existe os pós e contras de cada tecnologia, principalmente na parte do RabbitMQ no qual aqui foi abordado um esquema de mensageria simples sem utilizar os tipos de Exchange mais específicos e benéficos do rabbitmq. Outro ponto não abordado aqui é a integração do prometheus com Grafana que é bem simples (passo a passo).

Esperto que o artigo tenha contribuído em seu conhecimento!

Link do projeto Github

https://github.com/AirtonLira/Microservice-Go-Prometheus