No último mês, tenho trabalhado ativamente em projetos de prova de conceito relacionados ao CouchDB, explorando seus recursos e me preparando para tarefas futuras. Durante esse período, revisei a documentação do CouchDB várias vezes para garantir que entendi como tudo funciona. Ao ler a documentação, me deparei com uma afirmação de que, apesar do CouchDB ser fornecido com um Query Server padrão escrito em JavaScript, criar uma implementação personalizada é relativamente simples e soluções personalizadas já existem à solta.
Fiz algumas pesquisas rápidas e encontrei implementações escritas em Python, Ruby ou Clojure. Como toda a implementação não parecia muito longa, decidi experimentar o CouchDB tentando escrever meu próprio Query Server personalizado. Para fazer isso, escolhi Go como idioma. Não tive muita experiência com essa linguagem antes, exceto no uso de templates Go nos gráficos do Helm, mas queria tentar algo novo e pensei que este projeto seria uma grande oportunidade para isso.
Antes de começar a trabalhar, revisei a documentação do CouchDB mais uma vez para entender como o Query Server realmente funciona. De acordo com a documentação, a visão geral de alto nível do Query Server é bastante simples:
O servidor de consulta é um processo externo que se comunica com o CouchDB por meio do protocolo JSON em uma interface stdio e lida com todas as chamadas de função de design […].
A estrutura dos comandos enviados pelo CouchDB para o Query Server pode ser expressa como [
Então, basicamente, o que eu tive que fazer foi escrever uma aplicação capaz de analisar esse tipo de JSON do STDIO, realizando as operações esperadas e retornando respostas conforme especificado na documentação. Houve muita conversão de tipo envolvida para lidar com uma ampla variedade de comandos no código Go. Detalhes específicos sobre cada comando podem ser encontrados na seção Query Server Protocol da documentação.
Um problema que enfrentei aqui foi que o Query Server deveria ser capaz de interpretar e executar código arbitrário fornecido em documentos de design. Sabendo que Go é uma linguagem compilada, eu esperava ficar preso nesse ponto. Felizmente, encontrei rapidamente o pacote Yeagi, que é capaz de interpretar código Go com facilidade. Permite criar um sandbox e controlar o acesso a quais pacotes podem ser importados no código interpretado. No meu caso, decidi expor apenas meu pacote chamado couchgo, mas outros pacotes padrão também podem ser facilmente adicionados.
Como resultado do meu trabalho, um aplicativo chamado CouchGO! emergiu. Embora siga o Query Server Protocol, não é uma reimplementação individual da versão JavaScript, pois tem suas próprias abordagens para lidar com funções de documentos de design.
Por exemplo, no CouchGO!, não há função auxiliar como emit. Para emitir valores, basta retorná-los da função map. Além disso, cada função no documento de design segue o mesmo padrão: ela possui apenas um argumento, que é um objeto contendo propriedades específicas da função, e deve retornar apenas um valor como resultado. Este valor não precisa ser primitivo; dependendo da função, pode ser um objeto, um mapa ou até mesmo um erro.
Para começar a trabalhar com o CouchGO!, você só precisa baixar o binário executável do meu repositório GitHub, colocá-lo em algum lugar na instância do CouchDB e adicionar uma variável de ambiente que permita ao CouchDB iniciar o CouchGO! processo.
Por exemplo, se você colocar o executável couchgo no diretório /opt/couchdb/bin, você adicionaria a seguinte variável de ambiente para permitir que ele funcione.
export COUCHDB_QUERY_SERVER_GO="/opt/couchdb/bin/couchgo"
Para obter uma compreensão rápida de como escrever funções com o CouchGO!, vamos explorar a seguinte interface de função:
func Func(args couchgo.FuncInput) couchgo.FuncOutput { ... }
Cada função no CouchGO! seguirá este padrão, onde Func é substituído pelo nome da função apropriado. Atualmente, CouchGO! suporta os seguintes tipos de função:
Vamos examinar um exemplo de documento de design que especifica uma visualização com funções de mapa e redução, bem como uma função de validação_doc_update. Além disso, precisamos especificar que estamos usando Go como idioma.
{ "_id": "_design/ddoc-go", "views": { "view": { "map": "func Map(args couchgo.MapInput) couchgo.MapOutput {\n\tout := couchgo.MapOutput{}\n\tout = append(out, [2]interface{}{args.Doc[\"_id\"], 1})\n\tout = append(out, [2]interface{}{args.Doc[\"_id\"], 2})\n\tout = append(out, [2]interface{}{args.Doc[\"_id\"], 3})\n\t\n\treturn out\n}", "reduce": "func Reduce(args couchgo.ReduceInput) couchgo.ReduceOutput {\n\tout := 0.0\n\n\tfor _, value := range args.Values {\n\t\tout = value.(float64)\n\t}\n\n\treturn out\n}" } }, "validate_doc_update": "func Validate(args couchgo.ValidateInput) couchgo.ValidateOutput {\n\tif args.NewDoc[\"type\"] == \"post\" {\n\t\tif args.NewDoc[\"title\"] == nil || args.NewDoc[\"content\"] == nil {\n\t\t\treturn couchgo.ForbiddenError{Message: \"Title and content are required\"}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif args.NewDoc[\"type\"] == \"comment\" {\n\t\tif args.NewDoc[\"post\"] == nil || args.NewDoc[\"author\"] == nil || args.NewDoc[\"content\"] == nil {\n\t\t\treturn couchgo.ForbiddenError{Message: \"Post, author, and content are required\"}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif args.NewDoc[\"type\"] == \"user\" {\n\t\tif args.NewDoc[\"username\"] == nil || args.NewDoc[\"email\"] == nil {\n\t\t\treturn couchgo.ForbiddenError{Message: \"Username and email are required\"}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn couchgo.ForbiddenError{Message: \"Invalid document type\"}\n}", "language": "go" }
Agora, vamos detalhar cada função começando com a função de mapa:
func Map(args couchgo.MapInput) couchgo.MapOutput { out := couchgo.MapOutput{} out = append(out, [2]interface{}{args.Doc["_id"], 1}) out = append(out, [2]interface{}{args.Doc["_id"], 2}) out = append(out, [2]interface{}{args.Doc["_id"], 3}) return out }
No CouchGO!, não há função de emissão; em vez disso, você retorna uma fatia de tuplas de valores-chave em que a chave e o valor podem ser de qualquer tipo. O objeto document não é passado diretamente para a função como em JavaScript; em vez disso, está envolto em um objeto. O documento em si é simplesmente um hashmap de vários valores.
A seguir, vamos examinar a função de redução:
func Reduce(args couchgo.ReduceInput) couchgo.ReduceOutput { out := 0.0 for _, value := range args.Values { out = value.(float64) } return out }
Semelhante ao JavaScript, a função de redução no CouchGO! recebe chaves, valores e um parâmetro de nova redução, todos agrupados em um único objeto. Esta função deve retornar um único valor de qualquer tipo que represente o resultado da operação de redução.
Finalmente, vamos dar uma olhada na função Validate, que corresponde à propriedade valid_doc_update:
func Validate(args couchgo.ValidateInput) couchgo.ValidateOutput { if args.NewDoc["type"] == "post" { if args.NewDoc["title"] == nil || args.NewDoc["content"] == nil { return couchgo.ForbiddenError{Message: "Title and content are required"} } return nil } if args.NewDoc["type"] == "comment" { if args.NewDoc["post"] == nil || args.NewDoc["author"] == nil || args.NewDoc["content"] == nil { return couchgo.ForbiddenError{Message: "Post, author, and content are required"} } return nil } return nil }
Nesta função, recebemos parâmetros como o novo documento, o documento antigo, o contexto do usuário e o objeto de segurança, todos agrupados em um objeto passado como argumento de função. Aqui, esperamos validar se o documento pode ser atualizado e retornar um erro caso não. Semelhante à versão JavaScript, podemos retornar dois tipos de erros: ForbiddenError ou UnauthorizedError. Se o documento puder ser atualizado, devemos retornar nulo.
Para exemplos mais detalhados, eles podem ser encontrados em meu repositório GitHub. Uma coisa importante a notar é que os nomes das funções não são arbitrários; eles devem sempre corresponder ao tipo de função que representam, como Mapear, Reduzir, Filtrar, etc.
Embora escrever meu próprio Query Server tenha sido uma experiência muito divertida, não faria muito sentido se eu não o comparasse com as soluções existentes. Então, preparei alguns testes simples em um contêiner Docker para verificar o quão mais rápido o CouchGO! pode:
Semeei o banco de dados com o número esperado de documentos e medi os tempos de resposta ou logs de carimbo de data/hora diferenciados do contêiner Docker usando scripts de shell dedicados. Os detalhes da implementação podem ser encontrados em meu repositório GitHub. Os resultados são apresentados na tabela abaixo.
Teste | SofáGO! | SofáJS | Impulsionar |
---|---|---|---|
Indexação | 141.713s | 421.529s | 2,97x |
Reduzindo | 7672ms | 15642ms | 2,04x |
Filtragem | 28,928s | 80,594s | 2,79x |
Atualizando | 7.742s | 9.661s | 1,25x |
Como você pode ver, o impulso em relação à implementação de JavaScript é significativo: quase três vezes mais rápido no caso de indexação, mais de duas vezes mais rápido para funções de redução e filtro. O aumento é relativamente pequeno para funções de atualização, mas ainda mais rápido que JavaScript.
Como o autor da documentação prometeu, escrever um Query Server personalizado não foi tão difícil ao seguir o Query Server Protocol. Mesmo que o CouchGO! carece de algumas funções obsoletas em geral, fornece um impulso significativo em relação à versão JavaScript, mesmo neste estágio inicial de desenvolvimento. Acredito que ainda há muito espaço para melhorias.
Se precisar de todo o código deste artigo em um só lugar, você pode encontrá-lo em meu repositório GitHub.
Obrigado por ler este artigo. Eu adoraria ouvir sua opinião sobre esta solução. Você o usaria com sua instância do CouchDB ou talvez já use algum Query Server personalizado? Eu adoraria ouvir sobre isso nos comentários.
Não se esqueça de verificar meus outros artigos para obter mais dicas, insights e outras partes desta série à medida que são criados. Feliz hacking!
Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.
Copyright© 2022 湘ICP备2022001581号-3