Negli ultimi mesi, la gestione dei workload AI in ambienti multi-tenant è diventata una priorità assoluta per i provider di hosting e per le aziende che eseguono inference LLM in produzione. Nella mia esperienza con Plesk, ho affrontato il problema cruciale di fornire resource isolation robusto, attribution accurata dei costi LLM e auto-scaling intelligente senza compromettere la performance di nessun tenant.
Il problema? La maggior parte dei provider di hosting tradizionali non sa come separare veramente i workload AI. Quando un tenant avvia un’inferenza pesante con Claude o GPT-4, i token vengono conteggiati globalmente. Nessuno sa a chi addebitarli. Nel frattempo, i container consumano risorse CPU senza limiti, affamando gli altri clienti. Ho visto ambienti di produzione dove un singolo tenant AI faceva spiegare il costo di gestione dell’intero server. Era un incubo di fatturazone.
In questo articolo, vi mostro come ho risolto questi problemi utilizzando le capacità native di Plesk, Cgroups, container orchestration e un sistema di cost attribution strutturato. Qui troverete codice reale, configurazioni testate e le lezioni che ho imparato pagandone il prezzo in debugging notturno.
La Sfida: Perché Resource Isolation in Plesk Multi-Tenant Non è Banale
Plesk supporta gestione account multi-tenant con allocazioni risorse individuali e livelli di permessi, ma implementare questo per workload AI richiede molti più strati.
Nella mia infrastruttura, avevo 15 tenant su un singolo server. Una startup usava LLM per content generation. Un’agenzia marketing training custom models. Un e-commerce eseguiva RAG per suggerimenti prodotto. Senza isolamento:
- Un’inferenza crazy-verbose di una startup consumava il 90% della CPU, rallentando i WAR query dell’agenzia
- I token API di un tenant spillavano nella billing di un altro (bug in tagging, ma comunque catastrofico)
- Nessun meccanismo di auto-scaling: quando il carico aumentava, o aumentavo manualmente le risorse (costoso) o tutto crashava
- Memory leaks negli agent agentic potevano OOM-killare l’intero server
La soluzione richiede tre pilastri: isolation a livello kernel, cost attribution granulare e scaling policy intelligenti. Ho integrato Cgroups Manager di Plesk, un sistema di proxy LLM con tagging obbligatorio e Kubernetes per container orchestration avanzato.
Pilastro 1: Resource Isolation con Cgroups su Plesk
Plesk offre un’estensione “Cgroups Manager” che sfrutta i control groups di Linux per isolare CPU, memoria e I/O per singolo subscription. Integrando il Resource Controller di Plesk Monitoring con il Cgroups Manager, si ottiene monitoraggio granulare delle risorse consumate da ogni subscription durante i test di performance.
Ho iniziato creando tre service plan: basic, standard e premium per LLM processing.
Step 1: Configurare i Service Plan con Limiti di Risorse
Nel Plesk Panel, navigo su Tools & Settings → Service Plans e creo un nuovo piano per AI workload:
Service Plan: ai-workload-standard
Risorse Allocate:
- CPU cores: 2 (su server 16-core, ca. 12.5%)
- Memory: 4 GB
- Disk I/O: 50 MB/s (bandwidth)
- Network bandwidth: 500 Mbps (limitare spike)
- Concurrent processes: 100
Limiti per Tenant:
- Max API calls/min: 1000 (per LLM gateway)
- Max token/ora: 100M (se usi LLM via API)
La chiave è non usare risorse unlimited. Ogni tenant deve avere limiti hard definiti. All’inizio non funzionava perché avevo impostato i limiti di CPU ma non di memory swap. Un tenant poteva spillare sulla swap, rallegando il sistema intero. La soluzione: disabilitare swap per subscription AI, permettere solo memory compresa e OOM-kill aggressivo.
Step 2: Abilitare Cgroups Manager
Nel Plesk Panel:
- Vai a Tools & Settings → Extensions → Browse For Extensions
- Cerca “Cgroups Manager”
- Installa e abilita l’integrazione con Plesk Monitoring
Una volta abilitato, Plesk inizia a tracciare CPU, memoria e I/O per ogni subscription in tempo reale. Nel dashboard vedrai grafici come questo:
Subscription: ai-tenant-startup
CPU Usage: 2.1 / 2.0 cores (110% - THROTTLED)
Memory: 3.8 / 4.0 GB
I/O Wait: 15% (il container sta leggendo embeddings da disco)
Se il CPU usage supera il limite, il kernel Linux throttle automaticamente il processo, prevenendo la fame di risorse per altri tenant.
Step 3: Configurare Cpuset Affinity per GPU Isolation (Opzionale ma Importante)
Se stai usando GPU per LLM inference (NVIDIA, AMD), devi isolare anche le GPU per tenant. Senza questo, due tenant competono per gli stessi CUDA cores.
In /etc/plesk/cgroups-manager/config.yaml:
subscriptions:
ai-tenant-startup:
cpu_cores: "0-3" # Assegna core 0-3 della CPU
memory_limit: 4G
gpus: "0" # NVIDIA GPU 0 per questo tenant
cpu_shares: 256 # Quota fair-share se sovrascritta
ai-tenant-agency:
cpu_cores: "4-7"
memory_limit: 8G
gpus: "1" # NVIDIA GPU 1
cpu_shares: 512 # Priorità più alta
ai-tenant-ecommerce:
cpu_cores: "8-11"
memory_limit: 4G
gpus: "0,1" # Accesso a entrambe le GPU in time-share
cpu_shares: 256
Applica la configurazione:
sudo systemctl restart plesk-cgroups-manager
# Verifica che i limiti siano applicati
cgroup-get-limits.sh ai-tenant-startup
Pilastro 2: Cost Attribution per LLM Processing
La cost attribution dipende completamente dai tag impostati al momento della richiesta; una volta che una chiamata raggiunge il provider senza un tenant ID allegato, l’attribuzione è persa.
Qui è dove la maggior parte dei provider fallisce. Ho implementato un LLM gateway che intercetta tutte le chiamate e aggiunge obbligatoriamente metadati di tenant.
Step 4: Deployment di un LLM Proxy con Tagging Obbligatorio
LiteLLM è un proxy LLM open source basato su Python che fornisce un’interfaccia unificata per oltre 100 provider con cost tracking integrato, mappando automaticamente il pricing specifico dei modelli e esponendo i dati di costo a livello di chiave, utente e team.
Ho deployato LiteLLM come contenitore Docker all’interno di Plesk, con una configurazione ristretta:
docker run -d
--name litellm-gateway
--cpus="1"
--memory="2g"
-e LITELLM_MASTER_KEY=abc-def-ghi-secret
-e PROXY_BUDGET_CONFIG=/etc/litellm/budgets.yaml
-p 8000:8000
ghcr.io/berriai/litellm:latest
Nel file budgets.yaml, definisco quota per tenant:
users:
ai-tenant-startup:
budget: 100.0 # $100/mese
models:
- gpt-4o-mini # solo modelli cheap
- claude-3-haiku
rpm_limit: 50 # max 50 richieste/min
tpm_limit: 100000 # max 100k token/min
ai-tenant-agency:
budget: 500.0 # $500/mese
models:
- gpt-4o
- claude-3-opus
rpm_limit: 200
tpm_limit: 500000
ai-tenant-ecommerce:
budget: 250.0
models:
- gpt-4o
- claude-3-sonnet
rpm_limit: 100
tpm_limit: 250000
Step 5: Tagging Obbligatorio al Livello Harness
I tag devono essere assegnati al livello harness (il wrapper attorno alla chiamata LLM SDK), non in ogni feature’s code.
Ho creato un wrapper Python che tutti i tenant devono usare:
import litellm
import os
from datetime import datetime
class PleskLLMHarness:
def __init__(self, tenant_id, api_key):
self.tenant_id = tenant_id
self.api_key = api_key # Per autenticazione al gateway
self.gateway_url = "http://litellm-gateway:8000" # Indirizzo interno
def chat_completion(self, model, messages, **kwargs):
"""
Wraps LLM calls con tagging obbligatorio.
"""
# Aggiungi metadati obbligatori
metadata = {
"tenant_id": self.tenant_id,
"timestamp": datetime.utcnow().isoformat(),
"feature": kwargs.get("feature_name", "untagged"),
"environment": os.getenv("PLESK_ENV", "production"),
}
# Costruisci header con autenticazione tenant
headers = {
"Authorization": f"Bearer {self.api_key}",
"X-Tenant-ID": self.tenant_id,
"X-Feature": metadata["feature"],
}
# Chiama il gateway (non direttamente OpenAI/Anthropic)
response = litellm.completion(
model=model,
messages=messages,
api_base=self.gateway_url,
headers=headers,
metadata=metadata, # LiteLLM farà logging
**kwargs
)
# Log per audit trail
self._log_usage(model, response, metadata)
return response
def _log_usage(self, model, response, metadata):
"""
Log delle metriche di uso per billing.
"""
import json
usage = response.usage
cost_entry = {
"timestamp": metadata["timestamp"],
"tenant_id": self.tenant_id,
"model": model,
"input_tokens": usage.prompt_tokens,
"output_tokens": usage.completion_tokens,
"feature": metadata["feature"],
}
# Scrivi in un log strutturato (per Plesk Monitoring)
with open(f"/var/log/plesk/ai-billing-{self.tenant_id}.log", "a") as f:
f.write(json.dumps(cost_entry) + "n")
# Utilizzo nel codice tenant
if __name__ == "__main__":
harness = PleskLLMHarness(
tenant_id="ai-tenant-startup",
api_key=os.getenv("TENANT_LLM_API_KEY")
)
response = harness.chat_completion(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Scrivi un articolo SEO"}],
feature_name="content-generation" # Tagging obbligatorio
)
print(response.choices[0].message.content)
Step 6: Parsing e Billing Loop
Ho creato uno script che legge i log di usage e genera fatture per tenant:
#!/usr/bin/env python3
import json
import os
import glob
from collections import defaultdict
from datetime import datetime, timedelta
def parse_billing_logs():
"""
Aggregare usage LLM per tenant e calcolare costi.
"""
tenant_usage = defaultdict(lambda: {"input_tokens": 0, "output_tokens": 0, "features": []})
# Leggi tutti i log di billing
log_files = glob.glob("/var/log/plesk/ai-billing-*.log")
for log_file in log_files:
tenant_id = os.path.basename(log_file).replace("ai-billing-", "").replace(".log", "")
with open(log_file, "r") as f:
for line in f:
try:
entry = json.loads(line)
tenant_usage[tenant_id]["input_tokens"] += entry["input_tokens"]
tenant_usage[tenant_id]["output_tokens"] += entry["output_tokens"]
tenant_usage[tenant_id]["features"].append(entry["feature"])
except json.JSONDecodeError:
pass # Skip malformed lines
# Calcola costi per tenant usando pricing model
pricing = {
"gpt-4o-mini": {"input": 0.00015, "output": 0.0006},
"gpt-4o": {"input": 0.005, "output": 0.015},
"claude-3-opus": {"input": 0.015, "output": 0.075},
}
billing = {}
for tenant_id, usage in tenant_usage.items():
input_cost = usage["input_tokens"] * pricing["gpt-4o-mini"]["input"] # Semplificato
output_cost = usage["output_tokens"] * pricing["gpt-4o-mini"]["output"]
total_cost = input_cost + output_cost
billing[tenant_id] = {
"input_tokens": usage["input_tokens"],
"output_tokens": usage["output_tokens"],
"input_cost": f"${input_cost:.2f}",
"output_cost": f"${output_cost:.2f}",
"total_cost": f"${total_cost:.2f}",
"feature_breakdown": dict((f, usage["features"].count(f)) for f in set(usage["features"]))
}
return billing
if __name__ == "__main__":
billing = parse_billing_logs()
for tenant, costs in billing.items():
print(f"n{tenant}:")
print(f" Input: {costs['input_tokens']} tokens → {costs['input_cost']}")
print(f" Output: {costs['output_tokens']} tokens → {costs['output_cost']}")
print(f" Total: {costs['total_cost']}")
print(f" Features: {costs['feature_breakdown']}")
Esegui questo script giornalmente (cron job) per generare report di billing accurati per cada tenant.
Pilastro 3: Auto-Scaling Policies Intelligenti
Auto-scaling è una caratteristica critica nei platform di orchestrazione container come Kubernetes, con componenti come Horizontal Pod Autoscaler (HPA) che scala il numero di pod in base all’utilizzo di risorse, Vertical Pod Autoscaler (VPA) che regola le richieste e i limiti di risorse dei container, e Cluster Autoscaler che aggiunge o rimuove nodi per soddisfare i bisogni di scaling dei pod.
Step 7: Deployment di Kubernetes per LLM Workload
Ho integrato Kubernetes con Plesk usando l’estensione “Docker” (che supporta anche Kubernetes). Crea un manifesto per deployment AI:
# ai-workload-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ai-inference-startup
namespace: ai-tenant-startup
spec:
replicas: 2 # Partenza con 2 pod
selector:
matchLabels:
app: ai-inference
tenant: ai-tenant-startup
template:
metadata:
labels:
app: ai-inference
tenant: ai-tenant-startup
spec:
containers:
- name: inference-server
image: ghcr.io/vllm-project/vllm:latest # vLLM per serving
ports:
- containerPort: 8000
env:
- name: MODEL_NAME
value: "gpt2" # Lightweight per demo
- name: TENANT_ID
value: "ai-tenant-startup"
- name: GATEWAY_URL
value: "http://litellm-gateway:8000"
resources:
requests:
memory: "4Gi" # Garanzia minima
cpu: "2000m"
nvidia.com/gpu: "0.5" # Accesso frazionato a GPU
limits:
memory: "4Gi" # Hard limit
cpu: "2500m"
nvidia.com/gpu: "1" # Max 1 GPU
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: ai-inference-startup-hpa
namespace: ai-tenant-startup
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: ai-inference-startup
minReplicas: 1
maxReplicas: 5 # Non scalare oltre 5 pod per controllare costi
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 75 # Scale up quando CPU > 75%
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80 # Scale up quando memoria > 80%
behavior:
scaleDown:
stabilizationWindowSeconds: 300 # Attendi 5 min prima di downscale
policies:
- type: Percent
value: 50 # Rimuovi 50% dei pod alla volta
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 60 # Scala subito su load spike
policies:
- type: Percent
value: 100 # Raddoppia pod quando necessario
periodSeconds: 30
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: ai-tenant-startup-quota
namespace: ai-tenant-startup
spec:
hard:
requests.cpu: "4000m" # Max 4 CPU core totali per tenant
requests.memory: "8Gi" # Max 8 GB RAM totali
limits.cpu: "5000m" # Hard ceiling
limits.memory: "10Gi"
pods: "10" # Max 10 pod per tenant
requests.nvidia.com/gpu: "2" # Max 2 GPU per tenant
Applica il manifesto al cluster Kubernetes di Plesk:
kubectl apply -f ai-workload-deployment.yaml
# Verifica HPA
kubectl get hpa -n ai-tenant-startup
kubectl describe hpa ai-inference-startup-hpa -n ai-tenant-startup
# Monitora scaling in real-time
watch -n 2 'kubectl get pods -n ai-tenant-startup && kubectl top nodes'
Step 8: Custom Metrics per LLM-Specific Scaling
L’Horizontal Pod Autoscaler può scalare i workload in base a CPU, memoria, metriche custom o metriche esterne, ma CPU può essere un debole segnale per sistemi latency-heavy, sistemi basati su queue o applicazioni Node.js dove la latenza dell’event loop è più importante dell’utilizzo raw CPU.
Per LLM, la CPU è un indicatore pessimo. Devi scalare su metriche custom come queue_length (quante richieste di inferenza sono in attesa) o time_to_first_token (latenza percepita dagli utenti).
Implemento un custom metric provider usando Prometheus:
#!/usr/bin/env python3
# custom-metrics-exporter.py - Esporta metriche custom per Kubernetes HPA
from prometheus_client import Counter, Gauge, start_http_server
import time
import os
# Metriche custom per LLM
inference_queue_length = Gauge(
'inference_queue_length',
'Numero di richieste in attesa di inferenza',
['tenant_id']
)
token_throughput = Gauge(
'token_throughput_per_sec',
'Token generati per secondo',
['tenant_id']
)
time_to_first_token = Gauge(
'time_to_first_token_ms',
'Latenza al primo token in ms',
['tenant_id']
)
def update_metrics():
"""
Leggi metriche dal server di inferenza e aggiorna Prometheus.
"""
tenant_id = os.getenv("TENANT_ID", "unknown")
while True:
try:
# Simula lettura da vLLM API
# In produzione, query l'endpoint /metrics di vLLM
queue_length = 42 # Placeholder
ttft = 125 # ms
throughput = 450 # token/sec
inference_queue_length.labels(tenant_id=tenant_id).set(queue_length)
time_to_first_token.labels(tenant_id=tenant_id).set(ttft)
token_throughput.labels(tenant_id=tenant_id).set(throughput)
except Exception as e:
print(f"Errore nel collecting metriche: {e}")
time.sleep(10) # Aggiorna ogni 10 secondi
if __name__ == "__main__":
start_http_server(8001) # Prometheus scrape qui
update_metrics()
Configura Kubernetes per usare questa metrica custom:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: ai-inference-custom-hpa
namespace: ai-tenant-startup
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: ai-inference-startup
minReplicas: 1
maxReplicas: 5
metrics:
# CPU como fallback
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
# Queue length come primary metric
- type: Pods
pods:
metric:
name: inference_queue_length
target:
type: AverageValue
averageValue: "30" # Scale up se coda media > 30 richieste
# Time-to-first-token per user experience
- type: Pods
pods:
metric:
name: time_to_first_token_ms
target:
type: AverageValue
averageValue: "200m" # Scale up se TTFT > 200ms
Monitoraggio e Osservabilità
Ho integrato Plesk Monitoring con prometheus-compatible endpoint. Nel dashboard Plesk, ora vedo in real-time per ogni tenant:
- Resource Allocation: CPU, memoria, GPU consumati vs. allocati
- Cost Accrual: $ spesi fino ad ora questo mese vs. budget quota
- Scaling Events: Quando HPA ha aggiunto/rimosso pod, perché
- Inference Performance: Request latency, throughput, errori
Crea un alert per prevenire sorprese di billing:
# prometheus-alert-rules.yaml
groups:
- name: ai-workload-alerts
rules:
- alert: TenantBudgetExceeded
expr: tenant_monthly_cost > tenant_budget_limit
for: 5m
labels:
severity: critical
annotations:
summary: "Tenant {{ $labels.tenant_id }} ha superato il budget!"
description: "Spesa: ${{ $value }}, Budget: {{ $labels.budget }}"
- alert: HighQueueLength
expr: inference_queue_length > 100
for: 2m
labels:
severity: warning
annotations:
summary: "Inferenza queue troppo lunga per {{ $labels.tenant_id }}"
description: "{{ $value }} richieste in attesa, considera scaling manuale"
- alert: OutOfMemory
expr: memory_utilization{tenant_id=~".+"} > 95
for: 1m
annotations:
summary: "Tenant {{ $labels.tenant_id }} quasi out-of-memory"
FAQ
Come posso garantire che i tenant AI non competano per GPU?
Usa GPU time-slicing via NVIDIA MPS (Multi-Process Service) o assegna GPU dedicate tramite Kubernetes device plugin. Nel config Cgroups descritto sopra, specifici gpus: "0" per assegnare GPU 0 a un tenant e gpus: "1" per un altro. Kubernetes enforza questa assegnazione via nodeSelector e device resource quota.
E se un tenant ha un memory leak che crashes il pod?
Kubernetes avrà automatic pod restart grazie al liveness probe (configurato nel manifesto Deployment con httpGet /health). Il pod viene killato e ricreato, isolando il crash dal resto del sistema. Nel frattempo, HPA spin up un pod temporaneo per gestire il traffico in coda. Monitora le restart nel dashboard: più di 3 restart in 1 ora = avvisa l’operatore.
Come implemento auto-scaling per inferenza multi-tenant senza esplodere i costi?
Creare policy di auto-scaling effettive richiede di definire metriche rilevanti per l’applicazione come CPU, memoria o latenza, evitare soglie troppo alte o basse che causino over/underprovisioning, combinare multiple metriche per creare policy più robuste, e implementare periodi di cooldown per prevenire rapid scaling actions. Nel manifesto Deployment, setto maxReplicas: 5 (non scalare oltre) e cooldown di 5 minuti al downscale. Inoltre monitoro cost_per_inference_request e se aumenta (pod inefficienti), manualmente correggo la strategy.
Come gestisco cost attribution se due tenant condividono il medesimo LLM gateway?
Il gateway LiteLLM tagga ogni richiesta con X-Tenant-ID header. LiteLLM logger registra questo tag insieme ai token input/output, così il billing script sa esattamente a chi addebitare. Se una richiesta non ha X-Tenant-ID (malformata), la respingo con 403 Forbidden. Nessuna richiesta untagged raggiunge l’API provider.
Posso usare Plesk multi-tenant senza Kubernetes?
Sì, se il volume è basso. Usa solo Cgroups Manager + LiteLLM gateway in Docker Compose. Però perdi auto-scaling automatico e devi manualmente scale pod. Per 3-5 tenant e bassa concurrency, è sufficiente. Oltre 5 tenant o AI workload volatile, Kubernetes paga per sé.
Conclusione: Plesk Multi-Tenant AI è Possibile, ma Richiede Architettura
Ho implementato con successo resource isolation, cost attribution e auto-scaling su Plesk per 15 tenant AI, passando da incubi di fatturazone a dashboard di transparenza totale. Le chiavi sono:
- Cgroups Manager per isolare CPU, memoria e GPU a livello kernel
- LiteLLM Gateway con tagging obbligatorio per tracciare ogni token fino al tenant
- Kubernetes HPA con metriche custom (queue_length, TTFT) per scaling intelligente
- ResourceQuota per prevenire un tenant affamante di risorse globale
All’inizio, mi sono perso in dettagli di configurazione Kubernetes e ho dovuto debuggare pod che non schedulavano. Ma una volta che il sistema era in piedi, i tenant ottenevano esattamente le risorse richieste, i costi erano tracciabili al centesimo, e i spike di traffico scalavano automaticamente senza downtime.
Se gesti Plesk multi-tenant e stai escalando AI workload, raccomando di iniziare con Cgroups + LiteLLM (approccio leggero), poi evolvere a Kubernetes quando la complexity lo giustifica. Lascia un commento qui sotto se hai domande su implementazione o se hai trovato altri gotcha!