Історія розробки TWIME — нового високошвидкісного інтерфейсу Московської Біржі

В цьому хабі ми розповімо вам про свій унікальний досвід розробки високошвидкісного інтерфейсу TWIME для Московської біржі, пояснимо, чому нам так важлива низька latency (час відгуку) і як її скоротити. Сподіваємося, у висновку вам стане трохи зрозуміліше, чому Московська біржа більш технологічна в деяких областях, ніж, приміром, такі гіганти High Load як Nginx, VK або MailRu.

Щоб пояснити, що таке високошвидкісний інтерфейс TWIME, доведеться почати здалеку. Те, чим торгують на біржі називається торговим інструментом — у нього є ціна і його можна купити або продати. Торговим інструментом може бути, наприклад, барель нафти, акція Ощадбанку або пари валют. Строковий ринок — це сегмент Московської біржі, на якому торгують похідними інструментами (деривативами) – ф'ючерсами та опціонами.

Основна функція біржі —приймати заявки на купівлю/продаж торгівельних інструментів, зводити заявки в угоду за суворими правилами і видавати інформацію про скоєних угодах.
Відповідно, у бірж буває кілька типів інтерфейсів. Торгові інтерфейси дозволяють здійснювати операції й отримувати оперативну інформацію про здійснені угоди. Саме торгові інтерфейси біржі найбільш критичні до часу відгуку. Реалізує протокол TWIME шлюз — це новий, самий швидкий, торговий інтерфейс до термінового ринку.

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

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

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

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

Навіщо біржі потрібна низька latency?

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

З часом відгуку все складніше. Біржі потрібно рівне, без сплесків, час відгуку. Так як інакше порушується принцип «Fair Play». Якщо у одного користувача заявка выставилась за Х мкс, а в іншого за 10Х мкс, і при цьому затримка сталася саме на боці біржі, то з'являється загроза упущеної прибутку, що сильно засмучує трейдера. Неможливо зробити час відгуку строго однаковим у всіх, звести дисперсію latency до 0, однак завжди слід прагнути до такого показника.

Важлива медіана часу відгуку для біржі? Для клієнтів медіана важлива тільки якщо вона не відрізняється від медіани інших клієнтів. В іншому випадку вона не суттєва. Один з останніх трендів у биржестроении — це коли біржа уповільнює сама себе. Почався такий тренд з відомої книги Майкла Люіса «Flash Boys» і продовжився влітку 2016го, коли SEC (Securities and Exchange Commission) прийняла рішення зробити одну з таких уповільнених бірж публічною.

image

Важлива медіана часу відгуку для біржі? Це питання залишається відкритим. Як правило, доводиться шукати компроміс між пропускною здатністю, медіаною і дисперсією latency. Наприклад, відключення алгоритму Нейгла зменшує медіану, але і зменшує пропускну здатність. Таких прикладів можна навести багато.

Які торгові інтерфейси є термінового ринку? Про це ми писали в нашому блозі раніше, тому повторимо лише в загальних рисах.

Раніше самим швидким інтерфейсом був CGate API – набір бібліотек з єдиним API. CGate спілкується з біржею через закритий внутрішній протокол біржі. Хоча цей інтерфейс і самий швидкий, але, так як його протокол закритий і потрібно лінковка з бібліотекою, то виникають природні обмеження по підтримуваних мов і платформ, і немає можливості використовувати FPGA, чого хотіли б багато наші клієнти.

Ще один торговий інтерфейс — FIX. Він досить зручний для клієнтів, так як протокол FIX — це старий, перевірений часом стандарт для виставлення заявок. Під нього створено величезну кількість бібліотек і FPGA-рішень. На жаль, на терміновому ринку цей інтерфейс дещо повільніше CGate.

І більшість клієнтів природно воліли CGate. Тепер ми розробили новий інтерфейс, він швидше CGate, не вимагає лінкування, годиться для FPGA, при цьому в його розробці використані самі передові галузеві стандарти – і ім'я йому TWIME (Trading Wire Interface for Moscow Exchange).

Як нам вдалося зменшити час відгуку?

Ми провели копіткий аналіз наявних біржових інтерфейсів. Робота тривала все літо 2015 року. До осені у нас був готовий прототип, який показував latency в 10 мкс у бік ядра. Це було на порядок швидше найшвидшого на той момент інтерфейсу. До грудня ми запустили паблік тест, а у квітні почалася повноцінна експлуатація.

При розробці найбільш швидкого інтерфейсу ми зосередилися на трьох аспектах:

  • Архітектура системи, концептуальні зміни в інтерфейсах.
  • Новий протокол, який взяв найкраще від попередників.
  • Алгоритми – приватні поліпшення в реалізації логіки шлюзу.
image

Ми зрізали кути в архітектурі. Найбільший кут — це загальний шлюз заявок, через який раніше проходили всі повідомлення в ядро, в тому числі і нечутливі до часу відгуку, так звані неторгові накази. Цей шлюз виконує функції моніторингу, аутентифікації і flood-контролю. У підсумку ми реалізували потрібну функціональність всередині нашого шлюзу TWIME, а зайву викинули. Таким чином вийшло уникнути зайвого хопу в мережі, так як зазвичай загальний шлюз і ядро розташовувалися на різних боксах.

Ми забороняємо підключатися до біржі через шлюз TWIME (надсилати TCP SYN) частіше, ніж раз в секунду з однієї адреси. Це дозволило скоротити збитки від неправильно написаних клієнтських додатків і спроб зловмисного шкоди.

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

