PostgreSQL проти Redis для сесій, лімітів і черг

Було корисно?

Системи в продакшені рідко падають тому, що ви обрали «не ту базу даних». Вони падають тому, що ви обрали правильну базу для неправильної поведінки.
Сесії, що переживають деплої, ліміти, які не повинні скидатися при рестартах, черги, які не можуть втратити завдання, але й не повинні блокувати весь сервіс — це приклади поведінки.

Якщо ви зберігаєте все в одному місці, бо так зручно, ви заплатите пізніше. Зазвичай о 2:17 ночі, коли ваш «прості кеш»
стає системою автентифікації й канал інцидентів починає кардіо.

Рамки рішення: обирайте за моделлю відмов, а не за відчуттями

PostgreSQL і Redis мають достатньо перетинів, щоб спокусити вас зробити релігійний вибір. Опирайтеся цьому.
Обирайте, виходячи з того, що має бути правдою під час відмов: рестарти, розділення мережі, перевантаження, часткові записи, зсув часу, повтори клієнтів і rolling deploy.

Одне грубе правило

Якщо втрата припустима (або можливе відновлення), Redis — хороший вибір за замовчуванням. Якщо втрата неприйнятна (або болюча з юридичної/фінансової точки зору),
PostgreSQL — «дорослий» у кімнаті.

Для чого кожна система «на практиці»

  • Redis: швидка спільна пам’ять з мережею, атомарні операції, TTL, кілька структур даних. Стійкість до втрати даних — опційна і делікатна.
  • PostgreSQL: транзакційна система з write-ahead логом, обмеженнями, запитністю і стійкою семантикою за відомих умов.

Як одна й та ж вимога звучить по-різному на нараді після інциденту

«Ми зберігаємо сесії в Redis» — це проєктне рішення. «Перезапуск Redis вивів усіх із системи» — це звіт по інциденту.
Ваше завдання — перекласти проєктні твердження в інцидентні твердження до того, як продакшен зробить це за вас.

Цитата про надійність (парафраз)

Парафразована ідея: надія — не стратегія — приписується багатьом операційним лідерам; ця думка поширена в інженерії надійності.

Дві короткі жарти (саме дві)

Жарт №1: Кеш — це місце, куди дані йдуть на пенсію. Хіба що ви зберігаєте там автентифікацію — тоді це кар’єра з нічними змінами.

Жарт №2: Кожен «тимчасовий» ключ Redis живе рівно стільки, скільки ваша ротація on-call.

Матриця прийняття рішення (корисна під час суперечок)

Задавайте ці питання в порядку; зупиніться, коли отримаєте «жорстку ні».

  1. Чи можете ви терпіти втрату даних? Якщо ні: віддавайте перевагу PostgreSQL (або іншому стійкому сховищу/черзі).
  2. Потрібні атомарні інкременти/TTL під високим навантаженням? Якщо так: часто виграє Redis.
  3. Потрібні ad-hoc запити, аудит або бекфіл? Якщо так: PostgreSQL виграє.
  4. Потрібен fan-out / streams / consumer groups? Redis може бути відмінним, але зобов’язуйтеся експлуатувати його як систему, а не як іграшку.
  5. Потрібне суворе транзакційне зв’язування з бізнес-записами? Якщо так: PostgreSQL, бо «write в додатку + write в Redis» — це місце, де консистентність вмирає.

Модель загроз: відмови, які ви повинні проєктувати

  • Рестарт: перезапуск процесів, ребути нод, переселення контейнерів.
  • Розділення мережі: додаток бачить одну ноду сховища, але не іншу; клієнти повторюють запити.
  • Перевантаження: стрибки латентності, наростання черги, таймаути перетворюються на повтори, повтори — на шторм.
  • Час: TTL і вікна ліміту залежать від часу; годинники дрейфують, деплої_roll, користувачі нетерплячі.
  • Виведення ключів: Redis може евіктити ключі; «volatile-lru» не є планом безперервності бізнесу.
  • Вакуум/компактація: блоут Postgres та vacuum впливають на латентність; Redis форк для RDB снапшотів впливає на пам’ять та затримки.

Цікаві факти та історичний контекст (те, що змінює рішення)

  • Родовід PostgreSQL веде від POSTGRES (1980-ті, UC Berkeley), спроєктований з дослідницьким акцентом на коректність і розширюваність — це досі видно в MVCC і WAL.
  • Redis почався наприкінці 2000-х як сервер структур даних в пам’яті; його ключова можливість — не просто «ключ/значення», а атомарні операції над корисними структурами (списки, множини, відсортовані множини).
  • Постійність Redis почалася як опційні снапшоти (RDB). Append-only file (AOF) з’явився, щоб зменшити вікно втрати, але обмінює надійність на посилення записів і вибір fsync.
  • Postgres MVCC означає, що читання зазвичай не блокують записи. Це також означає накопичення мертвих кортежів; vacuum не опціональний, якщо ви любите стабільну латентність.
  • Однопоточне виконання команд у Redis — це фіча: вона робить більшість операцій атомарними без локів. Це також межа, коли ви навантажуєте важкими Lua-скриптами або тривалими командами.
  • Черги на основі LIST у Redis були популярні до появи streams; патерни BRPOP формували покоління «достатньо хороших» систем обробки завдань — поки людям не знадобився replay і consumer groups.
  • «Exactly-once processing» — повторювана ілюзія індустрії. Більшість реальних систем досягають at-least-once плюс ідемпотентність. Postgres полегшує забезпечення ідемпотентності через обмеження.
  • Еволюція лімітування відбулася від простих фіксованих вікон до ковзних вікон і token bucket, бо справедливість важлива при сплесках; атомарні інкременти Redis зробили ці патерни практичними в масштабі.

