Pesquisar este blog

domingo, 18 de outubro de 2020

Exemplo de Spark Stream com Twitter

 No último post vimos como instalar o Apache Spark com docker, agora vamos mostrar um exemplo de como usar sua api de Stream pra ler dados do Twitter e processá-los.

Data Lakes

Muitas vezes existe mais de um sistema rodando dentro da empresa, e cada um deles possui um serviço de armazenamento, uma hora será necessário juntar os dados de todos estes sistemas em apenas um lugar, para que seja possível fazer uma análise completa dos dados da empresa. 

Aqui mesmo neste blog temos um post explicando uma das limitações dos bancos de dados, conforme o volume de dados aumenta, muitas vezes os sistemas começam a ficar travados e lentos, e por isso uma outra cópia do armazenamento de dados deverá ser feita, este é um dos fatores que geram esta necessidade de separar os dados para relatórios, dos dados de produção.

Por estes motivos, é cada vez mais comum a criação de Data Lakes dentro das empresas. Estes sistemas centralizam os dados das empresas, neles são inseridos os dados de todos os sistemas que rodam dentro do daquele ambiente e a partir do data lake  são gerados os relatórios, que entre outras coisas, podem cruzar os dados de todos os sistemas que rodam naquela empresa.

Stream

 Stream tem sido muito usado para sincronizar sistemas de armazenamento de dados, por isso os Data Lakes se aproveitam bastante deste modelo de comunicação de sistemas.

Alguns dados não precisam ser copiados assim que são criados, então sistemas de baches e jobs espaçados, que rodam a cada 3h por exemplo, resolvem este tipo de sincronização, mas existem alguns dados, geralmente os dados do coração do produto das empresas, estes sim, necessitam estar quentes ou seja, assim que são criados eles precisam ser replicados, pois fatalmente existem alguns relatórios que precisam ver eles em tempo real.

É com o uso de streams que estes dados serão copiados, com eles é possível criar réplicas com até 30 segundos de diferença, o que é algo bem quente. Imagina que uma cópia da tabela principal da empresa, pode estar atualizada com 30 segundos de latência.

Estes streams, geralmente usam sistemas de filas, onde todas as modificações dos bancos são postadas, as vezes usa-se os logs de alteração do banco, para postar nestas filas. A partir da fila criamos um sistema de leitura por stream, que receberá os dados assim que eles chegam na fila. Um outro exemplo são os streams criados diretamente dos sistemas de arquivos, ou seja, assim que o dado é escrito no disco, ele já estará disponível no stream para ser processado.

Apache Spark Stream

O Stream do Spark é um pouco diferente do conceito de stream que estamos acostumados, ao invés de ser um fluxo de dados contínuo, neste caso ele trabalha com microbatches, ou seja configuramos serviços que irão ler do stream de tempos em tempos, e tanto as janelas, quanto o volume de dados lido, são parâmetros configuráveis e isso implica num fluxo quase contínuo de dados. Existem sistemas como o Apache Flink, que possuem um sistema de Stream direto, onde os dados são processados assim que eles chegam e não há um intervalo configurável.

A diferença crucial é o tempo, um sistema de stream direto os dados chegam quase que em tempo real, em um sistema de microbatches ele pode ter até 2x a janela de tempo do intervalo entre as chamadas do microbatch.

Preparação do ambiente

Para testar o stream do Spark, vamos usar uma conexão de Stream do twitter, algo que já foi explicado neste post. Desta vez não vamos usar um cluster, mas sim uma versão standalone do spark rodando dentro de um notebook jupyter.

Aqui vamos usar um notebook Jupyter, com um kernel de Scala. Kernel é o core de execução dos notebooks, por padrão eles rodam python, mas existem algumas alternativas que rodam o Scala, dentre outras linguagens, neste caso estamos usando o almond, que é uma implementação opensource de kernel do Scala no Jupyter.

Para rodar uma imagem docker com o spark usando este projeto, usa-se o seguinte comando:

docker run -it --rm -p 8888:8888 almondsh/almond:latest

Ao iniciar a imagem, ele irá imprimir uma série de logs, mas devemos prestar atenção a um deles especificamente, caso você não iteraja com a imagem, ao final ele irá imprimir um endereço parecido com este:

