DEV Community

Jhony Walker
Jhony Walker

Posted on • Updated on

Amazon DynamoDB - O banco NoSQL da AWS

O DynamoDB oferece muitos benefícios que outros bancos de dados não oferecem, como um modelo de preço flexível, um modelo de conexão sem estado que funciona perfeitamente com computação serverless e tempo de resposta consistente, mesmo quando o banco de dados aumenta para um tamanho enorme.

DynamoDB icon

No entanto, a modelagem de dados com o DynamoDB é complicada para aqueles acostumados aos bancos de dados relacionais que dominaram nossa área nas últimas décadas. Existem várias peculiaridades em torno da modelagem de dados com o DynamoDB, mas a maior delas é a recomendação da AWS de usar uma única tabela para todos os seus registros.

O que é design de tabela única

Mas afinal o que é design de tabela única? Para entender, vamos ver rapidamente o histórico dos bancos de dados. Examinando algumas modelagens básicas em bancos de dados relacionais e depois veremos por que você precisa modelar de forma diferente no DynamoDB. Com isso, veremos o principal motivo do uso do design de tabela única.

Informações básicas sobre modelagem e junções SQL

Vamos começar com o banco de dados relacional.
Com bancos de dados relacionais, você normalmente normaliza seus dados criando uma tabela para cada tipo de entidade em seu aplicativo. Por exemplo, se você estiver criando um aplicativo de comércio eletrônico, terá uma tabela para Clientes e uma tabela para Pedidos.

Tabelas para clientes

Cada pedido pertence a um determinado cliente e você usa chaves estrangeiras para se referir a um registro em uma tabela a um registro em outra. Essas chaves estrangeiras atuam como ponteiros — se eu precisar de mais informações sobre um Cliente que fez um pedido específico, posso seguir a referência de chave estrangeira para recuperar itens sobre o Cliente.

Chave Estrangeira

Para seguir esses ponteiros, a linguagem SQL, para consultar banco de dados relacionais, tem o conceito de junções (JOIN). As junções permitem combinar registros de duas ou mais tabelas no tempo de leitura.

O problema de falta de junções no DynamoDB

Embora conveniente, as junções SQL também são caras. Elas exigem a varredura de grandes porções de várias tabelas no banco de dados relacional, a comparação de valores diferentes e o retorno de um conjunto de resultados.

O DynamoDB foi desenvolvido para casos de uso enormes e de alta velocidade, como o carrinho de compras da Amazon.com. Esses casos de uso não podem tolerar a inconsistência e o desempenho lento das junções à medida que um conjunto de dados é escalado.

O DynamoDB se guarda muito bem de operações que não irão escalar e não há uma ótima maneira de escalar junções relacionais. Ao invés de trabalhar para melhorar a escala das junções, o DynamoDB evita o problema, removendo a capacidade de usar junções.

Mas como desenvolvedor de aplicativos, você ainda precisa de alguns dos benefícios das junções relacionais. E um dos grandes benefícios das junções é a capacidade de obter vários itens heterogêneos do seu banco de dados em uma única solicitação.

No exemplo acima, queremos obter um registro do cliente e todos os pedidos do cliente. Muitos desenvolvedores aplicam padrões de design relacional com o DynamoDB, mesmo que eles não tenham as ferramentas relacionais, como a operação de junção. Isso significa que eles colocam seus itens em tabelas diferentes, de acordo com o tipo. No entanto, como não há junções no DynamoDB, eles precisam fazer várias solicitações em série para buscar os registros Pedidos e Cliente.

Roadmap

Isso pode se tornar um grande problema em seu aplicativo. O I/O de rede é provavelmente a parte mais lenta do seu aplicativo e você está fazendo várias solicitações de rede em cascata, onde uma solicitação fornece dados que são usados ​​para solicitações subsequentes. À medida que o aplicativo é escalado, esse padrão fica cada vez mais lento.

A solução: pré-junte seus dados nas coleções de itens

Então, como você obtém um desempenho rápido e consistente do DynamoDB sem fazer várias solicitações ao seu banco de dados? Pré-juntando seus dados usando coleções de itens.

