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 описывается следующим образом:
Заголовок ответа
Все ответы API etcd содержат заголовок с метаданными кластера для данного ответа:
- Cluster_ID — идентификатор кластера, создавшего ответ.
- Member_ID — идентификатор участника, создавшего ответ.
- Revision — ревизия хранилища ключей и значений на момент создания ответа.
- Raft_Term — срок полномочий Raft участника на момент создания ответа.
Приложение может прочитать поле Cluster_ID или Member_ID, чтобы убедиться, что взаимодействует с предполагаемым кластером (участником).
По полю Revision приложения могут узнать последнюю ревизию хранилища ключей и значений. Это особенно полезно, когда приложение задаёт историческую ревизию для time travel query и хочет определить последнюю ревизию на момент запроса.
С помощью Raft_Term приложения могут определить момент завершения новых выборов лидера в кластере.
API ключей и значений
API ключей и значений управляет парами «ключ — значение», хранящимися в etcd. Обычно большинство запросов к etcd относится именно к ним.
Системные примитивы
Пара «ключ — значение»
Пара «ключ — значение» — минимальная единица, которой может управлять соответствующий API. Каждая пара содержит ряд полей, определённых в формате protobuf :
- 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:
- 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:
- Kvs — список пар «ключ — значение», соответствующих диапазонному запросу. При заданном
Count_OnlyполеKvsпусто. - More — при заданном
limitуказывает, остались ли в запрошенном диапазоне ключи для возврата. - Count — общее количество ключей, удовлетворяющих диапазонному запросу.
Для больших диапазонов ключей, когда буферизация полного ответа нежелательна, используйте RangeStream .
RangeStream
RangeStream возвращает тот же набор результатов, что и Range, однако сервер разбивает ответ на последовательность фрагментов и передаёт их клиенту потоком. Благодаря этому ни одной стороне не нужно целиком буферизовать большие диапазоны в памяти. RangeStream принимает тот же RangeRequest, что и Range.
В ответ на вызов RangeStream клиент получает поток сообщений RangeStreamResponse:
Заполнение полей во фрагментах:
- 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:
- Обрабатывать каждый фрагмент независимо. Подходит для высокопроизводительных сценариев, когда клиент хочет декодировать и обрабатывать ключи по мере поступления, а не сначала собирать весь результат. Клиент перебирает фрагменты и обрабатывает
kvsкаждого, а после успешного завершения потока читаетheader,moreилиcountиз последнего фрагмента. - Собрать единый ответ. Подходит, когда клиенту требуется результат, эквивалентный унарному
Range. Клиент объединяетrange_responseкаждого фрагмента в одинRangeResponse(например, черезproto.Merge). Объединённый результат содержит полныйkvs, а такжеheader,moreиcountиз последнего фрагмента. Для этого шаблона клиент Go предоставляет вспомогательную функциюclientv3.GetStreamToGetResponse.
Put
Ключи сохраняются в хранилище ключей и значений вызовом Put, принимающим PutRequest:
- Key — имя ключа, записываемого в хранилище ключей и значений.
- Value — значение в байтах, связываемое с ключом в хранилище.
- Lease — идентификатор аренды, связываемой с ключом. Значение аренды 0 означает отсутствие аренды.
- Prev_Kv — если задано, в ответе возвращаются данные пары «ключ — значение» до обновления запросом
Put. - Ignore_Value — если задано, ключ обновляется без изменения текущего значения. Если ключ не существует, возвращается ошибка.
- Ignore_Lease — если задано, ключ обновляется без изменения текущей аренды. Если ключ не существует, возвращается ошибка.
В ответ на вызов Put клиент получает сообщение PutResponse:
- Prev_Kv — пара «ключ — значение», перезаписанная операцией
Put, если вPutRequestбыло заданоPrev_Kv.
Удаление диапазона
Диапазоны ключей удаляются вызовом DeleteRange, принимающим DeleteRangeRequest:
- Key, Range_End — удаляемый диапазон ключей.
- Prev_Kv — если задано, возвращает содержимое удалённых пар «ключ — значение».
В ответ на вызов DeleteRange клиент получает сообщение DeleteRangeResponse:
- Deleted — количество удалённых ключей.
- Prev_Kv — список всех пар «ключ — значение», удалённых операцией
DeleteRange.
Транзакция
Транзакция — атомарная конструкция If/Then/Else над хранилищем ключей и значений. Она предоставляет примитив для объединения запросов в атомарные блоки (then/else), выполнение которых защищено условием (if), основанным на содержимом хранилища. Транзакции позволяют защищать ключи от непреднамеренных параллельных обновлений, строить операции сравнения с обменом и создавать механизмы управления параллелизмом более высокого уровня.
Транзакция может атомарно обработать несколько запросов в одном запросе. При изменении хранилища его ревизия увеличивается только один раз на транзакцию, а все созданные ею события имеют одинаковую ревизию. Однако многократное изменение одного ключа в рамках одной транзакции запрещено.
Все транзакции защищены конъюнкцией сравнений, подобной оператору If. Каждое сравнение проверяет один ключ в хранилище: отсутствие или наличие значения, равенство заданному значению либо ревизию или версию ключа. Два разных сравнения могут относиться к одному или разным ключам. Все сравнения применяются атомарно. Если они истинны, транзакция считается успешной и etcd применяет блок запросов then / success; иначе транзакция считается неудачной и применяется блок else / failure.
Каждое сравнение кодируется сообщением Compare:
- Result — тип логической операции сравнения (например, равно, меньше и т. д.).
- Target — сравниваемое поле пары «ключ — значение»: версия ключа, ревизия создания, ревизия изменения либо значение.
- Key — ключ для сравнения.
- Target_Union — заданные пользователем данные сравнения.
После обработки блока сравнений транзакция применяет блок запросов. Блок представляет собой список сообщений RequestOp:
- Request_Range —
RangeRequest. - Request_Put —
PutRequest. Ключи должны быть уникальны и не могут пересекаться с ключами других операций Put или Delete. - Request_Delete_Range —
DeleteRangeRequest. Ключи не могут пересекаться с ключами запросов Put или Delete.
В итоге транзакция выполняется вызовом API Txn, принимающим TxnRequest:
- Compare — список предикатов, представляющих конъюнкцию условий защиты транзакции.
- Success — список запросов, обрабатываемых, если все сравнения истинны.
- Failure — список запросов, обрабатываемых, если хотя бы одно сравнение ложно.
В ответ на вызов Txn клиент получает сообщение TxnResponse:
- Succeeded — результат вычисления
Compare: true или false. - Responses — список ответов, соответствующих результатам применения блока
Success, если succeeded равно true, либо блокаFailure, если succeeded равно false.
Список Responses соответствует результатам применённого списка RequestOp, причём каждый ответ кодируется как ResponseOp:
Включённый в каждый внутренний ответ ResponseHeader не следует интерпретировать каким-либо образом.
Если клиенту нужна последняя ревизия, он всегда должен проверять верхнеуровневый ResponseHeader в TxnResponse.
API наблюдения
API Watch предоставляет событийный интерфейс для асинхронного отслеживания изменений ключей. Наблюдение etcd ожидает изменения, непрерывно отслеживая ключи с заданной текущей или исторической ревизии, и потоком отправляет обновления клиенту.
События
Каждое изменение любого ключа представлено сообщением Event. Сообщение Event содержит данные и тип обновления:
- Type — тип события. PUT означает сохранение новых данных по ключу, DELETE — удаление ключа.
- KV — связанный с событием KeyValue. Событие PUT содержит текущую пару kv. PUT с kv.Version=1 означает создание ключа. DELETE содержит удалённый ключ, у которого ревизия изменения равна ревизии удаления.
- Prev_KV — пара «ключ — значение» из ревизии непосредственно перед событием. Для экономии пропускной способности заполняется, только если явно включена в наблюдении.
Потоки наблюдения
Наблюдения — длительные запросы, использующие потоки gRPC для передачи данных событий. Поток наблюдения двунаправлен: клиент записывает в него для создания наблюдений и читает для получения событий. Один поток может мультиплексировать множество отдельных наблюдений, помечая события их идентификаторами. Это снижает потребление памяти и накладные расходы соединений в основном кластере etcd.
Гарантии для событий наблюдения описаны в разделе гарантии API etcd .
Клиент создаёт наблюдение, отправляя WatchCreateRequest через поток, возвращённый Watch:
- Key, Range_End — наблюдаемый диапазон ключей.
- Start_Revision — необязательная ревизия, с которой включительно начинается наблюдение. Если не задана, поток передаёт события после ревизии из заголовка ответа о создании наблюдения. Всю доступную историю событий можно наблюдать с последней ревизии компактизации.
- Progress_Notify — если задано и недавних событий нет, наблюдение периодически получает WatchResponse без событий. Это полезно для восстановления отключённого наблюдателя с недавней известной ревизии. Сервер etcd выбирает частоту уведомлений по текущей нагрузке.
- Filters — список типов событий, отфильтровываемых на стороне сервера.
- Prev_Kv — если задано, наблюдение получает данные пары «ключ — значение» до события. Это позволяет узнать, какие данные были перезаписаны.
В ответ на WatchCreateRequest либо при появлении нового события для созданного наблюдения клиент получает WatchResponse:
- Watch_ID — идентификатор наблюдения, соответствующего ответу.
- Created — равно true, если это ответ на запрос создания наблюдения. Клиент должен сохранить идентификатор и ожидать события наблюдения в потоке. Все отправленные созданному наблюдателю события имеют одинаковый watch_id.
- Canceled — равно true, если это ответ на запрос отмены наблюдения. Отменённому наблюдателю больше не отправляются события.
- Compact_Revision — минимальная доступная etcd историческая ревизия, если наблюдатель пытается начать с компактизированной ревизии. Такое происходит при создании наблюдателя на компактизированной ревизии или когда наблюдатель не успевает за изменениями хранилища. Наблюдатель отменяется; создание новых наблюдений с тем же start_revision завершится ошибкой.
- Events — упорядоченный список новых событий, соответствующих данному идентификатору наблюдения.
Чтобы прекратить получение событий наблюдения, клиент отправляет WatchCancelRequest:
- Watch_ID — идентификатор отменяемого наблюдения, которому больше не будут передаваться события.
API аренды
Аренды служат механизмом определения активности клиента. Кластер выдаёт аренды со сроком жизни. Аренда истекает, если кластер etcd не получает keepAlive в течение заданного периода TTL.
Для связи аренд с хранилищем каждый ключ можно присоединить не более чем к одной аренде. При истечении или отзыве аренды все присоединённые ключи удаляются. Каждый истёкший ключ создаёт событие удаления в истории событий.
Получение аренд
Аренды получают вызовом API LeaseGrant, принимающим LeaseGrantRequest:
- TTL — рекомендуемый срок жизни в секундах.
- ID — запрошенный идентификатор аренды. Если ID равен 0, etcd выбирает идентификатор самостоятельно.
В ответ на вызов LeaseGrant клиент получает LeaseGrantResponse:
- ID — идентификатор выданной аренды.
- TTL — выбранный сервером срок жизни аренды в секундах.
- ID — идентификатор отзываемой аренды. При отзыве все присоединённые ключи удаляются.
Поддержание активности
Аренды обновляются через двунаправленный поток, созданный вызовом API LeaseKeepAlive. Чтобы обновить аренду, клиент отправляет через поток LeaseKeepAliveRequest:
- ID — идентификатор аренды, активность которой поддерживается.
Поток поддержания активности отвечает сообщением LeaseKeepAliveResponse:
- ID — аренда, обновлённая с новым TTL.
- TTL — новый оставшийся срок жизни аренды в секундах.