Перейти к содержанию

API etcd

Обзор основной конструкции API etcd

Этот документ содержит обзор основной конструкции API etcd v3. Не следует путать его с API etcd v2, объявленным устаревшим в etcd v3.5. Документ не претендует на полноту, а сосредоточен на основных идеях, необходимых для понимания etcd, без отвлечения на менее распространённые вызовы API. Все API etcd определены в службах gRPC , которые группируют удалённые вызовы процедур (RPC), распознаваемые сервером etcd. Полный перечень RPC etcd приведён в формате Markdown в справочнике API gRPC .

Службы gRPC

Каждый запрос API, отправленный серверу etcd, является удалённым вызовом процедуры gRPC. RPC в etcd объединяются в службы по назначению.

К важным для работы с пространством ключей etcd службам относятся:

  • KV — создаёт, обновляет, получает и удаляет пары «ключ — значение».
  • Watch — отслеживает изменения ключей.
  • Lease — предоставляет примитивы для обработки клиентских сообщений поддержания активности.

К службам управления самим кластером относятся:

  • Auth — механизм аутентификации пользователей на основе ролей.
  • Cluster — предоставляет сведения о составе кластера и средства конфигурации.
  • Maintenance — создаёт снимки для восстановления, дефрагментирует хранилище и возвращает сведения о состоянии отдельных участников.

Запросы и ответы

Все RPC в etcd имеют одинаковый формат. Каждый RPC содержит функцию Name, которая принимает NameRequest как аргумент и возвращает NameResponse как ответ. Например, RPC Range описывается следующим образом:

service KV {
  Range(RangeRequest) returns (RangeResponse)
  ...
}

Заголовок ответа

Все ответы API etcd содержат заголовок с метаданными кластера для данного ответа:

message ResponseHeader {
  uint64 cluster_id = 1;
  uint64 member_id = 2;
  int64 revision = 3;
  uint64 raft_term = 4;
}
  • Cluster_ID — идентификатор кластера, создавшего ответ.
  • Member_ID — идентификатор участника, создавшего ответ.
  • Revision — ревизия хранилища ключей и значений на момент создания ответа.
  • Raft_Term — срок полномочий Raft участника на момент создания ответа.

Приложение может прочитать поле Cluster_ID или Member_ID, чтобы убедиться, что взаимодействует с предполагаемым кластером (участником).

По полю Revision приложения могут узнать последнюю ревизию хранилища ключей и значений. Это особенно полезно, когда приложение задаёт историческую ревизию для time travel query и хочет определить последнюю ревизию на момент запроса.

С помощью Raft_Term приложения могут определить момент завершения новых выборов лидера в кластере.

API ключей и значений

API ключей и значений управляет парами «ключ — значение», хранящимися в etcd. Обычно большинство запросов к etcd относится именно к ним.

Системные примитивы

Пара «ключ — значение»

Пара «ключ — значение» — минимальная единица, которой может управлять соответствующий API. Каждая пара содержит ряд полей, определённых в формате protobuf :

message KeyValue {
  bytes key = 1;
  int64 create_revision = 2;
  int64 mod_revision = 3;
  int64 version = 4;
  bytes value = 5;
  int64 lease = 6;
}
  • Key — ключ в байтах. Пустой ключ недопустим.
  • Value — значение в байтах.
  • Version — версия ключа. Удаление сбрасывает её в ноль, а любое изменение ключа увеличивает версию.
  • Create_Revision — ревизия последнего создания ключа.
  • Mod_Revision — ревизия последнего изменения ключа.
  • Lease — идентификатор аренды, присоединённой к ключу. Если lease равен 0, аренда к ключу не присоединена.

Помимо ключа и значения etcd добавляет в сообщение ключа метаданные ревизии. Они упорядочивают ключи по времени создания и изменения, что полезно для управления параллелизмом при распределённой синхронизации. Распределённые совместные блокировки клиента etcd используют ревизию создания при ожидании владения блокировкой. Аналогично, ревизия изменения применяется для обнаружения конфликтов набора чтения программной транзакционной памяти и ожидания обновлений выборов лидера .

