Статьи

Простое двоичное кодирование

Финансовые системы взаимодействуют, отправляя и получая огромное количество сообщений в разных форматах. Когда люди используют такие термины, как «огромный», я обычно думаю: «на самом деле … сколько?» Так что давайте количественно определить «обширный» для финансовой индустрии. Рыночные потоки данных с финансовых бирж, как правило, могут отправлять десятки или сотни тысяч сообщений в секунду, а совокупные потоки, такие как OPRA, могут просматривать более 10 миллионов сообщений в секунду, причем объемы растут из года в год. Эта презентация дает хороший обзор .

В этом сумасшедшем мире мы все еще видим значительное использование презентаций в кодировке ASCII, таких как значение тега FIX , и некоторых более вменяемых презентаций в двоичном коде, таких как FAST . Некоторые рынки даже совершают грех отправки рыночных данных в виде XML! Ну, я не могу жаловаться слишком много, потому что они иногда приносили мне хороший доход, создавая ультрабыстрые парсеры XML.

В прошлом году CME, являющийся членом сообщества FIX, поручил Тодду Монтгомери , известному 29West LBM, и мне самим создать эталонную реализацию нового стандарта FIX Simple Binary Encoding (SBE). SBE — это кодек, предназначенный для решения вопросов эффективности в торговле с малой задержкой, с особым акцентом на рыночные данные. CME, работающий в сообществе FIX, проделал большую работу, придумав презентацию кодирования, которая может быть настолько эффективной. Возможно, подходящее искупление грехов прошлых реализаций значений тегов FIX. Мы с Тоддом работали над реализацией Java и C ++, а позже на стороне .Net нам помог удивительный Оливье Дехерлз из Adaptive . Работа над классной технической проблемой с такой командой — работа мечты.

SBE Обзор

SBE — это представление уровня 6 OSI для сообщений кодирования / декодирования в двоичном формате для поддержки приложений с малой задержкой. Из многих приложений, которые я профилирую с проблемами производительности, кодирование / декодирование сообщений часто является наиболее значительной стоимостью. Я видел много приложений, которые тратят значительно больше процессорного времени на анализ и преобразование XML и JSON, чем на выполнение бизнес-логики. SBE разработан для того, чтобы сделать эту часть системы максимально эффективной. SBE придерживается ряда принципов проектирования для достижения этой цели. Соблюдение этих принципов проектирования иногда означает, что функции, доступные в других кодеках, не будут предлагаться. Например, многие кодеки позволяют кодировать строки в любой позиции поля в сообщении; SBE допускает только поля переменной длины, такие как строки, как поля, сгруппированные в конце сообщения.

Ссылочная реализация SBE состоит из компилятора, который принимает схему сообщения в качестве входных данных и затем генерирует специфичные для языка заглушки. Заглушки используются для прямого кодирования и декодирования сообщений из буферов. Инструмент SBE также может генерировать двоичное представление схемы, которое можно использовать для оперативного декодирования сообщений в динамической среде, например для средства просмотра журнала или сетевого анализатора.

Принципы разработки определяют реализацию кодека, обеспечивающего потоковую передачу сообщений через память без обратного отслеживания, копирования или ненужного выделения. Шаблоны доступа к памяти не следует недооценивать при разработке высокопроизводительного приложения. В системах с малой задержкой на любом языке особенно необходимо учитывать все распределение, чтобы избежать возникающих проблем при утилизации. Это относится как к управляемой среде выполнения, так и к родным языкам. SBE полностью бесплатен для всех трех языковых реализаций.

Конечным результатом применения этих принципов разработки является кодек с пропускной способностью в 25 раз большей, чем у буферов протокола Google (GPB), с очень низкой и предсказуемой задержкой. Это наблюдается в микро-тестах и в реальных приложениях. Типичное сообщение с рыночными данными может быть закодировано или декодировано за ~ 25 нс по сравнению с ~ 1000 нс для того же сообщения с GPB на том же оборудовании. Сообщения значений тегов XML и FIX снова на несколько порядков медленнее.

Лучшее место для SBE — это кодек для структурированных данных, в основном это поля фиксированного размера, которые представляют собой числа, наборы битов, перечисления и массивы. Хотя он работает для строк и больших двоичных объектов, многие из них считают некоторые ограничения проблемой юзабилити. Этим пользователям лучше использовать другой кодек, более подходящий для строкового кодирования.

Структура сообщения

