Een medewerker op de spoedeisende hulp wacht ‘s nachts telkens bijna een minuut op een scherm. Een werkplek verderop laadt het in 1 seconde. Hoe kan dat? We ontdekten iets dat onzichtbaar is op CPU-dashboards en dat ongetwijfeld in veel meer ziekenhuizen ’s nachts traagheid in het EPD veroorzaakt. 

In deze case gaan we serieus de diepte in. Wil je alleen globaal weten wat het probleem was? Dan is deel 1 voor jou. Wil je dieper kijken? Duik ook in deel 2. Zit je klaar om dit op te lossen in jouw ziekenhuis, dan presenteren wij: deel 3.

 

Deel 1: stel, je hebt nachtdienst op de SEH

Je werkt op de spoedeisende hulp-afdeling van een ziekenhuis. Het is zaterdagnacht, 02:00 uur: topdrukte. Dan wil je continu kunnen zien of er ambulances naar je op weg zijn met patiënten in kritieke toestand. En voor de patiënten die in het ziekenhuis moeten blijven, wil je snel een bed kunnen zoeken op andere afdelingen. 

Hiervoor heb je één scherm nodig: het bedbezettingsoverzicht in het elektronisch patiëntendossier. Je opent het… Niets. Je wacht 5 seconden. Dan 20 seconden. Ondertussen vraagt een van de artsen waar haar volgende patiënt ligt. Welk scherm je daarvoor nodig hebt? Inderdaad. Het bedbezettingsoverzicht. En het verschijnt nog steeds niet. Uiteindelijk, 53 seconden later zie je dat cruciale scherm. En dit gebeurt elke keer opnieuw. 

Je kijkt intussen bij je collega en staat versteld: bij hem gaat het razendsnel. Dezelfde dienst, hetzelfde overzicht; vrijwel direct staat het op het scherm. Jij wacht telkens bijna een minuut. 

Als dat urenlang gebeurt, dan verlies je heel veel kostbare tijd. Je wilt nú weten of die patiënt die op hoge snelheid onderweg is straks direct een bed heeft.  

Edward Dortland, database consultant en medeoprichter van Twintos

Dit zijn precies de performanceproblemen waar we bij Twintos warm voor lopen. Alles lijkt oké en toch is het niet oké.

Edward Dortland, medeoprichter Twintos

De IT-afdeling ziet niets geks

Later bel je de IT-afdeling. Die ziet dit probleem niet terug. Het monitoringdashboard kleurt groen; de server draait prima. Dit probleem is niet-reproduceerbaar en dus niet-oplosbaar. Conclusie: jij of een andere medewerker blijft hier tegenaan lopen.

En zo kwamen wij in het verhaal, samen met Qaddie

Zo ging het ongeveer bij het IJsselland Ziekenhuis. De IT-afdeling meldde het probleem bij onze samenwerkingspartner Qaddie. Met deze specialist in EPD’s zoals ChipSoft HiX hebben we de krachten gebundeld in de EPD Boost. Onze specialismen vullen elkaar exact aan, waardoor we onverklaarbare vertragingen in het elektronisch patiëntendossier opsporen en oplossen.

Edward Dortland is database performance-specialist en mede-oprichter van Twintos. Hij vertelt hoe we begonnen aan dit project. Edward: ‘Lennard ‘de EPD-tovenaar’ Spanjer van Qaddie spoorde de gebruikers op die, bij dezelfde actie, langere doorlooptijden hadden dan hun collega’s. Hij vermoedde dat de database er iets mee te maken had en belde ons.’

Waarom is hetzelfde scherm soms 40 keer trager?

Edward: ‘de supportmelding sloeg, zoals zo vaak op het eerste gezicht, nergens op. Bij de ene medewerker is de totale duur 1.313 milliseconden. Bij de andere meting duurt alleen al één enkele query 53.403 milliseconden: bijna een minuut.’

Meting van de snelle gebruiker: totale duur 1.313 milliseconden
Gebruiker X. De totale duur is maar 1.313 milliseconden, waarvan 1.000 milliseconden CPU-tijd en 206 milliseconden query-tijd.
Meting van de trage gebruiker: één query duurt 53.403 milliseconden
Gebruiker Y. We zoomen in op de database query van deze gebruiker en alleen die is al 53.403 milliseconden.

 

