paint-brush
TCP Yeniden İletimlerini Kontrol Edin: Veri Kaybını Önlemek için Sorunun Erken Tespitiile@koilas
823 okumalar
823 okumalar

TCP Yeniden İletimlerini Kontrol Edin: Veri Kaybını Önlemek için Sorunun Erken Tespiti

ile Oleg Tolmashov14m2024/01/23
Read on Terminal Reader

Çok uzun; Okumak

Bu makalede TCP iletişiminin kritik bir yönünü ele alacağım: sunucunun yanıt vermediği senaryoları etkili bir şekilde yönetmek. Uygulamanın, sunucudan uygulama düzeyinde herhangi bir yanıt almadan yalnızca TCP üzerinden veri gönderdiği belirli bir senaryoya odaklanıyorum. Bu inceleme, TCP iletişimini uygulama açısından ele alarak hem uygulama katmanını hem de temeldeki işletim sistemi işlemlerini vurgulamaktadır. Yanıt vermeyen sunucu örnekleri sırasında veri kaybını önlemek için etkili zaman aşımlarını nasıl ayarlayacağınızı öğreneceksiniz.
featured image - TCP Yeniden İletimlerini Kontrol Edin: Veri Kaybını Önlemek için Sorunun Erken Tespiti
Oleg Tolmashov HackerNoon profile picture
0-item
1-item

giriiş

Bu makalede TCP iletişiminin kritik bir yönünü ele alacağım: sunucunun yanıt vermediği senaryoları etkili bir şekilde yönetmek. Uygulamanın, sunucudan uygulama düzeyinde herhangi bir yanıt almadan yalnızca TCP üzerinden veri gönderdiği belirli bir senaryoya odaklanıyorum.


Bu inceleme, TCP iletişimini uygulama açısından ele alarak hem uygulama katmanını hem de temeldeki işletim sistemi işlemlerini vurgulamaktadır. Yanıt vermeyen sunucu örnekleri sırasında veri kaybını önlemek için etkili zaman aşımlarını nasıl ayarlayacağınızı öğreneceksiniz. Ruby'de kod örnekleri vereceğim, ancak fikir her dil için aynı kalacak.

Sessiz TCP Sunucularının Zorlukları

Verileri sürekli olarak TCP soketi üzerinden ileten bir uygulamayla çalıştığınızı hayal edin. TCP, tanımlanmış TCP yığın yapılandırmaları dahilinde aktarım düzeyinde paket teslimatını sağlamak üzere tasarlanmış olsa da, bunun uygulama düzeyinde ne anlama geldiğini düşünmek ilginçtir.


Bunu daha iyi anlamak için Ruby kullanarak örnek bir TCP sunucusu ve istemcisi oluşturalım. Bu, iletişim sürecini çalışırken gözlemlememize olanak sağlayacaktır.


server.rb :

 # server.rb require 'socket' require 'time' $stdout.sync = true puts 'starting tcp server...' server = TCPServer.new(1234) puts 'started tcp server on port 1234' loop do Thread.start(server.accept) do |client| puts 'new client' while (message = client.gets) puts "#{Time.now}]: #{message.chomp}" end client.close end end


Ve client.rb :

 require 'socket' require 'time' $stdout.sync = true socket = Socket.tcp('server', 1234) loop do puts "sending message to the socket at #{Time.now}" socket.write "Hello from client\n" sleep 1 end


Ve bu kurulumu şu Dockerfile kullanarak kaplara yerleştirelim:

 FROM ruby:2.7 RUN apt-get update && apt-get install -y tcpdump # Set the working directory in the container WORKDIR /usr/src/app # Copy the current directory contents into the container at /usr/src/app COPY . .


ve docker-compose.yml :

 version: '3' services: server: build: context: . dockerfile: Dockerfile command: ruby server.rb volumes: - .:/usr/src/app ports: - "1234:1234" healthcheck: test: ["CMD", "sh", "-c", "nc -z localhost 1234"] interval: 1s timeout: 1s retries: 2 networks: - net client: build: context: . dockerfile: Dockerfile command: ruby client.rb volumes: - .:/usr/src/app - ./data:/data depends_on: - server networks: - net networks: net:


Artık bunu docker compose up ile kolayca çalıştırabilir ve günlüklerde istemcinin mesajı nasıl gönderdiğini ve sunucunun mesajı nasıl aldığını görebiliriz:


 $ docker compose up [+] Running 2/0 ⠿ Container tcp_tests-server-1 Created 0.0s ⠿ Container tcp_tests-client-1 Created 0.0s Attaching to tcp_tests-client-1, tcp_tests-server-1 tcp_tests-server-1 | starting tcp server... tcp_tests-server-1 | started tcp server on port 1234 tcp_tests-client-1 | sending message to the socket at 2024-01-14 08:59:08 +0000 tcp_tests-server-1 | new client tcp_tests-server-1 | 2024-01-14 08:59:08 +0000]: Hello from client tcp_tests-server-1 | 2024-01-14 08:59:09 +0000]: Hello from client tcp_tests-client-1 | sending message to the socket at 2024-01-14 08:59:09 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 08:59:10 +0000 tcp_tests-server-1 | 2024-01-14 08:59:10 +0000]: Hello from client tcp_tests-client-1 | sending message to the socket at 2024-01-14 08:59:11 +0000 tcp_tests-server-1 | 2024-01-14 08:59:11 +0000]: Hello from client tcp_tests-client-1 | sending message to the socket at 2024-01-14 08:59:12 +0000 tcp_tests-server-1 | 2024-01-14 08:59:12 +0000]: Hello from client tcp_tests-client-1 | sending message to the socket at 2024-01-14 08:59:13 +0000

Şu ana kadar oldukça kolay, değil mi?


Ancak aktif bir bağlantı için sunucu arızasını simüle ettiğimizde durum daha da ilginçleşiyor.


Bunu docker compose stop server kullanarak yapıyoruz:


 tcp_tests-server-1 | 2024-01-14 09:04:23 +0000]: Hello from client tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:04:24 +0000 tcp_tests-server-1 | 2024-01-14 09:04:24 +0000]: Hello from client tcp_tests-server-1 exited with code 1 tcp_tests-server-1 exited with code 0 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:04:25 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:04:26 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:04:27 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:04:28 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:04:29 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:04:30 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:04:31 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:04:32 +0000


Sunucunun artık çevrimdışı olduğunu ancak istemcinin bağlantı hala aktifmiş gibi davrandığını ve tereddüt etmeden sokete veri göndermeye devam ettiğini görüyoruz.


Bu durum beni bunun neden oluştuğunu sorgulamaya yöneltiyor. Mantıksal olarak, TCP, paketleri için onay alamadığından ve bağlantının kapanmasına neden olduğundan, istemcinin sunucunun kapalı kalma süresini kısa bir süre içinde, muhtemelen birkaç saniye içinde algılaması gerekir.


