- Published on
Ollama Load-Balancer ohne Kubernetes einrichten
- Authors

- Name
- Phillip Pham
- @ddppham
Mehrere Ollama-Instanzen hinter einem Endpoint betreiben
TL;DR
Ollama verarbeitet pro geladenem Modell standardmäßig nur eine Anfrage gleichzeitig — der Rest wandert in eine Warteschlange. Die Lösung ohne Kubernetes: eine Ollama-Instanz pro GPU über systemd, davor nginx mit least_conn und proxy_read_timeout 900s. Aufwand: ein halber Vormittag, wenn systemd und nginx kein Neuland sind. Keine zusätzliche Lizenz.
Warum ab fünf Nutzern die Warteschlange spürbar wird
Der Default in Ollama ist OLLAMA_NUM_PARALLEL=1. Das steht so im Quellcode (envconfig/config.go, Stand Juli 2026), und es bedeutet genau das, was da steht: Ein geladenes Modell bearbeitet eine Anfrage. Anfrage zwei wartet, bis Anfrage eins ihr letztes Token ausgespuckt hat.
Bei einer einzelnen Person am Chat-Fenster fällt das nie auf. Bei fünf Leuten in der Fachabteilung, die morgens gleichzeitig ihre Protokolle zusammenfassen lassen, schon: Wenn eine Antwort 20 Sekunden braucht, wartet der Fünfte in der Schlange 80 Sekunden auf sein erstes Token. Nichts stürzt ab, nichts loggt einen Fehler — es fühlt sich nur kaputt an.
Zwei Stellschrauben gibt es, bevor man überhaupt an einen Load-Balancer denkt:
| Variable | Default | Was sie tut | Wo die Grenze liegt |
|---|---|---|---|
OLLAMA_NUM_PARALLEL | 1 | Parallele Anfragen pro geladenem Modell | Multipliziert den Kontext-Speicher |
OLLAMA_MAX_LOADED_MODELS | 0 (= automatisch, 3× GPU-Anzahl) | Wie viele Modelle gleichzeitig im VRAM liegen | Jedes Modell belegt seinen vollen VRAM |
OLLAMA_MAX_QUEUE | 512 | Länge der Warteschlange | Darüber: HTTP 503 |
Alle drei Defaults stehen so in der offiziellen Ollama-FAQ und lassen sich im Quellcode gegenprüfen. Die Grenze von OLLAMA_NUM_PARALLEL wird dabei gern überlesen: Der Speicherbedarf skaliert mit OLLAMA_NUM_PARALLEL mal Kontextlänge. Ein Modell mit 2K Kontext und vier parallelen Slots belegt also 8K Kontext im VRAM. Wer auf einer 24-GB-Karte ein 14B-Modell mit 32K Kontext fährt und dann OLLAMA_NUM_PARALLEL=4 setzt, bekommt keinen vierfachen Durchsatz, sondern entweder eine Out-of-Memory-Meldung oder ein Modell, das teilweise in den System-RAM ausgelagert wird.
Der zweite Fall ist der unangenehmere, weil nichts scheitert: Es läuft weiter, nur um eine Größenordnung langsamer, und niemand sieht die Ursache. ollama ps beantwortet das in einer Zeile — die Spalte PROCESSOR zeigt, ob ein Modell zu 100 % auf der GPU liegt oder sich CPU und GPU teilt. Wenn dort etwas anderes als 100% GPU steht, ist jede Durchsatzmessung wertlos.
Die ehrliche Reihenfolge ist deshalb: Erst OLLAMA_NUM_PARALLEL vorsichtig auf 2 oder 4 heben und im nvidia-smi prüfen, ob der VRAM noch reicht. Bleibt es zu langsam, kommen mehrere Instanzen dran. Wer die VRAM-Grenzen seiner Karte nicht im Kopf hat, findet die Zahlen im GPU-Vergleich für LLM-Inferenz.
Setup 1: Eine Ollama-Instanz pro GPU
Das Ziel: Auf einem Host mit zwei GPUs laufen zwei getrennte Ollama-Prozesse, jeder sieht genau eine Karte, jeder hat seinen eigenen Port. Kein Sharding, kein geteilter Speicher — zwei unabhängige Server, die zufällig im selben Gehäuse stecken.
Zuerst den Standard-Dienst stoppen, den das Installationsskript anlegt:
# Standard-Instanz auf Port 11434 abschalten, sonst belegt sie beide GPUs
sudo systemctl disable --now ollama
Dann eine systemd-Template-Unit. Das @ ist der Trick: Eine Datei bedient beliebig viele Instanzen.
# /etc/systemd/system/ollama@.service
[Unit]
Description=Ollama Instanz %i
After=network-online.target
Wants=network-online.target
[Service]
Type=exec
User=ollama
Group=ollama
ExecStart=/usr/local/bin/ollama serve
# Pro Instanz eine eigene Env-Datei, benannt nach der Instanz-Nummer
EnvironmentFile=/etc/ollama/instanz-%i.env
Restart=always
RestartSec=3
# Ollama darf sich beim Laden großer Modelle Zeit lassen
TimeoutStartSec=0
[Install]
WantedBy=multi-user.target
Jetzt die Umgebung pro GPU. Instanz 0 bekommt Karte 0 und Port 11435:
# /etc/ollama/instanz-0.env
OLLAMA_HOST=127.0.0.1:11435
CUDA_VISIBLE_DEVICES=0
# Beide Instanzen teilen sich das Modell-Verzeichnis, das spart Plattenplatz
OLLAMA_MODELS=/var/lib/ollama/.ollama/models
# Zwei Slots pro Modell, konservativ - erst nach VRAM-Messung anheben
OLLAMA_NUM_PARALLEL=2
# Genau ein Modell im VRAM, damit die Karte nicht dauernd nachlädt
OLLAMA_MAX_LOADED_MODELS=1
# Modell dauerhaft geladen halten - sonst zahlt der erste Nutzer nach jeder
# Leerlaufphase das Nachladen aus dem Dateisystem in den VRAM
OLLAMA_KEEP_ALIVE=-1
# Kurze Queue: lieber schnell ein 503 als vier Minuten Wartezeit
OLLAMA_MAX_QUEUE=32
Instanz 1 ist identisch, nur mit OLLAMA_HOST=127.0.0.1:11436 und CUDA_VISIBLE_DEVICES=1.
Ein Schritt, der beim ersten Start gern kostet: OLLAMA_MODELS zeigt oben auf einen anderen Pfad als den Default. Das Installationsskript legt den Dienstnutzer ollama mit Home /usr/share/ollama an, dort liegen die Modelle sonst. Wenn Sie den Pfad verlegen, muss das Verzeichnis existieren und dem Dienstnutzer gehören — sonst startet die Unit und Ollama findet nichts:
sudo mkdir -p /var/lib/ollama/.ollama/models
sudo chown -R ollama:ollama /var/lib/ollama
Beide starten:
sudo systemctl daemon-reload
sudo systemctl enable --now ollama@0 ollama@1
# Kontrolle: sieht jede Instanz nur ihre eigene Karte?
curl -s http://127.0.0.1:11435/api/tags | head -c 200
curl -s http://127.0.0.1:11436/api/tags | head -c 200
nvidia-smi --query-compute-apps=pid,gpu_uuid,used_memory --format=csv
Ein Detail, das Ärger spart: Beide Instanzen lesen aus demselben Modell-Verzeichnis. Zur Laufzeit ist das unproblematisch, beim ollama pull aber nicht. Ziehen Sie Modelle immer nur über eine Instanz, sonst schreiben zwei Prozesse gleichzeitig in dieselben Blob-Dateien.
# Modell einmal über Instanz 0 ziehen, Instanz 1 sieht es danach automatisch
OLLAMA_HOST=127.0.0.1:11435 ollama pull qwen2.5:14b
# Instanz 1 lädt es beim ersten Request in ihren eigenen VRAM
curl -s http://127.0.0.1:11436/api/generate \
-d '{"model":"qwen2.5:14b","prompt":"Test","stream":false}' | head -c 120
Setup 2: nginx davor, und die vier Zeilen, die alle vergessen
Warum least_conn und nicht der nginx-Default Round Robin? Weil Anfragedauern bei LLMs um Größenordnungen streuen. Eine Klassifikation ist nach einem Sekundenbruchteil mit einer Handvoll Token durch; eine lange Zusammenfassung belegt dieselbe GPU minutenlang. Round Robin zählt Anfragen, nicht Arbeit — und schiebt der GPU, die gerade an der langen Generierung hängt, brav jede zweite neue Anfrage rein. least_conn schaut auf offene Verbindungen und trifft damit ziemlich genau die tatsächliche Last.
# /etc/nginx/conf.d/ollama-lb.conf
upstream ollama_pool {
# Nach offenen Verbindungen verteilen, nicht nach Anfrage-Zähler
least_conn;
server 127.0.0.1:11435 max_fails=2 fail_timeout=30s;
server 127.0.0.1:11436 max_fails=2 fail_timeout=30s;
# Persistente Verbindungen zum Backend, spart TCP-Handshakes
keepalive 16;
}
server {
listen 8080;
server_name ki-gateway.intern;
# Große Prompts und Base64-Bilder dürfen durch
client_max_body_size 64m;
location / {
proxy_pass http://ollama_pool;
# HTTP/1.1 ist Pflicht - HTTP/1.0 kann kein Chunked Transfer Encoding
proxy_http_version 1.1;
proxy_set_header Connection "";
# Ollama prüft den Host-Header und antwortet sonst mit 403
proxy_set_header Host localhost;
proxy_set_header X-Real-IP $remote_addr;
# Die wichtigste Zeile für Streaming: nicht puffern
proxy_buffering off;
proxy_request_buffering off;
proxy_cache off;
# Default wäre 60s - das killt jede längere Generierung mit 504
proxy_connect_timeout 5s;
proxy_send_timeout 900s;
proxy_read_timeout 900s;
send_timeout 900s;
# Nur bei Verbindungsfehlern umschwenken, nicht mitten im Stream
proxy_next_upstream error timeout http_502 http_503;
proxy_next_upstream_tries 2;
}
# Leichtgewichtiger Endpoint für externes Monitoring
location = /healthz {
proxy_pass http://ollama_pool/api/tags;
# Muss hier wiederholt werden: Direktiven aus "location /" gelten
# nicht in einem Geschwister-Block. Ohne HTTP/1.1 fällt dieser
# Endpoint gegen den keepalive-Upstream auf HTTP/1.0 zurück.
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host localhost;
proxy_connect_timeout 2s;
proxy_read_timeout 5s;
access_log off;
}
}
Zum Thema Health-Checks eine Klarstellung, die in vielen Anleitungen fehlt: Aktive Health-Checks kann das freie nginx nicht. Die Direktive health_check gehört zu nginx Plus. Was Sie bekommen, ist passives Ausbooten über max_fails=2 fail_timeout=30s — nginx merkt sich fehlgeschlagene Verbindungen und nimmt den Backend-Server für 30 Sekunden aus dem Pool. Das reicht für den Fall "Instanz abgestürzt", nicht für "Instanz antwortet, aber das Modell ist nicht geladen".
Wer aktiv prüfen will, nimmt einen systemd-Timer statt einer weiteren Software-Schicht:
#!/usr/bin/env bash
# /usr/local/bin/ollama-healthcheck.sh - alle 60s per systemd-Timer aufrufen
set -euo pipefail
for PORT in 11435 11436; do
if ! curl -sf --max-time 5 "http://127.0.0.1:${PORT}/api/tags" > /dev/null; then
logger -t ollama-hc "Instanz auf Port ${PORT} antwortet nicht - Neustart"
systemctl restart "ollama@$(( PORT - 11435 ))"
fi
done
Danach sudo nginx -t && sudo systemctl reload nginx, und der Endpoint steht auf Port 8080. Alles, was vorher gegen http://server:11434 lief, zeigt jetzt auf http://ki-gateway.intern:8080 — die API ist identisch.
Was in der Praxis schiefgeht
Sticky Sessions für den Chat-Kontext. Der häufigste Denkfehler. Jemand setzt ip_hash in den Upstream, weil "der Chatverlauf ja auf dem Server liegen muss". Tut er nicht: Ollamas /api/chat ist zustandslos, der Client schickt bei jeder Runde das komplette Message-Array mit. Das Ergebnis von ip_hash in einem Firmennetz ist ein Klassiker — alle Nutzer kommen über dieselbe NAT-Adresse, landen deshalb auf derselben Instanz, und im nvidia-smi steht GPU 0 bei 100 % Auslastung, GPU 1 bei 0 %. Sie haben dann eine zweite Karte gekauft und einen Load-Balancer gebaut, um exakt den Zustand von vorher zu bekommen. Wenn Sie ip_hash in einer Ollama-Config sehen, ist es fast immer ein Bug — löschen, nicht diskutieren.
Streaming durch puffernde Proxies. Fehlerbild: 40 Sekunden lang passiert im Frontend nichts, dann erscheint die komplette Antwort auf einen Schlag. Die Anfrage funktioniert, das Streaming nicht. Ursache ist ein Proxy, der die Chunked-Response sammelt statt durchzureichen. Häufig ist es nicht Ihr nginx, sondern eine zweite Schicht davor — ein Corporate-Proxy, ein Traefik, eine WAF. Prüfen Sie mit curl -N gegen jede Ebene einzeln:
# -N schaltet das Puffern in curl selbst ab. Token müssen einzeln eintröpfeln.
curl -N http://ki-gateway.intern:8080/api/generate \
-d '{"model":"qwen2.5:14b","prompt":"Zähle langsam von 1 bis 30.","stream":true}'
Kommen die JSON-Zeilen einzeln, ist die Kette sauber. Kommt alles am Stück, puffert etwas dazwischen. Als Notnagel hilft der Antwort-Header X-Accel-Buffering: no, den viele Proxies respektieren.
Der 60-Sekunden-Abbruch. Mit Abstand der häufigste Fehler beim ersten Setup. proxy_read_timeout steht per Default auf 60 Sekunden, und LLM-Generierungen überschreiten das regelmäßig. Im error.log steht dann upstream timed out (110: Connection timed out) while reading upstream, der Client bekommt eine abgeschnittene Antwort oder einen 504. Wichtig: Beim Streaming zählt der Timeout zwischen zwei Chunks, bei "stream": false gegen die gesamte Generierungsdauer. Daher die 900 Sekunden oben — großzügig, aber nicht unendlich.
Retries bei POST. proxy_next_upstream schließt nicht-idempotente Methoden standardmäßig aus, und das ist gut so. Wer non_idempotent dazuschreibt, riskiert, dass eine abgebrochene Generierung auf der zweiten GPU komplett neu läuft und der Nutzer die Antwort doppelt bezahlt — in Rechenzeit wie in Wartezeit.
Setup 3: LiteLLM, wenn mehr als Verteilung gebraucht wird
nginx verteilt Pakete. Es weiß nicht, was ein Token ist, was ein Modell kostet oder welches Team wie viel verbraucht. Sobald Sie Kostenverrechnung pro Abteilung, ein API-Key-Konzept oder Fallback von einem lokalen Modell auf ein Cloud-Modell brauchen, ist LiteLLM die passendere Ebene. Es bietet eine OpenAI-kompatible Schnittstelle, Routing-Strategien wie least-busy oder latency-based und schreibt Verbrauchsdaten nach Postgres.
# config.yaml - beide Ollama-Instanzen als ein Modell namens "qwen-lokal"
model_list:
- model_name: qwen-lokal
litellm_params:
model: ollama/qwen2.5:14b
api_base: http://127.0.0.1:11435
- model_name: qwen-lokal
litellm_params:
model: ollama/qwen2.5:14b
api_base: http://127.0.0.1:11436
router_settings:
routing_strategy: least-busy
Die Details stehen in unserem Beitrag zu LiteLLM-Fallback-Routing mit Ollama. Unsere Empfehlung: Fangen Sie mit nginx an. LiteLLM lohnt sich, wenn wirklich abgerechnet oder gegen Cloud-Modelle abgesichert werden muss — sonst holen Sie sich nur eine weitere Python-Komponente in den kritischen Pfad.
Wann nginx reicht — und wann vLLM die bessere Antwort ist
Klare Position: Ein Load-Balancer vor Ollama löst ein Verfügbarkeitsproblem, kein Durchsatzproblem. Zwei Instanzen verdoppeln bestenfalls den Durchsatz, weil zwei GPUs statt einer arbeiten. Am Grundproblem — Ollama fasst gleichzeitige Anfragen nicht in einen laufenden GPU-Batch zusammen — ändert der Balancer nichts.
Genau das kann vLLM. Continuous Batching schiebt neue Anfragen in einen bereits laufenden Batch, statt sie hintereinander abzuarbeiten.
Wie groß der Vorsprung ausfällt, sagen wir hier bewusst nicht in einer Zahl. Die kursierenden Vergleiche reichen vom knapp Doppelten bis zum Neunfachen, je nach Modell, Quantisierung, GPU und vor allem danach, bei wie vielen gleichzeitigen Anfragen gemessen wurde — und die wenigsten dieser Benchmarks legen ihr Setup so offen, dass man sie nachbauen könnte. Übernehmen Sie keine davon in eine Investitionsrechnung. Was sich dagegen reproduzieren lässt, ist die Richtung: Bei einer einzelnen Anfrage nehmen sich beide praktisch nichts, weil am Ende dieselben Kernel rechnen. Der Abstand entsteht ausschließlich unter Parallellast, und er wächst mit der Zahl gleichzeitiger Anfragen. Wer wissen will, was das im eigenen Haus bedeutet, misst mit dem eigenen Modell und der eigenen Lastkurve — zwei Nachmittage, und die Frage ist beantwortet statt geschätzt.
Die Entscheidungsregel, die wir Kunden mitgeben:
- Bis ungefähr 10 gleichzeitige Nutzer, viele verschiedene Modelle, kleines IT-Team: Ollama plus nginx. Betriebsaufwand nahe null, und jede Instanz kann ein anderes Modell fahren.
- Ab 20 gleichzeitigen Nutzern oder festem Modell mit hohem Volumen: vLLM. Einstieg im vLLM-Server-Leitfaden, für große Modelle im vLLM-Cluster für 70B-Modelle.
- Mehr als zwei oder drei Hosts, Bedarf an rollierenden Updates: Erst dann rechnet sich Ollama auf Kubernetes. Vorher zahlen Sie Komplexität für nichts.
Ein Sonderfall bleibt: Wenn ein einzelnes Modell gar nicht in eine GPU passt, hilft weder Load-Balancing noch vLLM-Batching, sondern nur das Aufteilen des Modells über mehrere Rechner. Das ist ein anderes Thema, beschrieben unter Ollama-Cluster über mehrere Rechner mit Sharding.
Zum Schluss der Realismus-Check: Was Sie hier bauen, will überwacht werden. Ohne Metriken merken Sie nicht, dass seit drei Wochen eine Instanz tot ist und nginx still alles auf die verbliebene GPU schiebt. Wie Sie GPU-Auslastung und Queue-Tiefe sichtbar machen, steht im Monitoring-Setup mit Prometheus, Grafana und DCGM.
FAQ
Was kostet ein Ollama-Load-Balancer im Betrieb?
Die Software kostet nichts — nginx, systemd und Ollama sind Open Source. Der Kostenblock ist die zweite GPU plus deren Strom. Die Rechnung mit offengelegten Annahmen: zwei Karten zu je 300 W ergeben 0,6 kW, mal 8.760 Stunden sind 5.256 kWh, bei einem Arbeitspreis von 0,30 EUR/kWh also rund 1.577 EUR im Jahr. Das ist bewusst die Obergrenze — die Rechnung unterstellt 365 Tage Volllast rund um die Uhr, was kein Inferenz-Server erreicht. Setzen Sie Ihren eigenen Arbeitspreis und Ihre echte Auslastung ein, statt diese Zahl zu übernehmen. Kühlung und Abschreibung der Hardware sind nicht enthalten.
Warum least_conn statt round_robin bei Ollama?
Weil LLM-Anfragen um Größenordnungen unterschiedlich lange dauern — von Sekundenbruchteilen für eine Klassifikation bis zu mehreren Minuten für eine lange Generierung. Round Robin verteilt nach Anzahl der Anfragen und schickt deshalb neue Arbeit an eine GPU, die noch minutenlang an der vorherigen rechnet. least_conn verteilt nach offenen Verbindungen und bildet die tatsächliche Last deutlich besser ab.
Warum kommt durch den nginx-Proxy ein 403 Forbidden zurück?
Ollama prüft den Host-Header eingehender Anfragen und lehnt alles ab, was nicht von einer lokalen Adresse zu kommen scheint. nginx reicht standardmäßig den Original-Host durch, weshalb jede Anfrage mit 403 endet. Die Lösung ist proxy_set_header Host localhost; im Location-Block — in jedem Location-Block, auch im Health-Check. OLLAMA_ORIGINS hilft hier ausdrücklich nicht: Die Variable steuert die CORS-Prüfung des Origin-Headers und läuft unabhängig von der Host-Prüfung. Wer sie aufdreht, weil ein 403 kommt, ändert nichts am Problem und macht nur die Browser-Freigabe weiter als nötig.
Wie viele parallele Nutzer schafft eine einzelne Ollama-Instanz?
Mit dem Default OLLAMA_NUM_PARALLEL=1 genau eine Anfrage gleichzeitig, alle weiteren warten in der Queue. OLLAMA_NUM_PARALLEL=4 macht daraus vier gleichzeitige Anfragen — vier Anfragen, nicht vier Nutzer. Wie viele Personen das trägt, hängt daran, wie oft die wirklich zeitgleich auf Senden drücken; eine Fachabteilung, die zwischendurch liest und tippt, kommt mit vier Slots deutlich weiter als vier Personen. Voraussetzung bleibt, dass der VRAM den vervierfachten Kontextspeicher trägt. Darüber hinaus hilft nur eine weitere Instanz auf einer weiteren GPU oder der Wechsel auf vLLM.
Ollama mit nginx oder lieber gleich vLLM?
Ollama hinter nginx gewinnt bei geringer bis mittlerer Last, wenn verschiedene Modelle parallel bereitstehen sollen und der Betriebsaufwand klein bleiben muss. vLLM gewinnt ab etwa 20 gleichzeitigen Anfragen auf einem festen Modell, weil Continuous Batching mehrere Anfragen in einen laufenden GPU-Batch zieht statt sie zu serialisieren. Um welchen Faktor, hängt an Modell und Hardware und gehört gemessen, nicht aus einem Blogpost übernommen. Bei einer einzelnen Anfrage ohne Parallellast nehmen sich beide praktisch nichts.
Fazit und nächster Schritt
Zwei systemd-Units, ein nginx-Block, richtig gesetzte Timeouts — mehr braucht es nicht, um zwei GPUs hinter einem Endpoint auszulasten. Messen Sie vorher, ob OLLAMA_NUM_PARALLEL=4 Ihr Problem schon löst; wenn ja, sparen Sie sich die zweite Karte. Wenn Sie noch bei der Grundinstallation stehen, fangen Sie mit der Ollama-Installationsanleitung für Ubuntu an und ziehen den Load-Balancer erst nach, wenn die Warteschlange wirklich weh tut.
📖 Verwandte Artikel
Weitere interessante Beiträge zu ähnlichen Themen
Ollama-Cluster über mehrere Rechner: Sharding
Ein LLM über zwei Rechner verteilen: Ollama kann es nicht, llama.cpp schon. Befehle, echte Benchmarks — und warum meist eine größere GPU gewinnt.
Ollama auf Kubernetes: LLM-Cluster mit Autoscaling
Ollama auf Kubernetes deployen: StatefulSet, NVIDIA GPU-Scheduling, Helm Chart und HPA-Autoscaling für einen produktiven LLM-Cluster.
Ollama GPU CUDA Setup: Ubuntu Server Anleitung
Ollama mit NVIDIA GPU und CUDA auf Ubuntu: 8x schneller als CPU. Anleitung für CUDA-Treiber, VRAM-Optimierung und Produktion.
Bereit für KI im Mittelstand?
Nutzen Sie unsere 10 kostenlosen KI-Tools und Praxis-Guides – oder sprechen Sie direkt mit unseren Experten.
Pexon Consulting – KI-Beratung für den Mittelstand | Scaly Academy – Geförderte KI-Weiterbildung (KI-Spezialist, KI-Experte, Workflow-Automatisierung)