Оверлеи
Транспорт одинаково обслуживает обращения любых узлов сети, но многие задачи касаются не всей сети, а лишь её части. Такое подмножество называется оверлеем — логической подсетью поверх общего транспорта, которая объединяет часть узлов для отдельной задачи. Оверлей бывает публичным, с открытым членством, и приватным, с фиксированным списком участников, а единый сервис на одном узле обслуживает оба вида.
Оверлей-сервис
Оверлей-сервис держит на узле две раздельные таблицы (публичные оверлеи и приватные оверлеи), обе с ключом по идентификатору оверлея. Сам он устроен как обычный сервис сетевого слоя: транспорт передаёт ему входящие обращения через Router, а дальше сервис по идентификатору оверлея в начале тела обращения находит нужный оверлей и передаёт обращение туда. Если оверлея с таким идентификатором нет, обращение молча отбрасывается. Один и тот же идентификатор не может принадлежать одновременно и публичному, и приватному оверлею — добавление оверлея в одну таблицу отклоняется, если этот идентификатор уже занят в другой.
Оверлей адресуется 32-байтовым идентификатором, который ставится префиксом перед телом каждого обращения внутри оверлея. В человекочитаемом виде он кодируется шестнадцатеричной строкой. Механизм адресации не определяет, как именно вычисляется идентификатор конкретного оверлея — из его назначения, ключа или чего-то ещё: это остаётся на стороне того, кто создаёт оверлей.
Оверлеи в таблицах регистрируются и снимаются во время работы узла, независимо друг от друга. Сам оверлей-сервис собирается с необязательным DHT-сервисом: без него у публичных оверлеев не будет ни публикации, ни обнаружения участников через DHT, а список записей будет пополняться только обменом друг с другом и обращениями самих участников.
Помимо адресных обращений, оверлей-сервис обрабатывает два общих запроса без префикса оверлея: обмен случайными записями участников и запрос собственной записи узла. Оба относятся к публичным оверлеям и разобраны дальше.
Приватный оверлей
Приватный оверлей хранит фиксированный набор участников — множество идентификаторов узлов, заданное при создании. Перед тем как передать входящий запрос или сообщение своему прикладному сервису, оверлей проверяет, что отправитель есть в этом множестве, и не обрабатывает обращение, если его там нет. Список членства и есть контроль доступа приватного оверлея.
Список редактируется во время работы узла: участников можно добавлять, удалять и полностью очищать. На каждое изменение оверлей рассылает поток событий о появлении и исчезновении участника. Участники приватного оверлея не подписаны никакой сетевой подписью и не публикуются в DHT: это просто согласованный между узлами список идентификаторов. У приватного оверлея нет ни фоновых задач обмена, ни записей со сроком годности — в этом его прямая противоположность публичному.
Публичный оверлей
Публичный оверлей при приёме запроса или сообщения не требует, чтобы отправитель был в его списке участников. Обращение обслуживается, если отправитель не в бан-листе этого оверлея и прошёл ограничитель частоты, а принадлежность к списку записей на это решение не влияет. Более того, если отправитель оверлею совсем неизвестен, оверлей его не отклоняет: он ставит идентификатор в очередь на «доразрешение» и позже по нему подтягивает подписанную запись.
Отсюда разное назначение списка участников у двух видов оверлея. У приватного оверлея список — это пропуск: кто не в списке, того не обслуживают. У публичного список записей, наоборот, это накопленное знание об известных участниках для обнаружения и распространения: кого выбрать целью обмена, кого отдать другим в ответ на такой же запрос. Доступ к обслуживанию при этом открыт всем незабаненным.
Запись участника
Запись публичного оверлея состоит из идентификатора узла, времени создания и подписи. Подпись ставится ключом самого узла над связкой «идентификатор оверлея + идентификатор узла + время создания», поэтому запись действительна только в своём оверлее: перенести её в другой нельзя — там в подписываемую связку войдёт уже другой идентификатор оверлея. Идентификатор узла в Tycho служит его собственным публичным ключом, а значит, запись проверяется кем угодно, без обращения к внешнему реестру.
Принимая записи из непроверенных источников (обмена, обнаружения через DHT, обращений неизвестных узлов), оверлей проверяет каждую. Запись молча пропускается, если она просрочена, если её узел в бан-листе этого оверлея, если это запись самого локального узла, если из идентификатора узла не извлекается публичный ключ, или если подпись не сходится. У записи есть срок годности: она считается просроченной, если время создания плюс срок годности уже меньше текущего времени, и точно так же отбраковывается, если её время создания лежит в будущем дальше порога рассинхронизации часов в 1 секунду. Просроченные и забаненные записи периодически вычищаются из оверлея. Если узел уже есть среди записей, новая запись заменяет прежнюю только если она новее по времени создания — более старую оверлей отбрасывает.
Ограничитель частоты и бан-лист
Публичному оверлею можно опционально придать ограничитель частоты. Если он задан, политика определяет класс каждого входящего обращения и выдаёт одно из трёх решений: пропустить с учётом лимита, пропустить в обход лимита или отбросить. Лимит считается раздельно для каждой пары «IP-адрес источника + класс запроса» и работает как токен-бакет — у пары есть свой запас проходов, который со временем восполняется. Без ограничителя частота обращений ничем не ограничена. У приватного оверлея ограничителя частоты нет вовсе.
Отдельно у каждого публичного оверлея есть собственный бан-лист идентификаторов узлов. Пока узел в этом бан-листе:
- его входящие обращения отбрасываются на входе оверлея;
- его записи не принимаются;
- уже лежащие в оверлее записи забаненного узла вычищаются при периодической очистке.
Это самостоятельный механизм, не связанный с баном в реестре известных узлов транспорта, который описан в статье о DHT: области действия не пересекаются, а бан в оверлее останавливает обслуживание только внутри этого оверлея. В версии 0.3.11 бан-лист публичного оверлея реализован, но методы бана и разбана нигде не вызываются — механизм заложен на будущее, но пока не задействован.
Наполнение списка участников публичного оверлея
Для каждого публичного оверлея оверлей-сервис ведёт четыре периодические фоновые задачи: обмен записями с другим участником, публикацию своего снимка записей в DHT, обнаружение чужих записей там же и сбор записей у узлов, которые сами обратились к оверлею. У каждой задачи свои период и случайная задержка к нему, и все они конфигурируются. Приватные оверлеи в этом цикле не участвуют: у них нет ни обмена, ни публикации в DHT.
Наполнение записями из этих источников ограничено вместимостью: оверлей добавляет новых участников из непроверенных источников только пока число записей не достигнет минимальной вместимости. После этого новые узлы из таких источников не добавляются, но обновление уже присутствующей записи на более свежую продолжает работать и после порога — ограничение действует только на добавление новых участников.
Обмен записями
Периодическая задача обмена выбирает из оверлея случайного участника, чья контактная информация уже разрешена, и посылает ему запрос с пачкой записей: своей собственной свежеподписанной записью (она в пачке всегда первая) и случайными записями оверлея, которых не больше размера пачки минус одна. Число случайных записей конфигурируется размером пачки exchange_public_entries_batch, который по умолчанию равен 20, то есть их не больше 19. Независимо от этого сам список записей в протоколе ограничен сетевым пределом в 20 записей, который по умолчанию совпадает с размером пачки. Обмен взаимный: принимающая сторона сначала добавляет присланные записи в свой оверлей, а затем возвращает в ответ свою случайную выборку — уже не «минус одна», а до полного размера пачки, — исключая только что присланные. Если в оверлее нет ни одного участника с уже разрешённой контактной информацией, обмен в этот раз пропускается.
Публикация и обнаружение через DHT
Список записей публичного оверлея — единственный вид группового (объединяемого) значения DHT в системе. Экземпляров такого значения при этом может быть много, по одному на оверлей: каждый хранится под ключом, собранным из имени этого вида значения и идентификатора соответствующего оверлея как группы. У этого группового значения есть свой обработчик слияния. Он принимает значение только из рук самого узла-издателя, а версию, пришедшую снаружи по сети от другого узла, отклоняет как значение из недопустимого источника. Новый локальный снимок он не дополняет прежним, а целиком его замещает. Оверлей-сервис регистрирует этот обработчик в DHT при появлении публичного оверлея и снимает при его удалении. Регистрация происходит, только если оверлей-сервис собран с DHT-сервисом.
Публикация поэтому односторонняя: периодическая задача сохранения кладёт свой снимок записей только в собственное DHT-хранилище узла, ничего никому не рассылая. Другой узел находит этот снимок, когда его собственная задача обнаружения ищет в DHT то же самое групповое значение и получает найденный снимок от узла-издателя — обычным поиском, без всякого слияния. Найденный список обрезается до предела вместимости снимка и добавляется в оверлей как записи из непроверенного источника. Полная механика групповых значений DHT и их единственного потребителя описана в статье о DHT.
Сбор от обратившихся узлов
Когда публичному оверлею приходит обращение от узла, которого нет среди его записей, оверлей ставит идентификатор такого узла в ограниченную очередь неизвестных узлов ёмкостью 5. Пока очередь заполнена, новые идентификаторы в неё не добавляются. Периодическая задача сбора забирает всю очередь целиком. По каждому узлу, который известен транспорту, она посылает запрос на собственную запись, проверяет, что идентификатор в ответе совпадает с запрошенным, и добавляет полученную запись в оверлей как запись из непроверенного источника. Узлы, вовсе неизвестные транспорту, из сбора отсеиваются.
Так активный участник, впервые обратившийся к узлу, со временем сам попадает в список его записей — оверлей достраивает знание об участниках не только обменом и обнаружением через DHT, но и по факту входящих обращений.
Собственная запись и её отсутствие
Запрос на собственную запись, который обрабатывает оверлей-сервис, возвращает подписанную запись адресуемого узла в указанном оверлее — именно эту запись собирает задача сбора у обратившихся узлов, и именно она всегда первой стоит в пачке обмена и в снимке для DHT. Узел хранит свою последнюю такую запись и обновляет её каждый раз, когда фоновые задачи обмена и сохранения формируют новую.
Такая запись формируется только периодическими фоновыми задачами. Если её ещё нет, узел отвечает тем же ответом, что и в случае, если у него вовсе нет оверлея с запрошенным идентификатором. Разница между «я не веду этот оверлей» и «я в нём, но фоновые задачи ещё не отработали» для спрашивающего узла неразличима: со стороны это выглядит как будто оверлей ведётся в теневом режиме.
Конфигурация
Общие параметры оверлей-сервиса — периоды фоновых задач публичного оверлея, их случайные задержки, размер пачки обмена, предел записей снимка и таймаут запроса сбора — задаются при построении оверлей-сервиса. Если конфиг не передан, применяются значения по умолчанию.
| Параметр | По умолчанию | Что задаёт |
|---|---|---|
public_overlay_peer_store_period | "3m" | Период сохранения снимка своих записей в DHT |
public_overlay_peer_store_max_jitter | "30s" | Максимальная случайная задержка к периоду сохранения |
public_overlay_peer_store_max_entries | 20 | Предел числа записей в снимке для DHT, тот же предел используется при обрезке обнаруженных записей |
public_overlay_peer_exchange_period | "3m" | Период обмена записями со случайным участником |
public_overlay_peer_exchange_max_jitter | "30s" | Максимальная случайная задержка к периоду обмена |
public_overlay_peer_collect_period | "10s" | Период сбора записей у обратившихся неизвестных узлов |
public_overlay_peer_collect_max_jitter | "5s" | Максимальная случайная задержка к периоду сбора |
public_overlay_peer_discovery_period | "3m" | Период обнаружения записей в DHT |
public_overlay_peer_discovery_max_jitter | "30s" | Максимальная случайная задержка к периоду обнаружения |
exchange_public_entries_batch | 20 | Число записей, отправляемых в одном запросе обмена |
query_timeout | "100ms" | Таймаут одиночного запроса в задаче сбора |
Часть параметров относится не ко всему оверлей-сервису, а к конкретному оверлею, и в эту конфигурацию не входит — они задаются программно при создании самого оверлея:
| Параметр | По умолчанию | Что задаёт |
|---|---|---|
with_entry_ttl | 1 час | Срок годности записи публичного оверлея |
with_min_capacity | 100 | Вместимость, до которой публичный оверлей добавляет новых участников из непроверенных источников |
with_entry_events_channel_size | 100 | Вместимость канала событий о появлении и исчезновении участника приватного оверлея |