Ancak gerçek sonuç bu beklentiden farklılaştı:

 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:20:11 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:20:12 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:20:13 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:20:14 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:20:15 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:20:16 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:20:17 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:20:18 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:20:19 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:20:20 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:20:21 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:20:22 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-14 09:20:23 +0000 tcp_tests-client-1 | client.rb:11:in `write': No route to host (Errno::EHOSTUNREACH) tcp_tests-client-1 | from client.rb:11:in `block in <main>' tcp_tests-client-1 | from client.rb:9:in `loop' tcp_tests-client-1 | from client.rb:9:in `<main>' tcp_tests-client-1 exited with code 1


Gerçekte, müşteri 15 dakika kadar bir süre boyunca kesintiye uğrayan bağlantıdan habersiz kalabilir!


Tespitteki bu gecikmeye ne sebep oluyor? Nedenlerini anlamak için daha derine inelim.

Ayrıntılı: TCP İletişim Mekaniği

Bu durumu tam olarak ele almak için, önce temel ilkeleri tekrar gözden geçirelim, ardından istemcinin verileri TCP üzerinden nasıl ilettiğini inceleyelim.

TCP'nin temelleri

TCP akışını gösteren temel şema aşağıda verilmiştir:

TCP akışı

Bağlantı kurulduğunda her mesajın iletimi genellikle iki temel adımdan oluşur:


  1. İstemci, PSH (Push) bayrağıyla işaretlenmiş mesajı gönderir.


  2. Sunucu, bir ACK (Onay) yanıtı göndererek alındığını onaylar.

Uygulama ile Soket Arasındaki İletişim

Aşağıda, bir uygulama tarafından bir TCP soketinin açılmasını ve ardından bu uygulama üzerinden veri aktarımını gösteren basitleştirilmiş bir sıra diyagramı bulunmaktadır:


TCP soketi ile iletişim


Uygulama iki işlem yapar:

  1. TCP soketi açma


  2. Açık sokete veri gönderme


Örneğin, Ruby'nin Socket.tcp(host, port) komutuyla yapıldığı gibi bir TCP soketi açılırken, sistem eş zamanlı olarak socket(2) sistem çağrısını kullanarak bir yuva oluşturur ve ardından connect(2) sistem çağrısı aracılığıyla bir bağlantı kurar. .


Veri göndermeye gelince, bir uygulamada socket.write('foo') gibi bir komutun kullanılması, öncelikle mesajı soketin gönderme arabelleğine yerleştirir. Daha sonra başarıyla kuyruğa alınan baytların sayısını döndürür. Bu verilerin ağ üzerinden hedef ana bilgisayara gerçek iletimi, TCP/IP yığını tarafından eşzamansız olarak yönetilir.


Bu, bir uygulamanın sokete yazdığında doğrudan ağ işlemlerine dahil olmadığı ve bağlantının hala aktif olup olmadığını gerçek zamanlı olarak bilemeyeceği anlamına gelir. Aldığı tek onay, mesajın TCP gönderme arabelleğine başarıyla eklendiğidir.

TCP Sunucusu Kapatıldığında Ne Olur?

Sunucu bir ACK bayrağıyla yanıt vermediğinden TCP yığınımız, onaylanmayan son paketin yeniden iletimini başlatacaktır:


sunucu çöktüğünde ne olur


Buradaki ilginç şey, varsayılan olarak TCP'nin üstel geri çekilmeyle 15 yeniden iletim yapması ve bunun da neredeyse 15 dakikalık yeniden denemeyle sonuçlanmasıdır!


Ana makinenizde kaç yeniden denemenin ayarlandığını kontrol edebilirsiniz:

 $ sysctl net.ipv4.tcp_retries2 net.ipv4.tcp_retries2 = 15


Dokümanlara daldıktan sonra şu netleşiyor; ip-sysctl.txt belgeleri şunu söylüyor:


Varsayılan değer olan 15, 924,6 saniyelik varsayımsal bir zaman aşımı sağlar ve etkin zaman aşımı için bir alt sınırdır. TCP, varsayımsal zaman aşımını aşan ilk RTO'da etkili bir şekilde zaman aşımına uğrayacaktır.


Bu süre zarfında yerel TCP soketi canlıdır ve veri kabul eder. Tüm yeniden denemeler yapıldığında yuva kapatılır ve uygulama, yuvaya herhangi bir şey gönderme girişiminde hata alır.

Neden Genellikle Bir Sorun Değildir?

TCP sunucusunun, FIN veya RST TCP bayraklarını göndermeden veya bağlantı sorunları olduğunda beklenmedik bir şekilde kapanması senaryosu oldukça yaygındır. Peki neden bu tür durumlar sıklıkla fark edilmiyor?


Çünkü çoğu durumda sunucu, uygulama düzeyinde bir miktar yanıtla yanıt verir. Örneğin, HTTP protokolü, sunucunun her isteğe yanıt vermesini gerektirir. Temel olarak, connection.get gibi bir kodunuz olduğunda iki ana işlem gerçekleştirir:

  1. Yükünüzü TCP soketinin gönderme arabelleğine yazar.

    Bu noktadan itibaren, işletim sisteminin TCP yığını, bu paketlerin TCP garantileriyle uzak sunucuya güvenilir bir şekilde teslim edilmesi sorumluluğunu üstlenir.


  2. Uzak sunucudan TCP alma arabelleğinde yanıt bekleniyor


    Tipik olarak uygulamalar, aynı TCP soketinin alma arabelleğinden engellemeyen okumalar kullanır.


Bu yaklaşım işleri önemli ölçüde basitleştirir çünkü bu gibi durumlarda, uygulama düzeyinde kolayca bir zaman aşımı ayarlayabilir ve belirli bir zaman dilimi içinde sunucudan yanıt gelmezse soketi kapatabiliriz.


Ancak sunucudan herhangi bir yanıt beklemediğimizde (TCP onayları hariç), bağlantının durumunu uygulama seviyesinden belirlemek daha az kolay hale gelir.

Uzun TCP Yeniden İletimlerinin Etkisi

Şu ana kadar aşağıdakileri belirledik:

  1. Uygulama bir TCP soketi açar ve ona düzenli olarak veri yazar.


  2. Bir noktada, TCP sunucusu bir RST paketi bile göndermeden kapanır ve gönderenin TCP yığını, onaylanmayan son paketi yeniden iletmeye başlar.


  3. Bu sokete yazılan diğer tüm paketler, soketin gönderme arabelleğinde kuyruğa alınır.


  4. Varsayılan olarak TCP yığını, üstel geri çekilmeyi kullanarak onaylanmamış paketi 15 kez yeniden iletmeye çalışır, bu da yaklaşık 924,6 saniyelik (yaklaşık 15 dakika) bir süreyle sonuçlanır.


Bu 15 dakikalık süre boyunca, yerel TCP soketi açık kalır ve uygulama, gönderme arabelleği dolana kadar (genellikle sınırlı bir kapasiteye sahiptir, genellikle yalnızca birkaç megabayttır) buraya veri yazmaya devam eder. Tüm yeniden iletimlerden sonra soket sonunda kapalı olarak işaretlendiğinde, gönderme arabelleğindeki tüm veriler kaybolur .


Bunun nedeni, uygulamanın gönderme arabelleğine yazdıktan sonra artık bundan sorumlu olmaması ve işletim sisteminin bu verileri basitçe atmasıdır.


Uygulama yalnızca TCP soketinin gönderme arabelleği dolduğunda bağlantının koptuğunu algılayabilir. Bu gibi durumlarda, sokete yazmaya çalışmak uygulamanın ana iş parçacığını bloke ederek durumu ele almasına olanak tanır.


Ancak bu tespit yönteminin etkinliği gönderilen verinin boyutuna bağlıdır.


Örneğin, uygulama metrikler gibi yalnızca birkaç bayt gönderirse bu 15 dakikalık zaman dilimi içinde gönderme arabelleğini tam olarak dolduramayabilir.


Peki, TCP sunucusu kapalıyken, bu süre zarfında 15 dakikalık yeniden iletim ve veri kaybını önlemek için, açıkça belirlenmiş bir zaman aşımı ile bağlantıyı kapatacak bir mekanizma nasıl uygulanabilir?

Soket Seçeneğini Kullanarak TCP Yeniden İletim Zaman Aşımı

Özel ağlarda, kapsamlı yeniden iletimler genellikle gerekli değildir ve TCP yığınını yalnızca sınırlı sayıda yeniden denemeyi deneyecek şekilde yapılandırmak mümkündür. Ancak bu yapılandırma genel olarak düğümün tamamına uygulanır. Birden fazla uygulama genellikle aynı düğümde çalıştığından, bu varsayılan değerin değiştirilmesi beklenmeyen yan etkilere yol açabilir.


Daha kesin bir yaklaşım, TCP_USER_TIMEOUT soket seçeneğini kullanarak soketimize özel olarak bir yeniden iletim zaman aşımı ayarlamaktır. Bu seçeneği kullanarak, genel olarak ayarlanan maksimum TCP yeniden iletim sayısına bakılmaksızın, yeniden iletimler belirtilen zaman aşımı süresi içinde başarılı olmazsa, TCP yığını soketi otomatik olarak kapatacaktır.


Uygulama düzeyinde bu, kapalı bir yuvaya veri yazılmaya çalışıldığında bir hata alınmasına neden olur ve veri kaybı önleme işleminin doğru şekilde yapılmasına olanak tanır.


Bu soket seçeneğini client.rb ayarlayalım:

 require 'socket' require 'time' $stdout.sync = true socket = Socket.tcp('server', 1234) # set 5 seconds restransmissions timeout socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_USER_TIMEOUT, 5000) loop do puts "sending message to the socket at #{Time.now}" socket.write "Hello from client\n" sleep 1 end


Ayrıca gözlemlerime göre TCP_USER_TIMEOUT soket seçeneği macOS'ta mevcut değil.


Şimdi her şeye docket compose up ile yeniden başlayın ve bir noktada sunucuyu docker compose stop server ile tekrar durduralım:

 $ docker compose up [+] Running 2/0 ⠿ Container tcp_tests-server-1 Created 0.0s ⠿ Container tcp_tests-client-1 Created 0.0s Attaching to tcp_tests-client-1, tcp_tests-server-1 tcp_tests-server-1 | starting tcp server... tcp_tests-server-1 | started tcp server on port 1234 tcp_tests-server-1 | new client tcp_tests-server-1 | 2024-01-20 12:37:38 +0000]: Hello from client tcp_tests-client-1 | sending message to the socket at 2024-01-20 12:37:38 +0000 tcp_tests-server-1 | 2024-01-20 12:37:39 +0000]: Hello from client tcp_tests-client-1 | sending message to the socket at 2024-01-20 12:37:39 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-20 12:37:40 +0000 tcp_tests-server-1 | 2024-01-20 12:37:40 +0000]: Hello from client tcp_tests-client-1 | sending message to the socket at 2024-01-20 12:37:41 +0000 tcp_tests-server-1 | 2024-01-20 12:37:41 +0000]: Hello from client tcp_tests-server-1 | 2024-01-20 12:37:42 +0000]: Hello from client tcp_tests-client-1 | sending message to the socket at 2024-01-20 12:37:42 +0000 tcp_tests-server-1 | 2024-01-20 12:37:43 +0000]: Hello from client tcp_tests-client-1 | sending message to the socket at 2024-01-20 12:37:43 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-20 12:37:44 +0000 tcp_tests-server-1 | 2024-01-20 12:37:44 +0000]: Hello from client tcp_tests-server-1 exited with code 1 tcp_tests-server-1 exited with code 0 tcp_tests-client-1 | sending message to the socket at 2024-01-20 12:37:45 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-20 12:37:46 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-20 12:37:47 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-20 12:37:48 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-20 12:37:49 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-20 12:37:50 +0000 tcp_tests-client-1 | sending message to the socket at 2024-01-20 12:37:51 +0000 tcp_tests-client-1 | client.rb:11:in `write': Connection timed out (Errno::ETIMEDOUT) tcp_tests-client-1 | from client.rb:11:in `block in <main>' tcp_tests-client-1 | from client.rb:9:in `loop' tcp_tests-client-1 | from client.rb:9:in `<main>' tcp_tests-client-1 exited with code 1


~12:37:45'te sunucuyu durdurdum ve istemcinin neredeyse 5 saniyede Errno::ETIMEDOUT aldığını gördük, harika.


docker exec -it tcp_tests-client-1 tcpdump -i any tcp port 1234 ile bir tcpdump yakalayalım:


tcpdump


Zaman aşımının aslında 5 saniyeden biraz daha uzun bir sürede gerçekleştiğini belirtmekte fayda var. Bunun nedeni TCP_USER_TIMEOUT aşımı kontrolünün bir sonraki yeniden denemede gerçekleşmesidir. TCP/IP yığını zaman aşımının aşıldığını tespit ettiğinde soketi kapalı olarak işaretler ve uygulamamız Errno::ETIMEDOUT hatası alır.


Ayrıca TCP keepalives kullanıyorsanız Cloudflare'deki bu makaleye göz atmanızı öneririm. TCP keepalives ile birlikte TCP_USER_TIMEOUT kullanmanın inceliklerini kapsar.