[WebServer, ASynch, HttpListener, .Net]
Ok, je zal misschien wat meer als 4 stappen nodig hebben, maar echt heel moeilijk is het niet om je eigen webserver te maken. Met .Net 3.0 is het redelijk tot zeer eenvoudig om de basis neer te leggen, maar wat kan je verwachten, en wat is de impact van de verschillende manieren op het aantal requests per second. Daar gaan we hier eens wat dieper op in, want uiteindelijk is dat toch meestal wel waar het om gaat. Verder komen er een aantal interessante punten en dilemma's naar boven, vooral als je wat meer let op performance. Je werkt namelijk al snel met een compleet andere orde grootte aan requests per second, waar je normaal als ASP.Net programmeur niet snel mee te maken krijgt. Ter vergelijking, een tradionele web server, welke een simpele aspx pagina stuurt naar de client zit rond de 450 Req/Sec, straks zul je zien dat we daar echt ruimschoots aan voorbij gaan. Laten we eerst een kijken hoe we een simpele web server kunnen bouwen, die netjes antwoord geeft...
Poging 1 : Synchroon
Gewoon eens luisteren naar poort 80, en een antwoord terug
Om een idee te krijgen hoe snel een server zou kunnen zijn zonder het direct complexer te maken als het nodig is, beginnen we met synchrone request afhandeling. .Net heeft hiervoor een speciale klasse namelijk de HttpListener, welke netjes de inkomende request uitleest en je een HttpContext geeft.
Dit is volledig synchroon, en indien er meerdere requests binnenkomen, zal de performance niet echt optimaal zijn. Maar toch omdat we eigenlijk niks doen, is het een mooie leiddraad qua performance, op onze test machine kregen we al snel 4000 Req/s. Wat toch al een aangename verrassing was, en beduidend meer dan we gewent waren van een asp.net applicatie. Wat ook niet verwacht was is dat de CPU geen bottleneck is, maar dat die dus ergens anders ligt. We kregen maar een CPU load van ongeveer 6%. Uiteraard indien er meerdere gebruikers komen, en de requests langer duren gaat de performance op deze manier erg snel omlaag. Alles moet op elkaar wachten, en indien er 1 request mis gaat, stopt de hele server. Leuk als test, maar in de praktijk niet echt bruikbaar.
Uitkomst:
- Req/Sec: 4000
- CPU Load: 6 %
- Errors: Geen
Poging 2: Multithread
Per request een thread starten en verwerken.
Poging 2, multi-threaden, elke binnenkomende request krijgt zijn eigen thread en mag ermee doen wat hij wilt.Zodat al het langdurige request/response werk netjes asynchroon verwerkt wordt.
Nu is dit zeker niet de meest optimale code, maar de CPU vloog naar de 90% en performance ging terug naar 150 Req/sec.Wel een hele extreme daling, dus echt een thread per request is geen goed idee.
Uitkomst:
- Req/Sec: 150
- CPU Load: 90 %
- Errors: None
Poging 3: ThreadPool
Alle requests op een hoop.
Kijk ThreadsPooling en WorkQueues, dat zijn termen die goed klinken voor ons scenario. Windows regelt zelf het aantal actieve threads, geeft je een queue waarin je werk kan aanmelden.
Performance schoot inderdaad weer omhoog naar de 12k req/sec, bleef stabiel bij meerdere gebruikers, maar laat hier en daar wel een steekje vallen. Hier worden de verschillende threads wel apart verwerkt ,maar blijven we synchroon naar de poort luisteren. Wat bij een heftige belasting hier en daar een voor verstoppingen kan leiden. Eens kijken of we de binnenkomende requests sneller kunnen verwerken...
Uitkomst:
- Req/Sec: 12000
- CPU Load: 12 %
- Errors: een paar bij meerdere gebruikers
Poging 4: ThreadPool + ASynch Listener
Minder wachttijd voor het oppakken van een request.
Aynchroon luisteren, we kunnen niet zomaar de GetContext verplaatsen naar de ThreadPool (in de DoWork) dat zou betekenen dat er meerdere objecten tegelijk naar dezelfde poort gaan lopen luisteren. We willen uiteindelijk gewoon 1 object hebben wat luistert, alleen moet hij sneller weer gaan luisteren dan in poging 3. er wordt namelijk wel wat werk verzet door de computer, nadat we de request binnen hebben. (o.a. het aanmelden bij de queue, en het maken van een WaitCallBack object) Ook daar is al in voorzien, en kunnen we de BeginGetContext en EndGetContext gebruiken.
Wat je hier ziet is dat we al meteen wanneer de EndGetContext wordt aangeroepen, we weer opnieuw gaan luisteren in de main thread naar een nieuwe request. Tegelijkertijd wordt rustig de vorige request op een de workerQueue gezet, en verwerkt. Performance is eigenlijk hetzelfde als de vorige poging, maar hij laat geen steken meer vallen. Fijn om te zien, dat er bijna tot geen overhead is in het gebruik van Callbacks, Events en ThreadPools. Deze opzet ziet er goed te gebruiken uit, we handelen de requests vlot en asynchroon af, draaien stabiel en hebben een lage CPU load.
Uitkomst:
- Req/Sec: 12000
- CPU Load: 12 %
- Errors: Geen
Uiteindelijk is de code welke nodig is om in .Net een asynchrone web server te bouwen erg weinig. De performance is enorm, waarbij wij nog op een simpele desktop hebben gewerkt. (en dus nog zeker winst valt te halen). In sommige gevallen zeker de moeite waard om als alternatieve optie mee te nemen. De volgende post zullen we eens kijken, hoe we nu echt iets kunnen doen, en wat daar allemaal extra bij komt kijken. Verder gaan we later eens kijken hoe nu om te gaan met langlopende requests, want we gaan er maar steeds van uit dat we een request zo snel mogelijk willen afhandelen. In dat geval kan er iets misgaan met de workerQueue, want deze is vooral goed is kortlopende processen, en als we 2k aan wachtende request hebben, wil je eigenlijk geen 2k aan actieve threads hebben.
Opmerking: indien je zelf deze voorbeelden wilt uitproberen, en je draait Vista krijg je een foutmelding. (Access Denied). Gebruik netsh om de benodigde rechten te geven aan de account waaronder de app draait.
netsh http add urlacl url=http//+:80/ user=domain\userMarinus
Geen opmerkingen:
Een reactie posten