Skalierbarkeitsproblem bei der Verwendung ausgehender asynchroner Webanforderungen unter IIS 7.5

Ein bisschen lang, aber es ist ein ziemlich kniffliges Problem. Ich habe versucht, das zu behandeln, was wir über das Problem wissen, um die Suche einzugrenzen. Die Frage ist eher eine laufende Untersuchung als eine Einzelfrage, aber ich denke, sie kann auch anderen helfen. Fügen Sie jedoch bitte Informationen in die Kommentare ein oder korrigieren Sie mich, wenn Sie der Meinung sind, dass einige der folgenden Annahmen falsch sind.

UPDATE 19/2, 2013: Wir haben hier einige Fragezeichen entfernt und ich habe eine Theorie, was das Hauptproblem ist, das ich unten aktualisieren werde. Noch nicht bereit, eine "gelöste" Antwort darauf zu schreiben.

UPDATE 24/4, 2013: Die Produktion ist seit einiger Zeit stabil (obwohl ich glaube, dass sie nur vorübergehend ist), und ich denke, das hat zwei Gründe. 1) Porterhöhung und 2) Reduzierung der Anzahl ausgehender (weitergeleiteter) Anfragen. Ich werde dieses Update weiter unten im richtigen Kontext durchführen.

Wir führen derzeit eine Untersuchung in unserer Produktionsumgebung durchErmitteln Sie, warum der IIS-Webserver nicht skaliert, wenn zu viele ausgehende asynchrone Webdienstanforderungen ausgeführt werden (Eine eingehende Anfrage kann mehrere ausgehende Anfragen auslösen.)

Die CPU ist nur bei 20%, aber wir erhalten HTTP 503-Fehler bei eingehenden Anforderungen und viele ausgehende Webanforderungen erhalten die folgende Ausnahme:"SocketException: Eine Operation an einem Socket konnte nicht ausgeführt werden, weil auf dem System nicht genügend Pufferplatz vorhanden war oder weil eine Warteschlange voll war." Offensichtlich gibt es irgendwo einen Skalierbarkeitsengpass und wir müssen herausfinden, was es ist und ob es möglich ist, ihn durch Konfiguration zu lösen.

Anwendungskontext:

Wir führen eine integrierte verwaltete IIS v7.5-Pipeline mit .NET 4.5 unter dem 64-Bit-Betriebssystem Windows 2008 R2 aus. In IIS wird nur ein Arbeitsprozess verwendet. Die Hardware variiert geringfügig, aber der zur Prüfung des Fehlers verwendete Computer ist ein Intel Xeon 8-Kern (16 Hyper-Threaded).

Wir verwenden sowohl asynchrone als auch synchrone Webanforderungen. Bei asynchronen Servern wird die neue .NET-Asynchronisierungsunterstützung verwendet, damit jede eingehende Anforderung mehrere HTTP-Anforderungen in der Anwendung an andere Server über dauerhafte TCP-Verbindungen (Keep-Alive) sendet. Die Ausführungszeit für synchrone Anforderungen ist niedrig (0 bis 32 ms) (längere Zeiten treten aufgrund der Thread-Kontextumschaltung auf). Bei asynchronen Anforderungen kann die Ausführungszeit bis zu 120 ms betragen, bevor die Anforderungen abgebrochen werden.

Normalerweise bedient jeder Server bis zu 1000 eingehende Anfragen. Ausgehende Anfragen sind ~ 300 Anfragen / Sek. Bis zu ~ 600 Anfragen / Sek., Wenn ein Problem auftritt. Probleme treten nur bei ausgehender Asynchronität auf. Anfragen sind auf dem Server aktiviert und wir überschreiten eine bestimmte Stufe ausgehender Anfragen (~ 600 req./s).

Mögliche Lösungen für das Problem:

Das Durchsuchen des Internets nach diesem Problem zeigt eine Vielzahl möglicher Lösungskandidaten. Sie hängen jedoch stark von den Versionen von .NET, IIS und dem Betriebssystem ab, sodass es einige Zeit in Anspruch nimmt, etwas in unserem Kontext zu finden (anno 2013).

Nachfolgend finden Sie eine Liste der Lösungskandidaten und die Schlussfolgerungen, zu denen wir im Hinblick auf unseren Konfigurationskontext bisher gekommen sind. Ich habe die erkannten Problembereiche bisher in folgende Hauptkategorien eingeteilt:

Einige Warteschlangen füllen sichProbleme mit TCP-Verbindungen und Ports(UPDATE 19/2, 2013: Das ist das Problem)Zu langsame Zuweisung von RessourcenSpeicherprobleme(UPDATE 19/2, 2013: Dies ist höchstwahrscheinlich ein weiteres Problem.)1) Einige Warteschlangen füllen sich

Die ausgehende asynchrone Anforderungsausnahmemeldung zeigt an, dass eine Pufferwarteschlange voll ist. Aber es sagt nicht, welche Warteschlange / Puffer. Über dieIIS-Forum (und Blog-Post, auf den dort verwiesen wird) Ich konnte 4 von möglicherweise 6 (oder mehr) verschiedenen Arten von Warteschlangen in der Anforderungs-Pipeline mit der Bezeichnung A-F unten unterscheiden.

Obwohl festgestellt werden sollte, dass von allen unten definierten Warteschlangen der Leistungsindikator Requests Queued für ThreadPool 1.B) während des problematischen Ladens sehr voll wird.Daher liegt die Ursache des Problems wahrscheinlich in .NET und nicht darunter (C-F).

1.A) Warteschlange auf .NET Framework-Ebene?

Wir verwenden die .NET Framework-Klasse WebClient für die Ausgabe des asynchronen Aufrufs (asynchrone Unterstützung) im Gegensatz zum HttpClient, bei dem das gleiche Problem aufgetreten ist, der Schwellenwert für die Anforderungen jedoch weit niedriger ist. Wir wissen nicht, ob die .NET Framework-Implementierung interne Warteschlangen verbirgt oder nicht über dem Thread-Pool. Wir glauben nicht, dass dies der Fall ist.

1.B) .NET-Thread-Pool

Der Thread-Pool fungiert als natürliche Warteschlange, da der .NET-Thread-Scheduler (Standardeinstellung) Threads aus dem auszuführenden Thread-Pool auswählt.

Leistungsindikator: [ASP.NET v4.0.30319]. [Requests Queued].

Konfigurationsmöglichkeiten:

(ApplicationPool) maxConcurrentRequestsPerCPU sollte 5000 sein (anstelle der vorherigen 12). In unserem Fall sollten es also 5000 * 16 = 80.000 Anfragen / Sek. Sein, was in unserem Szenario ausreichen sollte.(processModel) autoConfig = wahr / falsch was erlaubteinige threadPool-bezogene Konfiguration entsprechend der Maschinenkonfiguration einstellen.Wir verwenden true, was ein potenzieller Fehlerkandidat ist, da diese Werte möglicherweise fälschlicherweise für unser (hohes) Bedürfnis festgelegt werden.1.C) Globale, prozessweite, native Warteschlange (nur im integrierten IIS-Modus)

Wenn der Thread-Pool voll ist, häufen sich die Anforderungen in dieser systemeigenen (nicht verwalteten) Warteschlange.

Leistungsindikator:[ASP.NET v4.0.30319]. [Anforderungen in der systemeigenen Warteschlange]

Konfigurationsmöglichkeiten: ????

1.D) HTTP.sys-Warteschlange

