Udostępnij przez


Opóźnienie sieci i przepływność

Trzy główne problemy odnoszą się do optymalnego wykorzystania sieci:

  • Opóźnienie sieci
  • Nasycenie sieci
  • Implikacje dotyczące przetwarzania pakietów

W tej sekcji przedstawiono zadanie programistyczne wymagające użycia procedury RPC, a następnie projektuje dwa rozwiązania: jedną źle napisaną i jedną dobrze napisaną. Oba rozwiązania są następnie analizowane, a ich wpływ na wydajność sieci jest omówiony.

Przed omówieniem dwóch rozwiązań w kilku kolejnych sekcjach omówiono i wyjaśniliśmy problemy z wydajnością związane z siecią.

Opóźnienie sieci

Przepustowość sieci i opóźnienie sieci są oddzielnymi terminami. Sieci o wysokiej przepustowości nie gwarantują małych opóźnień. Na przykład ścieżka sieciowa przechodząca przez łącze satelitarne często ma duże opóźnienie, mimo że przepływność jest bardzo wysoka. Nie jest niczym niezwykłym w przypadku przechodzenia przez sieć przez łącze satelitarne, aby mieć co najmniej pięć sekund opóźnienia. Implikacją takiego opóźnienia jest to: aplikacja zaprojektowana do wysyłania żądania, czekania na odpowiedź, wysyłania innego żądania, oczekiwania na inną odpowiedź itd., będzie czekać co najmniej pięć sekund na każdą wymianę pakietów, niezależnie od tego, jak szybko jest serwer. Pomimo rosnącej szybkości komputerów transmisje satelitarne i nośniki sieciowe są oparte na szybkości światła, które na ogół pozostają stałe. W związku z tym poprawa opóźnienia istniejących sieci satelitarnych jest mało prawdopodobna.

Nasycenie sieci

Niektóre nasycenie występuje w wielu sieciach. Najprostszymi sieciami do saturacji są powolne łącza modemów, takie jak standardowe modemy analogowy 56k. Jednak połączenia Ethernet z wieloma komputerami w jednym segmencie mogą również stać się nasycone. Dotyczy to również sieci szerokich obszarów o niskiej przepustowości lub w inny sposób przeciążone łącze, takie jak router lub przełącznik, który może obsługiwać ograniczoną ilość ruchu. W takich przypadkach, jeśli sieć wysyła więcej pakietów niż jego najsłabszy link może obsłużyć, odrzuca pakiety. Aby uniknąć przeciążenia, stos TCP systemu Windows jest skalowany z powrotem po wykryciu porzuconych pakietów, co może spowodować znaczne opóźnienia.

Implikacje dotyczące przetwarzania pakietów

Gdy programy są opracowywane dla środowisk wyższego poziomu, takich jak RPC, COM, a nawet Windows Sockets, deweloperzy mają tendencję do zapominania, ile pracy odbywa się w tle dla każdego wysłanego lub odebranych pakietów. Po odebraniu pakietu z sieci przerwanie karty sieciowej jest obsługiwane przez komputer. Następnie wywołanie procedury odroczonej (DPC) jest kolejkowane i musi przejść przez sterowniki. Jeśli jest używana jakakolwiek forma zabezpieczeń, może być konieczne odszyfrowywanie pakietu lub zweryfikowany skrót kryptograficzny. W każdym stanie należy również przeprowadzić szereg kontroli ważności. Tylko wtedy pakiet dociera do końcowego miejsca docelowego: kod serwera. Wysyłanie wielu małych fragmentów danych powoduje obciążenie przetwarzania pakietów dla każdego małego fragmentu danych. Wysyłanie jednego dużego fragmentu danych zwykle zużywa znacznie mniej czasu procesora CPU w całym systemie, mimo że koszt wykonywania dla wielu małych fragmentów w porównaniu z jednym dużym fragmentem może być taki sam w przypadku aplikacji serwera.

Przykład 1: źle zaprojektowany serwer RPC

Wyobraź sobie aplikację, która musi uzyskiwać dostęp do plików zdalnych, a zadaniem jest zaprojektowanie interfejsu RPC do manipulowania plikiem zdalnym. Najprostszym rozwiązaniem jest dublowanie procedur plików studio dla plików lokalnych. Może to spowodować zwodniczo czysty i znajomy interfejs. Oto skrócony plik idl:

typedef [context_handle] void *remote_file;
... .
interface remote_file
{
    remote_file remote_fopen(file_name);
    void remote_fclose(remote_file ...);
    size_t remote_fread(void *, size_t, size_t, remote_file ...);
    size_t remote_fwrite(const void *, size_t, size_t, remote_file ...);
    size_t remote_fseek(remote_file ..., long, int);
}

Wydaje się to wystarczająco eleganckie, ale w rzeczywistości jest to uhonorowany przepis na awarię wydajności. W przeciwieństwie do popularnej opinii, zdalne wywołanie procedury nie jest po prostu lokalnym wywołaniem procedury z przewodem między obiektem wywołującym i wywoływanym.

Aby zobaczyć, jak ten przepis spala wydajność, rozważ plik 2K, w którym od początku są odczytywane 20 bajtów, a następnie 20 bajtów od końca i zobacz, jak to działa. Po stronie klienta są wykonywane następujące wywołania (wiele ścieżek kodu jest pomijanych w celu zwięzłości):

