Pesquisar este blog

segunda-feira, 28 de setembro de 2020

Criando um cluster de Apache Spark com o Docker Compose

Este post segue uma sequência de explicações a respeito dos containers, se você não sabe o que são containers, comece por aqui, onde são explicados como eles funcionam e de onde eles surgiram. 

Temos também, neste post, um exemplo de como instalar um banco de dados usando o Docker.

Aqui será mostrado como criar um cluster com o Docker, algo que poderá ser feito em uma máquina comum, na sua casa, se você não sabe o que é um cluster, veja aqui a explicação.

Introdução

Antes da versão 1.12, para criar uma cluster com o Docker, era necessário utilizar um serviço de armazenamento de chaves externo, mas isso mudou  com o aparecimento do Swarm. O Swarm é uma forma nativa de configurar um cluster com containers do Docker, com ele você pode tanto criar clusters em sua máquina local, como em plataformas de cloud.

Além do swarm, existem outras formas de criarmos clusters com o docker, dentre elas podemos citar o Apache Mesos, OpenShift e o Kubernetes.

A criação de clusters com containers permite a criação de diversos tipos de ambientes, incluindo banco de dados poderosos, espalhados em muitos containers, um serviço web que escalona rapidamente, conforme sua necessidade. Os containers possuem uma versatilidade muito interessante, sendo possível até, criar um cluster usando dispositivos RaspberryPi.

Mas o que é o Spark?

Histórico:

O Apache Spark é um cluster de processamento de dados em paralelo, tudo começou com o algoritmo de Map (mapeamento)/Reduce (computação dos resultados) desenvolvido para processar os dados de indexação das páginas HTML que seriam mostradas no Google, e com isso nasceu o famoso termo BigData, que remete ao processamento de um grande volume de dados. Conforme as empresas vão evoluindo seus produtos, mais e mais é necessário produzir relatórios mais complexos, os quais necessitam ligações e cruzamento dos dados e com um volume cada vez maior. 

A concorrência, pelo acesso aos dados no banco RDBMS fica cada vez maior e com isso, conforme demonstrado na explicação sobre travas em banco de dados RDBMS o acesso aos dados fica cada vez mais lento, e outros sistemas precisaram ser desenvolvidos para dar conta destes processos.

O primeiro sistema que apareceu foi o Apache Hadoop, que rapidamente ganhou populadidade, por ser um dos primeiros sistemas de cluster a permitir o processamento de dados em paralelo, de maneira open source. Ao redor dele, foram criados diversos softwares que permitiram acessar queries SQL em paralelo, como o Hive, um sistema de arquivos, o HDFS, um sistema de algoritmos de Machine Learning, como o Mahout

Todos estes sistemas criaram um ecosistema ao redor do Hadoop, e isso fez com que ele fosse usado em muitos lugares, mas uma das suas características era ler e escrever os dados no sistema de arquivos, toda vez que eles seriam processados e com o tempo, isso acabou ficando custoso.

O Spark, surgiu como uma ideia de acelerar este processamento de dados, e para isso ele, ao invés de escrever os dados no hd, resolveram fazer com que ele armazenasse os dados na memória. Isso fez com que o Spark fosse até 100 vezes mais rápido que o Hadoop, e isso atraiu muita atenção para este projeto. Em 2015, ele era o projeto opensource que mais se desenvolvia no mundo.

Ele foi escrito utilizando a linguagem Scala, que roda dentro da JVM, com isso ele já teria compatibilidade tanto com o Java, quanto com o próprio Scala. Dentro do Spark, existe uma implementação de DSL do SQL, chamado SparkSQL, que permite acessar os dados dos seus Dataframes usando SQL. Depois foram adicionados processadores de Python e R. o que trouxe uma flexibilidade muito boa para este projeto, pois ele abrange linguagens bem populares. O Java, o Python, o Scala e o R, que apesar de não ser muito conhecido pela maioria dos técnicos ela é bem conhecida na área de estatística. Atualmente estão sendo desenvolvidos implementações para o Go e o Kotlin, mostrando que a evolução da plataforma continua.

Esta flexibilidade também torna possível escrever programas em diversas linguagens, aproveitando o ponto forte e as bibliotecas de cada uma delas. R é muito conhecido por ter muita coisa de estatística implementada como biblioteca, assim caso, o trabalho a ser desenvolvido, precise de uma biblioteca bem específica, pode ser mais fácil aproveitar algo que já está implementado no R, do que desenvolver uma lib do zero.

O Spark é subdividido em Core, que possui as funcionalidades básicas, o SparkSQL, que implementa o SQL ansi nesta plataforma, a MLLib, que é a biblioteca de Machine Learning, o Stream, que é uma implementação de leitura de dados em Streaming.

Arquitetura:

O Spark contem, duas partes principais, o Driver e os executores e cada um deles possui um papel diferente nesta arquitetura. 

Ao executar o Spark, cria-se um contexto de execução e este contexto fica alocado no Driver. O Driver é a máquina que recebe os comandos e os resultados das computações realizadas no Spark. Cada comando recebido será distribuído para serem executados pelos executores.

Toda vez que algum dado é carregado no Spark, os dados são distribuídos entre os executores. Todos os dados são armazenados em três tipos de distribuição, os Datasets, os Dataframes e os RDDs e cada uma delas possui uma abstração diferente.