Edward: ‘Dit zijn precies de performanceproblemen waar we bij Twintos warm voor lopen. Alles lijkt oké en toch is het niet oké.’ 

Aan het queryplan ligt het niet, want bij gebruiker X werkt het wel snel. En die gebruikt hetzelfde plan, met dezelfde parameter waardes in de query. Geen time-outs. En de server? Die verveelt zich; de totale CPU-belasting is laag, want het is midden in de nacht.  

Hoe onderzoek je een onzichtbaar probleem?

Eén ding weten we wel – dit speelt alleen ’s nachts. Waarom is SQL Server juist ‘s nachts traag? En wat draait er ’s nachts op vrijwel elke SQL Server? Inderdaad: onderhoud op de tabellen, bedoeld om de database snel te houden. Dat is dan ook onze eerste verdachte.  

Toch lijkt ons dat niet logisch. We begrenzen namelijk het onderhoud altijd, zodat het de machine niet helemaal opeist. En dat zien we ook: het onderhoud gebruikt maar een fractie van de server. 

Toch blijft er iets knagen. Edward: ‘onze onderbuik zei dat het net té toevallig was, dat gebruikers er last van hadden tijdens het onderhoud.’  

Hypothese 1: staat de data even op slot?

Ons eerste vermoeden is locking, vertelt Edward: ‘als het voor onderhoud nodig is om een nieuwe index te bouwen, gaat de data waar die index op gebaseerd is soms ‘op slot’. SQL Server voorkomt daarmee dat een gebruiker data leest die op het moment van lezen aan het veranderen is.’ 

In principe richten we het onderhoud zo in dat dit niet gebeurt. Zit hier misschien een fout in?  We duiken in de logbestanden en zien geen langdurende locks op de momenten dat een gebruiker lang wacht. We richten zeer gedetailleerde logging in. Nog steeds vinden we hier geen bewijs voor. 

Hypothese 2: of is het de opslag naar de schijven?

Onze tweede hypothese is dat de storage – de opslag naar fysieke schijven – soms overbelast raakt, of het pad ernaartoe.  

Edward: ‘tabelonderhoud (zoals het opnieuw opbouwen van een index, of het scannen van een tabel om nieuwe statistieken te berekenen) is heel intensief voor de onderliggende storage-laag.’  

We onderzoeken alle stappen op het pad naar de schijven: SSD’s, storage bus, driverproblemen en alle gerelateerde logs. Niets bijzonders te zien. We voeren nog een storage stress-test uit en ook die slaagt met vlag en wimpel.  

Hypothese 3: virusscanners, ook klassieke vertragers

Veel servers hebben software ter bescherming tegen virussen, malware en soms ook hacking. Goede zaak natuurlijk, maar, vertelt Edward: ‘deze endpoint protection software is een notoire performance-killer die onder hoge belasting de meest vage, vaak vluchtige performance-problemen kan veroorzaken.’   

We hadden wel verwacht dat de storage stress-test dan ook al een probleem had gegeven. Toch vinden we dat we dit verder moeten onderzoeken. We analyseren de filterdrivers, de tussenlagen die al het schijfverkeer scannen, met een debugger. Maar op het moment dat er een nieuwe vertraging ontstaat voor een gebruiker, zien we in de filterdrivers geen problemen. 

Hypothese 4: toch te weinig vrije cores?

Oké, we zijn ervan overtuigd dat het niet de opslagkant is. Maar wat dan wel? Tabelonderhoud is niet alleen zwaar voor de opslag, maar ook voor de processor. Echter: we hebben heel bewust ingeregeld dat het onderhoud maar 4 cores op de processor mag gebruiken, van de in totaal 24 cores.  Die paar gebruikers die ’s nachts iets doen, hebben dus aan 20 cores ruim voldoende. Toch willen we dit zeker weten. 

Zoom in. Enhance. Enhance… Freeze.

We richten monitoring in om nog dieper in het processorgebruik te duiken. Elke 5 seconden legt de monitoring vast wat er tijdens het nachtelijke onderhoudsvenster gebeurt. We loggen van iedere sessie tot op de nanoseconde wanneer ze wat doen, op welke processor-core en hoelang ze tussendoor wachten.  

Bingo.

