Розподілений реєстр для колісних пар: досвід з Hyperledger Fabric

Привіт я працюю в команді проекту РРД КП (розподілений реєстр даних для контролю життєвого циклу колісних пар). Тут я хочу поділитися досвідом нашої команди у розробці корпоративного блокчейну для даного проекту в умовах обмежень, що накладаються технологією. Здебільшого я говоритиму про Hyperledger Fabric, але описаний тут підхід може бути екстраполирован на будь-який permissioned блокчейн. Кінцева мета наших досліджень - готувати корпоративні блокчейн-рішення так, щоб підсумковим продуктом було приємно користуватися і не дуже важко підтримувати.

Тут не буде жодних відкриттів, несподіваних рішень і тут не висвітлюватимуться жодні унікальні розробки (бо їх у мене немає). Я просто хочу поділитися своїм скромним досвідом, показати, що так можна було і, можливо, прочитати про чужий досвід прийняття хороших і не дуже рішень у коментарях.

Проблема: блокчейни поки що не масштабуються

Сьогодні зусилля багатьох розробників спрямовані на те, щоб зробити блокчейн справді зручною технологією, а не бомбою сповільненої дії у гарній обгортці. Канали станів, optimistic rollup, plasma та шардинг, можливо, стануть повсякденністю. Колись. Можливо, TON знову відкладе запуск на півроку, а чергова Plasma Group припинить своє існування. Ми можемо вірити у черговий roadmap та читати на ніч блискучі white papers, але тут і зараз потрібно щось робити з тим, що ми маємо. Get shit done.

Завдання, поставлене перед нашою командою в поточному проекті, виглядає загалом так: є безліч суб'єктів, що досягає декількох тисяч, не бажає будувати відносини на довірі; необхідно побудувати на DLT таке рішення, яке працюватиме на звичайних ПК без спеціальних вимог до продуктивності та забезпечуватиме користувацький досвід не гірше за будь-які централізовані системи обліку. Технологія, що лежить в основі рішення, має звести до мінімуму можливість зловмисних маніпуляцій з даними саме тому тут блокчейн.

Гасла з whitepapers та ЗМІ обіцяють нам, що чергова розробка дозволить здійснювати мільйони транзакцій на секунду. Що ж насправді?

Mainnet Ethereum зараз працює зі швидкістю ~30 tps. Вже тільки через це його складно сприймати як придатний для корпоративних потреб блокчейн. Серед permissioned-рішень відомі бенчмарки, що показують 2000 tps (Кворум) або 3000 tps (Тканина Hyperledger, У публікації трохи менше, але потрібно враховувати, що бенчмарк проводився на старому consensus engine). Була спроба радикальної переробки Fabric, Що дала не найгірші результати, 20000 tps, але поки це лише академічні дослідження, які чекають своєї стабільної імплементації. Навряд чи корпорація, яка може собі дозволити утримувати відділ блокчейн-розробників, миритиметься з такими показниками. Але проблема не тільки черезпочаток, є ще величезний.

Затримка

Затримка від моменту ініціації транзакції до остаточного затвердження системою залежить тільки від швидкості проходження повідомлення через всі етапи валідацій і упорядкування, а й від параметрів формування блоку. Навіть якщо наш блокчейн дозволяє нам коммітити зі швидкістю 1000000 tps, але вимагає при цьому 10 хвилин формування блоку розміром 488 Мб, чи стане нам легше?

Давайте подивимося ближче на життєвий цикл транзакції Hyperledger Fabric, щоб зрозуміти, на що йде час і як це співвідноситься з параметрами формування блоку.

Розподілений реєстр для колісних пар: досвід з Hyperledger Fabric
взято звідси: hyperledger-fabric.readthedocs.io/en/release-1.4/arch-deep-dive.html#swimlane

