MySQL проти PostgreSQL: «випадкові таймаути» — мережа, DNS і пулінг як винуватці

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

«Випадкові таймаути» — так кажуть, коли канал інцидентів рухається швидше, ніж графіки. Один запит зависає на 30 секунд, інший виконується за 12 мс, і в усіх підозру викликає база даних, бо це єдина спільна залежність, яку всі можуть написати правильно.

Зазвичай база даних ні в чому не винна. Або, принаймні: винна не в тому простому сенсі, як «Postgres повільний» або «MySQL розриває з’єднання». Таймаути живуть у тріщинах — між додатком і БД — там, де черги мережі, кешування DNS, балансувальники та пулінг підключень тихо змінюють правила гри.

Що насправді означають «випадкові» таймаути

«Випадкові» зазвичай означає корельовані, але не з тим, на що ви зараз дивитесь. Один і той самий запит може вичерпати таймаут у одного користувача й успішно виконатись у іншого, бо справа не лише в запиті. Це залежить від:

  • Якого інстансу додатку ви зачепили (різний кеш DNS, різний пул, різне ядро вузла, різна насиченість таблиці conntrack).
  • Який DB-ендпоінт ви резолвили (застарілий DNS, split-horizon, несумісність Happy-Eyeballs між IPv6/IPv4).
  • Яким шляхом пішов пакет (зміни ECMP-хешу, один завантажений лінк, «шумний» сусід VM).
  • Яке з’єднання ви повторно використали (пул віддав вам напівмертву TCP-сесію; БД вже «забула» вас).
  • На який лок ви чекали (транзакція на блокуванні рядка не відрізняється від мережевої паузи без інструментів спостереження).

Таймаути також сумуються. У додатку може бути таймаут запиту 2s, у драйвера — 5s на підключення, у пулера — 30s server_login_retry, а у балансувальника — 60s idle timeout. Коли хтось каже «воно таймаутиться десь через 30 секунд», це не підказка. Це зізнання: стандартне значення зробило це.

MySQL vs PostgreSQL: як зазвичай виглядають таймаути

Де зазвичай «випадкові таймаути» з’являються в MySQL-середовищах

У MySQL-оточеннях «випадкові таймаути» часто є побічним ефектом інтенсивного оновлення з’єднань і обмежень на потоки/з’єднання:

  • Шторми підключень після деплою (або після перезапуску пулера) можуть наситити MySQL витратами на автентифікацію та створення потоків.
  • wait_timeout і interactive_timeout можуть вбивати прості з’єднання, які пул вважав живими, викликаючи періодичні помилки «server has gone away» або «lost connection».
  • Зворотні DNS-пошуки (коли резолюція імен використовується для прав доступу чи логування) можуть додати несподівану затримку під час підключення, якщо DNS хворіє.
  • Проксі-шари (SQL-aware проксі, L4 балансувальники) можуть ввести idle таймаути, що виглядають як «MySQL нестабільний».

Де зазвичай «випадкові таймаути» з’являються в PostgreSQL-системах

У Postgres таймаути часто проявляються як очікування — на блокування, на слоти з’єднань або на ресурси сервера:

  • Насичення max_connections створює дуже специфічний малюнок: одні клієнти підключаються миттєво, інші зависають або падають — залежно від поведінки пулу і backoff.
  • Блокування та довгі транзакції змушують запити «випадково» висіти за одним поганим виконавцем.
  • statement_timeout і idle_in_transaction_session_timeout можуть перетворити повільний шлях на помилку — що добре, поки не буде невірно настроєно й не стане шумом.
  • PgBouncer у transaction/statement режимі може посилити дивності, якщо ваш застосунок покладається на стан сесії.

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

Парафраз ідеї Вернера Фогельса: «Усе ламається постійно». Це не песимізм; це вимога до дизайну.

Жарт №1: «Випадкові таймаути» — це просто детерміністичні відмови, які ще не зустріли ваші дашборди.

Цікаві факти та трохи історії (які насправді допомагають)

  1. PostgreSQL походить від POSTGRES (1980-ті), спроектованого з акцентом на розширюваність і коректність; це помітно у строгості навколо транзакцій і блокувань.
  2. Рання популярність MySQL (кінець 1990-х/2000‑х) була через швидкість і простоту для веб-навантажень; багато операційних значень за замовчуванням і припущень екосистеми досі відображають «безліч коротких запитів».
  3. MySQL історично опирався на нетранзакційні движки (на кшталт MyISAM), поки InnoDB не став нормою; операційні легенди про «MySQL простий» часто ігнорують, як InnoDB поводиться під конкуренцією.
  4. Postgres рано впровадив MVCC і пішов у цьому напрямку; «запити не блокують записів» в основному правда — доки не з’являться блокування і DDL.
  5. PgBouncer став поширеним, бо з’єднання Postgres недешеві; велика флотилія, що робить TLS + auth на кожен запит, може виглядати як випадковий DDoS, за який ви заплатили.
  6. MySQL-проксі (і балансувальники) стали популярні для масштабування читань; компоненти L4/L7 можуть ввести idle таймаути, що імітують нестабільність сервера.
  7. DNS TTL існують тому, що резолвери — кеші; клієнти по-різному дотримуються TTL, і деякі бібліотеки кешують довше, ніж ви думаєте — особливо у довгоживучих процесах.
  8. Linux conntrack (netfilter) може бути вузьким місцем у NAT-насичених розгортаннях; це не специфічно для баз даних, але трафік бази достатньо стабільний, щоб його виявити.
  9. Хмарні балансувальники часто мають фіксовані чи майже фіксовані idle timeout за замовчуванням; бази даних балакучі, але іноді тихі, тому «idle» все ще може бути «здоровим».

