Docker Nginx upstream помилки: налагодження 502/504 за правильними логами

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

Ви на виклику. Дашборд у червоному. Користувачі кажуть «сайт не працює», а Nginx відповідає лише цинічним маленьким 502 або 504. Команда бекенду запевняє, що нічого не змінювала. Хост Docker виглядає «добре». Але на проді справи дуже далекі від «добре».

Саме тут люди витрачають години, дивлячись не в ті логи. Трюк нудний: логувати правильні upstream-поля, довести, який саме шар/хоп впав, а потім виправити ту одну річ, яка насправді зламана. Не «перезапустити все», а конкретно поламану річ.

Ментальна модель: що насправді означають 502 і 504 у Docker + Nginx

Почніть із дисципліни: 502/504 рідко буває «проблемою Nginx». Зазвичай це Nginx, який виконує роль посередника між клієнтом і upstream (ваш додаток) і має підтвердження подій.

502 Bad Gateway: Nginx не отримав валідної відповіді від upstream

На практиці, у Docker, 502 часто означає одне з наступного:

  • Збої з’єднання: Nginx не зміг підключитися до upstream IP:port (контейнер вимкнений, неправильний порт, інша мережа, правила фаєрволу, DNS вказує на стару IP).
  • Upstream закрив з’єднання раніше: Nginx підключився, відправив запит, а upstream закрив до того, як надіслав коректну HTTP-відповідь (падіння, OOM kill, бага в додатку, невідповідність proxy protocol або TLS).
  • Невідповідність протоколу: Nginx очікує HTTP, а upstream говорить HTTPS, gRPC, FastCGI або сирий TCP; або очікується HTTP/1.1 keepalive, а upstream не справляється.

504 Gateway Timeout: Nginx підключився, але не отримав відповідь вчасно

504 зазвичай повільніший і підступніший: Nginx підключився до upstream, але не отримав відповідь (або заголовки) в межах налаштованих таймаутів. Це не завжди означає «додаток повільний». Це також може бути:

  • Перевантаження upstream: пул потоків вичерпано, пул з’єднань до БД вичерпано, блокування циклу подій або CPU, обмежений cgroups.
  • Зависання мережі: втрата пакетів, вичерпання conntrack, дивні ефекти Docker bridge під навантаженням або невідповідний MTU для великих відповідей.
  • Таймаути не відповідають реальності: Nginx очікує відповідь 60s, але додаток інколи потребує 120s для певних задач, і ви взагалі не планували проксувати такі запити через Nginx.

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

Парафраз ідеї Вернера Фогельса (CTO Amazon): «You build it, you run it» — це про відповідальність за операційну реальність, а не лише про доставку коду.

Короткий жарт #1: 502 — це Nginx, що каже «Я намагався подзвонити вашому додатку, але він потрапив на голосову пошту.»

Швидкий плейбук діагностики (перше/друге/третє)

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

Перше: доведіть, який хоп відмовляє

  1. Клієнт → Nginx: Чи отримує Nginx запити? Перевірте access-логи і кореляцію за $request_id.
  2. Nginx → upstream: Це помилка підключення (502) чи таймаут (504)? Шукайте «connect() failed» проти «upstream timed out».
  3. Upstream → його залежності: БД, кеш, черга, інші HTTP-сервіси. Вам не потрібен повний tracing, щоб підтвердити очевидне: таймаути залежностей ростуть одночасно зі спалахами 504.

Друге: захопіть правильні відмітки часу і таймінги upstream

  • Додайте (або підтвердьте) поля в access-лозі Nginx: $upstream_addr, $upstream_status, $upstream_connect_time, $upstream_header_time, $upstream_response_time, $request_time.
  • У Docker перевірте, чи перезапуски/OOM-кілли контейнерів співпадають з хвилями 502.
  • Визначте, чи помилки стосуються окремої інстанції upstream (один поганий контейнер), чи це системна проблема (всі контейнери повільні).

Третє: вирішіть, чи змінювати таймаути, чи лагодити upstream

  • Якщо $upstream_connect_time високий або відсутній: лагодьте мережу, discovery сервісу, порти, здоров’я контейнера, ємність.
  • Якщо $upstream_header_time високий: upstream повільно починає відповідати; перевірте латентність додатка і залежності.
  • Якщо заголовки приходять швидко, але $upstream_response_time величезний: повільне стрімінг відповіді; перевірте розмір payload, буферизацію, повільних клієнтів, ліміти швидкості.

Таймаути — це не стратегія продуктивності. Це контракт. Змінюйте їх тільки після розуміння, що ви підписуєте.

Отримайте правильні логи: Nginx, Docker і додаток