Diese Warteschlange ist nicht die gleiche wie oben unter 1.C). Hier ist eine Erklärung, wie sie mir angegeben wurde„Die HTTP.sys-Kernelwarteschlange ist im Wesentlichen ein Abschlussport, an dem der Benutzermodus (IIS) Anforderungen vom Kernelmodus (HTTP.sys) empfängt. Es gibt ein Warteschlangenlimit. Wenn dieses überschritten wird, erhalten Sie einen 503-Statuscode. Das HTTPErr-Protokoll zeigt auch an, dass dies geschehen ist, indem ein 503-Status und QueueFull protokolliert wurden. “.

Leistungsindikator: Ich konnte keinen Leistungsindikator für diese Warteschlange finden. Durch Aktivieren des IIS-HTTPErr-Protokolls sollte jedoch festgestellt werden können, ob diese Warteschlange überfüllt ist.

Konfigurationsmöglichkeiten: Dies wird in IIS im Anwendungspool festgelegt, erweiterte Einstellung: Warteschlangenlänge. Der Standardwert ist 1000. Ich habe Empfehlungen zur Erhöhung auf 10.000 gesehen. Der Versuch, diese Erhöhung durchzuführen, hat unser Problem nicht gelöst.

1.E) Unbekannte Warteschlange (n) des Betriebssystems?

Obwohl dies unwahrscheinlich ist, könnte das Betriebssystem tatsächlich eine Warteschlange zwischen dem Netzwerkkartenpuffer und der Warteschlange "HTTP.sys" haben.

1.F) Netzwerkkartenpuffer:

Wenn die Anforderung auf der Netzwerkkarte eintrifft, sollte es natürlich sein, dass sie in einem Puffer abgelegt werden, um von einem Betriebssystemkernel-Thread abgerufen zu werden. Da dies eine Ausführung auf Kernel-Ebene ist und somit schnell, ist es unwahrscheinlich, dass dies der Schuldige ist.

Windows-Leistungsindikator: [Netzwerkschnittstelle]. [Empfangene Pakete verworfen] unter Verwendung der Netzwerkkarteninstanz.

Konfigurationsmöglichkeiten: ????

2) Probleme mit TCP-Verbindungen und Ports

Dies ist ein Kandidat, der hier und da auftaucht, obwohl unsere ausgehenden (asynchronen) TCP-Anforderungen aus einer dauerhaften (Keep-Alive-) TCP-Verbindung bestehen. Wenn also der Datenverkehr zunimmt, sollte die Anzahl der verfügbaren ephemeren Ports eigentlich nur aufgrund der eingehenden Anforderungen zunehmen. Und wir wissen mit Sicherheit, dass das Problem nur auftritt, wenn ausgehende Anforderungen aktiviert sind.

Das Problem kann jedoch weiterhin auftreten, da der Port während eines längeren Zeitraums der Anforderung zugewiesen wird. Die Ausführung einer ausgehenden Anforderung kann bis zu 120 ms dauern (bevor der .NET-Task (Thread) abgebrochen wird). Dies kann dazu führen, dass die Anzahl der Ports für einen längeren Zeitraum zugewiesen wird. Durch die Analyse des Windows-Leistungsindikators wird diese Annahme überprüft, da die Anzahl von TCPv4. [Verbindung hergestellt] von normalen 2-3000 auf bis zu fast 12.000 steigt, wenn das Problem auftritt.

Wir haben überprüft, dass die konfigurierte maximale Anzahl von TCP-Verbindungen auf den Standardwert von 16384 festgelegt ist. In diesem Fall liegt möglicherweise nicht das Problem vor, obwohl wir uns gefährlich nahe an der maximalen Anzahl befinden.

