Problem ze skalowalnością podczas korzystania z wychodzących asynchronicznych żądań internetowych w usługach IIS 7.5

Trochę długi opis poniżej, ale jest to dość trudny problem. Próbowałem opisać to, co wiemy o problemie, aby zawęzić wyszukiwanie. Pytanie jest bardziej ciągłym dochodzeniem niż pytanie oparte na pojedynczym pytaniu, ale myślę, że może również pomóc innym. Ale proszę dodaj informacje w komentarzach lub popraw mnie, jeśli uważasz, że się mylę co do niektórych założeń poniżej.

AKTUALIZACJA 19/2, 2013: Usunęliśmy w tym miejscu kilka znaków zapytania i mam teorię, na czym polega główny problem, który zaktualizuję poniżej. Nie jestem jeszcze gotowy na napisanie „rozwiązanej” odpowiedzi.

UPDATE 24/4, 2013: Sprawy były stabilne w produkcji (choć uważam, że są tymczasowe) przez chwilę i myślę, że wynika to z dwóch powodów. 1) zwiększenie liczby portów i 2) zmniejszenie liczby żądań wychodzących (przesłanych). Kontynuuję tę aktualizację dalej w odpowiednim kontekście.

Obecnie prowadzimy dochodzenie w naszym środowisku produkcyjnymokreśl, dlaczego nasz serwer internetowy IIS nie skaluje się, gdy wykonywanych jest zbyt wiele asynchronicznych żądań usługi sieciowej (jedno żądanie przychodzące może wywołać wiele żądań wychodzących).

Procesor ma tylko 20%, ale otrzymujemy błędy HTTP 503 na przychodzących żądaniach, a wiele wychodzących żądań internetowych otrzymuje następujący wyjątek:„SocketException: nie można wykonać operacji na gnieździe, ponieważ w systemie brakowało wystarczającej ilości miejsca na bufor lub kolejka była pełna” Oczywiście istnieje wąskie gardło w zakresie skalowalności i musimy dowiedzieć się, co to jest i czy można je rozwiązać przez konfigurację.

Kontekst aplikacji:

Używamy zintegrowanego zarządzanego rurociągu IIS 7.5, używając .NET 4.5 w 64-bitowym systemie operacyjnym Windows 2008 R2. W IIS używamy tylko 1 procesu roboczego. Sprzęt różni się nieznacznie, ale maszyną używaną do badania błędu jest rdzeń Intel Xeon 8 (16 hiperwątkowych).

Używamy zarówno asynchronicznych, jak i synchronicznych żądań internetowych. Te asynchroniczne korzystają z nowej obsługi asynchronicznej .NET, aby każde przychodzące żądanie składało wiele żądań HTTP w aplikacji na inne serwery na utrwalonych połączeniach TCP (utrzymywanie aktywności). Czas wykonania żądania synchronicznego jest niski 0-32 ms (dłuższe czasy wynikają z przełączania kontekstu wątku). W przypadku żądań asynchronicznych czas wykonania może wynosić do 120 ms, zanim żądania zostaną przerwane.

Zwykle każdy serwer obsługuje do ~ 1000 żądań przychodzących. Żądania wychodzące mają ~ 300 żądań / s do ~ 600 żądań / s, gdy problem zaczyna się pojawiać. Problemy występują tylko przy asynchronizacji wychodzącej. żądania są włączone na serwerze i przekraczamy pewien poziom żądań wychodzących (~ 600 req./s).

Możliwe rozwiązania tego problemu:

Wyszukiwanie w Internecie na ten problem ujawnia mnóstwo możliwych rozwiązań kandydatów. Chociaż są one w znacznym stopniu zależne od wersji .NET, IIS i systemu operacyjnego, znalezienie czegoś w naszym kontekście wymaga czasu (anno 2013).

Poniżej znajduje się lista kandydatów na rozwiązania oraz wnioski, do których doszliśmy do tej pory w odniesieniu do naszego kontekstu konfiguracyjnego. Wykryłem wykryte obszary problemowe do tej pory w następujących głównych kategoriach:

Niektóre kolejki się zapełniająProblemy z połączeniami TCP i portami(AKTUALIZACJA 19/2, 2013: To jest problem)Zbyt wolny przydział zasobówProblemy z pamięcią(AKTUALIZACJA 19/2, 2013: To najprawdopodobniej kolejny problem)1) Niektóre kolejki się zapełniają

