Kiedy wymagana jest opcja TCP SO_LINGER (0)?

96

Myślę, że rozumiem formalne znaczenie tej opcji. W jakimś starszym kodzie, który teraz obsługuję, używana jest opcja. Klient narzeka na RST jako odpowiedź dla FIN ze swojej strony na połączenie zamknięte z jego strony.

Nie jestem pewien, czy mogę go bezpiecznie usunąć, ponieważ nie rozumiem, kiedy należy go użyć.

Czy możesz podać przykład, kiedy ta opcja byłaby wymagana?

dimba
źródło
1
Powinieneś to usunąć. Nie powinno być używane w kodzie produkcyjnym. Jedyny raz, kiedy widziałem, że jest używany, był wynikiem nieprawidłowego testu porównawczego.
Markiz Lorne

Odpowiedzi:

83

Typowym powodem ustawienia SO_LINGERlimitu czasu na zero jest uniknięcie dużej liczby połączeń znajdujących się w TIME_WAITstanie, które zajmują wszystkie dostępne zasoby na serwerze.

Gdy połączenie TCP zostanie prawidłowo zamknięte, koniec, który zainicjował zamknięcie („aktywne zamknięcie”), kończy się utrzymaniem połączenia TIME_WAITprzez kilka minut. Więc jeśli twój protokół to taki, w którym serwer inicjuje połączenie blisko i obejmuje bardzo dużą liczbę krótkotrwałych połączeń, może być podatny na ten problem.

Nie jest to jednak dobry pomysł - TIME_WAITistnieje z jakiegoś powodu (aby zapewnić, że zbłąkane pakiety ze starych połączeń nie będą kolidować z nowymi połączeniami). Lepszym pomysłem jest przeprojektowanie protokołu na taki, w którym klient inicjuje połączenie, jeśli jest to możliwe.

kawiarnia
źródło
4
W pełni się zgadzam. Widziałem aplikację monitorującą, która inicjowała wiele (kilka tysięcy krótkotrwałych połączeń co X sekund) i miała problem ze skalowaniem (o tysiąc połączeń więcej). Nie wiem dlaczego, ale aplikacja nie reagowała. Ktoś zasugerował SO_LINGER = true, TIME_WAIT = 0, aby szybko zwolnić zasoby systemu operacyjnego i po krótkim badaniu wypróbowaliśmy to rozwiązanie z bardzo dobrymi wynikami. TIME_WAIT nie stanowi już problemu dla tej aplikacji.
bartosz.r
24
Nie zgadzam się. Protokół poziomu aplikacji znajdujący się na szczycie TCP powinien być zaprojektowany w taki sposób, aby klient zawsze inicjował zamknięcie połączenia. W ten sposób TIME_WAITwola usiądzie na kliencie, nie czyniąc krzywdy. Pamiętaj, jak jest napisane w trzeciej edycji „UNIX Network Programming” (Stevens i in.) Na stronie 203: „Stan TIME_WAIT jest Twoim przyjacielem i jest po to, aby nam pomóc. Zamiast próbować go unikać, powinniśmy to zrozumieć (sekcja 2.7) ”.
mgd
8
A co, jeśli klient chce otwierać 4000 połączeń co 30 sekund (ta aplikacja monitorująca jest klientem! Ponieważ inicjuje połączenie)? Tak, możemy przeprojektować aplikację, dodać lokalnych agentów w infrastrukturze, zmienić model na push. Ale jeśli mamy już taką aplikację i rośnie, to możemy ją uruchomić, dostrajając twe linger. Zmieniasz jeden parametr i nagle masz działającą aplikację, bez inwestowania budżetu na wdrożenie nowej architektury.
bartosz.r
4
@ bartosz.r: Mówię tylko, że używanie SO_LINGER z timeoutem 0 powinno być naprawdę ostatecznością. Ponownie, w trzecim wydaniu „UNIX Network Programming” (Stevens i in.) Strona 203 jest również napisane, że istnieje ryzyko uszkodzenia danych. Rozważ przeczytanie RFC 1337, gdzie możesz zobaczyć, dlaczego TIME_WAIT jest Twoim przyjacielem.
mgd
7
@caf Nie, klasycznym rozwiązaniem byłaby pula połączeń, jak widać w każdym wydajnym interfejsie TCP API, na przykład HTTP 1.1.
Markiz Lorne
191

