Базы данных
Expand-contract: нулевой простой для ломающих изменений схемы
Команда переименовывает users.username в users.handle одним запросом ALTER TABLE во время деплоя. Миграция коммитится мгновенно. Но rolling deploy ещё продолжается — старые поды всё ещё выполняют SQL, ссылающийся на users.username. Каждый запрос от старого пода завершается ошибкой “column does not exist”. Быстрая миграция вызвала реальный инцидент.
Почему rename/drop/type-change нельзя сделать за один шаг
Rolling deploy — единственная безопасная стратегия деплоя в любом масштабе — запускает старый и новый код одновременно от секунд до минут. Если миграция закоммичена до того, как все старые поды завершили работу:
- Переименование: старый код ссылается на старое имя колонки → ошибки
column does not exist. - Удаление: старый код ссылается на удалённую колонку → то же самое.
- Смена типа: старый код ожидает int, колонка теперь text → ошибки приведения типов.
Инвариант, который должен соблюдаться в каждый момент: текущая схема одновременно совместима с предыдущей версией приложения и следующей. Одношаговое переименование, удаление или смена типа нарушает этот инвариант.
Общий фреймворк expand-contract
Каждое ломающее изменение разбивается на шесть фаз:
| Фаза | Действие в миграции | Действие в коде | Обе версии совместимы? |
|---|---|---|---|
| 1. Expand | Добавить новый элемент рядом со старым | — | Да — старый код не затронут |
| 2. Dual-write | — | Писать и в старую, и в новую | Да — старый читает старую, новый пишет обе |
| 3. Backfill | Копировать исторические данные старая → новая пакетами | — | Да |
| 4. Переключение чтений | — | Читать из новой, всё ещё писать обе | Да — полный откат ещё возможен |
| 5. Остановка старых записей | — | Писать только в новую | Да — старый элемент всё ещё присутствует |
| 6. Contract | Удалить старый элемент | — | Да — ни один код не ссылается на старый элемент |
Пошаговый разбор переименования колонки: username → handle
Канонический пример. Общая длительность: 3–7 дней.
День 1 — Expand:
-- Миграция 1: мгновенно, без дефолта
ALTER TABLE users ADD COLUMN handle TEXT;День 1 — Деплой dual-write:
Код приложения теперь пишет и username, и handle при каждом INSERT/UPDATE. Старый код всё ещё читает username. Новый код читает username (пока что).
День 2 — Backfill:
-- Вне транзакции миграции (пакетный цикл):
UPDATE users SET handle = username
WHERE handle IS NULL
AND id BETWEEN :batch_start AND :batch_end;
-- Повторять до SELECT COUNT(*) FROM users WHERE handle IS NULL = 0Пакеты по 1к–10к строк, каждый в короткой транзакции с pg_sleep(0.1) между ними.
День 3 — Переключение чтений:
Деплой кода, который читает только handle. Записи всё ещё идут в обе колонки. Мониторинг дашбордов 24 часа.
День 5 — Остановка dual-write:
Деплой кода, который пишет только в handle. Ожидание завершения rolling deploy.
День 7 — Contract:
-- Миграция 4: быстро (только метаданные)
ALTER TABLE users DROP COLUMN username;Каждый шаг независимо деплоится и отменяется. Нулевой простой на всём пути.
Down-миграции — не откат
Down-миграции (обратный SQL к up-миграции) есть во многих инструментах (файлы-партнёры down.sql). Они работают в разработке, но опасны в продакшне:
- Down-миграция, дропающая
handle, уничтожает все данные, которые новый код в неё записал. - Если up-миграция была
ADD COLUMN, down — этоDROP COLUMN— деструктивно.
Откат в продакшне идёт вперёд: напишите новую миграцию, исправляющую проблему, задеплойте её и двигайтесь вперёд. Рассматривайте down-миграции только как scaffolding для разработки.
- Типичная длительность переименования колонки
- 3–7 дней
- Количество деплоев при переименовании колонки
- 5+ отдельных PR
- Размер пакета бэкфила
- 1к–10к строк
- pg_sleep между пакетами
- 0.1 с (пространство для дыхания)
- Каждая фаза: независимо откатываема?
- Да
- Направление отката в продакшне
- Вперёд (новая миграция)
Почему это работает
pgroll (от Xata) автоматизирует expand-contract через Postgres-представления. При объявлении миграции, “переименовывающей username в handle”, pgroll создаёт представление v1 (раскрывает username) и представление v2 (раскрывает handle), оба поверх одной физической колонки, с триггерами для трансляции записей. Старые поды подключаются с search_path = v1; новые — с search_path = v2. По завершении миграции представление v1 дропается. Это сворачивает 5+ деплоев в одну декларативную миграцию — ценой сложности view-слоя при отладке и в выводе pg_dump.
Почему прямая миграция RENAME COLUMN может вызвать инцидент во время rolling deploy?
На какой фазе expand-contract откат наиболее прост и почему?
Команда запускает down-миграцию в продакшне, которая дропает новую колонку, добавленную в up-миграции. Какие данные теряются?
- 01Сформулируйте главный инвариант expand-contract и объясните, почему пропуск фазы dual-write его нарушает.
- 02Пройдите по шести фазам переименования: username → handle, назвав действие в миграции или коде на каждом шаге.
- 03Почему down-миграции небезопасны в продакшне и какова правильная стратегия отката в продакшне?
Expand-contract — единственный правильный паттерн для миграций с нулевым простоем при переименовании, удалении или смене типа. Он поддерживает суперсет схемы в каждый момент: и старая, и новая версии кода работают одновременно со схемой, поддерживающей обе. Переименование колонки превращается в пять независимых деплоев за 3–7 дней: добавить новую колонку (expand), задеплоить код с dual-write, выполнить бэкфил исторических строк пакетами, задеплоить код с переключением чтений, задеплоить код с остановкой старых записей, затем дропнуть старую колонку (contract). Down-миграции кажутся безопасными, но уничтожают данные, уже записанные новым кодом; откат в продакшне — всегда прямая миграция, исправляющая проблему.
встречается в140
- Почему GraphQL получает N+1junior
- Механика DataLoader: батчинг на границе тикаmiddle
- Контракты batch-функции: порядок, формы, ошибкиmiddle
- Federation и lookahead: батчинг за пределами DataLoadermiddle
- Защита сложности запросов: depth, cost, persisted queriesmiddle
- Senior GraphQL API: scheduling-контракт, изоляция арендаторов, наблюдаемостьsenior
- Зачем идемпотентность: безопасные retryjunior
- Серверный state machine: четыре состояния idempotency keymiddle
- Outbox и inbox: effectively-once через dual-write границуmiddle
- Конкурентность и архитектура кеша для идемпотентности на масштабеsenior
- Наблюдаемость, production-инциденты и дизайн для глобального масштабаsenior
- Event loop: один поток, три очередиjunior
- Задачи, микрозадачи и scheduler.yield()middle
- Голодание микрозадач, длинные задачи и LoAFsenior
- Event loop Node.js: фазы, nextTick и задержка циклаsenior
- React, Vue и наблюдаемость INP в продакшенеsenior
- Render pipeline: шесть стадий от байтов до пикселейjunior
- Цена стадий и модель процесса рендерераmiddle
- Инвалидация, dirty-биты и containmiddle
- Слои композитора: продвижение, перекрытие и память GPUmiddle
- Флейм-стрип DevTools и жизненный цикл кадраmiddle
- Layout thrash: форсированная синхронная компоновкаsenior
- BeginMainFrame, анимации на потоке compositor и память GPUsenior
- Observability в проде: LoAF, INP и полная поверхность атакиsenior
- Что такое V8 и почему производительность различается в 100 разjunior
- Четырёхуровневый JIT-конвейер V8 и профилированная тиеризацияmiddle
- Hidden classes, деревья переходов и расположение в памятиmiddle
- Inline caches, состояния IC и деоптимизацияmiddle
- Orinoco GC: параллельный scavenger, конкурентная разметка и барьеры записиmiddle
- Спекулятивный движок TurboFan и ловушка deopt-loopsenior
- V8 в production: Isolates, сжатие указателей и реальные аварииsenior
- Жизненный цикл service worker и стратегии кешированияmiddle
- Граничные случаи service worker: version skew, долговременность и ловушка навигацииsenior
- Что делает реконсилер: render vs commitjunior
- Объект fiber и дерево с двойной буферизациейmiddle
- Чистота фазы render и подшаги фазы commitmiddle
- Реконсиляция: эвристики диффа и ловушка ключейmiddle
- Приоритетные lanes, time-slicing и useTransitionmiddle
- Bailout, мемоизация и tearingsenior
- React Profiler, компилятор и продакшн-наблюдаемостьsenior
- Стратегии рендеринга: SSG, SSR, ISR, streaming и гидратацияjunior
- SSG, SSR, ISR, streaming и RSC — как работает каждая стратегияmiddle
- Цена гидратации: selective, progressive, острова, resumabilitymiddle
- Hydration mismatch: причины, обнаружение и правило детерминизмаsenior
- RSC, стратегия на маршрут и production-наблюдаемостьsenior
- Core Web Vitals: что измеряют LCP, INP и CLSjunior
- CLS: почему происходят сдвиги лейаута и как их остановитьmiddle
- Трейдоффы метрик, RUM-атрибуция и цикл CI+полеsenior
- Общая картина: от URL до LCP до INP как эстафетаjunior
- Восемь слоёв трассировки: от service worker до второй навигацииmiddle
- Пять канонических поломок: где производство стабильно ломаетсяsenior
- Метод трёх треков: чтение трасс и построение системы мониторингаsenior
- Что такое cache stampede и почему он делает всё хужеjunior
- Лок и single-flight: ограничение параллельных rebuildmiddle
- XFetch: вероятностное раннее истечение без координацииmiddle
- Stale-while-revalidate и CDN request coalescingmiddle
- Детектирование stampede и дизайн TTL для продакшенаmiddle
- Метастабильный сбой, fencing-токены и production-постмортемыsenior
- Роли Raft, term и почему majority-кворум предотвращает split brainjunior
- Как Raft реплицирует log entry и решает, что его безопасно коммититьmiddle
- Выборы лидера в Raft: таймауты, правила голосования и четыре свойства безопасностиmiddle
- Raft в реальном мире: partition, медленный диск и клиентская маршрутизацияmiddle
- Расширения Raft: pre-vote, learner, snapshot и линеаризуемые чтенияsenior
- Raft в production: membership change, Multi-Raft и observabilitysenior
- Где происходит data fetching — и почему это решает LCPjunior
- Fetch waterfall''''ы — диагностика и лечение через Promise.allmiddle
- React Server Components и Suspense streamingmiddle
- Клиентский кэш: TanStack Query, SWR и stale-while-revalidatemiddle
- LCP, prefetch и race conditions в интерактивном fetchingmiddle
- Senior internals: RSC payload, слои кэша и production паденияsenior
- Трёхстороннее рукопожатие TCPjunior
- Номера последовательности и состояние соединенияmiddle
- DNS: что делает и зачем существуетjunior
- Обход резолвера: перенаправления, типы записей и gluemiddle
- TTL, кеширование и распространение DNSmiddle
- Рукопожатие за 1 RTT: key share и ECDHEmiddle
- Возобновление сессии и 0-RTTmiddle
- WebSocket: HTTP-апгрейд до постоянного соединенияjunior
- Формат WebSocket-фрейма: opcodes, маскирование, фрагментацияmiddle
- Backpressure в WebSocket: когда клиенты не успеваютmiddle
- Реконнект: jittered backoff, thundering herd, восстановление сообщенийsenior
- WebSocket в масштабе: HTTP/2 мультиплексирование, permessage-deflate, C10Msenior
- WebSocket в production: прокси, безопасность и распределённая архитектураsenior
- Что делают обратные проксиjunior
- Health checks, connection draining и slow startmiddle
- Session affinity, consistent hashing и правильное решениеmiddle
- Retry-бури, circuit breakers и load sheddingsenior
- Устойчивая архитектура LB: anycast, zone-aware маршрутизация и observabilitysenior
- Почему QUIC, а не TCP+TLSjunior
- Connection ID и миграция сетиmiddle
- Возобновление 0-RTT и шифрование пакетовsenior
- DDoS: что это и почему работаетjunior
- Атаки усиления и истощение состоянияmiddle
- Ограничение скорости: алгоритмы и архитектураmiddle
- WAF, межсетевые экраны, mTLS и HSTSmiddle
- Отравление DNS-кэша и BGP-перехватsenior
- Эшелонированная защита и экономика атакsenior
- DNS, TCP, TLS по очереди: куда уходят миллисекундыmiddle
- Перехват прокси и шлюзы безопасности: rate limiter, WAF, mTLSmiddle
- Альтернативные пути: QUIC 0-RTT, WebSocket upgrade, миграция соединенияmiddle
- Наблюдаемость: распределённые трейсы, USE/RED и семплированиеsenior
- Устойчивость: каскадные повторы, circuit breakers и error budgetsenior
- Что такое три сигнала: метрики, логи, трейсыjunior
- Зачем нужны структурные логи: дневник против таблицыjunior
- Схема продакшн-лога: поля, которые несёт каждая строкаmiddle
- PII-редакция и log injectionsenior
- OTel Logs Data Model и audit-логи как подсистемаsenior
- SLI, SLO и error budget: надёжность в числахjunior
- Error budget policy, latency SLO и составные journeysmiddle
- Продакшн-отказы SLO, самонаблюдаемость, безопасность и общая картинаsenior
- Петля инцидента: от пейджера до постмортема до предотвращенияmiddle
- Cache lines и false sharing: когда параллелизм замедляет кодmiddle
- SIMD и data layout: AoS vs SoA и разница в 4–8xmiddle
- Cache-oblivious алгоритмы, PGO и production failuressenior
- GC в production: наблюдаемость, безопасность, edge cases и управление флотомsenior
- Batching: амортизируй фиксированную цену каждой операцииjunior
- Окно батчинга: размер и время ожиданияmiddle
- Batching в Kafka и Postgresmiddle
- io_uring и наблюдаемость пакетированияmiddle
- От Nagle до io_uring: эволюция пакетированияmiddle
- Backpressure, изоляция сбоев и безопасность батчей в продакшенеsenior
- CI enforcement и RUM: делаем бюджеты рабочимиmiddle
- V8 JIT-пайплайн, HTTP-приоритеты и безопасность bundlesenior
- Цикл performance: дисциплина, а не проектjunior
- Классификация и исправление: сопоставление family bottleneck с методамиmiddle
- Observability-стек и CI gates: ловить регрессии до выпускаmiddle
- От инцидента к enforcement: SLO burn до верифицированного исправления за 35 минутmiddle
- Культура, экономика и масштаб performancesenior
- At-most-once, at-least-once, exactly-once: три контракта доставкиjunior
- Три ножки сбоя — где реально происходят дубликаты и потериmiddle
- Consumer-side dedup: самый дешёвый путь к exactly-once processingmiddle
- Kafka exactly-once semantics: idempotent producer и транзакцииmiddle
- SQS visibility timeout, DLQ и outbox patternmiddle
- Exactly-once в production: impossibility-доказательство, гибридные паттерны и реальные инцидентыsenior
- Что такое OAuth и почему пароли — не ответjunior
- Authorization code flow с PKCEmiddle
- Валидация ID-токена и управление JWKS-кешемmiddle
- Ротация refresh-токенов и scope-based least privilegemiddle
- Sender-constrained токены: DPoP и mTLSsenior
- OAuth в production: audience атаки, observability и реальные провалыsenior