UPDATE X WHERE Y;
é uma tarefa não trivial a ser executada no cenário NoSQL, em oposição a outros bancos de dados baseados em SQL. Em SQL, é apenas uma operação, enquanto no NoSQL você tem que executar a operação PUT
em cada registro.
Aparentemente, essa não é uma tarefa fácil. Não é apenas demorado, porque temos que executar N operações de escrita, mas também exige muita taxa de transferência e é muito frágil - o throttling pode impedir que algumas atualizações ocorram, e você pode acabar com inconsistências nos dados.
Teoricamente, existem apenas três requisitos que precisam ser atendidos. Vamos ver alguns exemplos de como isso pode ser feito.
Uma grande atualização síncrona e por que isso é ruim
A primeira coisa que pode vir à sua cabeça é executar a operação query
para obter todos os registros desejados e executar update
dentro da mesma função em todas as linhas buscadas.
Embora essa solução possa ser perfeitamente adequada para pequenas operações. No entanto, depois de atingir um número maior de linhas, a execução poderá expirar devido à duração máxima da AWS Lambda. Isso é inaceitável. Portanto, podemos apenas dividir o processo em duas partes e desacoplar a consulta das atualizações.
Separar e paralelizar atualizações
A ideia é simples. Uma função obtém todas as linhas que precisam ser atualizadas, divide o conjunto em lotes e inicia N invocações da função Lambda "atualizadora".
Melhor. Porém, o problema anterior ainda não pode ser eliminado e, além disso, podemos atingir o limite da capacidade de escrita de nossa tabela, rejeitando efetivamente algumas de nossas chamadas, corrompendo os dados. Isso também é inaceitável.
Use SQS para armazenar em buffer
Para evitar atualizações rejeitadas, podemos introduzir a fila SQS como um buffer. Todas as operações que precisam ser executadas não são passadas diretamente para a função "atualizadora", mas colocadas em uma fila que é então consumida por uma função lambda “atualizadora”.
Este design tem uma grande vantagem. Ele permite colocar os trabalhos rejeitados de volta na fila e tentar novamente até que cada solicitação de atualização seja finalmente processada. Nosso primeiro requisito é atendido.
Observe que esse design requer alguns ajustes para obter o melhor desempenho para descobrir qual o melhor tamanho de lotes para trabalhar com SQS. Converter cada operação de atualização que está na fila em uma tarefa separada, não teria um bom custo-benefício, devido à quantidade de invocações da AWS Lambda e itens no SQS, por outro lado, dividir os itens na fila em grandes lotes pode bloquear o sistema e torná-lo inflexível.
Minha sugestão é criar lotes de 25 itens para tarefas de atualização usando BatchWriteItem e tentar definir os itens no SQS para conter um múltiplo de 25.
Porém, não é o ideal. Com uma simples mudança, podemos deixar isso melhor.
Fazer atualizações durante a transmissão de resultados da tabela
Em nosso design anterior, aguardamos até que todos os itens da tabela sejam buscados. Isso atrasa nossas atualizações e torna o processo mais lento no geral.
Devido à natureza da API do DynamoDB, obtemos resultados em páginas. Podemos usar essa característica para transmitir resultados página por página diretamente para uma fila, ao invés de esperar para buscar todos os dados e dividi-los em X lotes.
Portanto, apenas uma pequena alteração em nosso diagrama é necessária:
Mas você pode perguntar: "...e se a tabela de origem for grande demais para ser processada de uma só vez pela função "Get"?"
Minha solução é chamar a função context.getRemainingTimeInMillis()
após o término de cada página para verificar quanto tempo nos resta. Se houver menos de um segundo, essa chamada específica da AWS Lambda deve finalizar seu processamento e, em em seguida, re-invocar outra instância passando seu argumento LastProcessedKey
como ExclusiveStartKey
para a próxima instância.
Verificando o progresso
Para atender ao último requisito e saber quando o processo terminou, precisamos contar basicamente o número de linhas que precisam ser atualizadas e o número de operações de atualização que foram bem-sucedidas. Minha proposta é reunir essas duas métricas em uma tabela separada do DynamoDB, com um stream anexado à uma função Lambda, que irá comparar e enviar uma notificação assim que as métricas forem iguais.
Primeiro número que podemos obter assim que nossa função get chegar ao ponto em que não há mais itens em LastEvaluatedKeyiguais
(e.g. igual a null
).
Obter o segundo número é um pouco mais complicado. Como o modelo do DynamoDB é eventualmente consistente, a atualização da mesma linha por várias funções "atualizadoras" pode resultar em um número incorreto no final. Ao invés disso, devemos deixar que cada chamada de função coloque um novo registro separado e agregue uma soma de todos eles. Para evitar SCAN
s, nossa tabela deve ter uma chave composta pela chave de partição, que no nosso caso será sempre a mesma, e chave de intervalo que será um UUID exclusivo.
Assim que um novo registro é inserido, o DynamoDB transmitia a atualização para a função Lambda “orquestradora”, que consulta todas as operações realizadas, soma a contagem e compara com o “primeiro número” - número de linhas que precisam ser atualizadas.
Isso tudo é realmente uma boa ideia?
Como você deve ter notado, algo que é muito fácil e frequentemente visto no mundo SQL, requer uma solução bastante complexa no cenário NoSQL. Essa é uma das razões pelas quais as conversões 1 para 1 do design de dados relacionais não funcionam bem com o DynamoDB. Sempre antes de fazer migração para o DynamoDB, verifique seus padrões de acesso e avalie se ele atende às suas necessidades.
No entanto, esse padrão, com pequenas modificações, pode ser usado para migrar dados do SQL para o DynamoDB ou executar inserções de dados em uma escala massiva de modo confiável.
Se você precisar de dicas e experiência sobre DynamoDB ou Serverless, pode entrar em contato comigo.
Créditos
- Using AWS Lambda and SQS to perform mass, distributed and asynchronous updates on DynamoDB Tables, escrito originalmente por Rafal Wilinski.
Top comments (0)