Сесії: прив’язка, стателесс і проміжна брехня

Що насправді означає «зберігання сесій»

Сесії — це стан, але додаток хоче вдавати, що він stateless. Це напруга проявляється в трьох місцях:
автентифікація, скасування доступу й термін дії.

Небезпечне припущення — що сесії це «лише кеш». Вони такі лише, якщо вам справді байдуже, що користувач буде викинутий із сесії або повторно перевірений.
Для споживчих додатків це може бути прийнятно. Для B2B адмінки під час демонстрації продажу — це шлях до наступного скорочення бюджету.

Три патерни й де вони підходять

  1. Підписані токени (без сесії на стороні сервера)
    Нічого не зберігається на сервері; поміщайте клейми в підписаний токен (подібний до JWT). Чудово для API з переважно читальним навантаженням. Жахливо, коли потрібне миттєве скасування
    або короткі TTL спричиняють хвилі рефрешів.
  2. Серверні сесії
    Зберігайте session ID у cookie, тримайте дані сесії в Redis або Postgres. Операційно нудно — це комплімент.
  3. Гібрид
    Підписаний токен для ідентичності + серверний чорний список/список скасування. Тут сильний Redis: малі ключі, TTL, атомарні перевірки.

Redis для сесій: коли це правильний вибір

Redis добре підходить для сесій, коли:

  • Втрати сесій прийнятні (або можна легко переавторизуватися).
  • Потрібні швидкі читання й TTL-терміни без завдань очищення.
  • Ви дисципліновані щодо постійності (або свідомо обираєте допустимість втрат).
  • Ви можете терпіти іноді «всі залогіньтесь знову» день.

Redis для сесій: коли це пастка

Це пастка, коли сесія фактично є записом про права (ролі, scope-и, прапорці MFA) і втрата означає:
відновлення скасованого доступу або зникнення доступу для валідних користувачів. Обидва випадки породжують звернення в підтримку.

Також: політики евіктування Redis не турбуються про вашу воронку продажів. Якщо пам’ять тисне і ваші ключі сесій підлягають евіктації,
ви випадково створили лотерею масових виходів з сесій.

PostgreSQL для сесій: нудно й правильно

Сесії в Postgres повільніші на запит, але передбачувані й запитувані. Ви можете забезпечити унікальність, відстежувати last_seen, робити аудит
і запускати бекфіл для очищення. А також зв’язувати зміни стану сесії з бізнес-записами в рамках транзакцій.

Компроміс: ви тепер відповідаєте за очищення й контроль блоуту. Таблиці сесій дуже швидко створюють churn. Якщо ви не керуєте vacuum, побачите поступове збільшення латентності.

Практичний дизайн: розділяйте “ідентичність сесії” й “payload сесії”

Хороший компроміс: зберігайте мінімальний авторитетний запис у Postgres (session id, user id, created_at, revoked_at, expires_at),
а необов’язковий performant payload — у Redis (налаштування користувача, обчислені права) з ключем за session id і TTL.
Якщо Redis губить payload — ви його перераховуєте. Якщо Postgres втрачає авторитетний запис — у вас більші проблеми.

Ліміти: лічильники, часові вікна й справедливість

Що захищає лімітування

Лімітування — це не лише «зупинити зловмисників». Це також:
захист downstream-залежностей, формування шумних орендарів, уникнення thundering herds під час логінів або скидання паролю,
і запобігання самозавданим повторним викликам, які з’їдають весь ваш бюджет.

Redis для лімітування: дефолт з причин

Redis відмінний для лімітування, бо має:

  • Атомарні інкременти (INCR/INCRBY), щоб уникнути гонок.
  • TTL (EXPIRE), щоб лічильники зникали без cron-ів.
  • Lua-скрипти для поєднання «інкремент + перевірка + встановити TTL» в одну атомарну операцію.
  • Низьку латентність, щоб лімітер не став вузьким місцем.

Але: стійкість Redis не безкоштовна

Якщо ваш лімітер скидається при рестарті, це може бути прийнятно. Або ви можете залити партнера API на 90 секунд і отримати від нього бан.
Визначте, в якому світі ви живете.

Якщо потрібно «вижити після рестарту», налаштуйте постійність продумано й протестуйте її. AOF з політиками fsync може допомогти,
але він змінює робоче навантаження і режим відмов (disk IO стає обмежувачем).

Postgres для лімітування: коли варто застосувати

Postgres може реалізовувати лімітування, зазвичай у таких формах:

  • Лічильники по вікну з upserts (INSERT … ON CONFLICT DO UPDATE). Працює для помірних швидкостей і жорстких вимог до коректності.
  • Token bucket збережений в рядку на користувача/орендаря з полями “last_refill” і “tokens”. Потребує уважного поводження з часом і конкуренцією.
  • Leaky bucket через джоби, де база авторитарна, а додаток кешує рішення короткочасно.

