Ping Gidiyor Ama SSH Açılmıyor: 'Banner Exchange' Zaman Aşımının Arkasındaki OOM Zinciri
Bir sunucu ping'e 0,2 ms ile cevap veriyor ama SSH 'Connection timed out during banner exchange' diyor. Makine ayakta, ağ sağlam, peki sorun ne? Bir bellek sızıntısının journald'ı öldürüp CPU'yu kilitlediği gerçek bir arızanın teşhisi: hipervizör konsolundan kanıt toplama, yanlış hipotezi veriyle çürütme ve konteyner bellek limitiyle kalıcı çözüm.
Erdem Özyurt
Özet: Bir sunucu ping’e cevap veriyorsa ayakta sayılır, ama SSH
banner exchangeaşamasında zaman aşımına uğruyorsa sorun ağda değil kullanıcı alanındadır. Bu arızada bir bellek sızıntısı OOM killer’ı tetikledi, ardından systemd-journald bir daha kalkamadı ve girdiği yeniden başlatma döngüsü CPU’yu kilitledi. Teşhis hipervizör konsolundan yapıldı, çözüm konteyner bellek limitiyle geldi.
Alarm öğle saatlerinde düştü: “Virtual machine CPU usage”.
Sanallaştırma panelinde tek satır. Sunucu bir honeypot sensörüydü, yani internete kasten açık duran, saldırı toplayan bir makine. O sıralar gerçekten de yoğun bir tarama kampanyası sürüyordu, günde milyonlarca olay kaydediyordu. Yani ilk bakışta sebep ortadaydı: yük arttı, CPU doldu.
Bu açıklama yanlıştı. Nasıl yanlış olduğunu anlatmak, doğru cevabı anlatmaktan daha faydalı.
Belirti: Ping var, SSH yok
İlk refleks bağlanmaktı. Olmadı:
$ ssh sunucu
Connection timed out during banner exchange
Ping ise gayet iyiydi:
3 packets transmitted, 3 received, 0% packet loss
rtt min/avg/max/mdev = 0.166/0.200/0.231/0.026 ms
Bu ikilinin anlamı çok spesifik. banner exchange aşamasına gelmek için TCP el sıkışmasının tamamlanmış olması gerekir. Yani paket gitti, port açıktı, sunucu bağlantıyı kabul etti. Sonra sustu.
Ping’e cevap veren şey çekirdeğin ağ yığınıdır. Kernel ayakta olduğu sürece ICMP cevabı gelmeye devam eder. Dolayısıyla bu tablo şunu söylüyordu: makine ayakta, ağ sağlam, kullanıcı alanı ölü.
Yanlış hipotez ve onu çürüten ölçüm
Elimdeki hipotez “trafik arttı, CPU doldu” idi. Test etmek için iki eğriyi yan yana koydum: hipervizörden CPU zaman serisi, uygulama tarafından saatlik işlenen olay sayısı.
Sonuç hipotezi çürüttü:
| Saat (UTC) | İşlenen olay | CPU |
|---|---|---|
| 08:00 | 856.662 | ~%8,5 |
| 09:00 | 624.437 | %58,9 |
| 10:00 | 3.743 | %147 |
| 11:00 | 155 | %145 |
| 12:00 | 4.198 | %149 |
Trafik zirvedeyken CPU yüzde 8,5’ti. Makine yükü rahatça kaldırıyordu. CPU yüzde 147’ye çıktığında ise işlenen olay sayısı çöktü.
Bu ters korelasyon. Eğer yük CPU’yu tüketiyor olsaydı ikisi birlikte artardı. Burada biri artarken diğeri sıfırlanıyor. Yani CPU’yu yiyen şey iş değil, bir arıza. Ve o arıza işin yapılmasını da engelliyor.
Aynı desen bir gün öncesinde de vardı: CPU bir buçuk saat zirve yapmış, o sırada olay hacmi beşte bire düşmüş, CPU normale dönünce hacim geri gelmişti. Yani bu ilk nöbet değildi.
Hipervizör metrikleri tabloyu tamamladı:
| Ölçüm | Değer |
|---|---|
| Atanmış vCPU | 2 |
| VM CPU tavanı | 6.186 MHz |
| Anlık tüketim | 8.351 MHz (%135) |
cpu.ready |
0-1 ms |
| Host kapasitesi | 36 çekirdek, %23 kullanımda |
cpu.ready değerinin sıfıra yakın olması kritik: host, VM’in istediği CPU’yu anında veriyor. Yani bu bir kaynak çekişmesi değil. VM elindeki iki çekirdeği sonuna kadar kullanıyor ve host’ta bol bol boş çekirdek duruyor.
SSH ölüyken kanıt: hipervizör konsolu
Makineye giremiyorsanız, sanallaştırma katmanı hâlâ girebilir. VMware ortamında govc ile konsolun ekran görüntüsü alınabilir ve bunun için misafir sistemin kullanıcı adı parolası gerekmez:
govc vm.console -capture /tmp/konsol.png "/Datacenter/vm/klasor/SunucuAdi"
Bu tek komut arızayı çözdü. Ekranda iki şey vardı.
Birincisi, yüzlerce kez tekrarlayan aynı satır:
systemd[1]: Failed to start systemd-journald.service - Journal Service.
Kernel zaman damgalarına göre bu döngü yaklaşık iki buçuk saattir dönüyordu.
İkincisi, döngünün nedeni:
Out of memory: Killed process 5129 (tshark)
total-vm:5939432kB, anon-rss:5021844kB, UID:2000
tshark süreci 4,8 GB yerleşik belleğe ulaşmış, 8 GB’lık makinede çekirdek OOM killer’ı tetiklemiş ve süreci sonlandırmıştı.
Zincir
Parçalar birleşince olay şöyle okunuyor:
1. Bellek sızıntısı. Paket analizi yapan tshark süreci zamanla şişiyor. Sonradan ölçtüm: yoğun trafik altında dakikada yaklaşık 13 MiB. Saatte 790 MiB eder.
2. Bellek tükeniyor. Makinede 8 GB var ve takas alanı zaten neredeyse doluydu.
3. OOM killer devreye giriyor ve en büyük tüketiciyi, yani tshark’ı öldürüyor.
4. systemd-journald ölüyor ve kalkamıyor. Bellek baskısı altında servis başlatılamıyor, systemd tekrar deniyor, yine olmuyor. Sıkı bir döngü.
5. CPU bu döngüde kilitleniyor. İki çekirdek boyunca sürekli servis başlatma denemesi.
6. SSH ölüyor. Buradaki bağlantı ilk bakışta görünmüyor: sshd yeni oturum açarken journald’a kayıt yazar. Journald cevap vermiyorsa sshd orada bekler. Banner hiç gönderilmez. banner exchange zaman aşımının sebebi budur.
7. Uygulama katmanı da ölüyor. Loglama kırıldığı için veri toplama hattı da çalışamıyor. Saatte 850 bin olaydan 155’e düşüşün sebebi bu.
Yani tek bir bellek sızıntısı, kendisiyle hiç ilgisi yokmuş gibi görünen iki sonuç doğurdu: sunucuya erişilemez oldu ve asıl işini yapamaz hale geldi.
“Sızma mı, arıza mı” sorusunu şifresiz cevaplamak
Internete açık bir makinede CPU aniden dolduğunda akla ilk gelen kripto madencisidir. Makineye giremediğim için süreç listesine bakamıyordum. İki dolaylı ölçüm yeterli oldu.
Dışarı bağlantılar. Madenci bir havuza, arka kapı bir komuta sunucusuna bağlanır. Bu bağlantılar ağ geçidinin bağlantı tablosunda görünür:
/ip firewall connection print where src-address~"192.168.x.y"
25 saniye arayla iki ölçüm aldım. İkisinde de tek bir bağlantı vardı: dağıtımın NTP sunucusuna 76 baytlık saat sorgusu. Havuz yok, işaretçi yok.
OOM kurbanının kimliği. Kernel mesajındaki UID:2000 değerini /etc/passwd ile eşleştirdim: uygulamanın kendi servis hesabı. Yani ölen süreç sisteme ait meşru bir bileşendi, dışarıdan yerleştirilmiş bir ikili değil.
Makine ayağa kalktıktan sonra tam denetim de bunu doğruladı: wtmp kayıtlarında tek bir interaktif giriş yok, authorized_keys dosyaları kurulumdan beri değişmemiş, cron temiz, yabancı konteyner imajı yok.
Çözüm iki katmanlı
Birinci katman, nefes alanı. Makine 2 vCPU / 8 GB’dan 4 vCPU / 16 GB’a çıkarıldı. Host’ta 36 çekirdeğin dörtte üçü boştaydı, kaynak sıkıntısı yoktu, sadece VM’e verilmemişti.
İkinci katman, asıl çözüm. Sızıntı yukarı akış kaynaklı bir hata ve düzeltilmesi bende değil. Ama zararı hapsedilebilir. Sızdıran süreç bir konteynerde çalıştığı için compose tanımına tek satır eklendi:
fatt:
container_name: fatt
restart: always
mem_limit: 2g
image: ...
Uygulaması yalnız o servisi etkiler:
docker compose up -d --no-deps --force-recreate fatt
Doğrulama şart, çünkü compose dosyasını düzenlemek limitin uygulandığı anlamına gelmez:
docker inspect fatt --format '{{.HostConfig.Memory}}'
# 2147483648
Artık sızıntı 2 GiB’e ulaştığında çekirdek yalnız bu konteyneri sonlandırıyor, restart: always saniyeler içinde geri getiriyor. Host bir daha bu yüzden çökmüyor.
Ölçülen sızıntı hızıyla bu, yoğun yük altında iki saatte bir yeniden başlatma demek. Kabul edilebilir bir bedel: söz konusu bileşen tamamlayıcı veri üretiyor ve birkaç saniyelik boşluğun maliyeti, sunucunun üç buçuk saat komple sessizleşmesinin yanında hiç kalıyor.
Sonuç doğrulaması:
| Kontrol | Önce | Sonra |
|---|---|---|
| Takas alanı | %95 dolu | 0 bayt |
systemd-journald |
döngüde | active |
| SSH | banner zaman aşımı | normal |
| İşlenen olay | saatte 155 | saatte ~770.000 |
Çıkarılan dersler
Ping ayakta olmanın kanıtı değildir. Kernel cevap veriyor diye kullanıcı alanı çalışıyor sayılmaz. banner exchange zaman aşımı gördüğünüzde “makine kapanmış” veya “ağ koptu” teşhisine atlamayın; büyük ihtimalle sunucu ayakta ama log yazamıyor.
Yükün CPU’yu tükettiğini varsaymayın, ölçün. İş yükü metriğiyle CPU eğrisini yan yana koymak beş dakika sürüyor ve bu arızada hipotezi tersine çevirdi. İkisi aynı yönde hareket etmiyorsa sebep başka yerdedir.
Hipervizör, kilitli sunucuya açılan kapıdır. Konsol ekran görüntüsü kimlik bilgisi istemez ve kernel mesajlarını doğrudan verir. SSH’ın öldüğü her durumda ilk bakılacak yer burasıdır.
Kaynak limiti olmayan konteyner, host’un tamamını rehin alır. Limit koymak sızıntıyı düzeltmez ama etkisini bir konteynere hapseder. Hangi süreçlerin bellek tükettiğini bilmiyorsanız bile, ağır iş yapan konteynerlere makul bir tavan koymak ucuz bir sigortadır.
Bir arıza kendi teşhisini de engelleyebilir. Bu olayda loglama sisteminin ölmesi hem sorunu büyüttü hem de sorunun kaydını tutmasını engelledi. Kalıcı log altyapısını, izlediği sistemden bağımsız tutmak bu yüzden değerlidir.
Sıkça Sorulan Sorular
SSH 'Connection timed out during banner exchange' hatası ne anlama gelir?
TCP bağlantısı kurulmuş ama sunucu SSH banner'ını gönderememiş demektir. Makine ayakta ve ağ sağlamdır; sshd kullanıcı alanında bloke olmuştur. En sık nedeni sunucunun aşırı yük altında olması veya sshd'nin log yazamamasıdır, çünkü sshd oturum açarken journald'a yazar ve journald ölüyse orada bekler.
Ping cevap veriyorsa sunucu ayakta demek değil mi?
Ping'e cevap veren şey çekirdeğin ağ yığınıdır, kullanıcı alanı değil. Kernel ayakta olduğu sürece ICMP cevabı gelir. Bu yüzden ping başarılı ama SSH, HTTP veya veritabanı cevapsızsa sorun ağda değil kullanıcı alanındadır: bellek tükenmesi, CPU doyması veya kilitlenmiş bir servis.
SSH ölüyken sunucudan nasıl kanıt toplarım?
Sanallaştırma katmanı üzerinden. VMware ortamında govc ile hipervizör konsolunun ekran görüntüsü alınabilir ve bunun için misafir sistem kimlik bilgisi gerekmez; kernel mesajları, OOM kayıtları ve systemd hataları doğrudan okunur. VMware Tools çalışıyorsa govc guest.run ile SSH olmadan komut da çalıştırılabilir.
Konteynere bellek limiti koymak sızıntıyı düzeltir mi?
Hayır, sızıntıyı düzeltmez ama zararını hapseder. Limit dolduğunda çekirdek yalnız o konteyneri sonlandırır, restart politikası saniyeler içinde geri getirir. Limit yoksa aynı sızıntı host'un tamamını tüketir ve OOM killer'ın hangi süreci seçeceğini kestiremezsiniz.
Docker Compose'da bellek limiti nasıl tanımlanır?
Swarm dışı kullanımda servis bloğuna mem_limit alanı eklenir, örneğin mem_limit: 2g. Değişikliğin geçerli olması için konteynerin yeniden oluşturulması gerekir; docker compose up -d --no-deps --force-recreate servis_adi komutu yalnız o servisi etkiler. Limitin uygulandığı docker inspect ile HostConfig.Memory alanından doğrulanır.
Yüksek CPU her zaman yüksek trafik anlamına mı gelir?
Hayır ve bu varsayım teşhisi saptırır. Doğrulamak için CPU zaman serisi ile iş yükü metriğini yan yana koyun. Bu arızada trafik zirvedeyken CPU yüzde 8,5'ti; CPU yüzde 149'a çıktığında işlenen olay sayısı çöktü. Ters korelasyon, yükün değil bir arızanın CPU'yu tükettiğini gösterir.
Kaynaklar
- 1
systemd-journald günlük kayıtlarını toplayan servistir; başarısız olduğunda ona yazan servisler bloke olabilir
systemd-journald.service(8) man page (2025) ↗ - 2
Docker konteynerlerinde bellek limiti tanımlanabilir; limit aşıldığında çekirdek konteyneri sonlandırır
Docker Documentation: Runtime options with Memory, CPUs, and GPUs (2025) ↗ - 3
Çekirdek bellek tükendiğinde oom_score değerlerine göre bir süreç seçip sonlandırır
proc(5) man page, oom_score ve oom_score_adj (2025) ↗ - 4
govc, vSphere ortamını komut satırından yönetmeyi sağlayan govmomi tabanlı araçtır
VMware govmomi / govc (2025) ↗
Yazan: Erdem Özyurt
Bilişim Danışmanı · ISP Altyapı · Siber Güvenlik
Kablo döşemekten kod yazmaya, ağ tasarımından web geliştirmeye; Layer 1'den Layer 7'ye bütünlük içinde çalışıyorum.
Hakkımda daha fazla →