Ревизии

etcd поддерживает общий для кластера 64-битный счётчик — ревизию хранилища, которая увеличивается при каждом изменении пространства ключей. Ревизия служит глобальными логическими часами, последовательно упорядочивающими все обновления хранилища. Изменение, представленное новой ревизией, является инкрементным: связанные с ревизией данные — это данные, изменившие хранилище. На внутреннем уровне новая ревизия означает запись изменений в B+tree бэкенда с увеличенной ревизией в качестве ключа.

Особую ценность ревизии приобретают в бэкенде etcd с многоверсионным управлением параллелизмом . Модель MVCC позволяет просматривать хранилище ключей и значений на прошлых ревизиях, поскольку исторические ревизии ключей сохраняются. Администраторы кластера могут настроить политику хранения этой истории для точного управления хранилищем; обычно etcd удаляет старые ревизии ключей по таймеру. Типичный кластер etcd хранит замещённые данные ключей несколько часов. Это также обеспечивает надёжную обработку длительных отключений клиентов, а не только временных сетевых сбоев: наблюдатели просто продолжают работу с последней замеченной исторической ревизии. Аналогично, для чтения хранилища в определённый момент запрос чтения можно пометить ревизией, чтобы вернуть ключи из представления пространства на момент фиксации этой ревизии.

Диапазоны ключей

Модель данных etcd индексирует все ключи в плоском двоичном пространстве. Этим она отличается от других хранилищ ключей и значений, использующих иерархическую организацию ключей в каталогах. Вместо перечисления по каталогам ключи перечисляются по интервалам [a, b).

В etcd эти интервалы часто называют «диапазонами». Операции над диапазонами мощнее операций над каталогами. Подобно иерархическому хранилищу, интервалы поддерживают поиск одного ключа через [a, a+1) (например, [‘a’, ‘a\x00’) ищет ‘a’) и поиск в каталоге посредством кодирования ключей по глубине каталога. Кроме того, интервалы могут кодировать префиксы: например, интервал ['a', 'b') ищет все ключи с префиксом ‘a’.

По соглашению диапазон запроса обозначается полями key и range_end. Поле key содержит первый ключ диапазона и не должно быть пустым. range_end — ключ, следующий за последним ключом диапазона. Если range_end не задан или пуст, диапазон содержит только аргумент key. Если range_end равен key плюс один (например, “aa”+1 == “ab”, “a\xff”+1 == “b”), диапазон представляет все ключи с префиксом key. Если и key, и range_end равны ‘\0’, диапазон представляет все ключи. Если ‘\0’ равен только range_end, диапазон содержит все ключи, большие либо равные аргументу key.

Range

Ключи извлекаются из хранилища ключей и значений вызовом API Range, принимающим RangeRequest:

message RangeRequest {
  enum SortOrder {
	NONE = 0; // default, no sorting
	ASCEND = 1; // lowest target value first
	DESCEND = 2; // highest target value first
  }
  enum SortTarget {
	KEY = 0;
	VERSION = 1;
	CREATE = 2;
	MOD = 3;
	VALUE = 4;
  }

  bytes key = 1;
  bytes range_end = 2;
  int64 limit = 3;
  int64 revision = 4;
  SortOrder sort_order = 5;
  SortTarget sort_target = 6;
  bool serializable = 7;
  bool keys_only = 8;
  bool count_only = 9;
  int64 min_mod_revision = 10;
  int64 max_mod_revision = 11;
  int64 min_create_revision = 12;
  int64 max_create_revision = 13;
}
  • Key, Range_End — диапазон извлекаемых ключей.
  • Limit — максимальное количество ключей в ответе. Значение limit, равное 0, означает отсутствие ограничения.
  • Revision — момент состояния хранилища ключей и значений для диапазона. Если revision меньше или равна нулю, диапазон относится к последнему состоянию хранилища. Если ревизия компактизирована, возвращается ответ ErrCompacted.
  • Sort_Order — порядок для отсортированных запросов.
  • Sort_Target — поле пары «ключ — значение» для сортировки.
  • Serializable — задаёт использование сериализуемого локального чтения с участника для диапазонного запроса. По умолчанию Range линеаризуем и отражает текущий консенсус кластера. Ради повышения производительности и доступности ценой возможного чтения устаревших данных сериализуемый запрос диапазона обслуживается локально без достижения консенсуса с другими узлами кластера.
  • Keys_Only — возвращать только ключи без значений.
  • Count_Only — возвращать только количество ключей в диапазоне.
  • Min_Mod_Revision — нижняя граница ревизий изменения ключей; меньшие ревизии отфильтровываются.
  • Max_Mod_Revision — верхняя граница ревизий изменения ключей; большие ревизии отфильтровываются.
  • Min_Create_Revision — нижняя граница ревизий создания ключей; меньшие ревизии отфильтровываются.
  • Max_Create_Revision — верхняя граница ревизий создания ключей; большие ревизии отфильтровываются.

