backendhubvk | Unsorted

Telegram-канал backendhubvk - Backend VK Hub

-

Комьюнити VK для бэкендеров. Cамые хардовые кейсы, дискуссии в кругу своих и прямой доступ к нашим экспертам 😎

Subscribe to a channel

Backend VK Hub

Python 3.15 принёс ленивые импорты, UTF-8 по умолчанию и профилировщик Tachyon

Свежий релиз Python 3.15.0 вышел 9 октября. Ленивые импорты поменяли запуск приложений, текстовые файлы на Windows теперь читаются в кодировке UTF-8, а новый встроенный семплирующий профилировщик Tachyon подключается к живому процессу.

Разобрали главное, а подробности доступны в документации.

Ленивые импорты

Ключевое слово lazy откладывает загрузку модуля до первого обращения к имени (PEP 810).

lazy import json

print("Старт") # json ещё не загружен
data = json.loads("{}") # json загружается здесь

Вместо модуля интерпретатор держит прокси, а ошибка загрузки возникает в точке первого использования. В traceback при этом видны и место обращения, и исходная строка lazy import.

Разрешён такой импорт только на уровне модуля, а режим для всего процесса задают -X lazy_imports и PYTHON_LAZY_IMPORTS. Библиотекам с поддержкой нескольких версий подойдёт переменная lazy_modules, которую старые Python игнорируют.

Авторы PEP обещают сократить запуск CLI на 50–70%, но можно нарваться и на побочные эффекты, поскольку регистрация плагинов при импорте уедет на момент первого использования.

UTF-8 по умолчанию

Теперь open("file.txt") без encoding читает UTF-8 независимо от локали (PEP 686). В локали C и POSIX так было и раньше, поэтому основной удар приходится на Windows. CSV в cp1251, которые раньше читались без указания кодировки, теперь прочитаются как UTF-8.
Авторы PEP предупреждают, что возможна даже тихая порча данных. Найти такие места поможет команда для CI.
python -X warn_default_encoding -W error::EncodingWarning -m pytest

Старое поведение возвращается через PYTHONUTF8=0.

Профилировщик Tachyon

Пакет profiling.sampling читает стеки процесса снаружи, без хуков. Частота доходит до 1 МГц, а нагрузку на процесс авторы называют практически нулевой.
python -m profiling.sampling attach --flamegraph -d 30 -o profile.html <PID>

Команда подключается к работающему процессу и за 30 секунд собирает флеймграф. Режимы wall и cpu помогают отличить вычисления от ожидания, а gil показывает код, который монополизирует интерпретатор.

Среди форматов вывода есть pstats, дифференциальный флеймграф и JSON для Firefox Profiler. Время в отчётах оценивается по семплам, а профилировщик и цель должны работать на одной минорной версии Python.

JIT и free-threading

JIT остаётся экспериментальным и включается переменной PYTHON_JIT=1. Прирост по geomean pyperformance составляет 7–8% на x86-64 Linux и 11–12% на AArch64 macOS. На отдельных бенчмарках разброс велик, а для I/O-bound кода выигрыш незначителен.

На Windows официальные 64-разрядные сборки теперь используют интерпретатор на хвостовых вызовах. Он даёт 15–20% относительно switch-case интерпретатора.

В установщике для macOS пакет с free-threaded сборкой теперь отмечен по умолчанию и ставится рядом с обычной, но команда python3 по-прежнему запускает сборку с GIL.
Free-threaded сборка медленнее на однопоточном коде на 1–8%, но новый abi3t позволяет одному и тому же бинарнику работать и на обычных сборках и на сборках без GIL.

Новый синтаксис

Появились встроенные frozendict и sentinel, а распаковка аргументов заработала в comprehensions. Файлы .start заменяют исполняемые строки import в .pth.
flat = [*li for li in lists]    # вместо вложенного цикла

MISSING = sentinel("MISSING") # отличает «не передан» от None
def get(key, default=MISSING):
...

Заключение

Тесты на 3.15 можно гонять в CI уже сейчас, а для продакшена лучше дождаться колёс зависимостей и версии 3.15.1. Начинать миграцию разумнее с EncodingWarning, а lazy и JIT включать только после собственных замеров.

#backendvkhub #python

Читать полностью…

Backend VK Hub

🔊 Джависты, собираем рюкзаки!

Не в школу, конечно, а на Joker 2026 с 14 по 15 октября.

➡️ Егор Леванков, старший разработчик One-cloud, поделится на конференции опытом непрерывного профилирования прода на десятках тысяч хостов. Расскажет, чем отличаются подходы для Java и натива, как устроены форматы профилей и что стоит за готовыми решениями.

В этот раз мы подготовили самый большой стенд: заглядывайте в Java-класс, чтобы проверить инженерную интуицию в «Умниках и фактах», собрать рюкзак Java-разработчика, решать задачи на инциденты с экспертами VK и поиграть в настолки и ретроигры.

📌 14–15 октября
📍 Санкт-Петербург, площадь Победы, 1, Cosmos Saint-Petersburg Pulkovskaya Hotel

💻 Можно участвовать онлайн

➡️ Купить билет

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

#backendvkhub #конференция #java

Читать полностью…

Backend VK Hub

HTTP QUERY: почему поиск больше не должен притворяться методом POST

Поисковый фильтр в API давно перестал быть парой простых параметров — это дерево условий, массивы идентификаторов, диапазоны дат и состояние пагинации. В GET всё это пришлось бы кодировать в URI.

Стандарт HTTP советует поддерживать адреса длиной хотя бы 8 000 байт, но лимит у каждого звена свой. Поэтому длинный URL может обрезать браузер, балансировщик или WAF ещё до сервиса. Даже если адрес прошёл всю цепочку, он раздувается от percent-encoding и целиком оседает в логах и истории браузера.

Почти каждый поиск в API живёт как POST /search. Для протокола это обычный POST: он небезопасный и неидемпотентный, поэтому прокси не имеет права повторить оборванный запрос, кеш — переиспользовать такой ответ, а клиент с retry рискует задублировать операцию.

Что предложил RFC 10008

В июне 2026 года IETF опубликовал RFC 10008 с методом QUERY. Метод объединяет безопасность и идемпотентность GET с телом запроса, как у POST.

QUERY /feed HTTP/1.1
Host: api.example.test
Content-Type: application/json
{
"filter": {
"status": ["published", "scheduled"], # допустимые статусы
"publishedAt": { "gte": "2026-01-01" } # нижняя граница даты
},
"sort": [{ "field": "publishedAt", "direction": "desc" }],
"limit": 50
}

URI здесь задаёт ресурс и область выборки, а filter и sort в теле описывают условия поиска. Называть QUERY «GET с телом» неточно, ведь у тела GET нет определённой семантики, а у QUERY оно определяет всю операцию. Content-Type при этом обязателен: без него сервер отклоняет запрос, а угадывать формат по содержимому запрещено.

Кеширование

Ключ кеша обязан включать URI, тело запроса и Content-Type, поэтому запросы с "page": 1 и "page": 2 не могут делить одну запись. Чтобы построить ключ, кешу сначала придётся прочитать всё тело, и кеширование QUERY выходит дороже, чем GET.

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

Что с поддержкой?

OpenAPI 3.2 уже описывает query как first-class операцию. Серверные фреймворки подтягиваются с разной скоростью. Spring Framework 7.1 содержит HttpMethod.QUERY, заголовок Accept-Query и привязку тела запроса. В Gin и Fiber поддержка появилась в upstream, а Fastify и NestJS пока остановились на issues.