Wenn wir versuchen, netstat auf dem Server zu verwenden, wird meistens überhaupt keine Ausgabe zurückgegeben, und auch bei Verwendung von TcpView werden am Anfang nur sehr wenige Elemente angezeigt. Wenn wir TcpView eine Weile laufen lassen, zeigt es schnell neue (eingehende) Verbindungen an (sagen wir 25 Verbindungen / Sek.). Fast alle Verbindungen befinden sich von Anfang an im Status TIME_WAIT, was darauf hindeutet, dass sie bereits abgeschlossen sind und auf die Bereinigung warten. Verwenden diese Verbindungen kurzlebige Ports? Der lokale Port ist immer 80, und der Remote-Port nimmt zu. Wir wollten TcpView verwenden, um die ausgehenden Verbindungen zu sehen, aber wir können sie überhaupt nicht sehen, was sehr seltsam ist. Können diese beiden Tools nicht die Anzahl der Verbindungen verarbeiten, die wir haben?(Fortsetzung folgt)

Mehr noch, als Side Kick hier. Es wurde in diesem Blog-Beitrag vorgeschlagen "Verwendung von ASP.NET-Threads unter IIS 7.5, IIS 7.0 und IIS 6.0"dass ServicePointManager.DefaultConnectionLimit auf int maxValue gesetzt werden sollte, was ansonsten ein Problem sein könnte. In .NET 4.5 ist dies jedoch bereits von Anfang an die Standardeinstellung.

UPDATE 19/2, 2013:

Man kann davon ausgehen, dass wir tatsächlich die Höchstgrenze von 16.384 Ports erreicht haben. Wir haben die Anzahl der Ports auf allen Servern bis auf einen verdoppelt, und nur auf dem alten Server würden Probleme auftreten, wenn wir die alte Spitzenlast ausgehender Anforderungen erreichen. Warum hat uns TCP.v4. [Verbindungen hergestellt] zu Problemzeiten nie eine höhere Zahl als ~ 12.000 angezeigt? MEINE Theorie: Höchstwahrscheinlich entspricht der Leistungsindikator TCPv4 [Verbindungen hergestellt] nicht der Anzahl der derzeit zugewiesenen Ports, obwohl er (noch) nicht als Fakt festgelegt wurde. Ich hatte noch keine Zeit, mich über den TCP-Status zu informieren, aber ich vermute, dass es mehr TCP-Status gibt, als in der Anzeige "Connection Established" (Verbindung hergestellt) angegeben sind, wodurch der Port als belegt ausgewiesen wird. Da wir den Leistungsindikator "Connection Established" nicht verwenden können, um die Gefahr eines Auslaufens von Ports zu erkennen, ist es wichtig, dass wir einen anderen Weg finden, um zu erkennen, wann dieser maximale Portbereich erreicht wird. Und wie im obigen Text beschrieben, können wir dies auf unseren Produktionsservern weder mit NetStat noch mit der Anwendung TCPview tun. Das ist ein Problem! (Ich werde mehr darüber in einer anstehenden Antwort schreiben, die ich auf diesen Beitrag denke.)Die Anzahl der Ports ist unter Windows auf maximal 65.535 beschränkt (obwohl die ersten ~ 1000 wahrscheinlich nicht verwendet werden sollten). Es sollte jedoch möglich sein, das Problem zu vermeiden, dass die Ports nicht mehr ausreichen, indem die Zeit für den TCP-Status TIME_WAIT (Standardeinstellung 240 Sekunden) wie an zahlreichen Stellen beschrieben verringert wird. Dadurch sollten Ports schneller freigegeben werden. Ich war zuerst ein bisschen hestitant dabei, da wir sowohl lang laufende Datenbankabfragen als auch WCF-Aufrufe auf TCP verwenden und ich die Zeitbeschränkung nicht verringern möchte. Obwohl ich mich noch nicht mit dem Lesen meiner TCP-Zustandsmaschine beschäftigt habe, denke ich, dass dies kein Problem sein könnte. Der Zustand TIME_WAIT ist meines Erachtens nur vorhanden, um den Handshake eines ordnungsgemäßen Herunterfahrens für den Client zu ermöglichen. Daher sollte die tatsächliche Datenübertragung auf einer vorhandenen TCP-Verbindung aufgrund dieses Zeitlimits keine Zeitüberschreitung aufweisen. Im schlimmsten Fall wird der Client nicht ordnungsgemäß heruntergefahren und es tritt stattdessen eine Zeitüberschreitung auf. Ich vermute, dass alle Browser dies möglicherweise nicht richtig implementieren und es möglicherweise nur auf der Clientseite ein Problem geben kann. Obwohl ich hier ein bisschen rate ...