В ответ на вызов Range клиент получает сообщение RangeResponse:

message RangeResponse {
  ResponseHeader header = 1;
  repeated mvccpb.KeyValue kvs = 2;
  bool more = 3;
  int64 count = 4;
}
  • Kvs — список пар «ключ — значение», соответствующих диапазонному запросу. При заданном Count_Only поле Kvs пусто.
  • More — при заданном limit указывает, остались ли в запрошенном диапазоне ключи для возврата.
  • Count — общее количество ключей, удовлетворяющих диапазонному запросу.

Для больших диапазонов ключей, когда буферизация полного ответа нежелательна, используйте RangeStream .

RangeStream

RangeStream возвращает тот же набор результатов, что и Range, однако сервер разбивает ответ на последовательность фрагментов и передаёт их клиенту потоком. Благодаря этому ни одной стороне не нужно целиком буферизовать большие диапазоны в памяти. RangeStream принимает тот же RangeRequest, что и Range.

В ответ на вызов RangeStream клиент получает поток сообщений RangeStreamResponse:

message RangeStreamResponse {
  RangeResponse range_response = 1;
}

Заполнение полей во фрагментах:

  • Kvs — каждый фрагмент содержит непересекающуюся часть результата. Объединение kvs всех фрагментов в порядке получения даёт тот же набор ключей, что и один вызов Range.
  • Header, More, Count — заполняются только в последнем фрагменте и лишь при завершении потока без ошибки. В предыдущих фрагментах эти поля имеют нулевые значения. Применение proto.Merge ко всем range_response фрагментов даёт RangeResponse, эквивалентный ответу Range.

Если поток завершается ошибкой, ни один фрагмент не содержит действительных header, more или count.

Все фрагменты потока обслуживаются относительно одной ревизии. Если запрос не задаёт Revision, при запуске потока сервер фиксирует последнюю зафиксированную ревизию и использует её до конца потока.

RangeStream не поддерживает пользовательские порядки сортировки и фильтры ревизий (min_mod_revision, max_mod_revision, min_create_revision, max_create_revision). Использующие их запросы возвращают Unimplemented. Прокси gRPC etcd также не поддерживает RangeStream.

Существует два распространённых способа обработки RangeStream:

  1. Обрабатывать каждый фрагмент независимо. Подходит для высокопроизводительных сценариев, когда клиент хочет декодировать и обрабатывать ключи по мере поступления, а не сначала собирать весь результат. Клиент перебирает фрагменты и обрабатывает kvs каждого, а после успешного завершения потока читает header, more или count из последнего фрагмента.
  2. Собрать единый ответ. Подходит, когда клиенту требуется результат, эквивалентный унарному Range. Клиент объединяет range_response каждого фрагмента в один RangeResponse (например, через proto.Merge). Объединённый результат содержит полный kvs, а также header, more и count из последнего фрагмента. Для этого шаблона клиент Go предоставляет вспомогательную функцию clientv3.GetStreamToGetResponse.

Put