Os RDDs são datasets resilientes e distribuídos, mas o que significa isso? Imagine que você carregue uma coleção com 5.000 linhas, ao fazer isso, estes dados serão distribuídos em partições, e cada uma destas partições conterá um pedaço dos dados, por exemplo, se o luster tiver 100 partições, idealmente cada partição conterá 500 linhas daquela coleção. Nem sempre é isso que acontece, mas quanto melhor distribuídos forem os dados, mais rápido eles serão processados, pois cada uma destas partições irá executar as operaçõe em um Executor diferente.

Características 

Todas as operações são realizadas de forma lazy, desta forma, ele consegue otimizar bastante a execução das operações. Imagine que você pegue aquela mesma coleção de 5000 linhas, do exemplo anterior, e executa 4 filtros diferentes. Mesmo que você coloque estas operações em execuções diferentes e imagine que ele terá que percorrer a coleção 4 vezes, para executar os 4 filtros, não é isso que acontece, ele irá concentrar todas as operações e executá-las todas de uma vez, somente quando uma operação final for realizada. Operações finais são aquelas que exigem um retorno dos dados, ou uma extração de dados como resultado. Operações de count, collect, write e take são exemplos de destes tipos de operaçoes.

Claro que este foi um exemplo simples de otimização, mas estas otimizações acontecem em diversos níveis diferentes e, as vezes, até é possível eliminar algumas operações redundantes.

Performance:

Performance é uma das grandes do Spark, quando ele surgiu a propaganda era que ele rodava até 100x mais rápido que o Hadoop, sistema usado em plataformas de bigdata na época. Isso é fruto destas otimizações e também pois, com ele, é possível guardar snapshots (fotografia do estado das coleções) na memória do cluster, e isso faz com que o acesso aos dados seja extremamente rápido.

Os dados de cada partição ficam replicados em pelo menos três executores diferentes, para que caso um deles trave, ou tenha algum problema, o dado não seja perdido.

Existem uma série de outros fatores, que influenciaram na performance do Spark, e isso evolui a cada dia, lá em 2013 o Spark era o projeto opensource que mais rápido evoluia no mundo, isso trouxe muitas novidades.

Suporte a linguagens:

