[Este blog foi distribuído pelo site http://nitschinger.at/].
Motivação
Esta postagem do blog tem como objetivo ser um artigo muito detalhado e informativo para aqueles que já usaram o Couchbase Java SDK e querem saber como funcionam os aspectos internos. Não se trata de uma introdução sobre como usar o Java SDK, e abordaremos alguns tópicos bastante avançados no caminho.
Normalmente, quando falamos sobre o SDK, estamos nos referindo a tudo o que é necessário para você começar (biblioteca do cliente, documentação, notas de versão, etc.). No entanto, neste artigo, o SDK se refere à biblioteca do cliente (código), a menos que seja dito o contrário.
Introdução
Antes de mais nada, é importante entender que o SDK envolve e amplia a funcionalidade do cache de spymem (chamado de "spy") memcached library. Um dos protocolos usados internamente é o protocolo memcached, e muitas funcionalidades podem ser reutilizadas. Por outro lado, quando começar a remover as primeiras camadas do SDK, você perceberá que alguns componentes são um pouco mais complexos devido ao fato de que o spy fornece mais recursos do que o SDK precisa em primeiro lugar. A outra parte é lembrar que muitos dos componentes estão interligados, portanto, você sempre precisa acertar a dependência. Na maioria das vezes, lançamos uma nova versão do spy na mesma data em que lançamos um novo SDK, porque novas coisas foram adicionadas ou corrigidas.
Portanto, além de reutilizar a funcionalidade fornecida pelo spy, o SDK adiciona principalmente dois blocos de funcionalidade: gerenciamento automático de topologia de cluster e suporte para visualizações desde o servidor 1.1 (e 2.0). Além disso, ele também fornece recursos administrativos, como gerenciamento de documentos de design e de balde.
Para entender como o cliente opera, vamos dissecar todo o processo em diferentes fases do ciclo de vida do cliente. Depois de passarmos por todas as três fases (inicialização, operação e desligamento), você deverá ter uma visão clara do que está acontecendo nos bastidores. Observe que há uma postagem de blog separada em andamento sobre o tratamento de erros, portanto, não abordaremos isso aqui com mais detalhes (que será publicado algumas semanas depois no mesmo blog aqui).
Fase 1: Bootstrap
Antes que possamos realmente começar a servir operações como get() e set()precisamos fazer o bootstrap do Cliente Couchbase objeto. A parte importante que precisamos realizar aqui é obter inicialmente uma configuração de cluster (que contém os nós e o mapa do vBucket), mas também estabelecer uma conexão de streaming para receber atualizações de cluster em tempo (quase) real.
Pegamos a lista de nós que passam durante o bootstrap e iteramos sobre ela. O primeiro nó da lista que pode ser contatado na porta 8091 é usado para percorrer a interface RESTful no servidor. Se ele não estiver disponível, será tentado o próximo. Isso significa que, a partir do nó fornecido http://host:port/pools URI, eventualmente seguimos os links para a entidade bucket. Tudo isso acontece dentro de um Provedor de configuraçãoque, neste caso, é o com.couchbase.client.vbucket.ConfigurationProviderHTTP. Se você quiser dar uma olhada nos componentes internos, procure por getBucketConfiguration e readPools métodos.
Uma caminhada (bem-sucedida) pode ser ilustrada da seguinte forma:
- GET /pools
- procure os pools "padrão"
- GET /pools/default
- procure o hash "buckets" que contém a lista de buckets
- GET /pools/default/buckets
- analisa a lista de buckets e extrai o fornecido pelo aplicativo
- GET /pools/default/buckets/
Agora estamos no endpoint REST de que precisamos. Dentro dessa resposta JSON, você encontrará todos os detalhes úteis que também podem ser usados internamente pelo SDK (por exemplo streamingUri, nós e vBucketServerMap). A configuração é analisada e armazenada. Antes de prosseguirmos, vamos discutir rapidamente a parte dos pools estranhos em nossa caminhada REST:
O conceito de um pool de recursos para agrupar buckets foi projetado para o Couchbase Server, mas atualmente não está implementado. Ainda assim, a API REST foi implementada dessa forma e, portanto, todos os SDKs são compatíveis com ela. Dito isso, embora, teoricamente, pudéssemos ir diretamente para /pools/default/buckets e ignorar as primeiras consultas, o comportamento atual é à prova de futuro, portanto, você não precisará alterar o código de inicialização quando o servidor o implementar.
Voltamos à nossa fase de bootstrap. Agora que temos uma configuração de cluster válida que contém todos os nós (e seus nomes de host ou endereços IP), podemos estabelecer conexões com eles. Além de estabelecer as conexões de dados, também precisamos instanciar uma conexão de streaming com um deles. Por motivos de simplicidade, apenas estabelecemos a conexão de streaming com o nó da lista em que obtivemos nossa configuração inicial.
Isso nos leva a um ponto importante a ser lembrado: se você tiver muitos Cliente Couchbase se os objetos forem executados em vários nós e todos eles forem inicializados com a mesma lista, eles poderão acabar se conectando ao mesmo nó para a conexão de streaming e criar um possível gargalo. Portanto, para distribuir a carga um pouco melhor, recomendo embaralhar a matriz antes que ela seja passada para o Cliente Couchbase objeto. Quando você tem apenas alguns Cliente Couchbase conectados ao seu cluster, isso não será um problema.
O URI da conexão de streaming é retirado da configuração que obtivemos anteriormente e normalmente tem a seguinte aparência:
streamingUri: "/pools/default/bucketsStreaming/default?bucket_uuid=88cae4a609eea500d8ad072fe71a7290"
Se você apontar seu navegador para esse endereço, também receberá as atualizações da topologia do cluster transmitidas em tempo real. Como a conexão de streaming precisa ser estabelecida o tempo todo e pode bloquear um thread, isso é feito em segundo plano e tratado por diferentes threads. Estamos usando a estrutura NIO Netty para essa tarefa, que oferece uma maneira muito prática de lidar com operações assíncronas. Se quiser começar a se aprofundar nessa parte, lembre-se de que todas as operações de leitura são completamente separadas das operações de gravação, portanto, você precisa lidar com manipuladores que cuidam do que retorna do servidor. Além de alguns cabos necessários para o Netty, a lógica comercial pode ser encontrada em com.couchbase.client.vbucket.BucketMonitor e com.couchbase.client.vbucket.BucketUpdateResponseHandler. Também tentamos restabelecer essa conexão de streaming se o soquete for fechado (por exemplo, se esse nó for rebalanceado para fora do cluster).
Para realmente embaralhar os dados nos nós do cluster, precisamos abrir vários soquetes para eles. Observe que não há absolutamente nenhum pooling de conexão necessário dentro do cliente, pois gerenciamos todos os soquetes de forma proativa. Além da conexão especial de streaming com um dos servidores (que é aberta na porta 8091), precisamos abrir as seguintes conexões:
- Soquete do Memcached: Porta 11210
- Exibir soquete: Porta 8092
Observe que a porta 11211 não é usada dentro dos SDKs do cliente, mas é usada para conectar clientes genéricos do memcached que não estão cientes do cluster. Isso significa que esses clientes genéricos não recebem topologias de cluster atualizadas.
Portanto, como regra geral, se você tiver um cluster de 10 nós em execução, um objeto CouchbaseClient abrirá cerca de 21 (2*10 + 1) soquetes de cliente. Eles são gerenciados diretamente, portanto, se um nó for removido ou adicionado, os números serão alterados de acordo.
Agora que todos os soquetes foram abertos, estamos prontos para executar operações regulares de cluster. Como você pode ver, há muita sobrecarga envolvida quando o objeto CouchbaseClient é inicializado. Devido a esse fato, não recomendamos que você crie um novo objeto a cada solicitação ou execute muitos objetos CouchbaseClient em um servidor de aplicativos. Isso apenas adiciona sobrecarga e carga desnecessárias ao servidor de aplicativos e aumenta o total de soquetes abertos no cluster (resultando em um possível problema de desempenho).
Como ponto de referência, com o registro regular de nível INFO ativado, é assim que deve ser a conexão e a desconexão de um cluster de 1 nó (bucket do Couchbase):
Apr 17, 2013 3:14:49 PM com.couchbase.client.CouchbaseProperties setPropertyFile
INFO: Não foi possível carregar o arquivo de propriedades "cbclient.properties" porque: Arquivo não encontrado com o carregador de classes do sistema.
2013-04-17 15:14:49.656 INFO com.couchbase.client.CouchbaseConnection: Adicionado {QA sa=/127.0.0.1:11210, #Rops=0, #Wops=0, #iq=0, topRop=null, topWop=null, toWrite=0, interested=0} à fila de conexão
2013-04-17 15:14:49.673 INFO com.couchbase.client.CouchbaseConnection: Estado da conexão alterado para sun.nio.ch.SelectionKeyImpl@2adb1d4
2013-04-17 15:14:49.718 INFO com.couchbase.client.ViewConnection: Adicionado localhost à fila de conexão
2013-04-17 15:14:49.720 INFO com.couchbase.client.CouchbaseClient: A propriedade viewmode não está definida. Definindo viewmode para o modo de produção
2013-04-17 15:14:49.856 INFO com.couchbase.client.CouchbaseConnection: Encerrar o cliente Couchbase
2013-04-17 15:14:49.861 INFO com.couchbase.client.ViewConnection: O nó localhost não tem operações na fila
2013-04-17 15:14:49.861 INFO com.couchbase.client.ViewNode: Reator de E/S encerrado para localhost
Se você estiver se conectando a um Couchbase Server 1.8 ou a um Memcache-Bucket, não verá conexões View sendo estabelecidas:
INFO: Não foi possível carregar o arquivo de propriedades "cbclient.properties" porque: Arquivo não encontrado com o carregador de classes do sistema.
2013-04-17 15:16:44.295 INFO com.couchbase.client.CouchbaseConnection: Adicionado {QA sa=/192.168.56.101:11210, #Rops=0, #Wops=0, #iq=0, topRop=null, topWop=null, toWrite=0, interested=0} à fila de conexão
2013-04-17 15:16:44.297 INFO com.couchbase.client.CouchbaseConnection: Adicionado {QA sa=/192.168.56.102:11210, #Rops=0, #Wops=0, #iq=0, topRop=null, topWop=null, toWrite=0, interested=0} à fila de conexão
2013-04-17 15:16:44.298 INFO com.couchbase.client.CouchbaseConnection: Adicionado {QA sa=/192.168.56.103:11210, #Rops=0, #Wops=0, #iq=0, topRop=null, topWop=null, toWrite=0, interested=0} à fila de conexão
2013-04-17 15:16:44.298 INFO com.couchbase.client.CouchbaseConnection: Adicionado {QA sa=/192.168.56.104:11210, #Rops=0, #Wops=0, #iq=0, topRop=null, topWop=null, toWrite=0, interested=0} à fila de conexão
2013-04-17 15:16:44.306 INFO com.couchbase.client.CouchbaseConnection: Estado da conexão alterado para sun.nio.ch.SelectionKeyImpl@38b5dac4
2013-04-17 15:16:44.313 INFO com.couchbase.client.CouchbaseClient: A propriedade viewmode não está definida. Definindo viewmode para o modo de produção
2013-04-17 15:16:44.332 INFO com.couchbase.client.CouchbaseConnection: Estado da conexão alterado para sun.nio.ch.SelectionKeyImpl@69945ce
2013-04-17 15:16:44.333 INFO com.couchbase.client.CouchbaseConnection: Estado da conexão alterado para sun.nio.ch.SelectionKeyImpl@6766afb3
2013-04-17 15:16:44.334 INFO com.couchbase.client.CouchbaseConnection: Estado da conexão alterado para sun.nio.ch.SelectionKeyImpl@2b2d96f2
2013-04-17 15:16:44.368 INFO net.spy.memcached.auth.AuthThread: Autenticado em 192.168.56.103/192.168.56.103:11210
2013-04-17 15:16:44.368 INFO net.spy.memcached.auth.AuthThread: Autenticado em 192.168.56.102/192.168.56.102:11210
2013-04-17 15:16:44.369 INFO net.spy.memcached.auth.AuthThread: Autenticado em 192.168.56.101/192.168.56.101:11210
2013-04-17 15:16:44.369 INFO net.spy.memcached.auth.AuthThread: Autenticado em 192.168.56.104/192.168.56.104:11210
2013-04-17 15:16:44.490 INFO com.couchbase.client.CouchbaseConnection: Encerrar o cliente Couchbase
Fase 2: Operações
Quando o SDK é inicializado, ele permite que seu aplicativo execute operações no cluster anexado. Para os fins desta postagem do blog, precisamos distinguir entre operações que são executadas em um cluster estável e operações em um cluster que está passando por alguma forma de alteração de topologia (seja ela planejada devido à adição de nós ou não planejada devido a uma falha de nó). Vamos abordar primeiro as operações regulares.
Operações em um cluster estável
Embora não seja diretamente visível em primeiro lugar, dentro do SDK precisamos distinguir entre operações de memcached e operações de visualização. Todas as operações que têm uma chave exclusiva em sua assinatura de método podem ser tratadas como operações de memcached. Todas elas acabam sendo canalizadas por meio do spy. As operações de visualização, por outro lado, são implementadas completamente dentro do próprio SDK.
As operações do View e do memcached são assíncronas. Dentro do spy, há um thread (chamado de thread de E/S) dedicado a lidar com as operações de E/S. Observe que, em ambientes de alto tráfego, não é incomum que esse thread esteja sempre ativo. Ele usa os mecanismos de NIO do Java sem bloqueio para lidar com o tráfego e faz loops em torno de "seletores" que são notificados quando os dados podem ser gravados ou lidos. Se você traçar o perfil do seu aplicativo, verá que esse thread passa a maior parte do tempo aguardando um método select, o que significa que ele está ocioso, esperando ser notificado sobre um novo tráfego. Os conceitos usados no spy para lidar com isso são de conhecimento comum do Java NIO, portanto, talvez você queira dar uma olhada na seção
Internos da NIO antes de se aprofundar nesse caminho de código. Bons pontos de partida são os
net.spy.memcached.MemcachedConnection e
net.spy.memcached.protocol.TCPMemcachedNodeImpl classes. Observe que, dentro do SDK, substituímos o MemcachedConnection para conectar nossa própria lógica de reconfiguração. Essa classe pode ser encontrada dentro do SDK em
com.couchbase.client.CouchbaseConnection e para buckets do tipo memcached em
com.couchbase.client.CouchbaseMemcachedConnection.
Portanto, se uma operação do memcached (como get()) é emitido, ele é passado para baixo até chegar ao thread de E/S. O thread de IO o colocará em uma fila de gravação em direção ao nó de destino. Ele acaba sendo gravado e, em seguida, o thread de E/S adiciona informações a uma fila de leitura para que as respostas possam ser mapeadas adequadamente. Essa abordagem é baseada em futuros, portanto, quando o resultado realmente chega, o futuro é marcado como concluído, o resultado é analisado e anexado como objeto.
O SDK usa apenas o protocolo binário do memcached, embora o spy também ofereça suporte a ASCII. O formato binário é muito mais eficiente e algumas das operações avançadas são implementadas somente nele.
Você pode se perguntar como o SDK sabe para onde enviar a operação? Como já temos o mapa do cluster atualizado, podemos fazer o hash da chave e, com base na lista de nós e no vBucketMap, determinar qual nó acessar. O vBucketMap não contém apenas as informações do nó mestre da matriz, mas também as informações de zero a três nós de réplica. Veja este exemplo (resumido):
vBucketServerMap: {
hashAlgorithm: "CRC",
numReplicas: 1,
serverList: [
“192.168.56.101:11210”,
“192.168.56.102:11210”
],
vBucketMap: [
[0,1],
[0,1],
[0,1],
[1,0],
[1,0],
[1,0]
//…..
},
O serverList contém nossos nós, e o vBucketMap tem ponteiros para a matriz serverList. Temos 1024 vBuckets, portanto, apenas alguns deles são mostrados aqui. Você pode ver que todas as chaves que estão no primeiro vBucket têm seu nó mestre no índice 0 (ou seja, o nó .101) e sua réplica no índice 1 (ou seja, o nó .102). Quando o mapa do cluster mudar e os vBuckets se movimentarem, só precisaremos atualizar nossa configuração e saberemos o tempo todo para onde direcionar nossas operações.
As operações de visualização são tratadas de forma diferente. Como as visualizações não podem ser enviadas a um nó específico (porque não temos uma maneira de fazer o hash de uma chave ou algo assim), fazemos um rodízio entre os nós conectados. A operação é atribuída a um com.couchbase.client.ViewNode quando ele tem conexões livres e, em seguida, é executada. O resultado também é tratado por meio de futuros. Para implementar essa funcionalidade, o SDK usa a biblioteca Apache HTTP Commons (NIO) de terceiros.
Toda a API View fica oculta na porta 8092 em cada nó e é muito semelhante a CouchDB. Ele também contém uma API RESTful, mas a estrutura é um pouco diferente. Por exemplo, você pode acessar um documento de design em /_design/. Ele contém as definições de visualização em JSON:
{
linguagem: "javascript",
visualizações: {
todos: {
map: "function (doc) { if(doc.type == "city") {emit([doc.continent, doc.country, doc.name], 1)}}",
reduzir: "_sum"
}
}
}
Em seguida, você pode descer mais um nível, como /_design/_view/, para consultá-lo de fato:
{"total_rows":9,"rows" (linhas):[
{"id":"city:shanghai","chave":["asia","china","Xangai"],"valor":1},
{"id":"city:tokyo","chave":["asia","japão","tokyo"],"valor":1},
{"id":"city:moscow","chave":["asia","Rússia","moscou"],"valor":1},
{"id":"city:vienna","chave":["europa","austria","viena"],"valor":1},
{"id":"city:paris","chave":["europa","França","paris"],"valor":1},
{"id":"city:rome","chave":["europa","itália","roma"],"valor":1},
{"id":"city:amsterdam","chave":["europa","Países Baixos","amsterdam"],"valor":1},
{"id":"city:new_york","chave":["north_america","EUA","new_york"],"valor":1},
{"id":"city:san_francisco","chave":["north_america","EUA","san_francisco"],"valor":1}
]
}
Depois que a solicitação é enviada e uma resposta é recebida, depende do tipo de solicitação de visualização para determinar como a resposta será analisada. Isso faz diferença, pois as consultas de visualização reduzidas são diferentes das não reduzidas. O SDK também inclui suporte para visualizações espaciais e elas também precisam ser tratadas de forma diferente.
Toda a implementação da análise da resposta do View pode ser encontrada dentro do com.couchbase.client.protocol.views namespace. Lá você encontrará classes abstratas e interfaces como ViewResponse e, em seguida, suas implementações especiais como ViewResponseNoDocs, ViewResponseWithDocs ou ViewResponseReduced. Também faz diferença se setIncludeDocs() for usado no objeto Query, porque o SDK também precisa carregar os documentos completos usando o protocolo memcached nos bastidores. Isso também é feito durante a análise das visualizações.
Agora que você tem uma compreensão básica de como o SDK distribui suas operações em condições estáveis, precisamos abordar um tópico importante: como o SDK lida com as alterações na topologia do cluster.
Operações em um cluster de rebalanceamento
Observe que há uma postagem separada no blog que trata de todos os cenários que podem surgir quando algo dá errado no SDK. Como o rebalanceamento e o failover são partes essenciais do SDK, esta postagem trata mais do processo geral de como isso é feito.
Conforme mencionado anteriormente, o SDK recebe atualizações de topologia por meio da conexão de streaming. Deixando de lado o caso especial em que esse nó é realmente removido ou falha, todas as atualizações serão transmitidas quase em tempo real (em uma arquitetura eventualmente consistente, pode levar algum tempo até que as atualizações do cluster sejam preenchidas para esse nó). Os blocos que chegam pelo fluxo são exatamente iguais aos que vimos ao ler a configuração inicial. Depois que esses blocos foram analisados, precisamos verificar se as alterações realmente afetam o SDK (como há muito mais parâmetros do que o SDK precisa, não faz sentido ouvir todos eles). Todas as alterações que afetam a topologia e/ou o mapa do vBucket são consideradas importantes. Se os nós forem adicionados ou removidos (seja por falha ou planejados), precisaremos abrir ou fechar os soquetes. Esse processo é chamado de "reconfiguração".
Quando essa reconfiguração é acionada, muitas ações precisam acontecer em vários lugares. O Spymemcached precisa lidar com seus soquetes, os View nodes precisam ser gerenciados e a nova configuração precisa ser atualizada. O SDK garante que apenas uma reconfiguração possa ocorrer ao mesmo tempo por meio de bloqueios, para que não haja condições de corrida.
O BucketUpdateResponseHandler baseado em Netty aciona o método CouchbaseClient#reconfigure, que então começa a despachar tudo. Dependendo do tipo de bucket usado (ou seja, os buckets do tipo memcached não têm visualizações e, portanto, não têm ViewNodes), as configurações são atualizadas e os soquetes são fechados. Depois que a reconfiguração é feita, ele pode receber novas configurações. Durante as alterações planejadas, tudo deve ser praticamente controlado e nenhuma operação deve falhar. Se um nó estiver realmente inoperante e não puder ser acessado, essas operações serão canceladas. A reconfiguração é complicada porque a topologia muda enquanto as operações estão fluindo pelo sistema.
Por fim, vamos abordar algumas diferenças entre os buckets do tipo Couchbase e Memcache. Todas as informações que você leu anteriormente se aplicam apenas aos buckets do Couchbase. Os buckets do Memcache são bastante básicos e não têm o conceito de vBuckets. Como você não tem vBuckets, tudo o que o cliente precisa fazer é gerenciar os nós e seus soquetes correspondentes. Além disso, um algoritmo de hash diferente é usado (principalmente o Ketama) para determinar o nó de destino para cada chave. Além disso, os buckets do memcache não têm exibições, portanto, não é possível usar a API de exibição e não faz muito sentido manter os soquetes de exibição. Portanto, para esclarecer a afirmação anterior, se você estiver executando em um bucket de memcache, em um cluster de 10 nós, você terá apenas 11 conexões abertas.
Fase 3: Desligamento
Quando o método CouchbaseClient#shutdown() é chamado, não é permitido adicionar mais operações ao CouchbaseConnection. Até que o tempo limite seja atingido, o cliente quer ter certeza de que todas as operações foram realizadas adequadamente. Todos os soquetes das conexões do memcached e do View são fechados quando não há mais operações na fila (ou são descartados). Observe que os métodos de desligamento nesses soquetes também são usados quando um nó é removido do cluster durante as operações normais, portanto, é basicamente a mesma coisa, mas apenas para todos os nós conectados ao mesmo tempo.
Resumo
Depois de ler esta publicação do blog, você deve ter uma ideia muito mais clara de como o SDK do cliente funciona e por que ele foi projetado dessa forma. Temos muitos aprimoramentos planejados para versões futuras, principalmente para melhorar a experiência direta com a API. Observe que esta postagem do blog não abordou como os erros são tratados dentro do SDK; isso será publicado em uma postagem separada do blog porque também há muitas informações a serem abordadas.