Arquitetura de Software: Explicando Stream Processing, Event Source e Data Streaming

Algumas pessoas chamam isso de stream processing. Outros chamam de event streaming, ou complex event processing (CEP) , ou CQRS. Às vezes, esses jargões são apenas fumaça e espelhos (smoke and mirrors), inventadas por empresas que querem vender coisas para você. Mas, às vezes, eles contêm um núcleo de sabedoria, levando a melhores tecnologias que nos ajudam a projetar sistemas melhores.

Nesta palestra, examinaremos o que é event stream processing e como os dados em tempo real podem ajudar a tornar seu aplicativo mais escalável, mais confiável e mais sustentável. Fundado na experiência de criar sistemas de dados em larga escala no LinkedIn e implementado sistemas open source data streaming como Apache Kafka e Apache Samza, stream processing está finalmente chegando à maioridade.

Esta é uma transcrição editada de uma palestra que dei no /dev/winter 2015. Atualizado em 4 de Fevereiro de 2020.

Nesta apresentação, discutirei algumas das idéias que as pessoas têm sobre o event stream processing. A idéia de estruturar dados como um fluxo de eventos não é novidade, mas recentemente notei que ela reapareceu em muitos lugares diferentes, geralmente com terminologia diferente e diferentes casos de uso, mas com os mesmos princípios por baixo dos panos.

O problema quando uma técnica se torna moda é que as pessoas começam a gerar muito hype e jargões em torno dela, geralmente reinventando idéias que já são comuns em um campo diferente, mas usando palavras diferentes para descrever a mesma coisa. Nesta palestra, tentarei abordar alguns dos exageros e jargões na área geral de stream processing e me concentrar nas idéias fundamentais dessa tecnologia.

Embora o jargão possa ser desanimador quando você o encontra pela primeira vez, não há necessidade de se assustar. Muitas das idéias são bastante simples quando você chega ao núcleo principal. Além disso, há muitas boas idéias que merecem ser aprendidas, porque elas podem nos ajudar a criar aplicativos melhor.

Image for post
Image for post

A noção de processar eventos aparece em muitas áreas diferentes, o que é realmente confuso a princípio. Pessoas em campos diferentes usam vocabulário diferente para se referir à mesma coisa. Eu acho que isso ocorre principalmente porque as técnicas se originaram em diferentes comunidades de pessoas, e as pessoas parecem estar frequentemente dentro da própria bolha e não olham para o que seus vizinhos estão fazendo.

As ferramentas atuais para stream processing distribuído surgiram de empresas da internet como o LinkedIn, com raízes filosóficas na pesquisa de banco de dados do início dos anos 2000. Por outro lado, o complex event processing (CEP) teve origem na pesquisa de simulação de eventos nos anos 90 e agora é usado para fins operacionais em empresas. Event Sourcing tem suas raízes na comunidade de domain-driven design, que lida com o desenvolvimento de software corporativo — pessoas que precisam trabalhar com modelos de dados muito complexos, mas geralmente conjuntos de dados menores do que as empresas de internet.

Minha experiência é em empresas de internet, mas estou fazendo o possível para entender o jargão das outras comunidades e descobrir os pontos em comum e as diferenças. Se eu entendi algo errado, por favor me corrija.

Image for post
Image for post

Para tornar isso concreto, vou começar dando um exemplo do campo de stream processing, especificamente análise. Vou então traçar paralelos com outras áreas.

Image for post
Image for post

Para começar, imagine algo como o Google Analytics. Como você provavelmente sabe, o Google Analytics é um pouco de JavaScript que você pode colocar no seu site e ele mantém o controle de quais páginas foram visualizadas por quais visitantes. Um administrador pode explorar esses dados, detalhando os segmentos por período de tempo ou URL, e assim por diante.

Como você implementaria algo como o Google Analytics?

Primeiro, precisamos da entrada para o sistema. Sempre que um usuário visualiza uma página, precisamos registrar um evento para registrar esse fato. Um evento de exibição de página pode se parecer com isso (usando um tipo de pseudo-JSON):

Image for post
Image for post

Uma visualização de página possui um tipo de evento (PageViewEvent), um registro de data e hora do Unix que indica quando o evento ocorreu, o endereço IP do cliente, o ID da sessão (esse pode ser um identificador exclusivo de um cookie, que permite descobrir quais séries de visualizações de página é da mesma pessoa), o URL da página visualizada, como o usuário chegou a essa página (por exemplo, de um mecanismo de pesquisa ou clicando em um link de outro site), as configurações de navegador e idioma do usuário , e assim por diante.

Observe que cada evento de exibição de página é um fato simples e imutável. Simplesmente registra que algo aconteceu.

Agora, como você passa desses eventos de exibição de página para o bom painel gráfico no qual podemos explorar como as pessoas estão usando seu site?

Image for post
Image for post

De um modo geral, você tem duas opções.

Opção (a): você pode simplesmente armazenar todos os eventos que entram, despejá-los em um grande banco de dados, em um data warehouse ou em um cluster Hadoop. Agora, sempre que você quiser analisar esses dados de alguma forma, execute uma grande consulta SELECT nesse conjunto de dados. Por exemplo, você pode agrupar por URL e por período, filtrar por alguma condição e, em seguida, COUNT(*) para obter o número de visualizações de página para cada URL ao longo do tempo. Isso varrerá essencialmente todos os eventos, ou pelo menos algum subconjunto grande, e fará a agregação em tempo real.

Opção (b): se armazenar todos os eventos for demais para você, você poderá armazenar um resumo agregado dos eventos. Por exemplo, se você está contando coisas, você incrementa alguns contadores toda vez que um evento ocorre e depois joga fora o evento real. Você pode manter vários contadores em algo chamado cubo OLAP: imagine um cubo multidimensional, em que uma dimensão é a URL, outra dimensão é a hora do evento, outra dimensão é o navegador e assim por diante. Para cada evento, você só precisa incrementar os contadores para esse URL específico, para esse horário específico, etc.

Com um cubo OLAP, quando você deseja encontrar o número de visualizações de página para um URL específico em um dia específico, basta ler o contador dessa combinação de URL e data. Você não precisa digitalizar uma longa lista de eventos; é apenas uma questão de ler um único valor.

Image for post
Image for post

Agora, a Opção (a) pode parecer um pouco louca, mas na verdade funciona surpreendentemente bem. Acredito que o Google Analytics realmente armazena os eventos brutos, ou pelo menos uma grande amostra de eventos, e realiza uma grande varredura sobre esses eventos quando você olha os dados. Os bancos de dados analíticos modernos tornaram-se muito bons em digitalizar rapidamente grandes quantidades de dados.

A grande vantagem de armazenar dados brutos de eventos é que você tem flexibilidade máxima para análise. Por exemplo, você pode rastrear a sequência de páginas que uma pessoa visitou ao longo de sua sessão. Você não pode fazer isso se tiver agregado todos os eventos em contadores. Esse tipo de análise é realmente importante para algumas tarefas de processamento offline, como o treinamento de um sistema de recomendação (“pessoas que compraram X também compraram Y”, esse tipo de coisa). Para esses casos de uso, é melhor simplesmente manter todos os eventos brutos, para que você possa alimentá-los posteriormente em seu novo e brilhante sistema de aprendizado de máquina.

No entanto, a Opção (b) também tem seus usos, especialmente quando você precisa tomar decisões ou reagir às coisas em tempo real. Por exemplo, se você deseja impedir que as pessoas vasculhem seu site, pode introduzir uma taxa limite, para permitir apenas 100 solicitações por hora a partir de qualquer endereço IP específico; se um cliente ultrapassar o limite, você o bloqueia. Implementar isso com o armazenamento de eventos brutos seria incrivelmente ineficiente, porque você varreria continuamente seu histórico de eventos para determinar se alguém excedeu o limite. É muito mais eficiente manter apenas um contador do número de visualizações de página por endereço IP por janela de tempo e, em seguida, você pode verificar em todas as solicitações se esse número ultrapassou seu limite.

Da mesma forma, para fins de alerta, você precisa responder rapidamente ao que os eventos estão dizendo. Para negociação no mercado de ações, você também precisa ser rápido.

A linha inferior aqui é que o armazenamento de eventos brutos e os resumos agregados de eventos são muito úteis. Eles apenas têm casos de uso diferentes.

Image for post
Image for post

Vamos nos concentrar nesses resumos agregados por enquanto. Como você implementa isso?

