Спроектируй биржу
Движок матчинга: книга заявок, матчинг по приоритету цена-время, единый секвенсор для тотального порядка, низколатентный in-memory дизайн, долговечность через лог событий, детерминированный реплей и фан-аут рыночных данных участникам.
Два трейдера подают заявку на покупку той же акции по той же цене с разницей в микросекунду; осталась лишь одна лежащая заявка на продажу. Кто получит исполнение? На честной бирже ответ должен быть однозначным, идентичным на каждой реплике и воспроизводимым месяцы спустя в регуляторном аудите — потому что проигравший оспорит его, а «БД случайно закоммитила их в таком порядке» — не ответ, который примет регулятор. Вот удивительная правда о бирже: труднейшее требование — не пропускная способность и даже не сырая скорость, а детерминизм — каждая заявка должна обрабатываться в одной каноничной последовательности, и одни и те же входы обязаны всегда давать ровно те же сделки, на каждой машине, навсегда. Это единственное требование перекраивает всю архитектуру прочь от плейбука распределённых БД, что использовала остальная часть этого юнита.
Требования
Биржа сводит заявки на покупку и продажу инструментов и публикует результаты. Ограничения необычны и переворачивают обычные инстинкты масштабирования. Почему биржа выглядит так непохоже на всё остальное в этом юните? Потому что «кто торговал первым» — это юридический вопрос, а не только инженерный.
Функциональные. Принимать заявки (инструмент, сторона, цена, количество, тип — лимитная/рыночная). Поддерживать книгу заявок на инструмент: лежащие заявки на покупку (биды) и продажу (аски). Сводить входящие заявки против книги по честному правилу. Исполнять сделки, обновлять позиции и публиковать рыночные данные (книгу и ленту сделок) всем участникам. Поддерживать отмены и поправки.
Нефункциональные. Честность и детерминизм превыше всего: единый каноничный порядок событий, идентичные результаты на каждой реплике, воспроизводимость для аудита годы спустя. Ультранизкая задержка: матчинг за микросекунды, потому что участники конкурируют на скорости. Долговечность: ни одна подтверждённая заявка не должна теряться, даже через крэш. Высокая пропускная способность во всплесках (миллионы сообщений/с на открытии). Честность — ограничение, доминирующее в дизайне — оно вынуждает единую последовательность, что вынуждает однопоточное ядро, что переворачивает всё.
Определяющий разворот: остальная часть юнита боролась распределить состояние ради масштаба. Биржа делает наоборот — она намеренно сливает всё через один секвенсор и один поток матчинга, потому что лишь единый тотальный порядок может быть честным и детерминированным. Скорость приходит из того, чтобы сделать этот единый путь абсурдно быстрым, а не из параллелизма.
Оценка
инструментов = ~10 000 символов
пик сообщений = ~1 000 000–10 000 000 заявок+отмен/с (всплеск открытия)
задержка матчинга = цель < 10 микросекунд на заявку (in-memory)
книга заявок = тысячи ценовых уровней × заявок на уровень, всё в RAM
лог событий = каждое сообщение, append, fsync, реплика → долговечность + аудит
фан-аут = рыночные данные тысячам подписчиков, multicastЧисла объясняют архитектуру. Весь рабочий набор — книги заявок всех инструментов — влезает в RAM (это миллионы маленьких записей, не терабайты), так что движок матчинга — in-memory программа; касание диска на горячем пути взорвало бы микросекундный бюджет на порядки. Пик всплесковый и жестокий (открытие), так что путь должен быть худым. И каждое сообщение долговечно логируется и для восстановления после крэша, и для аудита — но запись лога устроена так, что никогда не стопорит матч (ниже подробнее). Поэтому биржа не похожа ни на что вроде шардированного веб-бэкенда: это быстрое однопоточное ядро, питаемое секвенсором и подпираемое логом.
Высокоуровневый дизайн
Паттерн (архитектура LMAX — знаменитый публичный пример): секвенсор назначает каждому сообщению глобальный номер последовательности, определяя единый истинный порядок; этот упорядоченный поток пишется в долговечный, реплицируемый лог событий; однопоточный движок матчинга потребляет поток по порядку против in-memory книги заявок и детерминированно производит сделки; результаты веером раздаются как рыночные данные. Восстановление и реплики тривиальны из-за детерминизма — переиграй лог с начала (или со снимка), и достигнешь ровно той же книги.
Глубокое погружение
Приоритет цена-время и книга заявок
Правило матчинга должно быть честным и тотальным, не оставляя двусмысленности о том, кто исполнится первым. Стандарт — приоритет цена-время (price-time priority): заявки ранжируются сперва по цене (покупатель, предлагающий больше, или продавец, просящий меньше, имеет приоритет), а среди заявок по той же цене — по времени (первым пришёл, первым исполнился — FIFO). Это в точности отвечает вступлению: две покупки по той же цене упорядочены своими номерами последовательности, и более ранняя исполняется.
Книга заявок — структура данных, делающая это быстрым: для каждого инструмента биды и аски держатся отсортированными по цене (часто как отображение от ценового уровня к FIFO-очереди заявок на этом уровне). Матчинг входящей заявки идёт по противоположной стороне от лучшей цены внутрь, исполняясь против лежащих заявок, пока входящая не исчерпана или цена не пересекается:
АСКИ (продавцы) БИДЫ (покупатели)
101.00 × 50 100.50 × 30 ← лучший бид
100.75 × 20 100.25 × 40
100.00 × 100
входящая: КУПИТЬ 60 @ рынок
→ исполнить 20 @ 100.75, затем 40 @ 101.00 (идём вверх по аскам) ⇒ две сделки, книга обновленаКаждый уровень FIFO, так что приоритет времени держится; лучшая цена O(1) на поиск, а матчинг O(затронутых заявок). Книга — чистое in-memory состояние, мутируемое единым потоком матчинга, поэтому она может работать за микросекунды.
▸Почему это работает
Почему один поток для ядра матчинга, когда каждая другая система в этом юните тянулась к конкурентности, чтобы стать быстрее? Потому что честность требует единого тотального порядка событий, и в момент, когда два потока могут сводить против той же книги конкурентно, у тебя гонка: их относительный порядок становится недетерминированным, две реплики могут разойтись, а «кто исполнился первым» зависит от планирования блокировок — разрушая и честность, и воспроизводимость. Один поток, обрабатывающий секвенсированный поток, не имеет никакой конкурентности для рассуждения: события применяются по одному в порядке номеров последовательности, одинаково на каждой машине и каждом реплее. Контринтуитивная выгода в том, что один поток ещё и быстрее для этой нагрузки — нет блокировок, нет конкуренции за кэш-линии, нет оверхеда координации, а вся книга в L2/L3 кэше. Ты не параллелишь матч; ты делаешь один поток ослепительно быстрым и параллелишь всё вокруг него (шлюзы, фан-аут рыночных данных, риск-чеки).
Детерминизм и долговечность через лог событий
Детерминизм — это свойство, что одна и та же последовательность входных сообщений всегда даёт те же сделки и ту же финальную книгу — никаких чтений настенных часов, никаких случайных чисел, никакой зависимости от планирования потоков внутри матча. Это и делает лог событий таким мощным: поскольку движок детерминирован, лог секвенсированных входных сообщений и есть система. Не надо логировать получившееся состояние; логируешь входы, и любая реплика, переигрывая их, перестраивает идентичную книгу.
Это даёт долговечность и восстановление дёшево: добавляй каждое секвенсированное сообщение в лог, что fsync’нут и реплицирован до (или одновременно с) матчингом, так что крэш не теряет ничего подтверждённого; при перезапуске переиграй лог (с периодического снимка, чтобы ограничить время реплея) для реконструкции точного in-memory состояния. Репликация — это и механизм failover — горячие реплики потребляют тот же лог и стоят готовыми принять на том же номере последовательности. Аудит — та же машинерия: неизменяемый, упорядоченный лог — идеальная запись, и вопрос регулятора («почему случилась эта сделка?») отвечается реплеем до того номера последовательности.
▸Частая ошибка
Тонкая, фатальная ошибка — протащить недетерминизм в ядро матчинга — и это легко сделать, не заметив. Чтение системных часов для метки времени сделки внутри матча, генерация ID из случайного источника, итерация хэш-мапы, чей порядок не стабилен, или ветвление на чём-то, что отличается между машинами — всё ломает инвариант, что одни входы дают одни выходы. В тот момент, как ядро недетерминировано, реплики расходятся, реплей больше не реконструирует реальное состояние, а аудит-трейл становится ложью. Дисциплина: поток матчинга потребляет лишь секвенсированные входы и чистые функции от них; всё, выводимое из времени или случайности, должно быть захвачено как вход (секвенсор штампует время и назначает ID, так что они часть логируемого сообщения), а не сгенерировано внутри детерминированного ядра. Детерминизм здесь не «приятно иметь»; это несущее свойство, на котором держится вся история долговечности/восстановления/аудита.
Фан-аут рыночных данных
Как только движок свёл, результаты — сделки и инкрементальные обновления книги — должны дойти до каждого участника честно и быстро. Это широковещание с высоким фан-аутом (тысячи подписчиков, каждый хочет ленту с минимальной задержкой). Биржи обычно используют multicast (групповую рассылку — один пакет, много получателей): движок публикует каждое обновление один раз, а сеть дублирует его всем подписчикам, так что цена фан-аута не растёт с числом подписчиков и все получают его почти в один миг (снова честность — нельзя дать одному подписчику фору). Лента сама — секвенсированный поток (протоколы gap-fill дают подписчику, пропустившему пакет, запросить реплей), и многие биржи публикуют ярусные ленты (полная глубина книги против вершины книги) для управления полосой. Фан-аут параллелится и держится прочь от потока матчинга — движок выдаёт, отдельный путь публикации распределяет.
Узкие места и компромиссы
Определяющая позиция — детерминизм и честность важнее параллелизма: ядро матчинга однопоточно намеренно, так что его потолок пропускной способности — один быстрый поток — масштабируешь, делая этот поток быстрее (книга в кэше, без аллокаций на горячем пути, mechanical sympathy) и партиционируя по инструментам (независимые книги могут работать на отдельных движках), но никогда параллеля одну книгу. Долговечность против задержки — центральное напряжение: запись лога должна случиться до того, как подтвердишь заявку, но fsync медленен относительно микросекундного матча — решается батчингом записей лога, записью в быстрое хранилище и перекрытием репликации, чтобы долговечная запись не сериализовалась за каждым матчем. In-memory значит ограничено RAM и временем восстановления: книга влезает в память, но реплей-из-лога должен быть ограничен периодическими снимками, иначе перезапуск слишком долог. И честность фан-аута ограничивает сеть: multicast держит его равным и масштабируемым, но требует протоколов gap-recovery и аккуратной ёмкости, чтобы ни один подписчик не был систематически в выгоде. Всюду биржа принимает однопоточный потолок и сложную долговечную обвязку как цену единственного, чем нельзя поступиться — честного, детерминированного, аудитируемого порядка сделок.
Две заявки на покупку той же акции по той же цене приходят с разницей в микросекунду, и осталась лишь одна лежащая продажа. Что решает, кто исполнится, и почему ядро должно быть однопоточным, чтобы это соблюсти?
Инженер ставит метку времени каждой сделки чтением системных часов внутри цикла матчинга и генерирует ID сделок из случайного источника. Почему это серьёзный баг на бирже?
Поскольку движок матчинга _______, одна и та же последовательность входных сообщений всегда производит те же сделки и ту же финальную книгу — поэтому одного лога событий-входов достаточно, чтобы восстановить состояние, гонять горячие реплики и ответить аудитору годы спустя.
- 01Почему биржа использует единый секвенсор и однопоточное ядро матчинга, а не распределяет ради масштаба?
- 02Объясни приоритет цена-время и структуру книги заявок.
- 03Как детерминизм и лог событий дают долговечность, восстановление и аудит?
Биржа переворачивает инстинкт распределяй-ради-масштаба остальной части этого юнита, потому что её труднейшее требование — честность и детерминизм: каждая заявка должна обрабатываться в одной каноничной последовательности, и идентичные входы обязаны всегда давать идентичные сделки — на каждой реплике и в аудите годы спустя. Поэтому всё сливается через единый секвенсор, что налагает глобальный порядок и персистит его в долговечный, реплицируемый лог событий, питая один однопоточный движок матчинга, что гоняет in-memory книгу заявок по приоритету цена-время (лучшая цена первой, FIFO при равной цене — ничья из вступления решена номером последовательности). Один поток — не ограничение, за которое надо извиняться; это единственный способ получить честный тотальный порядок, и он быстрее для этой нагрузки (нет блокировок, книга в кэше). Поскольку ядро детерминировано, поток логируемых входов — источник истины: реплей перестраивает точную книгу, горячие реплики стоят готовыми на том же номере последовательности, а любая сделка воспроизводима для аудита — поэтому протащить чтение часов или случайность в матч — фатальный баг. Рыночные данные веером раздаются multicast’ом прочь с горячего пути, чтобы все участники получали их честно и на масштабе. Компромиссы все вытекают из единственного неоспоримого: однопоточный потолок (масштабируй поток и партиционируй по инструментам, никогда не параллель одну книгу), долговечность-против-задержки в записи лога (батчь и перекрывай репликацию), ограниченное восстановление через снимки и честность multicast с gap-recovery — сложная машинерия, что биржа принимает ради гарантии честного, детерминированного, аудитируемого порядка сделок. Теперь, когда встретишь проектное решение, тянущееся к нескольким потокам матчинга или распределённой блокировке «ради масштаба», ты сразу задашь правильный вопрос: как две реплики договорятся о том, кто исполнился первым?
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.