Kubernetes’te .NET Pod’u Neden Çöküyor? Exit 139 / SIGSEGV Avı
CrashLoopBackOff klasik bir Kubernetes derdidir, ama bazen pod düzgün başlar, birkaç dakika çalışır, sonra aniden ölür. Loglar tertemizdir, hiçbir exception yoktur. Elinizde tek ipucu vardır: Exit Code 139.
Bu yazıda, on-prem bir kümede iki .NET 8/10 mikroservisinin neden çöktüğünü adım adım bulduğumuz gerçek bir vakayı özetliyorum. Sonunda suçlu beklediğim yerde değildi.
Exit Code 139 ne anlatır?
139 = 128 + 11. Buradaki 11, SIGSEGV sinyalidir — yani bir segmentation fault, native (yönetilmeyen) bir kod katmanında bellek ihlali. Bu, .NET’in yakaladığı normal bir exception değildir; runtime’ın altında, bir C kütüphanesinde patlar. Bu yüzden uygulama loglarında stack trace göremezsiniz.
İlk işiniz, çökmenin başlangıçta mı yoksa çalışırken mi olduğunu ayırmak:
kubectl -n <ns> describe pod <pod>
# Last State: Terminated / Reason: Error / Exit Code: 139
# Started ... Finished ... → arada 5-6 dakika varsa, çökme runtime'da
Bizim vakada pod ~5 dakika sağlıklı çalışıp /health 200 döndürüyor, sonra ölüyordu. Yani sorun başlangıçta değildi.
Tuzak: probe hataları sebep değil, sonuçtur
Event’lerde bunları görmek kafa karıştırır:
Warning Unhealthy Readiness probe failed: HTTP probe failed with statuscode: 503
Warning Unhealthy Startup probe failed: dial tcp ...:8080: connect: connection refused
Warning BackOff Back-off restarting failed container
Bunların hiçbiri kök neden değil. Container çökünce 8080 portu kapanır, doğal olarak probe’lar bağlanamaz. Probe konfigürasyonunu kurcalamak zaman kaybıdır — asıl soru segfault’ın hangi native kütüphanede olduğudur.
Crash dump’ı almak: asıl zorluk burada
Native segfault’ı görmenin tek yolu, .NET runtime’a çökme anında bir crash report ürettirmektir. Deployment’a şu env değişkenlerini ekleyin:
env:
- name: DOTNET_DbgEnableMiniDump
value: "1"
- name: DOTNET_DbgMiniDumpType
value: "2"
- name: DOTNET_EnableCrashReport # insan-okur JSON üretir
value: "1"
- name: DOTNET_DbgMiniDumpName
value: "/dumps/core.%p" # yazılabilir yola yönlendir
İki kritik tuzak:
1. read-only filesystem. Eğer pod’unuzda readOnlyRootFilesystem: true varsa (güvenlik için yaygındır), createdump dosyayı /tmp‘e yazamaz ve şunu görürsünüz:
[createdump] Crashing thread ... signal 11 (000b)
[createdump] Could not create ... '/tmp/coredump.1': Read-only file system (30)
Bu bir izin hatası değil, bir mount hatasıdır. Dosyanın sahibi olmanız (chown) işe yaramaz; root fs salt-okunur mount edilmiştir. Çözüm, dump için ayrı bir yazılabilir volume mount etmektir:
volumeMounts:
- name: dumps
mountPath: /dumps
securityContext:
fsGroup: 2000 # non-root user yazabilsin
volumes:
- name: dumps
emptyDir:
sizeLimit: 2Gi
emptyDir root fs’ten bağımsız ayrı bir mount olduğu için read-only kısıtının dışında kalır. Üstelik container restart’larında kalıcıdır — CrashLoop’ta dump kaybolmaz.
2. Dump’a baktığınız pod yanlış instance olabilir. Dump çöken eski instance’ta oluşur; pod restart olunca ephemeral filesystem silinir. emptyDir kullanmak bu sorunu da çözer.
Suçluyu bulmak: crash report JSON’ı okumak
Pod bir kez daha çökünce crashreport.json oluşur. İçinde crashed: true olan thread’i bulun — kök neden onun stack’indedir. Bizim vakada (alttan üste doğru):
ld-musl-x86_64.so.1 ← Alpine / musl libc
libcoreclr.so
System.Threading...
Npgsql.Internal.NpgsqlConnector.ConnectAsync [Npgsql.dll]
Npgsql.Internal.NpgsqlConnector.SetupEncryption
Npgsql.Internal.NpgsqlConnector.TryNegotiateGssEncryption
System.Net.Security.NegotiateAuthentication.GetOutgoingBlob
System.Net.NegotiateAuthenticationPal.InitializeSecurityContext [System.Net.Security.dll]
(unknown native) ← SIGSEGV burada
Tablo netti: Npgsql bir PostgreSQL bağlantısı açarken GSS/Kerberos şifreleme müzakeresi deniyor, bu .NET’in NegotiateAuthentication native katmanına iniyor ve Alpine/musl ortamında segfault üretiyordu. En dipteki ld-musl-x86_64.so.1, Alpine base image kullandığımızın kanıtıydı.
Çökmenin neden 5 dakika sonra geldiği de buradan anlaşıldı: uygulama açılışta havuzdaki bağlantıları kullanıyor, ama yeni bir bağlantı açılması gerektiğinde (cache refresh job, scheduled job, pool yenileme) Open çağrılıyor → GSS negotiate → segfault.
Kök neden: Alpine (musl) + native auth
mcr.microsoft.com/dotnet/aspnet:X.0-alpine image’ı musl libc kullanır. musl, glibc’nin tam ikamesi değildir; .NET’in bazı native auth yolları (GSSAPI, Kerberos, NTLM) musl’da kırılgandır ya da çöker. PostgreSQL’imiz basit kullanıcı/parola ile bağlanıyordu — yani GSS’e hiç ihtiyacımız yoktu — ama Npgsql yine de handshake’te onu deniyordu.
Çözüm: iki katman
1. Acil — GSS denemesini kapat (runtime). Connection string’e tek parametre:
Host=...;Username=postgres;Database=...;Gss Encryption Mode=Disable;
Bu, çöken kod yoluna (TryNegotiateGssEncryption) hiç girmeden bağlantıyı açar. Connection string secret/Helm values’tan geliyorsa tek yerden güncelleyip tüm servisleri korursunuz. Alternatif olarak ortam değişkeniyle de denenebilir: PGGSSENCMODE=disable.
2. Kalıcı — glibc tabanlı base image (build). Bir sonraki image’da Alpine’den çıkın:
# yerine: mcr.microsoft.com/dotnet/aspnet:10.0-alpine
FROM mcr.microsoft.com/dotnet/aspnet:10.0-bookworm-slim AS base
ENV TZ=Europe/Istanbul # "Turkey" legacy isim; kanonik olanı kullanın
glibc tabanlı image, bu sınıf native-auth çökmelerini topyekûn kapatır. musl + native auth kombinasyonu ileride LDAP, SMTP auth veya token doğrulama gibi başka yerlerde de patlayabilir.
Not: Alpine’de native paket eklemeye çalışmak (
gss-ntlmsspgibi) genelde çıkmaz sokaktır — o paket Alpine repolarında bile yoktur. Doğru hamle, ihtiyaç duymadığınız GSS yolunu kapatmaktır, native zinciri yamamak değil.
Çıkarımlar
- Exit 139 = SIGSEGV, native katmanda çökme. Uygulama loglarında stack trace aramayın.
- Probe hataları semptomdur, sebep değil. Container çökünce port kapanır.
- Crash dump şarttır.
DOTNET_EnableCrashReport=1+ yazılabiliremptyDirmount +fsGroupüçlüsü olmadan teşhis edemezsiniz. crashreport.jsonçoğu zaman binary dump’tan hızlı cevap verir —crashed: truethread’in stack’ine bakın, yeter.- Base image seçimi bir mimari karardır. Alpine küçüktür ama native auth’ta risklidir; kurumsal servisler için glibc (bookworm-slim / jammy-chiseled) daha az sürpriz üretir.
- Aynı template’ten doğan tüm servisleriniz aynı bombayı taşır. Düzeltmeyi platform katmanında (paylaşılan base image + ortak connection string template) yapın, servis servis dolaşmayın.
Crash dump altyapısını kümenize bir kez kurun; bir dahaki “5 dakikada bir ölen pod” vakasında teşhis 6 turdan 6 dakikaya iner.