Инфраструктура отстаёт от фреймворков. NGINX уже проксирует QUERY, но не кеширует его, потому что proxy_cache_methods по умолчанию ограничен GET, HEAD и POST. В браузерах метод не входит в CORS-safelist, поэтому междоменный вызов требует preflight, а HTML-формы method="query" не стандартизированы.

QUERY закрывает старый разрыв между GET и POST, но сам стандарт не заставляет промежуточные звенья его понимать. Перед продом прогоняйте всю цепочку на реальном трафике и держите POST /search как фолбэк.

#backendvkhub #api #поиск

Читать полностью…

Backend VK Hub

Pattern Matching в Java 21+: switch, который заменяет цепочки instanceof

С JDK 21 switch Java наконец умеет то, что программисты пятнадцать лет искали в Scala и Kotlin — паттерн-матчинг по типам, деструктуризацию записей и exhaustive-проверки на sealed-иерархиях.

В карточках — что именно поменялось, как выкинуть цепочки instanceof, зачем нужны record patterns и где лежат грабли.

#backendvkhub #java

Читать полностью…

Backend VK Hub

In-process базы данных: libSQL, DuckDB, LanceDB

Чтение из локального файла занимает микросекунды. Запрос к удалённой PostgreSQL — 30–80 мс. In-process БД убирают сетевой вызов из пути чтения: libSQL для OLTP, DuckDB для аналитики, LanceDB для векторного поиска. 

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

#backendvkhub #архитектура_бд

Читать полностью…

Backend VK Hub

📢 Как мы увеличили время в ленте ВКонтакте на 11%

Рекомендательная система ВКонтакте — зрелый пайплайн, но и в нём есть точки роста. В этом году команда AI VK улучшила несколько этапов: заменила классический мультитаргет ранкера на композитный, стохастику в блендере — на детерминированный алгоритм Брезенхема, эвристики в ALS — на вероятностную оценку.

🔥 Результат: +11% времени в ленте, CTR лайка вырос на 30%, CTR скрытия поста снизился на 10%.

➡️ Подробности в статье на Хабре

Читать полностью…

Backend VK Hub

Redis — это не только строки. Шесть структур данных, которые закрывают целые классы задач

Большинство используют Redis как key-value кеш: SET, GET, EXPIRE. При этом внутри есть Sorted Sets для рейтингов, HyperLogLog для приблизительного подсчёта уникальных значений, Bitmaps для аналитики, GEO для геопоиска, Hash для объектов и Sets для тегов.

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

#backendvkhub #redis

Читать полностью…

Backend VK Hub

Считанные минуты остаются до старта двух основных треков выступления. А пока делимся закулисьем атмосферы на нашем митапе ⬇️

Читать полностью…

Backend VK Hub

Как запретить пересечения периодов без триггеров в PostgreSQL

Стандартная задача из SaaS: календарь бронирования созвонов. Одна комната не должна быть занята двумя бронированиями сразу. Пользователь A ставит на 14:00–15:00, пользователь B — на 14:30–15:30, оба жмут «Сохранить» одновременно, в базе появляются оба. Классические подходы — SERIALIZABLE-транзакция с ловлей отказов, advisory lock на комнату или BEFORE-триггер. Все три работают, при этом легко ломаются при UPDATE, каскадах или под нагрузкой.

В PostgreSQL для этого есть EXCLUDE constraint — родной механизм, делающий то же самое на уровне базы через индекс. Пишется так:

CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE bookings (
id serial PRIMARY KEY,
room_id int NOT NULL,
period tstzrange NOT NULL,
EXCLUDE USING gist (
room_id WITH =,
period WITH &&
)
);


Читается как «не давать двум строкам иметь одновременно одинаковый room_id и пересекающийся period». && — оператор пересечения для range-типов, = — обычное равенство. btree_gist нужен, потому что room_id — обычный int, а GiST по умолчанию понимает только geometric и range.

Пробуем:
INSERT INTO bookings (room_id, period)
VALUES (1, '[2026-08-06 14:00, 2026-08-06 15:00)');
-- OK

INSERT INTO bookings (room_id, period)
VALUES (1, '[2026-08-06 14:30, 2026-08-06 15:30)');
-- ERROR: conflicting key value violates exclusion constraint

Ни триггеров, ни блокировок, ни SERIALIZABLE. Под капотом PostgreSQL проверяет пересечения через GiST-индекс, автоматически создающийся с констрейнтом. При UPDATE та же семантика. Race condition не пролезет, проверка идёт под нужными блокировками.

Дальше интереснее — EXCLUDE работает не только с временными диапазонами. Тот же паттерн закроет пересечение IP-диапазонов (inet WITH &&), непересекающиеся 2D-области (box WITH &&), уникальные тарифы на интервалы [valid_from, valid_to) для одной услуги. Один раз добавили — забыли про самописную валидацию.

Часто используют partial EXCLUDE с WHERE. Отменённые бронирования не должны блокировать новые:
ALTER TABLE bookings ADD EXCLUDE USING gist (
room_id WITH =, period WITH &&
) WHERE (status = 'active');

status = 'cancelled' строки в проверке не участвуют, освобождённое окно можно перебронировать.

При всём этом GiST-индекс дороже btree на point-lookup, если поиск по room_id частый, заведите отдельный btree, GiST оставьте под EXCLUDE.

Если в проекте есть триггер BEFORE INSERT/UPDATE с проверкой пересечений или app-код с SELECT ... FOR UPDATE вокруг вставки — он почти наверняка заменяется одним EXCLUDE constraint, из репозитория уходят сотни строк.

#backendvkhub #postgresql

Читать полностью…

Backend VK Hub

OneDWH VK: как консолидировали 0,5+ EB данных из 35 бизнес-вертикалей

В VK исторически сложилась децентрализованная модель управления данными: каждая из 35 бизнес-вертикалей развивала аналитику самостоятельно и выбирала технологии независимо от соседей. К моменту решения о переезде в облако это выросло в 10+ железных кластеров под Airflow и Hadoop, 0,5+ EB аналитических данных и тысячи кросс-запросов в месяц между командами.

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

В карточках — чем именно мешала разрозненная инфраструктура, какие принципы легли в основу новой платформы, как проходила миграция 30 тысяч объектов и что получилось на выходе.

➡️ Полная версия статьи

#backendvkhub #dwh #обзорстатьи

Читать полностью…

Backend VK Hub

Инвалидация кеша: скорость доступа vs согласованность данных 

Кеш и источник данных не могут быть строго консистентны. Если бы кеш при каждом чтении ходил в основное хранилище, сам смысл кеширования был бы утрачен. Поэтому любая стратегия инвалидации — компромисс между задержкой чтения, нагрузкой на источник и окном, где клиент видит устаревшие данные. Надо решать не «как сделать правильно», а «какое окно рассинхронизации вы готовы терпеть».

Это окно складывается из двух вещей, которые часто путают между собой: как кеш узнаёт, что данные устарели, и что он делает после того, как узнал. Первое определяет, сколько вы вообще будете жить с устаревшими данными. Второе — как быстро от них избавитесь, когда обнаружите.
 
➡️ Как кеш узнаёт, что данные устарели

TTL. Кеш считает запись невалидной через фиксированное время после записи. TTL не проверяет, изменились ли данные на самом деле — это просто таймер. Плюс: TTL не завязан на запись, живёт в cache-aside, не требует встраивать кеш куда-либо. Минус: окно неконсистентности задаёт произвольное число, а не реальное событие.

Write-through. Приложение пишет в кеш, а кеш сам синхронно сохраняет данные в источник. Точка входа для записи — кеш, а не база. Данные актуальны почти всегда. Почти — потому что кеш может уже обновиться, когда запись в источник ещё не подтвердилась. Минус: каждая запись становится медленнее на постоянную величину, даже если этот элемент никому в кеше не нужен.