Швидкий план діагностики

Це порядок дій, який я використовую, коли хтось каже «таймаути БД», і нема часу на дискусії про архітектуру.

Перше: класифікуйте таймаут

  • Connect timeout (не вдається встановити TCP/TLS/автентифікацію). Підозрювані: DNS, маршрутизація, фаєрвол, стан здоров’я балансувальника, max connections, SYN backlog.
  • Read timeout (підключено, потім зависає). Підозрювані: блокування, довгі транзакції, CPU/IO сервера, втрати пакетів/повторні передачі, пул видав вам мертвий сокет.
  • Write timeout (часто виглядає як read timeout але при commit). Підозрювані: fsync/IO-затримки, тиск реплікації, synchronous commit, затримка зберігання, мережа до сховища.
  • Application timeout (власний дедлайн спрацював). Підозрювані: усе вищеперелічене плюс «ви поставили 500ms і сподівались».

Друге: знайдіть обсяг проблеми

  • Один вузол додатку? Перевіряйте кеш DNS, ядро, conntrack, локальний стан пулу, «шумного» сусіда.
  • Одна AZ/підмережа? Дивіться зміни маршрутизації, security groups, MTU mismatch, втрати пакетів, brownouts.
  • Всі клієнти? Дивіться насичення БД, збої сховища або спільний проксі/балансувальник.
  • Тільки нові підключення? Дивіться автентифікацію/DNS, TLS handshake, виснаження пулера, max connections, SYN backlog.

Третє: вирішіть, чи ви чекаєте, чи пакети втрачаються

  • Очікування: сервер показує сесії, що застрягли на блокуваннях/IO/CPU; мережа показує низькі втрати, але високу латентність; запити відображають «active» але заблоковані.
  • Втрата: TCP resets, broken pipes, «server closed the connection», сплески retransmits/timeouts, тиск таблиці NAT.

Четверте: звужуйте поверхню проблеми

  • Спробуйте одного клієнта, одне з’єднання, прямо на IP БД (обійдіть DNS і пулер), щоб розділити шари.
  • Спробуйте з іншого вузла/підмережі, щоб відокремити локальне від системного.
  • Тимчасово зменшіть конкурентність, щоб зупинити самоподібні шторми підключень під час розслідування.

Практичні завдання: команди, виводи та рішення

Це реальні завдання, які можна виконати під час інциденту. Кожне містить, що означає вивід і яке рішення приймати далі. Вибирайте ті, що підходять вашому середовищу (bare metal, VM, Kubernetes).

1) Підтвердити відповідь DNS, TTL і чи флапає він

cr0x@server:~$ dig +noall +answer +ttlid db.internal.example A
db.internal.example. 5 IN A 10.20.30.41

Що це означає: TTL=5 секунд. Це агресивно. Якщо ендпоінт зміниться або резолвер повільний — ви це помітите.

Рішення: Якщо TTL дуже малий, перевірте стан резолвера і клієнтське кешування. Розгляньте підвищення TTL для стабільних сервісів або використання стабільного VIP/проксі-ендпоінта.

2) Перевірте, чи ваш вузол використовує битий шлях резолвера

cr0x@server:~$ resolvectl status | sed -n '1,80p'
Global
       Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: stub
Current DNS Server: 10.0.0.2
       DNS Servers: 10.0.0.2 10.0.0.3

Що це означає: Цей хост використовує systemd-resolved з двома DNS-серверами. Якщо 10.0.0.2 хворий, ви можете бачити переривчасті затримки запитів.

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

3) Виміряти латентність DNS напряму (не гадати)

cr0x@server:~$ for i in {1..5}; do time dig +tries=1 +timeout=1 @10.0.0.2 db.internal.example A >/dev/null; done
real    0m0.012s
real    0m0.980s
real    0m0.011s
real    0m1.003s
real    0m0.010s

Що це означає: Половина запитів ледь не дотягують до timeout 1s. Це не «нормально». Це рулетка.

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

4) Підтвердити маршрут і MTU до БД

cr0x@server:~$ ip route get 10.20.30.41
10.20.30.41 via 10.20.0.1 dev eth0 src 10.20.10.55 uid 1000
    cache

Що це означає: У вас маршрутизований шлях через 10.20.0.1. Якщо він змінюється під час інцидентів — це підказка.

Рішення: Якщо маршрути нестабільні, підключайте мережеву команду раніше. Нестабільні маршрути породжують «випадкові» таймаути, які жодна настройка БД не виправить.

5) Швидкий TCP connect тест (обхід драйверів)

cr0x@server:~$ nc -vz -w 2 10.20.30.41 5432
Connection to 10.20.30.41 5432 port [tcp/postgresql] succeeded!