Komunikat wyjątku wychodzącego żądania asynchronicznego wskazuje, że pewna kolejka bufora została wypełniona. Ale nie mówi, która kolejka / bufor. Za pośrednictwemForum IIS (i przywołany tam post na blogu) Udało mi się rozróżnić 4 z możliwych 6 (lub więcej) różnych typów kolejek w potoku żądania oznaczonym A-F poniżej.

Chociaż należy stwierdzić, że spośród wszystkich zdefiniowanych poniżej kolejek, widzimy na pewno, że licznik wydajności ThreadPool 1.B) Requested Queued staje się bardzo pełny podczas problematycznego obciążenia.Prawdopodobnie przyczyną problemu jest poziom .NET, a nie poniżej (C-F).

1.A) Kolejka poziomu .NET Framework?

Używamy WebClient klasy .NET Framework do wywołania asynchronicznego wywołania (async support), w przeciwieństwie do HttpClient, którego doświadczyliśmy miało ten sam problem, ale z znacznie niższym progiem req / s. Nie wiemy, czy implementacja .NET Framework ukrywa jakiekolwiek wewnętrzne kolejki lub nie nad pulą wątków. Nie sądzimy, żeby tak było.

1.B) Pula wątków .NET

Pula wątków działa jako naturalna kolejka, ponieważ wątek .NET (domyślny) program planujący wybiera wątki z puli wątków do wykonania.

Licznik wydajności: [ASP.NET v4.0.30319]. [Kolejki żądań].

Możliwości konfiguracji:

(ApplicationPool) maxConcurrentRequestsPerCPU powinien wynosić 5000 (zamiast poprzednich 12). W naszym przypadku powinno to być 5000 * 16 = 80 000 żądań / s, co powinno wystarczyć w naszym scenariuszu.(processModel) autoConfig = true / false, który pozwalaniektóre konfiguracje związane z threadPool do ustawienia zgodnie z konfiguracją maszyny.Używamy prawdy, która jest potencjalnym kandydatem na błąd, ponieważ te wartości mogą być błędnie ustawione dla naszej (wysokiej) potrzeby.1.C) Globalna, procesowa, natywna kolejka (tylko tryb zintegrowany IIS)

Jeśli pula wątków jest pełna, żądania zaczynają się gromadzić w tej rodzimej (nie zarządzanej) kolejce.

Licznik wydajności:[ASP.NET v4.0.30319]. [Żądania w kolejce macierzystej]

Możliwości konfiguracji: ??

1.D) Kolejka HTTP.sys

Ta kolejka nie jest tą samą kolejką, co 1.C) powyżej. Oto wyjaśnienie, które zostało mi podane„Kolejka jądra HTTP.sys jest zasadniczo portem zakończenia, na którym tryb użytkownika (IIS) odbiera żądania z trybu jądra (HTTP.sys). Ma limit kolejki, a gdy zostanie przekroczony, otrzymasz kod stanu 503. Dziennik HTTPErr będzie również wskazywał, że stało się to przez zarejestrowanie statusu 503 i kolejki pełnej ”.

Licznik wydajności: Nie udało mi się znaleźć żadnego licznika wydajności dla tej kolejki, ale po włączeniu dziennika IIS HTTPErr powinno być możliwe wykrycie zalania tej kolejki.

Możliwości konfiguracji: Jest to ustawione w usługach IIS w puli aplikacji, ustawienie zaawansowane: Długość kolejki. Wartość domyślna to 1000. Widziałem zalecenia, aby zwiększyć ją do 10.000. Chociaż próba tego wzrostu nie rozwiązała naszego problemu.

1.E) Nieznana kolejka systemu operacyjnego?

Chociaż mało prawdopodobne, sądzę, że system operacyjny może mieć kolejkę gdzieś pomiędzy buforem karty sieciowej a kolejką HTTP.sys.

1.F) Bufor kart sieciowych:

Gdy żądanie dotrze do karty sieciowej, powinno być naturalne, że są one umieszczane w jakimś buforze, aby mogły zostać pobrane przez wątek jądra systemu operacyjnego. Ponieważ jest to wykonanie na poziomie jądra, a więc szybko, nie jest prawdopodobne, że jest sprawcą.

