Содержание
Приложение на Gin и GORM записывало время в таблицу через Go, а база добавляла значения через NOW(). В итоге одна и та же запись хранила метки из трёх разных источников: UTC из приложения, времени сессии базы и момента транзакции. Разница в три часа приводила к тому, что фоновая джоба считала резервы просроченными через пять минут вместо тридцати. Ошибка воспроизводилась стабильно, потому что стейдж использовал ту же конфигурацию PgBouncer и не ловил расхождение. Такая ситуация важна для принятия решения о внедрении: если проект полагается на автоматические метки времени в фоновых задачах, расхождения могут привести к преждевременной обработке данных или потере актуальности резервов, что особенно критично в системах бронирования и очередей.
Разные типы и их поведение
В PostgreSQL два основных типа для хранения времени отличаются по смыслу. Timestamp without time zone хранит только цифры даты и часа без привязки к поясу, поэтому база воспринимает любое переданное значение как локальное и не выполняет конвертации. Timestamptz хранит момент в UTC и при выводе переводит его в таймзону текущей сессии, что делает его предпочтительным для распределённых систем. Приложение записывало в такие колонки значения из time.Time, приведённые к UTC, но база воспринимала их как локальное время. Когда код и база пишут в одну таблицу разными способами, значения перестают быть сопоставимыми. Это важно учитывать при выборе архитектуры: использование timestamp without time zone без явной нормализации повышает риск ошибок при сравнении, тогда как timestamptz снижает такие риски, но требует согласования таймзоны сессии.
В описанном случае колонки reserved_at и reserved_until заполнялись из Go в UTC, а created_at и prepared_at — через DEFAULT now(). В результате на сервере с московской таймзоной сессии разница между двумя группами значений составляла ровно три часа. Джоба, сравнивающая reserved_until с NOW(), получала момент из другой зоны и принимала свежие резервы за истёкшие. Для решения о доработке важно понимать, что такие несоответствия остаются незаметными до момента запуска фоновых процессов и могут привести к массовой обработке ещё актуальных записей.
Как найти расхождение в проде
Проверка показала, что released_at оказывался на двадцать пять минут раньше reserved_until, а deleted_at — раньше created_at. Запрос джобы использовал условие reserved_until <= NOW() — interval ‘5 minutes’. Поскольку NOW() возвращал время сессии, а не UTC, условие срабатывало преждевременно. Логи приложения и базы не содержали явных ошибок, потому что каждый компонент работал внутри своей таймзоны. Чтобы обнаружить проблему, достаточно выполнить SHOW timezone в том же соединении, которое использует приложение. Значение, отличное от UTC, сразу указывает на источник расхождения. Дополнительно стоит сравнить значения clock_timestamp() и NOW() внутри длинной транзакции: первое меняется, второе — нет. Такая диагностика помогает принять решение о необходимости централизованной нормализации времени, поскольку расхождения в проде часто проявляются только под нагрузкой и влияют на надёжность фоновых задач.
Проверка при старте и российский контекст
В российских проектах часто используют PgBouncer и несколько инстансов PostgreSQL с разными настройками сервера. Таймзона может задаваться на уровне роли, базы или самого подключения, поэтому значение в psql не отражает реальную картину для приложения. Рекомендуется при старте сервиса явно выполнять SET TimeZone = ‘UTC’ и логировать результат SHOW timezone. Это позволяет сразу отловить расхождение до того, как оно проявится на пользовательских данных. Такая проверка особенно важна, когда код на Go использует pgx в режиме simple protocol и GORM с автоматическими now(). В этом случае приложение и база пишут время независимо, и без явной нормализации на UTC расхождения неизбежны. Команды, которые переносят стейдж в прод без отдельной проверки таймзоны соединений, рискуют получить те же пять минут вместо тридцати на любых фоновых задачах, зависящих от сравнения с NOW(). При оценке рисков стоит учитывать, что PgBouncer переиспользует соединения, поэтому настройка на уровне пула влияет на все экземпляры и снижает вероятность скрытых расхождений.
Источник
Это краткий разбор материала Habr. Полная версия с примерами кода, схемами и деталями реализации — в оригинале: Три источника времени ломают резервы в PostgreSQL.
Курс по теме — Каталог курсов Школы Больших Данных