Що це означає: TCP handshake пройшов. Це не доводить автентифікацію чи успіх запиту, але виключає «порт заблоковано» у цей момент.

Рішення: Якщо TCP час від часу падає, дивіться security rules, SYN backlog, stateful firewalls, виснаження conntrack/NAT та health checks LB.

6) Інспектуйте retransmits і стан TCP з клієнта

cr0x@server:~$ ss -ti dst 10.20.30.41 | sed -n '1,40p'
ESTAB 0 0 10.20.10.55:49822 10.20.30.41:5432
	 cubic wscale:7,7 rto:204 rtt:2.3/0.7 ato:40 mss:1448 pmtu:1500 rcvmss:1448 advmss:1448 cwnd:10 bytes_sent:21984 bytes_retrans:2896 segs_out:322 segs_in:310 send 50.4Mbps lastsnd:12 lastrcv:12 lastack:12 pacing_rate 100Mbps unacked:2 retrans:1/7

Що це означає: Є retransmits (bytes_retrans і retrans лічильники). Трохи — нормально; сплески сильно корелюють з «випадковими» паузами.

Рішення: Якщо retransmits сплескують під час інцидентів, припиніть суперечки про плани запитів і почніть дивитися на втрати пакетів, затори, MTU/PMTU проблеми або ненадійні NIC.

7) Перевірте тиск conntrack (у середовищах з великою кількістю NAT)

cr0x@server:~$ sudo sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
net.netfilter.nf_conntrack_count = 248901
net.netfilter.nf_conntrack_max = 262144

Що це означає: Ви близькі до ліміту таблиці conntrack. Коли вона заповниться, ви отримаєте втрачені пакети, що виглядає як «переривчасті таймаути».

Рішення: Піднімайте ліміти conntrack (осмислено з урахуванням пам’яті), зменшуйте churn з’єднань, уникайте зайвого NAT і розгляньте пулінг на краю.

8) Перевірити поведінку idle timeout балансувальника / проксі з контрольованою паузою

cr0x@server:~$ (echo "ping"; sleep 75; echo "ping") | nc 10.20.30.50 3306
ping

Що це означає: Якщо другий «ping» ніколи не отримує відповіді або з’єднання розривається приблизно через ~60s, якась середня коробка примушує idle timeout.

Рішення: Узгодьте keepalive: kernel TCP keepalives, driver keepalives та LB/proxy idle timeouts. Або видаліть LB з шляху до БД, якщо він не підходить.

9) PostgreSQL: перевірте, чи клієнти застрягли в очікуванні блокувань

cr0x@server:~$ psql -h 10.20.30.41 -U ops -d appdb -c "select pid, wait_event_type, wait_event, state, now()-query_start as age, left(query,80) from pg_stat_activity where state <> 'idle' order by age desc limit 10;"
 pid  | wait_event_type |  wait_event   | state  |   age   |                                      left
------+-----------------+---------------+--------+---------+--------------------------------------------------------------------------------
 8123 | Lock            | transactionid | active | 00:00:31| update orders set status='paid' where id=$1
 7991 | Client          | ClientRead    | active | 00:00:09| select * from orders where id=$1

Що це означає: Запит чекає блокування вже 31 секунду. Це не мережевий таймаут. Це блокуюча транзакція.

Рішення: Знайдіть блокер (наступне завдання), потім вирішіть, чи вбивати його, чи мінімізувати обсяг транзакцій у коді, або додати індекси/переписати, щоб зменшити тривалість блокувань.

10) PostgreSQL: знайти блокуючий запит

cr0x@server:~$ psql -h 10.20.30.41 -U ops -d appdb -c "select blocked.pid as blocked_pid, blocker.pid as blocker_pid, now()-blocker.query_start as blocker_age, left(blocker.query,80) as blocker_query from pg_locks blocked_locks join pg_stat_activity blocked on blocked.pid=blocked_locks.pid join pg_locks blocker_locks on blocker_locks.locktype=blocked_locks.locktype and blocker_locks.database is not distinct from blocked_locks.database and blocker_locks.relation is not distinct from blocked_locks.relation and blocker_locks.page is not distinct from blocked_locks.page and blocker_locks.tuple is not distinct from blocked_locks.tuple and blocker_locks.virtualxid is not distinct from blocked_locks.virtualxid and blocker_locks.transactionid is not distinct from blocked_locks.transactionid and blocker_locks.classid is not distinct from blocked_locks.classid and blocker_locks.objid is not distinct from blocked_locks.objid and blocker_locks.objsubid is not distinct from blocked_locks.objsubid and blocker_locks.pid != blocked_locks.pid join pg_stat_activity blocker on blocker.pid=blocker_locks.pid where not blocked_locks.granted and blocker_locks.granted;"
 blocked_pid | blocker_pid | blocker_age |                         blocker_query
-------------+-------------+-------------+---------------------------------------------------------------
        8123 |        7701 | 00:02:14    | begin; select * from orders where customer_id=$1 for update;

Що це означає: PID 7701 тримає блокування понад 2 хвилини. Ваші «випадкові» таймаути — це додаток, який чемно чекає.

