Растёт очередь на своём inference-сервере
Свой inference-сервер (vLLM, TGI, Ollama) не эластичен как облачный API: у него фиксированное число GPU и конечная пропускная способность. Когда запросов приходит больше, чем карта успевает считать, они копятся в очереди — и пользователь видит спиннер на 30 секунд вместо ответа. Дашборд это покажет, только если на него смотреть; push приходит сам.
vLLM и TGI отдают Prometheus-метрики на /metrics. Опрашиваем их
scheduled-функцией и алёртим по порогу
на глубину очереди.
Вариант 1: vLLM — парсим /metrics
Заголовок раздела «Вариант 1: vLLM — парсим /metrics»У 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 — очередь бьёт напрямую по пользователям (они ждут), поэтому
громко, но не «десятка»: сервер жив, просто перегружен.
Вариант 2: TGI — свои имена метрик
Заголовок раздела «Вариант 2: TGI — свои имена метрик»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 timet0 = time.time()requests.post("http://localhost:11434/api/generate", json={"model": "llama3", "prompt": "ping", "stream": False}, timeout=30)dt = time.time() - t0if 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, включить троттлинг на входе.