END UPDATE 19/2, 2013

UPDATE 24/4, 2013: Wir haben die Anzahl der Ports auf den Maximalwert erhöht. Gleichzeitig erhalten wir nicht so viele weitergeleitete ausgehende Anfragen wie früher. Diese beiden in Kombination sollten der Grund sein, warum wir keine Zwischenfälle hatten. Es ist jedoch nur vorübergehend, da die Anzahl der ausgehenden Anforderungen auf diesen Servern in Zukunft wieder zunehmen wird. Das Problem liegt also meiner Meinung nach darin, dass der Port für die eingehenden Anfragen während des Zeitrahmens für die Beantwortung der weitergeleiteten Anfragen offen bleiben muss. In unserer Anwendung beträgt diese Stornierungsgrenze für diese weitergeleiteten Anforderungen 120 ms, was mit den normalen <1 ms verglichen werden kann, um eine nicht weitergeleitete Anforderung zu verarbeiten. Im Wesentlichen glaube ich, dass die definierte Anzahl von Ports der größte Engpass bei der Skalierbarkeit auf den von uns verwendeten Servern mit hohem Durchsatz (> 1000 Anfragen / Sek. Auf ~ 16-Kern-Rechnern) ist. Dies in Kombination mit der GC-Arbeit zum Neuladen des Caches (siehe unten) macht den Server besonders anfällig.

END UPDATE 24/4

3) Zu langsame Zuweisung von Ressourcen

Unsere Leistungsindikatoren zeigen, dass die Anzahl der Anforderungen in der Warteschlange im Thread-Pool (1B) während der Zeit des Problems stark schwankt. Dies bedeutet möglicherweise, dass wir eine dynamische Situation haben, in der die Warteschlangenlänge aufgrund von Änderungen in der Umgebung zu schwingen beginnt. Dies wäre beispielsweise der Fall, wenn Hochwasserschutzmechanismen vorhanden sind, die bei Hochwasser aktiviert werden. So wie es ist, haben wir eine Reihe dieser Mechanismen:

3.A) Web Load Balancer

Wenn die Dinge wirklich schlecht laufen und der Server mit einem HTTP 503-Fehler antwortet, wird der Webserver vom Lastenausgleich automatisch für einen Zeitraum von 15 Sekunden aus der Produktion entfernt. Dies bedeutet, dass die anderen Server die erhöhte Last während des Zeitrahmens übernehmen. Während der "Abkühlungsperiode" ist der Server möglicherweise damit fertig, seine Anforderung zu bearbeiten, und er wird automatisch wiederhergestellt, wenn der Load Balancer seinen nächsten Ping ausführt. Das ist natürlich nur dann gut, wenn nicht alle Server gleichzeitig ein Problem haben. Zum Glück waren wir bisher nicht in dieser Situation.

3.B) Anwendungsspezifisches Ventil

In der Webanwendung verfügen wir über ein eigenes Ventil (Ja, es ist ein "Ventil", kein "Wert"), das von einem Windows-Leistungsindikator für in die Warteschlange gestellte Anforderungen im Threadpool ausgelöst wird. In Application_Start wird ein Thread gestartet, der diesen Leistungsindikatorwert jede Sekunde überprüft. Wenn der Wert 2000 überschreitet, wird der gesamte ausgehende Verkehr nicht mehr initiiert. Wenn der Warteschlangenwert in der nächsten Sekunde unter 2000 liegt, wird der ausgehende Verkehr erneut gestartet.

