
> Стек и философия
Языки:
• PHP, Go, Node.js, Python
Инфраструктура:
• Docker, Kubernetes, Ansible, Terraform
Архитектура:
• DDD, SOLID, Clean Architecture
• Microservices, Event-driven, TDD
• Код должен быть понятным через полгода
• Тесты — это документация, которая не врёт
• Простое решение лучше сложного умного
• Технологии меняются, принципы остаются
> Посты
🔥 Временные файлы в проде: как не превратить хранилище в цифровую помойку
Без текста
GiST индексы: когда B-tree сдох, а GIN перебор
🎯 GiST индексы: когда B-tree сдох, а GIN переборGiST (Generalized Search Tree) — индекс для нестандартной хуйни, которую обычные индексы не переваривают. Геоданные, диапазоны, полнотекст, кастомные типы. Если данные не укладываются в "больше-меньше-равно", твой выбор.🗺️ PostGIS: координаты не работают с B-treeЗадача: найти все кафе в радиусе 1км. B-tree на lat/lng = Seq Scan и пиздец.-- Не делай так (B-tree на координатах):CREATE INDEX idx_fail ON locations(lat, lng);SELECT * FROM locations WHERE lat BETWEEN 55.74 AND 55.76 AND lng BETWEEN 37.60 AND 37.62;-- Найдет квадрат, не круг. Кафе из соседнего района тоже попадут-- Правильно (GiST + PostGIS):CREATE EXTENSION postgis;ALTER TABLE locations ADD COLUMN geom geography(POINT, 4326);UPDATE locations SET geom = ST_SetSRID(ST_MakePoint(lng, lat), 4326);CREATE INDEX idx_locations_geom ON locations USING GIST (geom);SELECT * FROM locations WHERE ST_DWithin(geom, ST_MakePoint(37.617635, 55.755826)::geography, 1000);-- Execution: 12ms на 5M точек vs 4500ms Seq ScanБенчмарк: 5M точек, радиус 1км от центра Москвы. B-tree квадрат: 180ms + ложные срабатывания. GiST круг: 12ms, точные результаты.🐕 Первое сравнение: геометрияПёс ищет еду в квадратной комнате — проверяет все углы (B-tree квадрат). Умный пёс ищет в радиусе от своей миски — только актуальная зона (GiST круг). Второй находит быстрее и без лишнего.📝 Полнотекстовый поиск: GiST vs GINGIN быстрее для поиска. GiST меньше весит и быстрее обновляется.CREATE TABLE articles (id SERIAL, title TEXT, content TEXT, tsv tsvector);-- GIN: быстрый поискCREATE INDEX idx_gin ON articles USING GIN (tsv);-- Размер: 450MB | Поиск: 8ms | INSERT: 120ms-- GiST: быстрая вставка CREATE INDEX idx_gist ON articles USING GIST (tsv);-- Размер: 280MB (на 38% меньше!) | Поиск: 25ms | INSERT: 45msSELECT * FROM articles WHERE tsv @@ to_tsquery('russian', 'postgresql & индексы');Когда GiST: частые INSERT/UPDATE (логи, новости, чаты), размер индекса критичен.Когда GIN: поиск важнее вставок, read-heavy база."GIN всегда лучше для полнотекста"Реальность: Новостной сайт, 500 статей/час. GIN обновление 120ms × 500 = 16 секунд CPU/час. GiST: 45ms = 6 секунд. Экономия 240 часов CPU/месяц ="Геоданные сложные, используем два WHERE на lat/lng"Реальность: Квадрат вместо круга = пользователь видит кафе в 5км, но не видит в 900м за углом. Конверсия -15%, потому что Иванов И.И. "упростил".```sql-- Бронирование переговорок:CREATE TABLE bookings ( id SERIAL PRIMARY KEY, room_id INT, time_range tstzrange);CREATE INDEX idx_bookings_time ON bookings USING GIST (time_range);-- Найти пересечения:SELECT * FROM bookings WHERE room_id = 5 AND time_range && tstzrange('2025-11-14 14:00', '2025-11-14 16:00');-- EXCLUSION: физически запретить двойное бронированиеCREATE TABLE bookings ( room_id INT, time_range tstzrange, EXCLUDE USING GIST (room_id WITH =, time_range WITH &&));-- База сама гарантирует непересечение, двойное бронирование невозможно```Спрашиваешь собаку "ты свободен 14:00-16:00?" B-tree проверяет начало и конец отдельно, путается. GiST понимает диапазоны целиком: пёс занят 15:00-17:00 = пересечение есть, слот занят. Логика правильная.
Hash индексы: от "не используй никогда" до "ну ладно, иногда можно"
🔨 Hash индексы: от "не используй никогда" до "ну ладно, иногда можно"Hash индексы в PostgreSQL — как та бывшая, которая была токсичной, исправилась, но все равно никто не доверяет. И правильно делают в 95% случаев.💀 История позора: PostgreSQL 9.6 и нижеДо версии 10 Hash индексы были говном:Не логировались в WAL → крашнулась база = индекс сгнилПосле сбоя: REINDEX вручную или Seq ScanНе реплицировались на слейвыНе работали с CONCURRENTLY-- PostgreSQL 9.6:CREATE INDEX idx_hash ON sessions USING HASH (token);-- База упала → индекс INVALID-- Продакшн лёг в 3 утраВывод до 2017: Hash индексы = профессиональное самоубийство.🐕 собака vs Hash индексы до PG10пес помнит где его миска даже после того как отключили свет. Hash индекс до PG10 забывал все после сбоя питания. пес надежнее.✅ PostgreSQL 16: Hash индексы вырослиЧто изменилось с PG10:WAL logging — выживают после крашаРепликация работаетCONCURRENTLY поддерживаетсяВ PG16: оптимизация коллизий (+15% скорость)Технически:CREATE INDEX idx_sessions_token ON sessions USING HASH (session_token);-- PG16: -- - Логируется в WAL ✓-- - Реплицируется ✓ -- - Не умирает при краше ✓-- - Быстрее чем в PG15 на 15%💰 Бенчмарки: Hash vs B-tree на миллиардахТестовая база:1 миллиард строкКолонка: UUID токены (уникальные)Запросы: точные поиски WHERE token = '...'Железо: 16CPU, 128GB RAM, NVMe-- Создаем индексы:CREATE INDEX idx_btree ON huge(token); -- B-tree: 42GBCREATE INDEX idx_hash ON huge(token) USING HASH; -- Hash: 38GB-- Запрос: точный поискSELECT * FROM huge WHERE token = 'cafebabe-1234-5678-90ab-cdef12345678';Результаты (1000 запросов, кэш прогрет):B-tree:Avg: 0.42msP95: 0.68msIndex size: 42GBHash:Avg: 0.38msP95: 0.52msIndex size: 38GBРазница: 10-12% в пользу Hash. Охуенно, правда? За эти 10% ты потерял:Диапазонные запросы (не работают)Сортировки (не работают)Частичные совпадения (не работают)🐕 B-tree vs HashB-tree — швейцарский нож: режет, открывает бутылки, подпиливает ногти.Hash — штопор: открывает бутылки охуенно, но только бутылки.Твой выбор: швейцарский нож за 100₽ или штопор за 90₽ который ничего кроме бутылок не умеет?⚡ Когда Hash реально быстрее (спойлер: почти никогда)Hash выигрывает ТОЛЬКО когда:Таблица >100M строк (иначе разница незаметна)Только точные поиски (=)Колонка уникальная или почти уникальная (UUID, токены)Никогда не нужны: диапазоны, сортировки, LIKE-- Идеальный кейс:CREATE TABLE sessions ( session_id BIGSERIAL PRIMARY KEY, token UUID UNIQUE NOT NULL, user_id BIGINT, created_at TIMESTAMPTZ);-- Hash для токенов (только = поиск):CREATE INDEX idx_token_hash ON sessions USING HASH (token);-- B-tree для остального:CREATE INDEX idx_user_created ON sessions(user_id, created_at);Реальная статистика использования:SELECT indexrelname, idx_scan, idx_tup_readFROM pg_stat_user_indexes WHERE indexrelname LIKE '%hash%';-- У 99% проектов: idx_scan = 0-- Потому что забыли или побоялись💩 Отмазка | Реальность"Hash быстрее, переделаю все индексы на Hash"Реальность: Первый же запрос WHERE created_at > NOW() - INTERVAL '1 day' ляжет в Seq Scan. Продакшн лег, ты уволен."Hash экономит место"Реальность: 38GB vs 42GB на таблице в 1TB. Экономия 10% места за потерю 90% функциональности. Как отрезать ноги чтобы меньше жрать."В PG16 Hash прокачали, теперь можно"Реальность: Прокачали скорость на 15%, но он все равно умеет только =. Это как научить мопса бегать быстрее — он все равно мопс, не борзая.🐕 сравнение для тупыхПес умеет: сидеть, лежать, дать лапу, принести мяч, охранять дом.Hash индекс умеет: =.Кого ты возьмешь домой? Собаню или одноразовый инструмент?🎯 Практический чеклистИспользуй Hash ТОЛЬКО если:✅ Таблица >100M строк✅ Колонка уникальная (UUID, токены, хэши)✅ Запросы ТОЛЬКО с =✅ Никогда не нужны: >, <, ORDER BY, LIKE✅ Проверил что B-tree реально тормозитНЕ используй Hash если:❌ Хоть раз нужен диапазон❌ Хоть раз нужна сортировка❌ Таблица <100M строк (разница незаметна)❌ Колонка с
B-tree в PostgreSQL 16: от "я умею CREATE INDEX" до "блять, вот как оно работает
🔬 B-tree в PostgreSQL 16: от "я умею CREATE INDEX" до "блять, вот как оно работает"Знаешь что общего между B-tree индексом и твоим пониманием баз данных? Оба выглядят простыми снаружи, но внутри — охуенно сложная штука, которую ты игнорируешь. Разница в том, что B-tree реально работает.🌳 Как B-tree работает внутри (и почему ты это игнорил)B-tree — это дерево блоков. Не путай с бинарным деревом из учебника, где каждая нода хранит один ключ. В PostgreSQL каждая страница индекса (8KB) хранит сотни ключей. Это как книжная полка: на одной полке (странице) лежит дохуя книг (ключей).Структура:Корневая страница: указатели на веткиВнутренние страницы: еще указателиЛистовые страницы: ключи + TID (Tuple ID - физический адрес строки)-- Проверим структуру индексаCREATE EXTENSION IF NOT EXISTS pageinspect;SELECT * FROM bt_metap('idx_users_email');-- magic: 340322 (идентификатор B-tree)-- version: 4 (в PG16)-- root: номер корневой страницы-- level: высота дерева (обычно 2-4)-- fastroot, fastlevel: оптимизация для частых вставокПочему это важно:Высота 3 = 3 чтения с диска (если нет в кэше)На 10M строк: высота обычно 3На 1B строк: высота обычно 4Разница между 3ms и 4ms может стоить $500/месяц🔥 Дедупликация в PostgreSQL 16: наконец-то не жрет местоДо PG13: одинаковые значения дублировались. Колонка status с 3 значениями на 10M строк = 10M записей в индексе.В PostgreSQL 16:CREATE INDEX idx_orders_status ON orders(status);-- До PG13: -- Индекс: 180MB (10M записей × ~18 bytes)-- PostgreSQL 16 с дедупликацией:-- Индекс: 110MB (экономия 38%!)-- Механизм: группирует одинаковые ключи + массив TIDКак проверить дедупликацию:SELECT pg_size_pretty(pg_relation_size('idx_orders_status')) as index_size, pg_stat_get_tuples_inserted('idx_orders_status'::regclass) as inserts;-- Если inserts >> unique values = дедупликация работает🐕 WoofWoof сравнение: собака vs твой индексПес помнит где лежит его миска. Одна миска = одна локация в памяти.Твой индекс БЕЗ дедупликации: 10 миллионов записей "status=pending". Это как если бы Рекс хранил 10 миллионов копий информации "миска на кухне". Песька не дебил, он помнит один раз. PostgreSQL 16 научился делать так же.⚡ Составные индексы: порядок колонок = жизнь или смертьЭто НЕ одно и то же:-- Вариант А:CREATE INDEX idx_orders_user_created ON orders(user_id, created_at);-- Вариант Б:CREATE INDEX idx_orders_created_user ON orders(created_at, user_id);Работает оптимально:-- Индекс А (user_id первый):SELECT * FROM orders WHERE user_id = 123 AND created_at > '2025-01-01';SELECT * FROM orders WHERE user_id = 123; -- РАБОТАЕТSELECT * FROM orders WHERE user_id = 123 ORDER BY created_at; -- ИДЕАЛЬНО-- Индекс Б (created_at первый):SELECT * FROM orders WHERE created_at > '2025-01-01' AND user_id = 123;SELECT * FROM orders WHERE created_at > '2025-01-01'; -- РАБОТАЕТSELECT * FROM orders WHERE user_id = 123; -- НЕ ИСПОЛЬЗУЕТ ИНДЕКС, ЕБАТЬПравило: первая колонка = самое частое условие WHERE. Вторая = сортировка или доп фильтр.💩 Отмазка | Реальность: составные индексы"Я создам два индекса: на user_id и на created_at"Реальность: PostgreSQL может использовать только один индекс на запрос (до PG16, Bitmap Index Scan помогает, но это медленно). Два индекса = 2× места, 2× время на INSERT/UPDATE, хуевая производительность."Порядок не важен, PostgreSQL сам разберется"Реальность: WHERE user_id = 123 с индексом (created_at, user_id) = Seq Scan на 5M строк. Execution time: 800ms вместо 3ms. Поздравляю, твой продакшн лег.🎯 Partial индексы: когда не нужно индексировать все-- Плохо: индекс на все 10M строкCREATE INDEX idx_users_email ON users(email);-- Размер: 450MB-- Хорошо: только активные (20% от всех)CREATE INDEX idx_active_users_email ON users(email) WHERE is_active = true;-- Размер: 90MB (экономия 80%!)-- Скорость: та же, но только для активныхКогда использовать:Колонка с перекосом (90% null, 10% не null)Логическое удаление (deleted_at IS NULL)Статусы (status IN ('pending',
Индексы в PostgreSQL: почему твой продакшн тормозит как Windows Vista
🔥 Индексы в PostgreSQL: почему твой продакшн тормозит как Windows VistaЗнаешь, что общего между твоей базой данных и трехлетним ребенком? Оба умеют орать посреди ночи, и в обоих случаях это твоя вина. Только ребенка ты кормил и менял памперсы, а в базу индексы не завез. Поздравляю, теперь твой SELECT работает 8 секунд вместо 3 миллисекунд.🐕 Почему даже собака умнее твоего тимлидаПредставь: у тебя книга на 500 страниц, и тебе нужно найти слово "ебанутый". Что сделает нормальный человек? Откроет предметный указатель сзади. Что сделает твоя база без индексов? Прочитает ВСЕ 500 СТРАНИЦ от начала до конца.Моя собака понимает, что миска с едой стоит в углу кухни – он не обыскивает всю квартиру каждый раз. А твой PostgreSQL при запросе WHERE email = 'ivan@corp.com' сканирует 5 миллионов строк. Каждый. Ебаный. Раз.Стоимость твоей тупости:Seq Scan по 5M строк: ~800msIndex Scan: ~3msРазница: в 250 раз медленнееПользователи ушли: пока ты читаешь этот текст💩 Отмазка | Реальность"У нас всего 100К записей, индексы не нужны"Реальность: Даже на 10K строк разница между 50ms и 2ms имеет значение, когда запрос выполняется 1000 раз в минуту. Это 48 секунд CPU времени VS 2 секунды. За месяц ты платишь на $300 больше за инстанс, потому что "всего 100K записей"."Мы добавили индекс, но ничего не изменилось"Реальность: Ты добавил индекс на колонку created_at, но запрос использует WHERE DATE(created_at) = '2025-01-15'. Функция DATE() делает индекс бесполезным, ебать тебя в космос. PostgreSQL не может использовать индекс, если ты трансформируешь колонку.🔍 Как узнать, что ты накосячилОткрой консоль и запусти:```sql-- Проверяем запросEXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@mail.ru';-- Видишь "Seq Scan on users"? ПОЗДРАВЛЯЮ, ТЫ ПРОСРАЛ-- cost=0.00..145000.00 - это пиздец, друг-- Должно быть: Index Scan, cost около 0.43..8.45Или проверь индексы, которые НИКТО НЕ ИСПОЛЬЗУЕТ:sqlSELECT schemaname, tablename, indexname, idx_scanFROM pg_stat_user_indexes WHERE idx_scan = 0 AND indexname NOT LIKE 'pg_toast%';-- idx_scan = 0 значит индекс жрет место и никому не нужен-- Как твой проджект-менеджер```🏆 Реальный кейс от Петрова П.П.Компания "ХуевыРешения Inc" делала выборку пользователей по статусу:```sql-- Код долбоеба:SELECT * FROM orders WHERE status IN ('pending', 'processing')ORDER BY created_at DESC LIMIT 50;-- Execution time: 4500ms-- БЛЯТЬ, КТО ЭТО НАПИСАЛ?!```Проблемы:Нет индекса на statusНет индекса на created_atPostgreSQL сканирует 8M строкПосле того как выебали всех:```sql-- Создаем составной индекс (порядок ВАЖЕН!)CREATE INDEX idx_orders_status_created ON orders(status, created_at DESC) WHERE status IN ('pending', 'processing');-- Execution time: 12ms-- Экономия: 4488ms на запрос-- Запросов в день: ~50K-- Сэкономили: 62 часа CPU времени в день```Стоимость косяка: $400/месяц на оверпрайснутый RDS инстанс. Потому что Петров П.П. не знает что такое индексы.✅ Что делать прямо сейчасПроверь эти запросы:SELECT с WHERE на колонках без индексовJOIN на колонках без индексов (особенно foreign keys)ORDER BY на колонках без индексовДобавь индексы:```sql-- На foreign keys (да, PostgreSQL их НЕ индексирует автоматически)CREATE INDEX idx_orders_user_id ON orders(user_id);-- На поля для поискаCREATE INDEX idx_users_email ON users(email);-- Partial индексы для частых условийCREATE INDEX idx_active_users ON users(id) WHERE is_active = true;```🐕 Заключение: собака vs твой сеньорМоя собака запоминает где лежит его игрушка после одного раза. Твой сеньор разработчик третий год пишет запросы без индексов. Собака приносит мяч за 5 секунд. Твой запрос работает 5 секунд на табличке в 100K строк.Вопрос: кого из них стоит повысить?Итого:Индексы = предметный указатель в книгеБез индексов = читать всю книгу каждый разСобаки умнее некоторых разработчиковТвой продакшн тормозит потому что ты ленивыйP.S. В следующей статье разберем VACUUM и почему твоя база в 3 часа ночи легла. Спойлер: ты, блять, забыл про autovacuum.
🔥 Как ПРАВИЛЬНО настроить Service Discovery в Docker Swarm
Доброго вечера, коллеги (с)В прошлы раз мы разобрали почему docker swarm это плохо, в этой статье покажу как сделать чтобы нивелировать большинство его проблем https://telegra.ph/Kak-PRAVILNO-nastroit-Service-Discovery-v-Docker-Swarm-10-26P.S Да-да опять не могу ужать информацию до одного поста в телеге (Продолжение: В следующей части разберём, почему Kubernetes DNS ещё хуже)
Service Discovery в Docker Swarm: Как мы проебали prod в 3 ночи
🔥 Service Discovery в Docker Swarm: Как мы проебали prod в 3 ночиВступление для тех, кто ещё не в курсеПоздравляю, ты дошёл до момента, когда твои 5 контейнеров больше не могут найти друг друга через localhost:3000. Теперь тебе нужен Service Discovery. И ты, нагуглил Docker Swarm.Спойлер: твоя собака настроит отказоустойчивый кластер быстрее, чем ты разберёшься с overlay networks.🐕 Что такое Service DiscoveryКогда контейнер api-gateway хочет достучаться до user-service, ему нужно знать: IP этого сервиса, порт, и что сервис вообще жив В Swarm это работает через встроенный DNS на 127.0.0.11:53 и overlay network. Звучит охуенно, правда?💀 Как это работает (технически)Overlay Network — твой новый ад```docker network create \ --driver overlay \ --attachable \ my-fucking-network```Что здесь важно:--driver overlay — сетка между нодами. Без этого контейнеры на разных хостах НЕ УВИДЯТ друг друга--attachable — чтобы можно было подключить контейнер руками (для дебага в 3 ночи)DNS Resolution — магия, которая не работаетSwarm поднимает встроенный DNS внутри каждого контейнера:```yamlversion: "3.8"services: api: image: api:latest networks: - backend deploy: replicas: 3 db: image: postgres:15 networks: - backend```Обращение внутри контейнера api:```javascript // РАБОТАЕТ (если ты не долбоёб)fetch('http://db:5432')// НЕ РАБОТАЕТ (догадайся почему)fetch('http://localhost:5432') // БЛЯТЬ, ЭТО ЖЕ КОНТЕЙНЕР```🔴 КРИТИЧЕСКИЙ КОСЯК №1: Round-robin DNS — лотерея смертиКогда у тебя 3 реплики api, Swarm отдаёт IP по очереди:```bash# Запрос 1dig api # -> 10.0.1.5# Запрос 2 dig api # -> 10.0.1.6# Запрос 3dig api # -> 10.0.1.7 (УЖЕ МЁРТВАЯ РЕПЛИКА)```Проблема: TTL = 0, но HTTP-клиенты кэшируют соединения. Результат — 30% запросов в труп.Решение (которое ты не применишь):```javascript// Правильноconst agent = new http.Agent({ keepAlive: false // Да, убиваем перформанс});// Как у тебя сейчас (НЕПРАВИЛЬНО)const agent = new http.Agent({ keepAlive: true, maxSockets: 50 // 50 КОННЕКТОВ В МЁРТВЫЙ КОНТЕЙНЕР, ГЕНИЙ});```🔴 КРИТИЧЕСКИЙ КОСЯК №2: Health checks — иллюзия контроля```yamlhealthcheck: test: ["CMD", "curl", "-f", "http://localhost/health"] interval: 10s timeout: 3s retries: 3```Отмазка CTO Петрова А.С.: "У нас есть health checks!"Реальность:Проверка идёт внутри контейнера (localhost)Сетевые проблемы НЕ ДЕТЕКТЯТСЯМежду unhealthy и удалением из DNS — 30-60 секундЗа эти 60 секунд ты получишь несколько сотен 503-ошибок. Поздравляю!🔴 КРИТИЧЕСКИЙ КОСЯК №3: Split-brain при деплоеКогда ты делаешь docker service update:Поднимается новая реплика (10.0.1.8)DNS сразу добавляет её в пулПриложение ещё грузится (20 секунд)💥 50% запросов в 503Пруф:```bashdocker service ps api# ID IMAGE NODE DESIRED CURRENT STATE# abc123 api:v2 node1 Running Starting 3 seconds ago ← БЛЯТЬ, НЕ ГОТОВА# def456 api:v1 node2 Running Running 2 hours ago```🐶 Собака vs Твой Service DiscoveryСобака: Понимает команды с первого разаТвой код: 4 дня настройки DNSСобака: Виляет хвостом при встречеТвой код: 503 Service UnavailableСобака: Приносит тапкиТвой код: Приносит алерты в 3 ночи🔥 Минимально рабочий конфиг ```yamlversion: "3.8"services: api: image: api:latest networks: - backend deploy: replicas: 3 update_config: parallelism: 1 delay: 30s # ПОДОЖДИ, ПОКА КОНТЕЙНЕР ПРОГРЕЕТСЯ failure_action: rollback restart_policy: condition: on-failure delay: 5s max_attempts: 3 healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"] interval: 5s timeout: 2s retries: 3 start_period: 40s # 40 СЕКУНД НА СТАРТ, НЕ 10networks: backend: driver: overlay driver_opts: encrypted: "true" # А ТО СНИФФЕРЯТ```📊 Что в итоге✅ Работает из коробки — для pet-проектов❌ Production — нужен Consul/etcd или хотя бы правильная настройка
> Теги
> system.log
Ожидание логов парсера...
> _█