Yüksek hacimli işlem yapan mikroservis mimarilerinde (ödeme geçitleri, fintech servisleri veya yoğun API tüketen altyapılar) zaman zaman şu gizemli tabloyla karşılaşılır:
Sistemdeki CPU, bellek ve ağ kullanımı oldukça düşük seviyelerdedir; ancak dış dünyadaki üçüncü taraf bir API’ye veya dahili bir mikroservise giden isteklerde tam olarak 5 saniye süren anlık gecikme sıçramaları yaşanır ya da uygulama loglarına rastgele aralıklarla şu hata düşer:Temporary failure in name resolution.
Gecikmenin rastgele değil de tam 5000 ms (5 saniye) sürmesi tesadüf değildir. Bu durum, Linux çekirdeğinin bağlantı takip modülü (nf_conntrack) ile glibc DNS çözümleyicisinin (resolv.conf) kesiştiği noktadaki iki temel kronik darboğazın doğrudan işaretidir.
Bugünkü yazımızda, Kubernetes pod’larında ve Linux konteyner ortamlarında bu 5 saniyelik kilitlenmenin kök nedenini ve çekirdek/ağ seviyesinde kalıcı çözüm adımlarını inceliyoruz.
1. Suçlu 1: glibc 5 Saniyelik DNS Zaman Aşımı ve conntrack Yarış Durumu
Linux işletim sistemlerinde standart C kütüphanesi (glibc), DNS sorguları için varsayılan olarak 5 saniyelik bir zaman aşımı (timeout) süresi kullanır. Bir sorgu gönderildiğinde yanıt 5 saniye içinde dönmezse paket tekrar iletilir (retry).
Peki paket neden kaybolur?
A ve AAAA Sorgularının Aynı Anda Çıkması
Modern bir Linux istemcisi bir alan adını çözmek istediğinde, aynı anda hem IPv4 (A) hem de IPv6 (AAAA) sorgusu başlatır. Bu iki UDP paketi:
- Aynı anda soketten çıkar.
- Kaynak IP ve Hedef IP aynıdır.
- Hedef port (DNS – 53) aynıdır.
- Çoğu zaman kaynak portları (Ephemeral Port) bile aynı veya ardışıktır.
Netfilter / Conntrack Kilit Çakışması (Race Condition)
Paketler Linux çekirdeğinin paket filtreleme katmanından geçerken, nf_conntrack modülü bu iki paket için bağlantı takip tablosunda (conntrack table) kayıt açmaya çalışır.
Paketler aynı CPU soketinde veya farklı çekirdeklerde mikro-saniyeler içinde işlenirken, çekirdek her iki UDP sorgusunu aynı akış (tuple) olarak değerlendirebilir. İkinci paket işlenirken çekirdek seviyesinde yarış durumu (race condition) oluşur:
[Uygulama Pod'u] ──┬──> UDP DNS Query: A ────> [conntrack: INSERT OK] ───> CoreDNS (Yanıt Döndü)
└───> UDP DNS Query: AAAA ────> [conntrack: DROP/CONFIRM FAIL] (Paket Çöpe Gitti!)
│
▼
(5 Saniye Timeout Beklemesi...)
Çekirdek ikinci sorguyu düşürdüğünde, glibc 5 saniye boyunca yanıt bekler. 5 saniyenin sonunda zaman aşımına uğrayıp paketi tekrar gönderir ve ikinci denemede adresi çözerek işine devam eder. Dışarıdan bakıldığında API çağrınız 5005 ms sürmüş olur.
2. Suçlu 2: ndots:5 Çılgınlığı (Tek Bir Adres İçin 5 Kat Fazla DNS Trafiği)
Kubernetes pod’ları oluşturulurken /etc/resolv.conf dosyası varsayılan olarak şu parametrelerle üretilir:
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
ndots:5 Ne Anlama Gelir?
Bir alan adının içindeki nokta sayısı 5’ten az ise, Linux çözümleyicisi bu adresin mutlak bir FQDN (Fully Qualified Domain Name) olmadığını varsayar ve önce search listesindeki yerel alan adlarını sonuna ekleyerek sorgular.
Örneğin uygulamanız api.odeme-partneri.com.tr (içinde 3 nokta var, yani < 5) adresine bağlanmak istediğinde, sırasıyla şu sorgular CoreDNS’e basılır:
api.odeme-partneri.com.tr.default.svc.cluster.local-> NXDOMAIN (Yok)api.odeme-partneri.com.tr.svc.cluster.local-> NXDOMAIN (Yok)api.odeme-partneri.com.tr.cluster.local-> NXDOMAIN (Yok)- En nihayetinde:
api.odeme-partneri.com.tr-> NOERROR (Çözüldü!)
Uygulamanız dış dünyadaki tek bir adresi çözmek için CoreDNS’e 4 kat (IPv6 ile birlikte 8 kat!) gereksiz sorgu gönderir. Saniyede 10.000 istek alan bir sistemde CoreDNS önbelleği çöker, CPU tavan yapar ve paket düşmeleri kaçınılmaz hale gelir.
3. Çözüm Yolları: 5 Saniyelik Gecikmeyi Kökten Engellemek
Çözüm A: Pod Seviyesinde DNS Yapılandırması (dnsConfig)
Uygulama dağıtımlarında (Deployment) dış servislere yoğun trafik atan pod’lar için ndots değerini düşürmek ve paralel sorgu çakışmalarını önleyen glibc bayraklarını eklemek ilk adımdır:
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-api-gateway
spec:
template:
spec:
dnsConfig:
options:
# Dış aramalarda doğrudan FQDN sorgulaması için ndots değerini 2'ye düşürün
- name: ndots
value: "2"
# glibc'nin A ve AAAA sorgularını farklı soketlerden göndermesini sağlayın (Race condition önlemi)
- name: single-request-reopen
# Zaman aşımı süresini 5 saniyeden 2 saniyeye indirin
- name: timeout
value: "2"
- name: attempts
value: "3"
containers:
- name: gateway
image: my-payment-app:latest
İpucu: Uygulama kodlarınızda veya konfigürasyonlarınızda dış servis alan adlarının sonuna mutlak nokta (.) koyarsanız (örneğin api.odeme-partneri.com.tr. gibi), Linux çözümleyicisi ndots kuralını tamamen baypas ederek doğrudan ana alan adını sorgular.
Çözüm B: NodeLocal DNSCache Mimarisi (Kalıcı Altyapı Çözümü)
Her sorgunun pod’dan çıkıp ağ üzerinden kümedeki 2-3 adet CoreDNS pod’una gitmesi yerine, her fiziksel/sanal Kubernetes düğümünde (Node) yerel bir DNS önbellek ajanı koşturulmalıdır.
NodeLocal DNSCache, her düğüm üzerinde bir DaemonSet olarak çalışır ve düğüm üzerinde sanal bir IP (169.254.20.10) dinler:
- Pod’lar DNS sorgularını yerel düğümdeki ajan üzerinden yanıtlar.
- TCP Kullanımı: NodeLocal DNSCache ile merkezi CoreDNS arasındaki iletişim UDP yerine TCP üzerinden yapılır; böylece
conntrackUDP yarış durumu tamamen ortadan kalkar. - Düğüm içi sorgularda ağ ve sanal switch gecikmesi 0 ms’ye iner.
4. Linux Çekirdek ve Conntrack İnce Ayarı
Sunucu ve düğüm katmanında conntrack tablosunun UDP oturumlarını gereksiz yere uzun süre bellekte tutmasını engellemek için /etc/sysctl.d/99-vmninja-dns.conf dosyasını yapılandırın:
# UDP DNS oturumlarının conntrack tablosunda kalma süresini 30 saniyeden 10 saniyeye düşürün
net.netfilter.nf_conntrack_udp_timeout = 10
net.netfilter.nf_conntrack_udp_timeout_stream = 30
# Conntrack tablosu taşmalarını önlemek için sınırı yükseltin
net.netfilter.nf_conntrack_max = 1048576