rfp = remote_fopen("c:\\sample.txt");
remote_read(...);
remote_fseek(...);
remote_read(...);
remote_fclose(rfp);

Teraz wyobraź sobie, że serwer jest oddzielony od klienta przez połączenie satelitarne z pięciosekundowym czasem rundy. Każde z tych wywołań musi czekać na odpowiedź, zanim będzie mogła kontynuować, co oznacza absolutne minimum do wykonania tej sekwencji 25 sekund. Biorąc pod uwagę, że pobieramy tylko 40 bajtów, jest to skandalicznie powolne działanie. Klienci tej aplikacji będą wściekłi.

Teraz wyobraź sobie, że sieć jest nasycona, ponieważ pojemność routera w ścieżce sieciowej jest przeciążona. Ten projekt wymusza, aby router obsługiwał co najmniej 10 pakietów, jeśli nie mamy zabezpieczeń (jeden dla każdego żądania i jeden dla każdej odpowiedzi). To też nie jest dobre.

Ten projekt wymusza również na serwerze odbieranie pięciu pakietów i wysyłanie pięciu pakietów. Ponownie nie jest to bardzo dobra implementacja.

Przykład 2: lepiej zaprojektowany serwer RPC

Przeprojektujmy interfejs omówiony w przykładzie 1 i sprawdźmy, czy możemy go ulepszyć. Należy pamiętać, że fakt, że ten serwer jest naprawdę dobry, wymaga znajomości wzorca użycia dla danych plików: taka wiedza nie jest zakładana na potrzeby tego przykładu. W związku z tym jest to lepiej zaprojektowany serwer RPC, ale nie optymalnie zaprojektowany serwer RPC.

W tym przykładzie chodzi o zwinięcie jak największej liczby operacji zdalnych do jednej operacji. Pierwsza próba jest następująca:

typedef [context_handle] void *remote_file;
typedef struct
{
    long position;
    int origin;
} remote_seek_instruction;
... .
interface remote_file
{
    remote_fread(file_name, void *, size_t, size_t, [in, out] remote_file ..., BOOL CloseWhenDone, remote_seek_instruction *...);
    size_t remote_fwrite(file_name, const void *, size_t, size_t, [in, out] remote_file ..., BOOL CloseWhenDone, remote_seek_instruction *...);
}

W tym przykładzie wszystkie operacje są zwijane do odczytu i zapisu, co umożliwia opcjonalne otwieranie w ramach tej samej operacji, a także opcjonalne zamknięcie i wyszukiwanie.

Ta sama sekwencja operacji, zapisana w skróconej postaci, wygląda następująco:

remote_read("c:\\sample.txt", ..., &rfp, FALSE, NULL);
remote_read(NULL, ..., &rfp, TRUE, seek_to_20_bytes_before_end);

Biorąc pod uwagę lepiej zaprojektowany serwer RPC, podczas drugiego wywołania serwer sprawdza, czy file_name jest null, i używa przechowywanego otwartego pliku w rfp. Następnie zobaczy, że istnieją instrukcje wyszukiwania i umieści wskaźnik pliku 20 bajtów przed jego odczytem. Po zakończeniu zostanie rozpoznana flaga CloseWhenDone ustawiona na true, a następnie zamknie plik i zamknij rfp.

W sieci o dużym opóźnieniu ta lepsza wersja trwa 10 sekund (2,5 razy szybciej) i wymaga przetwarzania tylko czterech pakietów; dwa odbiera z serwera, a dwa wysyła z serwera. Dodatkowe , jeśli i unmarshaling serwer działa, są nieznaczne w porównaniu do wszystkiego innego.

Jeśli kolejność przyczynowa jest określona prawidłowo, interfejs można nawet wykonać asynchronicznie, a dwa wywołania mogą być wysyłane równolegle. Gdy kolejność przyczynowa jest używana, wywołania są nadal wysyłane w kolejności, co oznacza, że w sieci o dużym opóźnieniu występuje tylko pięciosekundowe opóźnienie, mimo że liczba wysłanych i odebranych pakietów jest taka sama.

Możemy to jeszcze bardziej zwinąć, tworząc jedną metodę, która przyjmuje tablicę struktur, każdy element członkowski tablicy opisujący określoną operację pliku; zdalna odmiana punktowego/zbierania operacji we/wy. Podejście opłaca się tak długo, jak wynik każdej operacji nie wymaga dalszego przetwarzania na kliencie; innymi słowy, aplikacja odczytuje 20 bajtów na końcu niezależnie od tego, czym są odczytane pierwsze 20 bajtów.

Jeśli jednak niektóre operacje muszą być wykonywane na pierwszych 20 bajtach po odczytaniu ich w celu ustalenia następnej operacji, zwijanie wszystkiego w jedną operację nie działa (przynajmniej nie we wszystkich przypadkach). Elegancja RPC polega na tym, że aplikacja może mieć obie metody w interfejsie i wywołać jedną z metod w zależności od potrzeb.

Ogólnie rzecz biorąc, jeśli sieć jest zaangażowana, najlepiej połączyć jak najwięcej wywołań na jedno wywołanie, jak to możliwe. Jeśli aplikacja ma dwie niezależne działania, użyj operacji asynchronicznych i pozwól im działać równolegle. Zasadniczo zachowaj pełny potok.