[This blog was syndicated from http://nitschinger.at/]

Motivación

Esta entrada de blog pretende ser un artículo muy detallado e informativo para aquellos que ya han utilizado el Java SDK de Couchbase y quieren saber cómo funcionan las partes internas. Esto no es una introducción sobre cómo utilizar el SDK de Java y vamos a cubrir algunos temas bastante avanzados en el camino.

Normalmente, cuando hablamos del SDK nos referimos a todo lo necesario para ponerlo en marcha (biblioteca del cliente, documentación, notas de la versión, etc.). En este artículo, sin embargo, el SDK se refiere a la biblioteca del cliente (código) a menos que se indique lo contrario.

Introducción

En primer lugar, es importante entender que el SDK envuelve y amplía la funcionalidad de la aplicación spymemcached (llamada "espía") de la biblioteca memcached. Uno de los protocolos utilizados internamente es el protocolo memcached, y se puede reutilizar mucha funcionalidad. Por otro lado, una vez que empieces a pelar las primeras capas del SDK te darás cuenta de que algunos componentes son algo más complejos debido al hecho de que spy proporciona más funcionalidades de las que el SDK necesita en primer lugar. La otra parte es recordar que muchos de los componentes están entrelazados, por lo que siempre hay que acertar con las dependencias. La mayoría de las veces, lanzamos una nueva versión de spy al mismo tiempo que un nuevo SDK, porque se han añadido o corregido cosas nuevas.

Así pues, aparte de reutilizar la funcionalidad proporcionada por spy, el SDK añade principalmente dos bloques de funcionalidad: gestión automática de la topología del clúster y, desde 1.1 (y 2.0 server), compatibilidad con Views. Aparte de eso, también proporciona facilidades administrativas como la gestión de cubos y documentos de diseño.
Para entender cómo funciona el cliente, vamos a diseccionar todo el proceso en diferentes fases del ciclo de vida del cliente. Después de pasar por las tres fases (arranque, funcionamiento y apagado) deberías tener una idea clara de lo que ocurre bajo el capó. Tenga en cuenta que hay una entrada de blog separada en la fabricación sobre el manejo de errores, por lo que no vamos a cubrir aquí en mayor detalle (que se publicará unas semanas más tarde en el mismo blog aquí).

Fase 1: Bootstrap

Antes de que podamos empezar a servir operaciones como get() y set()necesitamos arrancar el CouchbaseClient objeto. La parte importante que tenemos que lograr aquí es obtener inicialmente una configuración de clúster (que contiene los nodos y el mapa vBucket), pero también establecer una conexión de streaming para recibir actualizaciones del clúster en tiempo (casi) real.

Tomamos la lista de nodos que pasan durante el bootstrap e iteramos sobre ella. El primer nodo de la lista que puede ser contactado en el puerto 8091 se utiliza para recorrer la interfaz RESTful en el servidor. Si no está disponible, se intentará con el siguiente. Esto significa que yendo desde el nodo http://host:port/pools URI finalmente seguimos los enlaces a la entidad cubo. Todo esto ocurre dentro de un ConfigurationProviderque en este caso es el com.couchbase.client.vbucket.ConfigurationProviderHTTP. Si quieres hurgar en el interior, busca getBucketConfiguration y readPools métodos.
Un paseo (exitoso) puede ilustrarse así:
  1. GET /pools
  2. busque las piscinas "por defecto
  3. GET /pools/default
  4. buscar el hash "buckets" que contiene la lista de cubos
  5. GET /pools/default/buckets
  6. analizar la lista de cubos y extraer el proporcionado por la aplicación
  7. GET /pools/default/buckets/

Ahora estamos en el punto final REST que necesitamos. Dentro de esta respuesta JSON, encontrará todos los detalles útiles que también pueden ser utilizados por SDK internamente (por ejemplo streamingUrinodos y vBucketServerMap). La configuración se analiza y almacena. Antes de seguir adelante, vamos a discutir rápidamente la parte extraña piscinas dentro de nuestro paseo REST:

El concepto de pool de recursos para agrupar buckets fue diseñado para Couchbase Server, pero actualmente no está implementado. Aún así, la API REST está implementada de esa manera y por lo tanto todos los SDKs lo soportan. Dicho esto, aunque teóricamente podríamos ir directamente a /pools/default/buckets y omitir las primeras consultas, el comportamiento actual es a prueba de futuro por lo que no tendrás que cambiar el código bootstrap una vez que el servidor lo implemente.
Volvemos a nuestra fase de arranque. Ahora que tenemos una configuración de cluster válida que contiene todos los nodos (y sus nombres de host o direcciones ip), podemos establecer conexiones con ellos. Aparte de establecer las conexiones de datos, también necesitamos instanciar una conexión de streaming a uno de ellos. Por razones de simplicidad, sólo estableceremos la conexión de streaming al nodo de la lista donde obtuvimos nuestra configuración inicial.
Esto nos lleva a un punto importante a tener en cuenta: si usted tiene un montón de CouchbaseClient que se ejecutan en muchos nodos y todos se arrancan con la misma lista, pueden acabar conectándose al mismo nodo para la conexión de streaming y crear un posible cuello de botella. Por lo tanto, para distribuir la carga un poco mejor recomiendo barajar la matriz antes de que se pasa a la CouchbaseClient objeto. Cuando sólo tiene unos pocos CouchbaseClient objetos conectados a su clúster, eso no será ningún problema.
El URI de la conexión de streaming se toma de la configuración que obtuvimos anteriormente, y normalmente tiene este aspecto:
streamingUri: "/pools/default/bucketsStreaming/default?bucket_uuid=88cae4a609eea500d8ad072fe71a7290"
Si diriges tu navegador a esta dirección, también obtendrás las actualizaciones de la topología del clúster en tiempo real. Dado que la conexión de streaming necesita establecerse todo el tiempo y potencialmente bloquea un hilo, esto se hace en segundo plano manejado por diferentes hilos. Estamos utilizando el framework NIO Netty para esta tarea, que proporciona una forma muy práctica de tratar con operaciones asíncronas. Si quieres empezar a profundizar en esta parte, ten en cuenta que todas las operaciones de lectura están completamente separadas de las operaciones de escritura, por lo que necesitas tratar con handlers que se encarguen de lo que vuelve del servidor. Aparte de algún cableado necesario para Netty, la lógica de negocio se puede encontrar en com.couchbase.client.vbucket.BucketMonitor y com.couchbase.client.vbucket.BucketUpdateResponseHandler. También intentamos restablecer esta conexión de streaming si el socket se cierra (por ejemplo, si este nodo se reequilibra fuera del clúster).
Para transferir los datos a los nodos del cluster, necesitamos abrir varios sockets hacia ellos. Tenga en cuenta que el cliente no necesita ningún tipo de agrupación de conexiones, ya que gestionamos todos los sockets de forma proactiva. Aparte de la conexión especial de streaming a uno de los severs (que se abre contra el puerto 8091), necesitamos abrir las siguientes conexiones:
  1. Socket Memcached: Puerto 11210
  2. Ver Socket: Puerto 8092

Tenga en cuenta que el puerto 11211 no se utiliza dentro de los SDK de cliente, sino que se utiliza para conectar clientes genéricos de memcached que no son conscientes del clúster. Esto significa que estos clientes genéricos no obtienen topologías de clúster actualizadas.

Así que como regla general, si tienes un cluster de 10 nodos funcionando, un objeto CouchbaseClient abrirá unos 21 (2*10 + 1) sockets de cliente. Estos son gestionados directamente, por lo que si un nodo se quita o se añade los números cambiarán en consecuencia.
Ahora que todos los sockets han sido abiertos, estamos listos para realizar operaciones regulares de cluster. Como puedes ver, hay mucha sobrecarga involucrada cuando el objeto CouchbaseClient es arrancado. Debido a este hecho, te desaconsejamos encarecidamente que crees un nuevo objeto en cada petición o que ejecutes muchos objetos CouchbaseClient en un servidor de aplicaciones. Esto sólo añade sobrecarga innecesaria en el servidor de aplicaciones y en el total de sockets abiertos contra el cluster (resultando en un posible problema de rendimiento).
Como punto de referencia, con el registro regular de nivel INFO activado, así es como debería verse la conexión y desconexión a un cluster de 1 nodo (bucket Couchbase):
Apr 17, 2013 3:14:49 PM com.couchbase.client.CouchbaseProperties setPropertyFile
INFO: No se ha podido cargar el fichero de propiedades "cbclient.properties" porque: Archivo no encontrado con el cargador de clases del sistema.
2013-04-17 15:14:49.656 INFO com.couchbase.client.CouchbaseConnection: Added {QA sa=/127.0.0.1:11210, #Rops=0, #Wops=0, #iq=0, topRop=null, topWop=null, toWrite=0, interested=0} to connect queue
2013-04-17 15:14:49.673 INFO com.couchbase.client.CouchbaseConnection: Connection state changed for sun.nio.ch.SelectionKeyImpl@2adb1d4
2013-04-17 15:14:49.718 INFO com.couchbase.client.ViewConnection: Añadido localhost a la cola de conexión
2013-04-17 15:14:49.720 INFO com.couchbase.client.CouchbaseClient: la propiedad viewmode no está definida. Estableciendo viewmode a modo de producción.
2013-04-17 15:14:49.856 INFO com.couchbase.client.CouchbaseConnection: Shut down Couchbase client
2013-04-17 15:14:49.861 INFO com.couchbase.client.ViewConnection: Node localhost has no ops in the queue
2013-04-17 15:14:49.861 INFO com.couchbase.client.ViewNode: Reactor de E/S terminado para localhost
Si te estás conectando a un Couchbase Server 1.8 o contra un Memcache-Bucket no verás que se establezcan conexiones View:
INFO: No se ha podido cargar el fichero de propiedades "cbclient.properties" porque: Archivo no encontrado con el cargador de clases del sistema.
2013-04-17 15:16:44.295 INFO com.couchbase.client.CouchbaseConnection: Added {QA sa=/192.168.56.101:11210, #Rops=0, #Wops=0, #iq=0, topRop=null, topWop=null, toWrite=0, interested=0} to connect queue
2013-04-17 15:16:44.297 INFO com.couchbase.client.CouchbaseConnection: Added {QA sa=/192.168.56.102:11210, #Rops=0, #Wops=0, #iq=0, topRop=null, topWop=null, toWrite=0, interested=0} to connect queue
2013-04-17 15:16:44.298 INFO com.couchbase.client.CouchbaseConnection: Added {QA sa=/192.168.56.103:11210, #Rops=0, #Wops=0, #iq=0, topRop=null, topWop=null, toWrite=0, interested=0} to connect queue
2013-04-17 15:16:44.298 INFO com.couchbase.client.CouchbaseConnection: Added {QA sa=/192.168.56.104:11210, #Rops=0, #Wops=0, #iq=0, topRop=null, topWop=null, toWrite=0, interested=0} to connect queue
2013-04-17 15:16:44.306 INFO com.couchbase.client.CouchbaseConnection: Connection state changed for sun.nio.ch.SelectionKeyImpl@38b5dac4
2013-04-17 15:16:44.313 INFO com.couchbase.client.CouchbaseClient: la propiedad viewmode no está definida. Estableciendo viewmode a modo de producción.
2013-04-17 15:16:44.332 INFO com.couchbase.client.CouchbaseConnection: Connection state changed for sun.nio.ch.SelectionKeyImpl@69945ce
2013-04-17 15:16:44.333 INFO com.couchbase.client.CouchbaseConnection: Estado de conexión cambiado para sun.nio.ch.SelectionKeyImpl@6766afb3
2013-04-17 15:16:44.334 INFO com.couchbase.client.CouchbaseConnection: Connection state changed for sun.nio.ch.SelectionKeyImpl@2b2d96f2
2013-04-17 15:16:44.368 INFO net.spy.memcached.auth.AuthThread: Autenticado en 192.168.56.103/192.168.56.103:11210
2013-04-17 15:16:44.368 INFO net.spy.memcached.auth.AuthThread: Autenticado en 192.168.56.102/192.168.56.102:11210
2013-04-17 15:16:44.369 INFO net.spy.memcached.auth.AuthThread: Autenticado en 192.168.56.101/192.168.56.101:11210
2013-04-17 15:16:44.369 INFO net.spy.memcached.auth.AuthThread: Autenticado en 192.168.56.104/192.168.56.104:11210
2013-04-17 15:16:44.490 INFO com.couchbase.client.CouchbaseConnection: Shut down Couchbase client

Fase 2: Operaciones
Cuando el SDK está arrancado, permite a su aplicación ejecutar operaciones contra el clúster adjunto. A efectos de esta entrada de blog, tenemos que distinguir entre las operaciones que se ejecutan en un clúster estable y las operaciones en un clúster que está experimentando algún tipo de cambio de topología (ya sea planificado debido a la adición de nodos o no planificado debido al fallo de un nodo). Abordemos primero las operaciones regulares.

Operaciones contra un clúster estable

Aunque no es directamente visible en primer lugar, dentro del SDK necesitamos distinguir entre operaciones memcached y operaciones View. Todas las operaciones que tienen una clave única en la firma de su método pueden ser consideradas operaciones memcached. Todas ellas acaban siendo canalizadas a través de spy. Por otro lado, las operaciones View se implementan completamente dentro del propio SDK.
Tanto las operaciones de View como las de memcached son asíncronas. Dentro de spy, hay un hilo (llamado hilo de E/S) dedicado a tratar las operaciones de E/S. Tenga en cuenta que en entornos de alto tráfico, no es raro que este hilo esté siempre activo. Utiliza los mecanismos Java NIO no bloqueantes para gestionar el tráfico, y realiza bucles alrededor de "selectores" que reciben notificaciones cuando los datos pueden escribirse o leerse. Si haces un perfil de tu aplicación verás que este hilo pasa la mayor parte del tiempo esperando en un método select, lo que significa que está ahí esperando a ser notificado de nuevo tráfico. Los conceptos utilizados dentro de spy para tratar con esto son conocimientos comunes de Java NIO, por lo que es posible que desees echar un vistazo a la sección Funciones internas de NIO antes de profundizar en esa ruta de código. Buenos puntos de partida son net.spy.memcached.MemcachedConnection y net.spy.memcached.protocol.TCPMemcachedNodeImpl clases. Ten en cuenta que dentro del SDK, sobreescribimos MemcachedConnection para enganchar nuestra propia lógica de reconfiguración. Esta clase se puede encontrar dentro del SDK en com.couchbase.client.CouchbaseConnection y para los buckets de tipo memcached en com.couchbase.client.CouchbaseMemcachedConnection.
Así, si una operación memcached (como get()) se emite, se pasa hacia abajo hasta que llega al hilo IO. El hilo IO lo pondrá entonces en una cola de escritura hacia su nodo de destino. Finalmente se escribe y luego el hilo IO añade información a una cola de lectura para que las respuestas puedan ser mapeadas en consecuencia. Este enfoque se basa en futuros, por lo que cuando el resultado realmente llega, el futuro se marca como completado, el resultado se analiza y se adjunta como objeto.
El SDK sólo utiliza el protocolo binario de memcached, aunque spy también soportaría ASCII. El formato binario es mucho más eficiente y algunas de las operaciones avanzadas sólo se implementan allí.
Puede que te preguntes cómo sabe el SDK dónde enviar la operación. Como ya tenemos el mapa del cluster actualizado, podemos hacer un hash de la clave y luego basándonos en la lista de nodos y el vBucketMap determinar a qué nodo acceder. El vBucketMap no sólo contiene la información del nodo maestro del array, sino también la información de cero a tres nodos réplica. Mira este ejemplo (abreviado):
vBucketServerMap: {
algoritmo hash: "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]
//…..
},
El serverList contiene nuestros nodos, y el vBucketMap tiene punteros al array serverList. Tenemos 1024 vBuckets, así que sólo algunos de ellos se muestran aquí. Puedes ver que todas las claves que entran en el primer vBucket tienen su nodo maestro en el índice 0 (el nodo .101) y su réplica en el índice 1 (el nodo .102). Una vez que el mapa del cluster cambie y los vBuckets se muevan, solo necesitamos actualizar nuestra configuración y saber todo el tiempo hacia donde apuntar nuestras operaciones.
Las operaciones de vista se gestionan de forma diferente. Dado que las vistas no se pueden enviar a un nodo específico (porque no tenemos una forma de hacer hash de una clave o algo así), hacemos round-robin entre los nodos conectados. La operación se asigna a un com.couchbase.client.ViewNode una vez que tiene conexiones libres y luego se ejecuta. El resultado también se maneja a través de futuros. Para implementar esta funcionalidad, el SDK utiliza la librería de terceros Apache HTTP Commons (NIO).

Toda la View API se esconde detrás del puerto 8092 en cada nodo y es muy similar a CouchDB. También contiene una API RESTful, pero la estructura es un poco diferente. Por ejemplo, puede acceder a un documento de diseño en /_diseño/. Contiene las definiciones de Vista en JSON:

{
lenguaje: "javascript",
vistas: {
todos: {
map: "function (doc) { if(doc.type == "city") {emit([doc.continente, doc.país, doc.nombre], 1)}}",
reducir: "_suma"
}
}
}
A continuación, puede bajar un nivel como /_design/_view/ para consultarlo:
{"total_rows":9,"filas":[
{"id":"ciudad:shanghai","llave":["asia","china","shanghai"],"valor":1},
{"id":"ciudad:tokio","llave":["asia","japón","tokyo"],"valor":1},
{"id":"ciudad:moscú","llave":["asia","rusia","moscú"],"valor":1},
{"id":"ciudad:viena","llave":["europa","austria","viena"],"valor":1},
{"id":"ciudad:parís","llave":["europa","france","parís"],"valor":1},
{"id":"ciudad:roma","llave":["europa","italia","roma"],"valor":1},
{"id":"ciudad:amsterdam","llave":["europa","PAÍSES BAJOS","amsterdam"],"valor":1},
{"id":"ciudad:nueva_york","llave":["norte_america","usa","nueva_york"],"valor":1},
{"id":"ciudad:san_francisco","llave":["norte_america","usa","san_francisco"],"valor":1}
]
}
Una vez que se envía la solicitud y se recibe una respuesta, depende del tipo de solicitud de vista para determinar cómo se analiza la respuesta. Esto supone una diferencia, ya que las consultas de vistas reducidas tienen un aspecto diferente a las no reducidas. El SDK también incluye soporte para vistas espaciales, que también deben tratarse de forma diferente.
Toda la implementación del análisis sintáctico de la respuesta View se encuentra dentro del archivo com.couchbase.client.protocol.views . Allí encontrarás clases abstractas e interfaces como ViewResponse, y luego sus implementaciones especiales como ViewResponseNoDocs, ViewResponseWithDocs o ViewResponseReduced. También es diferente si se utiliza setIncludeDocs() en el objeto Query, porque el SDK también necesita cargar los documentos completos utilizando el protocolo memcached entre bastidores. Esto también se hace mientras se analizan las vistas.
Ahora que tiene una comprensión básica de cómo el SDK distribuye sus operaciones en condiciones estables, tenemos que cubrir un tema importante: cómo el SDK se ocupa de los cambios de topología del clúster.

Operaciones contra un clúster de reequilibrio

Tenga en cuenta que hay una próxima entrada de blog separada que trata de todos los escenarios que pueden surgir cuando algo va mal en el SDK. Dado que el reequilibrio y la conmutación por error son partes cruciales del SDK, esta entrada trata más sobre el proceso general de cómo se gestiona.
Como se mencionó anteriormente, el SDK recibe actualizaciones de topología a través de la conexión de streaming. Dejando a un lado el caso especial en el que este nodo realmente se elimine o falle, todas las actualizaciones se transmitirán casi en tiempo real (en una arquitectura eventualmente consistente, puede pasar algún tiempo hasta que las actualizaciones del clúster lleguen a ese nodo). Los trozos que llegan a través del flujo son exactamente iguales a los que hemos visto al leer la configuración inicial. Una vez analizados esos trozos, tenemos que comprobar si los cambios afectan realmente al SDK (dado que hay muchos más parámetros de los que el SDK necesita, no tendrá sentido escucharlos todos). Todos los cambios que afectan a la topología y/o al mapa de vBucket se consideran importantes. Si se añaden o eliminan nodos (ya sea por fallo o de forma planificada), necesitamos abrir o cerrar los sockets. Este proceso se denomina "reconfiguración".
Una vez que se activa una reconfiguración de este tipo, muchas acciones tienen que ocurrir en varios lugares. Spymemcached necesita manejar sus sockets, los nodos View necesitan ser gestionados y la nueva configuración necesita ser actualizada. El SDK se asegura de que sólo una reconfiguración puede ocurrir al mismo tiempo a través de bloqueos para que no tengamos ninguna condición de carrera.
El BucketUpdateResponseHandler basado en Netty dispara el método CouchbaseClient#reconfigure, que entonces comienza a despachar todo. Dependiendo del tipo de bucket utilizado (por ejemplo, los buckets de tipo memcached no tienen Views y por tanto no tienen ViewNodes), se actualizan las configuraciones y se cierran los sockets. Una vez hecha la reconfiguración, puede recibir nuevas. Durante los cambios planificados, todo debería estar bastante controlado y ninguna operación debería fallar. Si un nodo está realmente caído y no se puede llegar a él, esas operaciones se cancelarán. La reconfiguración es complicada porque la topología cambia mientras las operaciones fluyen por el sistema.
Por último, vamos a cubrir algunas diferencias entre los buckets de tipo Couchbase y Memcache. Toda la información que has estado leyendo anteriormente sólo se aplica a los buckets Couchbase. Los buckets Memcache son bastante básicos y no tienen el concepto de vBuckets. Al no tener vBuckets, todo lo que el Cliente tiene que hacer es gestionar los nodos y sus correspondientes sockets. Además, se utiliza un algoritmo hash diferente (principalmente Ketama) para determinar el nodo de destino para cada clave. Además, los cubos de memcache no tienen vistas, por lo que no se puede utilizar la API de vistas y no tiene mucho sentido mantener sockets de vistas. Así que para aclarar la afirmación anterior, si estás ejecutando contra un cubo memcache, para un cluster de 10 nodos sólo tendrás 11 conexiones abiertas.

Fase 3: Apagado

Una vez que el método CouchbaseClient#shutdown() es llamado, no se permiten más operaciones en CouchbaseConnection. Hasta que se alcance el tiempo de espera, el cliente quiere asegurarse de que todas las operaciones se han realizado correctamente. Todos los sockets para las conexiones memcached y View se cierran una vez que no hay más operaciones en la cola (o son descartadas). Ten en cuenta que los métodos de cierre de esos sockets también se utilizan cuando un nodo se elimina del clúster durante las operaciones normales, por lo que es básicamente lo mismo, pero sólo para todos los nodos conectados al mismo tiempo.

Resumen

Después de leer esta entrada del blog, deberías tener una idea mucho más clara de cómo funciona el SDK de cliente y por qué está diseñado de la forma en que lo está. Tenemos muchas mejoras previstas para futuras versiones, sobre todo para mejorar la experiencia directa con la API. Tenga en cuenta que esta entrada de blog no cubre cómo se gestionan los errores dentro del SDK; esto se publicará en una entrada de blog separada porque también hay mucha información que cubrir.

Autor

Publicado por Michael Nitschinger

Michael Nitschinger trabaja como Ingeniero de Software Principal en Couchbase. Es el arquitecto y mantenedor del SDK Java de Couchbase, uno de los primeros controladores de bases de datos completamente reactivos en la JVM. También es autor y mantiene el conector Spark de Couchbase. Michael participa activamente en la comunidad de código abierto, contribuyendo a otros proyectos como RxJava y Netty.

Dejar una respuesta