RTP DesportoMiguel Oliveira abandona segunda corrida de Superbikes em Cremona após quedaESPN DeportesCruz Azul rescata el empate de último minuto ante TolucaPunchNollywood actor Rykardo Agbor survives car crashDaily MaverickDOWN TO EARTH: Eleven-man Wallabies resist Springboks’ rearguard to secure famous Perth winESPNWhat we know through CFB's Week 4, plus Kamario Taylor's rise and Michigan's karmaInquirer18 dengue deaths reported in Camarines SurThe Jerusalem PostOver one-quarter of Muslim Americans blame Israel, Zionists for 9/11, survey findsSky TG24Milano Fashion Week, la sfilata di Giorgio Armani Primavera/Estate 2027. FOTOLa TerceraTemblor hoy, domingo 27 de septiembre en Chile: consulta epicentro y magnitudWirtualna PolskaZamalowali rosyjskie miasta na znakach. Łotewska policja bada sprawęRai NewsNicolò Bulega campione del mondo Superbike: un italiano in vetta 14 anni dopo Max BiaggiSouth China Morning PostAfghanistan says 28 dead fighters came from Pakistan as Islamabad denies ties
The Daily Newsstand · Free, Always
Sunday, September 27, 2026

От «быстрого JSON» к потоковой обработке данных: смена парадигмы в оценке производительности протоколов

Translate

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

SilentJSON здесь – скорее инструмент и конкретная реализация идеи. Ту же архитектуру вполне можно реализовать самостоятельно, адаптировать под другой формат данных, другой язык или конкретные ограничения проекта. Она может получиться лучше или хуже SilentJSON, но главное – она может оказаться гораздо лучше приспособлена к реалиям конкретной задачи.

Сам SilentJSON – это результат многолетней практики работы с передачей и обработкой данных. Многие решения в нём появились не как попытка заранее придумать идеальную архитектуру, а как ответ на реальные ограничения: память, latency, количество аллокаций, размер потоков, стоимость промежуточных преобразований и необходимость обрабатывать данные как можно раньше.

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

И да – весь этот подход и сам пакет я публикую бесплатно. Мне интереснее поделиться самой идеей и накопленным практическим опытом, чем просто показать очередной benchmark JSON parser’а.

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

получить данные → parse → object → обработка

Так строится большинство benchmark’ов сериализации. Но для системы, работающей с непрерывным потоком, есть более интересный вопрос: насколько рано после получения данных мы можем начать что-то с ними делать?

Если полезное событие находится в начале потока, ожидание полного сообщения становится отдельной задержкой. Поэтому для потоковой обработки важны уже не только MB/s parser’а и количество аллокаций, но и время до первого полезного события, объём памяти и возможность одновременно получать, распознавать и обрабатывать данные.

Свойство JSON, которое привыкли считать недостатком

У JSON структура непосредственно находится в самом потоке:

{ } [ ] , : "

Эти символы позволяют находить границы структурных элементов без предварительного чтения отдельного заголовка или метаданных.

Это особенно интересно с точки зрения CPU. После обработки строк, escape-последовательностей и других специальных случаев parser может быстро сканировать вход и находить структурные позиции. SIMD позволяет делать такой поиск большими блоками данных.

Поэтому вопрос производительности протокола стоит рассматривать не только как “сколько байт занимает сообщение”, а как сколько работы требуется CPU, чтобы получить из этих байт информацию, управляющую дальнейшей обработкой.

Например, в protobuf часть этой информации находится в tag и wire type. Parser должен интерпретировать их, обработать varint или длину поля и определить, как перейти к следующему элементу. Это вполне эффективная модель, но она отличается от ситуации, когда структурные признаки непосредственно присутствуют в потоке.

SilentJSON: от decode к потоку

SilentJSON создавался как быстрый JSON encoder/decoder для Go, но потоковый режим позволяет использовать тот же механизм иначе.

сетевой поток
     ↓
StreamDecoder
     ↓
очередной объект
     ↓
обработка

Например:

registry := silentjson.NewRegistry[Employee]()

decoder := silentjson.NewStreamDecoder(reader, registry)

for {
    employee, err := decoder.Next()
    if err == io.EOF {
        break
    }
    if err != nil {
        return err
    }

    process(employee)
}

Граница Read() здесь не обязана совпадать с границей JSON-объекта. Один объект может прийти несколькими физическими chunks, а один chunk может содержать несколько объектов. Parser сохраняет состояние между чтениями.

Это позволяет отделить транспортный поток от логической структуры данных.

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

NextRawBlock: когда декодирование не требуется

Во многих задачах JSON вообще не нужно превращать в Go-структуру. Нужно только определить границы очередного блока и передать его дальше.

Для этого в SilentJSON есть NextRawBlock.

JSON stream
     ↓
structural scan
     ↓
raw block
     ↓
output

В таком режиме parser не тратит время на mapping полей, создание объектов и последующее преобразование обратно в JSON.

На этой модели построен silent-chunker. Он может разбивать большой JSON-массив на отдельные блоки со скоростью более 4 GB/s. Например:

./silent-chunker -file input.json -count 1000 -out chunk_

или:

cat large.json | ./silent-chunker -size 10485760

Для файла в 10 GB это уже принципиально другой класс обработки: parser занимается структурой потока, а не построением промежуточного представления каждого объекта.

Registry отвечает уже за другое

Registry не нужен для того, чтобы понять саму структуру JSON. Он нужен, чтобы сопоставить найденное поле с конкретным местом в Go-структуре и его типом.

JSON bytes
    ↓
структура
    ↓
поле
    ↓
Go field / type

В registry заранее находятся необходимые метаданные: offsets, типы, вложенные registry и другая информация для записи результата.