Postgres повільніший, але дає аудит і можливість запитів. Якщо треба відповісти «чому орендар X був затриманий вчора»,
Postgres — місце, де ця історія найпростіша для реконструкції.

Справедливість — це продуктове рішення, замасковане під алгоритм

Fixed-window ліміти прості й несправедливі на межах. Sliding window logs — справедливі й дорогі. Token bucket — достатньо справедливий
і досить дешевий. Головне: оберіть рівень справедливості, який ви можете підтримувати операційно. Класний алгоритм, який ви не можете відлагодити під час інциденту,
— це тягар.

Черги: надійність, видимість і зворотний тиск

Черга — це не просто список

Реальна черга — це контракт:
повідомлення надійно записуються, робітники захоплюють їх, при невдачі повідомлення повертаються, дублікати трапляються, і система залишається спостережуваною.
Якщо ви реалізуєте черги як «список плюс надія», ви пізнаєте крайні випадки в гіршому середовищі: продакшені.

Redis-черги: швидко, гнучко і легко зробити неправильно

Redis може реалізувати черги за допомогою списків (LPUSH/BRPOP), відсортованих множин (відкладені задачі), streams (consumer groups) і pub/sub (не черга).
Кожен підхід має свої компроміси:

  • Списки: прості, швидкі. Але вам потрібна власна модель надійності (ack, retry, dead-letter). Патерни BRPOPLPUSH допомагають.
  • Streams: ближче до реального логу з consumer groups, підтвердженнями й pending entries. Операційно складніше, але чесніше.
  • Pub/Sub: не надійний; підписники, що відключилися, пропускають повідомлення. Чудово для ефермерних сповіщень, але не для задач.

Postgres-черги: несподівано сильні для багатьох навантажень

Postgres може живити черги, використовуючи таблиці + індекси + блокування рядків:

  • SELECT … FOR UPDATE SKIP LOCKED — робоча конячка для «захопити задачу, щоб двоє робітників не взяли її одночасно».
  • Транзакційне enqueue дозволяє зв’язати створення задачі з бізнес-станом (outbox pattern).
  • Здатність до запитів дозволяє будувати дашборди й досліджувати застряглі задачі без кастомних інструментів.

Компроміс — пропускна здатність і конкуренція. Postgres чудовий, поки ним не стане: високі швидкості записів, гарячі партиції або довгі транзакції
можуть перетворити таблицю черг у поле бою. Але для багатьох корпоративних систем — помірні навантаження, висока коректність — Postgres-черги
— нудний вибір, що реально працює.

Visibility timeout: те, що визначає ваш сон

Задачі потребують концепції «виконується». Якщо робітник помирає посеред виконання, задача має стати знову видимою. Redis-спискові черги цього автоматично не дають.
Streams дають, але вам все одно треба обробляти pending entries. Postgres робить це, якщо ви моделюєте «locked_at/locked_by» і повертаєте задачі після таймауту.

Ідемпотентність: ви оброблятимете дублі

Між повторними викликами, таймаутами, мережевими розділеннями і деплоями — дублікати не гіпотетичні. «Exactly once» — це маркетинг.
Вбудуйте ідемпотентні ключі в обробники задач і забезпечуйте їх де можливо (унікальні обмеження Postgres — ваш найкращий друг).

Швидка діагностика: знайти вузьке місце за 10 хвилин

Вам подзвонили. Латентність зросла. Логіни падають. Черги ростуть. Не сперечайтеся про архітектуру. Запустіть план.

Почніть: визначте, де саме проблема — латентність сховища чи контенція в додатку

  1. Перевірте латентність Redis і заблоковані клієнти (якщо Redis у шляху). Якщо бачите повільні команди або blocked clients, ймовірно вузьке місце в Redis.
  2. Перевірте активні сесії Postgres і блокування. Якщо бачите wait locks або насичені з’єднання, ймовірно вузьке місце в Postgres.
  3. Перевірте патерни помилок: таймаути проти «OOM» проти «too many clients» проти «READONLY» допомагають швидко розставити пріоритети.

Друге: підтвердіть, ресурсна це проблема чи баги коректності

  1. Ресурсна насиченість: CPU високе, IO високе, мережа падає, пам’ять тисне, евіктація, пула з’єднань максимум.
  2. Баги коректності: раптовий сплеск повторів, випадковий tight loop, Lua-скрипт вийшов з-під контролю, необмежений ріст черги через відсутність ack.

Третє: оберіть найменш небезпечну міру

  • Жорсткіше лімітуйте (так, навіть якщо продукт скаржиться) для стабілізації.
  • Вимкніть дорогі фічі (збагачення сесії, глибокі перевірки прав), якщо вони б’ють по сховищу.
  • Поставте споживачів на паузу, якщо черга плавиться downstream-системи.
  • Масштабуйте читання, якщо навантаження дозволяє; не додавайте сліпо писачів, щоб вирішити проблему з локами.

Чого не робити

Не перезапускайте Redis як «фікс», якщо ви не готові до втрати волатильних даних і не знаєте, чому він завис. Не перезапускайте Postgres, коли є довгі транзакції,
якщо вам подобається пояснювати фінансам, чому рахунки дублювалися.

Практичні завдання: команди, виводи, що це означає, які рішення приймати