We meten een paar nachten, en dan valt er iets op. Bij de trage gebruikers registreert SQL Server steeds hetzelfde type wachttijd. De zogenaamde workers, die data voor de gebruiker ophalen, wachten een groot deel van de tijd om een plek te krijgen op de processor om te mogen rekenen.  

Dát is apart. Het is midden in de nacht, er zijn welgeteld 3 mensen aan het werk op een razendsnelle server met 24 plekken en maar 4 zijn er in gebruik voor het onderhoud. Hoezo moet deze gebruiker wachten?  

We kijken wat er op die momenten staat ingepland op deze plek. En wat gebeurt daar tijdens elke trage sessie? Nachtelijk database-onderhoud. Het kan dus inderdaad dat één medewerker last heeft van database-onderhoud.  

Wil je weten waarom dat gebeurt en hoe je dit oplost? Edward legt het hieronder uit. Ben je niet iemand die dagelijks in de IT duikt en wil je gewoon weten of er een oplossing is voor dit probleem?  

Ja, dit is heel goed op te lossen. We hebben zelfs al een werkend script klaarliggen. Dus neem gerust contact op, voor deze en andere database-mysteries die bij jullie de operatie ondermijnen 

Bel Twintos +31 85 1302910 of mail Edward Dortland 

 

Deel 2: duik onder de motorkap van besturingssysteem SQLOS  

Kun je wel wat database-jargon hebben? Dan is dit deel voor jou. Edward neemt je nu mee onder de motorkap van SQLOS, het besturingssysteem van SQL Server.  

Take it away, Edward

Je kunt de manier waarop SQL Server de processor-capaciteit verdeelt vergelijken met een atletiekbaan. Wil je iets op de processor, dan krijg je een baan toegewezen. Ben je klaar, dan ga je van de baan af. Er kan maar één iemand tegelijk rennen per baan, maar er kunnen wel meerdere mensen aan diezelfde baan toegewezen zijn. Die wachten dan aan de kant tot de baan vrij is.  

Ruim baan voor… de borstelwagen

In de nacht rent er bijna niemand, dus dan is het tijd voor onderhoud op de tabellen. Dat kun je zien als een borstelwagen. SQL Server beschouwt die gewoon als één loper op de baan. En SQL Server zegt dan niet: ik gooi die borstelwagen op een lege baan. SQL Server redeneert: ik zie dat er wel 2 mensen zijn toegewezen aan die baan maar ik zie niemand rennen, dus het is hartstikke rustig. De borstelwagen en de lopers kunnen prima op één baan.  

Hoe SQL Server de sessies verdeelt

Nou is het probleem niet dat de onderhoudstaak teveel capaciteit verbruikt. Het gaat om de manier waarop SQL Server de sessies verdeelt over de banen. En dat is iets waar zelden iemand bij stilstaat.  

Welkom in SQL Server internals

SQL Server draait zijn eigen besturingssysteem binnen dat van Windows: SQLOS. Elke CPU-core heeft één scheduler, dat is de baan uit de atletiek-analogie. Op elke scheduler draait op elk moment precies één worker, dat is de loper.  

Iedere worker krijgt maximaal 4 ms de tijd om achter elkaar op een scheduler te mogen draaien. Dit heet zijn ‘quantum’. De worker is ofwel eerder klaar met zijn werk, of geeft vrijwillig de beurt op als het quantum is verstreken. Dat heet cooperative scheduling. De worker sluit weer achteraan in de runnable queue van diezelfde scheduler, om er straks weer op te mogen. Dit mechanisme bestaat om te zorgen dat wanneer meerdere gebruikers aan één scheduler zijn toegewezen, hun workers om beurten CPU-tijd kunnen krijgen. 

Dit proces herhaalt zich tot de worker klaar is. Moet een worker tussentijds wachten op een resource, bijvoorbeeld op data van disk, een lock op een record door een andere gebruiker of een stukje geheugen, dan maakt hij ook direct plaats op de scheduler. Dan is een andere worker aan de beurt.  

En wat voor deze case cruciaal is om te onthouden: een worker komt in principe steeds weer op zijn eigen scheduler terecht. Een taak verhuist niet zomaar naar een lege scheduler ernaast. Elke sessie krijgt bij het verbinden een preferred scheduler toegewezen. Zolang de connectie van die sessie intact is, houdt de sessie dezelfde preferred scheduler.  