http://127.0.0.1:8888/?token=a307c1b97755d83deafd2361a2a2fe5a5817c211968e5a97

Aqui temos o endereço de conexão, localhost porta 8888, além do token de conexão, este token é usado para garantir que apenas quem deve, consiga acessar, ler e executar os notebooks neste ambiente.

Por padrão esta imagem roda com a versão 2.13 do Scala, mas no nosso exemplo estaremos usando a versão 2.12, por isso certifique-se de alterar o kernel que está sendo executado com a versão correta do Scala. Esta modificação pode ser feita no menu em:

Kernel > Change Kernel > Scala 2.12

Desta vez estaremos usando a versão 2.4.0, pois esta é a última versão que possui compatibilidade com as bibliotecas do Twitter Stream.

Spark Stream com Twitter

Agora, o último passo será executar uma conexão de Stream do Spark com o Twitter, para isso será necessário ter as chaves de acesso, para isso cada um terá que criar um aplicativo dentro da sua conta do Twitter. 

O primeiro passo será instalar as dependencias, e o almond facilita bastante nesta parte, pois ele permite que estas dependências sejam instaladas pelo próprio notebook.

import $ivy.`org.apache.spark::spark-sql:2.4.0`
import $ivy.`org.apache.spark::spark-streaming:2.4.0`
import $ivy.`org.apache.spark::spark-core:2.4.0`
import $ivy.`org.apache.bahir::spark-streaming-twitter:2.4.0`
import $ivy.`sh.almond::almond-spark:0.10.8`
import org.apache.log4j.{Level, Logger}

import org.apache.spark.SparkConf
import org.apache.spark.streaming.StreamingContext
import org.apache.spark.streaming.Seconds

import twitter4j.conf.ConfigurationBuilder
import twitter4j.auth.OAuthAuthorization
import twitter4j.Status

import org.apache.spark.streaming.twitter.TwitterUtils

Logger.getLogger("org").setLevel(Level.OFF)

As primeiras 5 linhas, instalam as dependencias, tanto do Spark, quanto do twitter, estes comandos irão baixar os jars necessários, diretamente dos repositórios maven, e adicioná-los ao contexto de execução do notebook.

No último comando, estamos desligando os logs, pois neste caso não iremos usar, e eles são muito verbosos.

Um ponto importante a levantar aqui é, devemos sempre fazer backups dos nossos notebooks, pois toda vez que paramos a execução e iniciamos ela novamente, os notebooks criados ali serão perdidos. No nosso caso isso não impacta muito pois o caso de exemplo é um notebook bem simples.

No próximo passo iremos criar o contexto de Stream do Spark, e é nele que iremos configurar as propriedades dos nossos microbatches:

 val appName = "TwitterSparkDSD"
 val conf = new SparkConf()
 conf.setAppName(appName).setMaster("local[*]")
 val ssc = new StreamingContext(conf, Seconds(5))

Neste passo definimos algumas configurações básicas, como o nome do aplicativo, assim comoa localização do master do Spark, que neste caso está rodando localmente, e iniciamos o contexto do streaming. Repare que no segundo argumento do contexto do streaming, passamos uma variável temporal de 5 segundos. Isso significa que, a cada 5 segundos o spark iniciará um processo de microbatch para ler os dados desta fonte. 

É muito importante notar que, o tempo de processamento de cada batch, deve ser menor do que 5 segundos, neste caso, senão isso significa que o nosso cluster não está sendo capaz de processar todos os dados que estão chegando. Neste caso os dados podem ficar acumulados e até travar o cluster.

Agora chegou a hora de criar o objeto de autenticação no sistema do twitter, isso será criado a partir do código:

val consumerKey = ""
val consumerSecret = ""
val accessToken = ""
val accessTokenSecret = ""

val cb = new ConfigurationBuilder
cb.setDebugEnabled(true).setOAuthConsumerKey(consumerKey)
      .setOAuthConsumerSecret(consumerSecret)
      .setOAuthAccessToken(accessToken)
      .setOAuthAccessTokenSecret(accessTokenSecret)
val auth = new OAuthAuthorization(cb.build)

Repare que aqui todos os tokens de acesso estão em branco, estes dados estão disponíveis na página da sua aplicação, dentro do twitter, substitua cada um deles pelos valores gerados para o seu aplicativo.

