Stakstørrelse i .NET

For nylig blev jeg involveret i at debugge et crash i en kompleks ASP.NET-løsning. Problemet havde drevet de ansvarlige til vanvid, for det kunne kun reproduceres i produktionsmiljøet, og de havde ikke kunne finde årsagen til crashet. Derfor hev de fat i mig, i håb om at jeg kunne lokalisere kilden til problemet ved hjælp af WinDbg.

Et par crash dumps pegede på, at der nok var tale om et stack overflow, og udvikleren kunne bekræfte, at der faktisk var en rekursiv metode i spil. Ved rekursion bruges stakken til at holde tilstanden i forbindelse med hvert kald, så derfor kan der let komme pres på stakken. Det er naturligvis underligt, at en sådan fejl ikke umiddelbart kunne reproduceres i udviklingsmiljøet, men det kan meget vel skyldes, at ASP.NET kører med en begrænset stakstørrelse i forhold til ”almindelige” .NET programmer.

I et almindelig .NET program er stakkens størrelse som udgangspunkt 1 MB per tråd. I ASP.NET er standarden 256 KB. Jeg er derfor overbevist om, at Visual Studio afvikler ASP.NET med en større stak end tilfældet er, når koden flyttes til en IIS, hvilket kan forklare, at udvikleren ikke kunne reproducere fejlen.

For at undgå et stack overflow er der egentlig tre løsninger:

  1. Vi kan gøre opgaven mindre og dermed belastningen på stakken mindre. Problemet havde allerede vist sig ved en begrænset datamængde, så det var ikke en mulighed i dette tilfælde. Faktisk ville det kun blive værre, når systemet for alvor blev taget i brug.
  2. Fjerne rekursionen fra koden. En rekursiv metode kan omskrives, så den ikke længere er rekursiv. Det kræver, at vi selv håndterer den tilstand, stakken normalt tager hånd om. I praksis vil det betyde, at vi flytter belastningen fra stakken til heapen, og eftersom denne ikke er begrænset på samme måde som stakken, vil det løse problemet. Desværre betyder det omskrivning af kode, så det var heller ikke en særlig attraktiv løsning i dette tilfælde. Ideen blev arkiveret som sidste udvej.
  3. Det efterlod os med 3. og sidste udvej, nemlig at gøre stakken større, men hvordan gør man lige det for en managed applikation?

Under normale omstændigheder er størrelse på stakken ”brændt” ind i selve exe-filen som en del af PE/COFF-headeren. Jeg må indrømme, at jeg nok ikke havde forestillet mig, at dette også kaldt for managed applikationer, men det gør det faktisk. Ergo, kan vi se stakken for en .NET applikation via værktøjer som f.eks. pedump, og vi kan tilmed modificere en given .NET applikation til at køre med en anden størrelse stak. Under normale omstændigheder er 1 MB mere end rigelig, så hvis vi absolut skal gøre noget, vil det typisk være at gøre stakken mindre. I de fleste tilfælde ser jeg dog ingen grund til at justere dette, men her var det naturligvis værd at prøve for at se om det kunne løse vores problem.

En .NET applikation er jo som bekendt ikke repræsenteret af maskinspecifik kode, som vi kender det fra almindelige programmer. Selve CLRen er blot et DLL (mscorwks.dll), og et .NET-program har derfor brug for en boot strap-applikation, der kan indlæse CLRen og derefter afvikle selve applikationen i denne. For en ASP.NET-løsning er denne boot strap-applikation w3wp.exe, så det var i givet fald den, der skulle rettes.

Det er let nok at sætte stakstørrelsen på exe-filen ved hjælp af editbin, men jeg havde ikke lige tænkt situationen igennem. w3wp.exe ligger i et af Windows’ systembiblioteker og er dermed beskyttet af den sindrige feature kendt som Windows File Protection, hvilket bevirker, at eventuelle ændringer til filen resulterer i en besked i event-loggen og en gendannelse af den oprindelige fil. Man kan muligvis komme uden om denne beskyttelse, men da jeg først var blevet mindet om Windows File Protections lyksagligheder, besluttede jeg mig for at droppe denne løsning. Ændringen af stakstørrelsen direkte i exe-filen ville betyde, at vi ikke alene skulle finde en vej udenom Windows File Protection, men også at vi skulle gentage øvelsen hver gang w3wp.exe blev opdateret.