Це перевірки, які ви запускаєте під час дизайну і інцидентів. Кожна містить команду, приклад виводу,
що означає вивід і яке рішення він підштовхує.

Redis: здоров’я, латентність, постійність, пам’ять, евіктація

Задача 1: Перевірити базове здоров’я Redis і роль

cr0x@server:~$ redis-cli -h redis-01 INFO replication
# Replication
role:master
connected_slaves:1
master_repl_offset:987654321
repl_backlog_active:1

Значення: Ви на master; один replica підключений; реплікаційний backlog активний.

Рішення: Якщо роль несподівано «slave», ваші клієнти можуть писати в read-only ноду. Виправте service discovery/failover перш за все.

Задача 2: Заміряти стрибки латентності команд Redis

cr0x@server:~$ redis-cli -h redis-01 --latency -i 1
min: 0, max: 12, avg: 1.23 (176 samples)
min: 0, max: 97, avg: 4.87 (182 samples)

Значення: Іноді стрибки до 97ms. На шляху лімітування це боляче. При читанні сесій може спричинити таймаути/повтори.

Рішення: Досліджуйте повільні команди, форки для persistence або мережеву джиттер. Якщо стрибки корелюють зі збереженням RDB, налаштуйте постійність або перейдіть на AOF-настройки, що підходять.

Задача 3: Знайти повільні команди Redis (вбудований slowlog)

cr0x@server:~$ redis-cli -h redis-01 SLOWLOG GET 3
1) 1) (integer) 12231
   2) (integer) 1735600000
   3) (integer) 24567
   4) 1) "EVAL"
      2) "..."
   5) "10.0.2.41:53422"
   6) ""

Значення: Lua-скрипт тривав ~24ms. Кілька таких під навантаженням можуть серіалізувати сервер, бо Redis виконує команди в основному однопотоково.

Рішення: Перепишіть скрипти простіше, скоротіть сканування ключів або винесіть важку логіку з Redis. Якщо потрібна складна логіка черги, розгляньте Redis streams або Postgres.

Задача 4: Перевірити використання пам’яті і фрагментацію Redis

cr0x@server:~$ redis-cli -h redis-01 INFO memory | egrep 'used_memory_human|maxmemory_human|mem_fragmentation_ratio'
used_memory_human:18.42G
maxmemory_human:20.00G
mem_fragmentation_ratio:1.78

Значення: Ви близькі до maxmemory і фрагментація висока. Евіктації або OOM помилки близько; форк для persistence може не пройти.

Рішення: Збільшіть пам’ять, зменшіть кардинальність ключів, виправте TTL-стратегію або свідомо оберіть політику евіктації. Якщо сесії не можуть зникати, не покладайтеся на конфігурації, дружні до евіктацій.

Задача 5: Підтвердити поведінку евіктації

cr0x@server:~$ redis-cli -h redis-01 CONFIG GET maxmemory-policy
1) "maxmemory-policy"
2) "allkeys-lru"

Значення: Будь-який ключ може бути евіктований під пам’яттєвим тиском.

Рішення: Якщо ви зберігаєте тут сесії або стан черги, це ризик надійності. Розгляньте “volatile-ttl” для ключів тільки з TTL, або перемістіть авторитетний стан у Postgres.

Задача 6: Перевірити режим постійності й останнє збереження

cr0x@server:~$ redis-cli -h redis-01 INFO persistence | egrep 'aof_enabled|rdb_last_save_time|aof_last_write_status'
aof_enabled:1
aof_last_write_status:ok
rdb_last_save_time:1735600123

Значення: AOF увімкнено і пише успішно; RDB також є. У вас є певна стійкість, залежно від fsync-політики.

Рішення: Якщо цей Redis — авторитет для сесій або джерело черг, перевірте політику fsync і час відновлення. Якщо ви не можете терпіти втрат, Redis все ще може бути недостатнім.

Задача 7: Перевірити заблокованих клієнтів (часто пов’язано з чергами)

cr0x@server:~$ redis-cli -h redis-01 INFO clients | egrep 'blocked_clients|connected_clients'
connected_clients:1823
blocked_clients:312

Значення: Багато клієнтів заблоковано, ймовірно чекають на blocking pops або повільні скрипти. Це може бути нормальним для BRPOP-патернів, але 312 — багато.

Рішення: Якщо blocked clients корелюють з таймаутами, переробіть споживання черги (streams, обмежена конкуренція) або підвищте ефективність воркерів.

PostgreSQL: з’єднання, блоки, vacuum, блоут, IO, поведінка запитів

Задача 8: Перевірити насиченість з’єднань Postgres

cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select count(*) as total, state from pg_stat_activity group by state order by total desc;"
 total | state
-------+----------------
   120 | active
    80 | idle
    35 | idle in transaction

Значення: 35 сесій idle in transaction. Часто це баг або неправильне налаштування пула, і це блокує vacuum та утримує локи.

Рішення: Виправте додаток, щоб commit/rollback відбувалися швидко; встановіть statement timeouts; переконайтеся, що пул не залишає транзакції відкритими.

Задача 9: Виявити очікування блокувань

cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select wait_event_type, wait_event, count(*) from pg_stat_activity where wait_event is not null group by 1,2 order by 3 desc;"
 wait_event_type |   wait_event   | count
-----------------+----------------+-------
 Lock            | transactionid   |     9
 LWLock          | BufferMapping   |     4

