Themenseite · SQL Server unter Linux und Docker

SQL Server unter Linux tunen: Was Microsoft empfiehlt und was davon wirkt

Ganz oben in Ihren Wartezeiten steht PREEMPTIVE_OS_FLUSHFILEBUFFERS. Unter Windows wäre das ein seltener Gast. Unter Linux ist es meist der Normalbetrieb: SQL Server bittet das Betriebssystem nach dem Schreiben ausdrücklich, die Daten auf den Datenträger zu bringen, und wartet darauf. In unserem Lab geschah das nach jedem einzelnen Commit.

Stand 12.11.2026 · ca. 14 Minuten

Teil 4 · PerformanceSQL-Abfrage 4Lab SQL Server 2017–2025
Ein Commit unter LinuxSQL Server 2025 · LabLog schreibenfdatasyncfertigWartetyp PREEMPTIVE_OS_FLUSHFILEBUFFERSStandard: je Commit 7,1 ms, davon 6,4 ms FlushOhne Flush (Trace Flag 3979, nur Messwert): je Commit 3,4 ms0 ms10 ms20 ms

Diese Seite geht durch, was unter Linux die Leistung und den stabilen Betrieb von SQL Server bestimmt: die Kernel- und Systemeinstellungen, die Microsoft empfiehlt, das Flushen beim Schreiben, die Grenzen für Speicher und CPU, den Ort der Datenbankdateien und das Überwachen. Was wir messen konnten, haben wir auf SQL Server 2017, 2019, 2022 und 2025 in Linux-Containern gemessen. Was wir nicht messen konnten, steht als Empfehlung von Microsoft da, mit Quelle.

In 60 Sekunden

  • Microsoft empfiehlt für SQL-Server-Hosts eine Reihe von Kernel-Einstellungen, gebündelt im TuneD-Profil mssql: Transparent Huge Pages an, swappiness 1, NUMA-Balancing aus, großer Read-ahead, noatime, XFS. Viele Standardinstallationen weichen davon ab, auch unser Lab-Server. Der Host-Check auf dieser Seite zeigt Ihre Abweichungen.
  • Schreiben kostet unter Linux einen Flush je Commit. Das ist der sichere Standard. Schneller geht es nur mit Speicher, der FUA beherrscht, und den passenden Einstellungen. writethrough = 0 kann Daten kosten.
  • Speicher in drei Stufen begrenzen: Container- oder Host-Grenze, memory.memorylimitmb für den ganzen SQL-Server-Prozess, max server memory für den Puffer-Pool. Dazwischen 10 bis 20 Prozent Kopfraum. SQL Server 2017 und 2019 ohne Einstellung wurden in unserem 4-GB-Container vom Kernel beendet, ohne eine Zeile im Errorlog.
  • Eine CPU-Quote sieht SQL Server nicht. PROCESS AFFINITY auf so viele CPUs, wie die Quote erlaubt, war in unserem Lab 16 bis 20 Prozent schneller und hob die Drosselung fast ganz auf. Trace Flag 8002 brachte zusätzlich nichts Messbares.
  • Container-Images richten sich nur teilweise ein: Ab 2019 legen sie tempdb-Dateien nach CPUs an (höchstens acht), MAXDOP bleibt aber 0 und die Kostenschwelle 5. Das gehört nach dem ersten Start angepasst.
  • Wo die Dateien im Container liegen, war für die Leistung egal: Schreibschicht, Bind-Mount und Volume lagen gleichauf. Die Schreibschicht scheidet trotzdem aus, aus Betriebsgründen.

1 · Microsofts Liste für den Host

Microsoft beschreibt die empfohlenen Einstellungen auf der Seite „Performance best practices and configuration guidelines for SQL Server on Linux“ und fasst sie im TuneD-Profil mssql zusammen (Red Hat liefert es mit, unter Ubuntu legt man es von Hand an). Alle diese Werte gehören dem Host: In einem Container lassen sie sich lesen, aber nicht ändern.

EinstellungEmpfehlung (Microsoft)Linux-Standard (Ubuntu 24.04, unser Lab)Wozu
Transparent Huge Pagesalways (mehrere Instanzen: madvise)madvisegroße Speicherseiten für den Puffer-Pool
vm.swappiness160Puffer-Pool nicht auslagern
vm.dirty_ratio / dirty_background_ratio80 / 320 / 10Schreibpuffer des Kernels
vm.max_map_count1.600.000 (mindestens 262.144)1.048.576viele Speicherbereiche des Prozesses
kernel.numa_balancing0 bei mehreren NUMA-Knoten1 (2 Knoten)SQL Server verwaltet NUMA selbst
TuneD-Profilmssqlkeinesbündelt die Werte oben
Read-ahead4096 Sektoren je Gerät128 KBgrößere Lesevorgänge
Mount-Optionnoatimerelatimekein Zugriffszeit-Schreiben
DateisystemXFS (ext4 unterstützt)ext4
Swapvorhanden, als SicherheitsnetzkeinerSchutz vor dem OOM-Killer

Wie viel jede Abweichung kostet, haben wir nicht gemessen: Unser Lab-Server wird von weiteren Messungen geteilt, Änderungen am Kernel hätten sie beeinflusst. Die Empfehlungen gelten für Server, auf denen SQL Server die Hauptlast ist; auf einem Host mit mehreren Anwendungen oder Instanzen nennt Microsoft selbst Ausnahmen (etwa madvise statt always).

Unterschiede gibt es schon zwischen Microsofts Seite und dem Profil, das Red Hat ausliefert: Das Upstream-Profil setzt die Dirty-Werte als Byte-Anteile und lässt CPU-Scheduler-Werte aktiv, die Microsoft für neuere Kernel weglässt. Wer TuneD unter Ubuntu nachbaut, sollte sich für eine Fassung entscheiden und sie dokumentieren.

Host-Check

Das folgende Skript liest die Werte aus und vergleicht sie mit der Empfehlung. Es ändert nichts.

#!/usr/bin/env bash
# PSG Stack · SQL Server unter Linux und Docker · Host-Check
# Vergleicht Linux-Einstellungen eines SQL-Server-Hosts mit den Empfehlungen von Microsoft
# (Learn: „Performance best practices: Storage, kernel, CPU, and network“, Stand 05/2026).
#
# !!! WARNUNG — bitte vor dem Ausführen lesen !!!
# - Nur lesend. Das Skript ändert keine Einstellung und schreibt keine Datei.
# - Aufruf: bash host-check-de.sh [Datenverzeichnis] [Logverzeichnis]
#   Standard: /var/opt/mssql/data für beides. Ohne root fehlen evtl. einzelne Werte (dann „?“).
# - Kernel-Werte gelten für den ganzen Host. In einem Container zeigt das Skript die Werte des Hosts;
#   ändern lassen sie sich nur auf dem Host, nicht im Container.
# - „prüfen“ heißt: abweichend von Microsofts Empfehlung. Ob die Abweichung bei Ihnen etwas kostet,
#   sagt das Skript nicht; Empfehlungen gelten für dedizierte SQL-Server-Hosts.
# - Geprüft im PSG-Lab auf Ubuntu 24.04 (Host) und in den Containern von SQL Server 2017 bis 2025
#   (13.11.2026); auf Red Hat und SUSE nicht geprüft.
set -u
DATA=${1:-/var/opt/mssql/data}
LOG=${2:-$DATA}
lies() { cat "$1" 2>/dev/null | tr -d '\n' || true; }
zeile() { printf '%-34s %-28s %-30s %s\n' "$1" "$2" "$3" "$4"; }
pruef() { if [ "$1" = "$2" ]; then echo ok; elif [ -z "$1" ] || [ "$1" = "?" ]; then echo "?"; else echo prüfen; fi; }

