Rodamos um sistema de autenticação inteiro em armazenamentos que só conhecem get e put
Um sistema de autenticação parece querer um banco de dados relacional. Usuários, papéis, sessões, grants OAuth, refresh tokens, tudo cruzado. O nosso não usa um. Ele roda no Azure Table Storage, um armazenamento que te dá uma partition key, uma row key e quase nada mais. Sem joins, sem índices secundários que valham alguma coisa, sem operador de incremento, sem transações multi-linha no formato a que você está acostumado. Isso foi uma escolha. Aqui está o que a escolha compra, os padrões que a fazem funcionar, e a parte em que o SQL teria sido de fato mais fácil.
A razão para fazer isso é custo e simplicidade operacional. Sem connection pool para esgotar, sem instância de banco para dimensionar, aplicar patch ou fazer failover, e escala por partição de graça. Uma tabela custa quase nada para manter e não há nada que possa acabar. O preço disso é um armazenamento sem query planner, que só compensa quando seus padrões de acesso são tão previsíveis quanto o armazenamento é burro. Os da autenticação são, então todo o design se apoia nisso.
A chave faz o trabalho que um layout de tabela faria. Sem joins, você projeta a chave de modo que a consulta seja a chave. Você desnormaliza de propósito, escolhe as partições para que suas leituras quentes sejam point-gets, e codifica identidades compostas direto na row key, por exemplo "{pk}|{rk}" para entradas que precisam de uma identidade composta num armazenamento que oferece apenas um espaço de row key. A pergunta "como eu procuro isto" tem que ser respondida quando você projeta a chave, não na hora da consulta. Para autenticação, tudo bem, porque os padrões de acesso são conhecidos e não mudam: buscar um usuário por id, buscar os grants de um usuário, consumir um código. Você não está explorando os dados. Você conhece cada pergunta de antemão.
Você mantém o seu próprio change log. Cada linha carrega um Timestamp gerenciado pelo servidor, e é tentador construir o backup incremental em cima dele: puxar tudo onde Timestamp gt watermark. Isso funciona num demo e morre em produção, porque o armazenamento indexa a partition key e a row key e nada mais. Timestamp não está em índice nenhum, então filtrar por ele inspeciona cada linha da tabela. É um full scan fantasiado de consulta, e fica mais lento a cada dia que a tabela cresce. Então, em vez disso, o armazenamento escreve o seu próprio change log. Cada mutação, cada upsert e cada delete, acrescenta uma linha a uma tabela de change log indexada pelo nome lógico da tabela, com o composto "{pk}|{rk}" como row key e uma flag indicando se a linha foi escrita ou deletada. Agora "o que mudou desde o watermark" é a leitura de uma partição pequena e indexada, em vez de um scan da tabela inteira, e o backup faz point-read apenas das linhas que de fato se moveram. Você reconstrói change-data-capture a partir de escritas comuns, e ela escala com a rotatividade em vez de com o tamanho da tabela.
Concorrência sem transações. Aqui não existe SELECT ... FOR UPDATE. O que você tem, em vez disso, é uma primitiva atômica: a escrita condicional, entregue a você na forma de um ETag. Você escreve uma linha só se a cópia que você leu não mudou por baixo de você. Tudo que precisa de segurança sob acesso concorrente é expresso como optimistic concurrency mais retry em caso de conflito. O exemplo mais claro é o contador de bloqueio de conta. AccessFailedCount precisa incrementar de forma atômica, mas o armazenamento não tem incremento. Então você lê a linha, aumenta a contagem, escreve de volta condicionado ao ETag que você leu, e tenta de novo se alguém chegou antes. Esse loop é como você torna um contador atômico num armazenamento que não tem contador. Dispare cinquenta senhas erradas de uma vez e todas elas entram, porque o conflito é detectado e repetido em vez de perdido.
Um delete pode ser um lock. O consumo de uso único, um código de autorização ou um token de uso único que precisa ser resgatado exatamente uma vez, é um delete condicional. É um conditional-delete por ETag: se o delete tem sucesso, você venceu a corrida e pode prosseguir; se ele falha porque a linha já sumiu, outra pessoa consumiu antes e você para. O delete é o lock. A mesma ideia faz leader election, construída sobre um blob lease, um lock feito de uma escrita atômica em vez de um serviço de coordenação separado. Você quase nunca precisa de um serviço de lock quando a própria escrita é atômica.
Deletes têm que ser de primeira classe, que é metade da razão de o change log existir. Um key-value store não tem marcador de delete: uma linha deletada simplesmente some, então qualquer coisa que varresse em busca de mudanças nunca chegaria a ver que ela saiu. Um backup que se apoiasse nos timestamps das linhas carregaria silenciosamente o usuário deletado para frente para sempre. O change log registra o delete como um delete, então um restore o reproduz em vez de ressuscitá-lo. Esse modo de falha, um backup que fielmente traz de volta todo mundo que você removeu, é uma história por si só e merece um post próprio, mas o padrão pertence a esta lista: num armazenamento sem log de delete, você mantém o log de delete você mesmo.
Agora o outro lado da moeda, o que você abre mão. Você abre mão de consultas ad-hoc: um padrão de acesso genuinamente novo pode significar um novo design de chave ou um full scan, porque não há query planner para te salvar. Você abre mão de joins, então você mantém cópias desnormalizadas na mão e é dono da consistência disso. Você abre mão de transações multi-linha, então você projeta cada invariante para viver dentro de um único item, porque atomicidade de item único é tudo que te dão. Se os seus dados são profundamente relacionais, ou os seus padrões de acesso são desconhecidos e ainda em movimento, esta é a ferramenta errada e vai doer.
É exatamente por isso que ela combina com autenticação. Um banco de dados relacional justifica o seu custo respondendo perguntas em que você ainda nem pensou. Um sistema de autenticação não tem essas perguntas. O conjunto de coisas que você pergunta, buscar este usuário, consumir este código, eleger este líder, fazer backup do que mudou, é pequeno, conhecido e estável. Abra mão do query planner que você nunca ia usar e, em troca, você ganha uma camada de armazenamento que custa quase nada para operar e não tem nada para fazer failover. Para a maioria dos softwares, esse é um mau negócio. Para este, é o certo.
Esses padrões são o motor de armazenamento por trás do Authagonal: um design key-value que custa quase nada para operar, o que é boa parte de como colocamos cada recurso em cada plano.