Bem, no caso mais simples, você simplesmente precisa que o servidor da Web atualize os valores agregados diretamente. Digamos que você queira contar as visualizações de página por endereço IP por hora, para fins de criar uma taxa de limitação. Você pode manter esses contadores em algo como memcached ou Redis, que possuem uma operação de incremento atômico. Sempre que um servidor da Web processa uma solicitação, ele envia diretamente um comando de incremento, com uma chave criada a partir do endereço IP do cliente e da hora atual (truncada para a hora mais próxima).

Image for post
Image for post

Se você quiser se tornar um pouco mais sofisticado, pode introduzir um event stream, ou uma message queue ou um log de eventos ou o nome que você quiser chamar. As mensagens nesse fluxo são os registros PageViewEvent que vimos anteriormente: uma mensagem contém as propriedades de uma visualização de página específica.

O bom dessa arquitetura é que agora você pode ter vários consumidores para os mesmos dados do evento. Você pode ter um consumidor que simplesmente arquiva os eventos brutos em um grande data warehouse; mesmo se você ainda não tiver a capacidade de processar os eventos brutos, poderá armazená-los, pois o armazenamento é barato e pode ser usado no futuro. Em seguida, você pode ter outro consumidor que faz alguma agregação (por exemplo, incrementar contadores) e outro consumidor que faz outra coisa. Todos podem se alimentar do mesmo fluxo de eventos.

Image for post
Image for post

Vamos agora mudar de assunto por um momento e analisar idéias semelhantes de um campo diferente. Event Sourcing é uma idéia que surgiu da comunidade de domain-driven design — parece ser bastante conhecida entre os desenvolvedores de software corporativo, mas é totalmente desconhecida nas empresas de internet. Ele vem com uma grande quantidade de jargões que acho muito confuso, mas parece ter algumas idéias muito boas em event sourcing.

Então, vou tentar extrair essas boas idéias sem entrar em todo o jargão e veremos que há alguns paralelos surpreendentes com meu último exemplo do campo de análise de stream processing.

Image for post
Image for post

Event Sourcing se preocupa com a maneira como estruturamos os dados nos bancos de dados. Como banco de dados de exemplo, vou usar um carrinho de compras em um site de comércio eletrônico. Cada cliente pode ter um certo número de produtos diferentes no carrinho de uma só vez e, para cada item do carrinho, há uma quantidade. Nada muito complicado aqui.

Image for post
Image for post

Agora digamos que o cliente 123 atualiza seu carrinho: ao invés da quantidade 1 do produto 999, agora eles querem a quantidade 3 desse produto. Você pode imaginar isso sendo registrado no banco de dados usando uma consulta UPDATE, que corresponde à linha do cliente 123 e ao produto 999, e modifica essa linha, alterando a quantidade de 1 para 3.

Este exemplo usa um modelo de dados relacionais, mas isso realmente não importa. Com a maioria dos bancos de dados não relacionais, você faria mais ou menos a mesma coisa: substitua o valor antigo pelo novo valor quando ele mudar.

No entanto, event sourcing diz que essa não é uma boa maneira de criar bancos de dados. Ao invés disso, devemos registrar individualmente todas as alterações que acontecem no banco de dados.

Image for post
Image for post

Por exemplo, primeiro registramos um evento AddToCart quando o cliente 123 adiciona o produto 888 ao carrinho, com a quantidade 1. Registramos um evento UpdateCartQuantity separado quando alteramos a quantidade para 3. Mais tarde, o cliente muda de idéia novamente e reduz a quantidade para 2 e, finalmente, eles vão ao caixa. Cada uma dessas ações é registrada como um evento separado e anexado ao banco de dados. Você pode imaginar quer temos um registro de data e hora em todos os eventos também.

Image for post
Image for post

Quando você estrutura os dados dessa maneira, todas as alterações no carrinho de compras são um evento imutável — um fato. Mesmo se o cliente alterou a quantidade para 2, ainda é verdade que, em um momento anterior, a quantidade selecionada era 3. Se você sobrescrever dados no banco de dados, perderá essas informações históricas. Manter a lista de todas as alterações como um log de eventos imutáveis ​​fornece informações estritamente mais ricas do que se você sobrescrever coisas no banco de dados.

E essa é realmente a essência de event sourcing: em vez de executar mutações destrutivas de estado em um banco de dados ao escrevê-lo, devemos registrar cada gravação como um “comando”, como um evento imutável.

Image for post
Image for post

E isso nos leva de volta ao nosso exemplo de stream processing (Google Analytics). Lembre-se de que discutimos duas opções para armazenar dados: (a) eventos brutos ou (b) resumos agregados.