Agora vamos nos conectar a api do Twitter, isso será feito com o seguinte comando:

val tweets = TwitterUtils.createStream(ssc, Some(auth))

Repare que aqui foi criado um stream de tweets, mas realmente precisamos de 2 streams? Já não tinha sido criado um stream do spark?

Bom a ideia deste pedaço de código é criar a conexão de leitura, passamos os dados de autenticação, além do stream do spark, este método irá conectar o contexto de Stream do Spark, com a api de stream do twitter.

O volume de dados que chega do Twitter é obviamente muito grande, por isso vamos filtrar para conseguir provar que o código realmente está funcionando, para isso faremos o seguinte filtro:

val dsdTweets = tweets.filter(_.getText() == "#DSD")
dsdTweets.saveAsTextFiles("tweets", "json")

Com este filtro, serão considerados apenas os casos em que os tweets tiverem a string #DSD dentro do texto digitado. Assim dá pra fazer um teste ao vivo e twittar um conteúdo com este texto e validar se ele realmente vai aparecer gravado nos arquivos. Vejam que no comando seguinte, os dados estão sendo gravados como arquivos texto com o json de conteúdo dos tweets recebidos. Repare também, que as duas strings passadas como argumento para o método que salva os dados, são tweets e json, elas serão usadas para criar diretórios, cada batch gravará um diretório iniciando com tweets e terminados como json. Para não haver conflito com estes diretórios, o timestamp será adicionado ao nome de cada um deles, assim não teremos dois batches gravando no mesmo diretório.

Esta api é aberta e pode ser usada gratuitamente, mas quando estamos usando ela sem pagar, não acessaremos todos os tweets da base, para fazer isso precisamos pagar para o twitter, por ser de graça ela tem alguns limites e, por isso, talvez alguns tweets do seu teste serão perdidos.

Falta ainda um passo para terminar o nosso leitor de tweet, que é inicializar o processo:

ssc.start()
ssc.awaitTermination()

Aqui foram 2 comandos, o primeiro inicia o stream, e o segundo diz quando que o job de stream irá terminar. Com este comando, nosso stream só vai terminar quando o job falhar, ou quando ele for manualmente parado. Para desligar o job manualmente, é só parar o kernel do jupyter, isso fará que toda a execução seja desligada.

Para validar que conseguimos acessar algum tweet, vamos checar o sistema de arquivos, isso pode ser feito, acessando a tab inicial do jupyter, conforme mostra a imagem abaixo:

Aqui são mostrados vários diretórios, isso significa que o processo de salvamento dos dados funcionou, agora está noa hora de acessar estes diretórios para ver se os dados realmente estão lá:

Repare que temos vários arquivos nomeados part-xxxxx, isso acontece pois cada executor do spark irá gravar um arquivo separado. Cada executor do spark processará parte dos dados, e ele tem a capacidade de conectar cada executor para ler em paralelo da api, com isso a velocidade de processamento dos dados será em paralelo.

Veja que alguns arquivos tem dados, e outros não, isso significa que alguns executores tiveram dados para processar, mas outros não.

Dentro dos arquivos é possível ver os dados de alguns tweets, não vou postar aqui pois isso vai ser diferente cada vez que você acessar, então não faz sentido.

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

quinta-feira, 14 de maio de 2020

Dicas de programação: Como definir nomes em programação.

Dicas de programação

Nomenclatura de variáveis