Таким образом, две задачи разделены: JSON сообщает parser’у, где находятся данные, а registry – что с ними делать в Go.

Это позволяет убрать значительную часть работы reflection-heavy generic path с hot path.

RawJson: данные как вложенный протокол

RawJson позволяет оставить часть JSON в исходном виде и передать её следующему уровню обработки.

Protocol A
    ↓
  RawJson
    ↓
Protocol B
    ↓
  RawJson
    ↓
Protocol C

Это позволяет строить вложенные протоколы без обязательного полного декодирования каждого уровня. Текущий слой обрабатывает только ту структуру, которая ему нужна, а вложенный фрагмент передаёт дальше.

Такая модель напоминает обычное разделение протоколов: один уровень знает о своём формате, а содержимое следующего уровня передаётся ему как payload.

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

От сообщений к событиям

Отсюда возникает ещё одна возможность.

Если системе требуется только обнаружить событие, не всегда имеет смысл сначала инициализировать полный pipeline обработки.

Например:

data received
     ↓
event detected
     ↓
event registered
     ↓
normal processing

Вместо:

initialize → receive → parse → process → detect

можно в некоторых сценариях получить:

receive → detect → register

Это особенно интересно для систем, где само событие важно зафиксировать как можно раньше.

В банковских операциях это может быть регистрация этапов транзакции по мере их появления. В логировании – фиксация критического события до запуска более тяжёлого logging pipeline. В мониторинге – обнаружение роста задержек, retry, проблем с соединением или деградации сервиса до того, как они начнут распространяться по цепочке микросервисов.

Здесь оптимизируется уже не только обработка данных, но и стоимость подготовки системы к обработке.

Как меняется измерение производительности

Для обычного parser’а достаточно спросить:

Сколько MB/s он декодирует?

Для потоковой системы этого недостаточно.

Имеют значение:

  • время до первого полезного события;

  • время до первой обработанной записи;

  • throughput непрерывного потока;

  • рабочий объём памяти;

  • количество аллокаций;

  • стоимость обработки структурных событий;

  • возможность перекрывать получение и обработку;

  • время обнаружения проблемы.

Две системы могут обработать одинаковые 10 GB за одинаковое время, но одна начнёт работу только после получения всего объёма, а другая будет обрабатывать первые записи уже во время поступления остальных.

Для throughput это может быть одинаковый результат. Для latency и поведения системы под нагрузкой – совершенно разный.

Результаты SilentJSON

В одном из benchmark’ов StreamDecoder.Next показывает около 614 MB/s при 41 MB аллокаций. NextRawBlock, где отсутствует полноценное mapping в структуры, – около 4009 MB/s при примерно 0.9 MB аллокаций.

NextChan, использующий producer-consumer модель, показывает около 447 MB/s и 41 MB аллокаций.

Режим

Throughput

Allocations

Что измеряется

StreamDecoder.Next

~614 MB/s

~41 MB

декодирование объектов

NextRawBlock

~4009 MB/s

~0.9 MB

структурное извлечение

NextChan

~447 MB/s

~41 MB

потоковый producer/consumer

В другом тесте на 100 000 объектов SilentJSON в parallel mode показал около 24 670 MB/s против примерно 644 MB/s для Sonic и 110 MB/s для encoding/json.

Scalar fallback без AVX2 показал около 810 MB/s. Это важно хотя бы потому, что сама модель не сводится к использованию SIMD: SIMD ускоряет структурный поиск, но основная архитектура остаётся работоспособной и без него.

Эти цифры относятся к конкретным тестам, данным и реализации, поэтому их нельзя напрямую переносить на любой JSON workload. Но они показывают разницу между полноценным декодированием и операциями, которым вообще не требуется построение Go-объектов.

Zero-copy

Потоковая обработка также меняет отношение к строкам.

Если возвращаемая строка является представлением исходного JSON buffer, её можно получить без отдельного копирования. Но lifetime такой строки связан с lifetime исходных данных.

Если значение нужно сохранить независимо от входного буфера, копирование должно быть явным:

strings.Clone(s)

Это позволяет не платить за копирование там, где данные нужны только во время текущей обработки, и явно платить за него там, где требуется долгоживущая копия.

От быстрого parser’а к потоковой архитектуре

В итоге SilentJSON интересен не только как попытка сделать Unmarshal быстрее.

Главное изменение – в самой единице обработки.

JSON можно рассматривать не как одно неделимое сообщение, а как поток структурных событий и логических блоков:

byte stream
    ↓
structural scan
    ↓
logical block
    ↓
RawJson / typed object
    ↓
event / processing
    ↓
next layer

Не каждый уровень обязан декодировать всё сообщение. Если нужны границы – достаточно структурного сканирования. Если нужен объект – используется registry. Если нужен вложенный протокол – можно передать RawJson. Если требуется только событие – его можно зарегистрировать отдельно от полной обработки.

Именно поэтому классический вопрос “насколько быстрый у нас JSON parser?” становится недостаточным.

Для непрерывного потока важнее другое:

как рано мы можем получить полезную информацию из данных и сколько работы требуется до первой реакции системы?

В этом смысле производительность протокола – это не только MB/s.

Это время до первого события, объём промежуточного состояния, стоимость перехода между уровнями и способность получать, распознавать, передавать и обрабатывать данные одновременно.

И тогда JSON становится не просто форматом сериализации. Его структурные байты становятся частью механизма управления потоком.

А SilentJSON – не просто быстрым JSON parser’ом, а экспериментом с другой моделью обработки:

получать
   ↓
распознавать
   ↓
реагировать
   ↓
передавать
   ↓
обрабатывать
View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.