Das Seltsame dabei ist, dass es uns nicht geholfen hat, das Fehlerszenario zu erreichen, da wir nicht viel davon protokollieren müssen. Dies kann bedeuten, dass die Situation sehr schnell schlecht wird, wenn der Datenverkehr uns hart trifft, sodass die Zeitintervallprüfung von 1 Sekunde zu hoch ist.

3.C) Thread-Pool nimmt langsam zu (und ab) von Threads

Es gibt noch einen weiteren Aspekt. Wenn mehr Threads im Anwendungspool benötigt werden, werden diese Threads sehr langsam zugewiesen. Nach dem, was ich gelesen habe, 1-2 Threads pro Sekunde. Dies liegt daran, dass das Erstellen von Threads teuer ist und Sie sowieso nicht zu viele Threads benötigen, um teure Kontextwechsel im synchronen Fall zu vermeiden. Dies halte ich für selbstverständlich. Es sollte jedoch auch bedeuten, dass die Anzahl der Threads nicht ausreicht, um den Bedarf im asynchronen Szenario zu decken, wenn ein plötzlicher hoher Datenverkehr auftritt, und die Warteschlange für Anforderungen beginnt. Dies ist ein sehr wahrscheinlicher Problemkandidat, denke ich. Eine mögliche Lösung besteht dann darin, die Mindestanzahl der im ThreadPool erstellten Threads zu erhöhen. Dies kann sich aber auch auf die Leistung der synchron ausgeführten Anforderungen auswirken.

4) Speicherprobleme

(Joey Reyes hat darüber geschriebenhier in einem Blogbeitrag) Da Objekte später für asynchrone Anforderungen erfasst werden (in unserem Fall bis zu 120 ms später), kann ein Speicherproblem auftreten, da Objekte auf Generation 1 hochgestuft werden können und der Speicher nicht so oft wie gewünscht neu erfasst wird. Der erhöhte Druck auf den Garbage Collector kann sehr wohl zu einer erweiterten Thread-Kontextumschaltung führen und die Kapazität des Servers weiter schwächen.

Während des Problems wird jedoch keine erhöhte GC- oder CPU-Auslastung festgestellt, sodass wir nicht der Meinung sind, dass der vorgeschlagene CPU-Drosselungsmechanismus eine Lösung für uns darstellt.

UPDATE 19/2, 2013: In regelmäßigen Abständen verwenden wir einen Cache-Swap-Mechanismus, bei dem ein (fast) voller In-Memory-Cache neu in den Speicher geladen wird und der alte Cache Müll sammeln kann. In diesen Fällen muss der GC härter arbeiten und Ressourcen aus der normalen Anforderungsbearbeitung stehlen. Wenn der Windows-Leistungsindikator für die Thread-Kontextumschaltung verwendet wird, wird angezeigt, dass die Anzahl der Kontextumschaltungen zum Zeitpunkt einer hohen GC-Auslastung gegenüber dem normalen hohen Wert erheblich abnimmt. Ich denke, dass während solcher Cache-Neuladungen der Server für das Einreihen von Anfragen besonders anfällig ist und es notwendig ist, den Platzbedarf des GC zu verringern. Eine mögliche Lösung für das Problem besteht darin, den Cache nur zu füllen, ohne ständig Speicher zuzuweisen. Ein bisschen mehr Arbeit, aber es sollte machbar sein.

UPDATE 24/4, 2013: Ich bin immer noch in der Mitte des Cache-Reload-Memory-Tweaks, um zu vermeiden, dass der GC so oft ausgeführt wird. Normalerweise haben wir jedoch ungefähr 1000 Anfragen in der Warteschlange, wenn der GC ausgeführt wird. Da es auf allen Threads ausgeführt wird, stiehlt es natürlich Ressourcen aus der normalen Verarbeitung von Anforderungen. Ich werde diesen Status aktualisieren, sobald diese Optimierung implementiert wurde und wir einen Unterschied feststellen können.

END UPDATE 24/4

Antworten auf die Frage(2)

Ihre Antwort auf die Frage