Мы продолжаем развивать Exonum. В этот раз мы сосредоточили внимание на двух вещах: полностью перевели хранение данных на RocksDB, при этом прекратив поддержку LevelDB, и переписали сетевой код на Tokio. Зачем: эти решения позволили нам повысить эффективность хранения данных и увеличили производительность кода в сети.
О переходе с LevelDB на RocksDB мы говорили в статье о релизе прошлой версии фреймворка. Поэтому в сегодняшнем анонсе хотим подробнее остановиться на изменениях, которые пришли с Tokio, рассказать, как мы реализовали асинхронную обработку событий, и отметить другие улучшения.
/ Exonum
Tokio — это обработчик цикла событий, который специально заточен под асинхронное программирование. Он базируется на асинхронном вводе/выводе (I/O), что обеспечивает непрерывную работу с событиями в потоке. Отдельные события обслуживаются в фоновом режиме до того момента, пока не будет получен конечный результат.
Параметр, обеспечивающий асинхронное управление событиями, называется future. Это значение, которое будет подсчитано в будущем, однако на текущий момент не известно. Futures являются составными объектами и могут включать в себя цепочки событий, реализующих бизнес-логику процессов.
Например, в контексте Exonum, это может быть представлено следующим образом:
Подсоединиться к узлу «В» —> Обработать байты узла «В» и разбить их на сообщения —> Перенаправить каждое сообщение в канал узла
Помимо futures в Tokio также присутствует базовый компонент для асинхронного I/O — stream. Это поток байтов, который преобразуется в другие элементы. В то время как future возвращает только один конечный результат, stream может возвращать несколько результатов, пока не будет полностью выполнен. Иными словами, stream оперирует серией событий, что делает его эффективным при работе со сложными схемами взаимодействий.
Например, в Exonum TCP stream будет преобразован в сообщения Exonum. Этот stream направляется в код консенсуса по соответствующему каналу, и после их слияния код отбирает необходимые ему сообщения от сети и обрабатывает их.
Еще одним существенным улучшением стала возможность узлов работать с тремя разными очередями вместо одной, которая существовала в сетевом коде на Mio. Глобально сеть управляет тремя основными типами событий при выполнении алгоритма консенсуса: входящими транзакциями, сообщениями и таймером.
Раньше транзакции и сообщения перегружали одну очередь, и события таймера оставались за её пределами. Соответственно, новые раунды алгоритма консенсуса не начинались, и он попросту стопорился. Наличие дополнительных очередей резко повысило стабильность узлов.
Аналогично для нового кода была организована работа в два потока: один для обслуживания внешних сетевых событий (ответы на сетевые сообщения), а второй — для самого консенсуса. При этом также существует отдельный поток для передачи транзакций, полученных через REST API. В результате это повысило производительность и стабильность работы сети: новые блоки принимаются с равномерным интервалом в 0,5 секунды, при этом, при необходимости скорость принятия блока может быть снижена.
Удвоилась скорость подтягивания, то есть снизилось время, необходимое на получение отстающим узлом отсутствующих у него блоков. Например, в сети с 4 узлами скорость принятия блока при параллельном подтягивании ранее составляла 200–300 мс. Перевод только отстающего узла на новый код привел к тому, что интервал сократился до 100 мс. Ожидается, что если адаптировать всю сеть, показатели станут еще лучше. Статистика выше приведена для пустых блоков и может отличаться для сети «под нагрузкой».
Мы также провели приблизительные расчеты влияния описанных изменений на производительность кода в сети с 4 валидаторами и одним дата-центром. Средняя скорость обработки при принятии блоков с тысячей транзакций составила:
- 7318 транзакций в секунду для кода на Mio и LevelDB;
- 20237 транзакций в секунду для кода на Tokio и LevelDB;
- 31571 транзакций в секунду для кода на Tokio и RocksDB.
Таким образом, с переходом на Tokio код в Exonum стал более производительным и структурированным, что упростило его поддержание.
Еще одним улучшением стало внедрение нового индекса в хранилище — SparseListIndex. Он представляет собой упорядоченную структуру (перечень элементов), который может содержать пропуски. Это означает, что при удалении произвольной строки в списке, индекс сохранит работоспособность. Для сравнения в ListIndex все вхождения строго пронумерованы от 0 до n-1, где n — количество элементов списка, и удалять их можно только с конца списка.
Также была добавлена базовая инфраструктура для сбора метрик и статистических данных в Exonum и сервисах. Опция полезна для оценки эффективности работы узла и будет окончательно реализована в будущих релизах.
Наконец, мы бы хотели обратить внимание разработчиков сервисов на следующие позиции:
- Поле events_pool_capacity в MemoryPoolConfig было заменено на новую конфигурацию EventsPoolCapacity;
- NodeBuilder теперь использует ServiceFactory как объект, а не как тип;
- Была изменена сигнатура функции gen_prefix в модуле schema;
- Изменился конструктор индексов. В связи с использованием column families, доступных в RocksDB, стало возможным определение индексов при помощи строкового наименования, как это обычно реализовано в других базах данных (например, Transactions, Wallets и т. д.).
Все выше перечисленные изменения необходимо внести в существующий сервисный код, чтобы поддерживать его в актуальном состоянии по отношению к фреймворку.
/ Exonum
Еще больше о нововведениях — по ссылке. С вопросами к нашей команде и предложениями по развитию проекта вы можете обратиться в Gitter или GitHub.
Автор: alinatestova