Banco NoSql em Azure

Olá pessoal, continuando a série iniciada aqui, esta semana vou detalhar como migramos nossa API de controle de acesso de um banco de dados relacional Sql Server para um banco de dados NoSql, em Azure.

Os bancos NoSql estão sendo cada vez mais empregados em aplicações web devido à sua escalabilidade e menor custo. No entanto, seu uso exige uma série de cuidados e nem todos os domínios podem ser facilmente mapeados para utilizar este tipo de estrutura de dados. Domínios que tenham consultas complexas, que dependam de índices ou de extensas manipulações de entidades ou que utilizem um número elevado de transações em entidades diversas, podem ter uma alta dificuldade de mapeamento.

No nosso caso, escolhemos migrar a nossa API de segurança e autenticação. Ela foi uma boa opção por ser baseada em domínio pequeno, com ações pontuais (login, cadastro) e poucas consultas avançadas. Outro ponto favorável é que nosso sistema de segurança é multi-tenant, o que é relativamente fácil de implementar em um banco NoSql mas no mínimo problemático, em um banco relacional. Mais detalhes sobre isto abaixo.

Para a migração para NoSql no Azure, avaliamos utilizar o CosmosDb e o Azure TableStorage padrão. O interessante é que a forma de acesso e a estruturação de dados são praticamente iguais nos dois, o que muda é que no CosmosDb temos alguns recursos a mais (como por exemplo a remoção automática de registros por tempo), além de um melhor suporte para escalabilidade. Porém, enquanto o Azure TableStorage é bem barato (praticamente cobrança baseada em espaço alocado), o CosmosDb já é bem mais caro – cobrado por operações realizadas. No nosso cenário, o custo mensal do CosmosDb chegou perto do custo do Sql Server – cerca de USD 60 por mês, para o tamanho e uso do nosso banco. Como não temos grandes necessidades de contingência, acabamos escolhendo o TableStorage, e o seu custo mensal ficou inferior a 10 USD.

A ideia do uso de um banco NoSql usando o TableStorage é relativamente simples: um conjunto de tabelas identificadas por 2 tipos de índices, PartitionKey e RowKey. Alguns bancos suportam mais tipos de índices, mas no caso do Azure TableStorage, só temos estes 2 mesmo. Por ser uma aplicação muti-tenant, o identificador do Tenant é o PartitionKey, o que nos deixa somente a RowKey para identificar as entidades. Se partirmos de um mapeamento clássico relacional, o RowKey seria a Primary Key (PK) que identificaria cada instância. As Foreign Keys (FK) podem ser mapeadas por propriedades (colunas) na tabela, resolvendo os mapeamentos do tipo 1:N. O problema são chaves compostas e relacionamentos N:N, que exigem um esforço maior de mapeamento. A ausência de outros índices explica por que é complicado mapear um sistema com grande quantidade de relatórios e consultas, já que teríamos que criar uma estrutura para representar cada índice complexo e armazenar neles as RowKeys relativas às entidades que são identificadas pelo índice. Possível, mas complexo.

No nosso caso, simplificamos o modelo para poucas entidades (ver diagramas abaixo, do modelo original e do NoSql), cada um identificado pela sua RowKey. Tínhamos algumas necessidades de relacionamentos N:N, como por exemplo a de aplicações e de permissões em usuários. Para estes casos, criamos uma coluna na tabela do usuário contendo a serialização da coleção destas entidades. Estas colunas são carregadas e deserializadas quando a entidade é trazida da TableStorage.

Modelo Relacional
Tabelas Azure Table Storage
Dashboard Azure Storage

Para fazer a manutenção das tabelas no Azure TableStorage, sugiro usar o Azure Storage Explorer. É uma aplicação bem simples mas que permite efetuar todas as operações necessárias.

Outro ponto que é bem diferente do modelo relacional é carga (load) das entidades. No modelo relacional, o típico é mapear todas as colunas para o objeto e carrega-lo de uma vez do banco de dados. Como no NoSql podemos ter muitas colunas que são utilizadas como repositório (como as de coleções, por exemplo), carregar todas as colunas seria um custo elevado. Assim, tipicamente, a cada operação de acesso ou de salvamento, somente as colunas afetadas são solicitadas ou alteradas.

Para facilitar a persistência das entidades, criamos um pequeno framework com as operações básicas. Denominamos “entidade raiz” aquelas que estão ligadas diretamente ao Tenant, tais como Application, User ou RefreshToken (ver modelo acima). As entidades raiz são carregadas diretamente pelo RowKey e pelo TenantId específico – ver trecho de código abaixo. As demais entidades são subordinadas a uma entidade raiz (como Roles ou Permissions) nelas, a PartitionKey é sempre a chave da entidade Raiz e a RowKey é a sua chave específica – ver abaixo. Fizemos também métodos para carregar ou salvar objetos na TableStorage que permitem especificar as colunas necessárias para cada operação. Finalmente, fizemos também métodos para facilitar a serialização/deserialização de coleções quando são repositórios em colunas.

public Task<T> LoadTenantRootEntity<T>(Guid id, params string[] columns) 
	where T : TableStorageEntity, new() {
	return LoadTenantRootEntity<T>(GuidToString(id), columns);
}

public async Task<T> LoadTenantRootEntity<T>(string id, params string[] columns) 
	where T: TableStorageEntity, new() {
	var retrieveOperation = TableOperation.Retrieve<T>(tenant.Identifier, id, columns.Length > 0 
		? columns.ToList() 
		: null);
	var result = await Table<T>().ExecuteAsync(retrieveOperation);
	return result.Result as T;
}

Código para carga de entidade raiz com TenantId

protected async Task<IList<T>> LoadAll<T>(string partitionKey, params string[] columns) where T : TableStorageEntity, new() {
	var query = new TableQuery<T>().Select(columns).Where(TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey));
	TableContinuationToken continuationToken = null;
	var results = new List<T>();
	do {
		var result = await Table<T>().ExecuteQuerySegmentedAsync(query, continuationToken);
		continuationToken = result.ContinuationToken;
		results.AddRange(result.Results);

	} while (continuationToken != null);
	return results;
}

Código para carga de entidades filtradas por Partition Key

O resultado ficou bem interessante. As operações básicas ficaram bastante simples e o tempo de resposta das operações ficou muito bom. Em vários cenários, temos um desempenho bem superior ao modelo relacional – nas operações de login, por exemplo, tivemos um ganho muito expressivo. A implementação de multi-tenant ficou muito mais simples do que seria em um modelo relacional, graças aos índices de PartitionKey.

A conclusão é que o banco NoSql é uma boa alternativa pra vários cenários, mas nem sempre é algo simples de se modelar. É um mecanismo de persistência muito mais barato, em termos de Azure, quando comparado a um banco Sql Server. E, dependendo da implementação, é mais rápido e mais facilmente escalável do que um Sql Server tradicional. Como tudo em TI, é conveniente pesar os prós e contras antes de partir para uma linha NoSql. E sempre fazer uma prova de conceito antes de iniciar a migração. Mas tenho certeza que, se bem construído, um sistema com banco NoSql pode ser uma melhor opção do que a linha relacional padrão, em vários cenários.

Fiquem à vontade para comentar ou se tiverem alguns outros cenários de aplicações NoSql e quiserem trocar uma ideia. O próximo post será sobre o Azure Api Management. Até lá!

, , ,

  1. Deixe um comentário

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s

%d blogueiros gostam disto: