Еволюція архітектури торгово-клірингової системи Московської біржі. Частина 2

Еволюція архітектури торгово-клірингової системи Московської біржі. Частина 2

Це продовження довгої розповіді про наш тернистий шлях до створення потужної, високонавантаженої системи, що забезпечує роботу Біржі. Перша частина тут: habr.com/ua/post/444300

Таємнича помилка

Після численних тестів оновлена ​​торгово-клірингова система була введена в дію, і ми зіткнулися з багом, про який можна писати детективно-містичну повість.

Незабаром після запуску на головному сервері одна з транзакцій опрацювалася помилково. При цьому на резервному сервері все було гаразд. Виявилося, що проста математична операція обчислення експонентів на головному сервері дала негативний результат від дійсного аргументу! Продовжили дослідження, і в SSE2-реєстрі знайшли відмінність в одному биті, який відповідає за округлення під час роботи з числами з плаваючою комою.

Написали просту тестову утиліту для обчислення експоненти із виставленим бітом округлення. З'ясувалося, що в тій версії RedHat Linux, яку ми використовували, був баг у роботі з математичною функцією, коли вставлявся бит. Ми повідомили про це RedHat, через деякий час отримали від них патч і накотили його. Помилка більше не виникала, але було незрозуміло, звідки узявся цей біт? За нього відповідала функція fesetround з мови С. Ми ретельно проаналізували свій код у пошуках передбачуваної помилки: перевірили всі можливі ситуації; розглянули всі функції, які використовували заокруглення; намагалися відтворити збійну сесію; використовували різні компілятори з різними опціями; застосовували статичний та динамічний аналіз.

Причини помилки знайти не вдалося.

Тоді почали перевіряти апаратну частину: - провели навантажувальне тестування процесорів; перевірили оперативну пам'ять; навіть прогнали тести на дуже малоймовірний сценарій багатобітової помилки в одному осередку. Безрезультатно.

Зрештою зупинилися на теорії зі світу фізики високих енергій: у наш дата-центр прилетіла якась високоенергетична частка, пробила стінку корпусу, потрапила в процесор і викликала залипання тригерної клямки в тому самому биті. Ця абсурдна теорія отримала назву нейтрино. Якщо ви далекі від фізики елементарних частинок: нейтрино майже не взаємодіють з навколишнім світом, і точно не здатні вплинути на роботу процесора.

Оскільки знайти причину збою так і не вдалося, про всяк випадок виключили сервер з експлуатації, що «провинився».

Через якийсь час ми почали покращувати систему гарячого резервування: запровадили так звані «теплі резерви» (warm) — асинхронні репліки. Вони отримували потік транзакцій, які можуть бути в різних дата-центрах, але при цьому warm'и не підтримували активної взаємодії з іншими серверами.

Еволюція архітектури торгово-клірингової системи Московської біржі. Частина 2

Навіщо це було зроблено? Якщо резервний сервер виходить з ладу, то прив'язаний до головного warm серверу стає новим резервним. Тобто після збою система залишається до кінця торгової сесії з одним головним сервером.

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

У результаті чергового аналізу ситуації виникла теорія, що це може бути пов'язані з ОС. Ми написали просту програму, яка у нескінченному циклі викликає функцію fesetround, Запам'ятовує поточний стан і перевіряє його через sleep, причому робиться це в безлічі конкуруючих потоків. Підібравши параметри sleep і кількості потоків, ми стали відтворювати збій бітів приблизно через 5 хвилин роботи утиліти. Проте служба підтримки Red Hat не змогла її відтворити. Тестування інших наших серверів показало, що помилки схильні лише з них, у яких встановлені певні процесори. У цьому перехід на нове ядро ​​вирішував проблему. Зрештою, ми просто замінили ОС, а справжня причина бага так і залишилася нез'ясованою.

І раптом минулого року на Хабрі вийшла стаття «Як я знайшов баг у процесорах Intel Skylake». Описана в ній ситуація була дуже схожа на нашу, але автор просунувся далі і висунув теорію, що помилка була в мікрокоді. А при оновленні ядер Linux виробники також оновлюють мікрокод.

Подальший розвиток системи

Хоча помилки ми позбулися, ця історія змусила знову переглянути архітектуру системи. Адже ми не були захищені від повторення таких багів.