При розробці нового торгового протоколу ми використовували рекомендації Fix Community, спільноти організацій, зацікавлених в уніфікації доступу до різних торговельних майданчиків.

За основу нашого сесійного рівня ми взяли FIXP, на презентаційному рівні ми використовуємо SBE, а рівень додатків з одного боку для ефективності максимально наближений до внутрішнього формату повідомлень ядра торгової системи, а з іншого боку використовує семантику FIX. В результаті вийшов дуже простий для реалізації клієнтів протокол. Ми отримали позитивні відгуки від виробників рішень для торгівлі на основі FPGA.

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

Хто ні будь використовував Google ProtoBuf? На деяких тестах SBE показує прискорення в десятки разів порівняно з ГПБ. SBE — це як структури мови, які відправляються в мережу «як є», шляхом передачі в системний виклик send покажчика на структуру і sizeof структури. Відповідно, сериализация і десериализация є просто З-style cast під час компіляції. Насправді все трохи складніше, але достоїнства і недоліки такого підходу, думаю, зрозумілі.

Кожне повідомлення у TWIME має фіксований розмір. Перше поле в кожному повідомленні — розмір повідомлення без заголовку, а друге — ідентифікатор типу повідомлення – структури. За типом повідомлення можна однозначно отримати довжину, але це був би зайвий switch, що довго. Ще два поля в заголовку — це ідентифікатор схеми і версія схеми. Це все стандартний заголовок SBE. Далі йдуть поля бізнес логіки: ціна, інструмент, клієнтський рахунок.

Протокол більш ефективний, ніж FIX, тому що у нас використовується бінарне кодування. Наприклад, у FIX час з точністю до мілісекунд займає 19 байт, в TWIME час з точністю до наносекунди займає 8 байт. Це кількість наносекунд від юниксовой епохи.

Так само він більш ефективний, ніж внутрішній протокол біржі (Plaza2), розмір повідомлень у нас менше в середньому в 4 рази за рахунок відсутності внутрішніх полів ядра.

image

Самий головний секрет низькою latency полягає у відсутності блокувань на критичному шляху.
М'ютекси не можуть використовуватися в low-latency додатках на критичному шляху, так як час одного слайсу часу (мінімальний час виділяється ОС програмами) планувальника ОС зазвичай більше час обробки заявки. Виділення пам'яті не може проводитися на критичному шляху так як там використовується м'ютекс.

Кількість контекст світчів необхідно зводити до мінімуму за допомогою affinity і realtime scheduling. В ідеалі – краще взагалі не мати context-switch для робочих потоків.

Комусь це може здатися банальністю, але відсутність м'ютексів і виділень пам'яті на критичному шляху, а також зведення перемикань контексту до мінімуму — необхідна умова успіху low-latency програми.

image

Одна з оптимізацій, яку ми зробили, що стосується роботи з heartbeat. Значна частина наших клієнтів постійно протягом дня надсилають і отримують повідомлення, в такому випадку відразу видно, що клієнт активний і heartbeat не потрібен. Ми вимагаємо, щоб клієнт надсилав heartbeat тільки в відсутність інших повідомлень.

Розрив сесії пропущеного heartbeat дуже важливий для клієнтів біржі. Так як є можливість заздалегідь попросити біржу скасувати всі заявки у разі розриву з'єднання.

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

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

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

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

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

У новому шлюзі ми пішли по іншому шляху, ми читаємо з сокетных буферів тільки ті повідомлення, які можемо обробити. У разі великого навантаження на шлюз заб'ється TCP буфера на стороні сервера і клієнтам перестануть приходити TCP ACK повідомлення. Таким чином клієнт при забитих буферах і асинхронних сокетах отримає помилку EAGAIN і зможе сам прийняти рішення про те чи варто в ситуації надмірного навантаження на біржу продовжувати торгувати як раніше або змінити стратегію.

image

Один з найцікавіших алгоритмів в нашому шлюзі — алгоритм пріоритетного розміру пакета.

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

Рішення проблеми, яке використовувалося раніше передбачало відстеження кількості повідомлень від кожного клієнта і розподіл часу шлюзу між клієнтами. У новому гейта ми знайшли більш просте рішення. На кожній ітерації полінга сокетів, ми читаємо не більше, ніж заздалегідь встановлене N-ое кількість байт. Таким чином, якщо один з клієнтів надіслав 100 повідомлень в одному TCP сегменті, другий надіслав 15 повідомлень, третій 5 повідомлень, а пріоритетний розмір пакета коштує в 10 повідомлень, то на першій ітерації повністю будуть оброблені тільки повідомлення від третього клієнта. Повідомлення від першого клієнта займуть 10 ітерації полінга реактора, а другого дві ітерації.

Ще одна оптимізація яку ми зробили в новому шлюзі — використання статичної схеми замість динамічної як було раніше.

Схема — це набір повідомлень і полів в них. У разі, коли поля повідомлень не відомі під час компіляції — це динамічна схема, а якщо поля відомі на етапі компіляції, то отримуємо вже статичну схему. Обидва підходи мають свої позитивні і негативні сторони. У першому випадку один і той же код може працювати з різними схемами; в другому — зміна схеми вимагає перекомпиляцию коду. Статична схема дозволяє компілятору зробити безліч додаткових оптимізацій, наприклад, при запису повідомлень в лог. Код запису в лог повідомлень з статичної схеми буде, швидше за все, заинлайнен компілятором, за рахунок того, що кількість і типи полів відомі ще на етапі компіляції.

Матеріал підготовлений Миколою Висковым — інженером Московської Біржі.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.