Помилка Nginx: де починається правда

Якщо ви дивитесь лише access-логи Nginx, ви побачите коди статусу, але не «чому». Error-log містить режим відмови upstream: connect refused, no route to host, upstream prematurely closed, upstream timed out, resolver failure.

У контейнері переконайтесь, що Nginx пише error-логи в stdout/stderr або на змонтований том. Якщо він пише в /var/log/nginx/error.log всередині контейнера без тому, ви все ще зможете прочитати його через docker exec, але це незручно під час інциденту.

Access-лог Nginx: де ви вчитеся шаблонам

Access-логи — найкраще місце відповісти на питання «Чи це всі ендпоінти або лише один?» і «Чи це один upstream-екземпляр?». Але лише якщо ви логируєте upstream-поля.

Моя думка: логувати JSON. Людям все ще читабельно, а машинам — однозначно. Якщо сьогодні не можете змінити формат, принаймні додайте upstream-таймінги у поточний формат.

Docker-логи: контейнер брешe, якщо ви не дивитесь

Спалахи 502, що збігаються з перезапусками контейнера — не загадка. Це хронологія. Docker підкаже коли контейнер перезапустився, коли його вбили через OOM і чи перевірки здоров’я не проходять.

Логи додатка: підтвердьте, що upstream отримав запит

Логи додатка повинні відповісти: чи прийшов запит, який шлях, яка латентність, яка помилка. Якщо можете додати заголовок з ID запиту (наприклад, X-Request-ID) від Nginx до upstream і логувати його, сварки припиняться, а робота почнеться.

Короткий жарт #2: «Upstream timed out» — це операційна версія «Я вам передзвоню» від того постачальника, який ніколи не перетелефоновує.

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

Ось задачі, які я виконую, коли Nginx у Docker починає видавати 502/504. Кожне завдання містить: команду, що означає типовий вивід, і яке рішення застосувати далі.

Завдання 1: Підтвердити, що 502/504 повертає саме Nginx

cr0x@server:~$ curl -sS -D- -o /dev/null http://127.0.0.1/health
HTTP/1.1 502 Bad Gateway
Server: nginx/1.25.3
Date: Sat, 03 Jan 2026 10:12:01 GMT
Content-Type: text/html
Content-Length: 157
Connection: keep-alive

Що це означає: Відповідь йде від Nginx (Server header). Не від додатку.

Рішення: Ідіть у error-логи Nginx; ще не витрачайте час на логи додатку.

Завдання 2: Прочитати error-логи Nginx з контейнера

cr0x@server:~$ docker logs --tail=200 -f nginx
2026/01/03 10:11:58 [error] 28#28: *194 connect() failed (111: Connection refused) while connecting to upstream, client: 10.0.2.15, server: _, request: "GET /health HTTP/1.1", upstream: "http://172.19.0.5:8080/health", host: "example.internal"

Що це означає: Nginx намагався підключитися до upstream IP:port і отримав Connection refused. Контейнер існує в мережі, але на тому порті нічого не слухає (або процес не готовий).

Рішення: Перевірте стан upstream-контейнера, відображення портів і чи процес додатку слухає порт.

Завдання 3: Визначити, до якого upstream намагається звернутися Nginx

cr0x@server:~$ docker exec -it nginx nginx -T 2>/dev/null | sed -n '1,180p'
user  nginx;
worker_processes  auto;
error_log  /var/log/nginx/error.log warn;
events { worker_connections  1024; }
http {
    upstream app_upstream {
        server app:8080;
        keepalive 32;
    }
    server {
        listen 80;
        location / {
            proxy_set_header Host $host;
            proxy_set_header X-Request-ID $request_id;
            proxy_pass http://app_upstream;
            proxy_connect_timeout 5s;
            proxy_read_timeout 60s;
            proxy_send_timeout 60s;
        }
    }
}

Що це означає: Upstream використовує Docker DNS-ім’я app на порту 8080 з увімкненим keepalive.

Рішення: Перевірити розв’язування DNS в Docker і підтвердити, що додаток слухає 8080 всередині контейнера. Майте на увазі keepalive — він може посилити певні баги додатка.

Завдання 4: Перевірити DNS всередині контейнера Nginx

cr0x@server:~$ docker exec -it nginx getent hosts app
172.19.0.5     app

Що це означає: Вбудований DNS Docker розв’язує app у IP. Якщо це не вдається, Nginx може розв’язувати при запуску лише один раз або не розв’язувати взагалі (залежно від конфігурації).

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

Завдання 5: Протестувати з’єднання з Nginx до upstream-порту