I .NET har hver tråd i applikationen en stak på 1 MB. Størrelsen bliver som standard sat i exe-filen, men vi kan også selv angive størrelsen ved oprettelsen af en tråd (det kræver dog, at vi kører på Windows XP/2003 eller nyere). Microsoft fraråder dog at man piller ved størrelsen, da 1 MB som nævnt skulle være mere end rigeligt i de fleste tilfælde. Stakproblemer skyldes oftere en fejl end et egentlig behov, og det er nok grunden til Microsofts anbefaling. I dette tilfælde var der dog et reelt behov.

Min ide var at indkapsle selve applikationen i en ny tråd med den nødvendige stakstørrelse, og jeg begyndte derfor at lede koden igennem for at se, hvordan det kunne gøres. Det viste sig langt lettere, end jeg kunne have håbet på, for faktisk var den tunge beregning allerede isoleret i sin egen tråd. Derfor var det blot at ændre oprettelsen af denne tråd, så den blev oprettet med en specifik stakstørrelse.

Det løste problemet, men det er selvfølgelig ikke den optimale løsning, for hvad sker der, når datamængden bliver større? På et eller andet tidspunkt vil applikationen ramme loftet igen. Maskinen skal efter sigende ikke køre andet, så i realiteten kan vi blot blive ved at skrue på stakkens størrelse til den passer, så i praksis kan det være, at problemet faktisk er løst. Personligt havde jeg dog foretrukket at skrive koden om, så rekursion ikke var nødvendig. Derved ville belastningen som sagt blive flyttet til heapen og således ville fremtidige problemer kunne løses med mere RAM.

3 Responses to “Stakstørrelse i .NET”

  1. Det var mig der hyrede Brian til at løse denne opgave. Den havde drevet en af mig hyret senior konsultent til vanvid i lang tid, og siden min kunde ikke længere havde den fornødne tålmodighed til at få det hele skrevet om igen, var gode råd dyre. Det var en fornøjelse at arbejde sammen med Brian omkring dette problem. Opgaven – og ikke mindste begrænsningerne (tidspres, omskrivning, osv) blev taget meget alvorligt.
    Der skal ikke herske tvivl om at hvis jeg nogen sinde havner i en lignede situation vil jeg omgående tage fat i Brian og få løst problemet med det samme.
    Tak for din hjælp Brian. Den var og er meget velkommen

    Med venlig hilsen
    Kris Kristensen
    AKTANT A/S
    GM

  2. Jesper Møllegaard says:

    Hej Brian,

    Interessant artikel, tak for den.

    I begyndelsen af artiklen skriver du følgende:

    “Et par crash dumps pegede på, at der nok var tale om et stack overflow, og udvikleren kunne bekræfte, at der faktisk var en rekursiv metode i spil.”

    Vil du uddybe, hvilke indikatorer i disse crash dumps, der henledte din opmærksomhed på at der var tale om et stack overflow, og hvilken type fejl besked resulterer denne type fejl i?

    Det ville være interessant at forstå, da det øjensynligt er en udfordring at finde det underliggende problem baseret på fejl beskeden.

    Og endelig, kan/bør udviklere “guarde” stak størrelsen på sine tråde i sine programmer (evt. i debug mode)?

    Med venlig hilsen,
    Jesper Møllegaard

  3. Normalt vil et stack overflow vise sig som en StackOverflowException, og derfor er det relativt let at spotte det i et crash dump. Desværre havde vi et par problemer med at få taget et korrekt crash dump. Jeg husker ikke detaljerne, men jeg mener ikke, at der var en exception i dumpet, men da en af kaldestakkene indeholdt den samme metode mange gange i træk, pegede det på en rekursiv funktion.

    Jeg er ikke helt sikker på, hvad du mener med at guarde stakken. Der er som sådan ikke noget direkte, man kan gøre. Man kan ændre størrelsen på den (hvilket også var løsningen i dette tilfælde) og/eller begrænse brugen, men ellers er der ikke meget at gøre.

Leave a Reply