Waarom je juist bij EPD’s extra lang op dezelfde baan blijft

Nou is het zo dat applicaties als HiX en vrijwel elk ander modern EPD werken met connection pooling. Dat houdt in dat ze een setje databaseverbindingen openhouden en hergebruiken. Dat is sneller dan voor elke handeling opnieuw een verbinding opzetten. Maar wat betekent dat voor dit probleem: de verbinding blijft lang in stand, en de sessie blijft daardoor urenlang op dezelfde scheduler.  

Even over terminologie, want de woorden worker en thread lopen in de praktijk door elkaar. In SQLOS-taal is een worker de uitvoerder van een taak, en elke worker is gekoppeld aan een thread van het besturingssysteem. Voor dit verhaal zijn ze dus vrijwel inwisselbaar; wij houden het op worker.

Zo kiest SQLOS een baan voor elke taak

De gebruiker heeft dus voor zijn sessie een preferred scheduler gekregen. Nou heeft SQLOS wel een maatstaf om te bepalen of een scheduler overbelast raakt: de load factor. Bij de start van elke query bekijkt het systeem hoe druk het is op de schedulers.  

SQLOS telt hierbij niet het aantal sessies per scheduler, maar het aantal taken dat op dat moment actief draait op een scheduler. En wat zo frustrerend is voor die ene verpleegkundige die de baan deelt met het onderhoud: elke taak telt even zwaar.  

 

Edward Dortland, database consultant en medeoprichter van Twintos

Een index met 400 miljoen rijen of een bedbezettingsoverzicht van 15 records: voor SQLOS telt het even zwaar.

Edward Dortland, medeoprichter Twintos

SQLOS kijkt niet hoeveel CPU de taak verbruikt. Zelfs niet als de update statistics-worker de statistieken bijwerkt van een index met, zeg, 400 miljoen rijen. Probeer dan maar eens het bedbezettingsoverzicht voor je afdeling op te halen, met 15 records.  

Nu denk je misschien: maar we wisselen elkaar toch telkens af op de baan? Klopt. Maar er is een belangrijk verschil tussen jouw sessie en die van de onderhoudsworker.  

Hoe jouw sessie ‘s nachts de loterij wint

Een update van de statistieken met MAXDOP 4 zet 4 workers aan het werk die vrijwel non-stop CPU verbruiken. Die 4 workers landen op 4 schedulers en maken daar telkens hun quantum vol en gaan er dan weer af.  

Dat klinkt heel coöperatief. Maar als jij, als gebruiker, ook op die scheduler zit, voelt dat heel anders. Want jouw query doet veel random reads en bestaat uit duizenden kleine CPU-stapjes, een index seek is in een fractie van die 4 ms klaar. En tussen die stapjes door moet je telkens wachten: op een pagina van disk, op een latch, een lock, een netwerk-roundtrip. Elke keer dat jij moet wachten, ga je vrijwillig van de scheduler af, vaak al na een minieme fractie van je quantum (van 4 ms). 

Waarom onderhoudstaken niet hoeven te wachten en jij wel

Hier zit de scheefgroei. De update statistics-worker leest de data sequentieel, waardoor de volgende pagina’s (mede dankzij read-ahead) vrijwel altijd al klaarstaan. Hij hoeft dus zelden op een resource te wachten en stoomt van stapje naar stapje door tot zijn quantum van 4 ms op is. En op jou hoeft hij al helemaal niet te wachten: jouw worker staat immers meestal aan de kant, wachtend op een resource. 

Voor jou werkt het precies andersom. Iedere keer dat jouw wachten voorbij is (de pagina is binnen, de latch is vrij), ben je nog niet aan de beurt: eerst maakt de update statistics-worker zijn volle 4 ms af. En omdat jouw query heel vaak héél even CPU nodig heeft, herhaal je dit duizenden keren. Elk stapje van een fractie van een milliseconde kost je zo tot 4 ms extra wachttijd. Opgeteld brengt jouw worker bijna al zijn tijd door met wachten op het onderhoud.  

Stel nu dat de server 24 cores heeft. Dan zijn er ’s nachts 20 schedulers waarop helemaal niets gebeurt, en 4 schedulers waarop een update statistics-worker staat te stampen. 

