Статьи

Couchbase Java SDK Internals

Примечание куратора: содержание этой статьи изначально было написано Майклом Нитчингером здесь:  http://nitschinger.at/

мотивация

Этот пост предназначен для очень подробной и информативной статьи для тех, кто уже использовал Couchbase Java SDK и хочет знать, как работают внутренние компоненты. Это не введение в то, как использовать Java SDK, и мы рассмотрим некоторые довольно сложные темы.


Обычно, говоря о SDK, мы имеем в виду все, что нужно для работы (клиентская библиотека, документация, заметки о выпуске и т. Д.).
В этой статье SDK ссылается на клиентскую библиотеку (код), если не указано иное.
 

Вступление

Прежде всего, важно понимать, что SDK обертывает и расширяет функциональность библиотеки memcached spymemcached (называемой «spy»). Одним из внутренних протоколов является протокол memcached, и многие функции можно использовать повторно. С другой стороны, как только вы начнете снимать первые слои SDK, вы заметите, что некоторые компоненты несколько сложнее из-за того факта, что шпион предоставляет больше возможностей, чем нужно SDK. Другая часть состоит в том, чтобы помнить, что многие компоненты переплетены, поэтому вы всегда должны правильно понимать зависимость. Большую часть времени мы выпускаем новую версию шпиона в тот же день с новым SDK, потому что новый материал был добавлен или исправлен.


Таким образом, помимо повторного использования функциональности, предоставляемой шпионом, SDK в основном добавляет два блока функциональности: автоматическое управление топологией кластера и начиная с версии 1.1 (и сервера 2.0) для представлений.
Помимо этого он также предоставляет административные средства, такие как ведение и управление проектной документацией.
 

Чтобы понять, как работает клиент, мы разберем весь процесс на разных этапах жизненного цикла клиента.
После того, как мы пройдем все три этапа (начальная загрузка, работа и отключение), у вас должно быть четкое представление о том, что происходит под капотом. Обратите внимание, что в процессе подготовки к обработке ошибок есть отдельная запись в блоге, поэтому мы не будем здесь подробно останавливаться на ней (которая будет опубликована через несколько недель в том же блоге здесь).

Фаза 1: Бутстрап

Прежде чем мы сможем начать обслуживать такие операции, как get () и set () , нам нужно загрузить объект CouchbaseClient . Важная часть, которую нам необходимо выполнить, — это сначала получить конфигурацию кластера (которая содержит узлы и карту vBucket), а также установить потоковое соединение для получения обновлений кластера в (почти) режиме реального времени.


Мы берем список узлов, проходящих во время начальной загрузки, и перебираем его.
Первый узел в списке, с которым можно связаться через порт 8091, используется для обхода интерфейса RESTful на сервере. Если он недоступен, будет испробован следующий. Это означает, что, исходя из предоставленного
http: // host: port / pool URI, мы в конечном итоге перейдем по ссылкам на сущность корзины. Все это происходит внутри
ConfigurationProvider , который в данном случае является
com.couchbase.client.vbucket.ConfigurationProviderHTTP . Если вы хотите покопаться во внутренних
органах,
поищите методы getBucketConfiguration и
readPools .
 

(Успешная) прогулка может быть проиллюстрирована следующим образом:
 
  1. GET / бассейны
  2. ищите пулы «по умолчанию»
  3. GET / pool / default
  4. ищите хеш «buckets», который содержит список bucket
  5. GET / pool / default / buckets
  6. разобрать список ведер и извлечь тот, который предоставляется приложением
  7. GET / pool / default / buckets /

Теперь мы находимся в конечной точке REST, которая нам нужна. В этом ответе JSON вы найдете все полезные детали, которые также используются SDK для внутреннего использования (например, streamingUri , node и vBucketServerMap ). Конфиг анализируется и сохраняется. Прежде чем мы продолжим, давайте быстро обсудим странную часть пулов в нашей прогулке по REST:


Концепция пула ресурсов для группировки сегментов была разработана для Couchbase Server, но в настоящее время не реализована. Тем не менее, REST API реализован таким образом, и поэтому все SDK поддерживают его. Тем не менее, хотя теоретически мы могли бы просто перейти непосредственно к
/ pools / default / buckets и пропустить первые несколько запросов, текущее поведение является будущим, поэтому вам не придется изменять загрузочный код, как только сервер его реализует.
 

Вернемся к нашей фазе начальной загрузки. Теперь, когда у нас есть действующая конфигурация кластера, которая содержит все узлы (и их имена хостов или IP-адреса), мы можем устанавливать с ними соединения. Помимо установления подключений к данным, нам также необходимо создать потоковое подключение к одному из них. По причинам простоты мы просто устанавливаем потоковое соединение с узлом из списка, в котором мы получили нашу первоначальную конфигурацию.
 