Nomear as variáveis é um passo importante para conseguir construir um software, e os nomes das variáveis precisam identificar muito bem, o que aquele objeto contém.
É extremamente desaconselhado, nomear variáveis com apenas um caracter, isso é bem claro quando você está programando porém, com o passar do tempo, você esquecerá aquele significado e identificar o que foi atribuído aquela variável se tornará uma tarefa muito custosa, pois muito código deverá ser lido para encontrar o significado dela.
A dica é dar nomes significativos aos componentes, tanto objetos, classes, interfaces, como métodos, parâmetros e variáveis. Isso geralmente implica em ter um nome com mais de uma palavra para componente.
Gosto de colocar os objetos sempre no singular, uma vez que eles representam apenas um valor, a não ser que sejam listas, arrays, iteráveis ou mapas, estes sim vão no plural, pois representam mais de um valor. Assim, seguindo este padrão, toda vez que você mapear um objeto do banco de dados, imagine que estamos mapeando uma entidade chamada Leitor (você no caso), esta entidade terá algumas propriedades (colunas de uma tabela, caso esteja usando um banco com schema, ou valores de um json, ...). Imagine que foi definido que Leitor tem estas propriedades, id, nome, ip, lista[Visita].
Ao definir esta entidade, todos os nomes des suas propriedades ficariam no singular, a não ser a lista, esta sim ficaria no plural, veja a seguir
   objeto Leitor {
      numero privado id
      texto privado nome
      texto privado ip
      lista[Visita] privada sitesVisitados
  } 

Padrões de nomenclaturas

Existem alguns padrões de nomenclaturas, tanto pra variáveis, quanto para nomes dos objetos, os mais comuns são:
  • camelCase: onde a primeira letra de cada palavra fica em maiúsculo, e o resto em minúsculo, para os nomes dos objetos. Já nos nomes das variáveis a primeira letra, sempre será em minúsculo, mas as outras primeiras letras das palavras serão maiúsculas, para o exemplo de nome sites visitados, neste padrão ficaraia assim.
    • SitesVisitados: para nomes de enuns, interfaces, classes e objetos.
    • sitesVisitados: para nomes de variáveis.
  • snake_case: neste caso, todas as palavras ficam em minúsculo e elas são separadas por um '_', este padrão foi muito usado em linguagens como Lisp e COBOL, por isso também pode ser chamado de lisp_case, COBOL_CASE ou também kebab_case. Constantes, neste caso são definidas todas em MAIÚSCULO, no nosso exemplo ficaria assim:
    • sites_visitados: para nomes de variáveis, enuns.
    • Em alguns casos de snake case, o camelCase é usado para nomes de objetos e classes, como no python.
Esta será uma série de posts feitos aqui no blog que começa com este primeiro de nomenclatura.

domingo, 10 de maio de 2020

O que são containers de Software? Docker, kubernetes, mesos...

Definição

Container é um software que empacota um software e todas as suas dependências para que elas sejam distribuídas e, desta maneira, consigam ser executado corretamente em qualquer ambiente para onde ele for levado.

Todo software, para ser executado, precisa que suas dependências sejam instaladas no sistema, por isso, muitas vezes é bem complicado de colocar um software para rodar. Existem ainda o problema das dependências cruzadas, onde você necessita de 2 softwares instalados em um ambiente (Software A e B), em alguns casos, Software A depende de uma biblioteca X, na versão 1.0, já o Software B, depende da versão 1.5 da mesma biblioteca X. Caso estas duas versões da biblioteca X sejam incompatíveis, esta instalação causará problemas e estes softwares não conseguirão ser instalados num mesmo ambiente.

Empresas que desenvolvem software, usualmente possuem vários ambientes diferentes, desde a máquina dos seus desenvolvedores, cada qual com os seus softwares instalados, até ambientes de teste e produção, os quais geralmente são idênticos. Quando um desenvolvedor completa uma tarefa, este código será colocado para rodar, os testes, em um ambiente de teste. Neste passo é possível pegar problemas de incompatibilidade de bibliotecas, mas isso só vai acontecer após o desenvolvedor completar o desenvolvimento. 

Como os containers resolvem estes problemas

Conforme mostrado acima, os containers carregam todas as dependências, arquivos e configurações necessárias, para um software rodar. Com isso, é possível replicar o ambiente de produção, em qualquer lugar, inclusive na máquina dos desenvolvedores, e assim evita-se estes problemas de incompatibilidade. Dentro deles, os softwares rodam de uma maneira bem isolada, e muitos problemas de infra estrutura são evitados.

Quando eles surgiram

A história dos containers começa em 1979, com o surgimento de uma ferramenta, no Unix, chamada de chroot, este é considerado o primeiro dos containers. A partir dos anos 2000, com o surgimento do Jails do FreeBSD, do VServer e do Solaris Containers, estes softwares começaram a ficar mais populares e a ganhar novas funcionalidades. Em 2008 surgiu o LXC, que depois foi usado como base, para o software de container mais popular, ou o que mais popularizou a utilização dos containers, que foi o Docker, o qual só foi aparecer em 2013.

