Первая версия кода была сделана в феврале, после этого этот же код был партирован в другой наш проект. Пока полет нормальный и все это время жили на том, что было написано тогда в режиме ASAP. Я не писал статью сразу – велосипед как вино, ему нужно настоятся и оправдать себя.
Многих фичей в изначальной версии не было, например Nullable, они нам были не нужны. Для публичной версии я добавил побольше типов, скоро планирую еще Variant добавить, чтобы уж совсем универсально стало.
Библиотека пока в ранней версии, поэтому 0.1.3, а не 1.0.0. Изначально она была сделана ASAP за пару ночей для срочного решения задачи, я попробовал привести до публикации ее в более приличный вид, но еще не все исправил. Комментарии читаю, записываю на листочек и буду делать – так же приветствую пулл реквесты.
Проблема в том, что приходится поддерживать схему в двух местах – Pydyntic с которым работаем внутри питона и одельно .proto схему + кодген по нему. Плюс PyByntic в том, что все поля задаются один раз в одном месте.
Для сложных систем, где много микросервисов протобаф конечно удобнее, можно передать схему другому разработчику и он сам в своем языке с ней разберетс. Цель моего проекта – эффективно кэшировать объекты в рамках одного сервиса.
Average size PyByntic: 2157.96 bytes Average size PyByntic+gzip: 1178.82 bytes Average size JSON+gzip: 2364.51 bytes
Сжатый JSON все еще хуже PyByntic, а если сжать PyByntic, то разница в два раза. Ну и CPU вы на gzip туда сюда будете заметно больше тратить, чем на запись и чтение байт.
Я планирую в начале модели записывать один байт версии и дальше в нотации добавить возможность у каждого поля в питоне задавать минимальную и максимальную версию. Это решит часть проблем с разными версиями, но глобально для кэша это не особо критично.
В протобаф весь нестед объект лежит подряд, вот каждый итем, сначала его ид, потом тип, потом остальные поля и так по кругу. Я же раскрываю в "колонки", будут сначала все айди нестед итемов подряд, потом все типы и т.д. Если, например, одно текстовое поле с одинаковым текстом "text" повторяется в каждом итеме, то в моем формате будет texttexttexttexttext, для алгоритмов компрессии это выгоднее. В бенчмарке мы справниваем без компрессии, поэтому к нему это не относилось, а просто комментарий в общем о формате.
DateTime32 потерял зону - было 2106-02-07 06:28:15, стало 2106-02-07 03:28:15+00:00
Зоны мы не сохраняем, но планируется добавить опцию timezone-aware. Это можно через кастомный тип сделать. Для нас этой необходимости небыло, мы на бэкенде храним все даты в UTC.
У FastApi есть механизм Depends, который решает эту задачу заметно удобнее – один раз прописываешь для роутера и дальше в каждой ручке внутри роутера будет проверка токена и уже готовый объект инит даты
Скрытый текст
def parse_init_data(init_data: str) -> TelegramInitData:
parsed_query = urllib.parse.parse_qs(init_data)
data = {}
for key in parsed_query:
data[key] = parsed_query[key][0]
data["user"] = json.loads(data["user"])
return TelegramInitData.validate(data)
def validate_telegram_data(query_string: str) -> bool:
data = dict(urllib.parse.parse_qsl(query_string))
received_hash = data.pop("hash", None)
if received_hash is None:
return False
data_check_string = "\n".join(
f"{key}={urllib.parse.unquote(value)}" for key, value in sorted(data.items())
)
secret_key = hmac.new(
key="WebAppData".encode("utf-8"),
msg=BOT_TOKEN.encode("utf-8"),
digestmod=hashlib.sha256,
).digest()
check_hash = hmac.new(
key=secret_key, msg=data_check_string.encode("utf-8"), digestmod=hashlib.sha256
).hexdigest()
return hmac.compare_digest(check_hash, received_hash)
async def decode_auth_header(request: Request):
expected_header = "authorization"
if expected_header not in request.headers:
raise HTTPException(status_code=401, detail="Authorization header is required")
token_encoded = request.headers[expected_header].removeprefix("TMA ")
token = base64.b64decode(token_encoded).decode()
if not validate_telegram_data(token):
raise HTTPException(status_code=401, detail="Invalid initData")
init_data = parse_init_data(token)
request.state.data = init_data
return init_data
Мой пример инит дату в Base64 в Authorization: TMA .... передает, но думаю концепт понятен.
Модель юзеров взята из реального хайлоуд проекта, я лишь убрал и переименовал часть полей, чтобы уж не совсем было явно откуда.
PyByntic – 2300 байт на юзера Protobuf – 3500 байт на юзера Json – порядка 10+ тысяч байт на юзера
В тесте данные рандомизируются, от запуска к запуску могут быть немного разные цифры, если не лень можете прогнать с большим семплом. В статье был тест на базе этой же модели юзера, я честно забыл какой там был параметр о количестве итемов/тасков на юзера, помню, что в районе 100, вроде с такими параметрами данные +- как в статье.
За счет чего выигрыши: 1. Вообще не сохраняется структура данных, использован абсолютный возможный минимумом байтов. Это совершенно не подходит для долгого хранения данных, например на диске, так как в байтах не сохраняется даже версия модели (это в планах добавить), но для хранения кэша в Редисе это не является большой проблемок. 2. В теории PyByntic лучше подходит для сжатия, потому что одинаковые поля хранятся рядом.
Например:
class Tag:
description = "some long text"
id = 1
class Post:
text = "long text"
tags: list[Tag]
tags = []
for i in range(1000):
tags.append(Tag(description="very long description", id=i))
post = Post(text="test", tags=tags)
После сериализации структура становится колоннообразной:
Затем сохраняются только сами компактные бинарные данные. Поскольку все повторяющиеся длинные описания расположены в памяти подряд, а не разбросаны среди объектов, алгоритмы сжатия могут работать гораздо эффективнее.
https://github.com/sijokun/PyByntic/releases/tag/v0.1.4
типы для datetime с таймзонами добавил.
Первая версия кода была сделана в феврале, после этого этот же код был партирован в другой наш проект. Пока полет нормальный и все это время жили на том, что было написано тогда в режиме ASAP. Я не писал статью сразу – велосипед как вино, ему нужно настоятся и оправдать себя.
Многих фичей в изначальной версии не было, например Nullable, они нам были не нужны. Для публичной версии я добавил побольше типов, скоро планирую еще Variant добавить, чтобы уж совсем универсально стало.
Спасибо!
Библиотека пока в ранней версии, поэтому 0.1.3, а не 1.0.0. Изначально она была сделана ASAP за пару ночей для срочного решения задачи, я попробовал привести до публикации ее в более приличный вид, но еще не все исправил. Комментарии читаю, записываю на листочек и буду делать – так же приветствую пулл реквесты.
Проблема в том, что приходится поддерживать схему в двух местах – Pydyntic с которым работаем внутри питона и одельно .proto схему + кодген по нему. Плюс PyByntic в том, что все поля задаются один раз в одном месте.
Для сложных систем, где много микросервисов протобаф конечно удобнее, можно передать схему другому разработчику и он сам в своем языке с ней разберетс. Цель моего проекта – эффективно кэшировать объекты в рамках одного сервиса.
Average size PyByntic: 2157.96 bytes
Average size PyByntic+gzip: 1178.82 bytes
Average size JSON+gzip: 2364.51 bytes
Сжатый JSON все еще хуже PyByntic, а если сжать PyByntic, то разница в два раза. Ну и CPU вы на gzip туда сюда будете заметно больше тратить, чем на запись и чтение байт.
Enum?
Раскрою секрет: Все типы моей библиотеке взяты и полностью совместимы с нативным форматом ClickHouse.
Ради интереса можно даже бинарные даты от SELECT ... FORMAT Native ей распарсить (нужно оставить только сами данные без хедера).
Я планирую в начале модели записывать один байт версии и дальше в нотации добавить возможность у каждого поля в питоне задавать минимальную и максимальную версию. Это решит часть проблем с разными версиями, но глобально для кэша это не особо критично.
В Protobuf нет ничего ниже uint32: документация.
В протобаф весь нестед объект лежит подряд, вот каждый итем, сначала его ид, потом тип, потом остальные поля и так по кругу. Я же раскрываю в "колонки", будут сначала все айди нестед итемов подряд, потом все типы и т.д. Если, например, одно текстовое поле с одинаковым текстом "text" повторяется в каждом итеме, то в моем формате будет texttexttexttexttext, для алгоритмов компрессии это выгоднее. В бенчмарке мы справниваем без компрессии, поэтому к нему это не относилось, а просто комментарий в общем о формате.
Зоны мы не сохраняем, но планируется добавить опцию timezone-aware. Это можно через кастомный тип сделать. Для нас этой необходимости небыло, мы на бэкенде храним все даты в UTC.
У FastApi есть механизм Depends, который решает эту задачу заметно удобнее – один раз прописываешь для роутера и дальше в каждой ручке внутри роутера будет проверка токена и уже готовый объект инит даты
Скрытый текст
Мой пример инит дату в Base64 в Authorization: TMA .... передает, но думаю концепт понятен.
И пример испоьзования:
В библиотеке есть DateTime64 – это основная суть библиотеки, возможность самому выбрать максимально точно сколько байт тебе нужно для твоих данных.
В моем случае я не думаю, что структура которая сегодня сохраняется на пару часов в редисе будет актуальна через сколько-то там десятков лет.
Типы DateTime и Date полностью позаимстованы из ClickHouse, со всеми их минусами и плюсами.
https://github.com/sijokun/PyByntic/tree/test_protobuf_vs_pybyntic/protobuf_vs_pybyntic
Модель юзеров взята из реального хайлоуд проекта, я лишь убрал и переименовал часть полей, чтобы уж не совсем было явно откуда.
PyByntic – 2300 байт на юзера
Protobuf – 3500 байт на юзера
Json – порядка 10+ тысяч байт на юзера
В тесте данные рандомизируются, от запуска к запуску могут быть немного разные цифры, если не лень можете прогнать с большим семплом. В статье был тест на базе этой же модели юзера, я честно забыл какой там был параметр о количестве итемов/тасков на юзера, помню, что в районе 100, вроде с такими параметрами данные +- как в статье.
За счет чего выигрыши:
1. Вообще не сохраняется структура данных, использован абсолютный возможный минимумом байтов. Это совершенно не подходит для долгого хранения данных, например на диске, так как в байтах не сохраняется даже версия модели (это в планах добавить), но для хранения кэша в Редисе это не является большой проблемок.
2. В теории PyByntic лучше подходит для сжатия, потому что одинаковые поля хранятся рядом.
Например:
После сериализации структура становится колоннообразной:
Затем сохраняются только сами компактные бинарные данные. Поскольку все повторяющиеся длинные описания расположены в памяти подряд, а не разбросаны среди объектов, алгоритмы сжатия могут работать гораздо эффективнее.
В микрафонах часто бывает именно крона