Это подводит нас к важному моменту, о котором следует помнить: если у вас много
объектов
CouchbaseClient, запущенных на многих узлах, и все они загружаются с одним и тем же списком, они могут в конечном итоге подключиться к одному узлу для потокового соединения и создать возможное узкое место. Поэтому, чтобы распределить нагрузку немного лучше, я рекомендую перетасовать массив перед его передачей в
объект
CouchbaseClient . Когда у вас есть только несколько
объектов
CouchbaseClient, подключенных к вашему кластеру, это не будет проблемой вообще.
 

URI потокового соединения берется из конфигурации, которую мы получили ранее, и обычно выглядит так:
streamingUri: "/pools/default/bucketsStreaming/default?bucket_uuid=88cae4a609eea500d8ad072fe71a7290"

   Если вы укажете свой браузер на этот адрес, вы также получите обновления топологии кластера в режиме реального времени. Поскольку потоковое соединение необходимо устанавливать постоянно и потенциально блокировать поток, это делается в фоновом режиме, обрабатываемом различными потоками. Для этой задачи мы используем инфраструктуру NIO Netty, которая обеспечивает очень удобный способ работы с асинхронными операциями. Если вы хотите начать копаться в этой части, имейте в виду, что все операции чтения полностью отделены от операций записи, поэтому вам нужно иметь дело с обработчиками, которые заботятся о том, что возвращается с сервера. Помимо некоторой проводки, необходимой для Netty, бизнес-логику можно найти в com.couchbase.client.vbucket.BucketMonitor и com.couchbase.client.vbucket.BucketUpdateResponseHandler.Мы также пытаемся восстановить это потоковое соединение, если сокет закрывается (например, если этот узел перебалансирован из кластера).
 

Чтобы фактически перетасовать данные на узлы кластера, нам нужно открыть для них различные сокеты.
Обратите внимание, что внутри клиента абсолютно не требуется пул соединений, потому что мы активно управляем всеми сокетами. Помимо специального потокового соединения с одним из серверов (которое открыто для порта 8091), нам нужно открыть следующие соединения:
 
  1. Разъем Memcached: порт 11210
  2. Просмотр сокета: порт 8092

Обратите внимание, что порт 11211 не используется внутри клиентских SDK, но используется для подключения общих клиентов memcached, которые не поддерживают кластер. Это означает, что эти общие клиенты не получают обновленные топологии кластера.


Так что, как правило, если у вас работает кластер из 10 узлов, один объект CouchbaseClient откроет около 21 (2 * 10 + 1) клиентских сокетов.
Они управляются напрямую, поэтому, если узел будет удален или добавлен, номера изменятся соответственно.
 

Теперь, когда все сокеты открыты, мы готовы выполнять обычные кластерные операции.
Как видите, при загрузке объекта CouchbaseClient возникает много накладных расходов. В связи с этим мы настоятельно рекомендуем вам не создавать новый объект при каждом запросе или запускать множество объектов CouchbaseClient на одном сервере приложений. Это только добавляет ненужные накладные расходы и нагрузку на сервер приложений, а также увеличивает общее количество сокетов, открытых для кластера (что приводит к возможной проблеме производительности).
 

Для справки: при включенном ведении журнала на уровне INFO подключение и отключение кластера с 1 узлом (Couchbase Bucket) должны выглядеть следующим образом:
Apr 17, 2013 3:14:49 PM com.couchbase.client.CouchbaseProperties setPropertyFile
INFO: Could not load properties file "cbclient.properties" because: File not found with system classloader.
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:  Added localhost to connect queue
2013-04-17 15:14:49.720 INFO com.couchbase.client.CouchbaseClient:  viewmode property isn't defined. Setting viewmode to production mode
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:  I/O reactor terminated for localhost
Если вы подключаетесь к Couchbase Server 1.8 или к Memcache-Bucket, вы не увидите Просмотр установленных соединений:
INFO: Could not load properties file "cbclient.properties" because: File not found with system classloader.
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:  viewmode property isn't defined. Setting viewmode to production mode
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:  Connection state changed for 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:  Authenticated to 192.168.56.103/192.168.56.103:11210
2013-04-17 15:16:44.368 INFO net.spy.memcached.auth.AuthThread:  Authenticated to 192.168.56.102/192.168.56.102:11210
2013-04-17 15:16:44.369 INFO net.spy.memcached.auth.AuthThread:  Authenticated to 192.168.56.101/192.168.56.101:11210
2013-04-17 15:16:44.369 INFO net.spy.memcached.auth.AuthThread:  Authenticated to 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