O que temos aqui com event sourcing é muito parecido. Você pode pensar nesses comandos originados por eventos (AddToCart, UpdateCartQuantity) como eventos brutos: eles compreendem o histórico do que aconteceu ao longo do tempo. Mas quando você olha para o conteúdo do seu carrinho de compras, vê o estado atual — o resultado final, que é o que você obtém quando aplica todo o histórico de eventos e os reúne em uma única coisa.

Portanto, o estado atual do carrinho pode dizer a quantidade 2. O histórico de eventos brutos dirá que, em algum momento anterior, a quantidade era 3, mas que o cliente posteriormente mudou de idéia e a atualizou para 2. O resultado final agregado apenas informa que a quantidade atual é 2.

Image for post
Image for post

Pensando nisso, é possível observar que os eventos brutos são a forma ideal para gravar os dados: todas as informações na gravação do banco de dados estão contidas em um único blob. Você não precisa atualizar cinco tabelas diferentes se estiver armazenando eventos brutos — você só precisará anexar o evento ao final de um log. Essa é a maneira mais simples e rápida de gravar em um banco de dados.

Por outro lado, os dados agregados são a forma em que é ideal ler dados do banco de dados. Se um cliente estiver visualizando o conteúdo do carrinho de compras, não estará interessado em todo o histórico de modificações que levaram ao estado atual — ele só quer saber o que está no carrinho no momento. Portanto, ao ler, você pode obter melhor desempenho se o histórico de alterações já tiver sido compactado em um único objeto que representa o estado atual.

Image for post
Image for post

Indo ainda mais longe, pense nas interfaces do usuário que levam às gravações e leituras do banco de dados. Uma gravação no banco de dados normalmente acontecia porque o usuário clicou em algum botão: por exemplo, editou alguns dados e agora clica no botão salvar. Portanto, os botões na interface do usuário correspondem a eventos brutos no histórico de fornecimento de eventos.

Por outro lado, uma leitura de banco de dados normalmente acontece porque o usuário visualiza alguma tela: clica em algum link ou abre algum documento e agora precisa ler o conteúdo. Essas leituras geralmente querem saber o estado atual do banco de dados. Portanto, as telas na interface do usuário correspondem ao estado agregado.

Essa é uma idéia bastante abstrata, então vamos ver alguns exemplos.

Image for post
Image for post

Veja o Twitter, por exemplo. A maneira mais comum de escrever no banco de dados do Twitter — ou seja, fornecer informações ao sistema do Twitter — é twittar alguma coisa. Um tweet é muito simples: consiste em algum texto, um carimbo de data e hora e o ID do usuário que tweetou. (Talvez, opcionalmente, um local, uma foto ou algo assim.) O usuário clica no botão “Tweet”, o que faz com que uma gravação no banco de dados ocorra, ou seja, um evento é gerado.

No lado da saída, a maneira como você lê no banco de dados do Twitter é visualizando sua linha do tempo. Ele mostra todas as coisas que foram escritas pelas pessoas que você segue. É uma estrutura muito mais complicada:

Image for post
Image for post

Agora para cada tweet, você não possui apenas o texto, o carimbo de data e hora e o ID do usuário, mas também o nome do usuário, a foto do perfil e outras informações que foram associadas ao tweet. Além disso, a lista de tweets foi selecionada com base nas pessoas que você segue, que podem ser alteradas.

Como você passaria da entrada simples para a saída mais complexa? Bem, você pode tentar expressá-la no SQL, algo como:

Image for post
Image for post

Ou seja, encontre todos os usuários que $user está seguindo, encontre todos os tweets que eles escreveram, ordene-os por hora e escolha os 100 mais recentes. Acontece que essa consulta realmente não escala muito bem. Você se lembra nos primeiros dias do Twitter, quando eles continuavam tendo a tela da baleia o tempo todo? Aparentemente, foi porque eles estavam usando algo como a consulta acima.

Quando um usuário visualiza sua linha do tempo, é muito caro interagir com todas as pessoas que ele está seguindo para obter os tweets desses usuários. Ao invés disso, o Twitter deve calcular a linha do tempo de um usuário com antecedência e armazená-la em cache, para que seja mais rápido quando o usuário olha para ela. Para fazer isso, eles precisam de um processo que seja traduzido do “evento otimizado para gravação” (um único tweet) para o “agregado otimizado para leitura” (uma linha do tempo). O Twitter tem esse processo e o chama de serviço de fanout.