Ключи сохраняются в хранилище ключей и значений вызовом Put, принимающим PutRequest:

message PutRequest {
  bytes key = 1;
  bytes value = 2;
  int64 lease = 3;
  bool prev_kv = 4;
  bool ignore_value = 5;
  bool ignore_lease = 6;
}
  • Key — имя ключа, записываемого в хранилище ключей и значений.
  • Value — значение в байтах, связываемое с ключом в хранилище.
  • Lease — идентификатор аренды, связываемой с ключом. Значение аренды 0 означает отсутствие аренды.
  • Prev_Kv — если задано, в ответе возвращаются данные пары «ключ — значение» до обновления запросом Put.
  • Ignore_Value — если задано, ключ обновляется без изменения текущего значения. Если ключ не существует, возвращается ошибка.
  • Ignore_Lease — если задано, ключ обновляется без изменения текущей аренды. Если ключ не существует, возвращается ошибка.

В ответ на вызов Put клиент получает сообщение PutResponse:

message PutResponse {
  ResponseHeader header = 1;
  mvccpb.KeyValue prev_kv = 2;
}
  • Prev_Kv — пара «ключ — значение», перезаписанная операцией Put, если в PutRequest было задано Prev_Kv.

Удаление диапазона

Диапазоны ключей удаляются вызовом DeleteRange, принимающим DeleteRangeRequest:

message DeleteRangeRequest {
  bytes key = 1;
  bytes range_end = 2;
  bool prev_kv = 3;
}
  • Key, Range_End — удаляемый диапазон ключей.
  • Prev_Kv — если задано, возвращает содержимое удалённых пар «ключ — значение».

В ответ на вызов DeleteRange клиент получает сообщение DeleteRangeResponse:

message DeleteRangeResponse {
  ResponseHeader header = 1;
  int64 deleted = 2;
  repeated mvccpb.KeyValue prev_kvs = 3;
}
  • Deleted — количество удалённых ключей.
  • Prev_Kv — список всех пар «ключ — значение», удалённых операцией DeleteRange.

Транзакция

Транзакция — атомарная конструкция If/Then/Else над хранилищем ключей и значений. Она предоставляет примитив для объединения запросов в атомарные блоки (then/else), выполнение которых защищено условием (if), основанным на содержимом хранилища. Транзакции позволяют защищать ключи от непреднамеренных параллельных обновлений, строить операции сравнения с обменом и создавать механизмы управления параллелизмом более высокого уровня.

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

Все транзакции защищены конъюнкцией сравнений, подобной оператору If. Каждое сравнение проверяет один ключ в хранилище: отсутствие или наличие значения, равенство заданному значению либо ревизию или версию ключа. Два разных сравнения могут относиться к одному или разным ключам. Все сравнения применяются атомарно. Если они истинны, транзакция считается успешной и etcd применяет блок запросов then / success; иначе транзакция считается неудачной и применяется блок else / failure.

Каждое сравнение кодируется сообщением Compare:

message Compare {
  enum CompareResult {
    EQUAL = 0;
    GREATER = 1;
    LESS = 2;
    NOT_EQUAL = 3;
  }
  enum CompareTarget {
    VERSION = 0;
    CREATE = 1;
    MOD = 2;
    VALUE= 3;
  }
  CompareResult result = 1;
  // target is the key-value field to inspect for the comparison.
  CompareTarget target = 2;
  // key is the subject key for the comparison operation.
  bytes key = 3;
  oneof target_union {
    int64 version = 4;
    int64 create_revision = 5;
    int64 mod_revision = 6;
    bytes value = 7;
  }
}
  • Result — тип логической операции сравнения (например, равно, меньше и т. д.).
  • Target — сравниваемое поле пары «ключ — значение»: версия ключа, ревизия создания, ревизия изменения либо значение.
  • Key — ключ для сравнения.
  • Target_Union — заданные пользователем данные сравнения.

После обработки блока сравнений транзакция применяет блок запросов. Блок представляет собой список сообщений RequestOp:

message RequestOp {
  // request is a union of request types accepted by a transaction.
  oneof request {
    RangeRequest request_range = 1;
    PutRequest request_put = 2;
    DeleteRangeRequest request_delete_range = 3;
  }
}
  • Request_Range — RangeRequest.
  • Request_Put — PutRequest. Ключи должны быть уникальны и не могут пересекаться с ключами других операций Put или Delete.
  • Request_Delete_Range — DeleteRangeRequest. Ключи не могут пересекаться с ключами запросов Put или Delete.

В итоге транзакция выполняется вызовом API Txn, принимающим TxnRequest:

message TxnRequest {
  repeated Compare compare = 1;
  repeated RequestOp success = 2;
  repeated RequestOp failure = 3;
}
  • Compare — список предикатов, представляющих конъюнкцию условий защиты транзакции.
  • Success — список запросов, обрабатываемых, если все сравнения истинны.
  • Failure — список запросов, обрабатываемых, если хотя бы одно сравнение ложно.

В ответ на вызов Txn клиент получает сообщение TxnResponse:

message TxnResponse {
  ResponseHeader header = 1;
  bool succeeded = 2;
  repeated ResponseOp responses = 3;
}
  • Succeeded — результат вычисления Compare: true или false.
  • Responses — список ответов, соответствующих результатам применения блока Success, если succeeded равно true, либо блока Failure, если succeeded равно false.

Список Responses соответствует результатам применённого списка RequestOp, причём каждый ответ кодируется как ResponseOp:

message ResponseOp {
  oneof response {
    RangeResponse response_range = 1;
    PutResponse response_put = 2;
    DeleteRangeResponse response_delete_range = 3;
  }
}

Включённый в каждый внутренний ответ ResponseHeader не следует интерпретировать каким-либо образом. Если клиенту нужна последняя ревизия, он всегда должен проверять верхнеуровневый ResponseHeader в TxnResponse.

API наблюдения

API Watch предоставляет событийный интерфейс для асинхронного отслеживания изменений ключей. Наблюдение etcd ожидает изменения, непрерывно отслеживая ключи с заданной текущей или исторической ревизии, и потоком отправляет обновления клиенту.

События

Каждое изменение любого ключа представлено сообщением Event. Сообщение Event содержит данные и тип обновления:

message Event {
  enum EventType {
    PUT = 0;
    DELETE = 1;
  }
  EventType type = 1;
  KeyValue kv = 2;
  KeyValue prev_kv = 3;
}
  • Type — тип события. PUT означает сохранение новых данных по ключу, DELETE — удаление ключа.
  • KV — связанный с событием KeyValue. Событие PUT содержит текущую пару kv. PUT с kv.Version=1 означает создание ключа. DELETE содержит удалённый ключ, у которого ревизия изменения равна ревизии удаления.
  • Prev_KV — пара «ключ — значение» из ревизии непосредственно перед событием. Для экономии пропускной способности заполняется, только если явно включена в наблюдении.

Потоки наблюдения

Наблюдения — длительные запросы, использующие потоки gRPC для передачи данных событий. Поток наблюдения двунаправлен: клиент записывает в него для создания наблюдений и читает для получения событий. Один поток может мультиплексировать множество отдельных наблюдений, помечая события их идентификаторами. Это снижает потребление памяти и накладные расходы соединений в основном кластере etcd.

Гарантии для событий наблюдения описаны в разделе гарантии API etcd .

Клиент создаёт наблюдение, отправляя WatchCreateRequest через поток, возвращённый Watch:

message WatchCreateRequest {
  bytes key = 1;
  bytes range_end = 2;
  int64 start_revision = 3;
  bool progress_notify = 4;

  enum FilterType {
    NOPUT = 0;
    NODELETE = 1;
  }
  repeated FilterType filters = 5;
  bool prev_kv = 6;
}
  • Key, Range_End — наблюдаемый диапазон ключей.
  • Start_Revision — необязательная ревизия, с которой включительно начинается наблюдение. Если не задана, поток передаёт события после ревизии из заголовка ответа о создании наблюдения. Всю доступную историю событий можно наблюдать с последней ревизии компактизации.
  • Progress_Notify — если задано и недавних событий нет, наблюдение периодически получает WatchResponse без событий. Это полезно для восстановления отключённого наблюдателя с недавней известной ревизии. Сервер etcd выбирает частоту уведомлений по текущей нагрузке.
  • Filters — список типов событий, отфильтровываемых на стороне сервера.
  • Prev_Kv — если задано, наблюдение получает данные пары «ключ — значение» до события. Это позволяет узнать, какие данные были перезаписаны.

В ответ на WatchCreateRequest либо при появлении нового события для созданного наблюдения клиент получает WatchResponse:

message WatchResponse {
  ResponseHeader header = 1;
  int64 watch_id = 2;
  bool created = 3;
  bool canceled = 4;
  int64 compact_revision = 5;

  repeated mvccpb.Event events = 11;
}
  • Watch_ID — идентификатор наблюдения, соответствующего ответу.
  • Created — равно true, если это ответ на запрос создания наблюдения. Клиент должен сохранить идентификатор и ожидать события наблюдения в потоке. Все отправленные созданному наблюдателю события имеют одинаковый watch_id.
  • Canceled — равно true, если это ответ на запрос отмены наблюдения. Отменённому наблюдателю больше не отправляются события.
  • Compact_Revision — минимальная доступная etcd историческая ревизия, если наблюдатель пытается начать с компактизированной ревизии. Такое происходит при создании наблюдателя на компактизированной ревизии или когда наблюдатель не успевает за изменениями хранилища. Наблюдатель отменяется; создание новых наблюдений с тем же start_revision завершится ошибкой.
  • Events — упорядоченный список новых событий, соответствующих данному идентификатору наблюдения.

Чтобы прекратить получение событий наблюдения, клиент отправляет WatchCancelRequest:

message WatchCancelRequest {
   int64 watch_id = 1;
}
  • Watch_ID — идентификатор отменяемого наблюдения, которому больше не будут передаваться события.

API аренды

Аренды служат механизмом определения активности клиента. Кластер выдаёт аренды со сроком жизни. Аренда истекает, если кластер etcd не получает keepAlive в течение заданного периода TTL.

Для связи аренд с хранилищем каждый ключ можно присоединить не более чем к одной аренде. При истечении или отзыве аренды все присоединённые ключи удаляются. Каждый истёкший ключ создаёт событие удаления в истории событий.

Получение аренд

Аренды получают вызовом API LeaseGrant, принимающим LeaseGrantRequest:

message LeaseGrantRequest {
  int64 TTL = 1;
  int64 ID = 2;
}
  • TTL — рекомендуемый срок жизни в секундах.
  • ID — запрошенный идентификатор аренды. Если ID равен 0, etcd выбирает идентификатор самостоятельно.

В ответ на вызов LeaseGrant клиент получает LeaseGrantResponse:

message LeaseGrantResponse {
  ResponseHeader header = 1;
  int64 ID = 2;
  int64 TTL = 3;
}
  • ID — идентификатор выданной аренды.
  • TTL — выбранный сервером срок жизни аренды в секундах.
message LeaseRevokeRequest {
  int64 ID = 1;
}
  • ID — идентификатор отзываемой аренды. При отзыве все присоединённые ключи удаляются.

Поддержание активности

Аренды обновляются через двунаправленный поток, созданный вызовом API LeaseKeepAlive. Чтобы обновить аренду, клиент отправляет через поток LeaseKeepAliveRequest:

message LeaseKeepAliveRequest {
  int64 ID = 1;
}
  • ID — идентификатор аренды, активность которой поддерживается.

Поток поддержания активности отвечает сообщением LeaseKeepAliveResponse:

message LeaseKeepAliveResponse {
  ResponseHeader header = 1;
  int64 ID = 2;
  int64 TTL = 3;
}
  • ID — аренда, обновлённая с новым TTL.
  • TTL — новый оставшийся срок жизни аренды в секундах.