Spark foi desenvolvido em Scala, e por isso esta, assim como o Java, eram as linguagens suportadas na sua primeira versão. Depois vieram as implementações de SQL, que é uma implementação própria do Spark, do Python, do R (linguagem estatística, estas duas últimas surgiram pois haviam muitas bibliotecas de Machine Learning disponíveis nelas, .Net ganhou suporte no ano passado e o suporte ao Kotlin está atualmente em desenvolvimento.

Flexibilidade:

Além do suporte a múltiplas linguagens, uma outra característica do Spark é rodar em múltiplos ambientes.

Desde a sua versão inicial o Spark usa como sistema de arquivos o HDFS, que é padrão nos sistemas Hadoop, e ele sempre foi compatível com as ferramentas da stack do Hadoop, isso abriu muitas portas, para fazer com que o Spark fosse escolhido como engine de execução em sistemas de bigdata. Hoje ele é suportado por todas as clouds, além do Mesos, Yarn e Kubernetes, que são administradores de recursos.

Caso de uso:

Neste post faremos um exemplo de um cluster spark, contendo um notebook, um driver e 2 executores, para isso vamos usar a composição de imagens Docker, começando pela criação de uma imagem básica, e o primeiro passo será criar uma imagem básica para o cluster, ela foi criada a partir do github do próprio Spark, para que assim, garantíssemos que tanto o sistema operacional, quanto a instalação do Java, estavam na versão correta, para fazer  isso criarmos o seguinte conteúdo num arquivo cluster-base:

ARG debian_buster_image_tag=8-jre-slim
FROM openjdk:${debian_buster_image_tag}

# -- Layer: OS + Python 3.7

ARG shared_workspace=/opt/workspace

RUN mkdir -p ${shared_workspace} && \
    apt-get update -y && \
    apt-get install -y python3 && \
    ln -s /usr/bin/python3 /usr/bin/python && \
    rm -rf /var/lib/apt/lists/*

ENV SHARED_WORKSPACE=${shared_workspace}

# -- Runtime

VOLUME ${shared_workspace}
CMD ["bash"]

O próximo passo será definir uma imagem base para o spark, nela iremos definir tanto a versão do spark, como a versão do python, assim como outras variáveis de ambiente, que são usadas na execução. Uma nota é que aqui o Driver é chamado de master, nomenclatura que tem sido abolida devido ao peso de preconceito da época da escravidão.

FROM cluster-base

# -- Layer: Apache Spark

ARG spark_version=3.0.1
ARG hadoop_version=2.7

RUN apt-get update -y && \
    apt-get install -y curl && \
    curl https://archive.apache.org/dist/spark/spark-${spark_version}/spark-${spark_version}-bin-hadoop${hadoop_version}.tgz -o spark.tgz && \
    tar -xf spark.tgz && \
    mv spark-${spark_version}-bin-hadoop${hadoop_version} /usr/bin/ && \
    mkdir /usr/bin/spark-${spark_version}-bin-hadoop${hadoop_version}/logs && \
    rm spark.tgz

ENV SPARK_HOME /usr/bin/spark-${spark_version}-bin-hadoop${hadoop_version}
ENV SPARK_MASTER_HOST spark-master
ENV SPARK_MASTER_PORT 7077
ENV PYSPARK_PYTHON python3

# -- Runtime

WORKDIR ${SPARK_HOME}

  • SPARK_HOME é uma variável de ambiente que define o diretório onde o Spark estará instalado
  • SPARK_MASTER_HOST é o local onde estará instalado o Driver do Spark.
  • SPARK_MASTER_PORT é a porta onde o Diver irá rodar
  • PYSPARK_PYTHON é a versão do python que será usada dentro o Spark.

Repare que a versão usada pelo Spark, é diferente da versão usada na imagem base, mas elas não tem relação entre si, esta é a versão que será usada dentro do Spark.

Nos próximos passos serão criadas as imagens do Driver e dos Executores do spark, as quais serão colocadas, respectivamente, nos seguintes arquivos spark-master e spark-executor, sendo o arquivo do driver:

FROM spark-base

# -- Runtime

ARG spark_master_web_ui=8080

EXPOSE ${spark_master_web_ui} ${SPARK_MASTER_PORT}
CMD bin/spark-class org.apache.spark.deploy.master.Master >> logs/spark-master.out

E o arquivo do executor:

FROM spark-base

# -- Runtime

ARG spark_worker_web_ui=8081

EXPOSE ${spark_worker_web_ui}
CMD bin/spark-class org.apache.spark.deploy.worker.Worker spark://${SPARK_MASTER_HOST}:${SPARK_MASTER_PORT} >> logs/spark-worker.out

Perceba que as configurações realizadas aqui são mínimas, web-ui é uma interface que o spark prove para debugarmos a execução dos seus jobs, e nesta configuração setamos a porta 8081 para cada uma delas.

Tirando isso a outra configuração é simplesmente o software que será executado, além da localização do log de saída da sua execução. 

Agora a última imagem que está faltando é a imagem do notebook, nela será instalado o jupyter e configurada a sua execução:

FROM cluster-base

# -- Layer: JupyterLab

ARG spark_version=3.0.1
ARG jupyterlab_version=2.1.5

RUN apt-get update -y && \
    apt-get install -y python3-pip && \
    pip3 install pyspark==${spark_version} jupyterlab==${jupyterlab_version} wget

# -- Runtime

EXPOSE 8888
WORKDIR ${SHARED_WORKSPACE}
CMD jupyter lab --ip=0.0.0.0 --port=8888 --no-browser --allow-root --NotebookApp.token=

Repare que as versões tanto do spark, quanto do jupyter estão como argumentos desta imagem, pois ela precisa instalar o pyspark e o jupyter.

Como último passo, falta colocar esta estrutura pra rodar, e aqui usaremos o docker-compose, que foi feito para criar clusters dentro de uma mesma rede, ou máquina.

version: "3.6"
volumes:
  shared-workspace:
    name: "hadoop-distributed-file-system"
    driver: local
services:
  jupyterlab:
    image: spark-jupyter
    container_name: spark-jupyter
    ports:
      - 8888:8888
    volumes:
      - shared-workspace:/opt/workspace
  spark-master:
    image: spark-master
    container_name: spark-master
    ports:
      - 8080:8080
      - 7077:7077
    volumes:
      - shared-workspace:/opt/workspace
  spark-worker-1:
    image: spark-executor
    container_name: spark-executor-1
    environment:
      - SPARK_WORKER_CORES=1
      - SPARK_WORKER_MEMORY=512m
    ports:
      - 8081:8081
    volumes:
      - shared-workspace:/opt/workspace
    depends_on:
      - spark-master
  spark-worker-2:
    image: spark-executor
    container_name: spark-executor-2
    environment:
      - SPARK_WORKER_CORES=1
      - SPARK_WORKER_MEMORY=512m
    ports:
      - 8082:8081
    volumes:
      - shared-workspace:/opt/workspace
    depends_on:
      - spark-master

Este conteúdo vai dentro de um arquivos chamado docker-compose.yml, que será usado para executar todas as imagens, mas antes disso, precisamos construir cada uma das imagens. Veja que cada executor vai usar apenas um core e 512m de memória, esta é uma configuração bem básica, mas ela pode ser alterada caso você tenha uma máquina poderosa.

Construção das imagens:

A construção das imagens é feita através de um comando de build do docker e como as imagens possuem dependência entre si, deveremos executar o build nesta sequencia:

SPARK_VERSION="3.0.1
HADOOP_VERSION="2.7"
JUPYTERLAB_VERSION="2.1.5"

docker build -f cluster-base -t cluster-base .

docker build --build-arg spark_version="${SPARK_VERSION}" --build-arg hadoop_version="${HADOOP_VERSION}" -f spark-base -t spark-base .

docker build -f spark-master -t spark-master .

docker build -f spark-worker -t spark-worker .

docker build --build-arg spark_version="${SPARK_VERSION}" --build-arg jupyterlab_version="${JUPYTERLAB_VERSION}" -f spark-jupyter -t spark-jupyter .

Repare que as versões usadas aqui neste cluster, são todas definidas em variáveis de ambiente nos primeiros comandos e elas são usadas para construir a imagem base do spark. Assim caso você esteja aqui no futuro e a versão mais recente do spark seja outra, você só precisa mudar este comando que suas imagens estarão atualizadas. Para saber qual é a última versão do Spark acesse a página deles.

Execução:

Assim que construídas, chegou a hora de executarmos as imagens e para fazer isso é só executar o comando:

docker-compose up

Se ele executar com sucesso, algumas mensagens de log serão impressas no seu console, sendo que as últimas serão:

spark-executor-2  | 20/09/28 19:04:10 INFO Worker: Successfully registered with master spark://spark-master:7077

Isso será mostrado para cada uma das instâncias do nosso cluster, mas para verificar se todas elas subiram corretamente, vamos acessá-las usando os seguintes endereços:

  • JupyterLab at localhost:8888;
  • Spark master at localhost:8080;
  • Spark worker I at localhost:8081;
  • Spark worker II at localhost:8082;
O jupyterlab é onde executaremos os comandos no cluster, os endereços do Driver e dos executores mostrarão as interfaces de debug do Spark.

 Mão na massa:

Agora chegou a parte mais legal do artigo, que é quando vamos para a parte prática, para acessar o cluster vamos criar um notebook no Jupyter e executar alguns comandos. Para acessar um cluster Spark, precisamos criar um contexto do Spark e isso será feito a partir dos seguintes comandos:

 


 No primeiro comando é onde criamos a conexão com o Spark, para isso dois fatores serão definidos, o nome do applicativo (exemplo-pyspark) e o endereço do master, porta 7077. Repare que as setas vermelhas apontam para um arquivo, que foi baixado, através do comando wget. Este arquivo que foi usado como exemplo de coluna de dados.

Veja que aqui ainda não fizemos nada de muito interessante, mas muita coisa pode ser feita com este servidor para processar dados em paralelo.

Apresentação

 

segunda-feira, 21 de setembro de 2020

Introdução prática ao Docker Swarm

Introdução:

Antes de iniciar este exemplo prático, é necessário que você tenha o docker instalado na sua máquina. Docker é um software de containers, já o swarm é uma de suas ferramentas de clusterização.

Para um teste completo do material apresentado aqui, seria necessário termos diversas máquinas, isso pode ser feito usando máquinas virtuais, mas nem todos possuem disponível máquinas capazes de rodar mais de um sistema operacional ao mesmo tempo, portanto vamos focar apenas no exemplo de swarm rodando em apenas uma máquina.

Aqui vamos construir um cluster docker swarm, que servirá como guia de como fazer isso com sua aplicação. Por isso, o primeiro passo será construir as imagens do Docker, necessárias para rodar nosso teste.

Para aproveitar bem o material apresentado aqui, é importante que você tenha conhecimento do que são e quais são as ferramentas de container, o que já foi mostrado anteriormente aqui mesmo no blog.

Docker Swarm

O Swarm já faz parte da instalação do Docker, então nada mais é necessário para acompanhar este tutorial, além de uma instalação básica do Docker.

Para testar se está tudo ok, acesse o terminal da sua máquina e digite:

$ docker swarm init
Swarm initialized: current node (yj7woptvnye02ad0cckypiozw) is now a manager.
To add a worker to this swarm, run the following command:

    docker swarm join --token SWMTKN-1-4w6b5ebqb8bgydglgtrc1kwqbq33ady86bbvz2qxjd7dqovgga-5ts220sngpt4ptzssux7upp2z 192.168.1.45:2377

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

Pronto, se a mensagem de inicialização aparecer como mostrado acima, está tudo certo e podemos continuar. Se o docker não estiver instalado na sua máquina, procure um tutorial de como fazer isso na sua máquina, é um processo bem simples.

Para o Swarm, computadores serão chamados de nós, e estes nós podem ter dois papéis em um cluster swarm:

  1. Manager node - são os nós administratores, onde temos disponíveis os comandos de administração e controle do Swarm
  2. Worker node -  que são os nós onde o seu software será realmente executado, por exemplo se estivermos falando de um webservice, é aqui que vai rodar o seu servidor web.
Pela configuração padrão, os nós do Swarm são configurados tanto como managers, quanto como workers, com isso ja temos um local disponível para adicionarmos um nó de teste.

Construção das imagens:

Neste primeiro teste, vamos criar uma imagem simples com os seguintes arquivos:

$ ls -al

├── Dockerfile
├── docker-compose.yml
├── node_modules (diretório)
├── package.json
├── package-lock.json
├── server.js 

Conteúdo dos arquivos:

Dockerfile é o arquivo que contem as instruções de build da nossa imagem, nela definiremos quais são as dependencias, qual imagem será usada como base, quais comandos serão executados ao executar a nossa imagem.

docker-compose.yml é onde definiremos os serviços que estarão em nossa aplicação, aqui teremos a definição de qual serviço vai rodar e em qual porta da imagem, isso acontecerá. 

package-(lock).json estes dois arquivos json são dependencias do npm, que é a ferramenta que estamos usando aqui para constuir e executar nosso serviço. Package.json contem dependencias e metadados do nosso projeto, package-lock.json é um arquivo que tem travadas em versões específicas que não podem ser alteradas.

Os arquivos package.json e package-lock.json serão criados durante a inicialização do nosso container.

$ docker run --rm -v $(pwd):/home/node -w /home/node node:11.1.0-alpine npm init -y
$ docker run --rm -v $(pwd):/home/node -w /home/node node:11.1.0-alpine npm i -S express

O primeiro comando irá criar o arquivo package.json com os valores padrão e o segundo irá baixar e instalar a imagem express, assim como suas dependencias, no arquivos package.json.

Agora vamos criar os conteúdo do arquivos server.js:

const express = require("express");
const os = require("os");

const app=express();

app.get("/", (req, res) => {
    res.send("Olá do Swarm " + os.hostname());
});

app.listen(3000, () => {
    console.log("O servidor está rodando na porta 3000");
});

Este pequeno pedaço de código, irá iniciar um servidor Express e irá mostrar uma mensagem que contem o identificador do container, o que é feito pelo os.hostname. Estranho não, o comando que chama hostname, retorna o id do container? Bom isso acontece no Docker para ser possível identificar de qual conteiner veio aquela mensagem, e é exatamente isso que queremos aqui. Quando tivermos mais de uma instancia rodando, eu gostaria de saber de onde veio aquela mensagem.

Agora ainda falta criar o Dockerfile, que é onde colocamos as instruções necessárias para criar nossas imagens do docker. Nela vamos pegar uma imagem do Node, e copiar nossos arquivos package.json, package-lock.json e server.js, e fazer ela instalar as dependencias, além de especificar o comando npm start como comando padrão de inicialização.

FROM node:11.1.0-alpine

WORKDIR /home/node

COPY . .

RUN npm install

CMD npm start

Repare que o comando Copy, está copiando os arquivos do diretório corrente, apra o diretório da imagem, para que este comando funcione, você deve rodá-lo obrigatoriamente, no diretório onde foram criados os arquivos deste post.

Como último passo, criaremos o arquivos docker-compose, que é quem contem o comportamento de build e de execução do nosso projeto:

version: '3'

services:
    web:
        build: .
        image: dirceuprofessor/exemplo-swarm:1.0
        ports:
            - 80:3000
        networks:
            - mynet

networks:
    mynet:

Aqui definimos um roteamento da porta http 80 para a 3000, ou seja, nosso servidor express, que estará rodando na porta 3000 da imagem, será exposto para fora, pela porta 80. Um outro ponto importante aqui, foi a definição da rede padrão desta imagem.

A construiçao desta imagem é feita pelo comando:

$ docker-compose build
Building web
Step 1/5 : FROM node:11.1.0-alpine
---> 4b3c025f5508
Step 2/5 : WORKDIR /home/node
---> Running in 5aff5c6ce487
Removing intermediate container 5aff5c6ce487
---> 41d6018e73d8
Step 3/5 : COPY . .
---> e839ab4d7f74
Step 4/5 : RUN npm install
---> Running in fa1fd458b20e
npm WARN node@1.0.0 No description
npm WARN node@1.0.0 No repository field.

audited 50 packages in 0.819s
found 0 vulnerabilities

Removing intermediate container fa1fd458b20e
---> 64607c63cf41
Step 5/5 : CMD npm start
---> Running in e7bc55781581
Removing intermediate container e7bc55781581
---> 2c8c0cd30ba7
Successfully built 2c8c0cd30ba7
Successfully tagged dirceuprofessor/exemplo-swarm:1.0

Para subir o serviço agora vamos usar o Swarm, note que usamos o docker-compose para fazer o build da nossa imagem, e isso aconteceu pois o Swarm não faz builds. O compose também é usado para criar imagens, porém apenas imagens que rodem localmente, o Swarm adiciona a possibilidade das imagens criadas, rodarem em várias máquinas diferentes, e com isso abre a possibilidade de construirmos um cluster.

$ docker stack deploy exemplo_swarm -c docker-compose.yml
Ignoring unsupported options: build

Creating service exemplo_swarm_web

Docker stack é o comando responsável por administrar as imagens do Swarm, e o comando deploy é responsável por colocar em execução uma imagem, aqui passamos o argumento -c que é para usar o arquivo de compose para isso. A última parte do comando é o nome que terá o nosso container, exemplo_swarm.

Repare também que a primeira mensagem que ele mostra é:

Ignoring unsupported options: build
Isso acontece pois temos comando de build no arquivo de compose, mas o Swarm, como dito anteriormente, não faz os builds, então aqui ele ignora esses comandos.

A partir de agora, você já pode abrir o seu browser e testar se o serviço está funcionando, para isso basta acessar localhost.


 

A resposta será exibida on browser e conforme foi mostrado antes, a gente está mostrando o id do componente onde ele está rodando.

Repare que, ao listar as instancias dockers da minha máquina, eu obtenho o mesmo id que foi mostrado lá pelo browser, como era esperado.

Agora chegou a hora de criarmos um cluster com o docker, assim a gente pode scalar e criar quantas instâncias a gente quiser, no nosso caso estaremos testando com 4, para fazer isso basta executar o comando:

$ docker service scale exemplo_swarm_web=4
exemplo_swarm_web scaled to 4
overall progress: 4 out of 4 tasks
1/4: running   
2/4: running   
3/4: running   
4/4: running   
verify: Service converged

As 4 instâncias vão mudando de status, até chegar ao ponto onde todas estão executando, assim que todas estiverem rodando, ele fará uma checagem do status das aplicações e depois, se tudo estiver correto, ele mostra a mensagem de Serviço convergiu.

Para checar se todos as instâncias estão rodando, usamos o seguinte comando:

$ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
nuffnzzogy9y exemplo_swarm_web replicated 4/4 dirceuprofessor/exemplo-swarm:1.0 *:80->3000/tcp

Vejam que, com este comando, ele mostra metadados do nosso serviço, como id, nome, modo, quantidade de réplicas, a imagem que está sendo usada inclusive as portas que foram roteadas na nossa rede.

Também podemos listar todas as instancias e os status de cada uma delas, isso é feito através deste comando:

$ docker service ps exemplo_swarm_web
ID                  NAME                  IMAGE                               NODE                 DESIRED STATE       CURRENT STATE            ERROR               PORTS
wynr0t1zymdl        exemplo_swarm_web.1   dirceuprofessor/exemplo-swarm:1.0   noteeleflow-dirceu   Running             Running 11 minutes ago                       
ibel5ffapl9m        exemplo_swarm_web.2   dirceuprofessor/exemplo-swarm:1.0   noteeleflow-dirceu   Running             Running 9 minutes ago                        
mj3gwxoeqwy6        exemplo_swarm_web.3   dirceuprofessor/exemplo-swarm:1.0   noteeleflow-dirceu   Running             Running 9 minutes ago                        
rwh5m1yd1jo5        exemplo_swarm_web.4   dirceuprofessor/exemplo-swarm:1.0   noteeleflow-dirceu   Running             Running 9 minutes ago

 Agora, se você pegar o seu navegador, e acessar localhost algumas vezes, ele irá mostrar ids diferentes da imagem, isso não é muito determinístico, mas atualizando a página você verá que os ids uma hora irão mudar.

O interessante é pensar no motivo deles mudarem, por que será que isso acontece? Bom o swarm possui um load balancer interno e hora ou outra ele repassa a chamada para um outro container.

Vejam que este teste foi feito em apenas uma máquina, mas isso poderia ser facilmente replicado em mais de uma máquina e você conseguiria construir um cluster de maneira muito fácil.

Apresentação

 

 
 

domingo, 13 de setembro de 2020

Como instalar e usar, MariaDb com o Docker

 Neste post será mostrado como instalar e usar o MariaDb com o Docker, se você ainda não sabe o que é o Docker, aqui temos um post que explica como ele funciona.

Introdução:

Antes de começar, certifique-se de ter o docker instalado na sua máquina, para fazer isso:

No windows:

Abra o CMD e use um dos seguintes comandos:

docker --version

ou

docker-compose --version

ou

docker ps

Se você não tiver nenhum erro ao executar algum destes comandos e a verrsão for mostrada na tela, pronto o docker já está instalado na sua máquina.

No Linux e Mac:

Abra um terminal e rode o seguinte comando:

docker --version

Caso nenhum erro aconteça e a versão do docker for mostrada no terminal, pronto está tudo certo e você pode continuar.

Instalando uma imagem do Mariadb:

O processo de instalação com o docker é bem simples, para fazer isso basta achar uma imagem do que você deseja instalar, e no caso do Mariadb, existem diversas imagens disponíveis gratuitamente, aqui vamos usar a versão oficial, para fazer isso basta executar o seguinte comando no CMD/Terminal:

docker pull mariadb/server

Com este comando ele irá baixar a versão mais recente do servidor do mariadb, como resposta a ele temos a seguinte saída:

$ docker pull mariadb/server
Using default tag: latest
latest: Pulling from mariadb/server
7595c8c21622: Pull complete
d13af8ca898f: Pull complete
70799171ddba: Pull complete
b6c12202c5ef: Pull complete
69489d4b50c9: Pull complete
9f96a63e93c1: Pull complete
25c7f3eb141b: Pull complete
c41dc96a05b4: Pull complete
0b1165703f02: Pull complete
46c2bcef0880: Pull complete
3a085a383697: Pull complete
aee546b35dc6: Pull complete
f1d8894f6fb0: Pull complete
455851a7a727: Pull complete
Digest: sha256:e40e539ec4dbf9a0c84eb3725828682589f24763840f61e504a1dc60bbbdc3be
Status: Downloaded newer image for mariadb/server:latest
docker.io/mariadb/server:latest

Estes códigos alfanuméricos que aparecem, são referentes as camadas que o docker tem que baixar, ao final ele mostra no Status, que uma imagem nova foi baixada.

Para se certificar que a imagem foi corretamente baixada, execute o comando de listagem das imagens:

$ docker images
REPOSITORY                                      TAG                            IMAGE ID            CREATED             SIZE
mariadb/server                                  latest                         e59408954028        3 weeks ago         358MB

Neste caso a imagem baixada tem 358Mb de tamanho e ela foi criada há semanas atrás.

 Uma imagem, ainda não é um software rodando, é apenas um software executável, que foi baixado na nossa máquina, para rodá-lo, será necessário criar um container, pra fazer isso executamos o seguinte comando:

docker run --name mariadbtest -e MYSQL_ROOT_PASSWORD=senha_teste -d mariadb/server

Atente pras partes do comando que serão o nome da imagem, e a senha usada para acessar. Caso você não queira dar um nome, um id será gerado pelo docker.

Para se certificar que a sua imagem está rodando, utilize o seguinte comando:

 $ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
991e141f5dfd        mariadb/server      "docker-entrypoint.s…"   5 minutes ago       Up 5 minutes        3306/tcp            mariadbtest

Veja que aqui temos todas as informações, da imagem, incluindo a porta que ela está usando, e também há quanto tempo ela está rodando.

Conectando ao banco de dados

Agora a primeira coisa que nos vem a cabeça é tentar executar uma conexão ao localhost, já que o banco está rodando. Porém isso não funciona, uma vez que este banco instalado, está rodando em um container e o seu sistema de arquivos estará isolado do Sistema Operacional corrente, para acessar o sistema de arquivos da sua imagem, utilize o comando a seguir:

 $ docker exec -it mariadbtest bash

 A partir deste momento, você estará acessando o terminal da sua imagem, e nele é possível executar e fazer alterações de configuração no seu servidor. Na imagem que está sendo usada neste post, nenhuma configuração é necessária para acessar o banco, pois este container já vem com uma interface de rede, e o banco estará liberado para receber conexões através dela, agora será necessário descobrir qual é o ip deste container, isso é feito através do comando:

$ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mariadbtest
172.17.0.2

Sabendo o ip do container, agora basta conectar usando a senha que foi usada na execução da imagem:

 $ mysql -h 172.17.0.2 -u root -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 11
Server version: 5.5.5-10.4.14-MariaDB-1:10.4.14+maria~bionic mariadb.org binary distribution

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

Depois de digitar a senha, você entra no console do banco de dados e isso significa que tudo foi executado corretamente.

sábado, 5 de setembro de 2020

Banco de dados e suas "Chaves"

O título deste post começa com uma brincadeira, com chaves não queria dizer dos relacionamentos e chaves primárias coisas que banco de dados SQL fazem muito bem, aqui vamos conversar a respeito das travas (trancas ou cadeados) que estes bancos possuem para lhe dar com o paralelismo das transações.

Banco de dados Relacionais RDBMS (Relationship Database Management System) são bancos consistentes e este é um fator importante em como ele trabalha com os dados.

Introdução:

Aqui no blog, existem alguns posts falando sobre banco de dados SQL, como este que mostra como controlar transações nos banco de dados usando Spring e Java. Um banco de dados é composto de um servidor, onde podemos criar conexões para executar operações.

Estes bancos organizam os dados em tabelas, com schema definido e isso significa que antes de inserir dados, você precisa criar a estrutura por trás dos dados. Existem uma série de organizações possíveis para os dados, como banco de dados, schemas, tabelas, chaves e relacionamentos e cada um deles possui um nível de abstração.

Existem várias teorias de como armazenar os dados e como organizar/dividir os dados em cada um destas interfaces. Geralmente, os banco de dados são únicos, no começo do desenvolvimento do sistema, já que geralmente não temos muitas tabelas. A partir do momento em que existirem muitas tabelas, estes bancos podem ser divididos e eles geralmente agrupam dados de entidades, sistemas, ou departamentos de uma empresa. É muito comum existir um banco de dados de cliente, com as tabelas dos dados dos clientes. A forma como cada lugar organiza os dados está mais relacionado com como é a organização dentro de uma empresa, do que com alguma teoria em si.

Distribuição e organização dos dados

O importante é saber que, nas tabelas, é preciso setar os nomes de cada coluna, e as propriedades de cada uma delas, como qual é o tipo daquele campo, como numérico, texto e etc. Uma outra propriedade que pode ser setada é, se aquele campo pode ou não ser nulo, se ele é uma chave, o que significa que ele deve conter valores únicos naquela tabela. Isto já embute no banco uma certa inflexibilidade nestes bancos, pois não será possível inserir dados, com uma estrutura diferente da pré definida. Por outro lado, isto é muito bom, pois os dados inseridos ali já estão, de certa maneira, pré formatados em linhas e colunas.

Programando no seu banco de dados:

De uma maneira mais avançada, é possível adicionar funções, no seu banco de dados, que serão executadas a partir de uma operação. Aqui é possível fazer coisas simples como, verificar se o valor é nulo e substituí-lo para um valor padrão, caso o seja. Até coisas mais complexas como realizar cálculos e disparar alterações, ou inserções de dados em outras tabelas de acordo com a sua lógica. Isso pode ser feito tanto através de Triggers (gatilhos), quanto em Stored Procedures (procedimentos pré compilados). Algumas pessoas amam stored procedures, pois elas, por estarem no banco, executam bem rápido, mas até hoje não conheço um banco de dados que permite uma administração limpa e fácil dos códigos e do versionamento dos códigos que estão sendo executado nestas funções e isso se torna uma bela desvantagem quando estas ferramentas são usadas muito intensivamente, podendo provocar reações do banco inesperadas ou não mapeadas, por simplesmente desconhecer o que acontecerá com o dado após entrar no ali.

Conexões/Sessões:

Ao se conectar a um banco de dados, abre-se uma sessão de comunicação com ele, nesta sessão será possível executar comandos no banco. Este comandos incluem desde criação dos componentes do banco, até a inserção/modificação dos dados.

Qualquer  tipo de conexão realizada com o banco de dados, será feita através de uma sessão.

Agora imagine que dois usuários estão conectados ao banco de dados e cada um deles tenta alterar um mesmo valor ao mesmo tempo, devido a consistência forte, dos dados de um banco SQL isso não será possível e estes dados deverão ser alterados um após o outro. Pros usuários até pode parecer que a operação foi feita em paralelo, mas na verdade elas aconteceram em sequência.

Mas o que são estas travas ou cadeados de um banco SQL?

Internamente, quando uma transação de alteração de um dado é iniciada , o banco de dados trava aquele dado. Ou seja, o acesso aquele dado fica travado, até a transação completar, ou ser abortada, com isso não é possível que nenhuma outra operação de alteração seja realizada naqueles dados, enquanto aquela transação estiver aberta.

Assim, as alterações que, supostamente são em paralelo, na verdade rodaram em sequência, mas devido a velocidade dos processadores, a impressão é que ela aconteceu em paralelo. Para mostrar o impacto de uma alteração em uma linha, fiz um código para demonstrar isso, e este código está aqui neste repositório, em um código bem simples. Antes de executá-lo lembre-se de criar a estrutura de dados e inserir dados na tabela, o que pode ser feito executando os comandos deste arquivo.

O código mostrado aqui é bem simples, existem três classes, sendo duas principais, onde podemos executar alterações no banco de dados. Neste projeto foi feito usando o banco de dados MariaDb, e aqui tem um post que ensina a instalá-lo em sua máquina, de maneira bem simples.

No projeto, será mostrado qual é o impacto de ter uma alteração de linha, sendo realizada no exato momento em que um select é feito. Para fazer isso o código executa o mesmo select diversas vezes, e o mesmo update diversas vezes também. Aqui não era a intenção demonstrar isso em um ambiente complexo, a tabela utilizada como teste tem apenas 5 linhas, mesmo assim, já é possível perceber uma diminuição da performance em 30%, com apenas duas Threads executando.

Caso de Exemplo

Os selects rodam 12000 vezes, e os updates 1000 vezes, e estes números foram feitos, baseando-se na performance da minha máquina, sem mais nada executando em paralelo. O ideal é fazer estes testes sem mais nada executando na sua máquina, assim conseguimos isolar a execução do código, sem alguma interferência externa. Sabemos que um sistema operacional roda diversas coisas ao mesmo tempo, e isso pode gerar cargas de processamento de tempos em tempos, e assim interferir na execução do nosso teste. Executar os selects 12000 vezes é interessante para evitar que alguns fatores influenciem na medida da performance, quando mais vezes executarmos os selects, menos interferência a média de tempo geral sofrerá de alguns corner cases, que são casos em que a query demora muito mais, ou muito menos tempo, para ser executada.

Para executar este teste, de maneira mais honesta possível, usei a mesma classe, para executar os updates e os selects, quando fazia os selects, eu simplesmente commentei esta linha, que é a responsável por executar os updates. Repare que para assegurar que os selects e os updates estão rodando em paralelo, foram usadas duas Threads, e aqui existe um antigo post ensinando como fazer isso no java. A criação de uma Thread, quase não impacta na performance de um programa Java, ela só executará, quando o método start for chamado. 

Como resultado temos os seguintes números:

Média de tempo update108052,25
Média de tempo Select79167,75
Differença %26,73%
Média de acesso ao dado com update2066
Média de acesso ao dado com select1391,75
Differença %32,64%

 Foram levantadas duas métricas, o tempo, em nanosegundos, que uma query demora para executar, e depois da query, quanto tempo demora para um valor ser acessado. Repare que, nos dois casos houve perda de performance quando os updates são executados em paralelo. No geral, podemos assumir que o banco ficou 30% mais lento, quando um valor estava sendo alterado, enquanto os selects rodavam.

Isso em um banco simples, com poucas linhas e tabelas, agora imagine isso em um ambiente empresarial com diversas tabelas/sistemas acessando os dados de uma vez. Com o tempo, conforme os sistemas vão crescendo, isso acaba impactando na performance do banco, até que chega uma hora em que o banco, não suporta mais a demanda daquela empresa, ai nasce a necessidade de haver outros sistemas de armazenamento para complementarem o banco de dados tradicional.

Tipos de travas

Até agora vimos apenas os locks (travas) das transações, existem vários outros tipos, incluindo travas exclusivas, travas distribuídas, DML locks e locks de backup ou recuperação de dados. No exemplo também só foi citado a trava de uma linha, mas ele também pode acontecer em outros níveis como:

  • Tabelas
  • Linhas
  • Bloco de dados
  • Itens cacheados
  • Conexões
  • Sistema Todo
O que vimos aqui foi a trava em uma transação, mas isso pode gerar um impacto muito maior, quando a transação continua fazendo outras operações, e isso mantem a trava naquela linha alterada, quando ela fica esperando alguma operação e isso é muito perigoso, já que temos uma linha travada e isso pode gerar complicações no sistema.
É natural que as travas sejam pequenas, no começo, mas vão aumentando quanto maior for a quantidade de acessos aquele banco de dados e a concorrência aumenta. Com isso, coisas que inicialmente eram uma trava de uma linha, acabam virando uma trava de blocos de dados, depois de tabelas, até chegar ao sistema todo.

Existem diversas configurações, e ações dos desenvolvedores que podem aliviar, ou até acabar com as travas mais graves dos bancos, mas isso foge ao escopo deste post e não será mostrado aqui.

Apresentação