Image for post
Image for post

Outro exemplo: pegue o Facebook. Possui muitos botões que permitem escrever algo no banco de dados do Facebook, mas um clássico é o botão “Curtir”. Quando você clica nele, isso gera um evento, um fato com uma estrutura muito simples: você (identificado pelo seu ID do usuário) gosta (um verbo de ação) de algum item (identificado pelo seu ID).

No entanto, se você olhar para o lado da saída, lendo algo no Facebook, é incrivelmente complicado. Neste exemplo, temos uma postagem no Facebook que não é apenas um texto, mas também o nome do autor e sua foto de perfil; e está me dizendo que 160.216 pessoas gostam dessa atualização, das quais três foram especialmente destacadas (presumivelmente porque o Facebook acha que, entre os que gostam dessa atualização, são esses que eu provavelmente conheço); está me dizendo que existem 6.027 compartilhamentos e 12.851 comentários, dos quais os 4 principais comentários são exibidos (claramente algum tipo de classificação de comentários está acontecendo aqui); e assim por diante.

Image for post
Image for post

Deve haver algum processo de tradução acontecendo aqui, que recebe os eventos muito simples como entrada e produz a estrutura de saída massivamente complexa e personalizada. Você não pode nem imaginar como seria a consulta ao banco de dados para buscar todas as informações nessa atualização do Facebook. Não há como eles consultarem com eficiência tudo isso em tempo real, não com mais de 100.000 curtidas. O armazenamento em cache inteligente é absolutamente essencial se você deseja criar algo assim.

Image for post
Image for post

Nos exemplos do Twitter e do Facebook, podemos ver um certo padrão: os eventos de entrada, correspondentes aos botões na interface do usuário, são bastante simples. São fatos imutáveis, podemos simplesmente armazenar todos eles, e podemos tratá-los como a fonte da verdade.

Tudo o que você pode ver em um site, ou seja, tudo o que você lê no banco de dados, pode ser derivado desses eventos brutos. Existe um processo que deriva essas agregações dos eventos brutos e que atualiza os caches quando novos eventos são recebidos, e esse processo é inteiramente determinístico. Você pode, se necessário, re-executá-lo do zero: se você alimentar o histórico de tudo o que já aconteceu no site, poderá reconstruir cada entrada de cache para estar exatamente como era antes. O banco de dados do qual você lê é apenas uma exibição em cache do log de eventos.

O lado bom dessa separação entre fonte de verdade e caches é que, em seus caches, você pode desnormalizar os dados da forma que quiser. Em bancos de dados regulares, geralmente é considerado uma boa prática normalizar dados, porque se algo mudar, você precisará alterá-lo apenas um lugar. A normalização torna as gravações rápidas e simples, mas significa que você precisa fazer mais trabalho (JOIN) no tempo de leitura.

Para acelerar as leituras, você pode desnormalizar os dados, ou seja, duplicar as informações em vários lugares para que possam ser lidas mais rapidamente. O problema agora é que, se os dados originais forem alterados, todos os locais onde você os copiou também precisarão ser alterados. Em um banco de dados típico, isso é um pesadelo, porque talvez você não conheça todos os lugares em que algo foi copiado. Mas se seus caches são construídos a partir de seus eventos brutos usando um processo repetitivo, você tem muito mais liberdade para desnormalizar, porque sabe quais dados estão fluindo para onde.

Image for post
Image for post

Outro exemplo: Wikipedia. Isso é quase um contra-exemplo para o Twitter e o Facebook, porque na Wikipedia a entrada e a saída são quase as mesmas.

Image for post
Image for post

Quando você edita uma página na Wikipedia, você obtém um grande campo de texto contendo todo o conteúdo da página (usando a marcação wiki) e, quando você clica no botão Salvar, ele envia todo o conteúdo da página de volta ao servidor. O servidor substitui a página inteira pelo que você postou nela. Quando alguém visualiza a página, ele retorna o mesmo conteúdo de volta ao usuário (formatado em HTML).

Portanto, neste caso, a entrada e a saída são as mesmas. O que event sourcingsignificaria neste caso? Talvez faria sentido representar um evento de gravação como um diff, como um arquivo de correção, ao invés de uma cópia da página inteira? É um caso interessante para se pensar. Por exemplo, o Google Docs funciona aplicando diferenças contínuas na granularidade de caracteres — efetivamente um evento por pressionamento de tecla.