cr0x@server:~$ docker exec -it nginx bash -lc 'nc -vz -w2 app 8080'
nc: connect to app (172.19.0.5) port 8080 (tcp) failed: Connection refused

Що це означає: IP доступний, але ніхто не приймає підключення на 8080.

Рішення: Перевірте контейнер додатку: чи працює, чи слухає на правильному інтерфейсі, чи не в crash-loop?

Завдання 6: Перевірити здоров’я upstream-контейнера і перезапуски

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
NAMES   STATUS                      PORTS
nginx   Up 2 hours                  0.0.0.0:80->80/tcp
app     Restarting (1) 12 seconds ago

Що це означає: Додаток перебуває у циклі перезапусків. Nginx невинний; він не може говорити з тим, що не в стані працювати.

Рішення: Витягніть логи додатку і шукайте причину падіння (помилка конфігу, відсутній секрет, OOM, міграції, відмова залежності).

Завдання 7: Шукати OOM-kill (класичний генератор 502)

cr0x@server:~$ docker inspect app --format '{{json .State}}'
{"Status":"restarting","Running":false,"Paused":false,"Restarting":true,"OOMKilled":true,"Dead":false,"Pid":0,"ExitCode":137,"Error":"","StartedAt":"2026-01-03T10:11:42.020785322Z","FinishedAt":"2026-01-03T10:11:52.901123812Z","Health":null}

Що це означає: OOMKilled:true і код виходу 137. Ядро вбило процес. Nginx лише повідомляє про наслідок.

Рішення: Додати пам’яті, зменшити навантаження, виправити витік пам’яті або встановити адекватні ліміти та автоскейлінг. Також розгляньте зменшення буферизації Nginx для великих відповідей лише якщо ви розумієте компроміси.

Завдання 8: Корелювати спалахи 502/504 із подіями перезапусків у Docker

cr0x@server:~$ docker events --since 30m --filter container=app
2026-01-03T10:02:11.000000000Z container die 1f2a3b4c5d (exitCode=137, image=app:prod, name=app)
2026-01-03T10:02:12.000000000Z container start 1f2a3b4c5d (image=app:prod, name=app)
2026-01-03T10:11:52.000000000Z container die 1f2a3b4c5d (exitCode=137, image=app:prod, name=app)
2026-01-03T10:11:53.000000000Z container start 1f2a3b4c5d (image=app:prod, name=app)

Що це означає: Додаток помер двічі за 30 хвилин. Якщо ваші 502 співпадають із цими відмітками, у вас причинно-наслідковий зв’язок, а не відчуття.

Рішення: Зосередьтеся на тому, чому додаток помирає. Не налаштовуйте таймаути Nginx — це не вирішить корінь проблеми.

Завдання 9: Якщо це 504, логувати таймінги і перевірити, де витрачається час

cr0x@server:~$ docker exec -it nginx awk 'NR==1{print; exit}' /var/log/nginx/access.log
10.0.2.15 - - [03/Jan/2026:10:14:09 +0000] "GET /api/report HTTP/1.1" 504 564 "-" "curl/8.5.0" rt=60.001 uct=0.001 uht=60.000 urt=60.000 ua="172.19.0.5:8080" us="504"

Що це означає: uct (connect) швидкий, але uht (час до заголовків) досяг 60s, що збігається з proxy_read_timeout. Upstream прийняв з’єднання, але не віддав заголовки вчасно.

Рішення: Це повільність upstream або дедлок, а не мережа. Перевірте латентність додатку, виклики залежностей, виснаження воркерів і БД.

Завдання 10: Підтвердити конфігурацію timeout в Nginx, яка фактично спричинила 504

cr0x@server:~$ docker exec -it nginx nginx -T 2>/dev/null | grep -R --line-number -E 'proxy_(connect|read|send)_timeout|send_timeout' -
69:            proxy_connect_timeout 5s;
70:            proxy_read_timeout 60s;
71:            proxy_send_timeout 60s;

Що це означає: Nginx дає upstream 60s на відповідь (заголовки). Це ваш контракт сьогодні.

Рішення: Якщо 60s забагато для законного ендпоінта, розділіть маршрути: підвищуйте таймаут лише для конкретної локації або переробіть ендпоінт (асинхронна робота + пулінг). Уникайте глобального збільшення таймаутів.

Завдання 11: Перевірити, що додаток слухає очікуваний порт всередині контейнера

cr0x@server:~$ docker exec -it app bash -lc 'ss -lntp | head -n 5'
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      4096   0.0.0.0:8080      0.0.0.0:*    users:(("app",pid=1,fd=7))

Що це означає: Додаток слухає на 0.0.0.0:8080, що правильно для мережі контейнера.