Aby uzyskać moją sugestię, przeczytaj ostatnią sekcję: „Kiedy używać SO_LINGER z limitem czasu 0” .

Zanim przejdziemy do tego małego wykładu o:

  • Normalne zakończenie TCP
  • TIME_WAIT
  • FIN, ACKiRST

Normalne zakończenie TCP

Normalna sekwencja zakończenia TCP wygląda następująco (uproszczona):

Mamy dwóch rówieśników: A i B.

  1. Wzywa close()
    • A wysyła FINdo B
    • A wchodzi w FIN_WAIT_1stan
  2. B otrzymuje FIN
    • B wysyła ACKdo A
    • B wchodzi w CLOSE_WAITstan
  3. A otrzymuje ACK
    • A wchodzi w FIN_WAIT_2stan
  4. B woła close()
    • B wysyła FINdo A
    • B wchodzi w LAST_ACKstan
  5. A otrzymuje FIN
    • A wysyła ACKdo B
    • A wchodzi w TIME_WAITstan
  6. B otrzymuje ACK
    • B przechodzi do CLOSEDstanu - tj. Jest usuwany z tablic gniazd

CZAS OCZEKIWANIA

Zatem peer, który zainicjuje zakończenie - tj. Wywołuje close()pierwszy - znajdzie się w TIME_WAITstanie.

Aby zrozumieć, dlaczego TIME_WAITpaństwo jest naszym przyjacielem, przeczytaj sekcję 2.7 w trzecim wydaniu „UNIX Network Programming” autorstwa Stevensa i wsp. (Strona 43).

Jednak może to być problem z wieloma TIME_WAITstanami gniazd na serwerze, ponieważ może to ostatecznie uniemożliwić akceptowanie nowych połączeń.

Aby obejść ten problem, widziałem wielu sugerujących ustawienie opcji gniazda SO_LINGER z limitem czasu 0 przed wywołaniem close(). Jest to jednak złe rozwiązanie, ponieważ powoduje zakończenie połączenia TCP z błędem.

Zamiast tego zaprojektuj protokół aplikacji, tak aby zakończenie połączenia było zawsze inicjowane po stronie klienta. Jeśli klient zawsze wie, kiedy odczytał wszystkie pozostałe dane, może zainicjować sekwencję zakończenia. Na przykład przeglądarka wie z Content-Lengthnagłówka HTTP, kiedy przeczytała wszystkie dane i może zainicjować zamknięcie. (Wiem, że w HTTP 1.1 pozostawi go otwarty przez chwilę w celu ewentualnego ponownego użycia, a następnie zamknie).

Jeśli serwer musi zamknąć połączenie, zaprojektuj protokół aplikacji tak, aby serwer poprosił klienta o połączenie close().

Kiedy używać SO_LINGER z limitem czasu 0

Ponownie, zgodnie z trzecim wydaniem strony 202-203 „Programowanie sieciowe UNIX”, ustawienie SO_LINGERz limitem czasu 0 przed wywołaniem close()spowoduje, że normalna sekwencja kończenia nie zostanie zainicjowana.

Zamiast tego, peer ustawiając tę ​​opcję i wywołując close(), wyśle RST(reset połączenia), który wskazuje stan błędu i tak będzie to postrzegane po drugiej stronie. Zazwyczaj pojawiają się błędy, takie jak „Resetowanie połączenia przez partnera”.

Dlatego w normalnej sytuacji jest naprawdę złym pomysłem ustawienie SO_LINGERlimitu czasu 0 przed wywołaniem close()- od teraz nazywanym nieudanym zamknięciem - w aplikacji serwerowej.