Image for post
Image for post

Exemplo final: LinkedIn. Digamos que você atualize seu perfil do LinkedIn e adicione seu trabalho atual, que consiste em um cargo, uma empresa e algum texto. Novamente, o evento de edição para gravar no banco de dados é muito simples.

Existem várias maneiras de como você pode ler esses dados e, neste exemplo, usamos o recurso de pesquisa. No lado da saída, uma maneira de ler o banco de dados do LinkedIn é digitando algumas palavras-chave e talvez o nome de uma empresa em uma caixa de pesquisa, e encontre todas as pessoas que atendem a esses critérios.

Image for post
Image for post

Como isso é implementado? Bem, para pesquisar, você precisa de um índice de texto completo, que é essencialmente um grande dicionário — para cada palavra-chave, ele indica os IDs de todos os perfis que contêm a palavra-chave. Esse índice de pesquisa é outra estrutura agregada e, sempre que alguns dados são gravados no banco de dados, essa estrutura precisa ser atualizada com os novos dados.

Por exemplo, se eu adicionar meu trabalho “Autor na O’Reilly” ao meu perfil, o índice de pesquisa deverá agora ser atualizado para incluir meu ID de perfil nas entradas de “autor” e “o’reilly”. O índice de pesquisa é apenas outro tipo de cache. Ele também precisa ser construído a partir da fonte da verdade (todas as edições de perfil que já ocorreram) e precisa ser atualizado sempre que um novo evento ocorrer (alguém edita seu perfil).

Image for post
Image for post

Agora, voltando ao stream processing. Descrevi primeiro como você pode criar algo como o Google Analytics e comparei o armazenamento bruto de eventos de exibição de página versus contadores agregados e discuti como você pode manter esses agregados consumindo um fluxo de eventos.

Image for post
Image for post

Expliquei event sourcing, que aplica uma abordagem semelhante aos bancos de dados: trate todas as gravações do banco de dados como um fluxo de eventos e construa agregados (visualizações, caches, índices de pesquisa) desse fluxo. Depois de ter esse fluxo de eventos, você pode fazer muitas coisas excelentes com ele:

  • Você pode pegar todos os eventos brutos, talvez transformá-los um pouco, e carregá-los em um grande data warehouse, onde os analistas podem consultar os dados de acordo com o conteúdo de seus interesses.
  • Você pode atualizar os índices de pesquisa de texto completo, para que, quando um usuário acessar a caixa de pesquisa, ele pesquise uma versão atualizada dos dados.
  • Você pode invalidar ou refazer qualquer cache, para que as leituras possam ser feitas em caches rápidos, além de garantir que os dados no cache permaneçam atualizados.
  • E, finalmente, você pode até pegar um fluxo de eventos e processá-lo de alguma forma (talvez juntando outros fluxos) para criar um novo fluxo de saída. Dessa forma, você pode conectar a saída de um sistema na entrada de outro sistema. Essa é uma maneira muito poderosa de criar aplicativos complexos de forma limpa.
Image for post
Image for post

Usando uma abordagem semelhante event sourcing para bancos de dados é uma grande mudança em relação à maneira como os bancos de dados são tradicionalmente usados ​​(onde você pode atualizar e excluir dados à vontade). Por que você gostaria de fazer todo esse esforço para mudar a maneira de fazer as coisas? Qual é o benefício de usar append-only streams de eventos imutáveis?

