Lab 09-02: Proxy e monitoraggio¶
Obiettivo¶
Mettere llama serve dietro Caddy con HTTPS e chiave API, raccogliere le metriche con Prometheus, costruire
una dashboard Grafana e usarla per leggere cosa succede sotto carico: quante richieste finiscono in coda,
quanti token al secondo, quanta cache del prompt riusi.
sequenceDiagram
participant C as Client (carico.py)
participant P as Caddy :8443
participant S as llama serve :8080
participant M as Prometheus
C->>P: POST /v1/chat/completions (Bearer)
P->>S: inoltra (HTTP, rete interna)
S-->>P: stream SSE
P-->>C: stream SSE (flush immediato)
M->>S: GET /metrics ogni 15 s (Bearer)
S-->>M: llamacpp:* Prerequisiti¶
- Lab 01-04 (Docker e Compose funzionanti) e lab 09-01 (concetti di chiave API e
/health). - ~3 GB di RAM liberi per lo stack (server con Qwen3.5-0.8B ≈ 1,2 GB, Prometheus + Grafana ≈ 0,5 GB, Caddy trascurabile).
- Porte libere: 8443 (Caddy), 9090 (Prometheus, solo localhost), 3000 (Grafana, solo localhost).
Materiale¶
Tutto nella cartella di questo lab:
| File | Ruolo |
|---|---|
compose.yaml |
quattro servizi: llama, caddy, prometheus, grafana |
Caddyfile |
HTTPS su localhost con certificato interno, /health pubblico, Bearer passante |
prometheus.yml |
scrape di /metrics con la chiave API |
grafana/provisioning/datasources/prometheus.yml |
Prometheus già configurato come data source |
api-keys.txt.example |
da copiare in api-keys.txt |
carico.py |
generatore di carico (solo libreria standard) |
| Modello | ggml-org/Qwen3.5-0.8B-GGUF → Qwen3.5-0.8B-Q8_0.gguf, 785 MiB, Apache-2.0 |
Procedura¶
1. Preparare modello e chiave¶
cd corso/laboratori/lab-09-02-proxy-monitoraggio
mkdir -p models
# copia il file dalla cache Hugging Face (o scaricalo con llama download e poi copialo)
cp ~/.cache/huggingface/hub/models--ggml-org--Qwen3.5-0.8B-GGUF/snapshots/*/Qwen3.5-0.8B-Q8_0.gguf models/
cp api-keys.txt.example api-keys.txt
sed -i "s/chiave-di-prova-cambiami/$(openssl rand -hex 16)/" api-keys.txt prometheus.yml # (1)!
cat api-keys.txt
- La stessa chiave deve finire sia nel file letto dal server sia nella sezione
authorizationdi Prometheus:/metricsnon è pubblico.
2. Leggere compose.yaml¶
Prima di avviarlo, cerca nel file le risposte a queste domande: perché llama non ha ports:? Cosa fa
start_period? Perché caddy ha depends_on … condition: service_healthy? Perché Prometheus e Grafana
sono pubblicati solo su 127.0.0.1?
3. Avviare lo stack¶
docker compose up -d
watch -n 2 docker compose ps # aspetta che llama sia "Up (healthy)" e caddy "Up"
Output atteso di docker compose ps dopo 1-2 minuti:
NAME IMAGE STATUS
lab-09-02-caddy-1 caddy:2 Up 40 seconds
lab-09-02-grafana-1 grafana/grafana:12.1.0 Up 2 minutes
lab-09-02-llama-1 ghcr.io/ggml-org/llama.cpp:server-b11046 Up 2 minutes (healthy)
lab-09-02-prometheus-1 prom/prometheus:v3.5.0 Up 2 minutes
Caddy parte dopo che il server è healthy. Nei log del server: docker compose logs llama | grep -E "n_slots|listening".
4. Provare il proxy¶
KEY=$(grep -v '^#' api-keys.txt | head -1)
curl -sk https://localhost:8443/health; echo # (1)!
curl -sk https://localhost:8443/v1/models; echo # 401: manca la chiave
curl -sk https://localhost:8443/v1/chat/completions \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"messages":[{"role":"user","content":"Ciao, chi sei?"}],"max_tokens":40,"chat_template_kwargs":{"enable_thinking":false}}' | python3 -m json.tool | head -20
-kperché il certificato è emesso dalla CA interna di Caddy (tls internal). Con un dominio vero e Let's Encrypt non serve.
Poi lo streaming: aggiungi "stream": true e osserva che i chunk arrivano uno alla volta (è flush_interval -1
nel Caddyfile; commentalo, docker compose restart caddy, e riprova: arrivano tutti insieme).
Vedi anche i log di accesso del proxy: docker compose logs caddy | tail -3 (una riga JSON per richiesta, con
status e durata).
5. Prometheus¶
Apri http://localhost:9090/targets: il job llamacpp deve essere UP. Se è DOWN con 401, la chiave in
prometheus.yml non coincide con quella del server (docker compose restart prometheus dopo averla corretta).
Nella barra di ricerca prova: llamacpp:tokens_predicted_total, poi rate(llamacpp:tokens_predicted_total[1m]).
Fai qualche richiesta e guarda i valori cambiare (lo scrape è ogni 15 s).
6. Grafana: la dashboard¶
Apri http://localhost:3000 (admin / cambiami). Il data source Prometheus è già presente. Crea una dashboard con questi pannelli:
| # | Titolo | Tipo | Query PromQL |
|---|---|---|---|
| 1 | Token generati / s | Time series | rate(llamacpp:tokens_predicted_total[1m]) |
| 2 | Prompt token / s | Time series | rate(llamacpp:prompt_tokens_total[1m]) |
| 3 | Richieste in corso e in coda | Time series (2 query) | llamacpp:requests_processing · llamacpp:requests_deferred |
| 4 | Slot occupati per decode | Gauge (max = -np) |
llamacpp:n_busy_slots_per_decode |
| 5 | Cache hit del prompt | Stat (%) | rate(llamacpp:prompt_tokens_cached_total[5m]) / (rate(llamacpp:prompt_tokens_total[5m]) + rate(llamacpp:prompt_tokens_cached_total[5m])) |
| 6 | Secondi per token generato | Stat | rate(llamacpp:tokens_predicted_seconds_total[5m]) / rate(llamacpp:tokens_predicted_total[5m]) |
| 7 | Server up | Stat (0/1, rosso/verde) | up{job="llamacpp"} |
Imposta l'intervallo di aggiornamento a 10 s (in alto a destra) e salva la dashboard. Esporta il JSON (Share → Export) e mettilo nella cartella del lab: farà parte della consegna.
7. Sotto carico¶
python3 carico.py --url https://localhost:8443 --key "$KEY" --n 2 --insecure
python3 carico.py --url https://localhost:8443 --key "$KEY" --n 6 --insecure
Output atteso (valori indicativi su CPU a 4 core):
== 2 richieste in parallelo, max_tokens=32, durata totale 9.8 s
#0: primo token dopo 1.41 s, totale 9.6 s, 32 chunk
#1: primo token dopo 1.52 s, totale 9.8 s, 32 chunk
latenza al primo token: mediana 1.47 s, max 1.52 s
throughput aggregato: 6.5 token/s
/metrics: +64 token generati, +58 token di prompt, requests_deferred ora = 0
== 6 richieste in parallelo, max_tokens=32, durata totale 31.2 s
#0: primo token dopo 1.6 s ...
#2: primo token dopo 11.9 s ... ← ha aspettato uno slot libero
...
latenza al primo token: mediana 12.1 s, max 22.4 s
Con -np 2 e 6 richieste, quattro finiscono in coda: durante il test guarda il pannello 3 (requests_deferred
sale a 4) e il pannello 4 (slot occupati = 2). Il throughput aggregato resta simile: la macchina era già satura.
8. Cambiare il dimensionamento¶
Porta LLAMA_ARG_N_PARALLEL a 4 in compose.yaml (con LLAMA_ARG_CTX_SIZE: 8192 ogni slot scende a 2048
token), docker compose up -d llama, aspetta healthy e ripeti il test con --n 6. Confronta: latenza al
primo token, throughput aggregato, requests_deferred. Su CPU aspettati un throughput totale quasi uguale e
una velocità per utente più bassa; su GPU il throughput aggregato cresce.
9. Spegnere¶
Verifica del risultato¶
-
https://localhost:8443/healthrisponde 200 senza chiave;/v1/modelsrisponde 401 senza chiave e 200 con. - Il target
llamacppin Prometheus è UP ellamacpp:tokens_predicted_totalcresce dopo una richiesta. - La dashboard ha almeno 5 dei 7 pannelli e mostra il picco di
requests_deferreddurantecarico.py --n 6. - Sai spiegare a parole la differenza tra i due test (
-np 2vs-np 4).
Domande e varianti¶
- Attiva
basic_authnel Caddyfile (decommenta, genera l'hash condocker compose exec caddy caddy hash-password --plaintext ...) eLLAMA_ARG_UI: 1: la web UI ora chiede utente e password; le API continuano a volere il Bearer? Perché sì? - Rimuovi
condition: service_healthye faidocker compose up -d --force-recreate: che cosa risponde il proxy nei primi secondi? - Sostituisci Caddy con Nginx usando il blocco
??? notedel modulo (immaginenginx:alpine, certificato autofirmato conopenssl req -x509). Quale delle due configurazioni è più lunga e perché? - Aggiungi un alert in Prometheus (
rules.yml) perllamacpp:requests_deferred > 0per 2 minuti e verifica che scatti concarico.py --n 8. - Con un dominio vero: cambia
localhostinllm.tuodominio.it, toglitls internal, pubblica80:80e443:443. Cosa deve essere vero lato DNS e firewall perché Caddy ottenga il certificato? - Router mode: togli
LLAMA_ARG_MODEL, monta la cartella con due GGUF su/modelse aggiungiLLAMA_ARG_MODELS_DIR: /models. Cosa devi cambiare inprometheus.yml(?model=) e nel body delle richieste?
Consegna¶
compose.yaml,Caddyfile,prometheus.ymlfinali (chiavi oscurate) e il JSON esportato della dashboard.- Uno screenshot della dashboard durante il test con
--n 6. - L'output di
carico.pynelle tre configurazioni (-np 2con--n 2e--n 6,-np 4con--n 6) e 5 righe di commento: quanti utenti concorrenti reggerebbe la tua macchina con una latenza al primo token sotto i 3 secondi?