De verpleegkundige met het snelle scherm? Die sessie zat op één van de 20 rustige schedulers. Jij had pech: jouw sessie zat op een scheduler samen met een update statistics-worker. Jij moest na elk wachtmoment opnieuw achteraan aansluiten voor 4 ms, terwijl de onderhoudsworker vrijwel nooit voor jou opzij hoefde. 

SOS_SCHEDULER_YIELD, oftewel: een rotnacht

Daarom zie je telkens 53 seconden lang een leeg scherm: 200 milliseconden rekentijd en wel 52,8 seconden wachten. Onwerkbaar, als er ondertussen patiënten in kritieke toestand naar je op weg zijn. In de SQL DMV heet dat SOS_SCHEDULER_YIELD, en in de praktijk heet dat een rotnacht. 

Waarom dit probleem niet-reproduceerbaar is

Dit verklaart meteen waarom dit probleem zo grillig is. Het is letterlijk een loterij. Welke scheduler je connectie toevallig treft, bepaalt of je nacht soepel verloopt of tergend traag. En omdat CPU-dashboards gemiddelden over alle cores tonen, is er op serverniveau niets te zien. Van de 24 schedulers hebben er 20 het rustig, dus alles kleurt groen. 

Overdag lost het probleem zichzelf op

Je weet nu ook waarom dit probleem alleen ‘s nachts voorkomt. Overdag starten en eindigen er per scheduler honderden taken per seconde. Dan werkt het algoritme dat de load factor bepaalt op basis van het aantal actieve taken perfect. Elke nieuwe batch is een nieuw plaatsingsmoment. En al die plaatsingsmomenten samen herverdelen de load doorlopend vanzelf over alle schedulers. SQLOS is dus overdag continu aan het balanceren.  

Het zelfherstellend vermogen staat ‘s nachts uit

’s Nachts valt dat mechanisme stil: er gebeurt maar af en toe iets in het EPD. Er zijn dus nauwelijks gelijktijdige actieve taken waardoor er niets op een andere scheduler herplaatst hoeft te worden. Het zelfherstellend vermogen van SQLOS staat ’s nachts dus effectief uit. Precies op het moment dat het onderhoud begint. 

Je hebt dus én geen automatische herverdeling én de load factor herkent één stampende update statistics-worker niet als druk. Je hebt, kortom, een probleem. 

Dit treft juist de ziekenhuismedewerkers die kritiek werk doen

Laten we ook even stilstaan bij wie hier in de praktijk mee te maken krijgt. Wie ’s nachts intensief in het EPD werkt, zit op de spoedeisende hulp, de intensive care of een verpleegafdeling met een drukke dienst. Precies op de plekken waar elke seconde telt. 

En de collega één werkplek verderop merkt helemaal niets. Daardoor lijkt de melding voor de servicedesk des te ongeloofwaardiger, of in ieder geval ontzettend moeilijk te debuggen.  

Wij vermoeden dat dit in veel meer ziekenhuizen speelt

Dit kan geen exotisch randgeval zijn van één ziekenhuis. De factoren zijn overal hetzelfde: nachtelijk indexonderhoud, connection pooling en ’s nachts weinig maar wél kritieke gebruikers. Dat is de standaardconfiguratie van vrijwel elk ziekenhuis. En als je de SQL Server-performance van een ziekenhuis alleen op serviceniveau zou monitoren, dan zie je dit patroon nooit. De kans is dus groot dat dit patroon op veel meer plekken speelt. 

Tijd om bewijs te verzamelen

Nu willen we onze hypothese bevestigd krijgen. We gaan nog dieper kijken. Elke afzonderlijke baan, elke loper en elk wachtmoment. Daarom bouwen we een stored procedure die elke 10 seconden een complete foto van de atletiekbaan maakt, op 4 niveaus tegelijk. Elk niveau schrijven we weg naar een eigen snapshot-tabel. Speel je die achter elkaar af, dan zie je de nacht zich ontvouwen: wie er op welke baan staat, wie er rent, wie er wacht – en waarop. 

Niveau 1: de banen zelf in beeld brengen

We kijken eerst naar de banen zelf: hoeveel taken staan er per scheduler te wachten om te mogen rennen, en hoe druk vindt SQLOS het zelf? 