Рішення: Якщо це runaway-транзакція — завершіть її. Якщо це нормальна поведінка — змініть код: тримайте транзакції короткими, уникайте FOR UPDATE, якщо не потрібно, і індексуйте предикат.

11) PostgreSQL: виявити вичерпання слотів підключення

cr0x@server:~$ psql -h 10.20.30.41 -U ops -d appdb -c "select count(*) as total, sum(case when state='active' then 1 else 0 end) as active from pg_stat_activity;"
 total | active
-------+--------
  498  |   112

Що це означає: 498 сесій присутні. Якщо max_connections = 500, ви живете на межі.

Рішення: Поставте PgBouncer попереду (уважно), зменшіть розміри пулів додатку і зарезервуйте з’єднання для обслуговування. «Просто підняти max_connections» зазвичай — план по пам’яті, замаскований під оптимізм.

12) MySQL: перевірте, чи ви досягаєте лімітів підключень або тиску потоків

cr0x@server:~$ mysql -h 10.20.30.42 -u ops -p -e "SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Threads_running'; SHOW VARIABLES LIKE 'max_connections';"
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| Threads_connected | 942   |
+-------------------+-------+
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| Threads_running | 87    |
+-----------------+-------+
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 1000  |
+-----------------+-------+

Що це означає: Ви близькі до max connections. Навіть якщо CPU в нормі, сервер може стрибати від обробки підключень і контекстного перемикання.

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

13) MySQL: ідентифікуйте корені «server has gone away» (таймаути vs розмір пакету)

cr0x@server:~$ mysql -h 10.20.30.42 -u ops -p -e "SHOW VARIABLES LIKE 'wait_timeout'; SHOW VARIABLES LIKE 'interactive_timeout'; SHOW VARIABLES LIKE 'max_allowed_packet';"
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| wait_timeout       | 60    |
+--------------------+-------+
+--------------------+-------+
| Variable_name      | Value |
+--------------------+-------+
| interactive_timeout| 60    |
+--------------------+-------+
+--------------------+----------+
| Variable_name      | Value    |
+--------------------+----------+
| max_allowed_packet | 67108864 |
+--------------------+----------+

Що це означає: Прості з’єднання вмирають через 60 секунд. Це підходить для короткоживучих клієнтів, але жахливо для пулів, що тримають з’єднання довше.

Рішення: Або: підвищити таймаути і увімкнути keepalives; або скоротити idle lifetime пулу, щоб пул відкидав з’єднання раніше, ніж сервер їх закриє.

14) Перевірити таймери TCP keepalive (клієнтська сторона)

cr0x@server:~$ sysctl net.ipv4.tcp_keepalive_time net.ipv4.tcp_keepalive_intvl net.ipv4.tcp_keepalive_probes
net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9

Що це означає: Перший keepalive відправляється через 2 години. Якщо ваш балансувальник вбиває idle сесії через 60 секунд, keepalive вам не допоможе.

Рішення: Налаштуйте keepalives (обережно) для DB-клієнтів за балансувальниками/NAT, або припиніть ставити бази даних за пристроями, що вбивають idle-з’єднання.

15) Перевірити насичення пулу додатку на рівні ОС

cr0x@server:~$ ss -s
Total: 1632 (kernel 0)
TCP:   902 (estab 611, closed 221, orphaned 0, synrecv 0, timewait 221/0), ports 0

Transport Total     IP        IPv6
RAW	  0         0         0
UDP	  12        10        2
TCP	  681       643       38
INET	  693       653       40
FRAG	  0         0         0

Що це означає: 611 встановлених TCP-з’єднань. Якщо ваш додаток «повинен» мати 50 — у вас витік пулу або шторм підключень.

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

16) Швидкий погляд зі сторони сервера: CPU чи IO завантажені?

cr0x@server:~$ iostat -x 1 3
avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          18.21    0.00    6.10   24.77    0.00   50.92

Device            r/s     rkB/s   rrqm/s  %rrqm r_await rareq-sz     w/s     wkB/s   w_await aqu-sz  %util
nvme0n1         120.0   40960.0     0.0   0.00    8.10   341.3   220.0   53248.0   21.40   4.90   96.80

Що це означає: Диск ~97% завантажений; w_await ~21 ms. Це затримка коміту, що чекає на сховище, а не «баг драйвера».

Рішення: Якщо IO завантажене, дивіться checkpointing, fsync-поведінку, налаштування реплікації, тротлінг сховища і «шумних» сусідів.

DNS: мовчазний саботажник

DNS рідко є коренем проблеми з таймаутами БД. Він зазвичай є підсилювачем: невелика проблема з DNS перетворює рутинну логіку перепідключення в синхронізований збій.

Як DNS викликає «випадкові» таймаути БД

  • Повільні запити під час створення підключення: кожне перепідключення чекає на DNS, тож ваш пул підключень ставить рекорди по DNS-латентності.
  • Застарілі відповіді: клієнти продовжують використовувати стару IP-адресу після failover; БД на новому IP в порядку, а ви штурмуєте старий, ніби він винен.
  • Негативне кешування: тимчасовий NXDOMAIN або SERVFAIL кешується, і тепер ви «випадково» не можете розв’язати БД деякий час.
  • Split-horizon невідповідності: внутрішній/зовнішній DNS дають різні відповіді; половина подів резолвить до недоступних адрес.
  • Квірки Happy Eyeballs: AAAA відповіді призводять до спроб по зламаному v6-шляху перед тим, як v4 спрацює.

