Backend VK Hub


Гео и язык канала: Россия, Русский
Категория: Технологии


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

Связанные каналы

Гео и язык канала
Россия, Русский
Категория
Технологии
Статистика
Фильтр публикаций


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


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

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

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

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

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

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

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

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

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




Сентябрь в бэкенде вышел плотным. 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


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 #поиск


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 VM.class_hierarchy или метрика jvm_classes_loaded) и прогнать нагрузочный тест с теми агентами, которые реально работают в бою. Если счётчик держится в десятках тысяч, потолок в четыре миллиона вас не касается.

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

#backendvkhub #jdk #java




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

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

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

#backendvkhub #java




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


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

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

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

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


🔵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 #дайджест


Репост из: AI VK Hub
📢 Как мы увеличили время в ленте ВКонтакте на 11%

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

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

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


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

В каждом проекте с горутинами есть тест, который проходит локально, падает в 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



Показано 15 последних публикаций.