Uma coleção de itens no DynamoDB refere-se a todos os itens em uma tabela ou índice que compartilham uma chave de partição. No exemplo abaixo, temos uma tabela do DynamoDB que contém atores e os filmes nos quais eles foram exibidos. A chave primária é uma chave primária composta em que a chave da partição é o nome do ator e a chave de classificação é o nome do filme.

Filmes

Você pode ver que existem dois itens para Tom Hanks — Cast Away e Toy Story. Como eles têm a mesma chave de partição Tom Hanks, eles estão na mesma coleção de itens.

Você pode usar a operação Query da API do DynamoDB para ler vários itens com a mesma chave de partição. Portanto, se você precisar recuperar vários itens heterogêneos em uma única solicitação, organize esses itens para que eles estejam na mesma coleção de itens.

Vejamos um breve exemplo de um aplicativo de comércio eletrônico que envolve usuários e pedidos (semelhante ao que apresentei aqui), temos um padrão de acesso no qual queremos buscar o registro do usuário e os registros do pedido. Para tornar isso possível em uma única solicitação, garantimos que todos os registros de pedidos estejam na mesma coleção de itens que o registro de usuário ao qual eles pertencem.

Pedidos

Agora, quando queremos buscar o usuário e os pedidos, podemos fazer isso em uma única solicitação sem precisar de uma operação custosa de junção:

Tabela única

É disso que se trata o design de tabela única — ajustando sua tabela para que seus padrões de acesso possam ser tratados com o menor número possível de solicitações ao DynamoDB, idealmente uma.
E como tudo fica melhor em citações sofisticadas, digamos mais uma vez:

“ O principal motivo para usar tabela única no DynamoDB é recuperar vários tipos de itens heterogêneos usando uma única solicitação. ”

Outros benefícios do design de tabela única

Embora a redução do número de solicitações de um padrão de acesso seja o principal motivo para o uso de um design de tabela única com o DynamoDB, também existem outros benefícios. Vou discutir isso brevemente.

Primeiro, há alguma sobrecarga operacional em cada tabela que você possui no DynamoDB. Embora o DynamoDB seja totalmente gerenciado e muito prático em comparação com um banco de dados relacional, você ainda precisa configurar alarmes, monitorar métricas etc. Se você tiver uma tabela com todos os itens ao invés de oito tabelas separadas, você reduz o número de alarmes e métricas a serem observados.

Segundo, ter uma única tabela pode economizar dinheiro em comparação a ter várias tavelas. Com cada tabela que você possui, é necessário provisionar unidades de capacidade de leitura e gravação. Freqüentemente, você fará algumas contas na ponta do lápis para ver o tráfego que você espera, aumentará em X% e os converterá em RCUs e WCUs. Se você tiver um ou dois tipos de entidade em sua tabela única que são acessados ​​com muito mais frequência do que os outros, você poderá alocar parte da capacidade extra de itens acessados ​​com menos frequência no buffer para os outros itens.

Embora esses dois benefícios sejam reais, eles são bastante marginais. A responsabilidade das operações no DynamoDB é bastante baixo e os preços economizarão apenas um pouco de dinheiro nas margens. Além disso, se você estiver usando o DynamoDB On-Demand, você não vai poupar dinheiro indo a um projeto multi-tabela. Em geral, ao pensar no design de tabela única, você deve considerar o principal benefício como a melhoria do desempenho, fazendo uma única solicitação para recuperar todos os itens necessários.

Desvantagens de um design de tabela única

Desvantagens de um design de tabela única
Embora o padrão de tabela única seja poderoso e ridiculamente escalável, ele não vem sem custos. Nesta seção, analisaremos algumas das desvantagens de um design de tabela única.
Na minha opinião, há três desvantagens do design de tabela única no DynamoDB:

  • A curva de aprendizado íngreme para entender o design de tabela única;
  • A inflexibilidade de adicionar novos padrões de acesso;
  • A dificuldade de exportar suas tabelas para análise.