Значення: Очікування блокувань на transaction IDs вказують на контенцію (оновлення/видалення, довгі транзакції), що впливає на інших.

Рішення: Знайдіть блокуючу транзакцію. Якщо це міграція або батч-робота — зупиніть або обмежте її. Якщо воркери черги утримують локи — виправте поведінку воркерів.

Задача 10: Знайти блокувальників

cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select pid, usename, state, now()-xact_start as xact_age, query from pg_stat_activity where state <> 'idle' order by xact_age desc limit 5;"
 pid  | usename | state  | xact_age |                  query
------+--------+--------+----------+------------------------------------------
 8421 | app    | active | 00:14:32 | update sessions set last_seen=now() ...
 9110 | app    | active | 00:09:11 | delete from queue_jobs where ...

Значення: Довгі записи в sessions/queue таблицях. Це гаряча зона для churn сесій і контенції черг.

Рішення: Додайте індекси, зменшіть частоту оновлень (write-behind для last_seen) і переконайтеся, що видалення з черги батчуються з розумними лімітами.

Задача 11: Перевірити стан vacuum для таблиці з великим churn

cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select relname, n_dead_tup, last_autovacuum, autovacuum_count from pg_stat_user_tables where relname in ('sessions','queue_jobs');"
  relname  | n_dead_tup |     last_autovacuum     | autovacuum_count
-----------+------------+-------------------------+------------------
 sessions  |    812334  | 2025-12-30 01:10:02+00  |              421
 queue_jobs|    120998  | 2025-12-30 01:08:47+00  |              388

Значення: Велика кількість мертвих кортежів. Autovacuum працює, але може відставати під навантаженням.

Рішення: Налаштуйте autovacuum для цих таблиць (знизьте пороги), розгляньте партиціонування за часом і зменшіть churn оновлень.

Задача 12: Переглянути використання індексів для sessions або таблиць лімітів

cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select relname, idx_scan, seq_scan from pg_stat_user_tables where relname='sessions';"
 relname  | idx_scan | seq_scan
----------+----------+----------
 sessions | 98234122 |     4132

Значення: Індексні скани домінують (добре). Якщо seq_scan було б високе — ймовірні повні сканування гарячої таблиці.

Рішення: Якщо seq_scan стрибнув, додайте/відновіть індекси або виправте предикати запитів. Для сесій вам потрібні пошуки по session_id і expires_at.

Задача 13: Заміряти кеш Postgres проти тиску на диск (грубий сигнал)

cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select datname, blks_hit, blks_read, round(100.0*blks_hit/nullif(blks_hit+blks_read,0),2) as hit_pct from pg_stat_database where datname='appdb';"
 datname | blks_hit  | blks_read | hit_pct
---------+-----------+-----------+---------
 appdb   | 891234567 |  23123456 |   97.46

Значення: Хіт кешу ~97%. Це непогано. Якщо він різко падає — диск може бути вузьким місцем.

Рішення: Якщо hit_pct падає, перевірте розмір робочого набору, індекси і чи новий патерн запитів читає багато холодних даних.

Задача 14: Перевірити backlog черги і вік найстарішої задачі (Postgres-черга)

cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "select count(*) as ready, min(now()-created_at) as oldest_age from queue_jobs where state='ready';"
 ready | oldest_age
-------+------------
 48211 | 02:41:18

Значення: Backlog великий; найстарша задача майже 3 години. Це проблема пропускної здатності або повільної залежної системи.

Рішення: Масштабуйте споживачів обережно, але спочатку підтвердіть, що БД не є вузьким місцем. Розгляньте призупинення продюсерів або застосування зворотного тиску вгорі.

Задача 15: Перевірити простір ключів Redis і поведінку TTL для сесій

cr0x@server:~$ redis-cli -h redis-01 INFO keyspace
# Keyspace
db0:keys=4123891,expires=4100022,avg_ttl=286000

Значення: Майже всі ключі мають TTL; середній TTL ~286 секунд. Типово для лімітів, ризиково для сесій, якщо не намірено.

Рішення: Якщо це сесії і TTL короткий — ви створюєте churn і виходи з системи. Налаштуйте TTL-стратегію, використайте refresh tokens або перемістіть авторитет у Postgres.

Задача 16: Підтвердити поведінку захоплення задачі в Postgres (SKIP LOCKED)

cr0x@server:~$ psql -h pg-01 -U app -d appdb -c "begin; select id from queue_jobs where state='ready' order by id limit 3 for update skip locked; commit;"
BEGIN
  id
------
 9912
 9913
 9914
(3 rows)
COMMIT

Значення: Робітники можуть безпечно захоплювати задачі без подвійної обробки завдяки блокуванню рядків.

Рішення: Якщо це блокується або повертає нічого при наявному backlog, дослідіть контенцію блокувань, відсутні індекси або задачі, що застрягли «in progress».

Три міні-історії з практики

1) Інцидент через неправильне припущення: «Сесії — це кеш»

Середня SaaS-компанія перевела сесії з Postgres у Redis, щоб «знизити навантаження». На папері все було чисто:
session_id → JSON blob, TTL 30 днів. Читання стали швидшими, графіки по базі гарні, всі пішли додому раніше.