Сообщение должно быть способно считываться или записываться последовательно, чтобы сохранить принцип проектирования потокового доступа, то есть без необходимости возврата. Некоторые кодеки вставляют указатели местоположения для полей переменной длины, таких как строковые типы, которые должны быть перенаправлены для доступа. Это косвенное обращение происходит за счет дополнительных инструкций и потери поддержки аппаратных средств предварительной выборки. Конструкция SBE обеспечивает чистый последовательный доступ и семантику нативного доступа без копирования.

SBE-сообщ-формат
Рисунок 1

Сообщения SBE имеют общий заголовок, который определяет тип и версию тела сообщения, которому необходимо следовать. За заголовком следуют корневые поля сообщения, которые имеют фиксированную длину со статическими смещениями. Корневые поля очень похожи на структуру в C. Если сообщение более сложное, то может следовать одна или несколько повторяющихся групп, похожих на корневой блок. Повторяющиеся группы могут вкладывать другие повторяющиеся групповые структуры. Наконец, строки переменной длины и BLOB-объекты идут в конце сообщения. Поля также могут быть необязательными. Схему XML, описывающую представление SBE, можно найти здесь .

SbeTool и компилятор

Для использования SBE необходимо сначала определить схему для ваших сообщений. SBE предоставляет независимую от языка систему типов, поддерживающую целые числа, числа с плавающей запятой, символы, массивы, константы, перечисления, наборы битов, композиты, повторяющиеся сгруппированные структуры, а также строки и BLOB-объекты переменной длины.

Схема сообщения может быть введена в SbeTool и скомпилирована для создания заглушек на разных языках или для генерации двоичных метаданных, подходящих для декодирования сообщений на лету.

1
java [-Doption=value] -jar sbe.jar <message-declarations-file.xml>

SbeTool и компилятор написаны на Java. В настоящее время инструмент может выводить заглушки на Java, C ++ и C #.

Программирование с заглушками

Полный пример сообщений, определенных в схеме со вспомогательным кодом, можно найти здесь . Сгенерированные заглушки следуют шаблону упрощенной формы с повторным использованием экземпляров, чтобы избежать выделения Заглушки обертывают буфер со смещением, а затем читают его последовательно и изначально.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
// Write the message header first
    MESSAGE_HEADER.wrap(directBuffer, bufferOffset, messageTemplateVersion)
                  .blockLength(CAR.sbeBlockLength())
                  .templateId(CAR.sbeTemplateId())
                  .schemaId(CAR.sbeSchemaId())
                  .version(CAR.sbeSchemaVersion());
 
    // Then write the body of the message
    car.wrapForEncode(directBuffer, bufferOffset)
       .serialNumber(1234)
       .modelYear(2013)
       .available(BooleanType.TRUE)
       .code(Model.A)
       .putVehicleCode(VEHICLE_CODE, srcOffset);

Сообщения могут быть написаны через сгенерированные заглушки свободно. Каждое поле отображается как сгенерированная пара методов для кодирования и декодирования.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
// Read the header and lookup the appropriate template to decode
    MESSAGE_HEADER.wrap(directBuffer, bufferOffset, messageTemplateVersion);
 
    final int templateId = MESSAGE_HEADER.templateId();
    final int actingBlockLength = MESSAGE_HEADER.blockLength();
    final int schemaId = MESSAGE_HEADER.schemaId();
    final int actingVersion = MESSAGE_HEADER.version();
 
    // Once the template is located then the fields can be decoded.
    car.wrapForDecode(directBuffer, bufferOffset, actingBlockLength, actingVersion);
 
    final StringBuilder sb = new StringBuilder();
    sb.append("\ncar.templateId=").append(car.sbeTemplateId());
    sb.append("\ncar.schemaId=").append(schemaId);
    sb.append("\ncar.schemaVersion=").append(car.sbeSchemaVersion());
    sb.append("\ncar.serialNumber=").append(car.serialNumber());
    sb.append("\ncar.modelYear=").append(car.modelYear());
    sb.append("\ncar.available=").append(car.available());
    sb.append("\ncar.code=").append(car.code());

Сгенерированный код на всех языках дает производительность, аналогичную приведению структуры C к памяти.

Декодирование на лету

Компилятор создает промежуточное представление (IR) для схемы входного сообщения XML. Этот IR может быть сериализован в двоичном формате SBE для последующего оперативного декодирования сохраненных сообщений. Это также полезно для таких инструментов, как сетевой анализатор, которые не будут скомпилированы с заглушками. Полный пример использования ИК можно найти здесь .