A curva de aprendizado íngreme do design de tabela única

A maior reclamação de quem começa a estudar sobre é a dificuldade de aprender o design de tabela única no DynamoDB. Uma única tabela do DynamoDB sobrecarregada parece realmente estranha em comparação com as tabelas limpas e normalizadas do seu banco de dados relacional. É difícil desaprender todas as lições que você aprendeu ao longo de anos de modelagem de dados relacionais.

O desenvolvimento de software é uma jornada contínua de aprendizado, e você não pode usar a dificuldade de aprender coisas novas como uma desculpa para usar mal uma coisa nova, você deve entender absolutamente os princípios por trás do design de tabela única antes de tomar a decisão de utilizar ou não. A ignorância não é uma razão para evitar as melhores práticas.

A inflexibilidade de novos padrões de acesso

Uma segunda reclamação sobre o DynamoDB é a dificuldade de acomodar novos padrões de acesso em um design de tabela única.

Ao modelar um design de tabela única no DynamoDB, você começa primeiro com seus padrões de acesso. Pense bem (e anote!) Como você acessará seus dados e modele cuidadosamente sua tabela para satisfazer esses padrões de acesso. Ao fazer isso, você organizará seus itens em coleções para que cada padrão de acesso possa ser tratado com o menor número possível de solicitações — idealmente, uma única solicitação.

Depois de modelar sua tabela, você a coloca em ação e escreve o código para implementá-la. E, feito corretamente, isso funcionará muito bem! Seu aplicativo poderá escalar infinitamente, sem degradação no desempenho.

No entanto, o design da sua tabela é estritamente adaptado para a finalidade exata para a qual foi projetado. Se seus padrões de acesso mudarem porque você está adicionando novos objetos ou acessando vários objetos de maneiras diferentes, pode ser necessário executar um processo ETL (Extract, Transform, Load) para varrer todos os itens da sua tabela e atualizar com novos atributos. Esse processo não é impossível, mas adiciona atrito ao seu processo de desenvolvimento.

A dificuldade da análise

O DynamoDB foi projetado para casos de uso OLTP — acesso a dados com alta rapidez e alta velocidade, onde você está operando em alguns registros por vez. Mas os usuários também precisam de padrões de acesso OLAP — consultas grandes e analíticas em todo o conjunto de dados para encontrar itens populares, número de pedidos por dia ou outras informações.

O DynamoDB não é bom em consultas OLAP. Isso é intencional. O DynamoDB se concentra em ter um desempenho super alto nas consultas OLTP e deseja que você use outros bancos de dados criados especificamente para OLAP. Para fazer isso, você precisará enviar seus dados do DynamoDB em outro sistema.

Se você tiver um design de tabela única, colocá-lo no formato adequado para um sistema de análise pode ser complicado. Você desnormalizou seus dados e os transformou em um pretzel projetado para lidar com seus casos de uso exatos. Agora você precisa desenrolar essa tabela e normalizá-la novamente para que seja útil para análises.

Tem uma citação sobre isso quem vem em conjunto com uma excelente explicação de Forrest Brazeal sobre o design de tabela única:

“ Uma tabela única e otimizada do DynamoDB parece mais com código de máquina do que com uma simples planilha ”

As planilhas são fáceis para análise, enquanto um design de tabela única exige algum trabalho para desenrolar. O trabalho de infraestrutura de dados precisará ser aprimorado em seu processo de desenvolvimento para garantir que você possa reconstituir sua tabela de maneira amigável para análise.

Entrando a fundo do DynamoDB

DynamoDB é um banco NoSQL totalmente gerenciado pela AmazonWS, de forma que não precisamos criar a infraestrutura para tal, como faríamos utilizando o Node.js ou o Cassandra. Ele é orientado a Documento ou Chave-valor. Melhor dizendo é um banco Chave-valor que suporta o tipo de dado Documento, onde você pode armazenar um JSON. É rápido, consistente, possui controle de acesso e event driven programming, ou seja, as alterações nele disparam eventos que podem ser observados e utilizados em algum serverless como Lambda.

