Режим сырых пакетов: protobuf без реестра схем
Первые четыре месяца Runnev разбирал каждое публикуемое вами событие. Мы валидировали JSON, считали события и хранили структурированные объекты. Потом мы выпустили режим сырых пакетов и совсем перестали заглядывать внутрь полезных нагрузок. Вот почему и что это дало.
Проблема разбора всего подряд
У разбора три цены, и все растут с вашим трафиком. Он жжёт CPU на пути приёма — том самом пути, который меньше всего хочется замедлять. Он навязывает мнение о схеме пользователям, у которых уже есть своё. И он создаёт класс отказов — некорректный JSON, — не имеющий отношения к тому, являются ли байты тем, что издатель собирался отправить.
Первыми это ощутили те, кто уже сериализовал через protobuf. У них были схема, кодогенератор и эффективные на проводе байты, а мы просили их обернуть эти байты в JSON, чтобы мы разобрали обёртку и проигнорировали содержимое. Это чистые накладные расходы: раздувание base64, ненужный нам разбор и вторая схема — наша — поверх их собственной.
Режим raw: байты внутрь, байты наружу
Поток, созданный с "mode":"raw", принимает при публикации тело
application/octet-stream и хранит его дословно. Мы не разбираем его, не валидируем и не
преобразуем. Пакет — это те байты, что вы отправили, и при повторе вы получаете ровно эти байты обратно
с тем content-type, который вы задали.
curl https://runnev.dev/v1/streams/$STREAM/9001 \
-H "Authorization: Bearer $RUNNEV_API_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary @order.pbНет реестра схем, который нужно держать, и нет схемы, которую нужно регистрировать. Ваш издатель и ваш потребитель договариваются о формате так, как делали всегда, — вне полосы, в своём собственном коде. Мы труба, а не сторона контракта.
Как теперь выглядит путь приёма
Для сырого пакета обработчик приёма делает четыре вещи: проверяет авторизацию, проверяет лимит размера,
проверяет номер и дописывает байты в кольцевой буфер. Нет json.Unmarshal, нет перебора по
событиям, нет подсчёта. Пакет считается за одно событие для учёта, а байты считаются для пропускной
способности и исходящего трафика.
Разница в пропускной способности проявилась сразу для крупных пакетов. На пакете 512 КиБ из нескольких тысяч мелких записей отказ от разбора и от кругооборота base64 примерно вдвое сократил время CPU, которое мы тратили на пакет при приёме, и убрал целый хвост всплесков задержки, которые коррелировали с давлением сборки мусора от выделения разобранных структур, выбрасываемых миллисекундой позже. Мы не станем делать вид, что одно число обобщается, потому что это целиком зависит от формы полезной нагрузки, но тенденция была очевидной: самый дешёвый разбор — тот, который вы не делаете.
Цена того, чтобы не заглядывать
Режим raw перекладывает ответственность на вас, и мы хотим быть честными на этот счёт. Мы не можем сказать
вам, что сырой пакет некорректен, потому что не знаем, что «корректно» означает для ваших данных. Мы не
можем сосчитать события внутри него, поэтому лимиты на события считают пакет за одно событие и
тарифицируют по байтам. А по живому пути SSE сырые байты кодируются в base64 в поле
data, потому что SSE — текстовый протокол; эндпоинт повтора возвращает сырые байты
напрямую, так что потребитель, которому важна эффективность по байтам, читает из повтора.
Для структурированных событий, где вы хотите, чтобы платформа понимала ваши данные, режим json по-прежнему по умолчанию и по-прежнему верный выбор. Режим raw — для случая, когда вы уже владеете форматом и хотите, чтобы мы не путались под ногами. Выпустить оба и позволить потоку объявлять при создании, какой из них он, оказалось проще, чем пытаться умничать с автоопределением, которое мы попробовали сначала и справедливо забросили.