Этап 2. Операции
Когда SDK загружен, он позволяет вашему приложению выполнять операции с подключенным кластером. Для этого поста в блоге нам необходимо различать операции, выполняемые в отношении стабильного кластера, и операции в кластере, который в настоящее время испытывает некоторую форму изменения топологии (будь то запланированное из-за добавления узлов или незапланированное из-за сбоя узла). ). Давайте сначала займемся обычными операциями.

Операции против стабильного кластера


Хотя в SDK это не видно напрямую, внутри SDK мы должны различать операции memcached и операции View. Все операции, имеющие уникальный ключ в сигнатуре метода, могут обрабатываться как операции memcached. Все они в конечном итоге попадают через шпиона. С другой стороны, операции просмотра полностью реализованы внутри самого SDK.
 

Операции View и memcached являются асинхронными. Внутри шпиона есть один поток (вызов потока ввода-вывода), предназначенный для работы с операциями ввода-вывода. Обратите внимание, что в средах с высоким трафиком весьма обычно то, что этот поток всегда активен. Он использует неблокирующие механизмы Java NIO для работы с трафиком и использует циклы вокруг «селекторов», которые получают уведомление, когда данные могут быть либо записаны, либо прочитаны. Если вы профилируете свое приложение, вы увидите, что этот поток тратит большую часть своего времени на ожидание метода select, это означает, что он находится в режиме ожидания и ожидает уведомления о новом трафике. Концепции, используемые внутри шпиона для решения этой проблемы, являются общими знаниями Java NIO, поэтому вы можете
сначала изучить
внутреннюю часть NIO, прежде чем копаться в этом пути кода. Хорошей отправной точкой являются
Классы net.spy.memcached.MemcachedConnection и
net.spy.memcached.protocol.TCPMemcachedNodeImpl . Обратите внимание, что внутри SDK мы переопределяем MemcachedConnection, чтобы подключить нашу собственную логику реконфигурации. Этот класс можно найти внутри SDK по адресу
com.couchbase.client.CouchbaseConnection и для блоков типа memcached в
com.couchbase.client.CouchbaseMemcachedConnection .
 

Поэтому, если выполняются операции memcached (например,
get () ), они передаются до тех пор, пока не достигнут потока ввода-вывода. Затем поток ввода-вывода поместит его в очередь записи в направлении своего целевого узла. Это в конечном итоге записывается, а затем поток ввода-вывода добавляет информацию в очередь чтения, чтобы ответы могли отображаться соответственно. Этот подход основан на фьючерсах, поэтому, когда результат действительно приходит, будущее помечается как завершенное, результат анализируется и присоединяется как объект.
 

SDK использует только двоичный протокол memcached, хотя шпион также поддерживает ASCII.
Бинарный формат намного эффективнее, и некоторые из расширенных операций реализованы только там.
 

Вы можете задаться вопросом, как SDK знает, куда отправить операцию?
Поскольку у нас уже есть современная карта кластеров, мы можем хэшировать ключ, а затем на основе списка узлов и vBucketMap определить, к какому узлу получить доступ. VBucketMap содержит не только информацию для главного узла массива, но также информацию от нуля до трех узлов реплики. Посмотрите на этот (сокращенный) пример:
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]
//.....
},
   The serverList contains our nodes, and the vBucketMap has pointers to the serverList array. We have 1024 vBuckets, so only some of them are shown here. You can see from looking at it that all keys that has into the first vBucket have its master node at index 0 (so the .101 node) and its replica at index 1 (so the .102 node). Once the cluster map changes and the vBuckets move around, we just need to update our config and know all the time where to point our operations towards.
 
View operations are handled differently. Since views can’t be sent to a specific node (because we don’t have a way to hash a key or something), we round-robin between the connected nodes. The operation gets assigned to a
com.couchbase.client.ViewNode once it has free connections and then executed. The result is also handled through futures. To implement this functionality, the SDK uses the third party Apache HTTP Commons (NIO) library.

The whole View API hides behind port 8092 on every node and is very similar to CouchDB. It also contains a RESTful API, but the structure is a little bit different. For example, you can reach a design document at /_design/. It contains the View definitions in JSON:

{
    language: "javascript",
    views: {
        all: {
            map: "function (doc) { if(doc.type == "city") {emit([doc.continent, doc.country, doc.name], 1)}}",
            reduce: "_sum"
        }
    }
}

  You can then reach down one level further like /_design/_view/ to actually query it:

{"total_rows":9,"rows":[
{"id":"city:shanghai","key":["asia","china","shanghai"],"value":1},
{"id":"city:tokyo","key":["asia","japan","tokyo"],"value":1},
{"id":"city:moscow","key":["asia","russia","moscow"],"value":1},
{"id":"city:vienna","key":["europe","austria","vienna"],"value":1},
{"id":"city:paris","key":["europe","france","paris"],"value":1},
{"id":"city:rome","key":["europe","italy","rome"],"value":1},
{"id":"city:amsterdam","key":["europe","netherlands","amsterdam"],"value":1},
{"id":"city:new_york","key":["north_america","usa","new_york"],"value":1},
{"id":"city:san_francisco","key":["north_america","usa","san_francisco"],"value":1}
]
}

Once the request is sent and a response gets back, it depends on the type of View request to determine on how the response gets parsed. It makes a difference, because reduced View queries look different than non-reduced. The SDK also includes support for spatial Views and they need to be handled differently as well.
 
The whole View response parsing implementation can be found inside the
com.couchbase.client.protocol.views namespace. You’ll find abstract classes and interfaces like ViewResponse in there, and then their special implementations like ViewResponseNoDocs, ViewResponseWithDocs or ViewResponseReduced. It also makes a different if setIncludeDocs() is used on the Query object, because the SDK also needs to load the full documents using the memcached protocol behind the scenes. This is also done while parsing the Views.
 
Now that you have a basic understanding on how the SDK distributes its operations under stable conditions, we need to cover an important topic: how the SDK deals with cluster topology changes.
 

Operations against a rebalancing cluster

Note that there is a separate blog post upcoming dealing with all the scenarios that may come up when something goes wrong on the SDK. Since rebalancing and failover are crucial parts of the SDK, this post deals more with the general process on how this is handled.
 
As mentioned earlier, the SDK receives topology updates through the streaming connection. Leaving the special case aside where this node actually gets removed or fails, all updates will stream in near real-time (in a eventually consistent architecture, it may take some time until the cluster updates get populated to that node). The chunks that come in over the stream look exactly like the ones we’ve seen when reading the initial configuration. After those chunks have been parsed, we need to check if the changes really affect the SDK (since there are many more parameters than the SDK needs, it won’t make sense to listen to all of them). All changes that affect the topology and/or vBucket map are considered as important. If nodes get added or removed (be it either through failure or planned), we need to open or close the sockets. This process is called «reconfiguration».
 
Once such a reconfiguration is triggered, lots of actions need to happen in various places. Spymemcached needs to handle its sockets, View nodes need to be managed and new configuration needs to be updated. The SDK makes sure that only one reconfiguration can happen at the same time through locks so we don’t have any race conditions going on.
 
The Netty-based BucketUpdateResponseHandler triggers the CouchbaseClient#reconfigure method, which then starts to dispatch everything. Depending on the bucket type used (i.e. memcached type buckets don’t have Views and therefore no ViewNodes), configs are updated and sockets closed. Once the reconfiguration is done, it can receive new ones. During planned changes, everything should be pretty much controlled and no operations should fail. If a node is actually down and cannot be reached, those operations will be cancelled. Reconfiguration is tricky because the topology changes while operations are flowing through the system.
 
Finally, let’s cover some differences between Couchbase and Memcache type buckets. All the information hat you’ve been reading previously only applies to Couchbase buckets. Memcache buckets are pretty basic and do not have the concept of vBuckets. Since you don’t have vBuckets, all that the Client has to do is to manage the nodes and their corresponding sockets. Also, a different hashing algorithm is used (mostly Ketama) to determine the target node for each key. Also, memcache buckets don’t have views, so you can’t use the View API and it doesn’t make much sense to keep View sockets around. So to clarify the previous statement, if you are running against a memcache bucket, for a 10 node cluster you’ll only have 11 open connections.
 

Phase 3: Shutdown

Once the CouchbaseClient#shutdown() method is called, no more operations are allowed to be added onto the CouchbaseConnection. Until the timeout is reached, the client wants to make sure that all operations went through accordingly. All sockets for both memcached and View connections are shut down once there are no more operations in the queue (or they get dropped). Note that that the shutdown methods on those sockets are also used when a node gets removed from the cluster during normal operations, so it’s basically the same, but just for all attached nodes at the same time.
 

Summary

After reading this blog post, you should have a much more clear picture on how the client SDK works and why it is designed the way it is. We have lots of enhancements planned for future releases, mostly enhancing the direct API experience. Note that this blog post didn’t cover how errors are handled inside the SDK; this will be published in a separate blog post because there is also lots of information to cover.