Vários motivos:

  • ‌Acoplamento Fraco (Loose Coupling): Se você gravar dados no banco de dados no mesmo esquema usado para a leitura, há um forte acoplamento entre a parte do aplicativo que está escrevendo (o “botão”) e a parte que faz a leitura (a “tela”). Sabemos que o acoplamento fraco é um bom princípio de design de software. Ao separar o formulário no qual você escreve e lê dados, e ao converter explicitamente de um para o outro, você obtém um acoplamento muito mais flexível entre diferentes partes do aplicativo.
  • ‌Desempenho de Leitura e Escrita: O debate de décadas sobre normalização (gravações mais rápidas) versus desnormalização (leituras mais rápidas) existe apenas devido à suposição de que gravações e leituras usam o mesmo esquema. Se você separar os dois, poderá ter gravações e leituras rápidas.
  • ‌Event Streams são ótimos para escalabilidade, porque são uma abstração simples (comparativamente fácil de paralelizar e dimensionar em várias máquinas) e porque permitem decompor seu aplicativo em produtores e consumidores de streams (que podem operar de forma independente e tirar proveito de mais paralelismo em hardware).
  • ‌Flexibilidade e Agilidade: Eventos brutos são tão simples e óbvios que uma “migração de esquema” não faz muito sentido (você pode simplesmente adicionar um novo campo de tempos em tempos, mas geralmente não é necessário reescrever dados históricos em um novo formato). Por outro lado, as maneiras pelas quais você deseja apresentar dados aos usuários são muito mais complexas e podem estar mudando continuamente. Se você tiver um processo de conversão explícito entre a fonte da verdade e os caches dos quais você lê, poderá experimentar novas interfaces de usuário criando novos caches usando nova lógica, executando o novo sistema em paralelo com o antigo, movendo gradualmente as pessoas do sistema antigo e depois descartando o sistema antigo (ou revertendo para o sistema antigo se o novo não funcionar). Essa flexibilidade é incrivelmente libertadora.
  • Finalmente, os cenários de erros são muito mais fáceis de entender se os dados são imutáveis. Se algo der errado no seu sistema, você sempre poderá reproduzir eventos na mesma ordem e reconstruir exatamente o que aconteceu (especialmente importante em finanças, onde a auditoria é crucial). Se você implementar um código bugado que grava dados incorretos em um banco de dados, basta executá-lo novamente após corrigir o erro e, assim, corrigir as saídas. Essas coisas não são possíveis se as gravações do banco de dados forem destrutivas.
Image for post
Image for post

Finalmente, vamos falar sobre como você pode colocar essas idéias em prática.

Devo salientar que, na realidade, as gravações de banco de dados geralmente já possuem uma certa qualidade imutável, semelhante a um evento. O log de gravação antecipada que existe na maioria dos bancos de dados é essencialmente um fluxo de gravações de eventos, embora seja muito específico para um banco de dados específico. O mecanismo MVCC em bancos de dados como PostgreSQL, MySQL InnoDB e Oracle, e as append-only B-trees do CouchDB, Datomic e LMDB são exemplos do mesmo pensamento: é melhor estruturar gravações como um append-only log do que executar substituições destrutivas.

No entanto, aqui não estamos falando sobre as partes internas dos mecanismos de banco de dados, mas sobre o uso de event streams (fluxos de eventos) no nível do aplicativo.

Image for post
Image for post

Alguns bancos de dados, como o Event Store, se orientaram especificamente no modelo de event sourcing, e algumas pessoas implementaram o event sourcingsobre os bancos de dados relacionais. Essas podem ser soluções viáveis ​​se você estiver operando em pequena escala.

Os sistemas com os quais trabalhei mais são o Apache Kafka e o Apache Samza. Eles são projetos de código aberto originados no LinkedIn e agora têm uma grande comunidade ao seu redor. O Kafka é um intermediário de mensagens (‌message broker), como uma fila de mensagens de publicação/assinatura (‌publish-subscribe message queue), que suporta ‌event streams (fluxos de eventos) com muitos milhões de mensagens por segundo, armazenados de forma durável no disco e replicados em várias máquinas.

Samza é a contrapartida de processamento do Kafka: um framework que permite escrever código para consumir fluxos de entrada (input streams) e produzir fluxos de saída (output streams), além de lidar com coisas como implantar seu código em um cluster e recuperar-se de falhas.

Image for post
Image for post

Definitivamente, eu recomendaria o Kafka como um sistema para event streamsconfiáveis ​​de alto rendimento. No lado do processamento, existem algumas opções: Samza, Storm e Spark Streaming são os frameworks de stream processing(processamento de fluxo) mais populares. Todos eles permitem que você execute seu código de stream processing de modo distribuído em várias máquinas.

Existem diferenças de design interessantes (prós e contras) entre essas três estruturas, que não tenho tempo para abordar aqui. Você pode ler uma comparação detalhada entre eles na documentação do Samza. E sim, também acho engraçado que todos eles comecem com a letra S.

Os sistemas de stream processing distribuídos de hoje em dia saíram de empresas de internet (Samza do LinkedIn, Storm do /Twitter). Indiscutivelmente, eles têm suas raízes na pesquisa de stream processing do início dos anos 2000 (TelegraphCQ, Borealis, etc), com suas origens de fundo de banco de dados relacional. Assim como os banco de dados NoSQL reduziram os bancos de dados a um conjunto mínimo de recursos, os modernos sistemas de stream processingparecem bastante simplificados em comparação com a pesquisa anterior.

