Backend VK Hub
Відкрити в Telegram
Комьюнити VK для бэкендеров. Cамые хардовые кейсы, дискуссии в кругу своих и прямой доступ к нашим экспертам 😎
Показати більше1 141
Підписники
-224 години
-57 днів
-1430 день
Архів дописів
1 141
🔵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
1 141
+5
Cassandra как метастор для S3: как мы пришли к локальному скану и Kafka
В VK работает собственная S3-совместимая реализация — one-object-storage. Метаданные объектов лежат в Cassandra, и на масштабе в миллиарды объектов простая база ловит сразу несколько архитектурных ловушек.
В карточках рассмотрим, что хранит метастор, почему партиционирование выстрелило, что такое tombstones и почему перестало хватать договорённостей с клиентами, как пришли к локальному скану и переезду очереди на Kafka.
И это только часть истории. В полной статье на Хабре — про фильтр Блума для чистки пустых директорий (потому что локально определить пустоту нельзя — директория и её содержимое лежат на разных хостах), snapshot-статистику в реальном времени для ограничения размера бакета, и почему обычные снапшоты не покрывают всю задачу.
Цифры в проде: 14 млн RPM на скан 18 хостов, до 1.1 млн RPM на чистку в пике.
#backendvkhub #s3 #kafka #cassandra #статья #кейс
1 141
Как читать планы 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 = '...';
-- + сколько страниц прочитано из кеша и с диска
EXPLAIN без ANALYZE — оценки, EXPLAIN ANALYZE — реальные числа. ANALYZE действительно выполняет запрос, поэтому на UPDATE и DELETE безопаснее обернуть в 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
Дерево читается снизу вверх: Index Scan находит строку, потом Limit её обрезает. В каждой строке два главных числа — cost (оценка планировщика) и actual time (реальное время).
Что смотрим первым — расхождение между rows (оценка) и actual rows (факт). Если планировщик ожидал 10 строк, а получил 100 000, он выбрал план под маленькую выборку (обычно Nested Loop) и просел на больших данных:
Nested Loop (rows=10, actual rows=100000)Лечится это через
ANALYZE table_name для пересбора статистики или default_statistics_target = 1000 для более детальной выборки.
BUFFERS показывает работу с диском: 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 для сессии.
Полезный приём для прода — auto_explain, чтобы планы медленных запросов сами писались в логи:
LOAD 'auto_explain';
SET auto_explain.log_min_duration = 1000;
SET auto_explain.log_analyze = true;
После этого все запросы дольше секунды попадут в логи вместе с реальным планом. Спасает кучу времени в дебаге.
EXPLAIN — основной инструмент оптимизации SQL. Стоит хотя бы раз пройтись по топу медленных запросов из pg_stat_statements через EXPLAIN ANALYZE — обычно там пара-тройка запросов с расходящейся статистикой, которые после правки уносят 80% нагрузки.
А вы где находили самые неожиданные узкие места через EXPLAIN? Делитесь в комментариях — особенно интересны кейсы, где статистика расходилась с реальностью в десятки раз.
#backendvkhub #postgresql1 141
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. На таблицах в десятки миллионов записей разница на порядки — десятки миллисекунд против минут.
Второй частый кейс — развернуть JSON-массив с привязкой к строке:
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 отличается тем, что заказы без позиций просто не попадут в результат.
Где LATERAL выигрывает: топ-N по группе с подходящим индексом, разворачивание массивов и JSON в строки, вызов set-returning функций с аргументом из левой таблицы, агрегации с фильтром, зависящим от строки. Везде, где нужна логика «для каждой строки слева — свой запрос справа».
Где не нужен: простые соединения по равенству ключей, обычные подзапросы в SELECT. LATERAL не делает запрос быстрее по умолчанию, но на правильном паттерне переписывает CTE с window functions в 2–5 раз быстрее.
#backendvkhub #postgresql1 141
До 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 он не замечает.
Учёта топологии тоже нет. Рантайм не моделирует локальность сокетов, разделение кеша, размещение в NUMA. На многосокетной машине контексты P всё равно могут оказаться на далёких ядрах. Это отдельная задача, не квота cgroup.
Формулы GOMAXPROCS = requests.cpu и requests.cpu × 2 являются исключительно эмпирическими, официальной поддержки таких механизмов в рантайме нет. Kubernetes requests — это сигнал для планирования, а не принудительное ограничение.
Для CPU-bound сервисов штатное значение по умолчанию теперь близко к оптимальному. Для I/O-нагруженных с сетевыми патологиями троттлинг и P99 нужно измерять вместе, прежде чем что-то менять. Для NUMA-чувствительной нагрузки cgroup-aware GOMAXPROCS проблему не закрывает.
Для сервиса на Go 1.25+ под Linux и Kubernetes automaxprocs можно удалить. GOMAXPROCS превратился из обязательной K8s-настройки в экспертное переопределение для тюнинга по результатам профилирования.
#backendvkhub #go1 141
+7
Современный взгляд на паттерны проектирования
Культовая книга Design Patterns «Банды четырёх» вышла в 1994 году. Многие паттерны из неё уже встроены в языки и фреймворки, а вместе с классическими появились паттерны распределённых систем. Объясняем в карточках современный взгляд на паттерны проектирования.
#backendvk #designpatterns
1 141
🔵 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 #дайджест
1 141
+5
Классический сценарий в проде: запрос не меняли, данные те же, а время выросло в разы — и не сразу, а после некоторого числа выполнений.
Причина почти всегда в том, как PostgreSQL кэширует планы за параметризованными запросами.
#backendvkhub #postgresql
1 141
Проблема двойной записи и transactional outbox
Типичная задача: сервис создаёт заказ и должен и сохранить его в базу, и сообщить о нём другим сервисам — через Kafka, RabbitMQ или вызов API. Очевидное решение выглядит так:
db.insert(order) # запись в БД
kafka.publish("order.created", order) # публикация события
Этот код содержит баг, который не виден на тесте и проявляется под нагрузкой или при сбоях. Две операции идут в две разные системы, и общей транзакции между ними нет.
Разберём поведение при сбое. Если процесс упадёт после db.insert, но до kafka.publish — заказ в базе есть, события нет, другие сервисы о заказе не узнают. Если поменять порядок и публиковать первым — при падении после publish событие ушло, а заказа в базе нет: подписчики обработают заказ, которого не существует. Любой порядок двух записей в две системы оставляет окно, в котором состояние рассинхронизировано. Это и есть dual-write problem.
Сетевой ретрай не спасает. Брокер может принять сообщение, но ответ потеряется по таймауту — сервис не знает, опубликовалось ли событие, и повторная отправка либо продублирует его, либо снова упрётся в ту же неопределённость.
Корень проблемы — попытка получить атомарность поверх двух систем без распределённой транзакции. Решение — свести задачу к одной системе, где атомарность уже есть. У базы есть транзакции; значит, факт «событие нужно отправить» должен записываться в ту же базу и в той же транзакции, что и сам заказ.
Это паттерн transactional outbox. К таблицам данных добавляется таблица исходящих сообщений:
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)
Сбой между двумя записями теперь невозможен: либо транзакция коммитится целиком — и заказ, и строка в outbox, либо откатывается целиком. Состояние БД всегда согласовано.
Остаётся доставить то, что лежит в outbox, в брокер. Этим занимается отдельный процесс — релей. Он читает неопубликованные строки и отправляет их:
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,
)
Здесь важно то, что релей может опубликовать сообщение в Kafka и упасть до того, как проставит published_at. На следующем проходе он отправит то же сообщение снова. Outbox гарантирует доставку at-least-once — каждое событие дойдёт хотя бы раз, но возможны повторы.
Поэтому потребители событий обязаны быть идемпотентными: повторная обработка того же события не должна давать повторный эффект. Это связывает outbox с идемпотентностью на стороне потребителя — первый паттерн гарантирует, что событие не потеряется, второй, что повтор не навредит.
У polling-релея есть цена, он постоянно опрашивает таблицу. Альтернатива — change data capture: релей читает не таблицу, а WAL базы (через Debezium и подобные инструменты) и реагирует на коммиты в outbox без опроса. Механика доставки другая, контракт тот же: запись данных и события атомарна, доставка — at-least-once.
Практический вывод очень простой. Как только в коде рядом стоят запись в базу и обращение к внешней системе — брокеру, платёжному шлюзу, другому сервису, — это уже кандидат на dual-write баг. Если обе операции должны произойти вместе, одну из них нужно свести к записи в ту же базу, а фактическое действие вынести в отдельный надёжный процесс.
#backendvkhub1 141
До 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.
Бенчмарки pganalyze и CYBERTEC на c7i.8xlarge с EBS показывают прирост 2-3× по throughput на cold-cache sequential scan при переходе с sync на io_uring. На локальном NVMe эффект скромнее: BetterStack получили 24% (2913 → 2221 мс), PlanetScale на своих Metal-серверах разницы между worker и io_uring практически не увидели, диск перестал быть узким местом.
Для наблюдения появилось два инструмента. Функция pg_get_aios() возвращает все запланированные операции с их состоянием:
FROM pg_get_aios();
В pg_stat_io добавились разрезы по асинхронным чтениям — видно, сколько байт прошло мимо синхронного пути и сколько времени ушло на ожидание completion.
Еще есть несколько мест, где легко ошибиться.
Во-первых, AIO работает только на чтения: WAL и обычные writes остались синхронными.
Во-вторых, EXPLAIN ANALYZE может занижать I/O time, потому что часть работы делается в воркерах и backend этого не видит, — для диагностики берите pg_stat_io.
В-третьих, effective_io_concurrency теперь напрямую управляет числом параллельных read-ahead запросов: на network storage его имеет смысл повышать до 32–64, на локальном NVMe оптимум обычно меньше.
Neon и Supabase пока держат io_method = sync — их prefetch-механика интегрирована с собственным storage layer и переписывается под новый интерфейс отдельно. Если обновляетесь на 18 на своих серверах, начните с worker, снимите профиль через pgbench и pg_stat_io, и только потом переключайтесь на io_uring, если у storage остался запас.
#backendvkhub #asyncio #postgresql1 141
+6
Ранее мы кратко писали о том, что в марте вышла Java 26 с 10 JEP. Structured Concurrency, Scoped Values, Flexible Main Methods, Derived Record Creation, HTTP/3, улучшения G1 GC. Релиз платформенный, ускоряет JVM и упрощает конкурентность.
➡️ Разобрали подробности в карточках.
#backendvk #java26
1 141
Джависты, помогите по-братски примите участие в большом исследовании! 💙
Вместе с JUG Ru Group составляем полную картину современной Java-разработки. Пожалуйста, пройдите опрос, он займёт не больше 20 минут. По итогам мы сделаем большой отчёт и поделимся результатами с вами!
P. S. Среди участников опроса JUG Ru Group разыграет 5 офлайн- и 10 онлайн-билетов на свои конференции. Ваш шанс 😏
1 141
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 .
Инструменты Astral объединены конфигурацией через pyproject.toml и лаунчером uvx. Workspaces для монорепозиториев заимствованы из Cargo: несколько пакетов, один uv.lock, консистентные зависимости между сервисами.
К началу 2026 года uv стал дефолтным инсталлером во многих CI-пайплайнах и сократил шаг установки с двух минут до десяти секунд. Ruff включает 800+ линт-правил с автофиксом и встроенный LSP-сервер на Rust (стабилизирован в 0.5.3). Ty работает как language server с навигацией, подсказками типов и автоимпортами.
Одна команда для установки окружения, одна для линтера, одна для проверки типов. Lockfile один на весь монорепозиторий.
➡️ Ограничения
Ty в бете и пока не поддерживает плагины для Pydantic, Django, SQLAlchemy. Глобальный кэш uv разрастается до 20 ГБ за год. Кроме того, uv строго разрешает зависимости, поэтому проекты с грязной историей pip freeze могут не собраться и требуют чистки freeze-файлов. Результат работы Ruff практически идентичен связке black + flake8 + isort, но при миграции на него диффы в гите всё же появятся.
Astral куплена OpenAI в марте 2026. Инструменты open source, но зависимость от одного вендора необходимо учитывать при выборе инфраструктуры.
Если заводите новый проект, то смело берите uv + ruff + ty с первого дня. Миграцию же старого проект лучше начинать с uv и ruff, а ty подключать параллельно с mypy с флагом --add-ignore и переводить ошибки по мере роста уверенности.
👇 А вы уже перешли на связку uv + ruff + ty?
#backendvk #python #pythontooling1 141
+5
Row-Level Security в PostgreSQL
Мультитенантные приложения обычно изолируют данные через
WHERE tenant_id = ? в каждом запросе. Это работает ровно до тех пор, пока кто-то не забывает написать этот фильтр. Один новый endpoint без WHERE — и данные одного клиента уйдут другому. С Row-Level Security фильтр живёт в базе, а не в коде, и применяется автоматически к каждому запросу — что бы приложение ни написало.
#backendvk #postgresql #rowlevelsecurity1 141
Kronk — Go SDK, который встраивает LLM прямо в приложение
Ardan Labs выпустили Kronk — Go SDK для локального запуска LLM прямо внутри приложения.
Стандартная схема работы Go с моделями — отправить HTTP-запрос в Ollama или OpenAI и получить ответ. То есть рядом с приложением всегда висит отдельный процесс, к которому ты ходишь по сети. Kronk делает иначе: llama.cpp встраивается прямо в Go-бинарь через модуль
yzma — никакого отдельного сервера, модель крутится в том же процессе.
krn, err := kronk.New(model.Config{
ModelPath: "/models/llama-3.2.gguf",
})
resp, err := krn.Chat(ctx, model.D{
Messages: []model.Message{
{Role: "user", Content: "Объясни structured concurrency"},
},
})
API похож на OpenAI — Chat, ChatStreaming, Embeddings, Rerank, Tokenize. Если работали с OpenAI SDK, синтаксис знакомый, только всё локально.
➡️ Полный RAG в одном бинаре
Главная идея в том, чтобы убрать модельный сервер как отдельную архитектурную единицу. Весь RAG пайплайн: чтение документов, создание эмбеддингов, поиск, ответ — можно упаковать в один бинарь. Без Ollama рядом, без HTTP между компонентами, без зависимости от провайдера.
➡️ Поддержка моделей и ускорения
Работает с GGUF-моделями — тот же формат что у Ollama, больше 147k моделей на Hugging Face. Hardware acceleration есть: CUDA, Metal, Vulkan, ROCm, OpenCL. Поддерживаются текстовые модели, vision и аудио. Через GBNF-грамматики можно жёстко ограничить формат вывода, применимо, когда нужен структурированный JSON без галлюцинаций в схеме.
➡️ Сервер — опционально
Сервер в проекте тоже есть и совместим с OpenWebUI, Cline и Claude Code,если нужен привычный endpoint для разработки. Но авторы честно говорят: долгосрочная цель — убрать сервер как отдельную сущность, твоё приложение и должно быть сервером.
Проект не v1, молодой. Но за ним Ardan Labs — авторы Ultimate Go, не студенческий эксперимент. По заявлению авторов, через yzma покрыто уже 94% функциональности llama.cpp.
Как тебе такое, Сергей?
«Как и следовало ожидать, ИИ добрался и до бэкенда. Конечно, приложения для ИИ на Go писали и раньше, но, так как львиная доля кода для машинного обучения реализована на Python, для написания ИИ-шного приложения надо было как-то взаимодействовать с Python — через удаленный API или напрямую с Python'овскими модулями через application binary interface. Это было довольно муторно. С появлением Kronk встраивание AI в Go-шные программы упростилось донельзя, теперь для этого не надо ваять никаких переходников для ML-ных библиотек на Python, теперь локальная LLM и все средства для работы с ней есть прямо в вашей любимой Goшке. Это как Lua, но для AI — можно встроить куда угодно. Как заявляют авторы, Kronk поддерживает свыше 94% фич llama.cpp — популярного инструмента для поднятия локальной LLM. В комплекте идут примеры для реализации разных кейсов работы с AI, от простого чата с моделью, до работы с аудио и изображениями, так что теперь начать писать на Go программы, работающие с ИИ, стало гораздо проще», — отметил Сергей Лебедев, старший backend-разработчик VK.А как вам такой подход? Хотели бы отказаться от отдельного LLM-сервера и держать модель прямо внутри приложения — или текущая архитектура с API вам кажется надёжнее? Обсудим в комментариях 👇 #backendvk #go
1 141
🔵nginx 1.30 — MPTCP, sticky sessions и HTTP/2 к бэкендам
Multipath TCP, шифрование TLS-параметров через ECH, привязка клиентов к серверам и HTTP/2 при проксировании к бэкендам. Криптоключи загружаются из аппаратных токенов.
🔵CVE-2025-24859 в Apache Roller — CVSS 10.0
Злоумышленники сохраняют доступ через активные сессии после смены пароля. Исправлено в 6.1.5: при изменении учётных данных все сессии инвалидируются централизованно.
🔵Solod — подмножество Go с ручным управлением памятью
Транскомпилируется в C11, отказывается от GC в пользу ручного управления памятью. Дает прирост производительности в ряде сценариев, но требует аккуратной работы с указателями.
🔵OpenJDK: фокус сместился на тестирование JDK 27
После релиза Java 26 команда переключилась на JDK 27. Обсуждаются удаление устаревших локализаций и переход JavaFX на Metal-рендеринг на macOS.
🔵Gitea 1.26 — параллельные Actions и переход на Vite
Три исправленных уязвимости, параллельное выполнение Actions с кастомными токенами и ускорение работы с большими репозиториями. Фронтенд переехал на Vite.
📌 Новые статьи от инженеров VK на Хабр:
🔵Проектирование микросервисов на Go: типичные сложности и лучшие практики
🔵Реализация автоудаления блокирующих сессий в MS SQL
#дайджест #backendvk
1 141
🍃 Бэкенд без воды, пиво — без открывашек, так прошел большой весенний Java meetup
Провели митап для джавистов, сфокусировались на практике: обсудили архитектурные подходы, масштабирование сервисов, оптимизацию и observability— без лишней теории, только реальные кейсы и рабочие инструменты из продакшена от спикеров VK и сообщества Spring АйО.
Без неформальной части тоже не обошлось. Позвали choco_nik — сначала он кратко рассказал про подходы, а потом началась практика: открывали пиво всем, что попадалось под руку (да, даже роутером). Зафиналили баттлом на скорость, где главный приз — ящик пива.
Итог простой: сильный технический контент + живое общение + немного фанового безумия = митап, на который хочется возвращаться.
#backendvk #javameetup #java #пивобезалкогольное
1 141
+5
🍿 Команда VK Видео поделилась опытом перехода с монолита на микросервисы на Go под высокую нагрузку. Ниже — конкретные техники из статьи, которые реально влияют на производительность.
#backendvk #VKВидео
1 141
Context в Go — это не просто способ передать данные
context.Context в Go используют для двух вещей: передать данные через стек вызовов и управлять временем жизни операций. Вторая часть важнее, и именно она чаще всего игнорируется.
Когда HTTP-запрос отменяется — клиент закрыл соединение, таймаут истёк — контекст этого запроса отменяется. Это сигнал: всё, что работало в рамках этого запроса, должно остановиться. Запросы в БД, вызовы внешних API, фоновые горутины.
Если контекст не прокидывается — ничего не останавливается.
// ❌ Запрос к БД продолжается даже если клиент ушёл
func handleRequest(w http.ResponseWriter, r *http.Request) {
result, err := db.QueryRow("SELECT ...", id).Scan(&data)
// ...
}
// ✅ Запрос к БД отменяется вместе с HTTP-запросом
func handleRequest(w http.ResponseWriter, r *http.Request) {
result, err := db.QueryRowContext(r.Context(), "SELECT ...", id).Scan(&data)
// ...
}
Разница между QueryRow и QueryRowContext — один параметр. Но без него при пике нагрузки, когда клиенты начинают закрывать соединения по таймауту, база продолжает обрабатывать уже ненужные запросы.
Отдельная ловушка — создать новый контекст там, где надо пробросить существующий:
// ❌ Создали новый контекст -- потеряли отмену, потеряли трейсинг
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
return s.repo.Find(context.Background(), id) // ctx проигнорирован
}
// ✅
func (s *Service) GetUser(ctx context.Context, id string) (*User, error) {
return s.repo.Find(ctx, id)
}
Такой баг не видно в тестах — context.Background() работает нормально. Виден в продакшне, когда трейс обрывается на границе сервиса или горутины не завершаются при shutdown.
Правило для горутин: если запускаешь горутину внутри handler — либо передай контекст, либо отвяжи её явно через context.WithoutCancel. Без этого горутина либо не реагирует на отмену, либо падает, когда родительский контекст отменяется раньше, чем она завершилась:
// Фоновая задача которая должна пережить запрос
go func() {
ctx := context.WithoutCancel(r.Context()) // сохраняет данные, убирает отмену
s.asyncJob(ctx, data)
}()
context.WithoutCancel появился в Go 1.21 — создаёт контекст, который наследует данные из родителя, но не наследует отмену. До 1.21 для этого использовали context.Background() с ручным копированием нужных значений, что было плохой практикой.
Ещё один момент: context не для хранения бизнес-данных. ctx.Value — для инфраструктурных вещей: trace ID, user ID для логов, deadline. Бизнес-параметры передаются явными аргументами. Если в context хранятся данные, которые влияют на бизнес-логику — это признак плохой архитектуры.
#backendvk #go #context1 141
+5
readOnly = true на @Transactional выглядит как подсказка-документация — мол, этот метод не меняет данные. На самом деле, он меняет поведение Hibernate и драйвера на уровне, который реально влияет на производительность.
#backendvk #spring #transactional