Коллатор

Единицы работы

Детерминированная модель объёма работы коллатора — расчёт по стадиям коллации, накопление между якорями, границы импорта и обратная связь для подстройки параметров.

Коллатор принимает часть решений о темпе своей работы не по показаниям системных часов, а по объёму уже проделанной работы. Эта мера называется единицами работы: детерминированное число, которое коллатор считает по каждому блоку и накапливает между обработанными якорями DAG-мемпула. По накопленному значению шард решает, когда импортировать следующий якорь. Кроме того, коллатор публикует данные о единицах работы наружу, как основу для последующей подстройки собственных параметров расчёта.

Единицы работы вместо системных часов

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

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

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

Часть формул возводит число аккаунтов в дробную степень. Такой показатель хранится в параметре конфигурации не как дробь, а как пара целых чисел от 0 до 99, упакованная в одно поле, а само возведение в степень вычисляется не арифметикой с плавающей точкой, а целочисленным двоичным поиском корня. Это тоже условие детерминизма: результат обязан совпасть бит в бит у всех узлов, потому что накопленные единицы работы записываются в состояние шарда.

Отсюда и смысл «часов» коллатора: пройденный объём шард меряет накопленными единицами работы, и решение об импорте следующего якоря получается одинаковым что на быстрой машине, что на медленной.

Стадии расчёта единиц работы

Коллация проходит три стадии, и у каждой своя формула единиц работы: подготовка групп сообщений, исполнение этих групп и финализация блока. Итог коллации: сумма трёх стадийных итогов. Отдельно от этой суммы считается стоимость возобновления коллации — цена того, что узел на время потерял темп после мастер-блока и должен вернуться к обычному ритму. В саму сумму трёх стадий она не входит и накапливается по особому правилу, разобранному в следующем разделе.

Подготовка групп сообщений

Стоимость подготовки складывается строго линейно: фиксированная часть на блок плюс слагаемые вида «счётчик читателя сообщений × параметр конфигурации» — по числу прочитанных внешних сообщений, прочитанных существующих внутренних сообщений, прочитанных новых внутренних сообщений и операций добавления сообщения в группу. Здесь нет ни логарифмов, ни деления на потоки, в отличие от исполнения и финализации. Счётчик операций добавления в группу растёт не по числу сообщений, а по числу попыток разложить сообщение по слотам группы, поэтому может заметно превышать число прочитанных сообщений.

Исполнение групп

Стоимость исполнения одной группы сообщений зависит от числа затронутых аккаунтов и суммарного израсходованного газа. Формула — (аккаунты × prepare + газ × execute / execute_delimiter) / потоки. Цена газа задана дробью, числителем и знаменателем, а не одним числом: деление целочисленное. Расчёт выполняется в конце исполнения каждой группы, когда уже известен фактический расход газа, и накапливается по всем группам блока.

Кроме исполнения в виртуальной машине, стадия учитывает постобработку транзакций: слагаемые вида «количество сообщений × двоичный логарифм этого количества × параметр» — по входящим сообщениям блока, исходящим сообщениям блока и новым сообщениям, вставленным в очередь. Логарифм берётся как целая часть двоичного, с нулём при количестве 0 или 1. Такая сложность отражает рост стоимости сериализации в очередь вместе с размером коллекции, а не строго линейную зависимость от числа элементов. Итог стадии исполнения: сумма стоимости работы в виртуальной машине и этой постобработки.

Финализация блока

Итог стадии финализации — не простая сумма: создание дифа очереди плюс максимум из стоимости финализации блока и стоимости применения дифа. Создание дифа обязано быть последовательным, потому что его результат нужен дальше, а применение дифа перекрывается по времени с работой над самим блоком, поэтому из двух параллельных ветвей в оценку идёт только более тяжёлая. Создание и применение дифа очереди считаются по одному входу, числу сообщений в дифе, каждое со своим параметром. Расчёт стоит в начале финализации, сразу после закрытия читателя сообщений, поэтому число уже известно к моменту, когда стартует параллельная работа над блоком и дифом.

Сама стоимость финализации блока внутри тоже собрана из параллельных ветвей: максимум из обновления словаря аккаунтов состояния вместе со сборкой блоков аккаунтов, построения словаря входящих сообщений и построения словаря исходящих сообщений, плюс стоимость меркл-обновления состояния и стоимость сериализации блока, которые добавляются уже после этого максимума. Максимум взят потому, что эти три ветви исполняются параллельно, а меркл-обновление и сериализация идут уже после них.

Обновление словаря аккаунтов состояния идёт линейно по числу обновлённых аккаунтов, плюс добавка — тоже пропорциональная числу обновлённых аккаунтов, но умноженная на логарифм числа аккаунтов шарда, делённый на число потоков, и на число аккаунтов шарда в дробной степени как поправку на размер состояния. Сборка блоков аккаунтов делится на раскладку транзакций по аккаунтам, которая выигрывает от потоков, и сборку итогового словаря блоков аккаунтов, которая от числа потоков не зависит вовсе.

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