Image for post
Image for post

Os frameworks modernos de stream processing (Samza, Storm, Spark Streaming) preocupam-se principalmente com questões de baixo nível: como escalar o processamento em várias máquinas, como implantar um trabalho em um cluster, como lidar com falhas (crashes, falhas de máquina, interrupções de rede) e como obter um desempenho confiável em um ambiente multi-tenant. As APIs que eles fornecem são de nível bastante baixo (por exemplo, um retorno de chamada que é chamado para cada mensagens). Eles se parecem muito mais com o MapReduce e menos com um banco de dados. Eles estão mais interessados ​​em operações confiáveis do que em recursos sofisticados.

Por outro lado, também há algum trabalho em linguagens de consulta de alto nível para stream processing, e o Complex Event Processing é especialmente digno de menção. Originou-se na pesquisa da década de 90 sobre event-driven simulation, e a maioria dos produtos CEP são softwares corporativos comerciais e caros (apenas do Esper ser de código aberto; “aberto” e está limitado à execução em uma única máquina).

Com o CEP, você escreve consultas ou regras que correspondem a determinados padrões nos eventos. Eles são comparáveis ​​às consultas SQL (que descrevem quais resultados você deseja retornar de um banco de dados), exceto que o mecanismo CEP pesquisa continuamente no stream por conjuntos de eventos que correspondem à consulta e notifica você (gerando um “evento complexo”) sempre que uma encontrada corresponder ao padrão. Isso é útil para detecção de fraudes ou monitoramento de processos de negócios, por exemplo.

Para casos de uso que podem ser facilmente descritos em termos de uma linguagem de consulta CEP, uma linguagem de alto nível é muito mais conveniente do que uma API de processamento de eventos de baixo nível. Por outro lado, uma API de baixo nível oferece mais liberdade, permitindo que você faça uma variedade maior de coisas do que uma linguagem de consulta permitiria. Além disso, concentrando seus esforços na escalabilidade e na tolerância a falhas, frameworks de stream processing fornecem uma base sólida sobre a qual as linguagens de consulta podem ser construídas.

Uma ideia relacionada é fazer uma pesquisa de texto completo nos streams, onde você registra uma consulta de pesquisa com antecedência e é notificado sempre que um evento corresponde à sua consulta. Fizemos um trabalho experimental com Luwak nesta área, mas ainda é muito novo.

Image for post
Image for post

Por fim, existem muitas outras coisas relacionadas ao stream processing. Indo para o resumo em modo “lista de compras”:

  • ‌Frameworks de Atores (Actor Frameworks): como Akka, Orleans e Erlang OTP também são baseadas em streams de eventos imutáveis. No entanto, eles são principalmente um mecanismo para simultaneidade, e não um mecanismo para gerenciamento de dados. Alguns desses frameworks possuem um componente distribuído, portanto, você pode criar uma estrutura de stream processing distribuído sobre os atores. No entanto, vale a pena examinar atentamente as garantias de tolerância a falhas e os modos de falha desses sistemas: muitos não oferecem durabilidade, por exemplo.
  • muita agitação em torno de “reativo”, que parece abranger um conjunto de idéias pouco definidas. Minha impressão é que há um bom trabalho acontecendo nas linguagens de fluxo de dados (dataflow languages) e na programação reativa funcional (FRP), onde eu vejo principalmente sobre como trazer event streams (fluxos de eventos) para a interface do usuário, ou seja, atualizar a interface do usuário quando alguns dados subjacentes são alterados. Essa é uma contrapartida natural dos event streams de dados do back-end.
  • Por fim, captura de dados de alteração (change data capture — CDC) significa usar um banco de dados existente da maneira familiar, mas extrair inserções, atualizações e exclusões em um fluxo de eventos de alteração de dados que outros aplicativos possam consumir. Esse é um ótimo caminho de migração para uma arquitetura orientada a streams, e falarei e escreverei mais sobre o CDC no futuro.

Espero que essa palestra tenha ajudado você a entender as muitas facetas do stream processing!

Martin Kleppmann é um engenheiro de software e empresário. Ele é um colaborador do Apache Samza e autor do próximo livro da O’Reilly, Designing Data-Intensive Applications. Ele é @martinkl no Twitter.

Créditos

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store