-
Комьюнити VK для бэкендеров. Cамые хардовые кейсы, дискуссии в кругу своих и прямой доступ к нашим экспертам 😎
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-моделями и реальной схемой.select Movie {
title,
actors: { name, awards: { title, year } },
director: { name, birthplace }
} filter .title = "Inception"links: Movie.actors.name.{}. NULL = NULL → UNKNOWN, NULL + 5 → NULL, агрегаты игнорируют NULL — в SQL это источник постоянных багов. В EdgeQL каждый тип — это множество значений, и пустое множество явно обозначает отсутствие данных. Устраняет целый класс ошибок, связанных с трёхзначной логикой.ext::ai добавляет векторный поиск и RAG: embedding-индексы на основе OpenAI/Anthropic моделей, поиск по similarity, HTTP-эндпойнт /ai/rag. Поиск и генерация в одной транзакции, без отдельного векторного хранилища. Deferred index пересчитывается автоматически при изменении данных.
Вам доклады по 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 #митап
VK и JUG Ru Group изучили, какими инструментами сейчас пользуются Java-разработчики. Главный вывод: команды активно внедряют ИИ, но не спешат менять проверенный стек.
Что показало исследование
➡️ Java 21 стала основной версией языка — её используют две трети разработчиков
➡️ 95% специалистов работают с ИИ, больше половины — каждый день
➡️ Spring Boot, PostgreSQL и Kafka остаются основой большинства проектов
➡️ Агентные ИИ-инструменты пока не стали массовыми
Получается интересная картина: новые инструменты быстро входят в ежедневную практику, а фундамент технологического стека остаётся прежним.
🚀 Java меняется, но без революций.
Полные результаты исследования
#backendvkhub #java #исследование
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);
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 из индекса, если он не даёт выигрыша на чтении, либо разнести схему — собрать часто меняющиеся поля в отдельную таблицу без лишних индексов. Второй подход — стандартный для горячих счётчиков.hot_ratio, при низком — проверить индексы на изменяемых полях, при необходимости почистить лишние и понизить FILLFACTOR. Работы немного, эффект на write-нагрузку и autovacuum обычно ощутимый.
Очередь задач в 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 — воркер забирает пачку, сеть до базы окупается на пачке.CREATE INDEX idx_jobs_pending
ON jobs (id)
WHERE status = 'pending';
processing навсегда. Решение — lease pattern: колонка taken_at и крон, возвращающий застрявшее в pending:UPDATE jobs
SET status = 'pending', taken_at = NULL
WHERE status = 'processing'
AND taken_at < now() - interval '10 minutes';
attempts с лимитом попыток.FOR UPDATE держит lock до COMMIT, закрываем быстро. Работа с payload идёт после, финальный статус фиксируем отдельным UPDATE. Приоритеты через ORDER BY, отложенные задачи через WHERE scheduled_at <= now().
Мало кто помнит про 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. Полтора — паника. Мониторинг обязателен на любой нагруженной базе.BEGIN у аналитика — и в понедельник age за трое суток удвоился, freeze не сдвинулся. Мониторинг xact_start из pg_stat_activity идёт в паре с возрастом xid.
LISTEN/NOTIFY — встроенный pub/sub в PostgreSQL. Если в проекте уже стоит PG, для уведомлений между процессами не нужны Redis или RabbitMQ — обходимся двумя SQL-командами. Cache invalidation, realtime-обновления через WebSocket, координация воркеров — всё закрывается без дополнительной инфраструктуры.
Базовое:
— в одной сессии
LISTEN cache_invalidate;
— в любой другой сессии (или из триггера)
NOTIFY cache_invalidate, '{"key":"user:123"}';
pg_notify() то же самое, но удобнее в триггерах с динамическим payload:SELECT pg_notify('cache_invalidate', json_build_object('key', 'user:' || id)::text)
FROM updated_rows;CREATE OR REPLACE FUNCTION notify_user_change()
RETURNS trigger AS $$
BEGIN
PERFORM pg_notify(
'user_changed',
json_build_object('id', NEW.id, 'op', TG_OP)::text
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER user_changes
AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW EXECUTE FUNCTION notify_user_change();
users шлёт уведомление подписчикам — идеально для инвалидации кеша или WebSocket-бэкенда.pg:client.on('notification', (msg) => {
invalidateCache(JSON.parse(msg.payload));
});
await client.query('LISTEN cache_invalidate');pgx.WaitForNotification, в Python — psycopg.notifies после autocommit=True.
Cassandra как метастор для S3: как мы пришли к локальному скану и Kafka
В VK работает собственная S3-совместимая реализация — one-object-storage. Метаданные объектов лежат в Cassandra, и на масштабе в миллиарды объектов простая база ловит сразу несколько архитектурных ловушек.
В карточках рассмотрим, что хранит метастор, почему партиционирование выстрелило, что такое tombstones и почему перестало хватать договорённостей с клиентами, как пришли к локальному скану и переезду очереди на Kafka.
И это только часть истории. В полной статье на Хабре — про фильтр Блума для чистки пустых директорий (потому что локально определить пустоту нельзя — директория и её содержимое лежат на разных хостах), snapshot-статистику в реальном времени для ограничения размера бакета, и почему обычные снапшоты не покрывают всю задачу.
Цифры в проде: 14 млн RPM на скан 18 хостов, до 1.1 млн RPM на чистку в пике.
#backendvkhub #s3 #kafka #cassandra #статья #кейс
LATERAL JOIN в PostgreSQL — то, что вы пропустили, но используете каждый день
LATERAL JOIN — конструкция, о которой большинство узнаёт случайно, лет через пять работы с SQL. До этого решают те же задачи через коррелированные подзапросы или window functions, иногда теряя производительность в разы.
Суть простая: LATERAL разрешает использовать в правой части JOIN колонки из левой таблицы.
Без LATERAL такое не работает:
SELECT u.name, p.title
FROM users u
JOIN (
SELECT * FROM posts
WHERE author_id = u.id LIMIT 3
) p ON true;
-- ERROR: invalid reference to FROM-clause entry for table "u"
JOIN по дефолту. Без LATERAL правая часть выполняется один раз, до соединения, и понятия не имеет про u.id.LATERAL подзапрос видит контекст:SELECT u.name, p.title
FROM users u
LEFT JOIN LATERAL (
SELECT title
FROM posts
WHERE author_id = u.id
ORDER BY created_at DESC
LIMIT 3
) p ON true;
u.id. На выходе — каждый пользователь и его три последних поста. ON true — формальность, условие соединения уже внутри.LATERAL, — топ-N по группе. Та же задача через ROW_NUMBER пишется чуть короче, но работает иначе:WITH ranked AS (
SELECT *, ROW_NUMBER() OVER (
PARTITION BY author_id
ORDER BY created_at DESC
) AS rn
FROM posts
)
SELECT u.name, p.title
FROM users u
JOIN ranked p ON p.author_id = u.id AND p.rn <= 3;
posts целиком — окно строится для всей таблицы, фильтр по rn срабатывает уже после. LATERAL с индексом на (author_id, created_at DESC) читает по три строки на каждого пользователя через index scan. На таблицах в десятки миллионов записей разница на порядки — десятки миллисекунд против минут.SELECT
o.id AS order_id,
item->>'product_id' AS product_id,
(item->>'qty')::int AS qty,
p.name
FROM orders o
CROSS JOIN LATERAL jsonb_array_elements(o.items) AS item
JOIN products p ON p.id = (item->>'product_id')::int;
jsonb_array_elements — set-returning функция, она и так неявно подразумевает LATERAL, но писать его явно — хороший тон: читатель сразу видит, что выражение справа зависит от строки слева. CROSS JOIN LATERAL от LEFT JOIN LATERAL отличается тем, что заказы без позиций просто не попадут в результат.SELECT. LATERAL не делает запрос быстрее по умолчанию, но на правильном паттерне переписывает CTE с window functions в 2–5 раз быстрее.
Современный взгляд на паттерны проектирования
Культовая книга Design Patterns «Банды четырёх» вышла в 1994 году. Многие паттерны из неё уже встроены в языки и фреймворки, а вместе с классическими появились паттерны распределённых систем. Объясняем в карточках современный взгляд на паттерны проектирования.
#backendvk #designpatterns
Классический сценарий в проде: запрос не меняли, данные те же, а время выросло в разы — и не сразу, а после некоторого числа выполнений.
Причина почти всегда в том, как PostgreSQL кэширует планы за параметризованными запросами.
#backendvkhub #postgresql
До 18-й версии PostgreSQL читал страницы с диска синхронно: на каждый промах кеша вызывался блокирующий pread(), бэкенд останавливался и ждал ответ ядра. На AWS EBS gp3 это около 1–2 мс на блок, и большое последовательное сканирование упиралось не в диск, а в накопленное ожидание. Спасались привычным набором: увеличенный shared_buffers, parallel workers, реплики на чтение.
В 18 появилась подсистема AIO и параметр io_method с тремя значениями: sync, worker (по умолчанию), io_uring. Параметр требует рестарта.
io_method = io_uring
io_workers = 3 # учитывается только для worker
effective_io_concurrency = 16 # дефолт в 18, было 1
io_combine_limit = 128kB
sync повторяет поведение 17 — для отката, если новая подсистема даёт регрессию. worker поднимает фоновые процессы, принимающие read-запросы через shared memory; бэкенд кладёт пачку запросов и продолжает обрабатывать предыдущие страницы. Издержки — context switch и конкуренция за очередь. io_uring обращается к ядру напрямую через ring buffer Linux 5.1+, без syscall на каждый блок; требует сборки с --with-liburing и kernel.io_uring_disabled = 0.sync на io_uring. На локальном NVMe эффект скромнее: BetterStack получили 24% (2913 → 2221 мс), PlanetScale на своих Metal-серверах разницы между worker и io_uring практически не увидели, диск перестал быть узким местом.pg_get_aios() возвращает все запланированные операции с их состоянием:
FROM pg_get_aios();
pg_stat_io добавились разрезы по асинхронным чтениям — видно, сколько байт прошло мимо синхронного пути и сколько времени ушло на ожидание completion.EXPLAIN ANALYZE может занижать I/O time, потому что часть работы делается в воркерах и backend этого не видит, — для диагностики берите pg_stat_io. effective_io_concurrency теперь напрямую управляет числом параллельных read-ahead запросов: на network storage его имеет смысл повышать до 32–64, на локальном NVMe оптимум обычно меньше.io_method = sync — их prefetch-механика интегрирована с собственным storage layer и переписывается под новый интерфейс отдельно. Если обновляетесь на 18 на своих серверах, начните с worker, снимите профиль через pgbench и pg_stat_io, и только потом переключайтесь на io_uring, если у storage остался запас.
Джависты, помогите по-братски примите участие в большом исследовании! 💙
Вместе с JUG Ru Group составляем полную картину современной Java-разработки. Пожалуйста, пройдите опрос, он займёт не больше 20 минут. По итогам мы сделаем большой отчёт и поделимся результатами с вами!
P. S. Среди участников опроса JUG Ru Group разыграет 5 офлайн- и 10 онлайн-билетов на свои конференции. Ваш шанс 😏
Инвалидация кеша: скорость доступа 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 #кеш #архитектура
🔵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 #дайджест
Нативные 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() отвечают честно, а инициализатор при первом обращении заполняет тот же самый экземпляр. $user = $reflector->newLazyProxy(
fn (User $user): User => $repository->find($id)
);
ReflectionProperty::getValue() и setValue(). Вызов метода сам по себе триггером не считается, но внутри метода почти всегда читается свойство, и оно-то инициализацию и запускает. Из-за этого обычный getId() сходил бы в базу за значением, которое и так известно. Обходится это записью в обход ленивости: $reflector->getProperty('id')
->setRawValueWithoutLazyInitialization($user, $id);getId() после этого отвечает без похода в базу. Рядом лежат два флага для $options: SKIP_INITIALIZATION_ON_SERIALIZE не даёт сериализации разбудить объект, SKIP_DESTRUCTOR отменяет деструктор у экземпляра, который так и не проснулся. Плюс resetAsLazyGhost() и resetAsLazyProxy(), возвращающие в ленивое состояние уже существующий объект.stdClass, на любом другом внутреннем классе прилетит Error. Объект без свойств ленивым не станет вовсе, откладывать в нём нечего.LazyGhostTrait и LazyProxyTrait устарели с 7.3 и удалены в 8.0. На практике переключение выглядит как одна строчка в конфиге:doctrine:
orm:
enable_native_lazy_objects: true
enable_lazy_ghost_objects при этом надо убрать.
Типы индексов в PostgreSQL — не только btree
PostgreSQL знает шесть типов индексов. Большинство разработчиков всю жизнь работает с одним — btree. Остальные пять закрывают целые классы задач, где btree либо не работает, либо съедает в разы больше места.
В карточках пройдёмся по каждому — от базового btree до BRIN, который сжимает индекс на терабайтную таблицу до десятков килобайт.
#backendvkhub #postgresql
📢 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 #митап
PostgreSQL JSONB — операторы, индексы и когда они не нужны
JSONB в PostgreSQL давно стал стандартом для гибких схем — настройки, метаданные, поля, которые не фиксируются заранее. Большинство ограничивается data->>'field' и на этом останавливается. На деле там полноценный язык запросов и пара видов индексов, которые делают поиск по JSONB сравнимым по скорости с поиском по обычным колонкам.
В карточках разберёмся с операторами, поиском по структуре, JSONPath и индексами, а в конце узнаем, когда JSONB вообще не стоит брать.
#backendvkhub #postgresql
🔵Postgres Pro Standard 18.4.1 — встроенная отказоустойчивость
Технология BiHA на базе Raft стала доступна в Standard-редакции, автоматизируя репликацию и failover. Ещё один шаг к снижению сложности эксплуатации критичных PostgreSQL-кластеров.
🔵nenya — AI Gateway на Go
Появился лёгкий open-source шлюз для маршрутизации и контроля запросов к LLM-провайдерам. Формируется отдельный класс инфраструктурных решений для управления AI-трафиком и политиками безопасности.
🔵jqwik и prompt injection — новый класс рисков
Автор фреймворка намеренно добавил в релиз скрытую инструкцию для ИИ-агентов, вызвав дискуссию о безопасности зависимостей в эпоху агентской разработки. Supply-chain риски начинают распространяться не только на код, но и на взаимодействие с агентами.
🔵Spring Boot 3.5 теперь без open-source поддержки
С 30 июня 2026 года Spring Boot 3.5 прекратил получать публичные исправления безопасности и багфиксы, что создаёт риски для команд, использующих этот стек. Пора обновляться до Spring Boot 4.
🔵Утечка данных 10,9 млн клиентов из-за пропавшего бэкапа
Японская энергетическая компания потеряла HDD с резервной копией клиентских данных. История напоминает, что надёжный бэкап без контроля хранения и шифрования не гарантирует безопасность.
📌 Новые статьи от инженеров VK на Хабре
PostgreSQL не тормозит. Почему мы перестали масштабировать базу данных и начали масштабировать архитектуру
Может ли Service сломать ваш K8s кластер?
Как мы тестировали Tarantool Database на 640 инстансов
#дайджест #backendvkhub
Как читать планы PostgreSQL и не дёргать DBA
EXPLAIN — встроенная команда PostgreSQL для разбора планов запросов. Большинство знают, что она есть, но реально читать вывод — навык, который приходит с практикой. Разберём, как читать план, на что смотреть в первую очередь и какие сигналы скрываются за безобидным Seq Scan.
Три режима:
EXPLAIN SELECT * FROM users WHERE email = '...';
-- только план, запрос не выполняется
EXPLAIN ANALYZE SELECT * FROM users WHERE email = '...';
-- план + реальное выполнение (запрос ВЫПОЛНИТСЯ)
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users WHERE email = '...';
-- + сколько страниц прочитано из кеша и с диска
BEGIN ... ROLLBACK.Limit (cost=0.43..8.45 rows=1)
(actual time=0.234..0.234 rows=1 loops=1)
Buffers: shared hit=4
-> Index Scan using users_email_idx on users
(cost=0.43..8.45 rows=1)
Buffers: shared hit=4
Planning Time: 0.124 ms
Execution Time: 0.279 ms
cost (оценка планировщика) и actual time (реальное время). actual rows (факт). Если планировщик ожидал 10 строк, а получил 100 000, он выбрал план под маленькую выборку (обычно Nested Loop) и просел на больших данных:Nested Loop (rows=10, actual rows=100000)
ANALYZE table_name для пересбора статистики или default_statistics_target = 1000 для более детальной выборки.Buffers: shared hit=42 read=128. hit — страницы из shared buffers, read — с диска. Чем выше read, тем медленнее запрос.Seq Scan на большой таблице с фильтром — нет индекса или планировщик решил, что он не нужен. Hash Join с Batches > 1 означает, что хеш-таблица не поместилась в work_mem. Sort с пометкой external merge Disk — память кончилась, сортировка ушла на диск. В двух последних случаях поднимаем work_mem для сессии.LOAD 'auto_explain';
SET auto_explain.log_min_duration = 1000;
SET auto_explain.log_analyze = true;
pg_stat_statements через EXPLAIN ANALYZE — обычно там пара-тройка запросов с расходящейся статистикой, которые после правки уносят 80% нагрузки.
До Go 1.25 GOMAXPROCS по умолчанию равнялся runtime.NumCPU(). В контейнере это количество CPU ноды, а не пода. Планировщик получал параллелизм на 32 ядра, cgroup разрешал 2. CFS выжигал квоту за долю периода и троттлил cgroup до следующего окна.
Коротко о модели: M/P/G
Три сущности в планировщике: M (поток ОС), P (токен выполнения), G (горутина). GOMAXPROCS задаёт число P, то есть сколько горутин могут выполняться параллельно. M без P не запускает Go-код. Выставить GOMAXPROCS = 32 на контейнере с лимитом 2 CPU — значит попросить планировщик выполнить работу, которую cgroup не пустит.
Почему страдает P99, а не средний RPS
CFS работает с полосой пропускания CPU через период/квоту. Контейнер с limits.cpu: 2000m получает 2 CPU на каждый период. Если Go стартует 32 P и планирует работу 32 потоков, то квота сгорает быстро и дальше cgroup простаивает до следующего периода.
Троттлинг бьёт по P95/P99/P99.9, а не по средней пропускной способности. Горутина с состоянием приложения держит очередь выполнения, за ней стоят другие. Среднее RPS почти не страдает, но хвост распределения уползает вправо.
automaxprocs как костыльuber-go/automaxprocs делал ровно одно: выставлял GOMAXPROCS = cpu.cfs_quota_us / cpu.cfs_period_us. Решение стало де-факто стандартом для Go-сервисов в Kubernetes. Работало, но требовало зависимости в каждом сервисе и не ловило изменение лимита на лету.
Что сделал Go 1.25
Рантайм читает cgroup при старте и вычисляет min(visible CPUs, effective cgroup limit). Для cgroups v2 это cpu.max, для v1 — cpu.cfs_quota_us и cpu.cfs_period_us. Файловые дескрипторы остаются открытыми, значения лимитов периодически перечитываются, а GOMAXPROCS при необходимости пересчитывается без рестарта процесса.
CFS лимит задаёт среднюю пропускную способность, а не число ядер. Контейнер с 1500m имеет право на 1.5 CPU в среднем. Рантайм округляет вверх: ceil(1.5) = 2. Это смещение в сторону пропускной способности, что для ultra-low-latency сервисов может оказаться избыточным.
В крупных кластерах фича убирает класс случайных троттлинг-выбросов: поды перестают наследовать масштаб параллелизма хоста по умолчанию.
Явный вызов GOMAXPROCS полностью отключает автоматическое поведение. Для раздельного управления есть GODEBUG-флаги:
GODEBUG=containermaxprocs=0 # выключить чтение cgroup
GODEBUG=updatemaxprocs=0 # выключить динамическое обновление
limits.cpu, не requests. Под без лимита ведёт себя как обычный процесс на хосте. NUMA, cpuset pinning, CPU Manager static — вне зоны ответственности. Квоту рантайм вычитает один раз при старте, миграцию процесса в другой cgroup он не замечает.GOMAXPROCS = requests.cpu и requests.cpu × 2 являются исключительно эмпирическими, официальной поддержки таких механизмов в рантайме нет. Kubernetes requests — это сигнал для планирования, а не принудительное ограничение.GOMAXPROCS проблему не закрывает.automaxprocs можно удалить. GOMAXPROCS превратился из обязательной K8s-настройки в экспертное переопределение для тюнинга по результатам профилирования.
🔵 Incus 7.0 LTS — контейнеры и VM до 2031 года
Вышел новый LTS-релиз платформы управления контейнерами и виртуальными машинами. Среди ключевых изменений — поддержка OCI-контейнеров, встроенное S3-хранилище, новые драйверы хранения и отказ от устаревших cgroupv1 и iptables.
🔵 Bumblebee — защита AI-разработки от supply-chain атак
Perplexity открыла исходный код сканера, который анализирует зависимости npm, PyPI и Go Modules без выполнения кода. Инструмент помогает выявлять риски в AI- и developer-инфраструктуре ещё до попадания вредоносных пакетов в пайплайны.
🔵 JavaOne 2026 — курс на HTTP/3 и современную многопоточность
На конференции показали ключевые изменения JDK 26: поддержку HTTP/3, развитие Structured Concurrency и новые возможности языка. Основной фокус — производительность, сетевые приложения и упрощение конкурентного программирования.
🔵 JEP 533 — Structured Concurrency становится всё ближе к релизу
API для управления группами связанных задач вышло на очередной этап превью. Подход упрощает отмену операций, обработку ошибок и делает многопоточный код заметно предсказуемее.
🔵 JEP 534 — компактные заголовки объектов в HotSpot
OpenJDK планирует включить Compact Object Headers по умолчанию. Изменение уменьшает потребление памяти и может дать дополнительный выигрыш в производительности за счёт лучшей работы процессорного кэша.
🔵 Go SIM DB — база данных для AI-агентов вместо LSP
В сообществе обсуждают новый подход к анализу кодовых баз: SQLite-совместимое хранилище, оптимизированное для работы AI-инструментов. Тренд показывает, как экосистема разработки начинает адаптироваться под агентные сценарии.
#backendvkhub #дайджест
Проблема двойной записи и transactional outbox
Типичная задача: сервис создаёт заказ и должен и сохранить его в базу, и сообщить о нём другим сервисам — через Kafka, RabbitMQ или вызов API. Очевидное решение выглядит так:
db.insert(order) # запись в БД
kafka.publish("order.created", order) # публикация события
db.insert, но до kafka.publish — заказ в базе есть, события нет, другие сервисы о заказе не узнают. Если поменять порядок и публиковать первым — при падении после publish событие ушло, а заказа в базе нет: подписчики обработают заказ, которого не существует. Любой порядок двух записей в две системы оставляет окно, в котором состояние рассинхронизировано. Это и есть dual-write problem.
id bigserial PRIMARY KEY,
topic text NOT NULL,
payload jsonb NOT NULL,
created_at timestamptz DEFAULT now(),
published_at timestamptz
);
with db.transaction():
db.insert(order)
db.insert_outbox("order.created", order)
rows = db.query(
"SELECT * FROM outbox WHERE published_at IS NULL "
"ORDER BY id LIMIT 100"
)
for row in rows:
kafka.publish(row.topic, row.payload)
db.execute(
"UPDATE outbox SET published_at = now() WHERE id = %s",
row.id,
)
published_at. На следующем проходе он отправит то же сообщение снова. Outbox гарантирует доставку at-least-once — каждое событие дойдёт хотя бы раз, но возможны повторы.
Ранее мы кратко писали о том, что в марте вышла Java 26 с 10 JEP. Structured Concurrency, Scoped Values, Flexible Main Methods, Derived Record Creation, HTTP/3, улучшения G1 GC. Релиз платформенный, ускоряет JVM и упрощает конкурентность.
➡️ Разобрали подробности в карточках.
#backendvk #java26
python tooling на Rust
uv, ruff, ty — три инструмента, которые заменяют солянку из pyenv, pip, venv, conda, Poetry, Black, Flake8, isort и mypy. Все три написаны на Rust, потому что сам python медленно делает то, что должно быть быстрым — разрешение зависимостей, парсинг AST и статический анализ.
Когда pip install занимает минуту, а полный линтинг работает полминуты, то разработчик либо отключает проверки, либо привыкают к медленному CI. Использование Rust устраняет этот компромисс.
На типичном проекте pip install -r requirements.txt занимает 60-120 секунд, uv sync на том же наборе 5-10 секунд. ruff check сканирует сотни тысяч строк за секунду, заменяя Flake8, Black и isort одним бинарником. ty check проверяет 50к строк за 150 мс, в то время как mypy на том же коде будет работать больше секунды.
➡️ Единый стек
# было
pip install -r requirements.txt
black . && flake8 . && isort .
mypy .
# стало
uv sync
uvx ruff check --fix .
uvx ty check .
pyproject.toml и лаунчером uvx. Workspaces для монорепозиториев заимствованы из Cargo: несколько пакетов, один uv.lock, консистентные зависимости между сервисами.pip freeze могут не собраться и требуют чистки freeze-файлов. Результат работы Ruff практически идентичен связке black + flake8 + isort, но при миграции на него диффы в гите всё же появятся. --add-ignore и переводить ошибки по мере роста уверенности.