A Ziniki Infrastructure Systems criou sua camada de integração com base no Couchbase, porque a combinação de armazenamento de documentos com mapreduce incremental proporcionou a eles uma maneira poderosa de consultar dados. Neste blog, Gareth Powell, fundador e arquiteto da Ziniki, descreve sua experiência com o uso de exibições de mapreduce no Couchbase.
Venho de um histórico de SQL pesado e de alguma exposição ao MongoDB. Como grande parte do SQL se baseia em junções e na instrução "SELECT", presumi ingenuamente que haveria no Couchbase uma funcionalidade de "localizar" semelhante à do MongoDB, que eu havia usado brevemente em outro projeto. Inicialmente, fiquei surpreso ao descobrir que não era esse o caso.
O problema
Na verdade, o motivo pelo qual decidi usar o Couchbase para esse projeto foi sua capacidade de criar índices complexos em segundo plano usando sua tecnologia de mapreduce incremental. Falarei um pouco mais sobre isso na próxima seção, mas, antes de entrar nesse assunto, deixe-me descrever primeiro o que eu estava tentando criar.
Estou criando uma camada de middleware sobre o Couchbase. Essa camada tem conhecimento sobre os usuários, suas credenciais, identidades, dados pessoais, requisitos de segurança e similares. Ela também é sensível ao aplicativo e tem lógica e regras para controlar o acesso do aplicativo aos dados de qualquer usuário individual.
Para que tudo isso funcione, é necessário ter uma "camada de definição de dados" que descreva o domínio de dados do aplicativo. Por acaso, estou usando XML para essa finalidade, mas qualquer coisa, desde diagramas UML ou ferramentas de design visual até JSON, seria aceitável. O importante é que haja um significado inequívoco para todas as definições e que não haja problemas complicados, como o "problema da parada". Também é importante que a representação seja essencialmente neutra em termos de linguagem e possa ser usada para gerar definições para qualquer linguagem de programação ou classe de ativos que possa estar envolvida. Por enquanto, você pode pensar que isso é equivalente a um conjunto de definições de classe em sua linguagem de programação orientada a objetos favorita.
Devido à natureza abstrata do meu modelo de dados, optei por usar chaves globalmente exclusivas (UUIDs) como chaves para os documentos armazenados no Couchbase em vez de codificar manualmente as chaves com base nos dados. Isso me permitiu gerar uma chave exatamente uma vez. A chave identificará o objeto de forma exclusiva e será sua identidade principal, independentemente da quantidade de alterações que o objeto possa sofrer.
O outro aspecto do meu modelo de dados é que ele pressupõe que os dados podem ser "aglomerados", ou seja, que será muito comum definir objetos compostos como grupos de objetos altamente interconectados, todos conectados ao gráfico do objeto principal por meio de um representante que também é responsável por lidar com os aspectos de segurança do aglomerado.
Mecanismos de armazenamento do Couchbase
O Couchbase define a noção de "visualizações", que são incrivelmente poderosas e podem ser usadas de muitas maneiras diferentes. No entanto, em grande parte, esta postagem do blog é uma advertência contra o uso dessas visualizações para coisas que não deveriam ser usadas.
Tudo no Couchbase foi projetado de forma a funcionar efetivamente "em escala". Isso difere da maioria dos outros sistemas que você usará, que são definidos em torno de um conjunto específico de semântica (teoria relacional, por exemplo) e, em seguida, adaptados até que possam operar efetivamente em escala (como o Star Schema em bancos de dados analíticos). A consequência disso é que as garantias que o Couchbase oferece a você são o conjunto mínimo que ele pode oferecer em grande escala. Como em todo sistema escalável, tudo é desacoplado. Se você planeja - como eu - oferecer um sistema altamente escalonável, é inútil criticar essas restrições. Mais cedo ou mais tarde, você encontrará esses limites e, na verdade, é apenas uma questão de saber como lidar com as consequências. Meu conselho é deixar o máximo possível para o Couchbase e selecionar um mecanismo de acesso que seja mais adequado ao seu aplicativo. Essencialmente, há dois mecanismos de acesso no Couchbase -
O armazenamento de chave/valor é essencialmente síncrono: em um único conjunto de operações, é possível garantir que a atribuição de um único valor a uma chave específica seja bem-sucedida, exclusiva ou atômica. Isso permite que você se certifique de que qualquer operação executada seja bem-sucedida somente se as restrições de exclusividade da chave forem aplicadas.
Consultar documentos usando exibições é assíncrono e eventualmente consistente: Ou seja, as visualizações são atualizadas em algum momento (possivelmente distante) do momento em que você solicitou uma alteração, mas, em última análise, se você parar de fazer qualquer coisa no sistema, elas serão "recuperadas" e, quando (eventualmente) o fizerem, o sistema será 100% consistente.
Esses dois mecanismos oferecem outras semânticas diferentes. Por exemplo, enquanto o armazenamento de chave/valor exige que as chaves tenham valores distintos, as exibições não o fazem. A função de mapeamento de uma visualização pode criar quantas chaves quiser com os mesmos valores; as visualizações também oferecem suporte a chaves de várias partes com uma riqueza que o armazenamento simples de chave-valor não oferece.
Por fim, os dois interagem, pois a entrada para uma visualização é exatamente o conjunto de documentos que estão no armazenamento de chave/valor.
Definição de visualizações
As visualizações no Couchbase são definidas usando um par de funções map e reduce. A função reduce é opcional e está presente simplesmente para permitir que várias linhas de uma visualização sejam "colapsadas" em uma única linha. Como, para os fins deste artigo, estou interessado principalmente em usar visualizações para criar um "índice" de objetos no Couchbase, não discutirei mais os métodos "reduce", mas se você quiser fazer coisas interessantes (como análise de dados), vale a pena dar uma olhada neles na documentação.
As funções map e reduce do Couchbase são definidas em JavaScript e logicamente são "chamadas" quando necessário. Para depuração, é possível definir um ambiente simples no Chrome ou no Rhino que envolva essas funções e permita que você veja como elas operariam "semanticamente" fora do Couchbase. No entanto, esse não é diretamente o mecanismo usado para implementar a criação de visualização dentro do Couchbase: em vez disso, as atualizações são atrasadas, agrupadas e processadas em lotes para obter o melhor desempenho em escala. Além disso, as atualizações são divididas por nó do servidor e podem ocorrer em qualquer ordem.
Obviamente, esse é um mecanismo incrivelmente poderoso para definir um índice, especialmente em comparação com os mecanismos SQL padrão. Por exemplo, se você tiver um atributo em um documento que seja um objeto de matriz, é possível calcular e colocar o comprimento (ou mesmo a soma) do objeto em um dos atributos.
Do meu ponto de vista, um problema com essa abordagem é que as funções operam em uma "caixa de areia", ou seja, cada documento apresentado à função de mapa foi separado de seu contexto. Se você tiver dados irregulares, isso pode ser um problema, mas em uma visão do mundo escalável e estritamente compartilhada, essa abordagem faz todo o sentido. Como os documentos têm hash em servidores diferentes, recuperá-los durante o processamento em lote do JavaScript seria relativamente caro e ineficiente.
Acesso a visualizações
Depois de definir uma exibição usando essas funções JavaScript, é bastante fácil acessar as exibições por intervalo de chaves, seja diretamente usando HTTP ou por meio de uma das muitas associações de clientes. Em meu projeto, estou usando a biblioteca de cliente Java, que abre as exibições que estou usando e, em seguida, emite consultas com base nas definições abstratas fornecidas em meu arquivo XML de definições. O problema com a consistência "eventual" Depois de definir os índices em meus dados, naturalmente tentei acessá-los para recuperar os dados que armazenei. Como optei por usar UUIDs semanticamente irrelevantes como chaves no armazenamento de chave/valor, desisti da oportunidade imediata de encontrar qualquer coisa usando uma chave "natural" e optei por usar o mecanismo de índice para gerar essas chaves "naturais". Mas logo descobri que tinha casos de uso que não combinavam bem com a semântica da visualização.
Credenciais
O primeiro problema que tive foi com as credenciais. O Couchbase atualiza suas exibições após um determinado volume de atividade ou um determinado período de tempo, o que ocorrer primeiro. Embora isso provavelmente fosse bom o suficiente para casos de uso reais, nossos testes se baseavam em scripts repetitivos e automatizados. O primeiro script que escrevi simulava um usuário se registrando no sistema e, em seguida, voltando imediatamente e fazendo login. Eu estava usando o mecanismo de visualização para extrair as credenciais "exclusivas" do usuário (mecanismo de login e ID de login) e mapear isso de volta para o UUID da credencial. No entanto, quando fui buscar isso na exibição depois de criar a credencial, ela ainda não havia atingido o índice. Tentei usar a opção "stale" na exibição, mas, para operações de login, isso pode ser caro (normalmente cerca de 2,5s para fazer a consulta).
Artefatos gerados
Outro problema relacionado era com artefatos que eu estava gerando durante o processamento das solicitações dos usuários. Esses artefatos "lembravam" as interações anteriores do usuário e permitiam que o sistema respondesse adequadamente. Em cada caso, o artefato tinha uma chave "natural" exclusiva que refletia o usuário, a operação que ele estava executando e o objeto no qual estava executando. Usei exibições para rastrear esses artefatos e depois recarregá-los quando o mesmo usuário executou a mesma operação no mesmo objeto em uma data posterior.
Deparei-me com o mesmo problema com eventual consistência: na velocidade em que meus scripts de teste automatizados estavam sendo executados, eu emitia a segunda solicitação antes que o objeto criado na primeira tivesse atingido o índice.
Documentos aninhados
O terceiro caso que encontrei foi com documentos no mesmo "grupo". Depois de encontrar o principal, aquele que estava sendo "protegido" por usuário, eu queria navegar para alguns dos outros objetos na mesma "vizinhança". Defini uma exibição que descrevia o tipo de objeto que eu queria encontrar e incluí o ID do objeto original. Ao pesquisar nesse índice o ID do objeto que eu tinha e as características que estava procurando, acreditei que conseguiria recuperar todos os documentos do grupo.
Mais uma vez, a velocidade com que meus testes geravam os documentos e depois tentavam acessá-los estava me matando. Eu criava os objetos, depois voltava e tentava acessar a visualização, mas descobria que ela ainda estava vazia. Alguns segundos depois, quando eu tentava diagnosticar o problema por meio da interface do usuário, os objetos estavam lá, mas o índice ainda não havia sido atualizado.
A primeira solução: Duplicação dos índices
Ao fazer experiências com o Couchbase, percebi a diferença entre o armazenamento de chave/valor e o mecanismo de visualização. Minha primeira tentativa de resolver esse problema foi simplesmente duplicar todo o trabalho que o Couchbase estava fazendo na visualização, mas em "tempo real" no armazenamento de chave/valor. Na verdade, isso não era um fardo tão grande quanto poderia parecer: como todas as minhas definições de dados estavam em forma abstrata, eu estava gerando as definições de visualização e era relativamente fácil estender isso para gerar o mesmo código em Java para armazenar os itens no armazenamento de chave/valor.
Isso resolveu meus problemas, mas nunca gostei disso por vários motivos. O mais óbvio é que a duplicação ineficaz me fez questionar a escolha do Couchbase. Porém, o mais importante é que o número de casos diferentes que surgiram no código sugeria que eu estava confundindo questões. As duas bifurcações mais importantes eram a diferença entre índices "exclusivos" e "não exclusivos" e a diferença entre índices que precisavam considerar contextos de segurança e aqueles que não precisavam.
A segunda solução: Reconhecer a natureza individual das coisas
O pessoal prestativo do Couchbase me indicou o padrão "lookup" na documentação do Couchbase, que descrevia em detalhes um problema muito semelhante ao que eu estava tendo com as credenciais.
O padrão de pesquisa descreve como usar uma indireção dentro do armazenamento de chave/valor para ter essencialmente um único objeto com várias chaves. Há uma chave exclusiva (o UUID, no meu caso) e todas as outras chaves secundárias apontam para ela. Consegui retrabalhar minhas definições de índice para distinguir entre o caso em que eu queria uma visualização que pudesse suportar várias linhas com a mesma chave e o caso em que eu queria apenas uma linha com uma determinada chave. Fiz isso especificando como uma chave exclusiva poderia ser construída a partir dos campos do objeto de dados e isso foi usado como uma chave de pesquisa apontando para o UUID do objeto.
Isso resolveu os dois primeiros desafios usados acima. Para as credenciais, pude usar o "mecanismo de credenciais" (básico, OpenId, OAuth etc.) e o ID de login exclusivo do usuário com esse mecanismo como chave exclusiva; para os artefatos, pude usar a combinação de ID do usuário, operação e UUID do objeto. Em cada caso, acrescentei automaticamente o fato de que essa era uma chave secundária e o tipo de objeto que estava sendo indexado.
O terceiro desafio era de natureza diferente e precisava de uma solução diferente, mas, novamente, usar uma visualização foi a escolha errada. Nesse caso, o conjunto de IDs de objetos exclusivos a ser considerado estava contido na definição do objeto real que eu já tinha na memória. Em vez de procurar os objetos em uma visualização, a abordagem correta era ler cada um desses objetos do armazenamento de chave/valor usando seus UUIDs e ver quais deles tinham as características apropriadas.
Embora não esteja claro se essa abordagem será dimensionada para milhares ou milhões de objetos contidos, no momento também não está claro se é necessário. Mas outras abordagens (híbridas) são possíveis nesse caso. Por exemplo, seria possível analisar cada objeto contido à medida que ele é gravado no armazenamento de chave/valor e adicionar a ele um conjunto apropriado de subitens caracterizados se ele corresponder aos critérios. Equilibrar o tamanho, o número e as relações entre os objetos é um desafio completamente diferente que estou enfrentando e que um dia poderá ser o conteúdo de outra postagem no blog.
Principais conclusões
A principal conclusão é que é importante entender o que você realmente deseja alcançar e certificar-se de que você combina o mecanismo de acesso correto do Couchbase para atender às necessidades do seu aplicativo.
No meu caso, eu estava tentando usar as exibições em excesso porque é um recurso interessante e poderoso, difícil de resistir. Mas o fato é que as exibições simplesmente não são adequadas para todos os requisitos de acesso a dados em seu aplicativo.
Outra coisa que deve ser levada em conta é ser compreensivo com as necessidades do software de infraestrutura e reconhecer que, para a escalabilidade, é importante ter dados não compartilhados. Reclamar contra isso ou tentar evitá-lo só lhe causará sofrimento.
Então, como você escolhe quais padrões de acesso ao Couchbase usar para seus dados? Com base nessa experiência, escrevi algumas diretrizes que os desenvolvedores podem usar ao decidir qual padrão de acesso a dados escolher. Embora essa lista não seja exaustiva, aqui estão quatro padrões típicos que você deve considerar:
1. Se você tiver a chave de um objeto, use-a para obter o objeto diretamente do armazenamento de chave/valor do Couchbase.
2. Se você não tiver isso, mas estiver procurando um objeto que tenha uma chave secundária exclusiva, tente encontrar a chave lendo-a no índice secundário e, em seguida, obtenha o objeto do armazenamento de chave/valor do Couchbase usando sua chave.
3. Se você já tiver um objeto e ele contiver referências a outros objetos, use essas referências diretamente; não procure por elas com base em uma relação que tenha sido gravada em uma visualização.
4. Por fim, se você quiser recuperar todos os objetos que correspondem a determinados critérios em todo o banco de dados e a semântica da sua operação for tal que não haja dependência inerente de ordenação, acesse uma visualização que você definiu. Lembre-se de que as exibições no Couchbase são eventualmente consistentes.
... [Trackback]
[...] Leia mais: http://www.couchbase.com/couchbase-and-ziniki/ […]