Para dimensionar e cobrar, a Amazon utiliza CUs, que são informadas no momento que a tabela é criada. Existem dois tipo de CUs:

  • Read Capacity Unit ou RCU — Capacidade de leitura
    Representa uma leitura consistente ou duas leituras eventualmente consistente por segundo, para um item de 4 KB, caso seja maior o DynamoDB irá consumir mais.

  • Write Capacity Unit ou WCU — Capacidade de escrita
    Representa uma escrita por segundo, para um item de 1 KB, caso seja maior o DynamoDB irá consumir mais.

Agora entendendo mais sobre sua estrutura:

Assim como nos bancos relacionais criamos uma tabela que possui itens que possuem n atributos. Você pode ter n itens em uma tabela, porém cada item pode ter no máximo 400 KB.

O único atributo obrigatório é a Partition Key, semelhante a chave primária de um banco relacional. Opcionalmente também podemos criar um segundo atributo obrigatório Sort Key, que nos permite ter um retorno maior e mais rápido dos dados ordenados e criar queries ricas em cima desse atributo, como <, >, ==, in, between, entre outros.

Quando Partition Key e Sort Key são combinadas, é criada uma chave composta, onde a Partition Key pode ser repetida desde que a Sort Key seja diferente. Tendo como exemplo uma tabela de música, onde temos o artista como Partition key e o nome da música como Sort Key, você pode recuperar uma música imediatamente se fornecer os dois valores. Essa combinação ainda trás mais flexibilidade ao executar uma query. Você pode por exemplo buscar apenas pelo artista e já recuperar toda as músicas dele. Partitions Keys são identificadores únicos e são usados para criar um hash aleatório e permite que a tabela seja particionada para escalar.

Sobre o banco é importante sabermos que:

  • Cada tabela possui 3 réplicas
  • O commit só é confirmado após o dado ter sido gravado em 2 réplicas
  • As leituras são eventualmente consistentes. Caso você queira uma leitura consistente é necessário informar na hora da consulta. Então ao invés da consulta ser feita em uma replica será feita em duas, utilizando 2 CUs ao invés de 1.

Existem dois tipos de index disponíveis onde podemos ter 5 de cada para uma tabela, são eles:

Local Secondary Index (LSIs)

  • Partition key é sempre a mesma
  • Você pode criar Sort Keys alternativas
  • Limitado a 10GB por partição
  • Quanto mais atributos existir nesse index mais Capacity Unit será usado
  • Deve ser criado no momento da criação da tabela

Local Secondary Index

Global Secondary Index (GSI)

  • Você pode utilizar outros atributos como Partition Key que não precisa ser única
  • Podemos pensar nela como uma shadow ou powerlow table
  • Devemos usar quando a tabela tiver mais que 10GB
  • Completamente separada da tabela principal
  • Eventualmente consistente, ou seja, os dados podem ainda não ter sido atualizados. Isso ocorre porque os indexes são atualizados somente após o registro ser adicionado e confirmado. Então quando a consulta for realizada, mesmo informando que deseja consistente, o índice pode não ter sido atualizado.

Global Secondary Index

Escalando

Para escalar são criadas partições.As partições são criadas por throughput ou tamanho. A cada 10 GB, limite da LSI, uma nova partição é criada. Throughput é provisionado a nível de tabela sendo que

  • 1 WCU é medido em 1 KB/s
  • 1 RCU é medido em 4 KB/s em caso de leitura consistente e no caso de eventualmente consistente custa metade da consistente

A conta feita para criar as partições são:

Partição conta

Exemplificando. Uma tabela com 8 GB e capacidade de leitura de 5000 e 500 de escrita terá 3 partições, conforme imagem abaixo.

Exemplo tabela

Mas por que isso é importante? Isso é importante porque as RCUs WCUs provisionadas são distribuídas através dessas partições. E se em algum momento você tiver vários acessos a uma mesma partição, excedendo o limite, podemos ter uma hot key e ter throttling.

Throttling