MySQL vs Postgres: DNS-пов’язані сюрпризи

Обидві СУБД постраждають у часі підключення, бо клієнт резолвит хост перед встановленням TCP. Сюрпризи зазвичай походять від того, що навколо них встановлено:

  • MySQL-розгортання часто включають SQL-aware проксі або L4 балансувальники для read/write сплітінгу. Це додає ще одне ім’я хоста, ще одну резолюцію і ще один шар кешування.
  • Postgres-розгортання часто включають PgBouncer. PgBouncer сам резолвит upstream-імена і має свою поведінку при перепідключеннях і повторних спробах.

Операційно: не дозволяйте кожному інстансу додатку робити свої DNS-гінгішки під час інциденту. Поставте стабільний ендпоінт перед БД (VIP, проксі або керований ендпоінт) і протестуйте режим відмов навмисно.

Реалії мережі: retransmits, MTU і «це не втрачене, це в черзі»

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

Retransmits: прихований податок

Один retransmit може коштувати десятки або сотні мілісекунд залежно від RTO і алгоритму керування заторами. Кілька відсотків втрат пакетів можуть перетворити балакучу БД-протокол на машину таймаутів, особливо з TLS зверху.

MTU і PMTUD: класичне «працює, поки не перестає»

MTU mismatch може проявлятися як:

  • Малі запити працюють, великі відповіді зависають.
  • Підключення вдається, перший великий результат зависає.
  • Деякі шляхи працюють (той самий рюк), деякі падають (між AZ).

Якщо деінде блокується ICMP «fragmentation needed», Path MTU Discovery ламається і ви отримуєте чорну діру. Це виглядає як таймаут БД, бо БД часто перша надсилає великі пакети послідовно.

Черги та bufferbloat

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

Жарт №2: Мережа — як офісний принтер: працює ідеально, поки хтось не дивиться.

Підозрювані в пулінгу: PgBouncer, проксі та «корисні» значення за замовчуванням

Пулінг існує, бо створення з’єднання з БД дороге — CPU, пам’ять, TLS-handshake, автентифікація, серверні бухгалтерські записи. Але пулінг також те місце, де реальність абстрагується в щось, що ваші розробники неправильно розуміють.

Три типи пулів (і чому вам це має бути важливо)

  • Client-side pools (в додатку): найпростіші, але можуть помножити кількість з’єднань на кількість інстансів. Чудово створюють шторми підключень.
  • External poolers (PgBouncer, ProxySQL): можуть обмежити серверні з’єднання і поглинати churn клієнтів. Але додають ще один хоп і ще один набір налаштувань таймаутів.
  • Managed proxies (хмарні DB-проксі): зручні, але їхня поведінка при failover і значення idle timeout за замовчуванням часто породжують «випадковість».

Режими відмов пулера, що виглядають як таймаути БД

  • Насичення пулу: запити шикуються в чергу в очікуванні підключення; додаток таймаутиться; БД сидить без діла.
  • Повторно використані мертві з’єднання: пул віддає сокет, який вбила середня коробка; перший запит зависає або видає помилку.
  • Шторми повторних спроб: пулер агресивно повторює; додає навантаження, коли БД вже не в порядку.
  • Припущення про стан сесії: transaction pooling ламає додатки, які покладаються на сесійні змінні, тимчасові таблиці, advisory locks, prepared statements або SET LOCAL семантику (залежно від БД і режиму).

MySQL vs Postgres: пастки пулінгу

Postgres особливо чутливий до кількості підключень, бо кожне з’єднання відповідає бекенд-процесу (класична архітектура). Саме тому PgBouncer так популярний. Але режими PgBouncer мають значення:

  • Session pooling: найнадійніший; менше сюрпризів; менш ефективний при різких сплесках конкурентності.
  • Transaction pooling: ефективний; ламає все, що потребує сесійної афінності, якщо не продумано.
  • Statement pooling: гострий інструмент; рідко вартий використання для загальних додатків.

MySQL зазвичай обробляє багато з’єднань інакше (thread-per-connection, якщо нема thread pool залежно від дистрибуції/видання). Кількість підключень все ще проблема, але підпис відмов часто виглядає як CPU-накопичення й перемикання контексту, а не миттєве «нема слотів».

Специфічні для MySQL режими відмов, що викликають таймаути

Несумісність idle timeout (wait_timeout vs pool lifetime)

Одна з найпоширеніших «випадкових» відмов: сервер таймаутить прості з’єднання, пул тримає їх, і ваш наступний запит виявляє труп.

Що робити: Зробіть max lifetime і idle timeout пулу коротшими, ніж wait_timeout, або підніміть wait_timeout і увімкніть TCP keepalive. Оберіть один підхід; не дозволяйте їм розходитися.

Повільність автентифікації та зворотних DNS-пошуків

Якщо MySQL налаштований так, що потребує резолюції імен (або середовище робить зворотні пошуки для логування/контролю доступу), то DNS-хапка стає затримкою під час підключення. Не в тому сенсі, що MySQL «не справляється»; а в тому, що ваш шлях автентифікації — розподілена система.