echo "Host-Check SQL Server unter Linux · $(date '+%Y-%m-%d %H:%M') · $(hostname)"
. /etc/os-release 2>/dev/null; echo "System: ${PRETTY_NAME:-?} · Kernel $(uname -r)"
if [ -f /.dockerenv ] || grep -qa docker /proc/1/cgroup 2>/dev/null; then echo "Hinweis: läuft in einem Container; Kernel-Werte sind die des Hosts."; fi
echo
zeile "Einstellung" "Ist" "Empfehlung (Microsoft)" "Bewertung"
zeile "-----------" "---" "----------------------" "---------"

thp=$(lies /sys/kernel/mm/transparent_hugepage/enabled | sed -n 's/.*\[\(.*\)\].*/\1/p'); thp=${thp:-?}
zeile "Transparent Huge Pages" "$thp" "always (mehrere Instanzen: madvise)" "$(pruef "$thp" always)"
sw=$(lies /proc/sys/vm/swappiness); sw=${sw:-?}
zeile "vm.swappiness" "$sw" "1" "$(pruef "$sw" 1)"
mm=$(lies /proc/sys/vm/max_map_count); mm=${mm:-?}
if [ "$mm" != "?" ] && [ "$mm" -ge 262144 ] 2>/dev/null; then b=ok; [ "$mm" -lt 1600000 ] && b="ok (Profil: 1600000)"; else b=$(pruef "$mm" 1600000); fi
zeile "vm.max_map_count" "$mm" "1600000 (mindestens 262144)" "$b"
dr=$(lies /proc/sys/vm/dirty_ratio); dbr=$(lies /proc/sys/vm/dirty_background_ratio)
zeile "vm.dirty_ratio / background" "${dr:-?} / ${dbr:-?}" "80 / 3 (TuneD-Profil mssql)" "$( [ "${dr:-}" = 80 ] && [ "${dbr:-}" = 3 ] && echo ok || echo prüfen )"
numa=$(ls -d /sys/devices/system/node/node* 2>/dev/null | wc -l)
nb=$(lies /proc/sys/kernel/numa_balancing); nb=${nb:-?}
if [ "$numa" -gt 1 ]; then b=$(pruef "$nb" 0); else b="ok (1 NUMA-Knoten)"; fi
zeile "kernel.numa_balancing" "$nb ($numa NUMA-Knoten)" "0 bei mehreren NUMA-Knoten" "$b"
if command -v tuned-adm >/dev/null 2>&1; then t=$(tuned-adm active 2>/dev/null | sed -n 's/.*: //p'); else t="kein TuneD"; fi
zeile "TuneD-Profil" "${t:-?}" "mssql" "$( [ "${t:-}" = mssql ] && echo ok || echo prüfen )"
gov=$(lies /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor); gov=${gov:-"n/a (VM?)"}
zeile "CPU-Governor" "$gov" "performance" "$( [ "$gov" = performance ] && echo ok || { [ "${gov#n/a}" != "$gov" ] && echo info || echo prüfen; } )"
sw_on=$(awk 'NR>1' /proc/swaps 2>/dev/null | wc -l)
zeile "Swap eingerichtet" "$( [ "$sw_on" -gt 0 ] && echo ja || echo nein )" "ja (Sicherheitsnetz, keine Kapazität)" "$( [ "$sw_on" -gt 0 ] && echo ok || echo prüfen )"

for pfad in $(printf "%s\n%s\n" "$DATA" "$LOG" | awk '!s[$0]++'); do
  [ -d "$pfad" ] || { zeile "Verzeichnis $pfad" "fehlt" "" "?"; continue; }
  read -r dev fs opts < <(awk -v p="$(df -P "$pfad" 2>/dev/null | awk 'NR==2{print $6}')" '$2==p{print $1, $3, $4}' /proc/mounts | tail -1)
  if [ "${fs:-}" = overlay ]; then
    zeile "Dateisystem $pfad" "overlay" "Volume oder Bind-Mount" "prüfen (Container-Schreibschicht)"; continue; fi
  zeile "Dateisystem $pfad" "${fs:-?}" "xfs (ext4 unterstützt)" "$( [ "${fs:-}" = xfs ] && echo ok || { [ "${fs:-}" = ext4 ] && echo ok || echo prüfen; } )"
  zeile "  Mount-Option atime" "$(echo "${opts:-?}" | grep -o 'noatime\|relatime\|strictatime' | head -1)" "noatime" "$(echo "${opts:-}" | grep -q noatime && echo ok || echo prüfen)"
  blk=$(basename "$(readlink -f "${dev:-x}" 2>/dev/null)" | sed 's/[0-9]*$//; s/p$//')
  if [ -n "$blk" ] && [ -d "/sys/block/$blk/queue" ]; then
    ra=$(lies /sys/block/$blk/queue/read_ahead_kb); sch=$(lies /sys/block/$blk/queue/scheduler | sed -n 's/.*\[\(.*\)\].*/\1/p')
    fua=$(lies /sys/block/$blk/queue/fua); wc=$(lies /sys/block/$blk/queue/write_cache)
    zeile "  Read-ahead ($blk)" "${ra:-?} KB" "4096 (blockdev --setra, Sektoren)" "info"
    zeile "  I/O-Scheduler ($blk)" "${sch:-?}" "keine Vorgabe (Multiqueue)" "info"
    zeile "  FUA / Write-Cache ($blk)" "${fua:-?} / ${wc:-?}" "FUA=1 nur dann TF 3979" "info"
  else
    zeile "  Blockgerät" "nicht ermittelbar" "" "?"
  fi
done

CONF=/var/opt/mssql/mssql.conf
echo
if [ -r "$CONF" ]; then
  echo "mssql.conf ($CONF), Einträge mit Leistungsbezug:"
  grep -E '^\s*(writethrough|alternatewritethrough|memorylimitmb|traceflag[0-9]*)\s*=' "$CONF" | sed 's/^/  /' || echo "  (keine; es gelten die Standardwerte)"
else
  echo "mssql.conf nicht lesbar oder nicht vorhanden (Standardwerte; im Container ggf. MSSQL_*-Variablen)."
fi
echo "Standard unter Linux: Forced Flush (Trace Flag 3982), control.writethrough = 1, control.alternatewritethrough = 1."
echo "Nie writethrough = 0 setzen, solange nicht belegt ist, dass der Speicher O_DIRECT-sicher ist."

Beispielausgabe

Aus dem Lab: unser Host mit Ubuntu 24.04, Daten und Log auf getrennten virtuellen Datenträgern. Fast jede Kernel-Einstellung weicht von Microsofts Liste ab; das ist der Normalfall einer Standardinstallation.

Beispielausgabe
Host-Check SQL Server unter Linux · 2026-11-13 10:33 · psg-sql-lab01
System: Ubuntu 24.04.5 LTS · Kernel 6.8.0-147-generic

Einstellung                        Ist                          Empfehlung (Microsoft)         Bewertung
-----------                        ---                          ----------------------         ---------
Transparent Huge Pages             madvise                      always (mehrere Instanzen: madvise) prüfen
vm.swappiness                      60                           1                              prüfen
vm.max_map_count                   1048576                      1600000 (mindestens 262144)    ok (Profil: 1600000)
vm.dirty_ratio / background        20 / 10                      80 / 3 (TuneD-Profil mssql)    prüfen
kernel.numa_balancing              1 (2 NUMA-Knoten)            0 bei mehreren NUMA-Knoten     prüfen
TuneD-Profil                       kein TuneD                   mssql                          prüfen
CPU-Governor                       n/a (VM?)                    performance                    info
Swap eingerichtet                  nein                         ja (Sicherheitsnetz, keine Kapazität) prüfen
Dateisystem /srv/sql/data          ext4                         xfs (ext4 unterstützt)        ok
  Mount-Option atime               relatime                     noatime                        prüfen
  Read-ahead (sdc)                 128 KB                       4096 (blockdev --setra, Sektoren) info
  I/O-Scheduler (sdc)              none                         keine Vorgabe (Multiqueue)     info
  FUA / Write-Cache (sdc)          0 / write back               FUA=1 nur dann TF 3979         info
Dateisystem /srv/sql/log           ext4                         xfs (ext4 unterstützt)        ok
  Mount-Option atime               relatime                     noatime                        prüfen
  Read-ahead (sdd)                 128 KB                       4096 (blockdev --setra, Sektoren) info
  I/O-Scheduler (sdd)              none                         keine Vorgabe (Multiqueue)     info
  FUA / Write-Cache (sdd)          0 / write back               FUA=1 nur dann TF 3979         info

mssql.conf nicht lesbar oder nicht vorhanden (Standardwerte; im Container ggf. MSSQL_*-Variablen).
Standard unter Linux: Forced Flush (Trace Flag 3982), control.writethrough = 1, control.alternatewritethrough = 1.
Nie writethrough = 0 setzen, solange nicht belegt ist, dass der Speicher O_DIRECT-sicher ist.

2 · Schreiben und Flushen

Ein Commit ist erst fertig, wenn der Log-Eintrag sicher auf dem Datenträger liegt. Unter Windows verlangt SQL Server das beim Schreiben mit einem Flag. Unter Linux hängt es davon ab, ob der Datenträger „Forced Unit Access“ (FUA) beherrscht, also einzelne Schreibvorgänge am Cache vorbei dauerhaft machen kann.

VarianteEinstellungenWann
Forced Flush (Standard)Trace Flag 3982 (intern), control.writethrough = 1, control.alternatewritethrough = 1immer, solange der Speicher kein FUA nachweislich kann
FUA-SchreibenTrace Flag 3979, writethrough = 1, alternatewritethrough = 0nur mit FUA-fähigem Speicher (XFS ab Kernel 4.18, ext4 ab 5.6), nicht auf overlayfs
unsicherwritethrough = 0nie, solange nicht belegt ist, dass der Speicher O_DIRECT-sicher ist

Im Standard ruft SQL Server nach dem Schreiben fdatasync auf; das ist der Wartetyp PREEMPTIVE_OS_FLUSHFILEBUFFERS. In unserem Lab (Daten und Log auf getrennten virtuellen Datenträgern, ext4, Datenträger ohne FUA) sah das bei vielen kleinen Transaktionen so aus:

Commits/s (eine Sitzung / vier Sitzungen)2017201920222025
Standard (Flush je Commit, 6,4 ms im Mittel)141 / 542141 / 463140 / 427140 / 380
Trace Flag 3979, alternatewritethrough = 0 (kein Flush)293 / 777293 / 931291 / 662290 / 894

Alle vier Versionen verhalten sich gleich: 20.000 Commits, rund 20.000 Aufrufe von fdatasync. Bei SQL Server 2025 dauerte ein Commit im Standard rund 7,1 ms, davon 6,4 ms Flush; ohne Flush rund 3,4 ms.

Standard (Forced Flush) · 140 Commits/s · je Commit 6,4 ms FlushTrace Flag 3979, alternatewritethrough = 0 · 290 Commits/s · kein Flush (nur Messwert)0 ms20 ms40 ms60 ms80 ms100 msBlau: Schreiben des Log-Eintrags bis zur Bestätigung. Orange: Warten auf fdatasync(PREEMPTIVE_OS_FLUSHFILEBUFFERS). Eine Sitzung, 20.000 Einzel-Commits, Mittelwerte.
Standard (Forced Flush) · 140 Commits/s · jeCommit 6,4 ms FlushTrace Flag 3979, alternatewritethrough = 0 · 290Commits/s · kein Flush (nur Messwert)0 ms50 ms100 msBlau: Schreiben des Log-Eintrags bis zurBestätigung. Orange: Warten auf fdatasync(PREEMPTIVE_OS_FLUSHFILEBUFFERS). Eine Sitzung,20.000 Einzel-Commits, Mittelwerte.
Die ersten 100 ms einer Sitzung mit vielen kleinen Transaktionen, SQL Server 2025 im Lab (Datenträger ohne FUA). Trace Flag 3979 ist hier keine Empfehlung, sondern zeigt, was der Flush kostet.

Die zweite Zeile ist keine Empfehlung: Unsere Datenträger melden kein FUA, und Microsoft nennt Trace Flag 3979 nur für Speicher, der es beherrscht. Ob der Kernel in diesem Fall für jeden Schreibvorgang den Cache des Datenträgers wirklich leert, haben wir nicht geprüft. Die Messung zeigt, was der Flush je Commit kostet, nicht, dass man ihn abschalten sollte.

Zwei Beobachtungen für den Betrieb:

  • DBCC TRACESTATUS zeigt den Standard nicht: Trace Flag 3982 erscheint mit Status 0, obwohl Forced Flush aktiv ist. Ist 3979 gesetzt, steht es mit Status 1 da und im Errorlog als Startparameter -T 3979.
  • control.writethrough und control.alternatewritethrough sieht SQL Server nicht; sie stehen in /var/opt/mssql/mssql.conf im Abschnitt [control], gesetzt mit mssql-conf set control.alternatewritethrough 0. Fehlt der Abschnitt, gelten die Standardwerte.

Hohe WRITELOG-Zeiten haben unter Linux dieselben Ursachen wie unter Windows: ein langsamer Log-Datenträger oder sehr viele sehr kleine Transaktionen. Mehrere Zeilen in einer Transaktion zusammenzufassen spart unter Linux zusätzlich je einen Flush.

3 · Speicher in drei Stufen

StufeBegrenztGesetzt mit
Host oder ContainerallesVM-Größe bzw. docker run --memory
memory.memorylimitmbden ganzen SQL-Server-Prozess (Puffer-Pool, SQLPAL, Agent, Full-Text)mssql-conf set memory.memorylimitmb, im Container MSSQL_MEMORY_LIMIT_MB
max server memorynur den Puffer-Poolsp_configure

Microsoft empfiehlt zwischen Container-Grenze und memory.memorylimitmb 10 bis 20 Prozent Kopfraum und max server memory darunter. Ohne Einstellung nimmt SQL Server 80 Prozent des kleineren Werts aus Host-Speicher und Container-Grenze, aber erst ab 2022 CU20 auch unter cgroup v2; 2017 und 2019 sehen die Grenze eines Containers dort nicht (Teil 3).

Was passiert, haben wir mit einem 4-GB-Container (--memory=4g, ohne Swap) und einer Last nachgestellt, die eine 4,7 GB große Tabelle mehrfach liest und gruppiert:

Versionohne Einstellungmemory.memorylimitmb 20 % unter der Grenze (3.277 MB)
2017sieht 64 GB, Ziel 60 GB: vom Kernel beendetLast fertig, Spitze 2,8 GB
2019sieht 64 GB, Ziel 61 GB: vom Kernel beendetLast fertig, Spitze 3,2 GB
2022sieht 3,2 GB (80 % der Grenze): Last fertig, Spitze 3,3 GBLast fertig, Spitze 3,3 GB
2025sieht 3,2 GB (80 % der Grenze): Last fertig, Spitze 3,3 GBLast fertig, Spitze 3,4 GB
SQL Server 2017ohne Einstellung · sieht 64 GBvom Kernel beendetmemory.memorylimitmb 20 % darunterSpitze 2,8 GBSQL Server 2019ohne Einstellung · sieht 64 GBvom Kernel beendetmemory.memorylimitmb 20 % darunterSpitze 3,2 GBSQL Server 2022ohne Einstellung · sieht 3,2 GBSpitze 3,3 GBmemory.memorylimitmb 20 % darunterSpitze 3,3 GBSQL Server 2025ohne Einstellung · sieht 3,2 GBSpitze 3,3 GBmemory.memorylimitmb 20 % darunterSpitze 3,4 GBContainer-Grenze 4 GB0 GB1 GB2 GB3 GB4 GBBalken: höchster Speicherverbrauch des Containers (cgroup) unter Last. Orange mit Kreuz: SQL Server plantemit dem Host-Speicher und wurde an der Grenze beendet, ohne Zeile im Errorlog.
SQL Server 2017ohne Einstellung · sieht 64 GBvom Kernel beendetmemory.memorylimitmb 20 % darunterSpitze 2,8 GBSQL Server 2019ohne Einstellung · sieht 64 GBvom Kernel beendetmemory.memorylimitmb 20 % darunterSpitze 3,2 GBSQL Server 2022ohne Einstellung · sieht 3,2 GBSpitze 3,3 GBmemory.memorylimitmb 20 % darunterSpitze 3,3 GBSQL Server 2025ohne Einstellung · sieht 3,2 GBSpitze 3,3 GBmemory.memorylimitmb 20 % darunterSpitze 3,4 GBContainer-Grenze 4 GB0 GB1 GB2 GB3 GB4 GBBalken: höchster Speicherverbrauch desContainers (cgroup) unter Last. Orange mitKreuz: SQL Server plante mit dem Host-Speicherund wurde an der Grenze beendet, ohne Zeile imErrorlog.
Ohne Einstellung planen SQL Server 2017 und 2019 mit 80 Prozent des Host-Speichers und werden im 4-GB-Container beendet. Mit memory.memorylimitmb 20 Prozent unter der Grenze lief die Last in allen geprüften Versionen durch.

SQL Server 2022 und 2025 überstanden bei dieser Last sogar memory.memorylimitmb gleich der Container-Grenze (Spitze 3,87 bzw. 3,76 von 4 GB). Das heißt nicht, dass es ohne Kopfraum sicher ist: Agent, CLR oder große Speicherzuteilungen für Abfragen haben wir nicht belastet.

Ein OOM-Kill hinterlässt im Errorlog von SQL Server keine Zeile. Das Errorlog endete in unserem Lab mit den normalen Startmeldungen, dann nichts mehr. Belege stehen an zwei anderen Stellen: im Kernel-Log (dmesg bzw. journalctl -k: oom-kill:constraint=CONSTRAINT_MEMCG … oom_memcg=/system.slice/docker-<id>.scope) und bei Docker im Container-Zustand (docker inspect: OOMKilled: true, Exitcode 137). Wer nur das Errorlog überwacht, sieht einen Neustart ohne Grund.

Ein Fehler, nach dem der Prozess mehr Speicher belegte als memory.memorylimitmb erlaubt, ist in SQL Server 2019 CU27 und 2022 CU14 behoben (KB5042369).

4 · CPU und NUMA

Eine CPU-Quote (docker run --cpus, Kubernetes-CPU-Limits) sieht SQL Server nicht: Er legt für jede CPU des Hosts einen Scheduler an und plant Parallelität danach (Teil 3). Microsoft nennt als Gegenmittel ALTER SERVER CONFIGURATION SET PROCESS AFFINITY auf so viele CPUs, wie die Quote erlaubt, unter cgroup v2 zusammen mit Trace Flag 8002 (weiche Affinität: die Scheduler dürfen die CPU wechseln). Wir haben das mit einer parallelen Aggregation in einem Container mit --cpus=4 auf einem Host mit 24 CPUs gemessen:

Laufzeit (Median aus 5) und vom Kernel gedrosselte Zeit201920222025
Standard: 24 Scheduler, Parallelität 24775 ms, 51 s873 ms, 60 s991 ms, 58 s
PROCESS AFFINITY CPU = 0 TO 3: 4 Scheduler648 ms, 0 s697 ms, 0 s791 ms, 0 s
Affinität und Trace Flag 8002637 ms, 0 s707 ms, 0 s776 ms, 0 s
nur Trace Flag 8002790 ms, 49 s874 ms, 61 s984 ms, 62 s
SQL Server 2019Standard · 24 Scheduler775 ms · gedrosselt 51 sPROCESS AFFINITY 0–3648 msAffinität + Trace Flag 8002637 msSQL Server 2022Standard · 24 Scheduler873 ms · gedrosselt 60 sPROCESS AFFINITY 0–3697 msAffinität + Trace Flag 8002707 msSQL Server 2025Standard · 24 Scheduler991 ms · gedrosselt 58 sPROCESS AFFINITY 0–3791 msAffinität + Trace Flag 8002776 ms0 ms500 ms1.000 msContainer mit --cpus=4 auf 24 CPUs, parallele Aggregation über 4 Mio. Zeilen, Median aus fünf Läufen.Orange: SQL Server plant mit 24 Schedulern und wird vom Kernel gedrosselt.
SQL Server 2019Standard · 24 Scheduler775 msvom Kernel gedrosselt: 51 s in 5 LäufenPROCESS AFFINITY 0–3648 msAffinität + Trace Flag 8002637 msSQL Server 2022Standard · 24 Scheduler873 msvom Kernel gedrosselt: 60 s in 5 LäufenPROCESS AFFINITY 0–3697 msAffinität + Trace Flag 8002707 msSQL Server 2025Standard · 24 Scheduler991 msvom Kernel gedrosselt: 58 s in 5 LäufenPROCESS AFFINITY 0–3791 msAffinität + Trace Flag 8002776 ms0 ms500 ms1.000 msContainer mit --cpus=4 auf 24 CPUs, paralleleAggregation über 4 Mio. Zeilen, Median aus fünfLäufen. Orange: SQL Server plant mit 24Schedulern und wird vom Kernel gedrosselt.
PROCESS AFFINITY passend zur Quote nimmt die Drosselung weg und macht die Abfrage 16 bis 20 Prozent schneller. Trace Flag 8002 ändert daran kaum etwas.

Ohne Affinität plant SQL Server 24 parallele Threads auf vier erlaubte CPUs; der Kernel bremst den Container dann immer wieder für den Rest seiner Zeitscheibe aus. Mit Affinität auf vier CPUs passt die Parallelität zur Quote, die Drosselung verschwindet, die Abfrage wird 16 bis 20 Prozent schneller. Trace Flag 8002 (weiche Affinität) änderte den Median um höchstens 2 Prozent, bei 2022 und 2025 mit starken Ausreißern in beide Richtungen. In einem kleineren Vorlauf mit 800.000 Zeilen war Affinität allein langsamer; das hing an der kleinen Datenmenge.

Unsere Empfehlung für Container mit CPU-Quote: PROCESS AFFINITY passend zur Quote setzen; Trace Flag 8002 nur, wenn Microsofts Hinweis für Ihre Umgebung gilt und Sie es messen. Einfacher sind feste CPUs (--cpuset-cpus), die SQL Server 2022 und 2025 erkennen; 2017 und 2019 sehen auch dann alle CPUs des Hosts.

NUMA: Auf Hosts mit mehreren NUMA-Knoten empfiehlt Microsoft, das automatische NUMA-Balancing des Kernels abzuschalten (kernel.numa_balancing = 0), weil SQL Server seine Speicherzuordnung selbst steuert. Gemessen haben wir das nicht.

Voreinstellungen im Container: So richten sich die Images beim ersten Start auf unserem Host mit 24 CPUs ein:

2017201920222025
tempdb-Datendateien, ohne Grenze oder mit --cpus=61888
tempdb-Datendateien, mit 6 festen CPUs (--cpuset-cpus)1866
MAXDOP / Kostenschwelle0 / 50 / 50 / 50 / 5

Ab 2019 legt das Image tempdb also wie eine Paketinstallation an: eine Datei je CPU, höchstens acht. Dabei zählt, was SQL Server als CPU sieht; eine Quote sieht er nicht, feste CPUs erst ab 2022. MAXDOP und Kostenschwelle stehen dagegen in allen Versionen auf der alten Voreinstellung. Setzen Sie beides nach dem ersten Start, wie in unserem TempDB-Ratgeber und unter Windows auch.

Eine Falle für Skripte: SQL Server meldet sich im Container schon als bereit („SQL Server is now ready for client connections“), bevor tempdb fertig ist. Das Errorlog zeigt erst „The tempdb database has 1 data file(s).“ und ein bis zwei Sekunden später die endgültige Zahl. Wer gleich nach der ersten Anmeldung prüft oder Last startet, sieht einen Zwischenstand oder bekommt Fehler 904 („cannot be autostarted during server shutdown or start“). Warten Sie in Startskripten auf „Recovery is complete“ im Errorlog, nicht nur auf eine erfolgreiche Anmeldung.

5 · Wo die Datenbankdateien liegen

Im Container können Datenbankdateien in der Schreibschicht des Containers liegen (overlayfs), auf einem Bind-Mount eines Host-Verzeichnisses oder in einem benannten Docker-Volume. Docker empfiehlt Volumes für schreibintensive Last; Microsoft verlangt für den FUA-Weg ausdrücklich einen Mount, nicht overlayfs. Gemessen haben wir dieselbe Last in vier Varianten:

SQL Server 2025Commits/s (1 / 4 Sitzungen)Bulk-Insert 1 Mio. ZeilenBackup
Schreibschicht des Containers (overlayfs)141 / 4223,21 s3,23 s
Bind-Mount, gleicher Datenträger141 / 4423,21 s3,28 s
benanntes Volume141 / 4243,23 s3,35 s
Bind-Mounts, Daten und Log auf getrennten Datenträgern141 / 4723,16 s3,43 s

SQL Server 2022 zeigte dasselbe Bild (141 Commits/s in allen Varianten, Bulk 2,99 bis 3,17 s); die Werte mit vier Sitzungen streuten zwischen 334 und 482 ohne Muster. Auf unserem Host war der Ort also für die Leistung egal. Trotzdem gehören Datenbankdateien nicht in die Schreibschicht: Sie verschwinden mit dem Container, und für Updates muss der Container neu erstellt werden (Teil 3). Getrennte Datenträger für Daten und Log bleiben sinnvoll, wenn die Datenträger selbst getrennt sind; bei virtuellen Datenträgern auf derselben SSD wie hier bringt das wenig.

Dateisystem: Microsoft empfiehlt XFS für Daten und Log, ext4 ist unterstützt; dazu noatime. Eine Messung XFS gegen ext4 haben wir nicht gemacht, eine veröffentlichte haben wir nicht gefunden.

6 · Überwachen unter Linux

Was in der Datenbank passiert, sehen Sie unter Linux wie unter Windows: Query Store, DMVs, Extended Events, das Performance Dashboard in SSMS. Anders ist die Sicht auf das Betriebssystem:

  • Kein Performance Monitor. Die SQL-Server-Zähler liefert sys.dm_os_performance_counters; Raten und Prozentwerte muss man aus den Rohwerten selbst berechnen.
  • Betriebssystem-Werkzeuge: top/htop, vmstat, iostat, sar (Paket sysstat). Red Hat liefert mit Performance Co-Pilot ein Plug-in für SQL Server (pmdamssql), das DMV-Werte als Metriken sammelt.
  • Wartetypen, die unter Linux anders aussehen: PREEMPTIVE_OS_FLUSHFILEBUFFERS steht oft weit oben und ist für sich allein kein Befund (Abschnitt 2).
  • Das Management Data Warehouse (Data Collector) in SSMS wird gegen Linux-Instanzen nicht unterstützt.

Betriebs-Check: Wie steht meine Instanz da?

Die folgende Abfrage bewertet die Punkte dieser Seite für eine Instanz, eine Zeile je Prüfpunkt. Was die Instanz von Plattform und Container-Grenzen sieht, zeigt Abfrage 2 in Teil 3; welche unter Linux fehlenden Funktionen sie nutzt, Abfrage 1 in Teil 1.

Abfrage: Betriebs-Check

Abfrage 4: Betriebs-Check · nur lesend · im PSG-Lab geprüft, Versionen im Kopf
/* PSG Stack · SQL Server unter Linux und Docker · Abfrage 4 „Betriebs-Check“   Prüft Einstellungen und Wartezeiten, die unter Linux für Leistung und stabilen Betrieb zählen:   Flush-Trace-Flags, Speichergrenzen und Speicherdruck, CPUs und Scheduler, MAXDOP, tempdb,   Wartezeiten beim Schreiben des Transaktionsprotokolls. Eine Zeile je Prüfpunkt mit Bewertung.    !!! WARNUNG — bitte vor dem Ausführen lesen !!!   - Nur lesend. Die Abfrage ändert keine Daten und keine Einstellung.   - Rechte: VIEW SERVER STATE (ab 2022 genügt für die meisten Punkte VIEW SERVER PERFORMANCE STATE);     ohne VIEW ANY DEFINITION meldet der tempdb-Punkt „nicht geprüft“.   - MAXDOP 1: Jede Teilabfrage läuft seriell und belegt keine weiteren Kerne.   - Wartezeiten zählen seit dem letzten Start oder seit dem letzten Leeren der Statistik.   - Bewertung „prüfen“ heißt: genauer hinsehen, nicht automatisch ändern. Richtwerte nach Microsoft     Learn (Linux-Performance-Seiten, Stand 05/2026) und PSG-Lab; sie ersetzen keine Analyse.   - Ab SQL Server 2017. Geprüft im PSG-Lab auf 2017, 2019, 2022 und 2025 in Linux-Containern,     als sa und ohne sysadmin (13.11.2026); unter Windows nicht geprüft (Linux-Punkte zeigen dort n/a).*/SET NOCOUNT ON; DECLARE @r TABLE (nr int, bereich nvarchar(20), pruefpunkt nvarchar(80), wert nvarchar(200),                  richtwert nvarchar(200), bewertung nvarchar(10), hinweis nvarchar(400));DECLARE @linux bit = 0, @cpu int, @sched int, @mem_mb bigint, @ziel_mb bigint, @modell nvarchar(60),        @maxmem bigint, @maxdop int, @ctp int, @sql nvarchar(max), @container nvarchar(60) = N'n/a'; IF OBJECT_ID(N'sys.dm_os_host_info') IS NOT NULL    SELECT @linux = CASE WHEN host_platform = N'Linux' THEN 1 ELSE 0 END FROM sys.dm_os_host_info OPTION (MAXDOP 1); SELECT @cpu = cpu_count, @mem_mb = physical_memory_kb / 1024, @ziel_mb = committed_target_kb / 1024,       @modell = sql_memory_model_descFROM sys.dm_os_sys_info OPTION (MAXDOP 1);IF EXISTS (SELECT 1 FROM sys.all_columns WHERE object_id = OBJECT_ID(N'sys.dm_os_sys_info') AND name = N'container_type_desc')    EXEC sys.sp_executesql N'SELECT @c = container_type_desc FROM sys.dm_os_sys_info OPTION (MAXDOP 1);', N'@c nvarchar(60) OUTPUT', @c = @container OUTPUT;SELECT @sched = COUNT(*) FROM sys.dm_os_schedulers WHERE status = N'VISIBLE ONLINE' OPTION (MAXDOP 1);SELECT @maxmem = CONVERT(bigint, value_in_use) FROM sys.configurations WHERE name = N'max server memory (MB)' OPTION (MAXDOP 1);SELECT @maxdop = CONVERT(int, value_in_use) FROM sys.configurations WHERE name = N'max degree of parallelism' OPTION (MAXDOP 1);SELECT @ctp = CONVERT(int, value_in_use) FROM sys.configurations WHERE name = N'cost threshold for parallelism' OPTION (MAXDOP 1); /* 1 · Schreiben und Flushen (nur Linux) */DECLARE @tf TABLE (TraceFlag int, Status int, Global int, Session int);BEGIN TRY    INSERT @tf EXEC (N'DBCC TRACESTATUS(3979, 3982, 8002, -1) WITH NO_INFOMSGS');END TRYBEGIN CATCHEND CATCH;INSERT @r SELECT 1, N'Flushen', N'Flush-Modus (Trace Flag 3979)',    CASE WHEN @linux = 0 THEN N'n/a (Windows)' WHEN NOT EXISTS (SELECT 1 FROM @tf) THEN N'nicht lesbar'         WHEN EXISTS (SELECT 1 FROM @tf WHERE TraceFlag = 3979 AND Global = 1) THEN N'FUA-Schreiben (3979 an)'         ELSE N'Forced Flush (Standard, 3979 aus)' END,    N'Forced Flush, außer der Speicher kann nachweislich FUA', N'info',    N'Forced Flush ist der Linux-Standard, auch wenn DBCC TRACESTATUS 3982 nicht als aktiv zeigt (PSG-Lab). 3979 nur mit FUA-fähigem Speicher und alternatewritethrough = 0.';INSERT @r SELECT 2, N'Flushen', N'writethrough / alternatewritethrough', N'in mssql.conf (SQL Server zeigt sie nicht)',    N'1 / 1 (Standard) oder 1 / 0 mit Trace Flag 3979', N'info',    N'Mit cat /var/opt/mssql/mssql.conf prüfen, Abschnitt [control]; fehlt er, gelten die Standardwerte. writethrough = 0 kann Daten kosten.'; /* 2 · Wartezeiten beim Schreiben (seit Start) */DECLARE @gesamt bigint, @flush bigint, @wl_ms bigint, @wl_n bigint;SELECT @gesamt = SUM(wait_time_ms) FROM sys.dm_os_wait_statsWHERE wait_type NOT LIKE N'SLEEP%' AND wait_type NOT LIKE N'BROKER%' AND wait_type NOT LIKE N'XE%'  AND wait_type NOT LIKE N'%QUEUE%' AND wait_type NOT LIKE N'HADR%' AND wait_type NOT LIKE N'SQLTRACE%'  AND wait_type NOT LIKE N'CLR%' AND wait_type NOT LIKE N'FT_%' AND wait_type NOT LIKE N'%IDLE%'  AND wait_type NOT IN (N'WAITFOR', N'CHECKPOINT_QUEUE', N'DIRTY_PAGE_POLL', N'DISPATCHER_QUEUE_SEMAPHORE',      N'LOGMGR_QUEUE', N'REQUEST_FOR_DEADLOCK_SEARCH', N'SP_SERVER_DIAGNOSTICS_SLEEP', N'QDS_ASYNC_QUEUE',      N'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP', N'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP', N'PWAIT_EXTENSIBILITY_CLEANUP_TASK',      N'SOS_WORK_DISPATCHER', N'PVS_PREALLOCATE', N'PREEMPTIVE_XE_DISPATCHER', N'PREEMPTIVE_OS_FLUSHFILEBUFFERS_IDLE')OPTION (MAXDOP 1);SELECT @flush = SUM(CASE WHEN wait_type = N'PREEMPTIVE_OS_FLUSHFILEBUFFERS' THEN wait_time_ms ELSE 0 END),       @wl_ms = SUM(CASE WHEN wait_type = N'WRITELOG' THEN wait_time_ms ELSE 0 END),       @wl_n  = SUM(CASE WHEN wait_type = N'WRITELOG' THEN waiting_tasks_count ELSE 0 END)FROM sys.dm_os_wait_stats OPTION (MAXDOP 1);INSERT @r SELECT 3, N'Flushen', N'Anteil PREEMPTIVE_OS_FLUSHFILEBUFFERS an den Wartezeiten',    CASE WHEN ISNULL(@gesamt, 0) = 0 THEN N'keine Wartezeiten'         ELSE REPLACE(CONVERT(nvarchar(20), CONVERT(decimal(5, 1), 100.0 * @flush / @gesamt)), N'.', N',') + N' %' END,    N'unter Linux oft weit oben, für sich allein kein Befund', N'info',    N'Entspricht dem fdatasync-Aufruf von SQL Server unter Linux (Paul Randal, Bob Dorr). Erst zusammen mit hohen WRITELOG-Zeiten ein Thema.';INSERT @r SELECT 4, N'Flushen', N'WRITELOG, mittlere Wartezeit je Commit-Warten',    CASE WHEN ISNULL(@wl_n, 0) = 0 THEN N'keine' ELSE REPLACE(CONVERT(nvarchar(20), CONVERT(decimal(9, 2), 1.0 * @wl_ms / @wl_n)), N'.', N',') + N' ms' END,    N'wenige Millisekunden', CASE WHEN @wl_n > 0 AND 1.0 * @wl_ms / @wl_n > 5 THEN N'prüfen' ELSE N'ok' END,    N'Hohe Werte: Log-Datenträger langsam, Log in der Container-Schreibschicht (overlayfs) oder sehr viele kleine Transaktionen.'; /* 3 · Speicher */INSERT @r SELECT 5, N'Speicher', N'max server memory gegen sichtbaren Speicher',    CONVERT(nvarchar(20), @maxmem) + N' MB von ' + CONVERT(nvarchar(20), @mem_mb) + N' MB',    N'gesetzt, unter dem sichtbaren Speicher, mit Kopfraum',    CASE WHEN @maxmem >= 2147483647 OR @maxmem > @mem_mb THEN N'prüfen' ELSE N'ok' END,    N'Unter Linux begrenzt memory.memorylimitmb (bzw. MSSQL_MEMORY_LIMIT_MB) den ganzen Prozess, max server memory nur den Puffer-Pool. Im Container 10–20 % Kopfraum zum Container-Limit lassen.';INSERT @r SELECT 6, N'Speicher', N'Speicherziel (committed_target)',    CONVERT(nvarchar(20), @ziel_mb) + N' MB', N'unter dem Container- oder Host-Limit', N'info',    N'2017 und 2019 sehen das cgroup-v2-Limit eines Containers nicht und planen mit dem Host-Speicher (PSG-Lab, Teil 3).';INSERT @r SELECT 7, N'Speicher', N'Speichermodell', @modell, N'CONVENTIONAL ist unter Linux normal', N'info',    N'Für „Lock Pages in Memory“ gibt es unter Linux keine dokumentierte Entsprechung.';DECLARE @tief int, @proz int;SELECT @tief = CONVERT(int, system_low_memory_signal_state) FROM sys.dm_os_sys_memory OPTION (MAXDOP 1);SELECT @proz = CONVERT(int, process_physical_memory_low) FROM sys.dm_os_process_memory OPTION (MAXDOP 1);INSERT @r SELECT 8, N'Speicher', N'Speicherdruck jetzt (System / Prozess)',    CONVERT(nvarchar(5), @tief) + N' / ' + CONVERT(nvarchar(5), @proz), N'0 / 0',    CASE WHEN @tief = 1 OR @proz = 1 THEN N'prüfen' ELSE N'ok' END,    N'1 heißt: SQL Server meldet gerade zu wenig Speicher. Bleibt es dauerhaft 1, Grenzen und speicherhungrige Abfragen prüfen.'; /* 4 · CPU, Scheduler, Parallelität */INSERT @r SELECT 9, N'CPU', N'CPUs und sichtbare Scheduler',    CONVERT(nvarchar(10), @cpu) + N' CPUs, ' + CONVERT(nvarchar(10), @sched) + N' Scheduler'      + CASE WHEN @container <> N'n/a' AND @container <> N'NONE' THEN N' · ' + @container ELSE N'' END,    N'so viele Scheduler, wie CPUs tatsächlich zur Verfügung stehen', N'info',    N'Eine CPU-Quote (docker --cpus) sieht SQL Server nicht. Gegenmittel laut Microsoft: PROCESS AFFINITY und unter cgroup v2 Trace Flag 8002.';INSERT @r SELECT 10, N'CPU', N'Trace Flag 8002 (weiche Prozessoraffinität)',    CASE WHEN @linux = 0 THEN N'n/a (Windows)' WHEN NOT EXISTS (SELECT 1 FROM @tf) THEN N'nicht lesbar'         WHEN EXISTS (SELECT 1 FROM @tf WHERE TraceFlag = 8002 AND Global = 1) THEN N'an' ELSE N'aus' END,    N'Microsoft: unter cgroup v2 zusammen mit PROCESS AFFINITY; vorher messen', N'info',    N'Ohne 8002 bindet PROCESS AFFINITY die Scheduler fest an einzelne CPUs. Im PSG-Lab (2019–2025, Quote 4 CPUs) brachte 8002 zur Affinität keinen messbaren Gewinn.';INSERT @r SELECT 11, N'CPU', N'max degree of parallelism', CONVERT(nvarchar(10), @maxdop),    N'höchstens 8 je NUMA-Knoten, nicht 0 bei mehr als 8 Schedulern',    CASE WHEN @maxdop = 0 AND @sched > 8 THEN N'prüfen' ELSE N'ok' END,    N'MAXDOP 0 nutzt so viele Threads, wie Scheduler sichtbar sind, auch wenn der Container weniger CPU bekommt.';INSERT @r SELECT 12, N'CPU', N'cost threshold for parallelism', CONVERT(nvarchar(10), @ctp),    N'nicht die Voreinstellung 5', CASE WHEN @ctp = 5 THEN N'prüfen' ELSE N'ok' END,    N'Die Voreinstellung 5 lässt schon sehr kleine Abfragen parallel laufen.'; /* 5 · tempdb */IF HAS_PERMS_BY_NAME(NULL, NULL, N'VIEW ANY DEFINITION') = 1BEGIN    DECLARE @n int, @gleich bit, @prozent int;    SELECT @n = COUNT(*), @gleich = CASE WHEN MIN(size) = MAX(size) AND MIN(growth) = MAX(growth) THEN 1 ELSE 0 END,           @prozent = MAX(CONVERT(int, is_percent_growth))    FROM sys.master_files WHERE database_id = 2 AND type = 0 OPTION (MAXDOP 1);    INSERT @r SELECT 13, N'tempdb', N'Datendateien',        CONVERT(nvarchar(10), @n) + N' Dateien, ' + CASE WHEN @gleich = 1 THEN N'gleich groß' ELSE N'unterschiedlich' END          + CASE WHEN @prozent = 1 THEN N', Wachstum in Prozent' ELSE N'' END,        N'so viele wie Scheduler, höchstens 8, gleich groß, festes Wachstum',        CASE WHEN @n < CASE WHEN @sched > 8 THEN 8 ELSE @sched END OR @gleich = 0 OR @prozent = 1 THEN N'prüfen' ELSE N'ok' END,        N'Ab 2019 legt eine Linux-Installation bis zu acht Dateien an (Microsoft); die Container-Images legen beim ersten Start eine je CPU an, höchstens acht (PSG-Lab; 2017: eine). Nicht bei einem In-place-Upgrade.';ENDELSE    INSERT @r SELECT 13, N'tempdb', N'Datendateien', N'nicht geprüft', N'', N'info', N'VIEW ANY DEFINITION fehlt (sys.master_files).'; SELECT nr, bereich, pruefpunkt, wert, richtwert, bewertung, hinweis FROM @r ORDER BY nr OPTION (MAXDOP 1);

Beispielausgabe

Aus dem Lab: SQL Server 2025 CU10 im Container mit 6 festen CPUs und 6 GB Grenze, als sa, nach einer kurzen Schreiblast (2.000 Einzel-Inserts).

Beispielausgabe
nr|bereich|pruefpunkt|wert|richtwert|bewertung|hinweis
--|-------|----------|----|---------|---------|-------
1|Flushen|Flush-Modus (Trace Flag 3979)|Forced Flush (Standard, 3979 aus)|Forced Flush, außer der Speicher kann nachweislich FUA|info|Forced Flush ist der Linux-Standard, auch wenn DBCC TRACESTATUS 3982 nicht als aktiv zeigt (PSG-Lab). 3979 nur mit FUA-fähigem Speicher und alternatewritethrough = 0.
2|Flushen|writethrough / alternatewritethrough|in mssql.conf (SQL Server zeigt sie nicht)|1 / 1 (Standard) oder 1 / 0 mit Trace Flag 3979|info|Mit cat /var/opt/mssql/mssql.conf prüfen, Abschnitt [control]; fehlt er, gelten die Standardwerte. writethrough = 0 kann Daten kosten.
3|Flushen|Anteil PREEMPTIVE_OS_FLUSHFILEBUFFERS an den Wartezeiten|22,9 %|unter Linux oft weit oben, für sich allein kein Befund|info|Entspricht dem fdatasync-Aufruf von SQL Server unter Linux (Paul Randal, Bob Dorr). Erst zusammen mit hohen WRITELOG-Zeiten ein Thema.
4|Flushen|WRITELOG, mittlere Wartezeit je Commit-Warten|0,56 ms|wenige Millisekunden|ok|Hohe Werte: Log-Datenträger langsam, Log in der Container-Schreibschicht (overlayfs) oder sehr viele kleine Transaktionen.
5|Speicher|max server memory gegen sichtbaren Speicher|2147483647 MB von 4916 MB|gesetzt, unter dem sichtbaren Speicher, mit Kopfraum|prüfen|Unter Linux begrenzt memory.memorylimitmb (bzw. MSSQL_MEMORY_LIMIT_MB) den ganzen Prozess, max server memory nur den Puffer-Pool. Im Container 10–20 % Kopfraum zum Container-Limit lassen.
6|Speicher|Speicherziel (committed_target)|4267 MB|unter dem Container- oder Host-Limit|info|2017 und 2019 sehen das cgroup-v2-Limit eines Containers nicht und planen mit dem Host-Speicher (PSG-Lab, Teil 3).
7|Speicher|Speichermodell|CONVENTIONAL|CONVENTIONAL ist unter Linux normal|info|Für „Lock Pages in Memory“ gibt es unter Linux keine dokumentierte Entsprechung.
8|Speicher|Speicherdruck jetzt (System / Prozess)|0 / 0|0 / 0|ok|1 heißt: SQL Server meldet gerade zu wenig Speicher. Bleibt es dauerhaft 1, Grenzen und speicherhungrige Abfragen prüfen.
9|CPU|CPUs und sichtbare Scheduler|6 CPUs, 6 Scheduler · LINUX CONTAINER|so viele Scheduler, wie CPUs tatsächlich zur Verfügung stehen|info|Eine CPU-Quote (docker --cpus) sieht SQL Server nicht. Gegenmittel laut Microsoft: PROCESS AFFINITY und unter cgroup v2 Trace Flag 8002.
10|CPU|Trace Flag 8002 (weiche Prozessoraffinität)|aus|Microsoft: unter cgroup v2 zusammen mit PROCESS AFFINITY; vorher messen|info|Ohne 8002 bindet PROCESS AFFINITY die Scheduler fest an einzelne CPUs. Im PSG-Lab (2019–2025, Quote 4 CPUs) brachte 8002 zur Affinität keinen messbaren Gewinn.
11|CPU|max degree of parallelism|0|höchstens 8 je NUMA-Knoten, nicht 0 bei mehr als 8 Schedulern|ok|MAXDOP 0 nutzt so viele Threads, wie Scheduler sichtbar sind, auch wenn der Container weniger CPU bekommt.
12|CPU|cost threshold for parallelism|5|nicht die Voreinstellung 5|prüfen|Die Voreinstellung 5 lässt schon sehr kleine Abfragen parallel laufen.
13|tempdb|Datendateien|6 Dateien, gleich groß|so viele wie Scheduler, höchstens 8, gleich groß, festes Wachstum|ok|Ab 2019 legt eine Linux-Installation bis zu acht Dateien an (Microsoft); die Container-Images legen beim ersten Start eine je CPU an, höchstens acht (PSG-Lab; 2017: eine). Nicht bei einem In-place-Upgrade.

Methode und Grenzen

SQL Server 2017 CU31, 2019 CU32, 2022 CU27 und 2025 CU10 (Docker-Images von Microsoft), Hauptlauf am 13.11.2026; je Variante ein frischer Container. Gemessen in Linux-Containern auf einer Ubuntu-24.04-VM unter Hyper-V (24 vCPU, 2 NUMA-Knoten, ext4 auf virtuellen Datenträgern einer SATA-SSD, Datenträger ohne FUA, kein Swap, Docker mit overlayfs und cgroup v2). Grenzen:

  • Keine Änderung am Host: Die Wirkung von THP, swappiness, NUMA-Balancing, Read-ahead, TuneD, noatime und XFS ist nicht gemessen; dafür gelten Microsofts Empfehlungen.
  • Container statt Installation: Voreinstellungen einer Paketinstallation (mssql-conf setup) haben wir nicht geprüft.
  • Virtuelle Datenträger ohne FUA: Der FUA-Weg ist hier nur als Messwert zu verstehen.
  • Synthetische Lasten, je Fall wenige Läufe; Zeiten zeigen Größenordnungen.

Weiterlesen

Sie betreiben SQL Server unter Linux?

Drei Dinge zuerst: Lassen Sie den Host-Check laufen und entscheiden Sie bewusst, welche Abweichungen Sie behalten. Prüfen Sie mit Abfrage 4 Speichergrenzen, Scheduler und tempdb. Und schauen Sie, bevor Sie am Flushen drehen, ob Ihr Speicher FUA überhaupt kann.

Weitere Teile der Reihe