Прямые буферы

SBE предоставляет абстракцию для Java через класс DirectBuffer для работы с буферами, которые являются байтами [], кучей или прямыми буферами ByteBuffer , и адресами памяти кучи, возвращенными из Unsafe.allocateMemory (long) или JNI. В приложениях с низкой задержкой сообщения часто кодируются / декодируются в отображенных в память файлах через MappedByteBuffer и, таким образом, могут передаваться ядром в сетевой канал, таким образом избегая копий пространства пользователя.

C ++ и C # имеют встроенную поддержку прямого доступа к памяти и не требуют такой абстракции, как в версии Java. Абстракция DirectBuffer была добавлена ​​для C # для поддержки Endianess и инкапсулирования небезопасного доступа к указателю.

Расширение сообщения и управление версиями

Схемы SBE содержат номер версии, который допускает расширение сообщения. Сообщение может быть расширено путем добавления полей в конце блока. Поля не могут быть удалены или переупорядочены для обратной совместимости.

Поля расширения должны быть необязательными, иначе новый шаблон, читающий старое сообщение, не будет работать. Шаблоны содержат метаданные для min, max, null, timeunit, кодировки символов и т. Д., Они доступны через статические (на уровне класса) методы на заглушках.

Порядок байтов и выравнивание

Схема сообщения позволяет точно выровнять поля, указав смещения. Поля по умолчанию закодированы в формате Little Endian, если в схеме не указано иное. Для максимальной производительности следует использовать встроенную кодировку с полями на границах, выровненных по словам. Наказание за доступ к не выровненным полям на некоторых процессорах может быть очень значительным. Для выравнивания необходимо учитывать протокол кадрирования и расположение буфера в памяти.

Протоколы сообщений

Я часто вижу, что люди жалуются, что кодек не может поддерживать конкретную презентацию в одном сообщении. Однако это часто можно решить с помощью протокола сообщений. Протоколы являются отличным способом разделить взаимодействие на его составные части, и тогда эти части часто являются составными для многих взаимодействий между системами. Например, реализация IR метаданных схемы является более сложной, чем может поддерживаться структурой одного сообщения. Мы кодируем IR, сначала отправляя шаблонное сообщение с обзором, а затем поток сообщений, каждый из которых кодирует токены из IR компилятора. Это позволяет создать очень быстрый OTF-декодер, который может быть реализован в виде резьбового прерывателя с гораздо меньшим разветвлением, чем типичные конечные автоматы на основе коммутатора.

Проектирование протоколов — это область, которую большинство разработчиков, похоже, не получают возможности учиться. Я чувствую, что это большая потеря. Тот факт, что многие разработчики будут называть «кодировку», такую ​​как ASCII, «протоколом», очень показателен. Ценность протоколов настолько очевидна, когда человек начинает работать с таким программистом, как Тодд, который провел свою жизнь, успешно разрабатывая протоколы.

Производительность заглушки

Заглушки обеспечивают значительное преимущество в производительности по сравнению с динамическим декодированием OTF. Для доступа к примитивным полям мы считаем, что производительность достигает пределов того, что возможно с помощью универсального инструмента. Сгенерированный ассемблерный код очень похож на то, что сгенерирует компилятор для доступа к структуре C, даже из Java!

Что касается общей производительности заглушек, мы заметили, что C ++ имеет очень незначительное преимущество по сравнению с Java, которое, по нашему мнению, связано с проверками Safepoint, вставленными во время выполнения. Версия C # немного отстает из-за того, что ее среда выполнения не так агрессивна с методами встраивания, как среда выполнения Java. Заглушки для всех трех языков способны кодировать или декодировать типичные финансовые сообщения за десятки наносекунд. Это эффективно делает кодирование и декодирование сообщений практически бесплатным для большинства приложений по сравнению с остальной логикой приложения.

Обратная связь

Это первая версия SBE, и мы будем рады получить отзывы . Эталонная реализация ограничена спецификацией сообщества FIX. Можно повлиять на спецификацию, но, пожалуйста, не ожидайте, что будут приняты запросы на извлечение, которые значительно противоречат спецификации . Поддержка Javascript, Python, Erlang и других языков обсуждалась и будет приветствоваться.

Ссылка: Простое двоичное кодирование от нашего партнера JCG Мартина Томпсона в блоге Mechanical Sympathy .