Usar índices secundários globais para consultas de agregação materializadas no DynamoDB - Amazon DynamoDB

Usar índices secundários globais para consultas de agregação materializadas no DynamoDB

Manter as agregações quase em tempo real e as métricas de chaves relacionadas aos dados em constante mudança é uma operação cada vez mais valiosa para a rápida tomada de decisões na empresa. Por exemplo, uma biblioteca de músicas pode querer mostrar as músicas mais baixadas quase em tempo real ou uma plataforma de comércio eletrônico pode precisar exibir produtos muito procurados por categoria.

Como o DynamoDB não oferece suporte nativo a operações de agregação entre itens, como SUM ou COUNT, o cálculo desses valores em tempo de leitura exigiria a verificação de um grande número de itens, o que pode ser lento e caro. Em vez disso, é possível pré-calcular as agregações à medida que os dados mudam e armazenar os resultados como itens regulares em sua tabela. Esse padrão é chamado de agregação materializada.

Exemplo de cenário e padrões de acesso

Considere um aplicativo de biblioteca de músicas com os seguintes requisitos:

  • O aplicativo grava downloads individuais de músicas em alto volume (milhares por segundo).

  • Os usuários precisam ver as músicas mais baixadas em um determinado mês com latência abaixo de 10 milissegundos.

  • O aplicativo também precisa permitir consultas como “as dez melhores músicas do mês” e “todas as músicas baixadas em um determinado mês”.

Calcular as contagens de downloads em tempo de leitura verificando todos os registros de download pode ser caro nessa escala. Em vez disso, é possível manter uma contagem contínua, atualizada à medida que cada download ocorre, e armazená-la de uma forma que permita consultas eficientes.

Por que pré-calcular agregações

Há várias abordagens para calcular agregações. A tabela a seguir compara alternativas comuns e explica por que a agregação materializada no DynamoDB geralmente é a melhor opção para esse tipo de caso de uso.

Abordagem Desvantagens Quando usar
Verificar e contar no momento da leitura Requer a leitura de todos os registros de download para cada consulta. A latência aumenta com o volume de dados e consome uma capacidade de leitura significativa. É adequada apenas para conjuntos de dados muito pequenos em que a latência não é uma preocupação.
Armazenamento de agregações externo (por exemplo, Amazon ElastiCache) Aumenta a complexidade operacional e requer um serviço separado para gerenciar. Requer lógica de sincronização entre o DynamoDB e o cache. Quando você precisa de leituras de menos de um milissegundo ou de uma lógica de agregação complexa que vai além da simples contagem.
Agregação em nível de aplicações na gravação Acopla a lógica de agregação ao caminho de gravação. Se a aplicação falhar após a gravação do download, mas antes da atualização da contagem, a agregação se tornará inconsistente. Quando você precisa de uma agregação síncrona e altamente consistente e pode aceitar maior latência de gravação.
Agregação materializada com o Streams e o Lambda Separa a agregação do caminho de gravação. A agregação tem consistência final (normalmente com segundos de atraso). Adiciona custos de invocação do Lambda. Quando você precisa de agregações quase em tempo real com baixa latência de leitura e pode aceitar uma consistência final. Essa é a abordagem descrita nesta página.

A abordagem de agregação materializada mantém o caminho de gravação simples (basta registrar o download), transfere a agregação para um processo assíncrono e armazena o resultado no DynamoDB, no qual ele pode ser consultado com latência abaixo de 10 milissegundos.

Design de tabelas

Esse design usa uma única tabela com dois tipos de item que compartilham a mesma chave de partição (songID), mas usam padrões de chave de classificação diferentes para diferenciá-los:

  • Registros de download: eventos de download individuais. A chave de classificação é DownloadID (um identificador exclusivo para cada download).

  • Itens de agregação mensal: contagens de downloads pré-calculados por música por mês. A chave de classificação é o mês no formato YYYY-MM (por exemplo, 2018-01). Esses itens também contêm um atributo DownloadCount com o total acumulado.

Somente os itens de agregação mensal contêm o atributo Month. Essa distinção é importante para o design do GSI esparso descrito posteriormente.

O seguinte diagrama mostra o layout da tabela com os dois tipos de item:

Layout da tabela da biblioteca de músicas mostrando registros de download e itens de agregação mensal que compartilham a mesma chave de partição (songID).
Tipo de item Chave de partição (songID) Chave de classificação Atributos adicionais.
Registro do download song1 download-abc123 UserID, Timestamp
Agregação mensal song1 2018-01 Month=2018-01, DownloadCount=1,746,992

Pipeline de agregação com o Streams e o AWS Lambda

O pipeline de agregação funciona da seguinte forma:

  1. Quando uma música é baixada, o aplicativo grava um novo item na tabela com Partition-Key=songID e Sort-Key=DownloadID.

  2. O DynamoDB Streams captura essa gravação como um registro de fluxo.

  3. Uma função do Lambda, anexada ao fluxo, processa o novo registro. Ela identifica songID e o mês atual e, em seguida, atualiza o item de agregação mensal correspondente incrementando o atributo DownloadCount.

  4. O item de agregação atualizado fica então disponível para consulta por meio do GSI esparso.