В основу чергових доопрацювань системи резервування лягли такі принципи:

  • Не можна нікому вірити. Сервери можуть працювати неправильно.
  • Мажоритарне резервування.
  • Забезпечення консенсусу. Як логічний додаток до мажоритарного резервування.
  • Можливі подвійні відмови.
  • Живітість. Нова схема гарячого резервування має бути не гіршою за попередню. Торгівля повинна йти безперебійно до останнього сервера.
  • Незначне збільшення затримки. Будь-який простий спричиняє величезні фінансові втрати.
  • Мінімальна взаємодія по мережі, щоб затримка була якнайменша.
  • Вибір нового головного сервера за секунду.

Жодне з рішень, що були на ринку, нам не підійшло, а протокол Raft ще тільки зароджувався, тому ми створили власне рішення.

Еволюція архітектури торгово-клірингової системи Московської біржі. Частина 2

Мережева взаємодія

Крім системи резервування, ми зайнялися модернізацією мережевої взаємодії. Підсистема вводу-виводу являла собою безліч процесів, що найгіршим чином впливало на джиттер та затримку. Маючи сотні процесів, що обробляють TCP-з'єднання, ми були змушені постійно перемикатися між ними, а в мікросекундному масштабі це досить тривала операція. Але найгірше те, що коли процес отримував пакет на обробку, він відправляв його в одну чергу SystemV, а потім чекав на події з іншої черги SystemV. Однак при великій кількості вузлів надходження нового TCP-пакета в одному процесі та отримання даних у чергу в іншому являють собою для ОС дві конкуруючі події. У цьому випадку, якщо для обох завдань не буде доступних фізичних процесорів, одне оброблятиметься, а друге стане в чергу очікування. Передбачити наслідки неможливо.

У таких ситуаціях можна застосувати динамічне управління пріоритетом процесу, але це вимагатиме використання ресурсомістких системних викликів. У результаті ми перейшли на один потік із використанням класичного epoll, це сильно підвищило швидкість та зменшило тривалість обробки транзакції. Також ми позбулися окремих процесів мережевої взаємодії та взаємодії через SystemV, значно скоротили кількість системних викликів і почали контролювати пріоритети операцій. На одній лише підсистемі введення-виведення вдалося заощадити близько 8-17 мікросекунд залежно від сценарію. Ця однопотокова схема з тих пір застосовується в незмінному вигляді, одного epoll-потоку із запасом достатньо для обслуговування всіх підключень.

Обробка транзакцій

Зростання навантаження на нашу систему вимагало модернізації багатьох її компонентів. Але, на жаль, стагнація у зростанні тактової частоти процесорів останніми роками вже не дозволяла масштабувати процеси «в лоб». Тому ми вирішили розділити процес Engine на три рівні, причому найнавантаженішим з них є система перевірки ризиків, яка оцінює наявність коштів на рахунках і створює угоди. Але гроші можуть бути в різних валютах, і потрібно було вигадати, за яким принципом розділяти обробку запитів.

Логічне рішення — поділити валюти: один сервер торгує доларами, інший фунтами, третій євро. Але якщо за такої схеми відправити дві транзакції на купівлю різних валют, то виникне проблема розсинхронізації гаманців. А синхронізувати складно та накладно. Тому правильно буде окремо шардувати за гаманцями та окремо за інструментами. До слова, більшість західних бірж завдання перевірки ризиків не стоїть так гостро, як у нас, тому найчастіше це робиться в офлайні. Нам необхідно було реалізувати онлайн-перевірку.

Пояснимо на прикладі. Трейдер хоче купити 30 доларів, і запит йде на валідацію транзакції: ми перевіряємо, чи цей трейдер допущений до цього режиму торгів, чи має він потрібні права. Якщо все гаразд, запит іде у систему перевірки ризиків, тобто. на перевірку достатності коштів на укладання угоди. Там ставиться позначка, що необхідну суму на поточний момент заблоковано. Далі запит переадресовується до торгової системи, яка схвалює або не схвалює цю транзакцію. Припустимо, транзакцію схвалено — тоді система перевірки ризиків зазначає, що гроші розблоковані, і рублі перетворюються на долари.

Загалом система перевірки ризиків містить складні алгоритми і виконує великий обсяг дуже ресурсоємних обчислень, а не просто перевіряє «залишок на рахунку», як може здатися на перший погляд.

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

Еволюція архітектури торгово-клірингової системи Московської біржі. Частина 2

