{"id":3260,"date":"2026-08-13T18:54:50","date_gmt":"2026-08-13T16:54:50","guid":{"rendered":"https:\/\/darioiannascoli.it\/blog\/plesk-multi-tenant-ai-workload-management-resource-isolation-cost-attribution-autoscaling\/"},"modified":"2026-08-13T18:54:50","modified_gmt":"2026-08-13T16:54:50","slug":"plesk-multi-tenant-ai-workload-management-resource-isolation-cost-attribution-autoscaling","status":"publish","type":"post","link":"https:\/\/darioiannascoli.it\/blog\/plesk-multi-tenant-ai-workload-management-resource-isolation-cost-attribution-autoscaling\/","title":{"rendered":"Come Implementare Plesk Multi-Tenant AI Workload Management: La Mia Procedura Resource Isolation, Cost Attribution per LLM Processing e Auto-Scaling Policies Agosto 2026"},"content":{"rendered":"<p>Negli ultimi mesi, la gestione dei workload AI in ambienti multi-tenant \u00e8 diventata una priorit\u00e0 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.<\/p>\n<p>Il problema? <strong>La maggior parte dei provider di hosting tradizionali non sa come separare veramente i workload AI<\/strong>. Quando un tenant avvia un&#8217;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&#8217;intero server. Era un incubo di fatturazone.<\/p>\n<p>In questo articolo, vi mostro come ho risolto questi problemi utilizzando le capacit\u00e0 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.<\/p>\n<h2>La Sfida: Perch\u00e9 Resource Isolation in Plesk Multi-Tenant Non \u00e8 Banale<\/h2>\n<p><cite>Plesk supporta gestione account multi-tenant con allocazioni risorse individuali e livelli di permessi<\/cite>, ma implementare questo per workload AI richiede molti pi\u00f9 strati.<\/p>\n<p>Nella mia infrastruttura, avevo 15 tenant su un singolo server. Una startup usava LLM per content generation. Un&#8217;agenzia marketing training custom models. Un e-commerce eseguiva RAG per suggerimenti prodotto. Senza isolamento:<\/p>\n<ul>\n<li>Un&#8217;inferenza crazy-verbose di una startup consumava il 90% della CPU, rallentando i WAR query dell&#8217;agenzia<\/li>\n<li>I token API di un tenant spillavano nella billing di un altro (bug in tagging, ma comunque catastrofico)<\/li>\n<li>Nessun meccanismo di auto-scaling: quando il carico aumentava, o aumentavo manualmente le risorse (costoso) o tutto crashava<\/li>\n<li>Memory leaks negli agent agentic potevano OOM-killare l&#8217;intero server<\/li>\n<\/ul>\n<p>La soluzione richiede <strong>tre pilastri<\/strong>: isolation a livello kernel, cost attribution granulare e scaling policy intelligenti. Ho integrato <em>Cgroups Manager<\/em> di Plesk, un sistema di proxy LLM con tagging obbligatorio e Kubernetes per container orchestration avanzato.<\/p>\n<h2>Pilastro 1: Resource Isolation con Cgroups su Plesk<\/h2>\n<p>Plesk offre un&#8217;estensione &#8220;Cgroups Manager&#8221; che sfrutta i control groups di Linux per isolare CPU, memoria e I\/O per singolo subscription. <cite>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<\/cite>.<\/p>\n<p>Ho iniziato creando tre service plan: <strong>basic<\/strong>, <strong>standard<\/strong> e <strong>premium<\/strong> per LLM processing.<\/p>\n<h3>Step 1: Configurare i Service Plan con Limiti di Risorse<\/h3>\n<p>Nel Plesk Panel, navigo su <em>Tools &amp; Settings \u2192 Service Plans<\/em> e creo un nuovo piano per AI workload:<\/p>\n<pre><code class=\"language-plaintext\">Service Plan: ai-workload-standard\n\nRisorse Allocate:\n- CPU cores: 2 (su server 16-core, ca. 12.5%)\n- Memory: 4 GB\n- Disk I\/O: 50 MB\/s (bandwidth)\n- Network bandwidth: 500 Mbps (limitare spike)\n- Concurrent processes: 100\n\nLimiti per Tenant:\n- Max API calls\/min: 1000 (per LLM gateway)\n- Max token\/ora: 100M (se usi LLM via API)\n<\/code><\/pre>\n<p>La chiave \u00e8 <strong>non usare risorse unlimited<\/strong>. Ogni tenant deve avere limiti hard definiti. All&#8217;inizio non funzionava perch\u00e9 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.<\/p>\n<h3>Step 2: Abilitare Cgroups Manager<\/h3>\n<p>Nel Plesk Panel:<\/p>\n<ol>\n<li>Vai a <em>Tools &amp; Settings \u2192 Extensions \u2192 Browse For Extensions<\/em><\/li>\n<li>Cerca <strong>&#8220;Cgroups Manager&#8221;<\/strong><\/li>\n<li>Installa e abilita l&#8217;integrazione con Plesk Monitoring<\/li>\n<\/ol>\n<p>Una volta abilitato, Plesk inizia a tracciare CPU, memoria e I\/O per ogni subscription in tempo reale. Nel dashboard vedrai grafici come questo:<\/p>\n<pre><code class=\"language-plaintext\">Subscription: ai-tenant-startup\nCPU Usage: 2.1 \/ 2.0 cores (110% - THROTTLED)\nMemory: 3.8 \/ 4.0 GB\nI\/O Wait: 15% (il container sta leggendo embeddings da disco)\n<\/code><\/pre>\n<p>Se il CPU usage supera il limite, il kernel Linux throttle automaticamente il processo, prevenendo la fame di risorse per altri tenant.<\/p>\n<h3>Step 3: Configurare Cpuset Affinity per GPU Isolation (Opzionale ma Importante)<\/h3>\n<p>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.<\/p>\n<p>In <code>\/etc\/plesk\/cgroups-manager\/config.yaml<\/code>:<\/p>\n<pre><code class=\"language-yaml\">subscriptions:\n  ai-tenant-startup:\n    cpu_cores: \"0-3\"  # Assegna core 0-3 della CPU\n    memory_limit: 4G\n    gpus: \"0\"  # NVIDIA GPU 0 per questo tenant\n    cpu_shares: 256  # Quota fair-share se sovrascritta\n    \n  ai-tenant-agency:\n    cpu_cores: \"4-7\"\n    memory_limit: 8G\n    gpus: \"1\"  # NVIDIA GPU 1\n    cpu_shares: 512  # Priorit\u00e0 pi\u00f9 alta\n\n  ai-tenant-ecommerce:\n    cpu_cores: \"8-11\"\n    memory_limit: 4G\n    gpus: \"0,1\"  # Accesso a entrambe le GPU in time-share\n    cpu_shares: 256\n<\/code><\/pre>\n<p>Applica la configurazione:<\/p>\n<pre><code class=\"language-bash\">sudo systemctl restart plesk-cgroups-manager\n# Verifica che i limiti siano applicati\ncgroup-get-limits.sh ai-tenant-startup\n<\/code><\/pre>\n<h2>Pilastro 2: Cost Attribution per LLM Processing<\/h2>\n<p><cite>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&#8217;attribuzione \u00e8 persa<\/cite>.<\/p>\n<p>Qui \u00e8 dove la maggior parte dei provider fallisce. Ho implementato un LLM gateway che intercetta tutte le chiamate e aggiunge obbligatoriamente metadati di tenant.<\/p>\n<h3>Step 4: Deployment di un LLM Proxy con Tagging Obbligatorio<\/h3>\n<p><cite>LiteLLM \u00e8 un proxy LLM open source basato su Python che fornisce un&#8217;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<\/cite>.<\/p>\n<p>Ho deployato LiteLLM come contenitore Docker all&#8217;interno di Plesk, con una configurazione ristretta:<\/p>\n<pre><code class=\"language-bash\">docker run -d   \n  --name litellm-gateway   \n  --cpus=\"1\"   \n  --memory=\"2g\"   \n  -e LITELLM_MASTER_KEY=abc-def-ghi-secret   \n  -e PROXY_BUDGET_CONFIG=\/etc\/litellm\/budgets.yaml   \n  -p 8000:8000   \n  ghcr.io\/berriai\/litellm:latest\n<\/code><\/pre>\n<p>Nel file <code>budgets.yaml<\/code>, definisco quota per tenant:<\/p>\n<pre><code class=\"language-yaml\">users:\n  ai-tenant-startup:\n    budget: 100.0  # $100\/mese\n    models:\n      - gpt-4o-mini  # solo modelli cheap\n      - claude-3-haiku\n    rpm_limit: 50  # max 50 richieste\/min\n    tpm_limit: 100000  # max 100k token\/min\n    \n  ai-tenant-agency:\n    budget: 500.0  # $500\/mese\n    models:\n      - gpt-4o\n      - claude-3-opus\n    rpm_limit: 200\n    tpm_limit: 500000\n    \n  ai-tenant-ecommerce:\n    budget: 250.0\n    models:\n      - gpt-4o\n      - claude-3-sonnet\n    rpm_limit: 100\n    tpm_limit: 250000\n<\/code><\/pre>\n<h3>Step 5: Tagging Obbligatorio al Livello Harness<\/h3>\n<p><cite>I tag devono essere assegnati al livello harness (il wrapper attorno alla chiamata LLM SDK), non in ogni feature&#8217;s code<\/cite>.<\/p>\n<p>Ho creato un wrapper Python che tutti i tenant devono usare:<\/p>\n<pre><code class=\"language-python\">import litellm\nimport os\nfrom datetime import datetime\n\nclass PleskLLMHarness:\n    def __init__(self, tenant_id, api_key):\n        self.tenant_id = tenant_id\n        self.api_key = api_key  # Per autenticazione al gateway\n        self.gateway_url = \"http:\/\/litellm-gateway:8000\"  # Indirizzo interno\n        \n    def chat_completion(self, model, messages, **kwargs):\n        \"\"\"\n        Wraps LLM calls con tagging obbligatorio.\n        \"\"\"\n        # Aggiungi metadati obbligatori\n        metadata = {\n            \"tenant_id\": self.tenant_id,\n            \"timestamp\": datetime.utcnow().isoformat(),\n            \"feature\": kwargs.get(\"feature_name\", \"untagged\"),\n            \"environment\": os.getenv(\"PLESK_ENV\", \"production\"),\n        }\n        \n        # Costruisci header con autenticazione tenant\n        headers = {\n            \"Authorization\": f\"Bearer {self.api_key}\",\n            \"X-Tenant-ID\": self.tenant_id,\n            \"X-Feature\": metadata[\"feature\"],\n        }\n        \n        # Chiama il gateway (non direttamente OpenAI\/Anthropic)\n        response = litellm.completion(\n            model=model,\n            messages=messages,\n            api_base=self.gateway_url,\n            headers=headers,\n            metadata=metadata,  # LiteLLM far\u00e0 logging\n            **kwargs\n        )\n        \n        # Log per audit trail\n        self._log_usage(model, response, metadata)\n        \n        return response\n    \n    def _log_usage(self, model, response, metadata):\n        \"\"\"\n        Log delle metriche di uso per billing.\n        \"\"\"\n        import json\n        usage = response.usage\n        cost_entry = {\n            \"timestamp\": metadata[\"timestamp\"],\n            \"tenant_id\": self.tenant_id,\n            \"model\": model,\n            \"input_tokens\": usage.prompt_tokens,\n            \"output_tokens\": usage.completion_tokens,\n            \"feature\": metadata[\"feature\"],\n        }\n        \n        # Scrivi in un log strutturato (per Plesk Monitoring)\n        with open(f\"\/var\/log\/plesk\/ai-billing-{self.tenant_id}.log\", \"a\") as f:\n            f.write(json.dumps(cost_entry) + \"n\")\n\n# Utilizzo nel codice tenant\nif __name__ == \"__main__\":\n    harness = PleskLLMHarness(\n        tenant_id=\"ai-tenant-startup\",\n        api_key=os.getenv(\"TENANT_LLM_API_KEY\")\n    )\n    \n    response = harness.chat_completion(\n        model=\"gpt-4o-mini\",\n        messages=[{\"role\": \"user\", \"content\": \"Scrivi un articolo SEO\"}],\n        feature_name=\"content-generation\"  # Tagging obbligatorio\n    )\n    \n    print(response.choices[0].message.content)\n<\/code><\/pre>\n<h3>Step 6: Parsing e Billing Loop<\/h3>\n<p>Ho creato uno script che legge i log di usage e genera fatture per tenant:<\/p>\n<pre><code class=\"language-python\">#!\/usr\/bin\/env python3\nimport json\nimport os\nimport glob\nfrom collections import defaultdict\nfrom datetime import datetime, timedelta\n\ndef parse_billing_logs():\n    \"\"\"\n    Aggregare usage LLM per tenant e calcolare costi.\n    \"\"\"\n    tenant_usage = defaultdict(lambda: {\"input_tokens\": 0, \"output_tokens\": 0, \"features\": []})\n    \n    # Leggi tutti i log di billing\n    log_files = glob.glob(\"\/var\/log\/plesk\/ai-billing-*.log\")\n    \n    for log_file in log_files:\n        tenant_id = os.path.basename(log_file).replace(\"ai-billing-\", \"\").replace(\".log\", \"\")\n        \n        with open(log_file, \"r\") as f:\n            for line in f:\n                try:\n                    entry = json.loads(line)\n                    tenant_usage[tenant_id][\"input_tokens\"] += entry[\"input_tokens\"]\n                    tenant_usage[tenant_id][\"output_tokens\"] += entry[\"output_tokens\"]\n                    tenant_usage[tenant_id][\"features\"].append(entry[\"feature\"])\n                except json.JSONDecodeError:\n                    pass  # Skip malformed lines\n    \n    # Calcola costi per tenant usando pricing model\n    pricing = {\n        \"gpt-4o-mini\": {\"input\": 0.00015, \"output\": 0.0006},\n        \"gpt-4o\": {\"input\": 0.005, \"output\": 0.015},\n        \"claude-3-opus\": {\"input\": 0.015, \"output\": 0.075},\n    }\n    \n    billing = {}\n    for tenant_id, usage in tenant_usage.items():\n        input_cost = usage[\"input_tokens\"] * pricing[\"gpt-4o-mini\"][\"input\"]  # Semplificato\n        output_cost = usage[\"output_tokens\"] * pricing[\"gpt-4o-mini\"][\"output\"]\n        total_cost = input_cost + output_cost\n        \n        billing[tenant_id] = {\n            \"input_tokens\": usage[\"input_tokens\"],\n            \"output_tokens\": usage[\"output_tokens\"],\n            \"input_cost\": f\"${input_cost:.2f}\",\n            \"output_cost\": f\"${output_cost:.2f}\",\n            \"total_cost\": f\"${total_cost:.2f}\",\n            \"feature_breakdown\": dict((f, usage[\"features\"].count(f)) for f in set(usage[\"features\"]))\n        }\n    \n    return billing\n\nif __name__ == \"__main__\":\n    billing = parse_billing_logs()\n    for tenant, costs in billing.items():\n        print(f\"n{tenant}:\")\n        print(f\"  Input: {costs['input_tokens']} tokens \u2192 {costs['input_cost']}\")\n        print(f\"  Output: {costs['output_tokens']} tokens \u2192 {costs['output_cost']}\")\n        print(f\"  Total: {costs['total_cost']}\")\n        print(f\"  Features: {costs['feature_breakdown']}\")\n<\/code><\/pre>\n<p>Esegui questo script giornalmente (cron job) per generare report di billing accurati per cada tenant.<\/p>\n<h2>Pilastro 3: Auto-Scaling Policies Intelligenti<\/h2>\n<p><cite>Auto-scaling \u00e8 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&#8217;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<\/cite>.<\/p>\n<h3>Step 7: Deployment di Kubernetes per LLM Workload<\/h3>\n<p>Ho integrato Kubernetes con Plesk usando l&#8217;estensione &#8220;Docker&#8221; (che supporta anche Kubernetes). Crea un manifesto per deployment AI:<\/p>\n<pre><code class=\"language-yaml\"># ai-workload-deployment.yaml\napiVersion: apps\/v1\nkind: Deployment\nmetadata:\n  name: ai-inference-startup\n  namespace: ai-tenant-startup\nspec:\n  replicas: 2  # Partenza con 2 pod\n  selector:\n    matchLabels:\n      app: ai-inference\n      tenant: ai-tenant-startup\n  template:\n    metadata:\n      labels:\n        app: ai-inference\n        tenant: ai-tenant-startup\n    spec:\n      containers:\n      - name: inference-server\n        image: ghcr.io\/vllm-project\/vllm:latest  # vLLM per serving\n        ports:\n        - containerPort: 8000\n        env:\n        - name: MODEL_NAME\n          value: \"gpt2\"  # Lightweight per demo\n        - name: TENANT_ID\n          value: \"ai-tenant-startup\"\n        - name: GATEWAY_URL\n          value: \"http:\/\/litellm-gateway:8000\"\n        resources:\n          requests:\n            memory: \"4Gi\"  # Garanzia minima\n            cpu: \"2000m\"\n            nvidia.com\/gpu: \"0.5\"  # Accesso frazionato a GPU\n          limits:\n            memory: \"4Gi\"  # Hard limit\n            cpu: \"2500m\"\n            nvidia.com\/gpu: \"1\"  # Max 1 GPU\n        livenessProbe:\n          httpGet:\n            path: \/health\n            port: 8000\n          initialDelaySeconds: 30\n          periodSeconds: 10\n---\napiVersion: autoscaling\/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: ai-inference-startup-hpa\n  namespace: ai-tenant-startup\nspec:\n  scaleTargetRef:\n    apiVersion: apps\/v1\n    kind: Deployment\n    name: ai-inference-startup\n  minReplicas: 1\n  maxReplicas: 5  # Non scalare oltre 5 pod per controllare costi\n  metrics:\n  - type: Resource\n    resource:\n      name: cpu\n      target:\n        type: Utilization\n        averageUtilization: 75  # Scale up quando CPU &gt; 75%\n  - type: Resource\n    resource:\n      name: memory\n      target:\n        type: Utilization\n        averageUtilization: 80  # Scale up quando memoria &gt; 80%\n  behavior:\n    scaleDown:\n      stabilizationWindowSeconds: 300  # Attendi 5 min prima di downscale\n      policies:\n      - type: Percent\n        value: 50  # Rimuovi 50% dei pod alla volta\n        periodSeconds: 60\n    scaleUp:\n      stabilizationWindowSeconds: 60  # Scala subito su load spike\n      policies:\n      - type: Percent\n        value: 100  # Raddoppia pod quando necessario\n        periodSeconds: 30\n---\napiVersion: v1\nkind: ResourceQuota\nmetadata:\n  name: ai-tenant-startup-quota\n  namespace: ai-tenant-startup\nspec:\n  hard:\n    requests.cpu: \"4000m\"  # Max 4 CPU core totali per tenant\n    requests.memory: \"8Gi\"  # Max 8 GB RAM totali\n    limits.cpu: \"5000m\"  # Hard ceiling\n    limits.memory: \"10Gi\"\n    pods: \"10\"  # Max 10 pod per tenant\n    requests.nvidia.com\/gpu: \"2\"  # Max 2 GPU per tenant\n<\/code><\/pre>\n<p>Applica il manifesto al cluster Kubernetes di Plesk:<\/p>\n<pre><code class=\"language-bash\">kubectl apply -f ai-workload-deployment.yaml\n\n# Verifica HPA\nkubectl get hpa -n ai-tenant-startup\nkubectl describe hpa ai-inference-startup-hpa -n ai-tenant-startup\n\n# Monitora scaling in real-time\nwatch -n 2 'kubectl get pods -n ai-tenant-startup &amp;&amp; kubectl top nodes'\n<\/code><\/pre>\n<h3>Step 8: Custom Metrics per LLM-Specific Scaling<\/h3>\n<p><cite>L&#8217;Horizontal Pod Autoscaler pu\u00f2 scalare i workload in base a CPU, memoria, metriche custom o metriche esterne, ma CPU pu\u00f2 essere un debole segnale per sistemi latency-heavy, sistemi basati su queue o applicazioni Node.js dove la latenza dell&#8217;event loop \u00e8 pi\u00f9 importante dell&#8217;utilizzo raw CPU<\/cite>.<\/p>\n<p>Per LLM, la CPU \u00e8 un indicatore pessimo. Devi scalare su metriche custom come <strong>queue_length<\/strong> (quante richieste di inferenza sono in attesa) o <strong>time_to_first_token<\/strong> (latenza percepita dagli utenti).<\/p>\n<p>Implemento un custom metric provider usando Prometheus:<\/p>\n<pre><code class=\"language-python\">#!\/usr\/bin\/env python3\n# custom-metrics-exporter.py - Esporta metriche custom per Kubernetes HPA\n\nfrom prometheus_client import Counter, Gauge, start_http_server\nimport time\nimport os\n\n# Metriche custom per LLM\ninference_queue_length = Gauge(\n    'inference_queue_length',\n    'Numero di richieste in attesa di inferenza',\n    ['tenant_id']\n)\n\ntoken_throughput = Gauge(\n    'token_throughput_per_sec',\n    'Token generati per secondo',\n    ['tenant_id']\n)\n\ntime_to_first_token = Gauge(\n    'time_to_first_token_ms',\n    'Latenza al primo token in ms',\n    ['tenant_id']\n)\n\ndef update_metrics():\n    \"\"\"\n    Leggi metriche dal server di inferenza e aggiorna Prometheus.\n    \"\"\"\n    tenant_id = os.getenv(\"TENANT_ID\", \"unknown\")\n    \n    while True:\n        try:\n            # Simula lettura da vLLM API\n            # In produzione, query l'endpoint \/metrics di vLLM\n            queue_length = 42  # Placeholder\n            ttft = 125  # ms\n            throughput = 450  # token\/sec\n            \n            inference_queue_length.labels(tenant_id=tenant_id).set(queue_length)\n            time_to_first_token.labels(tenant_id=tenant_id).set(ttft)\n            token_throughput.labels(tenant_id=tenant_id).set(throughput)\n            \n        except Exception as e:\n            print(f\"Errore nel collecting metriche: {e}\")\n        \n        time.sleep(10)  # Aggiorna ogni 10 secondi\n\nif __name__ == \"__main__\":\n    start_http_server(8001)  # Prometheus scrape qui\n    update_metrics()\n<\/code><\/pre>\n<p>Configura Kubernetes per usare questa metrica custom:<\/p>\n<pre><code class=\"language-yaml\">apiVersion: autoscaling\/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: ai-inference-custom-hpa\n  namespace: ai-tenant-startup\nspec:\n  scaleTargetRef:\n    apiVersion: apps\/v1\n    kind: Deployment\n    name: ai-inference-startup\n  minReplicas: 1\n  maxReplicas: 5\n  metrics:\n  # CPU como fallback\n  - type: Resource\n    resource:\n      name: cpu\n      target:\n        type: Utilization\n        averageUtilization: 70\n  # Queue length come primary metric\n  - type: Pods\n    pods:\n      metric:\n        name: inference_queue_length\n      target:\n        type: AverageValue\n        averageValue: \"30\"  # Scale up se coda media &gt; 30 richieste\n  # Time-to-first-token per user experience\n  - type: Pods\n    pods:\n      metric:\n        name: time_to_first_token_ms\n      target:\n        type: AverageValue\n        averageValue: \"200m\"  # Scale up se TTFT &gt; 200ms\n<\/code><\/pre>\n<h2>Monitoraggio e Osservabilit\u00e0<\/h2>\n<p>Ho integrato Plesk Monitoring con prometheus-compatible endpoint. Nel dashboard Plesk, ora vedo in real-time per ogni tenant:<\/p>\n<ul>\n<li><strong>Resource Allocation<\/strong>: CPU, memoria, GPU consumati vs. allocati<\/li>\n<li><strong>Cost Accrual<\/strong>: $ spesi fino ad ora questo mese vs. budget quota<\/li>\n<li><strong>Scaling Events<\/strong>: Quando HPA ha aggiunto\/rimosso pod, perch\u00e9<\/li>\n<li><strong>Inference Performance<\/strong>: Request latency, throughput, errori<\/li>\n<\/ul>\n<p>Crea un alert per prevenire sorprese di billing:<\/p>\n<pre><code class=\"language-yaml\"># prometheus-alert-rules.yaml\ngroups:\n- name: ai-workload-alerts\n  rules:\n  - alert: TenantBudgetExceeded\n    expr: tenant_monthly_cost &gt; tenant_budget_limit\n    for: 5m\n    labels:\n      severity: critical\n    annotations:\n      summary: \"Tenant {{ $labels.tenant_id }} ha superato il budget!\"\n      description: \"Spesa: ${{ $value }}, Budget: {{ $labels.budget }}\"\n  \n  - alert: HighQueueLength\n    expr: inference_queue_length &gt; 100\n    for: 2m\n    labels:\n      severity: warning\n    annotations:\n      summary: \"Inferenza queue troppo lunga per {{ $labels.tenant_id }}\"\n      description: \"{{ $value }} richieste in attesa, considera scaling manuale\"\n  \n  - alert: OutOfMemory\n    expr: memory_utilization{tenant_id=~\".+\"} &gt; 95\n    for: 1m\n    annotations:\n      summary: \"Tenant {{ $labels.tenant_id }} quasi out-of-memory\"\n<\/code><\/pre>\n<h2>FAQ<\/h2>\n<h3>Come posso garantire che i tenant AI non competano per GPU?<\/h3>\n<p>Usa GPU time-slicing via NVIDIA MPS (Multi-Process Service) o assegna GPU dedicate tramite Kubernetes device plugin. Nel config Cgroups descritto sopra, specifici <code>gpus: \"0\"<\/code> per assegnare GPU 0 a un tenant e <code>gpus: \"1\"<\/code> per un altro. Kubernetes enforza questa assegnazione via nodeSelector e device resource quota.<\/p>\n<h3>E se un tenant ha un memory leak che crashes il pod?<\/h3>\n<p>Kubernetes avr\u00e0 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\u00f9 di 3 restart in 1 ora = avvisa l&#8217;operatore.<\/p>\n<h3>Come implemento auto-scaling per inferenza multi-tenant senza esplodere i costi?<\/h3>\n<p><cite>Creare policy di auto-scaling effettive richiede di definire metriche rilevanti per l&#8217;applicazione come CPU, memoria o latenza, evitare soglie troppo alte o basse che causino over\/underprovisioning, combinare multiple metriche per creare policy pi\u00f9 robuste, e implementare periodi di cooldown per prevenire rapid scaling actions<\/cite>. 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.<\/p>\n<h3>Come gestisco cost attribution se due tenant condividono il medesimo LLM gateway?<\/h3>\n<p>Il gateway LiteLLM tagga ogni richiesta con X-Tenant-ID header. LiteLLM logger registra questo tag insieme ai token input\/output, cos\u00ec 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&#8217;API provider.<\/p>\n<h3>Posso usare Plesk multi-tenant senza Kubernetes?<\/h3>\n<p>S\u00ec, se il volume \u00e8 basso. Usa solo Cgroups Manager + LiteLLM gateway in Docker Compose. Per\u00f2 perdi auto-scaling automatico e devi manualmente scale pod. Per 3-5 tenant e bassa concurrency, \u00e8 sufficiente. Oltre 5 tenant o AI workload volatile, Kubernetes paga per s\u00e9.<\/p>\n<h2>Conclusione: Plesk Multi-Tenant AI \u00e8 Possibile, ma Richiede Architettura<\/h2>\n<p>Ho implementato con successo resource isolation, cost attribution e auto-scaling su Plesk per <strong>15 tenant AI<\/strong>, passando da incubi di fatturazone a dashboard di transparenza totale. Le chiavi sono:<\/p>\n<ul>\n<li><strong>Cgroups Manager<\/strong> per isolare CPU, memoria e GPU a livello kernel<\/li>\n<li><strong>LiteLLM Gateway<\/strong> con tagging obbligatorio per tracciare ogni token fino al tenant<\/li>\n<li><strong>Kubernetes HPA<\/strong> con metriche custom (queue_length, TTFT) per scaling intelligente<\/li>\n<li><strong>ResourceQuota<\/strong> per prevenire un tenant affamante di risorse globale<\/li>\n<\/ul>\n<p>All&#8217;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.<\/p>\n<p>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!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Come implementare resource isolation robusto, cost attribution granulare e auto-scaling intelligente su Plesk per multi-tenant AI workload: guida pratica con Cgroups, LiteLLM gateway e Kubernetes HPA.<\/p>\n","protected":false},"author":1,"featured_media":3261,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"_seopress_robots_primary_cat":"","_seopress_titles_title":"Plesk Multi-Tenant AI Workload: Resource Isolation e Cost Attribution | Agosto 2026","_seopress_titles_desc":"Scopri come gestire LLM processing multi-tenant su Plesk con Cgroups isolation, cost attribution per tenant e Kubernetes auto-scaling. Guida step-by-step con codice reale.","_seopress_robots_index":"","footnotes":""},"categories":[4],"tags":[1093,733,849,672,916,116],"class_list":["post-3260","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-plesk","tag-ai-workload","tag-cost-attribution","tag-kubernetes","tag-llm-inference","tag-multi-tenant","tag-plesk"],"_links":{"self":[{"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/posts\/3260","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/comments?post=3260"}],"version-history":[{"count":0,"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/posts\/3260\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/media\/3261"}],"wp:attachment":[{"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/media?parent=3260"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/categories?post=3260"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/darioiannascoli.it\/blog\/wp-json\/wp\/v2\/tags?post=3260"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}