A função do Lambda usa uma chamada UpdateItem com uma expressão ADD para incrementar atomicamente a contagem de downloads. Isso evita condições de disputa entre leitura, modificação e gravação:

import boto3 dynamodb = boto3.resource('dynamodb') table = dynamodb.Table('MusicLibrary') def handler(event, context): for record in event['Records']: if record['eventName'] == 'INSERT': new_image = record['dynamodb']['NewImage'] song_id = new_image['songID']['S'] # Derive the month from the download timestamp timestamp = new_image['Timestamp']['S'] month = timestamp[:7] # Extract YYYY-MM table.update_item( Key={ 'songID': song_id, 'SK': month }, UpdateExpression='ADD DownloadCount :inc SET #m = :month', ExpressionAttributeNames={ '#m': 'Month' }, ExpressionAttributeValues={ ':inc': 1, ':month': month } )
nota

Se uma execução do Lambda falhar depois de gravar o valor de agregação atualizado, o registro do fluxo poderá ser repetido. Como a operação ADD incrementa a contagem toda vez que é executada, uma nova tentativa incrementaria a contagem mais de uma vez para o mesmo download, oferecendo um valor aproximado. Para a maioria dos casos de uso de analytics e tabelas de classificação, essa pequena margem de erro é aceitável. Se você precisar de contagens exatas, considere a possibilidade de adicionar a lógica de idempotência, usando, por exemplo, uma expressão de condição que verifique se o DownloadID específico já foi processado.

Design de GSIs esparsos

Para consultar com eficiência os resultados agregados, crie um índice secundário global com o seguinte esquema de chaves:

  • Chave de partição do GSI: Month (string)

  • Chave de classificação do GSI: DownloadCount (número)

Esse GSI é esparso porque somente os itens de agregação mensal contêm o atributo Month. Os registros de download individuais não têm esse atributo, então eles são automaticamente excluídos do índice. Isso significa que o GSI contém somente os itens de agregação pré-calculados, que são uma pequena fração do total de itens na tabela.

Um GSI esparso oferece dois benefícios principais:

  • Custo mais baixo: como somente os itens de agregação são replicados no índice, o consumo de capacidade de gravação e armazenamento é bem menor em comparação com um índice que inclui todos os itens da tabela.

  • Consultas mais rápidas: como o índice contém somente os dados que precisam ser consultados, as leituras são eficientes e exibem resultados com latência abaixo de 10 milissegundos.

Para ter mais informações sobre como trabalhar com índices esparsos, consulte Aproveitar índices esparsos.

Consultar o GSI

Com o GSI esparso instalado, é possível responder com eficiência a vários tipos de consulta:

Mostrar a música mais baixada em um determinado mês:

aws dynamodb query \ --table-name "MusicLibrary" \ --index-name "MonthDownloadsIndex" \ --key-condition-expression "#m = :month" \ --expression-attribute-names '{"#m": "Month"}' \ --expression-attribute-values '{":month": {"S": "2018-01"}}' \ --scan-index-forward false \ --limit 1

Quando ScanIndexForward é definido como false, os resultados são classificados por DownloadCount em ordem decrescente, e Limit=1 exibe somente a música mais baixada.

Mostrar as dez músicas mais tocadas de um determinado mês:

aws dynamodb query \ --table-name "MusicLibrary" \ --index-name "MonthDownloadsIndex" \ --key-condition-expression "#m = :month" \ --expression-attribute-names '{"#m": "Month"}' \ --expression-attribute-values '{":month": {"S": "2018-01"}}' \ --scan-index-forward false \ --limit 10

Mostrar todas as músicas baixadas em um determinado mês (classificadas por contagem de downloads):

aws dynamodb query \ --table-name "MusicLibrary" \ --index-name "MonthDownloadsIndex" \ --key-condition-expression "#m = :month" \ --expression-attribute-names '{"#m": "Month"}' \ --expression-attribute-values '{":month": {"S": "2018-01"}}' \ --scan-index-forward false

Considerações

Ao implementar esse padrão, lembre-se sempre do seguinte:

  • Consistência final: os valores de agregação são atualizados de forma assíncrona por meio do DynamoDB Streams e do Lambda. Normalmente, há um atraso de alguns segundos entre o registro de um download e a atualização da agregação. Isso significa que o GSI mostra os dados quase em tempo real, não em tempo real.

  • Simultaneidade do Lambda: se a tabela tiver um alto volume de gravação, várias invocações do Lambda podem tentar atualizar o mesmo item de agregação simultaneamente. A operação atômica ADD lida com isso com segurança, mas é necessário monitorar as métricas de simultaneidade e controle de utilização do Lambda para garantir que sua função possa acompanhar o fluxo.

  • Capacidade de gravação do GSI: como o GSI esparso contém apenas itens de agregação, ele requer uma capacidade de gravação significativamente menor do que a tabela base. No entanto, ainda assim é necessário provisionar capacidade suficiente (ou usar o modo sob demanda) para lidar com a taxa de atualizações de agregação.

  • Contagens aproximadas: conforme observado anteriormente, novas tentativas do Lambda podem fazer com que as contagens sejam um pouco exageradas. Para casos de uso que exigem contagens exatas, implemente verificações de idempotência na função do Lambda.