Event-driven (Pub/Sub). Источник публикует событие об изменении, кеш подписан и обновляется асинхронно. Такой подход разводит запись и инвалидацию во времени, но добавляет риски, о которых часто забывают:

🔵брокер может потерять событие, если не даёт гарантию at-least-once
🔵нужно гарантировать сохранение порядка событий. Если придут не по порядку — старое перезапишет свежие данные
🔵пока событие создаётся и пересылается, кеш отдаёт устаревшие данные и не знает об этом

➡️ Что кеш делает после инвалидации

Триггер инвалидации не определяет, что происходит с данными дальше. Их обновление — это отдельная, независимая ось.

Purge. Кеш точечно удаляет элемент по известному ключу. Данные вернутся в кеш, только когда клиент их запросит.

Refresh. Кеш сам идёт в источник за свежим значением сразу после инвалидации — не ждёт запроса клиента.

Ban. В отличие от purge, кеш инвалидирует не один ключ, а пачку данных по паттерну или диапазону — даже не зная точных ключей. Запись может ещё какое-то время отдаваться как есть (grace period) и обновиться при следующем обращении. TTL по умолчанию ведёт себя как Ban: невалидная запись просто ждёт следующего запроса. Чтобы получить refresh, добавьте его поверх TTL явно.

➡️ Что выбрать

Смотрите на три вещи:
🔵насколько критична консистентность — допустимо ли показать пользователю данные пятисекундной давности
🔵как часто источник меняет данные
🔵какой у вас профиль нагрузки — read-heavy или write-heavy

На практике стратегии комбинируют. Например: берёте TTL как страховочный потолок на случай, если событие потерялось, и добавляете event-driven — для быстрой инвалидации горячих ключей. Так надёжнее, чем полагаться на что-то одно.

#backendvkhub #кеш #архитектура

Читать полностью…

Backend VK Hub

🔵PostgreSQL 19: Beta 2

Июль в бэкенде — история про PostgreSQL. 16 июля вышла Beta 2 версии 19: Feature freeze означает, что финальный список фич зафиксирован, от RC до GA API уже не поменяется, и то, что тестируется сейчас, реально поедет в прод в сентябре-октябре.

🔵Графовые запросы и REPACK

Из вышедшего самое громкое — SQL/PGQ, Property Graph Queries. MATCH-паттерны для графового обхода прямо в SQL. Раньше для такого приходилось либо гонять WITH RECURSIVE с массивом посещённых вершин от циклов, либо тащить отдельную графовую БД типа Neo4j. Теперь запрос вроде «Найди все узлы, до которых от A можно дойти через N связей типа friend» пишется декларативно через MATCH. За компанию — REPACK как встроенная SQL-команда: zero-downtime реорганизация таблиц становится частью базы, pg_repack extension постепенно уходит на пенсию.

🔵Autovacuum, планировщик и auto-scaling AIO

Плюс parallel autovacuum через autovacuum_max_parallel_workers (на терабайтных таблицах эффект будет заметный), pg_plan_advice для явного контроля планов и auto-scaling AIO через io_min_workers и io_max_workers. В версии 18 io_method=worker появился, в 19 количество workers само подстраивается под нагрузку.

🔵Что проверить на стейджинге перед GA

Feature freeze — как раз то состояние, когда стоит гонять реальный workload по стейджингу. В pg_dumpall, кастомных TOAST-настройках, RADIUS-auth и btree_gist на inet/cidr были правки, поэтому лучше словить регрессии сейчас, чем через два месяца после GA.

🔵Минорные релизы

Параллельно вышли минорки всех веток — 18.4, 17.10, 16.14, 15.18, 14.23. Суммарно закрыли 11 проблем с безопасностью и 60+ багов. Стоит обратить внимание: EOL ветки 14 — 12 ноября 2026 года, до конца поддержки полгода. Планировать миграцию стоит начинать сейчас.

🔵Redis

Redis провёл июль тихо. Software 7.2.4-154 (1 июля) — внутренние фиксы без громких заголовков. Из общей тенденции: 8.x продолжает интересно развиваться — Vector Sets как native-тип для similarity search (не workaround через ZSET плюс hashes), dev vs production database mode с type-to-confirm при destructive-операциях на проде. Не строго июльская новость, но контекст важный.

Новая статья от инженеров VK на Хабре

🔵Сериализация one-nio: от истоков к поддержке JDK 25

#backendvkhub #дайджест

Читать полностью…

Backend VK Hub

Нативные lazy objects в PHP

Ленивая загрузка в PHP до 8.4 жила на генерации кода. Doctrine отдавала из репозитория не сущность, а сгенерированного наследника, и за это приходилось платить: final на сущность не поставить, get_class() возвращает имя прокси, а в деплое живёт отдельный шаг, который эти классы порождает. В 8.4 всё это переехало в объектную модель.

Точек входа две, обе на ReflectionClass.

Первая создаёт ghost — объект нужного класса, у которого свойства пустые до первого обращения.

$reflector = new ReflectionClass(User::class);

$user = $reflector->newLazyGhost(function (User $user) use ($id, $persister): void {
$persister->loadById(['id' => $id], $user);
});


Возвращается настоящий User, так что instanceof и get_class() отвечают честно, а инициализатор при первом обращении заполняет тот же самый экземпляр.

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

$user = $reflector->newLazyProxy(
fn (User $user): User => $repository->find($id)
);


Объект просыпается от чтения или записи ленивого свойства, а также от ReflectionProperty::getValue() и setValue(). Вызов метода сам по себе триггером не считается, но внутри метода почти всегда читается свойство, и оно-то инициализацию и запускает. Из-за этого обычный getId() сходил бы в базу за значением, которое и так известно. Обходится это записью в обход ленивости:

$reflector->getProperty('id')
->setRawValueWithoutLazyInitialization($user, $id);


Doctrine раскладывает первичный ключ ровно так, сразу после создания ghost, и getId() после этого отвечает без похода в базу. Рядом лежат два флага для $options: SKIP_INITIALIZATION_ON_SERIALIZE не даёт сериализации разбудить объект, SKIP_DESTRUCTOR отменяет деструктор у экземпляра, который так и не проснулся. Плюс resetAsLazyGhost() и resetAsLazyProxy(), возвращающие в ленивое состояние уже существующий объект.

Ограничения простые: ленивым делается пользовательский класс или stdClass, на любом другом внутреннем классе прилетит Error. Объект без свойств ленивым не станет вовсе, откладывать в нём нечего.

В экосистеме всё уже на месте. Doctrine ORM умеет нативные объекты с версии 3.4, вышедшей летом 2025, а ORM 4.0 потребует PHP 8.4 и будет построена на них целиком. В Symfony LazyGhostTrait и LazyProxyTrait устарели с 7.3 и удалены в 8.0. На практике переключение выглядит как одна строчка в конфиге:

doctrine:
orm:
enable_native_lazy_objects: true


Старый ключ enable_lazy_ghost_objects при этом надо убрать.

#backendvkhub #php

Читать полностью…

Backend VK Hub

Типы индексов в PostgreSQL — не только btree

PostgreSQL знает шесть типов индексов. Большинство разработчиков всю жизнь работает с одним — btree. Остальные пять закрывают целые классы задач, где btree либо не работает, либо съедает в разы больше места.

В карточках пройдёмся по каждому — от базового btree до BRIN, который сжимает индекс на терабайтную таблицу до десятков килобайт.

#backendvkhub #postgresql

Читать полностью…

Backend VK Hub

📢 24 июля приглашаем на митап «Старый добрый Go в мире AI-агентов»

Как меняется роль Go с приходом AI-агентов? Где он остаётся незаменимым и где встраивается в новые сценарии?