Після невеликої адаптації коду ми створили конвеєр паралельної обробки транзакцій, в якому транзакція розбивалася на 4 етапи конвеєра: мережева взаємодія, валідація, виконання та публікація результату

Еволюція архітектури торгово-клірингової системи Московської біржі. Частина 2

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

Так у нас виникла система ASTS+.

Щоправда, із конвеєрами теж не все так гладко. Припустимо, ми маємо транзакцію, яка впливає на масиви даних у сусідній транзакції, це характерна ситуація для біржі. Таку транзакцію не можна виконувати у конвеєрі, бо вона може вплинути на інші. Ця ситуація називається data hazard, і подібні транзакції просто обробляються окремо: коли «швидкі» транзакції, що знаходяться в черзі, закінчуються, конвеєр зупиняється, система обробляє «повільну» транзакцію, а потім знову запускає конвеєр. На щастя, частка таких транзакцій у загальному потоці дуже мала, тому конвеєр зупиняється так рідко, що не впливає на загальну продуктивність.

Еволюція архітектури торгово-клірингової системи Московської біржі. Частина 2

Потім ми вирішили проблему синхронізації трьох потоків виконання. В результаті народилася система на основі кільцевого буфера з осередками фіксованого розміру. У цій системі все підпорядковане швидкості обробки, дані не копіюються.

  • Усі вхідні мережеві пакети потрапляють на стадію аллокації.
  • Ми розміщуємо їх у масиві і помічаємо, що вони доступні стадії № 1.
  • Прийшла друга транзакція, вона знову доступна стадії № 1.
  • Перший потік обробки бачить доступні транзакції, обробляє їх та переводить на наступну стадію другого потоку обробки.
  • Потім він обробляє першу транзакцію і позначає відповідний осередок прапором deleted - Тепер вона доступна для нового використання.

Таким чином обробляється вся черга.

Еволюція архітектури торгово-клірингової системи Московської біржі. Частина 2

Обробка кожної стадії займає одиниці чи десятки мікросекунд. І якщо використовувати стандартні схеми синхронізації ОС, ми втратимо більше часу на самої синхронізації. Тому ми почали використовувати spinlock. Однак це дуже поганий тон в real-time системі, і RedHat суворо не рекомендує так робити, тому ми застосовуємо spinlock протягом 100 мс, а потім переходимо в режим семафорів, щоб унеможливити deadlock.

В результаті ми досягли продуктивності близько 8 млн. транзакцій в секунду. І буквально через два місяці в статті для LMAX Disruptor побачили опис схеми з такою ж функціональністю.

Еволюція архітектури торгово-клірингової системи Московської біржі. Частина 2

Тепер на одній стадії могло бути кілька потоків виконання. Усі транзакції оброблялися по черзі, у порядку надходження. У результаті пікова продуктивність зросла з 18 тис. до 50 тис. транзакцій на секунду.

Біржова система ризик-менеджменту

Немає межі досконалості, і незабаром ми знову зайнялися модернізацією: у рамках ASTS+ почали виносити системи ризик-менеджменту та розрахункових операцій на автономні компоненти. Розробили гнучку сучасну архітектуру та нову ієрархічну модель ризику, постаралися скрізь, де можливо використовувати клас fixed_point замість double.

Але відразу ж постало завдання: як синхронізувати всю бізнес-логіку, яка вже працює багато років, і перенести її в нову систему? У результаті першої версії прототипу нової системи довелося відмовитися. В основі другої версії, яка сьогодні працює в production, лежить той самий код, який працює і в торговій частині, і в ризиковій. У ході розробки найважче було зробити git merge між двома версіями. Наш колега Євген Мазуренок щотижня виконував цю операцію і щоразу дуже довго лаявся.

При виділенні нової системи одразу довелося вирішувати завдання взаємодії. При виборі шини даних необхідно забезпечити стабільний джиттер і мінімальну затримку. Для цього найкраще підійшла мережа InfiniBand RDMA: середня тривалість обробки у 4 рази менша, ніж у мережах 10 G Ethernet. Але по-справжньому нас підкупила різниця у перцентилях – 99 та 99,9.

Звісно, ​​у InfiniBand є свої складнощі. По-перше, інший API - ibverbs замість sockets. По-друге, майже немає широкодоступних open source messaging-рішень. Ми спробували зробити свій прототип, але це виявилося дуже непросто, тому обрали комерційне рішення Confinity Low Latency Messaging (раніше IBM MQ LLM).

