“site açılmıyor” mesajıyla başladı
Sabah 08:00 sıralarında telefonuma bir mesaj geldi: “siteler düzeldi mi dostum? açılmıyor.”
mustafaerbay.com.tr açıyorum — 8 saniye timeout. Aynı VPS’teki diğer uygulamalar — aynısı. Bu ilk değil, ama bu kadar derin ilk.
Diagnostic akışı: SSH çalışıyor ama bekliyor
İlk SSH:
$ ssh vps
[5 saniye bekledi]
$ uptime
05:27:24 up 9 days, 7:51, 3 users,
load average: 52.51, 76.02, 70.66
Load 52, 76, 70. Sağlıklı bir sistemde 4 olur. Bu cehennem.
free -h:
total used free shared buff/cache available
Mem: 7.6Gi 7.5Gi 122Mi 147Mi 330Mi 76Mi
Swap: 4.0Gi 3.9Gi 106Mi
7.6 GB RAM’in tamamı kullanılıyor. 76 MB available. Swap dolu, 106 MB serbest. Sistem normalden 4-5 katı yavaş çalışıyor çünkü her memory allocation swap’a düşüyor.
Hangi process’ler bu fistirmayı yapıyor? ps aux --sort=-%cpu:
USER PID %CPU %MEM COMMAND
root 54 92.9 0.0 kcompactd0 ← BURADA
ubuntu 382248 24.4 32.3 node ... astro build --out-dir dist-new
root 383691 26.4 7.0 node ... next build
github-+ 379827 4.4 0.9 Runner.Worker spawnclient
kcompactd0 %92 CPU. Bu benim için yeni bir tanışma. Linux memory subsystem’inin “memory compaction” daemon’u — kernel RAM fragmente, contiguous block bulamıyorum durumunda devreye giriyor, küçük free chunk’ları biraraya getirmeye çalışıyor. Onun %92 CPU yiyor olması, sistemin çoğu CPU’sunu memory aramaya harcadığı demek.
Ve sebebi de görüyorum: aynı anda Astro build (2.5 GB) ve Next.js build (615 MB) koşuyor. Toplam ~3 GB tek başına. Geri kalan 4 GB sistem servisi + container’lar + sshd. Toplam talep > 7.6 GB → swap → swap dolu → kcompactd panik.
Sites neden timeout?
free -h ile RAM 76 MB free olduğunu görmüştüm. sshd bağlandığında “fork(2)” yapıyor, child process’e response vermek için. fork RAM ister. RAM’de deli gibi swap thrashing var, fork bekliyor → connection reset by peer.
Curl benzer hikayeyle yarı kuruyor:
$ curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" --max-time 8 https://mustafaerbay.com.tr
000 8.006s
Nginx bağlandı (port 443 açık), proxy_pass’i Node app’e yapacak (127.0.0.1:3040), Node app cevap veremiyor (RAM yok). Timeout.
Kim yaptı bu Astro build’i?
$ ps -p 382248 -o pid,user,cmd
PID USER CMD
382248 ubuntu node /opt/mustafaerbay/node_modules/.bin/astro build --out-dir dist-new
ubuntu user, /opt/mustafaerbay/dist-new çıktı. Bu update.sh deploy script’ime ait. Yani ben tetikledim — git push yaptım, VPS deploy timer pull etti, build başladı.
Ne süredir koşuyor? 18+ dakika. Normal Astro build 4-5 dakika. Stuck. Memory pressure altında her step yavaşlıyor, asıl olan bitemiyor.
Çözüm yok, hard reset
gh run cancel denedim — ulaşmadı (sinyal RAM thrashing altında delivery edilmiyor). kill 382248 ssh’ım çoğu zaman tutmuyordu. OOM killer da yetişmiyordu çünkü kendi de memory ister, sistem o kadar şişti ki kernel yavaş çalışıyor.
Tek çare kaldı: hostingerime gir, hard reset bas. 90 saniye sonra geri geldi:
$ ssh vps 'uptime; free -h | head -2'
05:38:07 up 0 min, 2 users, load average: 2.74, 0.58, 0.19
total used free shared buff/cache available
Mem: 7.6Gi 1.4Gi 4.9Gi 83Mi 1.6Gi 6.1Gi
6.1 GB available. Tüm container’lar restart: unless-stopped ile auto-start oldu. Postgres’ler WAL ile recover etti. Sites 200 dönüyordu.
Şimdi: bunun bir daha olmaması için
İlk gece çalışmaya başladım. Tek refleks “build daha az kullansın” yetmez — bu defansif olur. Önleyici olmalıyım. Birkaç katman lazım.
1. Pre-flight resource guard (workflow)
- name: Pre-flight resource check
id: preflight
run: |
AVAIL_GB=$(df -BG / | tail -1 | awk '{print $4}' | tr -d 'G')
LOAD=$(awk '{print $1}' /proc/loadavg)
LOAD_INT=${LOAD%.*}
MEM_AVAIL_MB=$(awk '/MemAvailable/{print int($2/1024)}' /proc/meminfo)
if [ "$AVAIL_GB" -lt 5 ] || [ "$LOAD_INT" -gt 8 ] || [ "$MEM_AVAIL_MB" -lt 1500 ]; then
echo "skip=1" >> "$GITHUB_OUTPUT"
echo "::warning::skipping — kaynaklar yetersiz"
fi
Workflow başlamadan önce VPS’in nefes alıp almadığını kontrol et. Yoksa graceful skip → success exit, mail spam yok.
2. Polling-wait yerine sleep 360
Önceden workflow’da sleep 360 (6 dk) vardı, deploy bitmesini beklemek için. Sleep aktif RAM kullanmaz ama runner slot’unu işgal eder. Eğer build sırasında OOM olursa sleep step’i SIGKILL alır → workflow fail → mail.
Yeni:
for i in $(seq 1 108); do
if curl -fsS -o /dev/null --max-time 5 "$URL"; then
echo "Deploy ${i}. denemede algılandı"
exit 0
fi
sleep 5
done
URL polling. Site live olur olmaz ilerle. Sleep yerine kısa intervaller — OOM-killable durumlar için resilient.
3. AI quirk auto-fixer
Önceki gün üç farklı AI quirk yüzünden cron’lar fail olmuştu. Hepsini “reject yerine fix” stratejisiyle tek bir normalizer’da topladım. Bunu daha önce yazmıştım, ama ilgili: fail-soft mantalitesi her tarafta.
4. Pipeline-health monitor
Bu en önemli olabilir. /var/lib/mustafaerbay/health-state file’ı tutuyor son durumu (healthy / degraded). Cron 4 saatte bir bakıyor son Bluesky paylaşımına. 4+ saat içinde paylaşım yoksa state-change = ilk DEGRADED maili. Sonra 4 saatte bir kontrol ediyor, paylaşım dönerse RECOVERED maili. Aynı state’te 100 cron koşsa bile mail yok.
Bu sabahki olayda 16 cron arka arkaya başarısız olmuştu, sadece 1 mail geldi. Klasik “her workflow run mail at” dünyasında 16 mail gelirdi.
Daha derin ders
Bu OOM’u yaşamamış olsam katmanlı savunmayı kurmazdım. Bir kere yaşamak gerekti — böyle olabilir dedirten somut deneyim. Şimdi her cron başında pre-flight, build wait yerine polling, AI çıktı normalizer, state-change alert.
Bunların hiçbiri OOM’u tek başına engellemez. Birlikte engellerler. Klasik Swiss cheese model:
[OOM gerçekleşiyor] →
layer 1: pre-flight skip ediyor (kaynaklar zaten yetersiz görüldü)
→ layer 2: polling-wait OOM-resilient
→ layer 3: auto-fixer AI quirk'lere takılmıyor
→ layer 4: pipeline-health monitor sadece state-change'de uyarıyor
→ result: yarı saatte 1 cron skip, mail yok, sistem sağlıklı
Tek bir defans olsa bir gün delik bulurdu. Dört delikli peynir sıralı, hizalanmıyor → top geçemez.
Sonuç
Bu OOM’a hazırlıksız yakalandım. Ama post-mortem ile dört katmanlı bir pipeline reliability sistemi kurmak gibi düşününce iyi oldu demek lazım. Kötü gün değer üretebilir — eğer yazıya dökersen.
Bir sonraki cron 30 dakika sonra koşacak. Pre-flight kontrol ediyor olacak. RAM hâlâ 5 GB’tan az ise skip diyecek. Ben artık endişelenmiyorum çünkü bu sistemi gördüm — pipeline kendi başına devam ediyor.