Поговорим об этом 24 июля в офисе VK в Санкт-Петербурге. Своим опытом поделятся инженеры из VK, Фланта, h3llo cloud и MWS Cloud Platform.

Обсудим:
🔵как безопасно давать AI доступ к проду с помощью Go и MCP
🔵как AI-агенты работают с инфраструктурой данных
🔵как DRA-драйвер на Go меняет подход к работе с GPU
🔵действительно ли простота Go — преимущество или уже ограничение

Митап организован совместно с каналом IT-подкастов «Алло, Ада».

📍 24 июля, 18:30
Офис VK, Санкт-Петербург, бизнес-центр «У Красного моста», набережная реки Мойки, 73

Регистрация

До встречи!

#backendvkhub #meetup #go #митап

Читать полностью…

Backend VK Hub

ON CONFLICT DO SELECT в PostgreSQL 19: get-or-create одним запросом

Get-or-create в PostgreSQL до сих пор пишется в обход. Задача знакомая: нужен id тега, пользователя или ключа идемпотентности, и если строка уже есть, следует вернуть её, а если нет — создать. Очевидный вариант с DO NOTHING не подходит, потому что RETURNING отдаёт только вставленные строки, и на существующую запрос ответит пустым результатом.

INSERT INTO tags (name) VALUES ('postgres')
ON CONFLICT (name) DO NOTHING
RETURNING id; -- тег уже есть: 0 строк

Дальше обычно выбирают из двух неудобных вариантов. Второй SELECT означает лишний поход в базу и окно, за которое строку могут удалить. Холостой DO UPDATE SET name = EXCLUDED.name заставляет RETURNING работать, но PostgreSQL выполняет физическое обновление, даже если данные не изменились. Каждый вызов оставляет новую версию строки, мёртвый кортеж для VACUUM и запись в WAL, а если HOT не сработал, ещё и правки во всех индексах. Вдобавок срабатывают триггеры на UPDATE, и аудит фиксирует изменения, которых не было.

В PostgreSQL 19 появилось третье действие при конфликте — DO SELECT. Существующая строка возвращается как есть, без записи:
INSERT INTO tags (name) VALUES ('postgres')
ON CONFLICT (name) DO SELECT
RETURNING id, old.id IS NULL AS created;

Вторая колонка опирается на ссылку old в RETURNING, её добавили в версии 18. У вставленной строки старые значения равны NULL, у найденной заполнены, так что приложение сразу понимает, создало оно запись или получило существующую.

От DO NOTHING конструкция отличается двумя требованиями: RETURNING обязателен, а цель конфликта нужно назвать явно. Арбитром может быть только уникальный индекс или ограничение NOT DEFERRABLE, а exclusion constraints не подходят.

Если найденную строку дальше меняют в той же транзакции, её можно заблокировать в этом же запросе:
INSERT INTO balances (account_id, amount) VALUES (42, 0)
ON CONFLICT (account_id) DO SELECT FOR UPDATE
RETURNING *;

Поддерживаются все четыре режима блокировки, от FOR UPDATE до FOR KEY SHARE, и условие WHERE, по которому отбираются возвращаемые строки. Блокировку получат все конфликтующие строки, в том числе те, что под условие не попали.

PostgreSQL 19 сейчас в четвёртой бете, вышедшей 24 сентября, а релиз-кандидат обещают в начале октября. В этой бете из релиза убрали SQL/PGQ, FOR PORTION OF и операции над партициями, но DO SELECT в документации версии 19 остался. Холостые DO UPDATE в коде стоит пометить уже сейчас: после обновления каждый из них заменяется одной правкой, и таблицы с частым get-or-create перестанут копить мёртвые кортежи.

#backendvkhub #postgresql

Читать полностью…

Backend VK Hub

Сентябрь в бэкенде вышел плотным. JDK 27 поменял сборщик мусора для маленьких контейнеров, PostgreSQL 19 незадолго до релиза лишился нескольких громких фич, а PgBouncer, PHP и NGINX выпустили исправления уязвимостей, с которыми лучше не тянуть.

🔵JDK 27: G1 везде и компактные заголовки

JDK 27 вышел 15 сентября. Раньше при скромных ресурсах, вроде одного CPU и хипа меньше 1,8 ГБ, JVM по умолчанию брала Serial GC, а теперь по JEP 523 всегда выбирает G1, и сервисы в маленьких подах это заметят.

Компактные заголовки из JEP 534 включены по умолчанию: заголовок объекта сократился с 12 до 8 байт. TLS 1.3 теперь первым предлагает гибридный постквантовый обмен X25519MLKEM768, а JFR сам маскирует похожие на секреты значения в аргументах JVM и переменных окружения.

🔵PostgreSQL 19 Beta 4: откаты перед релизом

Четвёртая бета от 24 сентября убрала из 19 версии SQL/PGQ, включение контрольных сумм на лету, FOR PORTION OF для UPDATE и DELETE, а также MERGE и SPLIT PARTITIONS.

Большую часть этого списка мы упоминали в августе. На месте остались REPACK CONCURRENTLY, параллельный автовакуум и ON CONFLICT DO SELECT, а релиз-кандидат ждут в начале октября.

🔵PgBouncer 1.26: три DoS-уязвимости

Версия от 23 сентября закрывает CVE-2026-19888, CVE-2026-6668 и CVE-2026-6669, причём две первые эксплуатируются без аутентификации. Онлайн-рестарт через -R удалён, так что скрипты деплоя с этим флагом стоит проверить до обновления.

🔵PHP 8.6 RC2 и патчи четырёх веток

24 сентября вышел второй релиз-кандидат PHP 8.6, первый пропустили из-за ошибки упаковки. Финальная версия запланирована на 19 ноября и меняет умолчания сессий: use_strict_mode и cookie_httponly включены, а cookie_samesite получает значение Lax. В тот же день вышли security-релизы 8.5.11, 8.4.26, 8.3.35 и 8.2.34.

🔵NGINX: переполнение буфера в HTTP/3

Уязвимость CVE-2026-90439 затрагивает ngx_http_v3_module в версиях с 1.29.2 по 1.31.5. Исправление вышло 15 сентября в версиях 1.30.5 и 1.31.6.

🔵Go: мелкие аллокации быстрее

Go 1.27.1 вышел 1 сентября с исправлениями в компиляторе, рантайме и net/http. В блоге Go разобрали изменение из 1.27: аллокации до 80 байт идут через специализированные варианты mallocgc и работают быстрее вплоть до 20–30%, а в масштабе всей программы выигрыш доходит до 1%.

🔵Valkey 9.2 RC1: снапшоты без fork

В первом кандидате 9.2 от 16 сентября появились RDB-снапшоты без fork, они включаются параметром forkless-infrastructure-enabled. EXEC получил условия IFEQ, IFNE, NX и XX для оптимистичной блокировки без WATCH, а большие sorted set теперь хранятся в B+ дереве вместо skiplist.

🔵Rust: секреты в кеше CI

21 сентября команда Rust предупредила, что cargo miri сохранял в target/ все переменные окружения. Если target/ кешировался в GitHub Actions, секреты мог прочитать автор любого pull request. Исправление вошло в nightly от 22 сентября, но кеши стоит очистить, а секреты перевыпустить.

#дайджест #backendvkhub

Читать полностью…

Backend VK Hub

compact object headers по умолчанию: JDK 27 отдаёт обратно пятую часть кучи

Сервис на Spring держит в памяти десятки миллионов мелких объектов: элементы кеша, DTO из очереди, узлы деревьев. Куча упирается в четыре гигабайта, хотя полезных данных там заметно меньше. Разница уходит в заголовки: каждый объект на 64-битной JVM несёт 96 бит служебной информации, а без сжатых указателей на класс все 128.