(1) Клієнт формує транзакцію, відправляє на endorsing peers, останні симулюють транзакцію (застосовують зміни, що вносяться чейнкодом, на поточний стан, але не комітують у леджер) та отримують RWSet - імена ключів, версії та значення, взяті з колекції в CouchDB 2) endorsers відправляють назад клієнту підписаний RWSet, (3) клієнт або перевіряє наявність підписів всіх необхідних бенкетів (endorsers), а потім відправляє транзакцію на ordering service, або відправляє без перевірки (перевірка все одно відбудеться пізніше), ordering service формує блок і ( 4) відправляє назад на всі бенкети, не тільки endorsers; бенкети перевіряють відповідність версій ключів у read set версіях у базі даних, наявність підписів всіх endorsers і нарешті комітят блок.

Але це ще не все. За словами «ордерер формує блок» ховається не тільки впорядкування транзакцій, а й 3 послідовні мережеві запити від лідера до фоловерів і назад: лідер додає повідомлення в лог, відправляє фолловерам, останні додають у свій лог, відправляють підтвердження успішної реплікації лідеру, лідер комітіт , відправляє підтвердження комміту фоловерів, фоловери коммітят. Чим менший розмір та час формування блоку, тим частіше доведеться ordering service встановлювати консенсус. Hyperledger Fabric має два параметри формування блоку: BatchTimeout - час формування блоку і BatchSize - розмір блоку (кількість транзакцій і розмір самого блоку в байтах). Як тільки один із параметрів досягає ліміту, випускається новий блок. Чим більше нод ордерерів, тим довше це відбуватиметься. Отже, потрібно збільшувати BatchTimeout та BatchSize. Але оскільки RWSet'и версіонуються, то чим більше ми зробимо блок, тим вища ймовірність MVCC-конфліктів. До того ж зі збільшенням BatchTimeout катастрофічно деградує UX. Мені здається розумною та очевидною наступна схема для вирішення цих проблем.

Як уникнути очікування фіналізації блоку та не втратити можливості відстеження статусу транзакції

Чим більший час формування та розмір блоку, тим вище пропускна здатність блокчейна. Одне з іншого прямо не слід, проте слід згадати, що встановлення консенсусу в RAFT вимагає трьох мережевих запитів від лідера до фоловерів і назад. Чим більше нод ордерів, тим довше це відбуватиметься. Чим менший розмір та час формування блоку, тим більше таких інтеракцій. Як збільшувати час формування та розмір блоку, не збільшуючи час очікування відповіді системи для кінцевого користувача?

По-перше, потрібно якось вирішувати MVCC-конфлікти, викликані великим розміром блоку, який може включати різні RWSet'и з однією версією. Очевидно, на стороні клієнта (стосовно блокчейн-мережі, це цілком може бути бекенд, і я маю на увазі саме його) потрібен хендлер MVCC-конфліктів, який може бути як окремим сервісом, так і звичайним декоратором над викликом, що ініціює транзакцію, з логікою retry.

Retry можна реалізувати з експоненційною стратегією, але тоді latency буде деградувати так само експоненційно. Тому слід використовувати або рандомізований у певних невеликих межах retry, або постійний. З огляду на можливі колізії у першому варіанті.

Наступний етап — зробити взаємодію клієнта із системою асинхронною, щоб він не чекав 15, 30 або 10000000 секунд, які ми встановимо як BatchTimeout. Але при цьому потрібно зберегти можливість переконатися в тому, що зміни, ініційовані транзакцією, записані/не записані блокчейн.
Для збереження статусу транзакцій можна використовувати базу даних. Найпростіший варіант - CouchDB через зручність використання: база має UI з коробки, REST API, для неї легко можна налаштувати реплікацію і шардування. Можна створити окрему колекцію в тому ж інстансі CouchDB, який використовує Fabric для зберігання свого world state. Нам слід зберігати документи такого виду.

{
 Status string // Статус транзакции: "pending", "done", "failed"
 TxID: string // ID транзакции
 Error: string // optional, сообщение об ошибке
}

Цей документ записується в базу до передачі транзакції на бенкети, користувачеві повертається ID сутності (цей же ID використовується як ключ), якщо це операція створення чогось, а потім поля Status, TxID і Error оновлюються в міру надходження релевантної інформації від бенкетів.

Розподілений реєстр для колісних пар: досвід з Hyperledger Fabric

У цій схемі користувач не чекає, коли ж нарешті сформується блок, спостерігаючи колесо, що крутиться, на екрані 10 секунд, він отримує моментальний відгук системи і продовжує працювати.