Потім постало завдання правильного поділу ризикової системи. Якщо просто винести Risk Engine і не зробити проміжного вузла, транзакції з двох джерел можуть перемішуватися.

Еволюція архітектури торгово-клірингової системи Московської біржі. Частина 2

У так званих Ultra Low Latency рішення є режим reordering: транзакції від двох джерел можуть при надходженні вибудовуватися в потрібному порядку, це реалізується за допомогою окремого каналу обміну інформацією про черговість. Але ми поки що не застосовуємо цей режим: він ускладнює весь процес, а в низці рішень взагалі не підтримується. До того ж довелося б кожній транзакції надавати відповідні тимчасові мітки, а в нашій схемі цей механізм дуже важко реалізувати коректно. Тому ми використовували класичну схему з message broker, тобто з диспетчером, який розподіляє повідомлення між Risk Engine.

Друга проблема була пов'язана з клієнтським доступом: за наявності кількох Risk Gateway клієнту необхідно підключатися до кожного з них, і для цього доведеться вносити зміни до клієнтського шару. Ми хотіли уникнути цього на даному етапі, тому в поточній схемі Risk Gateway обробляють весь потік даних. Це сильно обмежує максимальну пропускну здатність, але дуже полегшує інтегрування системи.

дублювання

У нашій системі не повинно бути єдиної точки відмови, тобто всі компоненти необхідно продублювати, зокрема брокер повідомлень. Це завдання ми вирішили за допомогою системи CLLM: вона містить RCMS-кластер, в якому два диспетчери можуть працювати в режимі master-slave, і коли один виходить з ладу, система автоматично перемикається на інший.

Робота з резервним ЦОДом

InfiniBand оптимізований для роботи як локальна мережа, тобто для з'єднання стійкового обладнання, а між двома географічно розподіленими дата-центрами InfiniBand-мережа не прокласти. Тому ми реалізували bridge/dispatcher, який за звичайними мережами Ethernet підключається до сховища повідомлень і ретранслює всі транзакції в другу IB-мережу. Коли потрібна міграція із ЦОД, ми можемо вибирати, з яким дата-центром зараз працювати.

Підсумки

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

Оскільки система була сильно оновлена, ми реалізували відновлення даних із двох незалежних джерел. Якщо сховище повідомлень з якихось причин функціонує неправильно, можна взяти балку транзакцій з другого джерела — з Risk Engine. Цей принцип дотримується у межах всієї системи.

Крім іншого, нам вдалося зберегти клієнтський API, щоб ні брокери, ні будь-хто ще не зажадали значної ситуації під нову архітектуру. Довелося змінити якісь інтерфейси, але вносити в модель роботи значних змін не потрібно.

Поточну версію нашої платформи ми назвали Rebus як скорочення від двох найпомітніших нововведень в архітектурі, Risk Engine і BUS.

Еволюція архітектури торгово-клірингової системи Московської біржі. Частина 2

Спочатку ми хотіли виділити лише клірингову частину, але в результаті вийшла величезна розподілена система. Тепер клієнти можуть взаємодіяти або з торговим Gateway, або з клірингом, або з обома одночасно.

Чого ми в результаті досягли:

Еволюція архітектури торгово-клірингової системи Московської біржі. Частина 2

Зменшили рівень затримки. При невеликому обсязі транзакцій система працює так само, як і попередня версія, але при цьому витримує набагато більшу навантаження.

Пікова продуктивність зросла з 50 тис. до 180 тис. транзакцій на секунду. Подальшому підвищенню заважає єдиний потік зведення заявок.

Є два шляхи подальшого покращення: розпаралелювання matching та зміна схеми роботи з Gateway. Зараз усі Gateway працюють за реплікаційною схемою, яка за такого навантаження перестає нормально функціонувати.

Насамкінець можу дати кілька порад тим, хто допрацьовує ентерпрайз-системи:

  • Весь час будьте готові до гіршого. Проблеми завжди виникають зненацька.
  • Швидко переробити архітектуру, зазвичай, неможливо. Особливо, якщо потрібно досягти максимальної надійності за багатьма показниками. Чим більше вузлів, тим більше потребує ресурсів на підтримку.
  • Усі спеціальні та пропрієтарні рішення додатково вимагатимуть ресурси на дослідження, підтримку та супровід.
  • Не відкладайте вирішення питань надійності та відновлення системи після збоїв, враховуйте їх на початковому етапі проектування.

Джерело: habr.com

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