Занадто багато підключень: «працювало до деплою»

MySQL може виглядати стабільно при 500 підключеннях і потім розвалитися при 900 — не тому, що запити змінилися, а тому, що зросли накладні витрати. Потоки, пам’ять на підключення, mutex contention і кеш-турбулентність проявляються як таймаути в додатку.

Сильна думка: Якщо у вас сотні інстансів додатків, кожен з яких може відкрити десятки з’єднань — у вас не проблема бази даних. У вас проблема координації.

Специфічні для PostgreSQL режими відмов, що викликають таймаути

Блокування та довгі транзакції

Postgres чудово справляється з конкуренцією — поки ви не тримаєте блокування довше, ніж думаєте. Звичайний винуватець — транзакція, яка починається рано, виконує частину «бізнес-логіки», а оновлення рядків робить наприкінці. Під навантаженням ці блокування накопичуються і все «випадково» чекає.

Що робити: Тримайте транзакції короткими, зменшуйте область блокування, індексуйте точно ті рядки, які змінюєте, і використовуйте таймаути, що відкидають швидко, щоб захистити систему.

Вичерпання слотів підключення і міф «просто підняти max_connections»

Кожне з’єднання Postgres споживає пам’ять і накладні витрати на управління. Піднімати max_connections без перегляду work_mem, паралелізму і загального обсягу RAM — шлях перетворити інцидент з таймаутів у OOM.

Що робити: Поставте PgBouncer в session або transaction pooling режим, а потім правильно підберіть розміри пулів додатку. Зарезервуйте запас для адміністративних підключень.

Таймаути, яких або бракує, або використовують як зброю

Postgres дає гарні регулятори: statement_timeout, lock_timeout, idle_in_transaction_session_timeout. Пастка — встановити їх глобально на значення, які підходять лише для одного випадку використання.

Що робити: Налаштовуйте таймаути по ролі або по сервісу. Різні робочі навантаження заслуговують на різні режими відмов.

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

Міні-історія 1: інцидент через невірне припущення

Компанія мала завантажений checkout-сервіс і Postgres primary з hot standby. Під час планового тесту failover вони перемістили роль primary. Ім’я ендпоінта бази даних мало слідувати за primary, TTL був малий, усе «сучасно».

Після failover третьина запитів почала таймаутитись. Не помилки — таймаути. Графіки БД були в нормі. CPU нормальний. Диск в порядку. Реплікація догнала. Усі витріщались на Postgres, бо так роблять, коли лячно.

Невірне припущення було просте: «Наші Java-додатки поважають TTL DNS». Вони — ні, не завжди. Деякі інстанси кешували розв’язану IP довше через JVM-level кешування в поєднанні з тим, як клієнтська бібліотека резолвить імена. Ті інстанси продовжували вдаряти по старому primary IP, який тепер відкидав записи або був недоступний через правила безпеки.

Виправлення не було героїчним. Вони зробили ендпоінт БД стабільним за проксі, який не міняє IP під час failover, і явно налаштували JVM DNS caching, щоб поважати TTL у цьому середовищі. Також додали runbook: під час failover виміряти резолюцію DNS і фактичний IP призначення з вибірки подів. Більше не було віри в мережу.

Міні-історія 2: оптимізація, що обернулася проти команди

Команда, що працювала з MySQL для мульти-тенантного API, вирішила зменшити накладні витрати підключень. Вони збільшили розміри пулів «щоб повторно використовувати з’єднання» і зменшили таймаути «щоб швидко падати». У staging це виглядало чудово: менше connect/s, нижча медіана латентності.

У продакшні на тижневий сплеск трафіку сервіс швидко масштабувався. Кожен новий інстанс включав великий пул, і всі одночасно намагались його «прогріти». MySQL брав підключення, поки не перестав. Потоки виросли. CPU піднявся не від запитів, а від управління підключеннями і перемикання контексту. Потім почались повтори.

Політика повторів клієнта разом з коротким connect timeout створила retry storm. З’єднання, які могли б пройти за 300 ms, тепер падали при 100 ms і одразу перепробовувалися. «Оптимізація» зробила систему менш терплячою саме тоді, коли треба було більше терпіння.

Вони відкотили збільшення пулів і ввели строгі глобальні бюджети підключень на сервіс, плюс повільний старт при запуску. Також зробили retries з експоненційним backoff і jitter та додали circuit breaker на підключення до БД. Медіанна латентність трохи зросла. Інцидентів стало значно менше. Продакшн-системи віддають перевагу нудному над хитрим.

Міні-історія 3: нудна, але правильна практика, що врятувала

Інша організація використовувала Postgres з PgBouncer у session режимі. Нічого екстраординарного. Їхня SRE команда мала нудну звичку: щокварталу вони проводили контрольовані drill-и — вбивали один вузол додатку, перезапускали PgBouncer, симулювали повільний DNS, вводили невеликі втрати пакетів у тестовому середовищі, що віддзеркалювало продакшн-топологію.