Query-uitvoer met per scheduler de load factor en het aantal wachtende taken
Niveau 1: een algemeen beeld van de schedulers, de load factor, aantal runnable taken.

 

Niveau 2: wie rent er, en waar

Van elke actieve request willen we weten op welke scheduler hij draait, in welke staat, en waarop hij het laatst wachtte. Dat levert dit beeld op: 

Query-uitvoer met elke actieve request, zijn scheduler, status en laatste wachttype
Niveau 2: elke actieve request, met zijn scheduler, status en laatste wachttype.

 

Niveau 3: de parallelle workers in beeld brengen

Niveau 2 toont per query alleen de coördinator (exec_context_id=0). Maar een statistics-update met MAXDOP 4 zet daarnaast 4 parallelle workers aan het werk. En juist díe bezetten 4 schedulers. Vandaar dat we ook per task willen loggen op welke scheduler ze draaien: 

Query-uitvoer die de parallelle onderhoudsworkers toont, elk op hun eigen scheduler
Niveau 3: hier zie je ook de parallelle onderhoudsworkers, elk op hun eigen scheduler.

 

Niveau 4: elk wachtmoment

Tot slot: wie wacht waarop, hoe lang, en wie blokkeert hem? Dit niveau dient ook als controle: als de trage sessie tóch op een lock, latch of IO zou wachten, kunnen we dat hier meteen zien. 

Query-uitvoer met per sessie het wachttype, de wachtduur en de blokkerende taak
NIveau 4: per sessie het wachttype, de wachtduur en de blokkerende taak.

 

We maken een SQL-job die tijdens het onderhoudsvenster iedere 10 seconden de output van deze 4 queries in een tabel insert. Vervolgens bekijken we met de volgende query op welke momenten er HiXgebruikers op dezelfde scheduler zaten én aan het wachten waren op een plekje op de scheduler. 

SQL-query die opspoort wanneer een HiX-gebruiker en het onderhoud dezelfde scheduler delen
De query die de momenten opspoort waarop een HiX-gebruiker en het onderhoud dezelfde scheduler delen.

 

En dan zie je precies waarom die ene gebruiker bizar lang moet wachten: 

Resultaat met sessie 424 die 90 seconden op scheduler 11 wacht naast een UPDATE STATISTICS-taak
Het bewijs: session_id 424 zit 90 seconden op scheduler 11, naast een UPDATE STATISTICS taak.

 

Dit is precies wat we zochten. De gebruiker met session_id 424, staat 90 seconden achter elkaar (9 snapshots om de 10 seconden) met zijn SELECT statements te wachten op een UPDATE STATISTICS taak omdat ze allebei op scheduler 11 zitten.

 

Deel 3: de oplossing  

De reflex is vaak: MAXDOP van het onderhoud verlagen naar 1. Dat verkleint de kans op een conflict, maar neemt die kans niet weg. En het maakt het voor de pechvogel juist nog erger. Het onderhoud duurt met één worker 4 keer zo lang. Dus die ene sessie die via connection pooling op diezelfde scheduler vastzit, heeft niet uren last, maar potentieel de hele nacht. Je hebt 4 prijzen in de loterij vervangen door één hoofdprijs.  

Nerds die tot het gaatje gaan

We zouden hier de standaard functionaliteit van Resource Governor voor kunnen gebruiken. Resource Governor maakt het mogelijk om resources zoals CPU en Memory in pools op te delen en queries op basis van een zogenaamde classificatiefunctie aan zo’n resourcepool toe te kennen.  

Nu kun je denken: prima, ik maak een pool aan voor al mijn database-onderhoud en richt het zo in dat SQL Server de pool begrenst.  Dat zou het al beter maken. Maar bij Twintos werken nerds die het probleem écht willen oplossen. 

Wat is er mis met ‘gewoon throttlen?’

Je zou kunnen kiezen voor throttling. Je kunt bijvoorbeeld een pool maken voor onderhoud en een cap inrichten op maximaal 25%. Maar throttling op een resource pool verdunt het probleem, het neemt het niet weg. De cap verandert namelijk niets aan het quantum van 4 ms. Wat de cap wel doet, is de bursts van de onderhoudsworker uit elkaar trekken: SQLOS parkeert de worker gedwongen zodra de pool boven zijn cap komt. Bij een CPU-cap van 50% ontstaat grofweg een patroon van 4 ms draaien, 4 ms gedwongen stil. Tijdens die stilte is de scheduler vrij en komt de gebruikersquery direct aan de beurt. 