Ми вибрали BoltDB для зберігання статусів транзакцій, тому що нам потрібно економити пам'ять і не хочеться витрачати час на мережеву взаємодію з сервером бази даних, що окремо стоїть, тим більше коли ця взаємодія відбувається по plain text протоколу. До речі, використовуєте ви CouchDB для реалізації описаної вище схеми або просто для зберігання world state, у будь-якому випадку є сенс оптимізувати спосіб зберігання даних у CouchDB. За замовчуванням у CouchDB розмір b-tree вузлів дорівнює 1279 байт, що значно менше розміру сектора на диску, а значить читання, так і перебалансування дерева вимагатиме більше фізичних звернень до диска. Оптимальний розмір відповідає стандарту Розширений формат і становить 4 кілобайти. Для оптимізації потрібно встановити параметр btree_chunk_size рівним 4096 у файлі конфігурації CouchDB. Для BoltDB таке ручне втручання не вимагається.

Backpressure: buffer strategy

Але повідомлень може бути дуже багато. Більше, ніж система здатна обробити, розділяючи ресурси з десятком інших сервісів, крім відбитих на схемі — і все це має працювати безвідмовно навіть на машинах, на яких запуск Intellij Idea буде справою вкрай стомлюючим.

Проблема різної пропускної спроможності сполучених систем, прод'юсера та консьюмера вирішується різними способами. Побачимо, що ми могли б зробити.

Випадання: ми можемо заявити, що здатні обробляти не більше X транзакцій за T секунд. Усі запити, що перевищують цей ліміт, скидаються. Це досить просто, але про UX можна забути.

Управління: у консьюмера повинен бути якийсь інтерфейс, через який він, залежно від навантаження, зможе контролювати tps прод'юсера. Непогано, але це накладає зобов'язання на розробників клієнта, що створює навантаження, реалізовувати цей інтерфейс. Для нас це неприйнятно, оскільки блокчейн у перспективі буде інтегрований у велику кількість систем, що давно існують.

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

Розподілений реєстр для колісних пар: досвід з Hyperledger Fabric

У схему додалися дві нові дії: (1) після надходження запиту на API в чергу кладеться повідомлення з параметрами, необхідними для виклику транзакції, і клієнт отримує повідомлення про те, що транзакція прийнята системою, (2) бекенд із швидкістю, що задається в конфізі, читає дані із черги; ініціює транзакцію та оновлює дані у сховищі статусів.
Тепер можна збільшувати час формування та місткість блоку настільки, наскільки захочеться, приховуючи затримки від користувача.

Інші інструменти

Тут не було нічого сказано про чейнкод, тому що в ньому, як правило, нема чого оптимізувати. Чейнкод має бути максимально простим і безпечним - це все, що потрібно. Писати чейнкод просто і безпечно нам дуже допомагає фреймворк ССKit від S7 Techlab та статичний аналізатор revive^CC.

Крім того, наша команда розробляє набір утиліт для того, щоб зробити роботу з Fabric простою та приємною: блокчейн експлорер, утиліта для автоматичної зміни конфігурації мережі (додавання/видалення організацій, нод RAFT), утиліта для відгуку сертифікатів та видалення identity. Якщо хочете зробити свій внесок — welcome.

Висновок

Цей підхід дозволяє легко замінити Hyperledger Fabric на Quorum, інші приватні Ethereum мережі (PoA або навіть PoW), істотно знизити реальну пропускну спроможність, але при цьому зберегти нормальний UX (як для користувачів у браузері, так і з інтегрованих систем). При заміні Fabric на Ethereum у схемі потрібно буде змінити лише логіку retry-сервісу/декоратора з обробки MVCC-конфліктів на атомарний інкремент nonce та повторне відправлення. Буферизація та сховище статусів дозволили відв'язати час відгуку від часу формування блоку. Тепер можна додавати тисячі нод ордерів і не боятися, що блоки формуються дуже часто і навантажують ordering service.

Загалом це все, чим я хотів поділитися. Радий, якщо це комусь допоможе в роботі.

Джерело: habr.com

Додати коментар або відгук