Заголовок собран из двух слов. В mark word лежат сведения о блокировке, возраст для сборщика и вычисленный identity hash, в class word лежит указатель на метаданные класса. Для объекта с парой полей сопоставимы с размером самих данных.

JEP 534 в JDK 27, вышедшей 15 сентября 2026, делает компактную раскладку значением по умолчанию: оба слова сливаются в одно 64-битное, а указатель на класс сжимается до 22 бит. Путь у фичи долгий: эксперимент в JDK 24, продуктовая настройка в JDK 25, теперь поведение по умолчанию. Заявленная экономия составляет 10–20% живых данных. То, что раскладка включилась, видно прямо на старте:

$ java -XX:+PrintFlagsFinal -version | grep -i UseCompactObjectHeaders
bool UseCompactObjectHeaders = true {product lp64_product} {default}



➡️ Размер конкретного класса можно глянуть через JOL, он печатает раскладку полей вместе с выравниванием:

System.out.println(ClassLayout.parseClass(CacheEntry.class).toPrintable());

На классе с двумя ссылочными полями заголовок падает с 12 байт до 8, и экземпляр укладывается в 16 байт вместо 24: выравнивание по 8 байт добавляет к экономии ещё четыре.
Ограничение вытекает из тех самых 22 бит: в них помещается около четырёх миллионов классов.

Приложение, которое плодит классы на лету (динамические прокси, скриптовые движки, тяжёлая кодогенерация), способно упереться в предел, и тогда загрузка класса завершится OutOfMemoryError без автоматического отката на старую раскладку.

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

Отключается всё флагом -XX:-UseCompactObjectHeaders, но рассчитывать на него вдолгую не стоит: старую раскладку планируют объявить устаревшей и со временем убрать. В том же релизе поменялся и сборщик по умолчанию: JEP 523 назначает G1 стандартом во всех окружениях, включая маленькие контейнеры, где раньше выбирался Serial.

Сервисам на 25-й ту же экономию даёт -XX:+UseCompactObjectHeaders без смены мажорной версии. Перед включением на проде стоит посмотреть счётчик загруженных классов (jcmd <pid> VM.class_hierarchy или метрика jvm_classes_loaded) и прогнать нагрузочный тест с теми агентами, которые реально работают в бою. Если счётчик держится в десятках тысяч, потолок в четыре миллиона вас не касается.

А вы уже мерили, сколько кучи у вас занимают заголовки объектов?

#backendvkhub #jdk #java

Читать полностью…

Backend VK Hub

AOT-кеш в JVM: минус половина времени старта без GraalVM

Пока сервис деплоили раз в неделю, никого не волновало, что JVM стартует пять секунд. В контейнерах эти секунды всплывают постоянно: при каждом рестарте пода, при каждой новой реплике под нагрузкой, при каждом rolling update. Инстанс уже съедает память и процессор, но запросы ещё не принимает, а под всплеск трафика он доезжает до готовности как раз к тому моменту, когда пик прошёл и p99 просел.

Обиднее всего, что работа каждый раз повторяется впустую. JVM читает байт-код, парсит, верифицирует, загружает и связывает все классы, до которых дотягивается код. Для типичного Spring Boot это тысячи классов, и разбирает она их совершенно одинаково — просто нигде не запоминает.

Классическое решение — GraalVM Native Image. Проблему он вроде как закрывает, но просит взамен closed-world анализ, конфиги под каждую рефлексию, отдельный профиль сборки и отказ от JIT ради статического бинарника. На проекте с историей такой переезд легко растягивается на квартал — и не факт, что заканчивается успехом.

Project Leyden берёт дешевле: JVM остаётся обычной, а разбор классов уезжает в разогревочный прогон, например, на этап сборки образа. Результат ложится в файл, и в проде JVM просто читает готовое. Старт сокращается примерно вдвое, в коде не меняется ни строки.

В JDK 25 всё делается двумя командами. Раньше, когда механизм только появился в JDK 24, шагов было три — сначала записать конфигурацию, потом собрать из неё кеш, но эти два действия объединили:

# разогревочный прогон, на выходе -- app.aot
java -XX:AOTCacheOutput=app.aot -jar app.jar

# продакшен
java -XX:AOTCache=app.aot -jar app.jar

Со Spring Boot есть нюанс: обучающий прогон должен чем-то закончиться, иначе приложение просто останется работать и кеш никогда не соберётся. Поэтому его поднимают до готовности контекста и сразу гасят флагом -Dspring.context.exit=onRefresh.

Классами дело не ограничивается. В кеш попадают ещё и профили методов: на обучении JVM запоминает, что вызывалось чаще всего, а в проде JIT читает эту статистику сразу и берётся за горячие места, вместо того чтобы собирать её самому в первые минуты работы. Сервис быстрее выходит на полную скорость.

Цифры получаются такие: на живом приложении Spring Boot старт сократился с 4,9 до 2,4 секунды, на PetClinic ранние сборки давали 40–41%, разогрев ускоряется на 15–25%.

Взамен придётся следить за одной вещью: обучение и продакшен должны совпадать — тот же classpath, те же флаги, та же версия приложения. Разойдутся — JVM остановится с ошибкой, а не станет работать на устаревшем кеше. Поэтому сборку кеша обычно вешают на тот же шаг пайплайна, где собирается образ.

У тех, кто сидит на ZGC, всё это до недавнего времени не работало вовсе: сборщик прячет служебные данные прямо внутри ссылок на объекты, и кеш такого не переносил. Починили только в JDK 26.

Главное же отличие от Native Image в том, что Leyden ничего не забирает взамен. Рефлексия, динамическая загрузка классов и JIT остаются на месте. Если в проде что-то пойдёт не так, как на обучении, JVM спокойно доработает обычным способом — без падений и без необходимости заранее описывать в конфигах каждый возможный случай.

#backendvkhub #graalvm #jvm

Читать полностью…

Backend VK Hub

🔵Go 1.27: дженерик-методы и профиль утёкших горутин

Релиз вышел 19 августа, и главное в нём — методы наконец получили параметры типов. Дженерики в Go живут с 1.18, но методы всё это время оставались за бортом: параметризовать можно было функцию или тип целиком, а метод нет. Обходились через свободные функции с приёмником-параметром, что ломало цепочки вызовов и читалось плохо.

Второе по важности — профиль утёкших горутин, доехавший до стабильного состояния после эксперимента в 1.26. Он показывает горутины, которые навсегда заблокировались, потому что канал, мьютекс или sync.Cond, где они ждут, стал недостижим.

Диагностика тут всегда была мучительной: горутина висит, а место, где ошиблись с синхронизацией, отработало полчаса назад и следов не оставило. Теперь runtime/pprof отдаёт это отдельным профилем, и GoLand 2026.2 умеет его читать с первого дня.

Из стандартной библиотеки приехал encoding/json/v2 вместе с низкоуровневым encoding/json/jsontext. Старый encoding/json никуда не делся, но внутри теперь работает на движке v2 — ускорение разбора получают все, ничего не переписывая. Плюс в стандартную библиотеку добавили uuid: генерация идентификаторов перестала требовать внешней зависимости.

🔵Java 27: финальный RC и G1 везде

JDK 27 прошёл финальный релиз-кандидат 20 августа, релиз ожидается в середине сентября. Список фич заморожен ещё в июне, так что сюрпризов не будет.

Из того, что затронет всех: G1 становится сборщиком по умолчанию во всех окружениях. Раньше в стеснённых условиях — один процессор и меньше 1792 МБ памяти — JVM молча выбирала Serial GC. Теперь такого переключения нет, хотя сам Serial никуда не убрали и явный выбор через флаг работает как прежде.

