A nova estrutura de dados do Redis 5, chamada de fluxos, despertou grande interesse na comunidade. De alguma forma, conversarei com quem usa fluxos na produção e escreverei sobre isso. Mas agora eu quero considerar um tópico um pouco diferente. Parece-me que muitas pessoas pensam nos fluxos como uma espécie de ferramenta surreal para resolver tarefas terrivelmente difíceis. De fato, essa estrutura de dados * também * fornece mensagens, mas será uma simplificação incrível supor que a funcionalidade do Redis Streams seja limitada apenas por isso.
Os fluxos são um excelente modelo e "modelo mental" que podem ser usados com grande sucesso no design do sistema, mas, na realidade, os fluxos, como a maioria das estruturas de dados Redis, são uma estrutura mais geral e podem ser usados para várias outras tarefas. Neste artigo, apresentaremos os fluxos como uma estrutura de dados pura, ignorando completamente as operações de bloqueio, grupos de destinatários e todas as outras funcionalidades de mensagens.
Streams - este é CSV em esteróides
Se você deseja registrar vários elementos de dados estruturados e considerar que o banco de dados será um excesso aqui, basta abrir o arquivo no modo
append only
e escrever cada linha como um CSV (valor separado por vírgula):
(open data.csv in append only) time=1553096724033,cpu_temp=23.4,load=2.3 time=1553096725029,cpu_temp=23.2,load=2.1
Parece simples. As pessoas já fizeram isso há muito tempo e ainda o fazem: é um modelo confiável, se você sabe o que é o quê. Mas qual será o equivalente na memória? Na memória, o processamento de dados muito mais avançado se torna possível e muitas restrições de arquivos CSV são removidas automaticamente, como:
- É difícil (ineficiente) atender às solicitações de intervalo.
- Muitas informações redundantes: cada registro tem quase o mesmo tempo e os campos são duplicados. Ao mesmo tempo, a exclusão de dados tornará o formato menos flexível se eu quiser mudar para um conjunto diferente de campos.
- As compensações de elementos são simplesmente compensações de bytes no arquivo: se alterarmos a estrutura do arquivo, as compensações ficarão erradas, portanto não há um conceito real de um identificador primário. As entradas em essência não podem ser apresentadas sem ambiguidade.
- Sem a capacidade de coletar lixo e sem reescrever o log, você não pode excluir entradas, mas apenas marcá-las como inválidas. Reescrever logs geralmente é péssimo por vários motivos, é aconselhável evitá-lo.
Ao mesmo tempo, esse registro CSV é bom à sua maneira: não há estrutura fixa, os campos podem mudar, é trivial gerá-lo e é bastante compacto. A idéia com os fluxos Redis era preservar virtudes, mas superar limitações. O resultado é uma estrutura de dados híbrida muito semelhante aos conjuntos classificados Redis: eles * se parecem com * a estrutura de dados fundamental, mas usam várias representações internas para obter esse efeito.
Introdução aos threads (você pode pular se já estiver familiarizado com o básico)
Os fluxos Redis são representados como nós de macro compactados em delta conectados por uma árvore base. Como resultado, você pode procurar rapidamente registros aleatórios, obter intervalos, excluir elementos antigos, etc. Ao mesmo tempo, a interface do programador é muito semelhante a um arquivo CSV:
> XADD mystream * cpu-temp 23.4 load 2.3 "1553097561402-0" > XADD mystream * cpu-temp 23.2 load 2.1 "1553097568315-0"
Como você pode ver no exemplo, o comando XADD gera e retorna automaticamente o identificador do registro, que aumenta monotonicamente e consiste em duas partes: <time> - <counter>. Tempo em milissegundos e o contador é incrementado para registros com o mesmo tempo.
Portanto, a primeira nova abstração para a idéia de um arquivo CSV no modo
append only
é usar o asterisco como o argumento de ID para o XADD: é assim que obtemos o identificador de registro do servidor gratuitamente. Esse identificador é útil não apenas para indicar um elemento específico no fluxo, mas também está associado à hora em que o registro foi adicionado ao fluxo. De fato, com o XRANGE, você pode executar consultas de intervalo ou recuperar elementos individuais:
> XRANGE mystream 1553097561402-0 1553097561402-0 1) 1) "1553097561402-0" 2) 1) "cpu-temp" 2) "23.4" 3) "load" 4) "2.3"
Nesse caso, usei o mesmo ID para iniciar e finalizar o intervalo para identificar um item. No entanto, eu posso usar qualquer argumento range e COUNT para limitar o número de resultados. Da mesma forma, não há necessidade de especificar identificadores completos para um intervalo, posso simplesmente usar apenas o tempo unix para obter elementos em um determinado intervalo de tempo:
> XRANGE mystream 1553097560000 1553097570000 1) 1) "1553097561402-0" 2) 1) "cpu-temp" 2) "23.4" 3) "load" 4) "2.3" 2) 1) "1553097568315-0" 2) 1) "cpu-temp" 2) "23.2" 3) "load" 4) "2.1"
No momento, não há necessidade de mostrar outros recursos da API, há documentação para isso. Por enquanto, vamos nos concentrar neste padrão de uso: XADD para adicionar, XRANGE (e também XREAD) para extrair intervalos (dependendo do que você deseja fazer) e vamos ver por que os fluxos são tão poderosos que os chamam de estruturas de dados.
Se você quiser saber mais sobre fluxos e APIs, leia o
tutorial .
Tenistas
Alguns dias atrás, um amigo meu que começou a estudar Redis e eu simulamos um aplicativo para rastrear quadras de tênis, jogadores e partidas locais. A maneira de modelar jogadores é óbvia, o jogador é um objeto pequeno, por isso precisamos apenas de um hash com teclas como o
player:<id>
. Então você perceberá imediatamente que precisa de uma maneira de acompanhar jogos em clubes de tênis específicos. Se o
player:1
e o
player:2
jogaram entre si e o
player:1
venceu, podemos enviar o seguinte registro para o stream:
> XADD club:1234.matches * player-a 1 player-b 2 winner 1 "1553254144387-0"
Uma operação tão simples nos dá:
- Identificador de correspondência exclusivo: ID no fluxo.
- Não há necessidade de criar um objeto para identificação de correspondência.
- Solicitações de intervalo livre para correspondências de paginação ou assistindo correspondências para uma data e hora específicas.
Antes que os fluxos aparecessem, teríamos que criar um conjunto classificado por tempo: os elementos do conjunto classificado seriam identificadores de correspondência, que são armazenados em uma chave diferente como um valor de hash. Não é apenas mais trabalho, mas também mais memória. Muito, muito mais memória (veja abaixo).
Agora, nosso objetivo é mostrar que os fluxos Redis são uma espécie de conjunto classificado no modo
append only
, com chaves por tempo, em que cada elemento é um pequeno hash. E, por sua simplicidade, é uma verdadeira revolução no contexto da modelagem.
A memória
O caso de uso acima não é apenas um padrão de programação mais coeso. O consumo de memória nos encadeamentos é tão diferente da abordagem antiga com um conjunto ordenado + hash para cada objeto que agora começam a funcionar algumas coisas que antes eram impossíveis de implementar.
Aqui estão as estatísticas sobre a quantidade de memória para armazenar um milhão de correspondências na configuração apresentada anteriormente:
+ = 220 (242 RSS) = 16,8 (18.11 RSS)
A diferença é mais do que uma ordem de magnitude (13 vezes). Isso significa poder trabalhar com tarefas que antes eram muito caras para serem executadas na memória. Agora eles são bastante viáveis. A mágica é introduzir fluxos Redis: os nós de macro podem conter vários elementos que são codificados de maneira muito compacta em uma estrutura de dados chamada listpack. Essa estrutura cuidará, por exemplo, da codificação de números inteiros na forma binária, mesmo que sejam cadeias semanticamente. Além disso, aplicamos a compactação delta e compactamos os mesmos campos. No entanto, ainda é possível pesquisar por ID ou hora, porque esses nós de macro estão vinculados em uma árvore base, que também é projetada com otimização de memória. Juntos, isso explica o uso econômico da memória, mas a parte interessante é que, semanticamente, o usuário não vê nenhum detalhe de implementação que torne os threads tão eficientes.
Agora vamos contar. Se eu posso armazenar 1 milhão de registros em aproximadamente 18 MB de memória, posso armazenar 10 milhões em 180 MB e 100 milhões em 1,8 GB. Com apenas 18 GB de memória, posso ter 1 bilhão de itens.
Séries temporais
É importante observar que o exemplo acima com partidas de tênis é semanticamente * muito diferente * do uso de fluxos Redis para séries temporais. Sim, logicamente ainda estamos registrando algum tipo de evento, mas há uma diferença fundamental. No primeiro caso, registramos e criamos registros para renderizar objetos. E nas séries temporais, simplesmente medimos algo que acontece fora, que na verdade não representa o objeto. Você pode dizer que essa distinção é trivial, mas não é. É importante entender a idéia de que os threads Redis podem ser usados para criar objetos pequenos com uma ordem comum e atribuir identificadores a esses objetos.
Mas mesmo a maneira mais simples de usar séries temporais é obviamente uma grande inovação, porque antes do advento dos threads, Redis era praticamente impotente para fazer qualquer coisa aqui. Características de memória e flexibilidade de fluxos, bem como a capacidade de limitar fluxos limitados (consulte os parâmetros XADD) são ferramentas muito importantes nas mãos do desenvolvedor.
Conclusões
Os fluxos são flexíveis e oferecem muitos casos de uso, mas eu queria escrever um artigo muito curto para mostrar claramente exemplos e consumo de memória. Talvez para muitos leitores esse uso de threads fosse óbvio. No entanto, conversas com desenvolvedores nos últimos meses me deixaram com a impressão de que muitos têm uma forte associação entre fluxos e dados de streaming, como se a estrutura de dados fosse boa apenas lá. Isto não é verdade. :-)