Рішення: Якби він слухав лише на 127.0.0.1, Nginx в іншому контейнері отримував би connection refused. Виправте bind-адресу додатку.

Завдання 12: Підтвердити, що контейнер Nginx і контейнер додатку поділяють Docker-мережу

cr0x@server:~$ docker inspect nginx --format '{{json .NetworkSettings.Networks}}'
{"frontend":{"IPAMConfig":null,"Links":null,"Aliases":["nginx"],"NetworkID":"8c1d...","EndpointID":"4a2b...","Gateway":"172.19.0.1","IPAddress":"172.19.0.3","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:13:00:03","DriverOpts":null}}
cr0x@server:~$ docker inspect app --format '{{json .NetworkSettings.Networks}}'
{"frontend":{"IPAMConfig":null,"Links":null,"Aliases":["app"],"NetworkID":"8c1d...","EndpointID":"9f8e...","Gateway":"172.19.0.1","IPAddress":"172.19.0.5","IPPrefixLen":16,"IPv6Gateway":"","GlobalIPv6Address":"","GlobalIPv6PrefixLen":0,"MacAddress":"02:42:ac:13:00:05","DriverOpts":null}}

Що це означає: Обидва знаходяться в мережі frontend. Зв’язність має бути можливою.

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

Завдання 13: Перевірити поведінку вбудованого DNS Docker і пастки розв’язування імен в Nginx

cr0x@server:~$ docker exec -it nginx cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:0

Що це означає: Вжито Docker DNS. Nginx розв’язує імена upstream залежно від того, як ви його налаштували. Якщо ви помістите hostname в блок upstream { server app:8080; }, Nginx зазвичай розв’язує його при старті і кешує.

Рішення: Якщо контейнери часто змінюють IP, або перезапускаються, або якщо ви використовуєте Compose — або перезавантажуйте Nginx при зміні IP, або використовуйте динамічне розв’язування (наприклад, resolver 127.0.0.11 плюс змінні в proxy_pass) з обережністю.

Завдання 14: Виявити проблеми з повторним використанням keepalive (сталi з’єднання)

cr0x@server:~$ docker logs --tail=200 nginx | grep -E 'upstream prematurely closed|recv\(\) failed|reset by peer' | head
2026/01/03 10:20:31 [error] 28#28: *722 upstream prematurely closed connection while reading response header from upstream, client: 10.0.2.15, server: _, request: "GET /api HTTP/1.1", upstream: "http://172.19.0.5:8080/api", host: "example.internal"

Що це означає: Upstream несподівано закрив з’єднання під час читання заголовків. Це може бути крах додатку, але також може бути невідповідність таймаутів keepalive upstream і повторного використання з’єднання Nginx.

Рішення: Порівняйте налаштування keepalive Nginx з таймаутами idle на upstream. Розгляньте тимчасове відключення keepalive upstream, щоб перевірити, чи припиняються помилки; потім виправляйте коректно (вирівняйте таймаути, налаштуйте keepalive_requests тощо).

Завдання 15: Перевірити тиск на хості, який робить усе «випадково» повільним

cr0x@server:~$ uptime
 10:24:02 up 41 days,  4:11,  2 users,  load average: 18.42, 17.90, 16.55

Що це означає: Високий load average може сигналізувати про перевантаження CPU, чергу runnable або блокований I/O. У контейнерному середовищі це може проявлятися як 504 через те, що upstream не може плануватися.

Рішення: Перевірте CPU і пам’ять; якщо хост насичений, жодні налаштування таймаутів Nginx не «полагодять» проблеми.

Завдання 16: Побачити тиск CPU/пам’яті по контейнерах у реальному часі

cr0x@server:~$ docker stats --no-stream
CONTAINER ID   NAME    CPU %     MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O
a1b2c3d4e5f6   nginx   2.15%     78.2MiB / 512MiB      15.27%    1.2GB / 1.1GB     12.3MB / 8.1MB
1f2a3b4c5d6e   app     380.44%   1.95GiB / 2.00GiB     97.50%    900MB / 1.3GB     1.1GB / 220MB

Що це означає: Додаток завантажує CPU і майже OOM. Очікуйте латентність і перезапуски. Це безпосередньо породжує 504 (повільно) і 502 (падіння).

Рішення: Додати потужності, виправити використання пам’яті, додати кешування, зменшити конкурентність або оптимізувати запит. Робіть по черзі — одне рішення за раз.

Завдання 17: Перевірити вичерпання connection tracking (підступне джерело 502/504 під навантаженням)

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

Що це означає: Ви близькі до макс conntrack. Нові з’єднання можуть псуватися або зависати; Nginx бачить помилки підключення або таймаути.