Стоимость возобновления коллации

Стоимость возобновления коллации считается отдельно от трёх стадий: это оценка работы, которую узел тратит на ожидание записи состояния и на возврат к обычному ритму после паузы на мастер-блоке. База растёт по числу обновлённых аккаунтов сразу в дробных степенях самого этого числа и числа аккаунтов шарда, и получает свой параметр стоимости. Перед возведением в степень число обновлённых аккаунтов нормируется: ноль остаётся нулём, а любое ненулевое значение поднимается до нижней границы, иначе на малых обновлениях экспоненциальная оценка вырождается. Дальше стоимость делится на число блоков шарда между двумя мастер-блоками, и так же делится измеренное время: фактически узел ждёт только последнюю запись состояния, а расход разносится по всем блокам между мастер-блоками. Для мастерчейна это число всегда равно единице, то есть деления фактически не происходит.

В сумму, накопленную с последнего якоря, полная стоимость возобновления входит только на первом блоке после мастер-блока. Подушевая доля используется отдельно — в цене единицы работы и в метриках.

Накопление между якорями и границы импорта

В конце финализации коллатор складывает накопленные единицы работы с последнего импортированного якоря и итоги трёх стадий этого блока. На первом блоке после мастер-блока к сумме добавляется ещё и полная стоимость возобновления коллации. Сумма не растёт бесконечно: она обрезается потолком, кратным порогу импорта одного якоря wu_used_to_import_next_anchor. Множитель — допустимое отставание обработки якорей от консенсуса, две трети от max_consensus_lag_rounds конфигурации консенсуса, делённые на три раунда одной волны DAG-мемпула и увеличенные на единицу запаса. Смысл потолка простой: это столько единиц работы, сколько нужно на импорт всех якорей, которые коллатор вообще имеет право не обработать, плюс один такой порог про запас. Результат уходит в новое состояние шарда, в поле overload_history, и в следующем блоке станет входом того же расчёта.

Расходуется накопленное по-другому: решив импортировать якоря, коллатор шарда крутит цикл, импортируя по одному следующему якорю и вычитая из накопленного порог импорта, пока накопленного хватает. Вычитание насыщающее, ниже нуля не уходит.

У этого цикла есть второй ограничитель, по времени: если время цепочки только что импортированного якоря достигло времени цепочки последнего мастер-блока плюс mc_block_max_interval_ms, цикл останавливается, даже если накопленных единиц работы хватило бы ещё на несколько якорей. Ограничитель страхует от неточного расчёта единиц работы: если они накопились с запасом, а коллатор обогнал мемпул, узел не должен зависнуть в ожидании якорей — мастер-блок обязан выпускаться не реже этого интервала.

В мастерчейне такого цикла нет вовсе: попытка коллации мастер-блока либо импортирует ровно один якорь и полностью обнуляет накопленные единицы работы, либо не импортирует его совсем. Порог здесь ни при чём — вычитание порога за якорь работает в цикле импорта коллатора шарда. Потолок относится не к циклу импорта, а к финализации блока, где он обрезает накопленную сумму перед записью в overload_history. До него в этой попытке коллации просто не доходит: попытка коллации мастер-блока не вызывает финализацию ни в одной из своих ветвей.

Имена параметров не всегда совпадают со смыслом

Часть полей параметра 28 названа не в том значении, которое подсказывает имя: коллатор переименовывает их прямо при разборе структуры, и дальше в формулах они фигурируют уже под другими именами.

  • finalize.state_update_msg — не стоимость сообщения в меркл-обновлении, а упакованный показатель дробной степени для числа аккаунтов шарда;
  • finalize.serialize_diff — тоже показатель степени, но для числа обновлённых аккаунтов;
  • finalize.diff_tail_len — параметр стоимости возобновления коллации, а не длина хвоста дифов;
  • execute.serialize_enqueue и execute.serialize_dequeue — параметры постобработки входящих и исходящих сообщений блока;
  • execute.subgroup_size — верхняя граница оценки параллелизма.

Практическое следствие: по именам полей структуру параметра 28 читать нельзя — смысл поля определяется тем, куда оно подставлено в коде расчёта. Набор и порядок полей при этом зафиксированы TL-B-схемой с тегом #00, то есть переименовать поле в параметре конфигурации нельзя, не поменяв схему целиком. Переименование живёт только на стороне коллатора, в месте разбора структуры.

При этом поля параметра 28 msgs_stats и remaning_msgs_stats (секция подготовки групп) и execute_err (секция исполнения) в расчёте не читаются вовсе. В генераторе нулевого состояния все три выставлены в ноль.

Цена единицы работы и обратный контур подстройки

Измеренное время попадает в модель только в одном месте — в цену единицы работы: отношение наносекунд к посчитанным единицам работы. Она считается на всех уровнях, от каждого слагаемого формулы до стадии и коллации целиком, а при нулевых единицах работы полагается равной нулю. В отличие от самой величины единиц работы, цена не детерминирована: она получается из настоящих часов конкретной машины и поэтому у каждого узла своя.