Задевает это тех, кто крутит контейнеры с маленькими лимитами и не указывает сборщик руками: стоит прогнать бенчмарк на обоих вариантах, для совсем мелких heap Serial иногда выигрывает.

🔵PostgreSQL: 28 уязвимостей за один заход

13 августа обновились все поддерживаемые ветки — 18.6, 17.11, 16.15, 15.19 и 14.24. Закрыли 28 проблем с безопасностью и больше 110 багов.

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

Отдельно стоит посмотреть CVE-2026-14666 — кеширование row-level security игнорирует изменения ролей. Если мультитенантность построена на политиках, это ровно тот класс проблем, ради которого их и вводили.

В релизе отдельно оговорены индексы: GIN, btree_gist и ltree требуют проверки после апдейта. Что именно проверять, написано в release notes, и заглянуть туда лучше до окна обслуживания.

🔵Beta 3 и операции над партициями

Третья бета девятнадцатой вышла тем же числом. К уже известным SQL/PGQ и встроенному REPACK добавились операции над секциями прямо в ALTER TABLE: MERGE PARTITIONS склеивает несколько в одну, SPLIT PARTITIONS режет одну на несколько. Раньше такая перестройка означала новые таблицы, перенос данных и переключение через ATTACH/DETACH.

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

#backendvkhub #дайджест

Читать полностью…

Backend VK Hub

Тесты конкурентного кода, которые не флакают

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

Причина почти всегда во времени. Код опирается на time.Sleep , context.WithTimeout и time.AfterFunc, а реальные часы идут вперёд независимо от того, успела горутина или нет. Тест проверяет таймаут в пять секунд — значит, честно висит пять секунд и всё равно иногда не угадывает момент.

Go 1.25 вывел testing/synctest из экспериментального статуса и закрывает эту проблему на уровне рантайма. В пакете всего две функции.

synctest.Test запускает код в изолированном пузыре, где time работает на фальшивых часах: время стоит, пока хоть одна горутина может выполняться, и прыгает вперёд, когда все заблокированы.

func TestReadTimeout(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ch := make(chan int)
_, err := ReadWithTimeout(ch, 60*time.Second)
if err == nil {
t.Fatal("expected timeout, got nil")
}
})
}

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

Вторая функция нужна, когда в пузыре работают фоновые горутины. synctest.Wait блокируется, пока все они не окажутся заблокированы на канале, таймере или подобном:
func TestWorkerProcessesJobs(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
w := NewWorker()
go w.Run(t.Context())

w.Submit("a")
w.Submit("b")

synctest.Wait() // обе задачи разобраны

if got := w.Processed(); got != 2 {
t.Fatalf("processed = %d, want 2", got)
}
})
}

Без Wait пришлось бы ставить time.Sleep наугад и надеяться, что воркер успел, — ровно то, из-за чего тест и флакал. Детектор гонок про Wait знает, так что -race работает корректно.

Правок в коде обычно не требуется, но одна встречается регулярно. Если компонент внутри себя вызывает context.Background(), контекст создаётся вне пузыря и под фальшивые часы не попадает. Такие места переделывают так, чтобы контекст приходил снаружи и в тесте передавался t.Context().

Границы у пузыря жёсткие. Горутины, запущенные до synctest.Test или в init, живут по настоящим часам. Сторонние абстракции над временем не перехватываются — подменяется только стандартный time, так что самописный Clock придётся инжектить как раньше.

И ещё: synctest.Run из Go 1.24 объявлен устаревшим, а в 1.26 удалён — если пробовали пакет на экспериментальной стадии, вызовы нужно заменить на Test.

Начинать разумнее с того теста, который все обходят стороной: обернуть в synctest.Test, убрать time.Sleep в пользу Wait и прогнать тысячу раз.

#backendvkhub #go

Читать полностью…

Backend VK Hub

Мы начинаем 🚀

Читать полностью…

Backend VK Hub

Как правильно делать параллельные задачи с отменой в Go

Знакомая картина в хендлере Go-сервиса: пришёл запрос с userID, надо параллельно сходить в БД за пользователем, в Redis за сессией и во внешний API за биллингом. Первый рефлекс — три горутины и sync.WaitGroup. 

Работает, пока одна не падает: тогда WaitGroup ждёт остальные, они молотят вхолостую и возвращают никому не нужный результат. У sync.WaitGroup нет знания ни про ошибки, ни про отмену.

Правильный инструмент — golang.org/x/sync/errgroup. Обёртка над sync.WaitGroup, работающая с контекстом: первая ошибка отменяет остальные горутины через общий ctx, Wait() возвращает её.

func fetchUserData(ctx context.Context, userID int64) (*UserData, error) {
g, ctx := errgroup.WithContext(ctx)

var user *User
var session *Session
var billing *Billing

g.Go(func() (err error) { user, err = db.QueryUser(ctx, userID); return })
g.Go(func() (err error) { session, err = redis.GetSession(ctx, userID); return })
g.Go(func() (err error) { billing, err = billingAPI.GetBilling(ctx, userID); return })

if err := g.Wait(); err != nil {
return nil, err
}
return &UserData{user, session, billing}, nil
}

errgroup.WithContext возвращает Group и производный ctx. Первая g.Go, вернувшая ошибку, отменяет этот ctx. Остальные горутины видят отмену через свой ctx и завершаются — правильные библиотеки типа  database/sql, net/http, redis уважают контекст, ошибки быстро всплывают. g.Wait() возвращает первую пришедшую ошибку.

Второй сценарий — batch processing с ограничением параллелизма. Загрузить тысячу файлов не по одному, но и не все сразу — по десять в параллель:

func processFiles(ctx context.Context, paths []string) error {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10)

for _, path := range paths {
g.Go(func() error {
return processOne(ctx, path)
})
}
return g.Wait()
}

SetLimit(10) разрешает не больше десяти одновременных горутин (в x/sync v0.1 с конца 2022 года). g.Go при заполненном пуле блокируется до освобождения слота,  g.TryGo возвращает false без блокировки. С Go 1.22 переменная цикла path живёт per-iteration автоматически.

Пара нюансов

Отмена контекста не убивает горутины — они должны сами проверять ctx.Done() или использовать библиотеки, уважающие ctx. Бесконечный цикл без проверки не остановится. Панику errgroup не ловит — пробрасывает наверх и валит процесс.

Если функция может паниковать (парсинг чужого JSON, недоверенный ввод), оборачивайте тело самой горутины в recover(). Результаты пишутся в переменные без синхронизации — это норм, пока каждая горутина пишет в свою, а вот если несколько пишут в один слайс или map, нужен sync.Mutex.

Если в проекте есть sync.WaitGroup, плюс переменная-ошибка под sync.Mutex, плюс ручная отмена через context.CancelFunc — почти наверняка это errgroup.WithContext, лишний код уходит.

#backendvkhub #go

Читать полностью…

Backend VK Hub

📢 Неделя до «Митапа с двойной начинкой»!

26 августа собираем Go- и Java-разработчиков на мероприятие от VK Just Tech.

Вас ждут два трека выступлений, общение и, конечно, афтепати с квизом, шавермой и мастер-классом✨

➡️ В треке Go разберёмся в нестандартной компиляции для ускорения выкатки фичи, расскажем, как в VK выстроили единую платформу для разработки Go-сервисов и как устроена доставка информации об адресах через сетевую инфраструктуру.

➡️ В треке Java поговорим о перформанс-трюках в поисковой платформе, разберём архитектуру современного платёжного движка и обсудим переход от федерации кластеров хранения к мультитенантному решению.

📍 26 августа, Санкт‑Петербург, Крестовский остров, ресторан Royal Beach