Місяці потому трафік виріс і ноду Redis перезавантажили під час maintenance. Нічого страшного, думали вони — постійність була «увімкнена».
Вона була, технічно. Снапшоти кожні 15 хвилин.

Радіус ураження був миттєвий: користувачі виявилися вийденими з системи, але гірше — деякі контролі безпеки були зв’язані з payload сесії.
Частина прапорців «недавно підтверджено MFA» була втрачена і знову викликала MFA. Підтримка отримала купу тикетів, демо продажів зірвалися,
і інцидент мав той особливий тон, коли всі технічно спокійні, але емоційно розлючені.

Неправильне припущення було не в тому, що «Redis ненадійний». Redis робив саме те, для чого його налаштували.
Неправильне припущення було в тому, що сесії — «відновлюваний кеш». Вони такими не були. Сесії стали записом про надання прав.

Виправлення було нудним і ефективним: авторитетні записи сесій повернули в Postgres з таблицею скасувань.
Redis залишився, але тільки для похідного enrichment сесій з TTL. Також змінили runbook: будь-який перезапуск Redis розглядається як подія, що може вплинути на автентифікацію, поки не доведено протилежне.

2) Оптимізація, що повернулася бумерангом: Lua-скрипт лімітер

Інша компанія вела публічний API і робила лімітування в Redis. Спочатку це було просто INCR + EXPIRE.
Хтось покращив це Lua-скриптом, що реалізував sliding window лог. Справедливість покращилась, дашборди виглядали краще, і розробника хвалили.

Потім API запустив batch-фічу. Клієнти почали штурмувати ендпоінт вибухами — як це й роблять батчі.
Lua-скрипт тепер запускався на гарячих ключах з великими відсортованими множинами. Під навантаженням скрипт почав з’являтися в SLOWLOG.

Redis, як однопотоковий по виконанню команд, став вузьким місцем. Латентність зросла, клієнти таймаутилися, клієнти повторювали запити,
а повтори збільшили навантаження. Лімітер — призначений захищати систему — став головним режимом відмови системи.

Вони намагалися масштабувати Redis вертикально, що дало час, але не вирішення. Насправді фікс був у спрощенні:
token bucket з атомарними операціями і короткими ключами; прийняти невелику несправедливість на межах; додати розмазування по орендарях в додатку.
Справедливість — гарна, але виживання — ще краще.

Після інциденту додали правило: будь-яка зміна Lua-скрипта вимагає load-тесту і плану відкату.
Сценарії scripting у Redis потужні. Вони також найпростіший спосіб внести прихований глобальний лок у вашу архітектуру.

3) Нудна, але правильна практика, що врятувала день: Postgres outbox + ідемпотентність

Компанія обробляла платежі. Потрібно було емітити події «payment_succeeded» для тригерів листів, провізіонінгу та аналітики.
Вони використовували Postgres для основних даних і окрему систему воркерів. Хтось запропонував штовхати події прямо в Redis для швидкості.

Команда, яка вже обпалилася раніше, наполягла на outbox-таблиці в Postgres: коли рядок платежу комітиться, в тому ж транзакційному блоці створюється рядок в outbox.
Фоновий воркер читає рядки outbox з SKIP LOCKED, публікує downstream і помічає їх як виконані.
Це не гламурно. Це надзвичайно відлагоджувано.

Одного разу downstream-сервіс деградував і публікація подій уповільнилася. Backlog outbox виріс, але платежі йшли безпечно.
Оскільки outbox був у Postgres, команда могла query-ити точні застряглі стани, безпечно повторювати публікацію і виконувати цілеспрямоване очищення.

Рятівною деталлю була ідемпотентність: outbox мав унікальне обмеження на (event_type, aggregate_id, version).
Коли воркери повторювалися через таймаути, дублікати були нешкідливі. Обмеження забезпечувало контракт під час хаосу.

Інцидент закінчився без втрат даних, без подвійного провізіонування і без тижневої криміналістики.
Ніхто не отримав оплесків за «круту» архітектуру. Вони отримали краще: спокійний постмортем.

Типові помилки (симптоми → корінь проблеми → виправлення)

1) Випадкові виходи з системи й сплески «session not found»

Симптоми: Користувачі періодично викидаються; auth-сервіс показує cache misses; пам’ять Redis близька до max.

Корінь проблеми: Політика евіктації Redis дозволяє евіктити ключі сесій або вікно постійності втрачає останні сесії після рестарту.

Виправлення: Перенесіть авторитетний стан сесій в Postgres, або налаштуйте maxmemory-policy Redis, щоб уникнути евіктації сесій, і забезпечте постійність відповідно до tolerance втрат.

2) Ліміти скидаються після деплоя/рестарту

Симптоми: Після рестарту Redis трафік проходить лімітер; партнер API скаржиться; різке падіння 429 до нуля.

Корінь проблеми: Волатильний стан лімітеру Redis без стійкої персистентності або ключі лімітеру існують тільки в пам’яті.

Виправлення: Визначте, чи прийнятний скидання. Якщо ні, використайте AOF з відповідним fsync, або зберігайте лічильники в Postgres для критичних лімітів, або реалізуйте гібрид (швидкий шлях в Redis + періодична реконсиляція).

3) Backlog черги росте, а воркери виглядають «здоровими»

Симптоми: Воркери працюють, CPU низький, але вік задач зростає. Redis blocked_clients високі або є блокування в Postgres.

