← Back to blog

NGINX: Upstream и деградационные сценарии

⚙️ Теория

🔄 Upstream lifecycle

Упрощённый путь проксированного запроса:

connectsendreadbuffersend downstream

По шагам:

  1. connect
    NGINX открывает соединение к backend.
  2. send
    отправляет upstream-запрос (headers/body).
  3. read
    читает ответ backend.
  4. buffer
    временно хранит данные в буферах/файлах (если включена буферизация).
  5. send downstream
    отправляет ответ клиенту с учётом скорости клиента.

📦 buffering

proxy_buffering on:

  • NGINX старается быстрее дочитать ответ upstream в свои буферы;
  • backend освобождается раньше;
  • медленный клиент меньше влияет на backend.

proxy_buffering off:

  • ответ ближе к stream-передаче клиенту;
  • меньше внутренней буферизации;
  • но медленный клиент сильнее “удерживает” request в работе.

🚦 backpressure

Backpressure появляется, когда следующий этап медленнее предыдущего:

  • backend отвечает быстрее, чем клиент читает;
  • или клиентов слишком много, и NGINX/ядро не успевают отправлять данные.

Тогда растут очереди, время жизни request и латентность.


🐢 slow upstream

Медленный backend увеличивает длительность каждого запроса.
При фиксированном числе connection это снижает throughput: меньше запросов завершается в единицу времени.


🔁 retry

NGINX может повторить запрос на другой upstream при ошибках/таймаутах (в пределах настроек):

  • proxy_next_upstream
  • proxy_next_upstream_tries
  • proxy_next_upstream_timeout

По документации retry возможен только если клиенту ещё ничего не отправлено;
для неидемпотентных методов (POST, LOCK, PATCH) повтор отключён по умолчанию и включается отдельно (non_idempotent).

Retry повышает устойчивость, но при перегрузе может усилить нагрузку на backend.


Что нужно уметь объяснить

Почему медленный upstream может “убить” throughput?

Если backend отвечает дольше, каждый request занимает worker-ресурсы дольше.
Одновременно растёт число незавершённых запросов, а завершённых в секунду становится меньше.
Когда в полёте слишком много запросов, система уходит в очереди и tail latency (p95/p99) резко растёт.

Что происходит при заполнении worker_connections?

  • worker перестаёт принимать новые клиентские соединения;
  • также может отказываться от новых upstream-соединений;
  • в stub_status метрика handled перестаёт догонять accepts;
  • даже при свободном CPU сервис деградирует из-за лимита соединений/FD.

То есть это hard-limit на параллелизм одного worker.

Как proxy_buffering влияет на память?

  • on: больше памяти (и иногда temp files), зато upstream освобождается быстрее;
  • off: меньше внутренней буферизации, но request дольше живёт при медленных клиентах.

Компромисс: память vs устойчивость к slow clients.


🧪 Практика

1. Создать backend с 500ms delay

python3 -c '
from http.server import BaseHTTPRequestHandler, HTTPServer
import time

class H(BaseHTTPRequestHandler):
    def do_GET(self):
        time.sleep(0.5)  # 500ms delay
        body = b"ok\n"
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

HTTPServer(("127.0.0.1", 18080), H).serve_forever()
'

2. Настроить proxy + статус

upstream slow_backend {
    server 127.0.0.1:18080;
    keepalive 64;
}

server {
    listen 8080;
    server_name _;

    location / {
        proxy_pass http://slow_backend;
        proxy_connect_timeout 1s;
        proxy_read_timeout 2s;
        proxy_buffering on;  # потом сравнить с off
    }

    location /nginx_status {
        stub_status;
        allow 127.0.0.1;
        deny all;
    }
}

Применить:

nginx -t && sudo nginx -s reload

3. Подать 1000 RPS

Если установлен wrk2 (фиксированный RPS):

wrk -t8 -c400 -d30s -R1000 http://127.0.0.1:8080/

Если обычный wrk, подбирайте -t/-c, чтобы выйти примерно на 1000 RPS:

wrk -t8 -c800 -d30s http://127.0.0.1:8080/

4. Наблюдать active connections, CPU, latency

active connections:

watch -n1 "curl -s http://127.0.0.1:8080/nginx_status"

CPU worker:

top -H -p $(pgrep -d',' -f 'nginx: worker process')

Latency:

  • из отчёта wrk (Latency, Req/Sec, tail values);
  • дополнительно смотрите ошибки/таймауты в access.log и error.log.

Сравните два прогона: proxy_buffering on и proxy_buffering off.


🧾 Вывод

Деградация upstream почти всегда проявляется как рост времени жизни запроса, затем рост активных соединений и падение throughput.
proxy_buffering и retry-политика меняют распределение нагрузки между backend, NGINX и клиентами, но не убирают корневую причину медленного upstream.
Надёжный анализ строится на трёх метриках одновременно: active connections, CPU по worker и latency под фиксированным профилем нагрузки.


📚 Ссылки


Проверка источников

NGINX: Upstream и деградационные сценарии | Aleksandr Suprun