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

Что такое Ticketing?

Ticketing — это выделенный сервер блокировок, созданный ровно для одной задачи — распределённой блокировки. Это единый бинарный файл сервера на Rust в паре с официальными клиентами для разных языков, каждый из которых реализует один и тот же бинарный протокол, байт в байт. Всего две операции — захват и освобождение, — очередь FIFO на каждый ключ, fencing-токены и опциональный кластер на Raft, который включается при необходимости. Всё, что нужно распределённой блокировке, и ничего лишнего.

Чем это отличается от блокировок на основе хранилища?

Большинство распределённых блокировок в продакшене опираются на хранилище ключ-значение — Lua-скрипт или Redlock поверх Redis, либо блокировку, построенную на сессиях и арендах (leases) ZooKeeper или etcd. Redis — отличное хранилище, но это хранилище ключ-значение, переиспользованное для распределённой блокировки, а не специализированная система блокировок, и это проявляется как структурные накладные расходы: стоимость разбора универсального протокола и накладные расходы универсальных структур данных прилагаются в довесок.

Ticketing с самого начала спроектирован исключительно для распределённой блокировки, поэтому каждый слой построен вокруг блокировок.

  • Протокол — бинарные поля фиксированной ширины, не текст и не универсальный формат сериализации. Значения читаются прямо по смещениям байтов без разбора, а конвейеризация позволяет отправлять следующий запрос, не дожидаясь предыдущего ответа.
  • Внутреннее устройство сервера — всё состояние — это очередь FIFO на каждый ключ плюс таймер аренды (lease). Путь обработки запроса не выполняет даже выделения памяти в куче.
  • Эксплуатация — запустите один бинарный файл сервера (или контейнер), настройте его несколькими переменными окружения — и всё готово. Не нужно проектировать схему, поднимать отдельное хранилище или разбираться в эксплуатационных тонкостях, не связанных с блокировкой.

Производительность

  • Обрабатывает более 1 миллиона операций в секунду, используя менее 10 МБ памяти
Сравнение скорости с Redisson (Redis) — самой распространённой системой распределённых блокировок.
Клиенты TicketingRedis (Redisson RLock)
JavaScript
210.2 ms · 190,295 ops/s
210.2 ms
Rust
225.9 ms · 177,069 ops/s
225.9 ms
Go
228.4 ms · 175,131 ops/s
228.4 ms
C#
286.9 ms · 139,421 ops/s
286.9 ms
Kotlin
443.5 ms · 90,192 ops/s
443.5 ms
Java
457.8 ms · 87,374 ops/s
457.8 ms
C/C++
659.7 ms · 60,634 ops/s
659.7 ms
Python
739.4 ms · 54,098 ops/s
739.4 ms
Ruby
1,682.4 ms · 23,776 ops/s
1,682.4 ms
Redisбаза
4,909.1 ms · 8,148 ops/s
4,909.1 ms
Длина полосы — общее время выполнения (мс); чем короче, тем быстрее.
32 contended keys × 1,250 ops · concurrency 64 · mac mini m4 2024 basic (10 core), single local server, acknowledged release, 1s min-work budget

Почему Ticketing

🚦 Целостность порядка — очередь FIFO с прямой передачей

Кто первый пришёл, тот первым обслужен. Когда владелец освобождает блокировку, она передаётся напрямую первому в очереди без повторной конкуренции. Никакого голодания (starvation) от конкуренции за блокировку через опрос и повтор, никаких штормов повторов.

🛟 Мёртвый клиент не оставляет блокировку висящей — lease

Если процесс, удерживающий блокировку, умирает или его соединение обрывается, сервер автоматически забирает блокировку по истечении времени lease, заданного при захвате, и передаёт её следующему ожидающему. Забытое освобождение не останавливает всю систему.

🔑 Предотвращение работы не по порядку — fencing-токены

Есть один сбой, который одна лишь блокировка предотвратить не может: владелец, который просыпается с опозданием — не зная, что его lease уже истёк, — и всё равно просит завершить свою работу. Ticketing выдаёт монотонно возрастающий fencing-токен при каждом захвате. Пока защищаемый ресурс соблюдает одно правило — «отклонять любой токен ниже последнего увиденного», — закрывается даже этот последний пробел. Смотрите Как это работает для полного объяснения.

🗳️ Непрерывный кластер на основе Raft

Одного сервера достаточно само по себе, но когда нужен нулевой простой, сформируйте кластер из 3 (или 5) узлов. Любое изменение состояния блокировки фиксируется только после согласия большинства узлов через консенсус Raft, поэтому одна и та же блокировка никогда не выдаётся дважды одновременно, даже если лидер умирает или сеть разделяется. При отказе лидера новый избирается примерно за секунду, и клиенты переключаются автоматически. Сервис продолжает работать, даже когда узлы отключаются один за другим — например, при перезагрузке облачных инстансов или последовательном обслуживании.

🔒 Встроенная аутентификация и TLS

Каждое соединение сразу после установления проходит аутентификацию токеном по схеме challenge-response, и токен никогда не передаётся по сети в открытом виде. При необходимости можно зашифровать всё соединение через TLS.

🌐 Один и тот же протокол, байт в байт, для всех основных языков

Rust, Java/Kotlin, JavaScript/TypeScript, Python, C#, Go, Ruby, C/C++ — каждый официальный клиент реализует один и тот же бинарный протокол, с API, естественным для экосистемы каждого языка (async или блокирующим). Блокировка, захваченная из любого языка, справедливо встаёт в очередь вместе с клиентами, написанными на любом другом. Инструкции по установке и примеры — в разделе Библиотеки.

Всего две операции

Верный своей природе сервера только для блокировок, набор возможностей тоже минимален.

  1. Привяжите ключ к ресурсу, который хотите защитить — например, order-1234, account-77, daily-batch.
  2. Захватите этот ключ перед началом работы — если он занят кем-то другим, вы ждёте своей очереди в FIFO.
  3. Освободите его по завершении работы — следующий ожидающий получает его немедленно, без повторной конкуренции.

Единственные опции поверх этих двух операций — lease (автоматический возврат) и waitTimeout (отказ от ожидания) — это весь API.

Что читать дальше