Рядом с каждой рассчитанной величиной коллатор хранит ещё и её базу расчёта — ту же формулу без параметра конфигурации, а для исполнения ещё и отдельно нормированные на потоки суммы аккаунтов и газа. Коллатор считает эти базы расчёта на каждом блоке и отдаёт их наружу в поблочном снимке метрик, а сам их между блоками не складывает. Накапливает базы расчёта и времена тюнер единиц работы, отдельный компонент, который живёт в собственном крейте вне коллатора. При этом копится не сырое измеренное время, а обрезанное по выбросам. По накопленным суммам можно посчитать обратное: при какой величине параметра наблюдённое время дало бы заданную целевую цену единицы работы. Эту математику коллатор только предоставляет: сам он целевые параметры не считает и никуда их не записывает, а вызывает её тот же тюнер единиц работы.

Наружу коллатор передаёт данные о единицах работы двумя видами событий: снимок метрик блока в конце каждой коллации и лаг импорта якоря на каждом якоре, успешно импортированном в цикле попытки коллации шарда. Сам канал передачи — необязательное поле: если бы подписчика не было, коллатор не потерял бы эти данные, а опубликовал бы их сам, в метрики узла. Но в штатной сборке узла подписчик коллатору передаётся всегда, так что этот запасной путь существует только для тестов и для других встраивателей коллатора. На этом контракт коллатора с тюнером и заканчивается: что тюнер делает с событиями и как решает менять параметры, к модели единиц работы коллатора отношения не имеет.

Лаг импорта якоря — разность между системными часами узла в момент запроса якоря у мемпула и временем цепочки, с которым этот якорь был создан. Это единственное место всей модели, где сравниваются две разные системы отсчёта, локальные часы и время цепочки, и поэтому лаг не участвует ни в одной формуле единиц работы: он не детерминированная величина для решений, а наблюдение для настройки.

Значения по умолчанию

Формулы этого раздела читают вложенные секции параметра 28 конфигурации блокчейна — prepare, execute и finalize. Это сетевые значения: они одинаковы для всех узлов и меняются голосованием, а не локальным конфигом узла. Ниже перечислены значения, с которыми параметр создаётся при генерации нулевого состояния сети.

СекцияПараметрЗначениеСмысл
preparefixed_part1 000 000фиксированная стоимость стадии подготовки групп на блок
preparemsgs_stats0не используется в расчёте
prepareremaning_msgs_stats0не используется в расчёте
prepareread_ext_msgs400чтение одного внешнего сообщения
prepareread_int_msgs2 300чтение одного существующего внутреннего сообщения
prepareread_new_msgs750чтение одного нового внутреннего сообщения
prepareadd_to_msg_groups130одна операция добавления сообщения в группу
executeprepare57 000один аккаунт в исполняемой группе
executeexecute8 000числитель цены единицы газа
executeexecute_err0не используется в расчёте
executeexecute_delimiter1 000знаменатель цены единицы газа
executeserialize_enqueue80постобработка входящих сообщений блока (в расчёте — insert_in_msgs)
executeserialize_dequeue80постобработка исходящих сообщений блока (в расчёте — insert_out_msgs)
executeinsert_new_msgs80вставка новых сообщений в очередь
executesubgroup_size16верхняя граница числа потоков в оценке параллелизма (в расчёте — max_threads_count)
finalizebuild_transactions160сборка блоков аккаунтов
finalizebuild_accounts400обновление словаря аккаунтов состояния
finalizebuild_in_msg130построение словаря входящих сообщений
finalizebuild_out_msg130построение словаря исходящих сообщений
finalizeserialize_min2 500 000нижняя граница стоимости сериализации блока
finalizeserialize_accounts2 600сериализация одного блока аккаунтов
finalizeserialize_msg2 600сериализация одного входящего или исходящего сообщения
finalizestate_update_min1 000 000нижняя граница стоимости меркл-обновления
finalizestate_update_accounts700цена базы расчёта меркл-обновления
finalizestate_update_msg425показатель дробной степени для числа аккаунтов шарда, не цена сообщения (в расчёте — state_pow_coeff)
finalizecreate_diff900одно сообщение при создании дифа очереди
finalizeserialize_diff105показатель дробной степени для числа обновлённых аккаунтов, не цена сериализации дифа (в расчёте — updated_accounts_count_pow_coeff)
finalizeapply_diff2 000одно сообщение при применении дифа очереди
finalizediff_tail_len1 300стоимость возобновления коллации, не длина хвоста дифов (в расчёте — store_state_wu_param)

Показатели степени state_update_msg и serialize_diff записаны не дробью, а парой целых чисел от 0 до 99, упакованных в одно поле по правилу «числитель × 100 + знаменатель»: 425 разбирается как 4/25 (то есть 0,16), 105 — как 1/5 (то есть 0,2).