惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
B
Blog RSS Feed
WordPress大学
WordPress大学
SecWiki News
SecWiki News
W
WeLiveSecurity
Hacker News: Ask HN
Hacker News: Ask HN
The Hacker News
The Hacker News
T
Troy Hunt's Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
S
Securelist
P
Privacy International News Feed
P
Palo Alto Networks Blog
Spread Privacy
Spread Privacy
N
News and Events Feed by Topic
E
Exploit-DB.com RSS Feed
T
Threatpost
Stack Overflow Blog
Stack Overflow Blog
GbyAI
GbyAI
博客园_首页
Y
Y Combinator Blog
P
Proofpoint News Feed
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
大猫的无限游戏
大猫的无限游戏
The Last Watchdog
The Last Watchdog
T
The Blog of Author Tim Ferriss
Martin Fowler
Martin Fowler
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Know Your Adversary
Know Your Adversary
Hugging Face - Blog
Hugging Face - Blog
O
OpenAI News
博客园 - 叶小钗
阮一峰的网络日志
阮一峰的网络日志
V
V2EX - 技术
L
LangChain Blog
Scott Helme
Scott Helme
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Hacker News - Newest:
Hacker News - Newest: "LLM"
aimingoo的专栏
aimingoo的专栏
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Microsoft Security Blog
Microsoft Security Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Secure Thoughts
Vercel News
Vercel News
Help Net Security
Help Net Security
The GitHub Blog
The GitHub Blog
J
Java Code Geeks
MongoDB | Blog
MongoDB | Blog
美团技术团队
L
LINUX DO - 热门话题
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Переименовал две колонки и поймал два инцидента
Игорь · 2026-06-15 · via Все публикации подряд на Хабре

Переименовал две колонки и поймал два инцидента

Средний

5 мин

7

Про безопасные миграции написано уже тысячу раз. Мы все наизусть знаем и про expand/contract, и про обратную совместимость, и про то, что схему нельзя ломать под трафиком. А потом всё равно наступаем на эти грабли.

Вот свежий случай. Была задача на полчаса, надо было переименовать две перепутанные колонки. Прод целиком не лёг, но за одну выкатку я собрал сразу два инцидента. Причём один прилетел изнутри сервиса, а второй снаружи, оттуда, откуда я вообще не ждал.

Дальше расскажу, как так вышло.

С чего всё началось

У нас есть таблица-связка seller_item_identifier_order_item, дальше буду звать её siioi. Она цепляет позицию FBS-заказа к её маркировке, то есть к IMEI или серийному номеру. В ней две ссылки:

  • order_item_id, которая должна вести на позицию заказа,

  • seller_item_identifier_id, которая должна вести на маркировку.

А исторически в эти колонки клали всё наоборот, и имена по факту врали. Работать-то оно работало, но любой, кто читал таблицу по названиям колонок, получал совсем не то, что ожидал.

Чинить мы решили по-честному, через expand/contract. Сначала добавляем правильные колонки и какое-то время живём на двух наборах сразу, а потом, когда всё устаканится, убираем старые. Ничего экзотического тут нет.

Фаза 1 прошла без сюрпризов

Фаза 1 прошла без сюрпризов

Фаза 1 прошла без сюрпризов

Мы добавили колонки real_order_item_id и real_seller_item_identifier_id, забэкфилили в них данные и перевели код на новые имена. А ещё повесили триггер, потому что во время rolling deploy в кластере какое-то время живут одновременно и старые поды, которые пишут в старые колонки, и новые, которые пишут в real_*. Чтобы обе пары всегда оставались в синхроне, и нужен был триггер:

CREATE TRIGGER siioi_sync_columns
    BEFORE INSERT ON public.seller_item_identifier_order_item
    FOR EACH ROW EXECUTE FUNCTION sync_siioi_columns();

Триггер двунаправленный, так что стоило заполнить одну пару колонок, и вторая подтягивалась сама. Прод пожил несколько дней, данные сошлись, всё было идеально. Тут мы и расслабились.

Фаза 2 и та самая «формальность»

Осталось убрать строительные леса, и на это ушло два changeset'а:

-- 1. снимаем триггер
DROP TRIGGER IF EXISTS siioi_sync_columns ON public.seller_item_identifier_order_item;
DROP FUNCTION IF EXISTS sync_siioi_columns();

-- 2. удаляем старые колонки
ALTER TABLE public.seller_item_identifier_order_item
    DROP COLUMN IF EXISTS order_item_id,
    DROP COLUMN IF EXISTS seller_item_identifier_id;

Казалось бы, что тут может пойти не так. А пошло не так вообще всё.

Инцидент №1, deadlock и пятисотки

Инцидент №1, deadlock и пятисотки

Инцидент №1, deadlock и пятисотки

Сначала всё шло по обычному пайплайну. Прогнали миграцию на dev, потом на stage и pre-prod, где данных примерно столько же, сколько на проде. Везде зелено, триггер снялся, колонки удалились, придраться не к чему. В прод выкатывались спокойно, как к формальности.

А на проде всё посыпалось прямо во время выката. Миграция упала с ошибкой посреди деплоя, в логах deadlock, и сама выкатка откатилась назад. Вот только откатилось не всё: первый changeset, который снимал триггер и функцию, успел закоммититься, поэтому обратно они не вернулись. А старые колонки так и остались с ограничением NOT NULL.