Рішення: Збільшити conntrack max (з урахуванням пам’яті), зменшити churn з’єднань (keepalive, pooling) або масштабувати. Також перевірте витоки з’єднань.

Завдання 18: Переконатися, що Nginx логирує таймінги upstream (або виправити)

cr0x@server:~$ docker exec -it nginx grep -R --line-number 'log_format' /etc/nginx/nginx.conf /etc/nginx/conf.d 2>/dev/null
/etc/nginx/nginx.conf:15:log_format upstream_timing '$remote_addr - $request_id [$time_local] '
/etc/nginx/nginx.conf:16:    '"$request" $status rt=$request_time uct=$upstream_connect_time '
/etc/nginx/nginx.conf:17:    'uht=$upstream_header_time urt=$upstream_response_time ua="$upstream_addr" us="$upstream_status"';

Що це означає: У вас є ключові таймінгові змінні. Добре. Тепер використовуйте їх.

Рішення: Якщо відсутні — додайте і перезавантажте Nginx. Без upstream-таймінгів ви неправильно діагностуватимете 504.

Завдання 19: Безпечно перезавантажити Nginx після змін конфігу

cr0x@server:~$ docker exec -it nginx nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
cr0x@server:~$ docker exec -it nginx nginx -s reload

Що це означає: Синтаксис валідний; reload застосує зміни без обриву існуючих з’єднань (у більшості типових налаштувань).

Рішення: Віддавайте перевагу reload над перезапуском контейнера посеред інциденту, якщо процес не застряг.

Завдання 20: Доведіть, що upstream повільний, роблячи прямі curl з неймспейсу мережі Nginx

cr0x@server:~$ docker exec -it nginx bash -lc 'time curl -sS -o /dev/null -w "status=%{http_code} ttfb=%{time_starttransfer} total=%{time_total}\n" http://app:8080/api/report'
status=200 ttfb=59.842 total=59.997

real    1m0.010s
user    0m0.005s
sys     0m0.010s

Що це означає: Upstream сам по собі потребує ~60s до першого байта і в цілому. 60s proxy_read_timeout Nginx знаходиться на межі; невелика нестабільність спричиняє 504.

Рішення: Лагодьте продуктивність upstream або переробіть ендпоінт. Підвищення таймаутів може тимчасово зупинити кровотечу, але також нагромаджує з’єднання і збільшує зону ураження.

Поширені помилки: симптом → корінь → виправлення

Цей розділ існує тому, що більшість «Nginx upstream помилок» — самонавічені. Ось ті, що постійно з’являються в контейнерних налаштуваннях.

1) Симптом: 502 з «connect() failed (111: Connection refused)»

  • Корінь: Upstream-контейнер перезапускається/впав; додаток слухає інший порт; додаток биндиться на 127.0.0.1 всередині контейнера.
  • Виправлення: Підтвердьте ss -lntp у контейнері додатку, змініть bind-адресу на 0.0.0.0, виправте порт у Nginx/Compose і додайте health checks, щоб Nginx не проксував до мертвих контейнерів.

2) Симптом: 502 з «no live upstreams»

  • Корінь: Усі upstream-сервери помічені Nginx як down (failed checks або max_fails), або ім’я upstream не розв’язалося при старті.
  • Виправлення: Переконайтесь, що Nginx може розв’язувати ім’я сервісу при старті; перезавантажуйте Nginx після змін мережі; перевірте записи upstream. Якщо робите blue/green, не лишайте Nginx вказаним на застаріле ім’я.

3) Симптом: 504 з «upstream timed out (110: Connection timed out) while reading response header»

  • Корінь: Upstream повільно генерує заголовки; пул потоків або event loop заблоковано; запити до БД повільні; upstream підтиснутий по CPU.
  • Виправлення: Логуйте $upstream_header_time. Якщо він високий — оптимізуйте upstream і залежності. Підвищуйте proxy_read_timeout лише для ендпоінтів, які справді цього потребують.

4) Симптом: 502 з «upstream prematurely closed connection»

  • Корінь: Додаток крашиться під час обробки запиту; upstream keepalive idle timeout коротший за інтервал повторного використання з’єднання Nginx; баги в proxy protocol/TLS.
  • Виправлення: Перевірте логи додатку на предмет падінь. Тимчасово відключіть upstream keepalive для тесту. Вирівняйте таймаути і розгляньте обмеження reuse через keepalive_requests в upstream.

