Обзор
Это спецификация протокола Garlic Farm, основанная на JRaft, его коде “exts” для реализации поверх TCP и примере приложения “dmprinter” JRAFT .
Нам не удалось найти реализацию с документированным сетевым протоколом. Однако реализация JRaft достаточно проста, чтобы мы могли изучить код и задокументировать его протокол. Этот документ — результат этих усилий.
Это будет бэкенд для координации маршрутизаторов, публикующих записи в Meta LeaseSet. См. предложение 123.
Цели
- Малый размер кода
- Основан на существующей реализации
- Без сериализованных Java-объектов или любых Java-специфичных функций или кодировок
- Любая загрузка (bootstrapping) находится за рамками спецификации. Предполагается, что хотя бы один другой сервер зашит в код или настроен вне протокола
- Поддержка как внеполосных, так и внутри-I2P сценариев использования
Дизайн
Протокол Raft не является конкретным протоколом; он определяет лишь машину состояний. Поэтому мы документируем конкретный протокол JRaft и основываем наш протокол на нём. Единственное изменение по сравнению с протоколом JRaft — добавление рукопожатия с аутентификацией.
Raft избирает Лидера, задача которого — публиковать журнал (log). Журнал содержит данные конфигурации Raft и прикладные данные. Прикладные данные содержат статус маршрутизатора каждого сервера и Destination для кластера Meta LS2. Серверы используют общий алгоритм для определения издателя и содержимого Meta LS2. Издатель Meta LS2 НЕ обязательно является Лидером Raft.
Спецификация
Сетевой протокол работает поверх SSL-сокетов или не-SSL I2P-сокетов. I2P-сокеты проксируются через HTTP-прокси. Поддержка нешифрованных clearnet-сокетов отсутствует.
Рукопожатие и аутентификация
Не определено в JRaft.
Цели:
- Метод аутентификации по логину/паролю
- Идентификатор версии
- Идентификатор кластера
- Расширяемость
- Простота проксирования при использовании I2P-сокетов
- Не раскрывать сервер как Garlic Farm сервер без необходимости
- Простой протокол, чтобы не требовалась полная реализация веб-сервера
- Совместимость с общепринятыми стандартами, чтобы реализации могли использовать стандартные библиотеки при желании
Мы будем использовать рукопожатие, похожее на WebSocket, и аутентификацию HTTP Digest RFC 2617 . HTTP Basic аутентификация по RFC 2617 НЕ поддерживается. При проксировании через HTTP-прокси общение с прокси происходит согласно RFC 2616 .
Учётные данные
Являются ли имена пользователей и пароли привязанными к кластеру или к серверу — зависит от реализации.
HTTP Запрос 1
Инициатор отправляет следующее.
Все строки завершаются CRLF, как требуется HTTP.
GET /GarlicFarm/CLUSTER/VERSION/websocket HTTP/1.1
Host: (ip):(port)
Cache-Control: no-cache
Connection: close
(любые другие заголовки игнорируются)
(пустая строка)
CLUSTER — имя кластера (по умолчанию "farm")
VERSION — версия Garlic Farm (сейчас "1")
HTTP Ответ 1
Если путь неверен, получатель отправит стандартный ответ “HTTP/1.1 404 Not Found”, как в RFC 2616 .
Если путь верен, получатель отправит стандартный ответ “HTTP/1.1 401 Unauthorized”, включая заголовок WWW-Authenticate с аутентификацией по HTTP Digest, как в RFC 2617 .
После этого обе стороны закрывают сокет.
HTTP Запрос 2
Инициатор отправляет следующее, как в RFC 2617 .
Все строки завершаются CRLF, как требуется HTTP.
GET /GarlicFarm/CLUSTER/VERSION/websocket HTTP/1.1
Host: (ip):(port)
Cache-Control: no-cache
Connection: keep-alive, Upgrade
Upgrade: websocket
(Sec-Websocket-* заголовки, если проксируется)
Authorization: (заголовок авторизации HTTP Digest, как в RFC 2617)
(любые другие заголовки игнорируются)
(пустая строка)
CLUSTER — имя кластера (по умолчанию "farm")
VERSION — версия Garlic Farm (сейчас "1")
HTTP Ответ 2
Если аутентификация неверна, получатель отправит ещё один стандартный ответ “HTTP/1.1 401 Unauthorized”, как в RFC 2617 .
Если аутентификация верна, получатель отправит следующий ответ, как в протоколе WebSocket.
Все строки завершаются CRLF, как требуется HTTP.
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
(Sec-Websocket-* заголовки)
(любые другие заголовки игнорируются)
(пустая строка)
После получения этого ответа сокет остаётся открытым. Начинается протокол Raft, определённый ниже, на том же сокете.
Кэширование
Учётные данные должны кэшироваться как минимум на один час, чтобы последующие подключения могли сразу переходить к “HTTP Запросу 2” выше.
Типы сообщений
Существует два типа сообщений: запросы и ответы. Запросы могут содержать записи журнала (Log Entries) и имеют переменный размер; ответы не содержат записей журнала и имеют фиксированный размер.
Типы сообщений 1–4 — стандартные RPC-сообщения, определённые в Raft. Это основа протокола Raft.
Типы сообщений 5–15 — расширенные RPC-сообщения, определённые в JRaft, для поддержки клиентов, динамического изменения серверов и эффективной синхронизации журнала.
Типы сообщений 16–17 — сообщения RPC для компрессии журнала, определённые в разделе 7 Raft.
| Сообщение | Номер | Отправитель | Получатель | Примечания |
|---|---|---|---|---|
| RequestVoteRequest | 1 | Кандидат | Последователь | Стандартный RPC Raft; не должен содержать записи журнала |
| RequestVoteResponse | 2 | Последователь | Кандидат | Стандартный RPC Raft |
| AppendEntriesRequest | 3 | Лидер | Последователь | Стандартный RPC Raft |
| AppendEntriesResponse | 4 | Последователь | Лидер / Клиент | Стандартный RPC Raft |
| ClientRequest | 5 | Клиент | Лидер / Последователь | Ответ — AppendEntriesResponse; должен содержать только прикладные записи журнала |
| AddServerRequest | 6 | Клиент | Лидер | Должен содержать только одну запись журнала ClusterServer |
| AddServerResponse | 7 | Лидер | Клиент | Лидер также отправит JoinClusterRequest |
| RemoveServerRequest | 8 | Последователь | Лидер | Должен содержать только одну запись журнала ClusterServer |
| RemoveServerResponse | 9 | Лидер | Последователь | |
| SyncLogRequest | 10 | Лидер | Последователь | Должен содержать только одну запись журнала LogPack |
| SyncLogResponse | 11 | Последователь | Лидер | |
| JoinClusterRequest | 12 | Лидер | Новый сервер | Приглашение присоединиться; должен содержать только одну запись журнала Configuration |
| JoinClusterResponse | 13 | Новый сервер | Лидер | |
| LeaveClusterRequest | 14 | Лидер | Последователь | Команда покинуть кластер |
| LeaveClusterResponse | 15 | Последователь | Лидер | |
| InstallSnapshotRequest | 16 | Лидер | Последователь | Раздел 7 Raft; должен содержать только одну запись журнала SnapshotSyncRequest |
| InstallSnapshotResponse | 17 | Последователь | Лидер | Раздел 7 Raft |
Установление соединения
После HTTP-рукопожатия последовательность установления следующая:
Новый сервер Alice Случайный последователь Bob
ClientRequest ------->
<--------- AppendEntriesResponse
Если Bob сообщает, что он лидер, продолжить ниже.
Иначе Alice должна отключиться от Bob и подключиться к лидеру.
Новый сервер Alice Лидер Charlie
ClientRequest ------->
<--------- AppendEntriesResponse
AddServerRequest ------->
<--------- AddServerResponse
<--------- JoinClusterRequest
JoinClusterResponse ------->
<--------- SyncLogRequest
OR InstallSnapshotRequest
SyncLogResponse ------->
OR InstallSnapshotResponse
Последовательность отключения:
Последователь Alice Лидер Charlie
RemoveServerRequest ------->
<--------- RemoveServerResponse
<--------- LeaveClusterRequest
LeaveClusterResponse ------->
Последовательность выборов:
Кандидат Alice Последователь Bob
RequestVoteRequest ------->
<--------- RequestVoteResponse
если Alice выигрывает выборы:
Лидер Alice Последователь Bob
AppendEntriesRequest ------->
(heartbeat)
<--------- AppendEntriesResponse
Определения
- Источник (Source): идентифицирует отправителя сообщения
- Назначение (Destination): идентифицирует получателя сообщения
- Термины (Terms): см. Raft. Инициализируются в 0, монотонно возрастают
- Индексы (Indexes): см. Raft. Инициализируются в 0, монотонно возрастают
Запросы
Запросы содержат заголовок и ноль или более записей журнала. Запросы содержат заголовок фиксированного размера и опциональные записи журнала переменного размера.
Заголовок запроса
Заголовок запроса — 45 байт, следующий формат. Все значения — беззнаковые, big-endian.
Тип сообщения: 1 байт
Источник: ID, 4-байтовое целое
Назначение: ID, 4-байтовое целое
Терм: Текущий терм (см. примечания), 8-байтовое целое
Последний терм журнала: 8-байтовое целое
Последний индекс журнала: 8-байтовое целое
Индекс коммита: 8-байтовое целое
Размер записей журнала: Общий размер в байтах, 4-байтовое целое
Записи журнала: см. ниже, общая длина как указано
Примечания
В RequestVoteRequest терм — это терм кандидата. В остальных случаях — текущий терм лидера.
В AppendEntriesRequest, если размер записей журнала равен нулю, это сообщение является heartbeat (keepalive).
Записи журнала
Журнал содержит ноль или более записей. Каждая запись имеет следующий формат. Все значения — беззнаковые, big-endian.
Терм: 8-байтовое целое
Тип значения: 1 байт
Размер записи: В байтах, 4-байтовое целое
Запись: длина как указано
Содержимое журнала
Все значения — беззнаковые, big-endian.
| Тип значения журнала | Номер |
|---|---|
| Прикладной | 1 |
| Конфигурация | 2 |
| ClusterServer | 3 |
| LogPack | 4 |
| SnapshotSyncRequest | 5 |
Прикладной
Содержимое кодируется в UTF-8 JSON . См. раздел “Прикладной уровень” ниже.
Конфигурация
Используется лидером для сериализации новой конфигурации кластера и репликации на пиры. Содержит ноль или более конфигураций ClusterServer.
Индекс журнала: 8-байтовое целое
Последний индекс журнала: 8-байтовое целое
Данные ClusterServer для каждого сервера:
ID: 4-байтовое целое
Длина данных Endpoint: В байтах, 4-байтовое целое
Данные Endpoint: ASCII-строка вида "tcp://localhost:9001", длина как указано
ClusterServer
Информация о конфигурации сервера в кластере. Включается только в сообщения AddServerRequest или RemoveServerRequest.
При использовании в AddServerRequest:
ID: 4-байтовое целое
Длина данных Endpoint: В байтах, 4-байтовое целое
Данные Endpoint: ASCII-строка вида "tcp://localhost:9001", длина как указано
При использовании в RemoveServerRequest:
ID: 4-байтовое целое
LogPack
Включается только в SyncLogRequest.
Следующие данные сжимаются с помощью gzip перед передачей:
Длина данных индекса: В байтах, 4-байтовое целое
Длина данных журнала: В байтах, 4-байтовое целое
Данные индекса: по 8 байт на каждый индекс, длина как указано
Данные журнала: длина как указано
SnapshotSyncRequest
Включается только в InstallSnapshotRequest.
Последний индекс журнала: 8-байтовое целое
Последний терм журнала: 8-байтовое целое
Длина данных конфигурации: В байтах, 4-байтовое целое
Данные конфигурации: длина как указано
Смещение: Смещение данных в базе, в байтах, 8-байтовое целое
Длина данных: В байтах, 4-байтовое целое
Данные: длина как указано
Is Done: 1 если завершено, 0 если нет (1 байт)
Ответы
Все ответы — 26 байт, следующий формат. Все значения — беззнаковые, big-endian.
Тип сообщения: 1 байт
Источник: ID, 4-байтовое целое
Назначение: Обычно фактический ID получателя (см. примечания), 4-байтовое целое
Терм: Текущий терм, 8-байтовое целое
Следующий индекс: Инициализируется как последний индекс журнала лидера + 1, 8-байтовое целое
Принято: 1 если принято, 0 если нет (см. примечания), 1 байт
Примечания
ID назначения обычно — фактический получатель сообщения. Однако для AppendEntriesResponse, AddServerResponse и RemoveServerResponse — это ID текущего лидера.
В RequestVoteResponse, Is Accepted равно 1 при голосовании за кандидата (запросившего), и 0 при отказе в голосовании.
Прикладной уровень
Каждый сервер периодически публикует прикладные данные в журнал через ClientRequest. Прикладные данные содержат статус маршрутизатора каждого сервера и Destination для кластера Meta LS2. Серверы используют общий алгоритм для определения издателя и содержимого Meta LS2. Сервер с «лучшим» недавним статусом в журнале — издатель Meta LS2. Издатель Meta LS2 НЕ обязательно является Лидером Raft.
Содержимое прикладных данных
Прикладные данные кодируются в UTF-8 JSON , для простоты и расширяемости. Полная спецификация ещё не определена. Цель — предоставить достаточно данных для написания алгоритма определения «лучшего» маршрутизатора для публикации Meta LS2 и для издателя — достаточной информации для взвешивания Destinations в Meta LS2. Данные будут содержать статистику маршрутизатора и Destination.
Данные могут опционально содержать данные дистанционного мониторинга о состоянии других серверов и возможности получения Meta LS. Эти данные не будут поддерживаться в первом релизе.
Данные могут опционально содержать конфигурационную информацию, опубликованную администраторским клиентом. Эти данные не будут поддерживаться в первом релизе.
Если указано “name: value”, это означает ключ и значение в JSON-карте. В остальных случаях спецификация ещё не определена.
Данные кластера (уровень верхнего уровня):
- cluster: имя кластера
- date: дата этих данных (long, мс с эпохи)
- id: Raft ID (целое)
Данные конфигурации (config):
- Любые параметры конфигурации
Статус публикации MetaLS (meta):
- destination: destination MetaLS, base64
- lastPublishedLS: если есть, base64-кодировка последнего опубликованного MetaLS
- lastPublishedTime: в мс, или 0 если никогда
- publishConfig: статус конфигурации издателя: off/on/auto
- publishing: статус издателя MetaLS, булево true/false
Данные маршрутизатора (router):
- lastPublishedRI: если есть, base64-кодировка последнего опубликованного Router Info
- uptime: время работы в мс
- Задержка заданий (Job lag)
- Исследовательские туннели
- Участвующие туннели
- Настроенная пропускная способность
- Текущая пропускная способность
Destinations (destinations): Список
Данные Destination:
- destination: destination, base64
- uptime: время работы в мс
- Настроенные туннели
- Текущие туннели
- Настроенная пропускная способность
- Текущая пропускная способность
- Настроенные соединения
- Текущие соединения
- Данные чёрного списка
Данные дистанционного мониторинга маршрутизатора:
- Последняя увиденная версия RI
- Время получения LS
- Данные теста соединения
- Данные профиля ближайших floodfill’ов за периоды вчера, сегодня и завтра
Данные дистанционного мониторинга Destination:
- Последняя увиденная версия LS
- Время получения LS
- Данные теста соединения
- Данные профиля ближайших floodfill’ов за периоды вчера, сегодня и завтра
Данные дистанционного мониторинга Meta LS:
- Последняя увиденная версия
- Время получения
- Данные профиля ближайших floodfill’ов за периоды вчера, сегодня и завтра
Интерфейс администрирования
Предстоит определить, возможно, в отдельном предложении. Не требуется для первого релиза.
Требования к интерфейсу администратора:
- Поддержка нескольких мастер-destination, т.е. нескольких виртуальных кластеров (ферм)
- Предоставление полного обзора общего состояния кластера — все статистики, опубликованные участниками, кто текущий лидер и т.д.
- Возможность принудительного удаления участника или лидера из кластера
- Возможность принудительной публикации metaLS (если текущий узел — издатель)
- Возможность исключения хэшей из metaLS (если текущий узел — издатель)
- Функциональность импорта/экспорта конфигурации для массовых развёртываний
Интерфейс маршрутизатора
Предстоит определить, возможно, в отдельном предложении. i2pcontrol не требуется для первого релиза, детальные изменения будут включены в отдельное предложение.
Требования к API Garlic Farm — маршрутизатор (внутри-JVM java или i2pcontrol)
- getLocalRouterStatus()
- getLocalLeafHash(Hash masterHash)
- getLocalLeafStatus(Hash leaf)
- getRemoteMeasuredStatus(Hash masterOrLeaf) // вероятно, не в MVP
- publishMetaLS(Hash masterHash, List
contents) // или подписанный MetaLeaseSet? Кто подписывает? - stopPublishingMetaLS(Hash masterHash)
- аутентификация — предстоит определить
Обоснование
Atomix слишком большой и не позволит нам настроить протокол для маршрутизации через I2P. Кроме того, его формат передачи данных не задокументирован и зависит от сериализации Java.
Примечания
Проблемы
- Нет способа для клиента узнать и подключиться к неизвестному лидеру. Небольшое изменение — чтобы Последователь отправлял конфигурацию как запись журнала в AppendEntriesResponse — могло бы решить это.
Миграция
Проблем обратной совместимости нет.