Одного дня сталася деградація мережі у хмарі. Втрати пакетів були низькими але ненульовими; латентність періодично стрибала. Сервіси почали повідомляти «таймаути бази даних». На черговому виклику виконувалися дії з плану: спочатку перевірили retransmits з клієнтського вузла, потім перевірили глибину черги PgBouncer, потім перевірили wait-ів Postgres. За десять хвилин вони знали, що це деградація мережі плюс ампліфікація повторів, а не регресія БД.

Оскільки вони репетирували, у них була безпечна міра пом’якшення: знизити конкурентність шляхом динамічного зменшення розмірів пулів додатку, трохи підвищити певні клієнтські таймаути, щоб уникнути storm-ів повторів, і тимчасово відключити одну фонову задачу, яка генерувала великі результати. Postgres залишився здоровим. Інцидент став коротким замість марафону з pager-ів.

Нічого гламурного. Це оперативний еквівалент чищення зубів. Нецікаво, доки не врятує вам тисячі на стоматолога.

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

1) Таймаути згруповані приблизно навколо 60 секунд

Симптом: збої приблизно через ~60s, незалежно від складності запиту.

Корінна причина: idle timeout балансувальника/проксі або server-side idle timeout, що вбиває pooled з’єднання.

Виправлення: узгодьте idle timeouts і keepalives; встановіть max lifetime пулу коротшим за DB idle timeout; уникайте LB в шляху до БД, якщо не знаєте їх поведінки.

2) Лише нові підключення таймаутяться; існуючі працюють

Симптом: встановлений трафік в порядку, але перепідключення під час інциденту падають.

Корінна причина: повільний DNS, вузьке місце TLS handshake, тиск на SYN backlog або проблеми автентифікації.

Виправлення: виміряйте DNS-латентність; перевірте SYN backlog і блокування фаєрволом; обмежте швидкість створення підключень; використовуйте пулери/проксі, щоб зменшити churn handshake-ів.

3) Запити «випадково» зависають, потім відновлюються

Симптом: запит призупиняється на секунди, потім завершується; CPU і IO виглядають нормально.

Корінна причина: очікування блокувань (особливо Postgres), або мережеві retransmits/чергування.

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

4) Один вузол додатку «проклятий»

Симптом: таймаути лише з конкретного хоста/pod/node.

Корінна причина: локальний кеш DNS, виснаження conntrack, помилки NIC, неправильно розмірений пул або битий маршрут у таблиці маршрутів.

Виправлення: порівняйте поведінку резолверів і retransmits між вузлами; злейте й замініть вузол; виправте конфігураційне відхилення; встановіть правила карантину вузлів на базі SLO.

5) «Server has gone away» / «broken pipe» після простих періодів

Симптом: перший запит після простою падає; повтор вдається.

Корінна причина: server idle timeout, LB idle timeout, NAT timeout вбиває стан.

Виправлення: зменшіть idle lifetime пулу; увімкніть keepalive; підкоригуйте DB таймаути; уникайте stateful middleboxes між додатком і БД.

6) Підняли max connections, і таймаути погіршилися

Симптом: менше помилок «too many connections», більше латентності/таймаутів.

Корінна причина: ресурсна конкуренція через надто багато конкурентних бекендів/потоків; тиск пам’яті; накладні витрати контекстного перемикання.

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

7) Таймаути зростають після деплоїв або рестартів

Симптом: короткі інциденти відразу після rollout-у.

Корінна причина: шторми прогріву підключень, синхронізовані повтори, холодні кеші, DNS-шторм.

Виправлення: повільно запускати створення підключень; додати джиттер; розтягувати rollout-и; обережно прогрівати; обмежити повтори.

8) Читання таймаутяться лише для «великих» відповідей

Симптом: малі запити в порядку, великі result sets зависають.

Корінна причина: MTU/PMTUD чорні діри, bufferbloat або проксі, що неправильно обробляє великі payload-и.

Виправлення: перевірте MTU end-to-end; дозволіть необхідні ICMP-повідомлення; тестуйте з контрольованими розмірами пакетів; уникайте зайвих проксі в шляху.

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

Чекліст A: Коли починаються таймаути (перші 10 хвилин)

  1. Класифікуйте таймаут: connect vs read vs app deadline. Зберіть вибірку помилок з часовими мітками.
  2. Перевірте латентність DNS щонайменше з двох клієнтських вузлів (завдання 3). Якщо вона спайкова — зупиніться і вирішіть DNS перш ніж робити щось складніше.
  3. Перевірте retransmits з клієнтського вузла (завдання 6). Втрати пояснюють «рандом».
  4. Перевірте насичення пулу: метрики додатку або OS-з’єднання (завдання 15). Якщо черги в пулі — налаштування БД не допоможе.
  5. Перевірте очікування в БД:
    • Postgres: pg_stat_activity wait events (завдання 9).
    • MySQL: threads connected/running (завдання 12) і метрики slow query/lock, якщо доступні.
  6. Застосуйте безпечне обмеження: знизьте конкурентність і швидкість повторів; зупиніть найгучнішу фонову задачу. Стабілізуйтеся спочатку, оптимізуйте потім.