De 53 seconden uit onze meting bestonden, je weet het inmiddels, uit duizenden wachtmomenten op het verlopen van het quantum van de update statistics onderhoudsjob. De rekentijd is maximaal 4 ms. Terugrekenend gaat het om ruwweg 13.000 cycli. 

Hoeveel een cap oplevert

Met een cap wordt de verwachte wachttijd per cyclus: de kans dat de worker net aan het draaien is (gelijk aan de cap) maal de gemiddelde resterende burstduur (een half quantum, 2 ms). Dat invullen geeft: 

CAP_CPU_PERCENT  Wachttijd per cyclus  Query van 53 s wordt  Duur onderhoud 
geen cap  ~4 ms  53 s  1x 
50%  ~1 ms  ~13 s  ~2x 
25%  ~0,5 ms  ~6,5 s  ~4x 
10%  ~0,2 ms  ~2,6 s  ~10x 

Oplossen is beter dan uitsmeren

In de tabel vallen 2 dingen op. Ten eerste: zelfs bij een stevige cap van 50% wacht de gebruiker nog steeds 13 seconden. Om in de buurt van acceptabel te komen, moet de cap zo laag staan dat het onderhoud er zelf onder lijdt.   

En dat is het tweede punt, de kolom rechts: de hinder per query schaalt met de cap, maar de duur van het onderhoud schaalt er omgekeerd mee. Bij 50% heeft elke getroffen query half zoveel last, maar het venster duurt 2 keer zo lang, dus treft het onderhoud ook 2 keer zoveel query’s. De gebruiker heeft dus langer last, maar minder heftig per query. Throttling smeert de pijn uit. 

Dit model is een benadering

De governor dwingt de cap af als gemiddelde over een kort venster, niet quantum-voor-quantum, dus in de praktijk zijn de uitschieters grilliger dan de tabel suggereert. Maar de richting en de ordegrootte kloppen. 

Nifty trucs met affinity

Wat werkt wel? CPU-affinity instellen. Je dwingt af dat een resource pool bepaalde CPU-cores gebruikt. Maar het is niet genoeg om met scheduler affinity alleen het onderhoud in een eigen resource pool te zetten. De EPD-gebruikers komen binnen op de DEFAULT pool, en die mag standaard op alle schedulers draaien. Dan kunnen verpleegkundigen alsnog op dezelfde scheduler als het onderhoud belanden.  

Scheduler affinity voor zowel gebruikers als onderhoud

Alleen het onderhoud afschermen is dus niet genoeg. De truc is om de affinity in op beide pools in te stellen. Het onderhoud krijgt zijn eigen schedulers. En tijdens het onderhoudsvenster beperken we de default pool tijdelijk tot de overige schedulers.  

— Vooraf, in de onderhoudsjob 

ALTER RESOURCE POOL [maintenance] WITH (AFFINITY SCHEDULER = (20 TO 23)); 

ALTER RESOURCE POOL [default]     WITH (AFFINITY SCHEDULER = (0 TO 19)); 

ALTER RESOURCE GOVERNOR RECONFIGURE;    

  

— … indexonderhoud …  

— Achteraf, ook als de job faalt  

ALTER RESOURCE POOL [default] WITH (AFFINITY SCHEDULER = AUTO);  

ALTER RESOURCE GOVERNOR RECONFIGURE;  

Daarmee kán een gebruikerssessie simpelweg niet landen op een scheduler waar het onderhoud draait.  

 

De risico’s van de affinity-aanpak en hoe je die oplost

Je hebt met deze aanpak ook een paar risico’s. Je wijzigt immers de beschikbare resources voor alle gebruikers. 

Risico 1: het mislukte-reset-scenario

Dit is een echt risico. Je perkt de default pool bewust in, en die móet je daarna terugdraaien. Faalt de job halverwege, crasht de server, of wordt de job hard gekild, dan draait de ochtendpiek van het hele ziekenhuis op verminderde capaciteit. Dan heb je juist een groter probleem.  