Встречаемся в 17:00, начало в 18:00. 
Участие бесплатное, регистрация обязательна.

Зарегистрироваться

Ждём вас!

#backendvkhub #meetup #go #java #митап

Читать полностью…

Backend VK Hub

Gel — TypeScript для PostgreSQL

Запрос с тремя уровнями вложенности через ORM — это не один SQL, а 5–10 отдельных запросов. Prisma делает отдельные запросы для каждого уровня, SQLAlchemy требует joinedload() на каждую связь, Hibernate — FETCH JOIN. Если про один уровень забыли — N+1.

Gel (бывший EdgeDB) решает проблему на уровне СУБД. Это графово-реляционная БД, которая работает как слой поверх PostgreSQL. Создатели описывают соотношение так: «Gel to Postgres is what TypeScript is to JavaScript».

Что такое Gel

Вместо таблиц — объектные типы с наследованием и миксинами. Вместо внешних ключей — first-class links. Связи могут быть single или multi, required или optional, с exclusive-ограничением. Обратные ссылки (backlinks) позволяют навигировать в обе стороны без явных JOIN.

Пример схемы:

abstract type Person {
required name: str { constraint exclusive; };
}

type Hero extending Person {
secret_identity: str;
multi link villains := .<nemesis[is Villain];
}

type Movie {
required title: str;
multi characters: Person;
}

Схема описывается в .esdl-файлах. При изменении схемы gel migration create сам анализирует разницу и генерирует миграцию. Без рассинхронизации между ORM-моделями и реальной схемой.

EdgeQL: один запрос вместо N+1

EdgeQL-запрос с тремя уровнями вложенности компилируется в один SQL с JOIN.

select Movie {
title,
actors: { name, awards: { title, year } },
director: { name, birthplace }
} filter .title = "Inception"


Эквивалент в SQL потребовал бы 4–5 JOIN, а результат был бы плоским списком строк, который нужно собирать в объект на стороне приложения. ORM на такой глубине сделают 5–10 отдельных запросов. В EdgeQL нет ключевого слова JOIN — связи обходятся через links: Movie.actors.name.

Нет NULL

В EdgeQL нет NULL — только пустые множества {}. NULL = NULL → UNKNOWN, NULL + 5 → NULL, агрегаты игнорируют NULL — в SQL это источник постоянных багов. В EdgeQL каждый тип — это множество значений, и пустое множество явно обозначает отсутствие данных. Устраняет целый класс ошибок, связанных с трёхзначной логикой.

AI на уровне БД

Расширение ext::ai добавляет векторный поиск и RAG: embedding-индексы на основе OpenAI/Anthropic моделей, поиск по similarity, HTTP-эндпойнт /ai/rag. Поиск и генерация в одной транзакции, без отдельного векторного хранилища. Deferred index пересчитывается автоматически при изменении данных.

Ограничения

Gel требует изучения EdgeQL, и экосистема пока меньше, чем у Prisma или Drizzle. EdgeQL не поддерживает оконные функции и рекурсивные CTE (хотя в 6.0 можно использовать SQL напрямую). Serializable isolation only — узкое место при высокой конкурентности. Нет кеширования исполняемых планов запросов.

Попробуйте Gel на новом проекте со сложной моделью данных. Для всего остального есть PostgreSQL.

#backendvkhub #postgresql #gel

Читать полностью…

Backend VK Hub

Вам доклады по Go или Java? VK приглашает на «Митап с двойной начинкой»

📢 26 августа собираем Go- и Java-инженеров, чтобы прокачать знания в разработке и архитектуре высоконагруженных сервисов.

💙 Вас ждут: летний Санкт-Петербург, любимый стритфуд, феерический закат, два трека докладов от ведущих инженеров VK и приглашённых экспертов, квиз, мастер-класс и вечеринка!


Трек Go

🔹«Компиляция Go в динамическую либу, или Как мы ускорили выкатку фичи в два раза, но нам не понравилось», Михаил Кичигин, бэкенд-разработчик, Avito Tech
🔹«Go-платформа VK: как мы построили единую основу для создания Go-сервисов», Александр Апанасенко, руководитель команды SDK Backend, VK
🔹«Почтальон от Kubernetes до Palo Alto. Как осуществляется доставка информации об адресах», Дарья Кулькина, бэкенд-разработчик, Точка Банк


Трек Java

🔹«Перформанс-трюки в поисковой платформе Ozon», Пётр Портнов, ведущий разработчик среднего поиска, Ozon
🔹«Архитектура современного движка платёжной системы и переход к новой архитектуре», Игорь Крикунов, технический лидер разработки DBC, Альфа-Банк
🔹«Переход от федерации S3-кластеров в пользу коммунального и мультитенантного кластера», Кирилл Боблак, ведущий разработчик One-cloud, VK


💙 Афтепати 

🔹Квиз «Что? Где? Кебаб?», чтобы окончательно прожарить мозги на технических вопросах
🔹Фуд-корнер
🔹Мастер‑класс, который сделает вас звездой любой вечеринки

📍 26 августа, Санкт‑Петербург, Крестовский остров, ресторан Royal Beach

Встречаемся в 17:00, начало в 18:00. 
Участие бесплатное, регистрация обязательна.

💙 Ждём вас!


#backendvkhub #meetup #go #java #митап

Читать полностью…

Backend VK Hub

VK и JUG Ru Group изучили, какими инструментами сейчас пользуются Java-разработчики. Главный вывод: команды активно внедряют ИИ, но не спешат менять проверенный стек.

Что показало исследование
➡️ Java 21 стала основной версией языка — её используют две трети разработчиков
➡️ 95% специалистов работают с ИИ, больше половины — каждый день
➡️ Spring Boot, PostgreSQL и Kafka остаются основой большинства проектов
➡️ Агентные ИИ-инструменты пока не стали массовыми

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

Полные результаты исследования

#backendvkhub #java #исследование

Читать полностью…

Backend VK Hub

HOT updates и FILLFACTOR — как избежать bloat на UPDATE-heavy таблицах

В PostgreSQL каждый UPDATE из-за MVCC создаёт новую версию строки, а старая помечается как dead и живёт до VACUUM. Плюс обновляются все индексы, даже те, где изменённое поле не участвует. На UPDATE-heavy таблицах это даёт bloat и постоянную работу autovacuum. Есть механизм, пропускающий эту работу, — HOT update, heap-only tuple. Работает не всегда, но настраивается одним параметром.

Условий два: новая версия помещается на ту же страницу, что и старая, и ни одно индексируемое поле не менялось. При этом PostgreSQL пишет новую версию рядом со старой на той же странице и ставит указатель со старой на новую. Индексы остаются — они по-прежнему указывают на «голову цепочки», живая версия достаётся за один hop. VACUUM собирает dead tuples внутри страницы без полного прохода.

Первое условие обеспечивает FILLFACTOR — процент заполнения страницы при вставке. По умолчанию 100, страницы заполняются под пробку. Для read-only это оптимально, для UPDATE-heavy ломает HOT: свободного места нет, новая версия уходит на другую страницу, индексы правятся.

Настраивается при создании таблицы или через ALTER.

CREATE TABLE user_stats (
user_id bigint PRIMARY KEY,
last_seen timestamptz,
view_count int
) WITH (fillfactor = 80);

ALTER TABLE user_stats SET (fillfactor = 80);


После ALTER существующие страницы перезаполнятся при VACUUM FULL. На проде без окна обслуживания — pg_repack, без блокировки.

Проверяется через pg_stat_user_tables:

SELECT relname, n_tup_upd, n_tup_hot_upd,
round(100.0 * n_tup_hot_upd / NULLIF(n_tup_upd, 0), 1) AS hot_ratio
FROM pg_stat_user_tables
WHERE n_tup_upd > 0
ORDER BY n_tup_upd DESC;