Корінь проблеми: Повільна downstream-залежність, неправильно налаштований visibility timeout або воркери застрягли на лаку або довгій транзакції.

Виправлення: Інструментуйте тривалість задач і латентність залежностей; реалізуйте таймаути; в Postgres-чергах уникайте довгих транзакцій і розділяйте claim+work від бізнес-записів, коли можливо.

4) Postgres-черга створює контенцію блоків і гальмує всю БД

Симптоми: Підвищені wait locks; зростання латентності для нерелевантних запитів; відставання autovacuum на таблиці черги.

Корінь проблеми: Гаряча таблиця черги з частими оновленнями/видаленнями; відсутні partial indexes; воркери занадто часто оновлюють рядки (heartbeats).

Виправлення: Використовуйте partial index для «ready»; оновлюйте мінімальні колонки; батчте видалення; партиціонуйте за часом; розгляньте переміщення високо-чергових ефермерних черг в Redis streams.

5) Redis-черга втрачає задачі при краху споживача

Симптоми: Задача зникає після падіння воркера; немає повтору; бізнес-процес не завершений.

Корінь проблеми: Використання простого попа без ack/requeue (RPOP/BLPOP) і відсутність відстеження in-flight.

Виправлення: Використовуйте BRPOPLPUSH з processing list і reaper, або Redis streams з consumer groups, або перемістіть на Postgres-чергу з semantics visibility timeout.

6) «Забагато з’єднань» в Postgres під час сплесків логінів

Симптоми: Postgres відкидає з’єднання; треди апу зависають; pgbouncer відсутній або неправильно налаштований.

Корінь проблеми: Один connection на запит; відсутність pooling; запис сесії на кожен запит.

Виправлення: Додайте pooling, зменшіть частоту записів (не оновлюйте last_seen на кожен запит) і попередньо обчислюйте дані, похідні від сесії, в Redis, якщо це безпечно.

7) Стрибки латентності Redis кожні кілька хвилин

Симптоми: P99 латентність підскакує; лімітер призводить до таймаутів; графіки показують періодичні піки.

Корінь проблеми: Форк для RDB snapshot, AOF rewrite або насичення диска, якщо fsync AOF агресивний.

Виправлення: Налаштуйте графік постійності, використовуйте «everysec» fsync, якщо прийнятно, моніторте час форків, забезпечте швидкий диск і уникайте дуже великих footprint-ів пам’яті, що роблять форки дорогими.

Чеклісти / покроковий план

Чекліст A: вирішення, куди зберігати сесії

  1. Класифікуйте, що містить сесія: лише ідентичність, права (entitlements), стан MFA чи налаштування користувача.
  2. Якщо payload містить права/MFA, робіть Postgres авторитетом (або розділіть авторитет і кеш).
  3. Визначте tolerance втрат: «користувачі можуть переавторизуватися» проти «скасування має бути миттєвим».
  4. Оберіть TTL-стратегію: абсолютний строк + idle timeout; вирішіть, хто і коли робить refresh.
  5. Заплануйте очищення: налаштування vacuum/autovacuum для Postgres; TTL і політика евіктації для Redis.
  6. Протестуйте поведінку при рестарті в staging з реалістичним навантаженням і налаштуваннями постійності.

Чекліст B: реалізація лімітерів без створення нового вузького місця

  1. Оберіть алгоритм: token bucket зазвичай найкращий компроміс.
  2. Визначте область дії: per-IP, per-user, per-tenant, per-endpoint — уникайте необмеженої кардинальності без плану.
  3. Використовуйте Redis, якщо потрібна висока пропускна здатність і ви можете терпіти короткі вікна втрат.
  4. Якщо ліміти контрактні (партнери), розгляньте Postgres-backed ліміти або надійну конфігурацію Redis.
  5. Інструментуйте латентність лімітеру окремо; ставтеся до нього як до залежності.
  6. Майте «режим деградації»: дозволяйте невеликі сплески, блокуйте дорогі ендпоінти і логьте рішення для форензики.

Чекліст C: вибір реалізації черги, яка не підведе

  1. Запишіть контракт: at-least-once, політика повторів, dead-letter, потреби в порядку, відкладене планування, visibility timeout.
  2. Якщо задачі мають бути транзакційно зв’язані з DB-записами, використовуйте Postgres outbox pattern.
  3. Якщо пропускна здатність висока і задачі ефермерні, Redis streams може бути сильною опцією — експлуатуйте його серйозно.
  4. Проєктуйте ідемпотентні ключі та забезпечуйте їх (унікальні обмеження в Postgres або dedupe sets в Redis з TTL).
  5. Зробіть зворотний тиск явним: продюсери має сповільнюватися, коли споживачі відстають.
  6. Побудуйте операційні запити: «найстаріша задача», «застряглі в progress», «розподіл кількості повторів».

Покроковий план міграції (від «як є» до розумного)

  1. Зробіть інвентар: сесії, ліміти, черги, ідемпотентні ключі. Для кожного визначте tolerance втрат і потреби аудиту.
  2. Оберіть авторитетні сховища: Postgres для правди; Redis для прискорення та похідних даних.
  3. Додайте спостережуваність: P95/P99 латентність для викликів Redis/Postgres, lag черг, кількість евіктацій, відставання autovacuum.
  4. Реалізуйте dual-write тільки якщо можете реконсиляцію; віддавайте перевагу staged cutovers і read-fallback стратегіям.
  5. Навантажте тест «шлях інциденту»: рестарт Redis, failover Postgres, симулюйте мережевий partition, обмежте IO диска.
  6. Напишіть runbooks: що робити при евіктації, реплікаційному відставанні, lock storms, spike в backlog.