Licznik wydajności systemu Windows: [Interfejs sieciowy]. [Pakiety odebrane odrzucone] przy użyciu instancji karty sieciowej.

Możliwości konfiguracji: ??

2) Problemy z połączeniami TCP i portami

Jest to kandydat, który pojawia się tu i tam, chociaż nasze wychodzące (asynchroniczne) żądania TCP są wykonane z trwałego (utrzymującego się przy życiu) połączenia TCP. W miarę wzrostu ruchu liczba dostępnych portów efemerycznych powinna rosnąć tylko z powodu nadchodzących żądań. Wiemy na pewno, że problem pojawia się tylko wtedy, gdy włączone są żądania wychodzące.

Problem może jednak nadal występować, ponieważ port jest przydzielany w dłuższym okresie czasu żądania. Żądanie wychodzące może trwać do 120 ms (zanim zadanie .NET (wątek) zostanie anulowane), co może oznaczać, że liczba portów zostanie przydzielona na dłuższy okres. Analizując licznik wydajności systemu Windows, weryfikuje to założenie, ponieważ liczba TCPv4. [Połączenie ustalone] przechodzi z normalnego 2-3000 do szczytów do prawie 12.000 łącznie, gdy wystąpi problem.

Sprawdziliśmy, że skonfigurowana maksymalna liczba połączeń TCP jest ustawiona na wartość domyślną 16384. W tym przypadku może nie być problemu, chociaż jesteśmy niebezpiecznie blisko maksymalnego limitu.

Kiedy próbujemy użyć netstat na serwerze, w większości powraca on bez żadnego wyjścia, również użycie TcpView pokazuje bardzo niewiele elementów na początku. Jeśli pozwolimy TcpView działać przez jakiś czas, wkrótce zacznie on pokazywać nowe (przychodzące) połączenia dość szybko (powiedzmy 25 połączeń / s). Prawie wszystkie połączenia są w stanie OCZEKIWANIE CZASU od początku, co sugeruje, że zostały już zakończone i czekają na wyczyszczenie. Czy te połączenia używają efemerycznych portów? Port lokalny ma zawsze wartość 80, a port zdalny rośnie. Chcieliśmy użyć TcpView, aby zobaczyć połączenia wychodzące, ale nie widzimy ich wcale, co jest bardzo dziwne. Czy te dwa narzędzia nie poradzą sobie z ilością posiadanych połączeń?(Aby być kontynuowanym… Ale proszę, podaj informacje, jeśli wiesz…)

Co więcej, jako kopnięcie boczne. Zasugerowano to w tym wpisie na blogu ”Wykorzystanie wątków ASP.NET w usługach IIS 7.5, IIS 7.0 i IIS 6.0„ServicePointManager.DefaultConnectionLimit powinien być ustawiony na int maxValue, który w przeciwnym razie mógłby być problemem. Ale w .NET 4.5 jest to domyślne już od początku.

AKTUALIZACJA 19/2, 2013:

Rozsądnie jest założyć, że w rzeczywistości osiągnęliśmy maksymalny limit 16.384 portów. Podwoiliśmy liczbę portów na wszystkich serwerach z wyjątkiem jednego, a tylko stary serwer napotkał problem, gdy osiągnęliśmy stare szczytowe obciążenie żądań wychodzących. Dlaczego więc TCP.v4. [Połączenia ustanowione] nigdy nie pokazują nam większej liczby niż ~ 12 000 w czasie problemów? MOJA teoria: najprawdopodobniej, chociaż nie została ustalona jako fakt (jeszcze), licznik wydajności TCPv4. [Połączenia ustanowione] nie jest równoważny liczbie aktualnie przydzielonych portów. Nie miałem jeszcze czasu, aby nadrobić zaległości w studiowaniu stanu TCP, ale zgaduję, że jest więcej stanów TCP niż to, co pokazuje „Connection Established”, co sprawiłoby, że port byłby zajęty. Chociaż nie możemy użyć licznika wydajności „Połączenie ustanowione” jako sposobu na wykrycie niebezpieczeństwa braku portów, ważne jest, abyśmy znaleźli inny sposób wykrywania po osiągnięciu tego maksymalnego zakresu portów. I jak opisano w powyższym tekście, nie jesteśmy w stanie korzystać z NetStat lub aplikacji TCPview w tym celu na naszych serwerach produkcyjnych. To jest problem! (Napiszę o tym więcej w nadchodzącej odpowiedzi, myślę o tym poście)Liczba portów w oknach jest ograniczona do maksymalnie 65,535 (chociaż pierwsze ~ 1000 prawdopodobnie nie będą używane). Ale powinno być możliwe uniknięcie problemu braku portów przez zmniejszenie czasu dla stanu TCP TIME_WAIT (domyślnie do 240 sekund), jak opisano w wielu miejscach. Powinno to zwolnić porty szybciej. Najpierw byłem trochę niezdecydowany w tej sprawie, ponieważ używamy zarówno długich zapytań do baz danych, jak i wywołań WCF na TCP i nie chciałbym ograniczać ograniczenia czasowego. Chociaż nie udało mi się jeszcze złapać mojego odczytu stanu maszyny TCP, myślę, że w końcu nie będzie to problemem. Myślę, że stan TIME_WAIT jest tylko po to, aby umożliwić uzgadnianie właściwego zamknięcia klienta. W związku z tym rzeczywisty transfer danych na istniejącym połączeniu TCP nie powinien przekroczyć limitu czasu. W gorszym scenariuszu klient nie jest poprawnie zamykany i zamiast tego przestaje działać. Myślę, że wszystkie przeglądarki mogą nie implementować tego poprawnie i może to być problem tylko po stronie klienta. Chociaż trochę tu zgaduję ...

KONIEC AKTUALIZACJI 19/2, 2013

UPDATE 24/4, 2013: Zwiększyliśmy liczbę portów do maksymalnej wartości. Jednocześnie nie otrzymujemy tylu przekazanych żądań wychodzących, co wcześniej. Te dwie kombinacje powinny być powodem, dla którego nie mieliśmy żadnych incydentów. Jest to jednak tylko tymczasowe, ponieważ liczba żądań wychodzących z pewnością ponownie wzrośnie w przyszłości na tych serwerach. Problem polega więc, jak sądzę, na tym, że port dla przychodzących żądań musi pozostać otwarty w czasie dla odpowiedzi na przekazane żądania. W naszej aplikacji ten limit anulowania dla tych przekazywanych żądań wynosi 120 ms, co można porównać z normalnym <1 ms, aby obsłużyć nie przekazane żądanie. Zasadniczo uważam, że określona liczba portów jest głównym wąskim gardłem skalowalności na takich serwerach o wysokiej przepustowości (> 1000 żądań na sekundę na maszynach o 16 rdzeniach), których używamy. W połączeniu z pracą GC przy przeładowaniu pamięci podręcznej (poniżej) sprawia, że ​​serwer jest szczególnie wulgarny.

KONIEC AKTUALIZACJI 24/4

3) Zbyt powolna alokacja zasobów

Nasze liczniki wydajności pokazują, że liczba żądań w kolejce w puli wątków (1B) ulega znacznym wahaniom w czasie wystąpienia problemu. Więc potencjalnie oznacza to, że mamy dynamiczną sytuację, w której długość kolejki zaczyna oscylować z powodu zmian w środowisku. Na przykład byłoby tak, gdyby istniały mechanizmy zabezpieczające przed zalaniem, które są aktywowane w czasie zalewania ruchu. W rzeczywistości mamy kilka takich mechanizmów:

3.A) Równoważenie obciążenia sieci

Gdy sprawy idą naprawdę źle, a serwer odpowiada błędem HTTP 503, system równoważenia obciążenia automatycznie usunie serwer WWW z produkcji w okresie 15 sekund. Oznacza to, że inne serwery zwiększą obciążenie w czasie. Podczas „okresu chłodzenia” serwer może zakończyć obsługę żądania i zostanie automatycznie przywrócony, gdy system równoważenia obciążenia wykona następny ping. Oczywiście jest to dobre tylko wtedy, gdy wszystkie serwery nie mają problemu od razu. Na szczęście do tej pory nie byliśmy w takiej sytuacji.

3.B) Zawór specyficzny dla aplikacji