5) Симптом: 502 тільки під час деплоїв

  • Корінь: Контейнери зупиняються до того, як нові будуть готові; відсутній readiness gate; Nginx розв’язав IP для контейнера, який щойно замінили.
  • Виправлення: Додайте readiness-ендпоїнти і health checks. У Compose чергуйте рестарти і перезавантажуйте Nginx, якщо ви залежите від розв’язування імен при старті. Краще мати стабільний VIP в оркестраторові або проксі, що робить динамічне розв’язування правильно.

6) Симптом: 504 спалахи, але логи додатку «на вигляд ок»

  • Корінь: Запити ніколи не доходять до додатку (застрягання в черзі Nginx, вичерпання conntrack, проблеми з SYN backlog або мережеві затори). Або додаток втрачає логи під навантаженням.
  • Виправлення: Порівняйте access-логи Nginx із логами додатку за request ID. Перевірте conntrack і навантаження хоста. Переконайтесь, що логування додатку не буферизується до смерті.

7) Симптом: випадкові 502 під навантаженням, зникають після масштабування

  • Корінь: Вичерпання дескрипторів файлів, вичерпання епемерних портів, тиск NAT таблиці або повільні клієнти, що викликають конкуренцію ресурсів.
  • Виправлення: Перевірте ulimit -n і відкриті файли, налаштуйте worker_connections, встановіть адекватні client timeouts і слідкуйте за conntrack.

8) Симптом: 502 після увімкнення HTTP/2 або змін TLS

  • Корінь: Неправильні очікування протоколу upstream (проксіювання на HTTPS upstream без proxy_ssl, або говорите HTTP на TLS-порт).
  • Виправлення: Перевірте схему і порти upstream, протестуйте напряму curl зсередини контейнера Nginx і переконайтесь, що upstream насправді HTTP там, де ви думаєте.

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

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

Компанія запустила простий Docker Compose стек: Nginx як зворотний проксі, Node.js API-контейнер і Redis. Одного понеділка вони побачили стіну 502. Перше і універсальне припущення команди було невірним: «Nginx не розв’язує upstream-ім’я». Тож вони змінили конфіг Nginx, захардкодивши IP, який бачили в docker inspect. Це «працювало» десять хвилин.

Потім знову впало. Тому що API-контейнер зациклювався на перезапусках; кожен рестарт давав нову IP, а їхнє «вирішення» застигло на вчорашній адресі. Error-log весь час говорив правду: connect() failed (111: Connection refused). Це не DNS. Це порт, на якому ніхто не слухає.

Вони нарешті подивились docker inspect і помітили OOMKilled:true. Контейнер мав малий ліміт пам’яті, а нова фіча створила великий in-memory кеш під певним шаблоном запитів. Під навантаженням ядро вбивало процес. Nginx не був зламаний; він стабільно спрямовував трафік до сервісу, який не був стабільно живий.

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

Міні-історія 2: Оптимізація, яка відбилась боком

Інша організація мала ініціативу з продуктивності: «Зменшимо латентність, увімкнувши keepalive скрізь». Хтось додав keepalive 128; в upstream-блок Nginx. Також підняли worker_connections. На папері — безкоштовний приріст швидкодії.

Дві тижні потому почалися періодичні 502: «upstream prematurely closed connection». Вони були рідкісні, але дратували клієнтів. Бекенд був Java-додатком з вбудованим сервером з меншим idle timeout, ніж вікно повторного використання з’єднань Nginx. Nginx повторно використовував з’єднання, яке upstream вже закрив. Іноді гонитва була на користь Nginx, іноді — ні.

Початкова реакція команди була типова: підвищити таймаути. Це зробило симптом рідшим… але збільшило використання ресурсів. Тепер навколо було більше «сирих» upstream-з’єднань, що займали файлові дескриптори й пам’ять. Під навантаженням проксі почав страждати, і латентність зростала.

Виправлення не було «більше keepalive». Виправлення — узгодити поведінку keepalive по всій ланцюжку: зменшити upstream keepalive в Nginx, вирівняти idle таймаути і обмежити повторне використання через keepalive_requests. Додали також логи таймінгів upstream, щоб майбутні проблеми показували, чи це час конекту чи чекання заголовків. Оптимізація стала керованим інструментом замість забобону.

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

Команда фінпослуг мала політику, яка здавалася майже комічно нудною: кожен reverse proxy повинен логувати таймінги upstream і коди статусу upstream, і кожен запит повинен мати request ID. Це застосовувалося в code review. Люди бурчали. Потім перестали бурчати.

Під час релізу в кварталі вони побачили сплеск 504 на одному ендпоінті. On-call витягнув access-логи Nginx і відфільтрував по шляху. Формат логу включав uct, uht та urt, плюс upstream_status. За кілька хвилин виявили шаблон: час підключення був малий, час до заголовків піднявся, а upstream status відсутній у деяких запитах — отже Nginx взагалі не отримував заголовків.