Jednak pewna sytuacja i tak to uzasadnia:

  • Jeśli klient twojej aplikacji serwerowej zachowuje się nieprawidłowo (przekracza limit czasu, zwraca nieprawidłowe dane itp.), Nieudane zamknięcie ma sens, aby uniknąć utknięcia CLOSE_WAITlub wylądowania w tym TIME_WAITstanie.
  • Jeśli musisz zrestartować aplikację serwera, która obecnie ma tysiące połączeń klientów, możesz rozważyć ustawienie tej opcji gniazda, aby uniknąć tysięcy gniazd serwera TIME_WAIT(podczas wywoływania close()z końca serwera), ponieważ może to uniemożliwić serwerowi uzyskanie dostępnych portów dla nowych połączeń klientów po ponownym uruchomieniu.
  • Na stronie 202 wspomnianej książki jest wyraźnie napisane: „Istnieją pewne okoliczności, które uzasadniają użycie tej funkcji do wysłania nieudanego zamknięcia. Jednym z przykładów jest serwer terminali RS-232, który może zawieszać się na zawsze, CLOSE_WAITpróbując dostarczyć dane do zablokowanego terminala port, ale poprawnie zresetowałby zablokowany port, gdyby miał RSTodrzucić oczekujące dane. "

Poleciłbym ten długi artykuł, który moim zdaniem jest bardzo dobrą odpowiedzią na Twoje pytanie.

mgd
źródło
6
TIME_WAITjest przyjacielem tylko wtedy, gdy nie zaczyna sprawiać problemów: stackoverflow.com/questions/1803566/…
Pacerier
2
a co jeśli piszesz serwer WWW? jak „powiedzieć klientowi, aby zainicjował zamknięcie”?
Shaun Neal
2
@ShaunNeal, oczywiście, że nie. Ale dobrze napisany klient / przeglądarka zainicjuje zamknięcie. Jeśli klient nie zachowuje się dobrze, na szczęście mamy zabójstwo TIME_WAIT, aby upewnić się, że nie zabraknie nam deskryptorów gniazd i efemerycznych portów.
mgd
To doskonała odpowiedź. Dziękuję Ci!
Juraj Martinka
17

Gdy linger jest włączony, ale limit czasu wynosi zero, stos TCP nie czeka na wysłanie oczekujących danych przed zamknięciem połączenia. Z tego powodu dane mogą zostać utracone, ale ustawiając tę ​​opcję w ten sposób, akceptujesz to i prosisz, aby połączenie zostało zresetowane od razu, a nie z wdziękiem. Powoduje to wysłanie RST zamiast zwykłego FIN.

Podziękowania dla EJP za komentarz, zobacz tutaj po szczegóły.

Len Holgate
źródło
1
Rozumiem to. proszę o "realistyczny" przykład, kiedy chcielibyśmy użyć twardego resetu.
dimba
5
Zawsze, gdy chcesz przerwać połączenie; więc jeśli twój protokół nie przejdzie walidacji i masz klienta gadającego bzdury, nagle przerwałbyś połączenie z RST itp.
Len Holgate
5
Mylisz zerowy czas zwłoki ze zwłoką. Linger off oznacza, że ​​close () nie blokuje. Utrzymanie się z dodatnim limitem czasu oznacza, że ​​close () blokuje się aż do limitu czasu. Utrzymywanie się z zerowym limitem czasu powoduje RST i właśnie o to chodzi.
Markiz Lorne
2
Tak, masz rację. Dostosuję odpowiedź, aby poprawić moją terminologię.
Len Holgate
6

To, czy możesz bezpiecznie usunąć zwłoki w kodzie, czy nie, zależy od typu twojej aplikacji: czy jest to „klient” (najpierw otwiera połączenia TCP i aktywnie je zamyka), czy też jest to „serwer” (nasłuchujący otwartego protokołu TCP i zamknięcie go po tym, jak druga strona zainicjowała zamknięcie)?

