Сейчас идёт тестирование: код на GitHub будет открыт после завершения.

Как это работает

Эта страница описывает реальный поток сообщений между клиентом и сервером (кластером), чтобы объяснить, как работает Ticketing. Точная побайтовая спецификация того, что передаётся по сети, находится в разделах Протокол (API) и Протокол (кластер).


Общая структура

Сервисы, принимающие пользовательские запросы (например, серверы событий, обрабатывающие открытие продажи билетов), встают в очередь за один и тот же ключ к кластеру Ticketing, и только сервер, чья очередь подошла, продолжает работу — например, уменьшает остаток или назначает место.

groups
Пользователи
Ваш сервис (напр., серверы событий)
dns Сервер события 1
dns Сервер события 2
dns Сервер события 3
Кластер Ticketing
confirmation_number ticketing-server 1
confirmation_number ticketing-server 2
confirmation_number ticketing-server 3

Захват и освобождение блокировки

Базовый поток. Свободный ключ выдаётся немедленно; занятый ключ ждёт своей очереди в FIFO. Когда владелец освобождает блокировку, она передаётся напрямую первому в очереди без повторной конкуренции — следующий ожидающий продолжает немедленно с новым токеном, так что блокировка никогда не остаётся пустой, и никто не может влезть без очереди.

Клиент X
Клиент Y
Сервер
A · acquire (order-1234)
A · acquired (token 41)
A · acquire (order-1234)
занято → в очередь, ответ задержан
R · release
R · released
A · прямая передача (token 42)
запросответ
  • lease — это страховочная сеть. Если клиент умирает или забывает освободить блокировку, по истечении времени lease сервер забирает ключ и передаёт его следующему ожидающему. Задавайте его с запасом — дольше, чем может занять худший случай вашей критической секции. v1 принимает только 1–250 секунд; более долгие работы явно не поддерживаются и отклоняются.
  • wait — это верхняя граница времени ожидания клиента. Если очередь не подходит за это время, сервер отвечает T (timeout) вместо выдачи блокировки, так что блокировка никогда не «утекает». 0 означает единственную немедленную попытку без постановки в очередь. Бесконечного ожидания нет.
  • Если тот же клиент пытается повторно захватить уже удерживаемый им ключ, он встаёт в очередь как любой другой ожидающий — это не реентерабельная блокировка.

Полные правила поведения сервера по состояниям ключа смотрите в Матрице запрос × состояние ключа, а диапазоны значений полей — в Ограничениях полей.

Fencing-токены

Даже идеальный сервер блокировок не может контролировать часы клиента. Одна лишь Ticketing не может предотвратить случай устаревшего владельца: клиент, который на время замер из-за пауз GC или перегрузки, удерживая блокировку, затем просыпается — не зная, что его lease уже истёк, — и продолжает свою работу. Именно поэтому token в каждом ответе на захват — это целое число u64, гарантированно возрастающее при каждой выдаче. Для финансовой или постоянной БД сравнение и обновление high-water fencing по ключу должно выполняться атомарно с реальной бизнес-записью в той же транзакции БД или одной условной записи. Обновить сначала только fencing-строку, а реальную запись выполнить позже — небезопасно.

Клиент X
Клиент Y
Сервер
Защищённый ресурс
A · acquire
A · acquired (token 7)
A · acquire → ожидание
зависание из-за GC / перегрузки
lease истёк → отозван
A · прямая передача (token 8)
запись (token 8)
фиксирует token 8 → принято
просыпается с опозданием
запись (token 7)
7 < 8 → отклонено
запросответ

Монотонность сохраняется и в масштабах кластера — сам счётчик токенов реплицируется через консенсус, поэтому даже после смены лидера новый лидер всегда продолжает с числа, большего предыдущего. Точный формат поля смотрите в Fencing-токен (спецификация).

Кластер — непрерывность через консенсус Raft

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