Vang dit dubbel af: een TRY/CATCH die in het foutpad de affinity van de default pool altijd terugzet naar AUTO, plus een aparte vangnetjob die ’s ochtends vroeg onvoorwaardelijk hetzelfde doet, onafhankelijk van hoe de nacht is verlopen. Je ontkomt er niet aan dat je hier goede monitoring in place moet hebben. Maar dat kan al zo simpel zijn als een e-mailtje dat je iedere ochtend vanaf 6 uur ieder kwartier krijgt zolang je default pool niet op AFFINITY = AUTO staat.    

Risico 2: verkeerd verdeelde banen kosten geheugensnelheid

Op grotere servers heeft elke NUMA-node zijn eigen lokale geheugen, dat sneller is dan het geheugen van een andere node. Verdeel je een pool per ongeluk over twee nodes, dan draait die dus deels op traag, remote geheugen. 

Wijs schedulers daarom toe langs nodegrenzen. Bouw de toewijzing dynamisch op uit sys.dm_os_schedulers in plaats van nummers te hardcoden. Zeker als hetzelfde script op servers met verschillende topologieën moet draaien.  

Risico 3: alleen nieuwe taken verhuizen

De reconfigure verplaatst geen lopende batches. Een query die al draait op een maintenance-scheduler, maakt zijn batch daar af en verhuist pas bij de volgende. Plan dus een paar minuten marge tussen het omzetten van de pools en de start van het zwaarste onderhoud. 

Risico 4: CPU is niet het hele verhaal

Schedulers scheiden lost alleen CPU-contention op. Zit de bottleneck ergens anders, dan blijft je database traag met deze oplossing. Een statistiekenupdate met fullscan leest complete tabellen en duwt, net als bij een index rebuild, zijn eigen pagina’s de buffer pool in: het is dus ook een IO-kanon. 

Had de trage query op IO gewacht in plaats van op de scheduler, dan had helpt deze oplossing niet. Controleer dus eerst met wait statistics wáár de tijd zit voordat je de oplossing kiest. In onze case wees SOS_SCHEDULER_YIELD ondubbelzinnig naar de scheduler.  

De lessen

Waarom is het EPD bij de ene gebruiker traag en bij de andere snel?

Moderne applicaties houden database-verbindingen lang vast. SQL Server koppelt elke verbinding aan één vaste scheduler. Draait daar ‘s nachts toevallig ook het database-onderhoud, dan concurreert die ene gebruiker de hele dienst met dat onderhoud. Collega’s hebben ondertussen nergens last van. De oplossing: geef onderhoud en gebruikers fysiek gescheiden schedulers via de Resource Governor. 

Gemiddelden kunnen liegen

Een server die op totaalniveau niets doet, kan op één scheduler compleet verstopt zitten. Kijk bij grillige klachten altijd één niveau dieper: per scheduler, niet per server. 

Rustige uren zijn niet automatisch veilige uren

Juist omdat er ’s nachts weinig sessies zijn, is de impact van één drukke worker op een gedeelde scheduler relatief enorm. 

‘Het onderhoud heeft een lage MAXDOP’ is geen garantie

Het gaat er niet om hoeveel schedulers het onderhoud gebruikt, maar wie er nog meer op die schedulers zit. 

De 2 identieke metingen die 40 keer verschillen zijn goud waard

De verleiding is groot om de trage meting als incident af te doen. Juist het contrast tussen de 2 maakte de oorzaak vindbaar. Anders was dit een speld in een hooiberg. 

Verzachten is niet hetzelfde als oplossen

Een CPU-cap op je maintenance resource pool voelt als een oplossing omdat de klachten afnemen, maar de rekensom laat zien dat je de totale hinder alleen uitsmeert. 

Een andere nacht op de spoedeisende hulp  

We zijn weer op de SEH-afdeling. De verpleegkundige met het trage scherm merkt niets meer van het nachtelijke onderhoud. Ze kan razendsnel handelen en zich volop inzetten voor de gezondheid van de patiënten die dat het hardst nodig hebben. 

Het beste databasebeheer is het beheer waar niemand iets van merkt. 

Ook last van traagheid in SQL Server? Of het nou een EPD is of een andere database; bij Twintos kijken we uit naar je uitdaging.  

Mail Edward Dortland, database performance-specialist. 
Of bel ons om te sparren: +31 85 1302910.