И тут захлопнулась ловушка. Приложение давно пишет только в новые real_*, синхронизировать их со старыми больше некому, а база на каждый INSERT ругается, что старые NOT NULL колонки нельзя оставлять пустыми. Через семь минут после старта легла ручка создания идентификаторов: 100% пятисоток, продавцы не могут добавить маркировку, а без неё не собрать заказ.

Откуда взялся сам deadlock, спросите вы. А вот из-за этого с виду безобидного ALTER:

deadlock

deadlock

ALTER TABLE ...
    DROP COLUMN order_item_id,
    DROP COLUMN seller_item_identifier_id;

Дело в том, что обе колонки несут на себе внешние ключи. И когда мы удаляем их одним стейтментом, Postgres берёт AccessExclusiveLock не только на саму siioi, но сразу и на обе таблицы, на которые ссылаются эти ключи. А в это время под трафиком крутятся обычные SELECT с джойнами, и они держат блокировки на тех же таблицах, только в другом порядке. Вот вам и встречка, в которой кто-то должен уступить, а уступает, как водится, миграция.

А почему тогда на dev и stage всё прошло гладко? Дело не в данных, их на стендах хватало. Просто deadlock рождается только из конкуренции за блокировки, а на тестовых стендах нет нагрузки и нет встречных запросов, так что опасное окно не успевает возникнуть. Тесты зелёные, прод красный, классика.

Чинили мы по сути тем, что доделали то, что должно было оказаться в миграции с самого начала:

-- сначала снимаем обязательность
ALTER TABLE ... ALTER COLUMN order_item_id DROP NOT NULL,
                ALTER COLUMN seller_item_identifier_id DROP NOT NULL;
-- и только потом дропаем колонки, причём по одной, а не пачкой
ALTER TABLE ... DROP COLUMN order_item_id;
ALTER TABLE ... DROP COLUMN seller_item_identifier_id;

В сумме где-то час импакта. А урок такой:

В contract-фазе сначала снимайте ограничения через DROP NOT NULL и только потом убирайте механизмы вроде триггера. Тогда даже если какой-то шаг упадёт, запись всё равно продолжит работать. По-хорошему каждое промежуточное состояние миграции должно быть рабочим само по себе, потому что любой шаг может не доехать до конца.

И есть второй урок, поменьше. Любой DDL под трафиком стоит резать на мелкие атомарные стейтменты, а multi-column DROP по колонкам с внешними ключами лучше не делать вообще.

Инцидент №2, или как о вашу схему спотыкается тот, о ком вы не знали

Пока мы тушили первый пожар, прилетел второй, на этот раз из дата-инженерии:

column siioi.seller_item_identifier_id does not exist

Оказалось, что DWH-DAG ходит в нашу боевую базу напрямую и джойнит ровно по тем колонкам, которые мы только что удалили. В результате витрина в ClickHouse встала, а аналитика поехала на устаревших данных.

И вот это, честно говоря, бьёт больнее, чем deadlock. Со стороны сервиса у нас всё было чисто: код переведён, тесты зелёные, миграция применена. Но штука в том, что схема базы вообще-то не приватная. Её читали снаружи, в обход любого нашего API, и для DWH имена колонок были полноценным контрактом. А наш «ренейм на полчаса» оказался для них ломающим изменением, про которое мы даже не предупредили, потому что про этого потребителя попросту не знали.

Любой, кто читает вашу таблицу напрямую, по факту становится клиентом вашего API, подписывались вы на это или нет. И ренейм колонки для него такой же breaking change, как ломающее изменение в REST или Kafka, так что и анонсировать его надо так же.

А как тогда правильно

«Не давайте читать базу напрямую» звучит красиво, а на практике где-нибудь да читают. Поэтому самое важное это вообще знать, кто к тебе ходит. У нас посыпалось не из-за кривой миграции, а из-за того, что про потребителя мы не знали, а контракт, о котором ты не в курсе, защитить нельзя. Так что минимум, который окупается всегда, это видимость зависимостей: каталог данных, lineage или хоть строчка в вики «сюда напрямую ходит DWH».

А дальше прямой доступ стоит спрятать за стабильным слоем. Дёшево это view на реплике с зафиксированными именами полей: переименовывай физические колонки сколько хочешь, снаружи ничего не меняется, тот же expand/contract со стороны чтения. По-хорошему же такие данные лучше отдавать не из боевой базы, а потоком доменных событий через outbox, который у нас и так есть, тогда схема развязана с потребителем полностью. Ну а если прямой доступ остаётся, относимся к нему как к публичному API: data-контракт, период депрекейта, предупреждение. Тихий DROP COLUMN тут не вариант.

Что в сухом остатке

Чек-лист на следующий contract

Чек-лист на следующий contract

Самое любопытное, что оба инцидента породил не сам рефакторинг, а уборка после него, тот самый contract-шаг, на который обычно не закладывают время.

Добавлять почти всегда безопасно, а беда начинается там, где что-то перестаёт существовать. И ломается сразу с двух сторон: изнутри, если промежуточные состояния миграции невалидны или DDL дерётся с трафиком за блокировки, и снаружи, если у удаляемого нашёлся читатель, которого ты не посчитал. Зелёный CI не ловит ни то, ни другое, потому что для deadlock нужна нагрузка, а скрытый потребитель живёт в чужом репозитории.

Проблема стара как мир. Но мы всё равно на неё наступаем, потому что «удалить пару колонок» звучит безопасно ровно до того момента, пока ты их не удалил.