Перейти к содержимому

Растёт очередь на своём inference-сервере

Свой inference-сервер (vLLM, TGI, Ollama) не эластичен как облачный API: у него фиксированное число GPU и конечная пропускная способность. Когда запросов приходит больше, чем карта успевает считать, они копятся в очереди — и пользователь видит спиннер на 30 секунд вместо ответа. Дашборд это покажет, только если на него смотреть; push приходит сам.

vLLM и TGI отдают Prometheus-метрики на /metrics. Опрашиваем их scheduled-функцией и алёртим по порогу на глубину очереди.

У vLLM есть готовый gauge vllm:num_requests_waiting (запросы в очереди) и vllm:num_requests_running (на исполнении). Порог + state-файл против спама:

import os, re, requests
NOTIFLY_URL = os.environ["NOTIFLY_URL"]
NOTIFLY_TOKEN = os.environ["NOTIFLY_TOKEN"]
METRICS_URL = os.environ.get("METRICS_URL", "http://localhost:8000/metrics")
WAITING_LIMIT = 20 # ожидающих запросов, выше которого — алёрт
FLAG = "/tmp/infq-alerted.flag"
def notify(title, msg, prio):
requests.post(f"{NOTIFLY_URL}/message",
params={"token": NOTIFLY_TOKEN},
json={"title": title, "message": msg, "priority": prio},
timeout=5)
def scrape(metric: str) -> float:
text = requests.get(METRICS_URL, timeout=5).text
# строка вида: vllm:num_requests_waiting{...} 42.0
for line in text.splitlines():
if line.startswith(metric):
m = re.search(r"\s([0-9.]+)\s*$", line)
if m:
return float(m.group(1))
return 0.0
def handler(event, context):
waiting = scrape("vllm:num_requests_waiting")
running = scrape("vllm:num_requests_running")
if waiting > WAITING_LIMIT:
if not os.path.exists(FLAG):
notify("🐌 Очередь инференса растёт",
f"В очереди {waiting:.0f} запросов (порог {WAITING_LIMIT}), "
f"на GPU считается {running:.0f}.\n"
f"Пользователи ждут — добавьте реплику или включите троттлинг.",
prio=8)
open(FLAG, "w").close()
elif os.path.exists(FLAG):
os.remove(FLAG) # очередь разобралась — снимаем flag, готовы к следующему разу
return {"statusCode": 200, "body": f"waiting={waiting} running={running}"}

priority=8 — очередь бьёт напрямую по пользователям (они ждут), поэтому громко, но не «десятка»: сервер жив, просто перегружен.

Text Generation Inference отдаёт tgi_queue_size и гистограмму tgi_request_queue_duration. Меняется только имя метрики:

waiting = scrape("tgi_queue_size")
# latency в очереди: берём сумму/счётчик гистограммы для среднего

Если важнее не длина очереди, а время ожидания, считайте среднее по гистограмме _sum / _count и алёртите, когда среднее ожидание в очереди превышает, скажем, 2 секунды.

Вариант 3: Ollama — метрик нет, меряем latency снаружи

Заголовок раздела «Вариант 3: Ollama — метрик нет, меряем latency снаружи»

У Ollama нет Prometheus-эндпоинта, поэтому очередь меряем косвенно — активным монитором или синтетическим запросом с замером времени. Растёт latency канареечного запроса = растёт очередь:

import time
t0 = time.time()
requests.post("http://localhost:11434/api/generate",
json={"model": "llama3", "prompt": "ping", "stream": False},
timeout=30)
dt = time.time() - t0
if dt > 5:
notify("🐌 Ollama медленно отвечает",
f"Канареечный запрос занял {dt:.1f}с — вероятно, очередь/перегрузка.",
prio=7)

Вариант 4: heartbeat «воркер разгребает очередь»

Заголовок раздела «Вариант 4: heartbeat «воркер разгребает очередь»»

Инвертированный сигнал: пусть воркер шлёт heartbeat после каждого обработанного запроса. Пропали пинги — значит, воркер завис или умер под нагрузкой, и Notifly пришлёт алёрт по таймауту, даже если /metrics уже недоступен.

  • имя сервера/модели и endpoint;
  • num_requests_waiting / queue_size и порог;
  • среднее время ожидания в очереди, если считаете;
  • подсказка: добавить реплику, поднять max_num_seqs, включить троттлинг на входе.