FAQ

1) Чи можна безпечно зберігати сесії в Redis?

Так, якщо «безпечно» означає, що ви приймаєте вікна втрат, ризик евіктації і поведінку при рестарті — або налаштуєте постійність і ємність під ваші вимоги.
Якщо сесії — авторитарні артефакти безпеки, тримайте авторитет у Postgres і кешуйте похідні дані в Redis.

2) Чи достатньо постійності Redis (AOF/RDB), щоб ставити його як базу даних?

Іноді так, але не робіть вигляд, що це просто. Налаштування постійності визначають вікно втрат і продуктивність. Форк-основні снапшоти і AOF rewrite мають операційні витрати.
Якщо бізнес не може терпіти втрат, Postgres зазвичай простіша історія по надійності.

3) Чому не просто використовувати Postgres для всього?

Можете, і багато команд мали б робити більше саме так. Обмеження — це пропускна здатність, латентність і контенція при дуже гарячих лічильниках або чергах.
Redis краще підходить, коли потрібні атомарні операції на дуже високих швидкостях із TTL-семантикою.

4) Чому не просто використовувати Redis для всього?

Бо «в пам’яті» не означає «невразливий», і бо запитність важлива під час інцидентів і аудитів.
Redis чудовий для певних патернів; він не є загальним джерелом істини, якщо ви не спроєктуєте його відповідно й не приймете компроміси.

5) Чи є Redis streams справжньою чергою?

Вони ближчі, ніж списки або pub/sub: ви отримуєте consumer groups, acknowledgments і pending entries, які можна інспектувати.
Вам все одно потрібно проєктувати retry-и, dead-letter обробку і операційні інструменти. Streams — це набір інструментів для черг, а не повний продукт черги.

6) Як реалізувати надійну чергу в Postgres?

Використовуйте таблицю задач зі стовпцем «state» і timestamp-ами, захоплюйте задачі через FOR UPDATE SKIP LOCKED, тримайте транзакції короткими і реалізуйте логіку retry/dead-letter.
Якщо enqueue має бути зв’язаний з бізнес-записом, використовуйте outbox pattern в тій самій транзакції.

7) Який найбільший операційний ризик Postgres для сесій?

Churn і блоут. Таблиці сесій швидко створюють мертві кортежі. Якщо autovacuum не налаштований, латентність стає непередбачуваною. Партиціонування і зниження частоти записів дуже допомагають.

8) Який найбільший операційний ризик Redis для лімітування?

Перетворення лімітеру на вузьке місце. Важкі Lua-скрипти, висока кардинальність ключів і латентні сплески, спричинені постійністю, можуть створити петлю відмов,
де таймаути викликають повтори, що спричиняє більше трафіку до лімітеру.

9) Чи мають ліміти бути стійкими?

Залежить від того, що вони захищають. Якщо це внутрішня справедливість, скидання часто прийнятне. Якщо це контракт (партнери, контроль шахрайства),
стійкість важлива — або через налаштований persistent Redis, або через Postgres як авторитет.

10) Як уникнути подвійної обробки задач?

Припускайте at-least-once доставку і робіть обробники ідемпотентними. У Postgres забезпечуйте ідемпотентність через унікальні обмеження.
У Redis використовуйте dedupe-ключі (з TTL) або вбудовуйте ідемпотентність у downstream-записи.

Наступні кроки, які можна виконати цього тижня

Припиніть сперечатися, який інструмент «кращий». Визначте, які відмови ви можете терпіти, а потім оберіть сховище, яке відмовляє у прийнятних для вас способах.
Redis фантастичний для швидкості і станів, керованих TTL. Postgres фантастичний для правди, зв’язування і ясності при розслідуванні.
Більшість стійких систем використовують обидва, з чітким розмежуванням між авторитетом і прискоренням.

  1. Запишіть, що станеться, якщо Redis втратить всі ключі. Якщо відповідь містить «інцидент безпеки» — перенесіть авторитет у Postgres.
  2. Запустіть практичні перевірки вище в staging і продакшені. Зафіксуйте базові значення латентності, евіктацій, wait locks і стану vacuum.
  3. Для черг оберіть один контракт (at-least-once + ідемпотентність — розумний дефолт) і реалізуйте visibility timeout і dead-lettering.
  4. Для сесій розділіть авторитет і кеш: Postgres для revocation/expiry; Redis для похідних, які можна повторно обчислити.
  5. Для лімітування зберігайте алгоритм простим, інструментуйте лімітер і ставтеся до нього як до залежності, яка може вас підвести.

Збережіть правильно зараз або заплатіть пізніше. Рахунок надходить під час інциденту. Він завжди надходить.

← Попередня
Proxmox «guest agent not running»: увімкнути QEMU Guest Agent і зробити це стабільним
Наступна →
MySQL vs RDS MySQL: приховані обмеження, які б’ють під час інцидентів

Залишити коментар