Throttling ocorre quando o throughput for além do provisionado por um longo tempo. Motivos para esse tempo maior seriam:

  • Vários acessos ao mesmo item (hot key) ou mesma partição (hot partition)
  • Items muito grandes
  • Misturar hot data (dados novos, adicionados e consultados em grande quantidade) e cold data (dados antigos, consultados com uma frequência bem menor) quando temos tabelas baseadas em período de tempo como logs e estatísticas.

E como evita-lô ? Segundo o Developer guide do DynamoDB, isso é evitado criando tabelas onde o atributo da partition key tem uma grande cardinalidade e um grande número de valores distintos e randômicos. Um bom exemplo aqui seria utilizar UUID (universal unique identifier) como valor. A busca também deve ser feita de forma uniforme através do tempo, evitando assim sobrecarregar uma partição.

Modelagem de dados

Diferente do banco relacional, aqui o banco é desnormalizado de forma que seja possível recuperar todo registro na mesma consulta, sem precisar fazer joins para acessar diversas tabelas.

  • Como representar a hierarquia de dados? Existem duas formas:

A primeira seria criar vários itens, onde cada item representa um nível. Como uma relação 1 para 1.

A segunda é armazenar essa hierarquia como Documento (JSON), em uma hierarquia pequena, essa é a solução ideal. Caso contrário é melhor optar pela solução anterior, uma vez que os itens são limitados a 400 KB.

Melhores Práticas e Desing Patterns

Storage Procedures - Para conseguir realizar uma Storage Procedure com o DynamoDB a melhor forma é utilizar Lambdas que são disparados de acordo com eventos disparados pelo DynamoDB Streams.

DynamoDB Streams são logs de alterações que são registrados a cada update, delete ou insert e qualquer operação que altere algum dado do banco. Esses eventos/logs disparados pelo DynamoDB Stream podem ser usados para disparar uma “Store Procedure” através da Lambda.

Cache - Utilizar cache é uma estratégia para evitar muitos acessos a um mesmo item e uma possível hot key. Como exemplo podemos pensar em um produto em promoção na Black Friday. Nesse caso o produto terá muitos acessos e teremos uma hot key e utilizar o cache irá evitar isso. Uma forma de atualizar esse cache seria utilizar Lambda para quando um item for alterado, alterar no cache também.

Tabelas baseada em períodos de tempo (Static TTL Timestamp) - Como exemplo podemos citar tabelas de log ou estatísticas. Tabelas onde apenas os dados mais atuais são mais importantes e acessados (hot data) e os mais antigos vão se tornando menos relevantes.
Para separar a hot data com da cold data o ideal é criar diversas tabelas, baseadas em períodos de tempo (diário, semanal, mensal, …). A mais atual com alta capacidade de escrita e leitura e as mais antigas com cada vez menos capacidade de leitura e escrita sempre 1, uma vez que não será adicionado mais nada.

Quando não usar o design de tabela única

Até agora, conhecemos os prós e os contras do design de tabela única no DynamoDB. Agora é hora de chegar à parte mais controversa — quando, se houver, você não deve usar o design de tabela única no DynamoDB? Em um nível básico, a resposta é “sempre que os benefícios não superam os custos”. Mas essa resposta genérica não nos ajuda muito. A resposta mais concreta é “sempre que eu precisar de flexibilidade de consulta e / ou análises mais fáceis ao invés do desempenho rápido e impressionante”. E acho que há duas ocasiões em que isso é mais provável:

  • em novas aplicações onde a agilidade do desenvolvedor é mais importante que o desempenho da aplicação;
  • em aplicativos usando o GraphQL.

⚠ Quero enfatizar que essas são exceções, não orientações gerais. Ao modelar com o DynamoDB, você deve seguir as práticas recomendadas. Isso inclui desnormalização, design de tabela única e outros princípios adequados de modelagem NoSQL. E mesmo se você optar por um design de várias tabelas, você deve entender o design de uma única tabela para saber por que não é adequado para o seu aplicativo específico.

Fontes onde pesquisei esse conteúdo:

Top comments (0)