Вони змістили фокус: не мережа, не порт. Проблема — пул потоків додатку. За допомогою request ID зіставили провалені запити в логах додатку і побачили, що вони всі зависали на виклику зовнішнього сервісу. Той сервіс почав лімітити після зміни конфігу.

Інцидент вирішили без випадкових перезапусків: відкочування конфігу downstream, додавання клієнтського backoff і налаштування таймаутів Nginx лише для іншого ендпоінта, що легітимно стрімив дані. Ось як виглядає «нудно», коли воно працює: швидка ізоляція, чітка причинність, мінімальні побічні ефекти.

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

Чекліст A: Коли бачите 502

  1. Читайте error-логи Nginx: шукайте connect() failed, no live upstreams, prematurely closed, помилки резолвінгу.
  2. З контейнера Nginx тестуйте getent hosts і nc до upstream host:port.
  3. Перевірте стан upstream-контейнера: restarting, exited, unhealthy, OOM killed.
  4. Переконайтесь, що додаток слухає очікуваний порт і інтерфейс (0.0.0.0).
  5. Якщо це періодично — розслідуйте невідповідність keepalive або деплойний churn.

Чекліст B: Коли бачите 504

  1. Підтвердьте, що це read timeout: error-log має фразу «while reading response header» або access-логи показують високий uht.
  2. Перегляньте таймінги в access-лозі: високий uct — проблема підключення; високий uht — час до першого байта upstream; високий urt — повільний стріминг.
  3. Прямо curl-те upstream зсередини контейнера Nginx і виміряйте TTFB.
  4. Перевірте завантаження CPU/пам’яті upstream і латентність залежностей (БД, кеш, інші HTTP-сервіси).
  5. Лише після встановлення кореня: налаштовуйте proxy_read_timeout для конкретного маршруту, якщо потрібно.

Чекліст C: Налаштування логування, що окупиться

  1. Access-логи включають: request ID, upstream addr, upstream status, connect time, header time, response time, request time.
  2. Error-логи пишуться в stdout/stderr (зручно для контейнера) або на змонтований том з ротацією.
  3. Передавайте request ID до upstream і логируйте його там також.
  4. Слідкуйте за перезапусками контейнерів/OOM і корелюйте з хвилями 502.

Покроковий план: лагодьте без метушні

  1. Заморозьте зміни: припиніть деплои і правки конфігу, доки не ізолюєте режим відмови.
  2. Зберіть докази: error-логи Nginx, зріз access-логів з таймінгами, docker events, стан контейнера.
  3. Класифікуйте відмову:
    • Connect refused/no route → мережа/порт/життєвий цикл контейнера.
    • Upstream timed out reading headers → латентність upstream/зависання залежностей.
    • Premature close/reset → краші, невідповідність keepalive, протокольні негаразди.
  4. Виберіть одне втручання: масштабування upstream, rollback зміни, підвищення ліміту пам’яті, виправлення порту, або зміну таймауту для конкретного місця. Одне.
  5. Перевірте: підтвердьте, що рівень помилок падає і розподіл латентності покращився, а не просто один вдалий curl.
  6. Запровадьте профілактику: додайте логи, health checks, алерти по перцентилям таймінгів upstream і по перезапускам контейнерів.

Цікавинки і історичний контекст

  1. Nginx починався як C10k-рішення: його створили для ефективної обробки великої кількості одночасних з’єднань, саме тому він часто вибирається як зворотний проксі.
  2. 502 vs 504 — це словник шлюзу HTTP: ці коди існують тому, що шлюзи/проксі мусили повідомляти «наступний хоп зазнав невдачі», не прикидаючись, що origin відповів.
  3. Вбудований DNS Docker (127.0.0.11) — це архітектурний вибір: забезпечує discovery сервісів у користувацьких мережах, але не вирішує, як кожен додаток кешує DNS.
  4. Nginx розв’язує імена upstream по-різному залежно від конфігу: hostname в блоках upstream зазвичай розв’язуються при запуску, що дивує під час churn контейнерів.
  5. Keepalive старше за хайп мікросервісів: постійні з’єднання існують десятиліттями; вони чудові, поки невідповідні idle таймаути не перетворять «оптимізацію» на періодичні відмови.
  6. 504 часто корелює з чергуванням, а не лише «повільним кодом»: коли пули воркерів заповнюються, латентність може різко зрости без змін у коді.
  7. OOM kills маскуються під мережеві проблеми: з точки зору Nginx, падаючий upstream виглядає як refused connections або premature closes, а не як «закінчилась пам’ять».
  8. Conntrack exhaustion — сучасна класика: NAT і stateful фаєрвол трекінг можуть стати вузьким місцем задовго до того, як CPU досягне 100%.
  9. Дефолтні таймаути — культурні артефакти: багато стеків успадковують 60s таймаути від старих уявлень про веб-запити, навіть коли навантаження змінилося на довготривалі API і стрімінг.