Aqui é importante notar que, o Docker nada mais é do que um facilitador da utilização, inicialmente do LXC, que já tinha sido inventado há 5 anos, quando ele surgiu. Mas devido a nova abordagem, como a construção de um ecossistema ao redor dos containers, uma nova maneira de monitorar e um modelo de imagens em camadas, foi o que o fez tão popular.

Esta ferramenta, sem dúvida é muito importante no ambiente de desenvolvimento hoje, pois ela facilitou muito a vida de quem pretende, ou precisa criar uma imagem de um container. Ela é amplamente utilizada pelos provedores de cloud, assim como pelas empresas de desenvolvimento.

Kubernetes, também é uma ferramenta muito importante no ambiente dos containers, por que ela foi criada para orquestrar containers. Com o Kubernetes, conseguimos colocar para rodar, um sistema distribuído, em vários containers, sendo eles réplicas um do outro, ou múltiplas partes do sistema. Através dele é possível configurar quanto cada parte do sistema está rodando, e controlar todos os componentes do sistema.

Um outro fato importante no mundo dos containers, aconteceu quando o Docker abriu o código do seu containerizador, o que se tornou Containerd, e algumas empresas se juntaram em uma associação chamada de CNCF, que hoje dita os padrões dos executores em cloud.

Qual é a diferença entre um Container e uma Máquina Virtual

As máquinas virtuais emulam o hardware, e nelas é possível instalar um Sistema Operacional, em cima do qual, você consegue instalar seus softwares. Já em um container, o Sistema Operacional é emulado, assim é possível você instalar bibliotecas, como se fosse em um Sistema Operacional novo, os containers conseguem isolar áreas da memória, do storage, e os processos que eles acessam, com isso você tem um ambiente isolado.

Para um provedor de Cloud, e para quem usa este tipo de serviço, os containers são muito interessantes, pois uma vez que, se você tiver que usar uma máquina virtual, pra cada um dos seus componentes, cada um deles terá um sistema operacional instalado, e isto ocupará memória e espaço em disco. Cada arquivo dos seus componentes, deverá carregar a instalação do Sistema Operacional e do ambiente do seu aplicativo.

Já em um container não, pois eles rodam sob o mesmo Sistema Operacional e partes deste sistema serão compartilhadas, como o kernel, o que o torna bem mais leve e cada arquivo de container, contém apenas o seu software e as suas dependências. Assim estes arquivos ficam bem menores o que torna mais fácil a sua administração. Dentro de um container, todas as partes compartilhadas do Sistema Operacional,  terão acesso somente de leitura, o que garante que os containers não vão interferir nos Sistemas Operacionais hospedeiros, o que foi feito por medida de segurança.

Por serem menores, e mais leves, é muito melhor trabalhar com containers, pois menos dinheiro é gasto com as máquinas e com os dados armazenados, assim usar containers nos seus sistemas é vantajoso tanto para os provedores de cloud, os quais economizam com o hardware, quanto para os clientes, que conseguem ter softwares mais ágeis e um custo menor.

Arquitetura dos Containers

Um container virtualiza um Sistema Operacional, por isso ele consegue compartilhar o Sistema Operacional de uma máquina, em vários ambientes isolados, fazendo com que seja possível executar um software em um ambiente isolado.

Os containers são armazenados em arquivos de imagens, cada imagem contem a instalação dos pacotes necessários para seu software rodar, além de guardar as configurações de qual software será executado por ela.

Estas imagens podem ser publicadas, e existem repositórios públicos destas imagens, o que é uma das vantagens deles. Imagine que você tenha algumas dependências no seu projeto, antes de construir uma imagem do zero, é possível buscar por imagens que contenham estas dependências, e já estejam prontas, assim é só baixar a imagem, e executá-la.

Arquitetura do Docker

O Docker possui um processo que administra e cuida dos containers, este processo é persistente. Existe uma API de comunicação, que pode ser utilizada para enviar comandos para o docker, este processo cuida desta interface. O comando docker é o cliente do docker, o qual é responsável por permitir que o usuário interaja com as imagens dos containers.

