Conceitos de Big Data explicados em um aplicativo simples
Neste artigo, mostrarei os problemas mais comuns que podem ocorrer em uma aplicação muito simples de Big Data e, com base nesses problemas, explicarei os principais conceitos do mundo do Big Data.
Vamos imaginar que somos contratados para implementar um aplicativo que conta o número de visitas a um portal de mídia social muito popular. Temos que rastrear logins de usuários e quantas vezes um usuário entra em uma determinada página. Os resultados devem estar disponíveis em tempo real.
Não parece complicado, não é? Um simples listener anexado aos nossos controladores web e um banco de dados com uma única tabela devem ser suficientes. Sempre que um usuário entra em uma determinada página, o evento é registrado e armazenado no banco de dados. Se esse usuário visitar a página novamente, o valor da contagem será atualizado adequadamente.
Os dados de amostra ficariam assim:
Tudo está indo bem até agora, mas sabemos que é apenas a calmaria antes da tempestade. À medida que o tráfego do site aumenta, teremos que resolver novos problemas que irão surgir.
O primeiro grande problema que provavelmente teremos que enfrentar será a latência. Com o aumento do número de usuários e do número de leituras/gravações, o tamanho da nossa tabela também aumentará. Cada solicitação do usuário gera uma atualização de banco de dados e com um grande número de visitas pode criar um gargalo em nosso banco de dados. É claro que existem muitas maneiras de resolver este problema, mas quero focar particularmente no processamento em lote.
Em vez de atualizar o banco de dados após cada visita, podemos esperar por um determinado número de eventos (ou um período de tempo) e então realizar uma única e grande atualização. Poderíamos introduzir uma fila que contém objetos antes de gravá-los no banco de dados. Dessa forma, podemos diminuir o número de gravações em nosso armazenamento e, eventualmente, torná-lo mais responsivo.
Naturalmente, existem desvantagens nesta abordagem:
- perda de dados em tempo real; nossos dados são tão precisos quanto as lacunas entre as atualizações em lote
- os dados não persistem, mas podem ser perdidos
Essas duas desvantagens podem ser facilmente superadas usando, por exemplo, Kafka como armazenamento temporário. Contudo, não é assunto deste artigo.
Espaço insuficiente
Mais cedo ou mais tarde, teremos que lidar com outro problema: falta de espaço em disco. É óbvio que um único disco não será suficiente e teremos que adicionar outro. Mas como podemos abranger nosso banco de dados em mais de um disco? É aqui que entra a fragmentação. A fragmentação é um padrão de divisão de uma tabela em partições, cada uma contendo um subconjunto de linhas. Fragmentação significa particionamento horizontal; não precisamos alterar o esquema da tabela para aumentar a capacidade do banco de dados. Mas precisamos implementar um algoritmo para encontrar o fragmento que contém a linha que procuramos. Aí vem o conceito chamado chave de partição. É um valor que determina qual fragmento deve ser usado para armazenar a linha. No nosso caso o valor mais natural seria a coluna ‘Utilizador’, mas claro que a decisão depende das agregações que o cliente pretende.
Também temos a opção de particionamento vertical. Nesse caso, dividimos as colunas em tabelas diferentes e colocamos essas tabelas em servidores separados.
Sem acesso a um fragmento
Nosso aplicativo já está instalado e funcionando. Finalmente resolvemos problemas de latência e espaço livre adicionando processamento em lote e introduzindo fragmentos. Agora, em vez de apenas um servidor de banco de dados, temos mais instâncias. Algumas dessas instâncias estão em máquinas separadas, o que significa melhor balanceamento de carga, mas, por outro lado, mais máquinas para manter e motivos adicionais para nossa aplicação perder acesso ao banco de dados. A falha de um único fragmento torna nossos dados inválidos e, portanto, inutilizáveis. Uma solução para tal situação reside na replicação. Em vez de ter apenas uma instância contendo um determinado subconjunto de dados, deveríamos ter mais deles, de preferência em uma máquina diferente. Dessa forma, em caso de falha, nosso despachante poderá buscar dados em uma máquina diferente.
A máquina 1 é um nó primário para dados do Shard A e um nó de backup para dados do Shard B. Quando a Máquina 2 falhar, a Máquina 1 servirá os dados do Shard B. Não existe um ponto único de falha, o que é muito importante para sistemas distribuídos. É claro que a replicação não é um recurso que você obtém gratuitamente. Com a replicação surgem problemas relacionados à sincronização de dados entre nós primários e de backup.
Recuperação do estado do sistema
Até agora tudo bem. Identificamos e fornecemos soluções para problemas comuns. Com alguma codificação, conseguimos lidar com problemas de latência e aumentamos a tolerância a falhas. Mas há questões que são impossíveis de prever e, o que é pior, impossíveis de resolver. Ou seja, problemas causados ​​por erros humanos. Na maioria das situações, uma simples correção no código é suficiente. Porém, como eu disse, às vezes não dá para consertar tudo. Imagine um erro que fez com que nossa contagem de visitas fosse atualizada duas vezes, resultando em um aumento de 2. Uma vez implantado em produção, é muito difícil encontrar tal bug, porque nada suspeito está no sistema. Mesmo quando você finalmente localizar esse problema, não saberá quantos dados foram quebrados.
Em situações semelhantes a esta, uma abordagem muito útil é evitar a mutabilidade e usar objetos imutáveis ​​tanto quanto possível. Para nós, isso significa retrabalhar o banco de dados. Em vez de ter uma tabela que retenha as visitas dos usuários, deveríamos armazenar eventos. Cada visita do usuário cria um evento com login do usuário, data e caminho solicitado. Não há necessidade de atualizar nada. Inserção simples e rápida. Essa abordagem nos permite restaurar o estado do aplicativo para uma determinada data — é chamada de origem de eventos.
Porém, a tabela de eventos não nos dá o número de visitas. Eventos são dados brutos. Portanto, temos que criar uma visão que irá agregá-la para nós. Os dados (eventos) são usados ​​para obter informações (número de visitas). Como desenvolvedor, você não pode prever que tipo de informação será necessária no futuro, mas tendo esses eventos armazenados em um local seguro, você pode ter certeza de que, mais cedo ou mais tarde, um cientista de dados fará uso desses dados.
Exemplo de atacado online
Como exemplo, gostaria de mostrar um design de banco de dados para uma loja online de atacado. Nosso objetivo é armazenar informações básicas sobre empresas e seus pedidos. Tudo é armazenado de maneira imutável.
A primeira tabela contém as empresas e a segunda contém as ações que as empresas realizam em relação às suas listas de pedidos. Você pode ver que a empresa Quantum Cosmetics fez alguns pedidos (e cancelou dois deles). Para contar a quantidade de pedidos ativos que a Quantum Cosmetics possui, você realiza uma consulta simples que filtra os pedidos que foram cancelados e conta o que resta.
Claro, pode-se dizer que pode ser demorado agregar todas as ações realizadas pela Quantum Cosmetics. Isso é verdade e um bom motivo para usar a arquitetura lambda. Basicamente falando, em determinadas datas você executa os lotes que agregam dados de eventos atuais para que você não precise calculá-los desde o início. No nosso caso, uma agregação que poderia ser armazenada em uma tabela separada seriam os pedidos “ativos”. Chamamos isso de visualização em lote. Agora, para obter a lista atual de pedidos ativos, temos que mesclar a visualização do lote com os eventos que vieram após a execução do lote e estão na camada de velocidade.
O que acontece quando a Quantum Cosmetics muda a sede para uma cidade diferente?
Uma nova entrada é adicionada à tabela. Como obter a versão atual do “Quantum Cosmetics”? Basta pegar a linha com o ID da empresa igual a 2 e com o carimbo de data/hora mais recente.
Precisão dos dados versus latência dos dados
Voltemos ao nosso site. Com o tempo, estamos obtendo cada vez mais dados. Agora é um bom momento para pensar em extrair informações valiosas do nosso banco de dados. Rastreamos a interação do usuário com nosso site e armazenamos suas atividades como eventos. Esses eventos nos permitirão construir um caminho que o visitante do site percorreu. Esse caminho é chamado de clickstreams ou, alternadamente, clickpaths. Tendo um grande número de usuários ativos em todo o mundo que visitam nosso portal, podemos construir relatórios muito interessantes. No final de cada dia, podemos executar um processo em lote para agregar esses dados. Por exemplo, se você notar que usuários de uma determinada região pedem regularmente um veterinário, é uma dica clara montar uma clínica veterinária.
Mas e se quisermos obter informações em tempo real sobre as interações dos usuários? Não podemos simplesmente consultar o banco de dados repetidamente, pois isso resultaria em uma sobrecarga. Em vez disso, devemos consumir os dados recebidos à medida que chegam ao nosso sistema. Precisamos usar streaming. A fila que apresentamos anteriormente em nosso aplicativo pode ser tratada como uma entidade observável — um tópico. Ter filas semelhantes para cada tipo de evento nos permite configurar mais consumidores de dados. Podemos ter mais de um consumidor para um determinado tema, cada um fazendo uma coisa diferente. Esses pipelines podem ser muito simples, como o do nosso primeiro caso — que armazena os eventos em buffer e os persiste no banco de dados a cada 60 segundos. Dizemos que os dados são processados ​​em uma janela. Porém, não precisamos nos limitar e esperar 60 segundos, podemos usar intervalos de tempo diferentes.
E se tivermos vários consumidores vinculados a cada tópico, cada um processando centenas de milhares de eventos por segundo? A largura de banda da rede se tornará nosso gargalo e sofreremos com latência de dados. Em sistemas menores, presumimos que os dados devem ser precisos. Mas no mundo do Big Data, a precisão perfeita dos dados não é realmente o Santo Graal. Existem situações em que a latência dos dados é mais importante. Ao agregar milhões de registros para calcular uma média, você se importa se alguns registros estiverem faltando? Você prefere sofrer atrasos ou ter resultados entregues mais rapidamente?
Semântica de sistemas de mensagens
Sempre há uma compensação entre a precisão e a latência dos dados. Mecanismos de processamento de fluxo orientados a eventos, como o Kafka, nos permitem estabelecer um contrato entre um produtor e um tópico.
Se quisermos que nosso sistema seja o mais preciso possível e não nos importarmos com a latência, devemos garantir que cada mensagem seja processada exatamente uma vez. Neste caso, o produtor envia o mesmo evento para o tópico até obter a confirmação de que o evento foi escrito. O tópico, por outro lado, deve verificar se o mesmo evento não foi escrito mais de uma vez. Isso adiciona sobrecarga em ambos os lados, desacelerando o sistema.
Desabilitar as verificações no lado do tópico leva a uma situação em que um evento é processado pelo menos uma vez. Se não nos importarmos com duplicatas em nosso aplicativo (ou com nosso aplicativo idempotente), esta solução melhorará o rendimento e a latência do sistema. Uma das maiores lojas online do mundo usa essa abordagem. Se você tiver sorte, poderá receber dois pacotes em vez de um. Se você não tiver sorte… poderá ser cobrado duas vezes – é claro que existe um serviço de reembolso especial para lidar com esse caso. A loja decidiu melhorar a experiência do usuário e tornar o sistema mais responsivo, sacrificando a precisão. No caso deles, é melhor pagar uma compensação, digamos, a cada milionésimo cliente, e ter mais clientes satisfeitos no total.
A referida loja também poderia fazer uma escolha diferente. Eles poderiam processar mensagens pelo menos uma vez. Neste caso, o nosso produtor do evento envia a mensagem e não verifica se essa mensagem foi escrita. Imagine-se encomendando um presente de véspera de Natal para seu filho. Mesmo que você tenha pago por isso, seu pedido poderá não ser processado posteriormente, porque o evento correspondente não foi processado. Você não receberá o pacote prometido e provavelmente desistirá de comprar nesse site no futuro. Neste caso a opção anterior era muito melhor.
Neste artigo, descrevi brevemente os problemas comuns que os aplicativos apresentam. No Big Data, não é uma questão de saber se esses problemas surgirão ou não, mas sim quando. Como esses problemas são comuns, eles já foram resolvidos em diversas soluções como Kafka ou na pilha Hadoop — por exemplo, com HDFS e HBase.
- Big Data - Nathan Marz
- Entendendo o NoSQL — Dan McCreary, Ann Kelly
- Hadoop na prática - Alex Holmes