FAQ

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

У Docker додається щонайменше один мережевий хоп і часто змінюється bind-поведінка. Додаток може слухати на 127.0.0.1 всередині контейнера, що працює локально, але недоступно для Nginx в іншому контейнері. Перевірте з ss -lntp всередині контейнера.

2) Як визначити, чи 504 — це таймаут Nginx, чи upstream повернув 504?

Перевірте $upstream_status в access-логах. Якщо Nginx згенерував 504 через таймаут, upstream status може бути порожнім або іншим. Також читайте error-log Nginx: там буде «upstream timed out … while reading response header».

3) Чи варто просто підвищити proxy_read_timeout, щоб зупинити 504?

Тільки якщо ви впевнені, що ендпоінт має займати саме стільки часу і ви готові прив’язувати проксі-з’єднання надовго. Інакше ви ховаєте проблему ємності і збільшуєте ризики. Краще виправляти латентність upstream або переводити довгі задачі в асинхронну роботу.

4) Мій upstream — це ім’я сервісу в Compose. Чому Nginx іноді потрапляє на неправильну IP після redeploy?

Nginx часто розв’язує upstream-імена при старті і тримає IP. Якщо контейнер замінено і отримав нову IP, Nginx може продовжувати використовувати стару, поки ви не перезавантажите. Рішення: перезавантажувати Nginx при деплої, обережно використовувати динамічне розв’язування або застосовувати VIP оркестратора.

5) Чому я бачу «upstream prematurely closed connection» без логів падіння додатку?

Це може бути через повторне використання keepalive до upstream, який закрив idle-з’єднання, або через протокольну невідповідність. Протестуйте, тимчасово відключивши upstream keepalive, і подивіться, чи пропаде симптом. Також перевірте, що логи додатку не втрачаються під навантаженням.

6) Чи може повільний клієнт спричиняти таймаути upstream?

Так. Якщо ви буферизуєте відповіді або стрімите великі payload-и, повільні клієнти тримають з’єднання відкритими і споживають воркер-ресурси, непрямо викликаючи черги і 504. Логуйте request time разом з upstream time, щоб відрізнити «upstream повільний» від «клієнт повільний».

7) Як відрізнити проблеми часу підключення від латентності додатку?

Використовуйте поля таймінгів upstream. Високий або невдалий $upstream_connect_time вказує на мережу/порт/доступність сервісу. Високий $upstream_header_time вказує на обробку upstream або проблеми залежностей.

8) Чи завжди включення upstream keepalive в Nginx допомагає?

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

9) Я використовую кілька upstream-контейнерів. Як побачити, чи лише один інстанс поганий?

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

10) Яка мінімальна зміна логування, яка робить дебаг upstream адекватним?

Додайте $request_id, $upstream_addr, $upstream_status і три upstream-таймінги (connect, header, response) в access-логи. І тримайте error-log доступним.

Висновок: наступні кроки, щоб не повторилося

Якщо запам’ятати одну річ: налагодження 502/504 — це питання таймінгів і топології. Ви не виправите це методом вгадування. Ви виправите, логуючи upstream-стик правильно і довівши, де запит помирає.

Зробіть наступне:

  1. Оновіть формат access-логу Nginx, щоб включати таймінги upstream, upstream addr і upstream status. Якщо ви цього не логируєте, ви обираєте повільні інциденти.
  2. Зробіть error-логи Nginx легкодоступними в Docker (stdout/stderr або змонтований том). Під час аутеджу питання «де логи?» — не найприємніша гра в пошук.
  3. Запровадьте request ID наскрізь і логируйте його в додатку. Кореляція перемагає суперечки.
  4. Додайте health checks і readiness gates, щоб деплои не виготовляли 502.
  5. Припиніть вважати таймаути за рішення. Використовуйте їх як сигнал. Якщо підвищуєте — робіть це по маршруту, свідомо і з моніторингом.

Потім прогайте game day: спеціально вбийте upstream-контейнер, уповільніть його і подивіться, чи ваші логи скажуть правду за менше ніж п’ять хвилин. Якщо не скажуть — це ваша реальна помилка.

← Попередня
WireGuard повільний: MTU, маршрутизація, CPU — прискорте без здогадок
Наступна →
Proxmox «зберігання резервних копій недоступне на вузлі»: чому «shared» не означає спільне

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