Клиент
Follower B
Leader A
Follower C
A · acquire
M · moved (leader)
A · acquire
репликация предложения
репликация предложения
ack
ack
ack большинства → commit
A · acquired (token)
запросответRPC консенсуса (Raft)
  • Если лидер умирает, текущие настройки избирают нового лидера примерно за 2,3–2,5 секунды. Тот же логический acquire можно продолжить в оставшемся бюджете wait только если не отправлен ни один байт запроса. Отправленный acquire без подтверждённого ответа — Indeterminate; он никогда не пересылается автоматически.
  • Даже истечение lease проходит через консенсус. Блокировка исчезает только после того, как лидер зафиксирует команду истечения, поэтому небольшие расхождения часов между узлами никогда не приводят к расхождению состояния блокировки.
  • Если кластер теряет большинство (например, 2 из 3 узлов недоступны), он прекращает принимать записи, вместо того чтобы рискнуть выдать неверную блокировку (безопасность прежде всего). Если потеряно волатильное состояние процессов 2 из 3 (или 3 из 5) узлов, этот cluster/fencing domain нельзя восстанавливать или автоматически bootstrap под той же идентичностью.

Поскольку важно именно большинство, допускаются только 3 или 5 узлов. Чётное число узлов лишь добавляет стоимость, не улучшая отказоустойчивость, поэтому сервер отказывается запускаться с таким числом.

Число узловНепрерывностьДопустимые одновременные отказыПримечания
1Монотонность token гарантирована лишь на время жизни процесса; защита перезапускаемой постоянной БД не поддерживается
31Стандартная непрерывная конфигурация. Достаточно для большинства случаев
52Избыточность сохраняется, даже пока один узел на обслуживании

Правила фрейминга и портов для RPC между узлами смотрите в Протокол (кластер), а процедуру поочерёдной замены — в непрерывной замене learner с новым NodeId.

Поведение клиента

Официальные клиенты разделяют следующее общее поведение (см. Библиотеки для API конкретных языков).

  • Поддерживает одно постоянное соединение на адрес в фоне. При одном адресе клиент держит два соединения к этому же узлу, так что кратковременный обрыв одного сокета не прерывает обслуживание.
  • Выбирает соединение сначала к лидеру, затем round-robin. Клиент запоминает лидера, к которому его последний раз перенаправили через M, и отправляет последующие запросы прямо ему.
  • Запросы конвейеризуются. Следующий запрос можно отправить, не дожидаясь ответа на предыдущий — см. Правила сопоставления ответов о том, как ответы сопоставляются с запросами.
  • Оборванное соединение постоянно повторяет попытки с экспоненциальной задержкой (от 0,1 с до потолка в 3,2 с). Мёртвое соединение не ломает объект брокера — он восстанавливается сам.
  • Тот же логический acquire повторяется внутри только если не отправлен ни один байт запроса. Сопоставленный B — определённый Busy/не-захват; он сразу возвращается вызывающему без внутреннего retry. M — только leader hint для следующего соединения; без owner/key он не является основанием переслать уже отправленный pending-запрос. Отправленный acquire без подтверждённого ответа сообщается как Indeterminate; входить в критическую секцию нельзя.
  • Явное release и компенсация известного token требуют точного совпадения owner/token/key и используют ограниченную выделенную очередь. Повторы прекращаются по абсолютному дедлайну в 5 секунд от request/enqueue, а не по оставшемуся lease. Без успешного ответа явное release не объявляется и не считается успешным; серверный lease остаётся последней страховкой.

Соединения и безопасность

Каждое соединение (клиентское или между узлами) устанавливается в порядке TCP → (TLS) → аутентификация challenge-response. Клиент хеширует nonce сервера вместе со своим токеном для ответа, поэтому токен никогда не передаётся по сети в открытом виде, а новый nonce при каждом соединении исключает повторное воспроизведение (replay). Точную спецификацию смотрите в Аутентификационном хендшейке.

Заметка о согласованности

  • Взаимное исключение гарантируется консенсусом. Поскольку каждая выдача проходит через фиксацию большинством, один и тот же ключ никогда не выдаётся двум владельцам одновременно, даже во время разделения сети или смены лидера.
  • Постоянные БД обязаны применять fencing. Только условие high-water по ключу, атомарно выполненное с защищаемой записью в той же транзакции/условной записи, останавливает клиента, проснувшегося после истечения lease. Блокировка упорядочивает работу; fencing БД блокирует последнюю устаревшую запись.