Existem três tipos de classes de objeto no ambiente Docker, o container, a imagem e os serviços. O container é responsável por rodar as aplicações, ele é um ambientes padronizado e encapsulado. Eles são administrados pelo cliente ou pela API.  

A imagem é um template, somente de leitura, que contem todas as informações necessárias para executar um container, elas são usadas por armazenar e distribuir as aplicações.

Um serviço de Docker permite que os containers sejam executados em diferentes processos, e máquinas, que se comunicam entre si, isso também é conhecido por Swarm.

Duas ferramentas são disponibilizadas pelo Docker, o compose, que é responsável por definir e rodar ambientes de múltiplos containers usando apenas um comando. Com ele é possível disparar comandos para múltiplos containers de uma vez só.

A outra ferramenta disponibilizada é o Swarm, o qual permite uma clusterização nativa de containers do docker. Nesta ferramenta permite rodar vários engines de Docker dentro de um processo de execução só.  

Outras vantagens 

Por ter apenas o ambiente, é possível construir uma imagem com apenas alguns megabytes, e quanto menor a sua imagem, mais rápido e fácil é para conseguir executá-la.

Com os containers tornou-se possível modularizar ainda mais as aplicações, já que basta produzir uma imagem, com cada módulo do sistema, e colocá-los para executar isolados uns dos outros, ao invés de rodar uma aplicação complexa dentro de uma mesma máquina. Aplicações mais modulares, permitem um monitoramento mais específico e a replicação de cada parte, quando necessário.

Uma máquina virtual, precisa de algum tempo até ser executada, pois o boot consome uma parte de startup, já em um container isto não acontece, a partir do momento que uma imagem começa a ser executada, ela já está executando aquele software para qual ela foi configurada.

Impactos dos containers

Não fossem os containers não seria possível haver a arquitetura serverless, pois nesta arquitetura, grandes máquinas ficam rodando, e cada serviço é um container, que será configurado para executar um serviço em específico. Assim que o serviço é chamado, a imagem do container começa a rodar, ao terminar, o container para, e as imagens são desligadas.

Para conseguir isso, uma série de dispositivos precisaram ser desenvolvidos, em cima dos containers, mas, aqui os orquestradores, tem um papel fundamental, pois eles conseguem ligar, e desligar (orquestrar) as imagens, quando elas forem necessárias. É possível termos máquinas ligadas a repositórios de imagens, e um serviço de fila, distribuindo as chamadas aos serviços entre diversas máquinas, conforme os serviços são chamados, as imagens são baixadas e executadas por estas máquinas, e isto permite que, um grande hardware seja compartilhado por diversos métodos, fazendo com que o custo daquele hardware seja distribuído para todos estes métodos. Assim ao invés de pagar pela execução de uma máquina 24/7, é possível pagar apenas pelo tempo que seu serviço foi executado.

O surgimento deste tipo de serviço, mexeu muito em como os softwares são desenvolvidos, isto impactou em arquitetura, modularização e diversos outros pontos dos sistemas distribuídos.

Kubernetes

Muitas vezes a comparação entre Docker e Kubernetes surge, mas essa comparação não deve ser feita, pois eles são coisas muito diferentes.

Kubernetes é um orquestrador, e com ele podemos criar clusters de imagens de containers rodando, podemos fazer isso em um cluster de máquinas virtuais. Isso é algo bem requisitado, imagine que você tem um cluster de máquinas, e vários clientes, cada um com a sua image de container, isso pode acontecer tanto dentro de uma mesma empresa, quanto em um provedor de serviços de execução. 

Com o Kubernetes você controla quais imagens vão rodar onde, e com isso é possível configurar muitos detalhes. No Kubernetes, os containers são agrupados em pods, e através destes pods, é possível configurar quantas instâncias daquela imagem vai rodar, quando e por quanto tempo. Com isso, é possível definir detalhadamente a configuração de uma, ou mais apps. 

Não é atoa que o kubernetes foi escolhido para gerenciar clusters em clouds, como o AKS na Azure, na Amazon no EKS, no Google Cloud GKE, na IBM Kubernetes Service, no Alibaba ACK e na Huawei CCE, ou seja todos os provedores cloud proveem, de alguma maneira, um serviço usando o Kubernetes.


Apresentação