Jeśli Twoja aplikacja ma posmak „klienta” (najpierw zamykasz) ORAZ inicjujesz i zamykasz ogromną liczbę połączeń z różnymi serwerami (np. Gdy Twoja aplikacja jest aplikacją monitorującą nadzorującą osiągalność ogromnej liczby różnych serwerów), Twoja aplikacja ma problem polegający na tym, że wszystkie połączenia klientów utknęły w stanie TIME_WAIT. Następnie zalecałbym skrócenie limitu czasu do wartości mniejszej niż wartość domyślna, aby nadal bezpiecznie zamykać, ale wcześniej zwolnić zasoby połączeń klienta. Nie ustawiłbym limitu czasu na 0, ponieważ 0 nie zamyka wdzięcznie z FIN, ale przerywa działanie z RST.

Jeśli twoja aplikacja ma posmak „klienta” i musi pobierać ogromną liczbę małych plików z tego samego serwera, nie powinieneś inicjować nowego połączenia TCP na plik i kończyć się ogromną liczbą połączeń klienta w CZAS_CZEKIWANIE, ale utrzymuj połączenie otwarte i pobieraj wszystkie dane przez to samo połączenie. Opcję Linger można i należy usunąć.

Jeśli Twoja aplikacja jest „serwerem” (blisko sekundy jako reakcja na zamknięcie peera), przy close () twoje połączenie jest zamykane z wdziękiem, a zasoby są zwalniane, ponieważ nie wchodzisz w stan TIME_WAIT. Nie należy używać Linger. Ale jeśli aplikacja na serwerze ma proces nadzorczy wykrywający nieaktywne otwarte połączenia, które pozostają w stanie bezczynności przez długi czas (należy zdefiniować „długie”), możesz zamknąć to nieaktywne połączenie ze swojej strony - potraktować to jako rodzaj obsługi błędów - z nieudanym zamknięciem. Odbywa się to poprzez ustawienie limitu czasu lingera na 0. Funkcja close () wyśle ​​następnie RST do klienta, informując go, że jesteś zły :-)

Grandswiss
źródło
1

Na serwerach możesz chcieć wysyłać RSTzamiast FINpodczas odłączania źle działających klientów. Że przeskakuje FIN-WAITnastępnie TIME-WAITstanach gniazda w serwerze, który zapobiega wyczerpywania zasobów serwera, a tym samym chroni przed tego typu denial-of-service atak.

Maxim Egorushkin
źródło
1

Podoba mi się obserwacja Maxima, że ​​ataki DOS mogą wyczerpać zasoby serwera. Dzieje się tak również bez faktycznie złośliwego przeciwnika.

Niektóre serwery muszą radzić sobie z „niezamierzonym atakiem DOS”, który ma miejsce, gdy aplikacja kliencka ma błąd związany z wyciekiem połączenia, w którym nadal tworzą nowe połączenie dla każdego nowego polecenia wysyłanego do serwera. A potem być może ostatecznie zamknięcie ich połączeń, jeśli uderzą w ciśnienie GC, lub może połączenia w końcu przekroczą limit czasu.

Inny scenariusz to scenariusz „wszyscy klienci mają ten sam adres TCP”. Wtedy połączenia klientów można rozróżnić tylko po numerach portów (jeśli łączą się z pojedynczym serwerem). A jeśli klienci zaczną szybko otwierać / zamykać połączenia z jakiegokolwiek powodu, mogą wyczerpać przestrzeń krotek (adres klienta + port, adres IP serwera + port).

Myślę więc, że najlepiej byłoby, gdyby serwery przełączyły się na strategię Linger-Zero, gdy widzą dużą liczbę gniazd w stanie TIME_WAIT - chociaż nie naprawia to zachowania klienta, może zmniejszyć wpływ.

Tim Lovell-Smith
źródło
0

Gniazdo nasłuchujące na serwerze może używać linger z czasem 0, aby mieć dostęp do natychmiastowego wiązania z powrotem z gniazdem i resetowania wszystkich klientów, których połączenia nie zostały jeszcze zakończone. TIME_WAIT to coś, co jest interesujące tylko wtedy, gdy masz sieć wielościeżkową i możesz skończyć z błędnie uporządkowanymi pakietami lub w inny sposób masz do czynienia z dziwnym porządkowaniem pakietów sieciowych / czasem przybycia.

Gregg Wonderly
źródło