hot_ratio показывает долю UPDATE, прошедших как HOT. Целевое значение на UPDATE-heavy таблицах — 80% и выше. При низком ratio первое, что стоит проверить, — не FILLFACTOR, а второе условие: какие индексы задевают изменяемые поля.

Второе условие ломается чаще. Типичный пример — составной индекс (user_id, updated_at) на таблице, где UPDATE меняет updated_at. Каждый UPDATE трогает индекс, HOT не работает, hot_ratio близок к нулю. Варианта два: убрать updated_at из индекса, если он не даёт выигрыша на чтении, либо разнести схему — собрать часто меняющиеся поля в отдельную таблицу без лишних индексов. Второй подход — стандартный для горячих счётчиков.

FILLFACTOR понижают на таблицах с регулярными UPDATE: счётчики, статистика, presence, состояние сессий. Значения 70–80 покрывают большинство случаев. На read-heavy таблицах менять не нужно.

Порядок действий на таблице с подозрением на bloat: посмотреть hot_ratio, при низком — проверить индексы на изменяемых полях, при необходимости почистить лишние и понизить FILLFACTOR. Работы немного, эффект на write-нагрузку и autovacuum обычно ощутимый.

А вы у себя FILLFACTOR настраивали на UPDATE-heavy таблицах?

#backendvkhub #postgresql

Читать полностью…

Backend VK Hub

Очередь задач в PostgreSQL без Redis и гонок

Пять воркеров разгребают задачи. Первое, что приходит: SELECT с pending, потом UPDATE в processing. Работает, пока воркеры не начинают хватать одну строку. Задача выполняется дважды, метрики летят, начинается ад.

Классика — Redis, RabbitMQ, Kafka. Но если PostgreSQL в стеке, всё закрывается одной конструкцией из PG 9.5: SELECT ... FOR UPDATE SKIP LOCKED. На ней построены Sidekiq, PG-boss, GoodJob, Solid Queue, River.

Таблица jobs:

CREATE TABLE jobs (
id bigserial PRIMARY KEY,
payload jsonb NOT NULL,
status text NOT NULL DEFAULT 'pending',
created_at timestamptz NOT NULL DEFAULT now()
);


Воркер забирает следующую задачу так:

BEGIN;

WITH next AS (
SELECT id FROM jobs
WHERE status = 'pending'
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1
)
UPDATE jobs
SET status = 'processing'
WHERE id IN (SELECT id FROM next)
RETURNING *;

COMMIT;


Основная пара — FOR UPDATE SKIP LOCKED. Первое берёт row-level lock, второе — если строка заблокирована, не ждём, берём следующую. Воркеры не толкаются, каждый забирает свою задачу. CTE с UPDATE атомарно вытаскивает строку и меняет статус в одной короткой транзакции, иначе lock снимется только на COMMIT.

Если задачи лёгкие, меняем LIMIT 1 на LIMIT 10 — воркер забирает пачку, сеть до базы окупается на пачке.

О чём часто забывают

Без индекса SELECT превращается в seq scan, и очередь встаёт на минимальной нагрузке. Partial index на pending — то, что нужно:

CREATE INDEX idx_jobs_pending 
ON jobs (id)
WHERE status = 'pending';


Индекс маленький — только необработанные задачи. После смены статуса строка выпадает из индекса, bloat не копится.

Второе — воркер может упасть посреди работы. Задача останется в processing навсегда. Решение — lease pattern: колонка taken_at и крон, возвращающий застрявшее в pending:

UPDATE jobs 
SET status = 'pending', taken_at = NULL
WHERE status = 'processing'
AND taken_at < now() - interval '10 minutes';


Retry живёт в колонке attempts с лимитом попыток.

Транзакция с FOR UPDATE держит lock до COMMIT, закрываем быстро. Работа с payload идёт после, финальный статус фиксируем отдельным UPDATE. Приоритеты через ORDER BY, отложенные задачи через WHERE scheduled_at <= now().

Где это перестаёт работать? На миллионах задач в час PostgreSQL страдает от write amplification: несколько UPDATE на задачу, autovacuum не успевает, копится bloat. Тогда пора на Kafka. Но для сотен тысяч в час SKIP LOCKED работает отлично: транзакционность, retry, приоритеты, delayed jobs — всё в одной базе рядом с прикладными данными.

Если PostgreSQL в стеке и объёмы разумные, SKIP LOCKED закрывает большинство сценариев без брокера. Не зря на нём построены Sidekiq, PG-boss, GoodJob, Solid Queue, River и куча самописных очередей, которые работают годами.

#backendvkhub #postgresql

Читать полностью…

Backend VK Hub

Мало кто помнит про xid wraparound. А зря

Есть аварии в PostgreSQL, которые случаются раз в несколько лет. База останавливает запись, чинить приходится через postgres --single. Wraparound из этой оперы: штука узкая, тихая, последствия громкие.

У транзакций есть номер xid: 32 бита, 4 миллиарда значений. Реально используется половина: MVCC сравнивает xid через круговую арифметику, чтобы понять, в прошлом строка относительно текущей транзакции или в будущем. Такое сравнение работает только в пределах 2 миллиардов.

А что случится, когда счётчик пройдёт границу?

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

Спасёт VACUUM FREEZE. Он проходит по старым строкам и меняет их xmin на замороженный xid, который всегда воспринимается как «в далёком прошлом», независимо от счётчика. Freeze нужен не для места на диске, а для сохранности данных: VACUUM освобождает место, VACUUM FREEZE защищает от wraparound.

Autovacuum запускает freeze при достижении autovacuum_freeze_max_age, по умолчанию 200 миллионов транзакций от старейшего замороженного xid таблицы. С PG 12 freeze легче, раньше он блокировал DDL. Но на терабайтных таблицах всё равно идёт долго.

Если приложение генерирует транзакции быстрее, чем autovacuum морозит, счётчик подходит к критической зоне. За 40 миллионов до конца PG отправляет в логи database is not accepting commands to avoid wraparound data loss. За 3 миллиона переходит в single-user mode: клиентам ошибка, база стоит, поддержка получает много звонков.

Что смотреть в проде?

Возраст xid по базам:

SELECT datname, age(datfrozenxid) AS age
FROM pg_database
ORDER BY age DESC;


И отдельно по самым старым таблицам — обычно именно там всё копится:

SELECT relname, age(relfrozenxid) AS age
FROM pg_class
WHERE relkind IN ('r', 'm')
AND age(relfrozenxid) > 100000000
ORDER BY age DESC;


age() показывает разрыв между текущим xid и старейшим замороженным. 200 миллионов норма, autovacuum вот-вот сработает. 500 — стоит разбираться, почему не справляется. Миллиард — руками звать VACUUM FREEZE. Полтора — паника. Мониторинг обязателен на любой нагруженной базе.

Есть побочка, парализующая autovacuum. Длинные транзакции блокируют freeze: старейший активный xid определяет, что можно морозить. Один забытый BEGIN у аналитика — и в понедельник age за трое суток удвоился, freeze не сдвинулся. Мониторинг xact_start из pg_stat_activity идёт в паре с возрастом xid.

Полный фикс — XID64, 64-битный счётчик. В mainstream пока не приземлился, живём с 32-битным и следим.
Wraparound редко случается в маленьких проектах, но с ростом transaction rate вероятность растёт нелинейно. Проспать алерт один раз — и в лучшем случае теряешь час на single-user, в худшем ищешь причину, почему автовакуум встал.

А у вас в мониторинге стоит алерт на возраст xid?

#backendvkhub #postgresql

Читать полностью…
Subscribe to a channel