W aplikacji internetowej mamy własny skonstruowany zawór (tak. Jest to „zawór”. Nie „wartość”) wyzwalany przez licznik wydajności systemu Windows dla żądań kolejkowanych w puli wątków. W aplikacji ApplicationStart istnieje wątek, który sprawdza wartość licznika wydajności co sekundę. A jeśli wartość przekracza 2000, cały ruch wychodzący przestaje być inicjowany. W następnej sekundzie, jeśli wartość kolejki jest mniejsza niż 2000, ruch wychodzący rozpoczyna się ponownie.

Dziwną rzeczą jest to, że nie pomogło nam to w osiągnięciu scenariusza błędu, ponieważ nie mamy zbyt wielu danych na ten temat. Może to oznaczać, że gdy ruch uderza w nas mocno, rzeczy bardzo szybko się psują, więc sprawdzenie 1-sekundowego przedziału czasowego jest rzeczywiście zbyt wysokie.

3.C) Pula wątków powoli zwiększa (i zmniejsza) wątki

Jest też inny aspekt tego. Gdy potrzeba więcej wątków w puli aplikacji, wątki te są przydzielane bardzo powoli. Z tego co czytałem, 1-2 wątki na sekundę. Dzieje się tak, ponieważ tworzenie wątków jest kosztowne, a ponieważ i tak nie chcesz zbyt wielu wątków, aby uniknąć kosztownego przełączania kontekstu w przypadku synchronicznym, myślę, że jest to naturalne. Powinno to jednak również oznaczać, że w przypadku nagłego dużego ruchu trafia do nas, liczba wątków nie będzie wystarczająco bliska, aby zaspokoić potrzebę asynchronicznego scenariusza i rozpocznie się kolejkowanie żądań. Myślę, że to bardzo prawdopodobny kandydat na problem. Jednym z potencjalnych rozwiązań może być wtedy zwiększenie minimalnej ilości utworzonych wątków w Puli wątków. Ale myślę, że może to również wpływać na wydajność synchronicznie uruchomionych żądań.

4) Problemy z pamięcią

(Joey Reyes napisał o tymtutaj w blogu) Ponieważ obiekty są gromadzone później dla żądań asynchronicznych (do 120ms później w naszym przypadku), może pojawić się problem z pamięcią, ponieważ obiekty mogą być promowane do generacji 1, a pamięć nie będzie ponownie zbierana tak często, jak powinna. Zwiększone ciśnienie w Garbage Collector może bardzo dobrze spowodować, że nastąpi zmiana kontekstu rozszerzonego wątku i dalsze osłabienie pojemności serwera.

Nie widzimy jednak zwiększonego wykorzystania GC ani procesora w czasie wystąpienia problemu, więc nie sądzimy, aby sugerowany mechanizm ograniczania wydajności procesora był dla nas rozwiązaniem.

AKTUALIZACJA 19/2, 2013: Używamy mechanizmu wymiany pamięci podręcznej w regularnych interwałach, w których (prawie) pełna pamięć podręczna w pamięci jest ładowana do pamięci, a stara pamięć podręczna może pobierać śmieci. W tym czasie GC będzie musiało ciężej pracować i kraść zasoby z normalnej obsługi żądań. Używając licznika wydajności systemu Windows do przełączania kontekstu wątku, pokazuje, że liczba przełączeń kontekstu znacznie się zmniejsza od normalnej wysokiej wartości w czasie wysokiego użycia GC. Myślę, że podczas takich przeładowań pamięci podręcznej serwer jest dodatkowo podatny na kolejkowanie żądań i konieczne jest zmniejszenie śladu GC. Jednym z potencjalnych rozwiązań tego problemu byłoby wypełnienie pamięci podręcznej bez przydzielania pamięci przez cały czas. Trochę więcej pracy, ale powinno być wykonalne.

UPDATE 24/4, 2013: Nadal jestem w trakcie dostosowywania pamięci przeładowania pamięci podręcznej, aby uniknąć tak dużego działania GC. Zwykle mamy czasowo około 1000 żądań w kolejce, gdy działa GC. Ponieważ działa na wszystkich wątkach, naturalnie kradnie zasoby z normalnej obsługi żądań. Zaktualizuję ten stan po wdrożeniu tej zmiany i zobaczymy różnicę.

KONIEC AKTUALIZACJI 24/4

questionAnswers(2)

yourAnswerToTheQuestion