Чекліст B: Запобігти повторним інцидентам (наступний робочий день)

  1. Стандартизуйте таймаути між додатком, драйвером, пулером і мережевими пристроями. Документуйте обрані значення і чому.
  2. Встановіть бюджети підключень для кожного сервісу. Наведіть їх у конфігурацію, а не у племінну пам’ять.
  3. Впровадьте backoff з jitter для повторів; додайте circuit breakers при проблемах підключення.
  4. Введіть стабільний DB-ендпоінт (проксі/VIP/керований ендпоінт), щоб зміни DNS не перетворювалися на хаос клієнтів.
  5. Додайте правила гігієни блокувань/транзакцій (особливо для Postgres): scope транзакцій, statement timeouts по ролям, алерти на довгі транзакції.
  6. Проводьте тренування відмов: симулюйте повільний DNS і помірні втрати пакетів; перевіряйте, що система деградує передбачувано.

Чекліст C: Вибір MySQL vs Postgres для прогнозованості операцій

  • Якщо організація не може забезпечити дисципліну підключень, плануйте пулер/проксі з першого дня, незалежно від вибору СУБД.
  • Якщо ваше навантаження чутливе до блокувань і бізнес-логіка тримає транзакції відкритими, Postgres покаже правду — боляче, але виправно.
  • Якщо ваше навантаження характеризується великим churn-ом підключень (serverless, сплески флотилії), архітектура важливіша за движок. Поставте стабільний шар пулінгу і вимірюйте DNS/мережу як перший клас залежності.

Поширені запитання

1) Чому таймаути здаються випадковими, хоча корінна причина детерміністична?

Тому що розподіл ховає патерни. Різні клієнти йдуть різними шляхами, використовують різні кеші DNS, потрапляють у різні стани пулу і конкурують за різні блокування. «Випадкове» часто означає «шаардоване».

2) Як зрозуміти, чи таймаут у пулі, а не в базі даних?

Шукайте час у черзі в метриках пулу або виведіть висновок: якщо CPU/IO БД низькі, але латентність додатку стрибає і кількість з’єднань висока — ймовірно, пул насичений або churn-ить. Допомагають і OS-рівневі лічильники ss.

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

Тільки якщо ви точно знаєте, що він робить з довгоживучими TCP-з’єднаннями і idle timeouts, і ви протестували поведінку при failover. LB добре для HTTP; бази даних — не HTTP.

4) Чи DNS TTL=5 секунд підходить для failover?

Може підходити, але лише якщо клієнти справді поважають TTL і ваші резолвери швидкі й надійні під навантаженням. Низький TTL без коректних клієнтів перетворює failover на лотерею.

5) Для Postgres чи PgBouncer завжди відповідь?

Часто — так, але режим має значення. Session pooling найменш дивний. Transaction pooling потужний, але вимагає дисципліни в додатку щодо стану сесії та prepared statements.

6) Для MySQL чи «server has gone away» завжди мережна проблема?

Ні. Це може бути idle timeout (wait_timeout), обмеження розміру пакету (max_allowed_packet), перезапуски сервера або середні коробки, що закривають idle сесії. Почніть з кореляції помилок з періодами простою і повторного використання з’єднань.

7) Чому retries погіршують ситуацію?

Повтори перетворюють латентність у навантаження. Якщо вузьке місце спільне (БД, DNS, проксі, conntrack), повтори синхронізують клієнтів і ампліфікують відмову. Використовуйте експоненційний backoff з jitter і бюджет повторів.

8) Який найшвидший виграш, щоб зменшити «випадкові» таймаути БД?

Контролюйте поведінку з’єднань. Обмежуйте пули, зупиніть шторми підключень і узгодьте таймаути між шарами. Потім виміряйте латентність DNS і TCP retransmits, щоб перестати шукати привидів.

9) Чи lock waits Postgres — проблема БД чи додатку?

Зазвичай це поведінка додатку, що проявляється через БД. Postgres чесний у своїй поведінці щодо очікування. Виправляйте scope транзакцій, індекси і шаблони доступу; потім розглядайте рівні ізоляції і таймаути.

10) Як уникнути кидання відповідальності між командами додатку, БД і мережі?

Збирайте три артефакти на початку: вибірки латентності DNS, свідчення TCP retransmit і знімки DB wait/lock. З цими даними обговорення перетворюється на план.

Наступні кроки (практичного штибу)

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

  1. Опануйте швидкий план діагностики і зробіть його м’язовою пам’яттю.
  2. Виберіть одну стабільну стратегію ендпоінта БД і тестуйте failover з реальними клієнтами, а не лише з теоретичною зміною DNS.
  3. Встановіть явні бюджети підключень і обмеження пулів для кожного сервісу. Застосовуйте їх.
  4. Узгодьте таймаути між додатком, драйвером, пулером, kernel keepalive і будь-якими middlebox-ами. Запишіть це.
  5. Інструментуйте очікування: lock waits у Postgres, thread pressure у MySQL і мережеві retransmits.

Зробіть це, і наступного разу, коли хтось скаже «випадкові таймаути», у вас буде незручна розкіш відповісти: «Круто. Якого саме шару?»

← Попередня
Segfault у продакшені: чому один збій може зіпсувати квартал
Наступна →
Сканування вразливостей Docker: чому довіряти, а що — шум

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