Mamy interfejs API, który jest implementowany za pomocą ServiceStack, który jest hostowany w IIS. Podczas przeprowadzania testu obciążenia interfejsu API odkryliśmy, że czasy odpowiedzi są dobre, ale szybko się pogarszają, gdy tylko trafimy około 3500 jednoczesnych użytkowników na serwer. Mamy dwa serwery, a gdy uderzasz je 7000 użytkowników, średni czas odpowiedzi wynosi poniżej 500 ms dla wszystkich punktów końcowych. Skrzynki znajdują się za modułem równoważenia obciążenia, więc otrzymujemy 3500 współbieżnych na serwer. Jednak gdy tylko zwiększymy liczbę jednoczesnych użytkowników, zauważymy znaczny wzrost czasu reakcji. Zwiększenie liczby jednoczesnych użytkowników do 5000 na serwer daje nam średni czas odpowiedzi na punkt końcowy około 7 sekund.
Pamięć i procesor na serwerach są dość niskie, zarówno przy dobrych czasach odpowiedzi, jak i po ich pogorszeniu. W szczytowym momencie przy 10 000 równoczesnych użytkowników procesor średnio osiąga nieco poniżej 50%, a pamięć RAM zajmuje około 3-4 GB z 16. To sprawia, że myślimy, że osiągamy gdzieś jakiś limit. Poniższy zrzut ekranu pokazuje niektóre kluczowe liczniki w perfmon podczas testu obciążenia z udziałem łącznie 10.000 użytkowników. Podświetlony licznik to żądania / sekundę. Po prawej stronie zrzutu ekranu widać, że żądania na sekundę stają się bardzo nieregularne. Jest to główny wskaźnik długiego czasu reakcji. Jak tylko zobaczymy ten wzór, zauważamy powolne czasy reakcji w teście obciążenia.
Jak przejść do rozwiązania tego problemu z wydajnością? Próbujemy ustalić, czy jest to problem z kodowaniem, czy problem z konfiguracją. Czy w web.config lub IIS są jakieś ustawienia, które mogłyby wyjaśnić to zachowanie? Pula aplikacji działa .NET v4.0, a wersja IIS to 7.5. Jedyną zmianą, którą wprowadziliśmy w ustawieniach domyślnych, jest aktualizacja wartości długości kolejki puli aplikacji z 1000 do 5000. Dodaliśmy także następujące ustawienia konfiguracji do pliku Aspnet.config:
<system.web>
<applicationPool
maxConcurrentRequestsPerCPU="5000"
maxConcurrentThreadsPerCPU="0"
requestQueueLimit="5000" />
</system.web>
Więcej szczegółów:
Celem interfejsu API jest łączenie danych z różnych źródeł zewnętrznych i powrót jako JSON. Obecnie używa implementacji pamięci podręcznej InMemory do buforowania pojedynczych połączeń zewnętrznych w warstwie danych. Pierwsze żądanie do zasobu pobierze wszystkie wymagane dane, a wszelkie kolejne żądania dla tego samego zasobu uzyskają wyniki z pamięci podręcznej. Mamy program uruchamiający pamięć podręczną, który jest wdrażany jako proces w tle, który aktualizuje informacje w pamięci podręcznej w określonych odstępach czasu. Dodaliśmy blokowanie wokół kodu, który pobiera dane z zasobów zewnętrznych. Wdrożyliśmy również usługi do pobierania danych ze źródeł zewnętrznych w sposób asynchroniczny, dzięki czemu punkt końcowy powinien być tak wolny jak najwolniejsze wywołanie zewnętrzne (chyba że mamy dane w pamięci podręcznej). Odbywa się to za pomocą klasy System.Threading.Tasks.Task.Czy możemy przekroczyć limit liczby wątków dostępnych dla tego procesu?