Sabah 08:20'de site açılmıyor. systemctl status mariadb yazıyorsunuz ve karşınıza şu satır çıkıyor: Main process exited, code=killed, status=9/KILL. Veritabanı loglarında hiçbir hata yok — servis düzgün çalışıyor, sonra bir satırın ortasında susuyor. Disk dolu değil, yapılandırma dosyasına dokunulmamış, kimse systemctl stop çalıştırmamış. Servisi elle başlatıyorsunuz, sorunsuz açılıyor ve iki gün sonra yine aynı saatlerde kapanıyor.
Bu tabloyu üreten şey neredeyse her zaman aynıdır: Linux çekirdeğinin OOM killer mekanizması. Sunucunun belleği fiilen bittiğinde çekirdek "hiçbir şey yapmama" seçeneğine sahip değildir; bellek ayıramayan bir sistem birkaç saniye içinde tamamen kilitlenir. Bu yüzden çekirdek bilinçli bir tercih yapar ve bir süreci öldürerek belleği geri alır. Ölen süreç, hatayı yapan süreç olmak zorunda değildir — genellikle sadece en çok bellek tutan süreçtir. Bu yüzden kurban çoğunlukla MySQL olur, oysa belleği tüketen taraf onlarca PHP-FPM işçisi olabilir.
Bu rehberde önce olayın kanıtını nasıl bulacağınızı, log satırındaki sayıların ne anlattığını ve kurbanı kimin seçtiğini göreceğiz. Ardından üç farklı "öldürülme" biçimini birbirinden ayıracak, en sık önerilen iki çözümün (swap eklemek ve oom_score_adj ile oynamak) neden çoğu vakada yanlış olduğunu gerekçesiyle açıklayacak ve kalıcı çözüme geçeceğiz.
Önce Kanıt: dmesg ve journalctl'de "Killed process" Satırını Bulun#
Bir servisin kendiliğinden kapandığından şüpheleniyorsanız tahmin yürütmeyin; kanıt çekirdek günlüklerindedir ve aramak birkaç saniye sürer.
# En hızlı yol: çekirdek halka tamponu, okunabilir zaman damgasıyla
dmesg -T | grep -iE 'killed process|out of memory|oom-kill'
# Yeniden başlatmalara dayanıklı, tarihli arama
journalctl -k --since "3 days ago" --grep "Killed process"
# systemd olmayan veya eski sistemlerde
grep -iE 'killed process|out of memory' /var/log/syslog /var/log/messages
Aradığınız satır tipik olarak şuna benzer:
Out of memory: Killed process 1487 (mysqld) total-vm:4194304kB, anon-rss:2097152kB,
file-rss:0kB, shmem-rss:0kB, UID:112 pgtables:4608kB oom_score_adj:0
Bu satırı bulduysanız teşhis tamamlanmıştır: servis çökmemiş, öldürülmüştür. Bulamadıysanız sebep başka yerdedir — segmentasyon hatası, disk dolması ya da bir yapılandırma hatası olabilir; bu durumda servisin kendi günlüklerine dönün. journalctl ile filtreleme ve zaman aralığı seçmenin ayrıntıları için journalctl ile log yönetimi yazısına bakabilirsiniz.
Bir uyarı: dmesg halka tamponu sınırlı boyuttadır ve sunucu yeniden başlatıldığında sıfırlanır. Olay birkaç gün öncesineyse journalctl -k kullanın; kalıcı günlük tutulmuyorsa /etc/systemd/journald.conf içinde Storage=persistent ayarlanmadığı sürece kayıtlar açılışla birlikte kaybolur.
Log Satırındaki Sayılar Ne Anlatır#
Öldürme satırının çevresindeki alanlar, "gerçekten bu servis mi suçluydu" sorusuna cevap verir.
| Alan | Anlamı | Neden önemli |
|---|---|---|
total-vm | Sürecin ayırdığı toplam sanal bellek | Genellikle abartılıdır, karar için kullanılmaz |
anon-rss | Fiilen RAM'de tutulan, diske geri yazılamayan bellek | Asıl karar sütunu budur |
file-rss | Dosyadan eşlenen, geri alınabilen bellek | Baskı anında serbest bırakılabilir |
shmem-rss | Paylaşılan bellek | tmpfs ve paylaşımlı bölgeler |
oom_score_adj | Sürecin puanına elle eklenen ayar | 0 ise kimse müdahale etmemiş demektir |
En sık yapılan yorum hatası total-vm üzerinden karar vermektir. Java ve MongoDB gibi uygulamalar gerçekte kullanmadıkları çok büyük sanal alanlar ayırabilir; bu, gerçek bellek tüketimi değildir. Fiziksel baskıyı gösteren değer anon-rss'tir.
Öldürme satırının hemen üstünde çekirdek genellikle bir aday tablosu basar: o anda çalışan süreçleri pid, rss ve oom_score_adj değerleriyle listeler. Bu tablo altın değerindedir, çünkü "MySQL 2 GB kullanıyordu" bilgisinin yanına "ve aynı anda 40 tane PHP-FPM işçisi toplam 3 GB tutuyordu" bilgisini koyar. Kurbanın kim olduğu kadar, belleği kimin tükettiği de burada görünür.
# Öldürme satırının etrafındaki aday tablosunu da görmek için
dmesg -T | grep -B 40 -A 5 'Killed process' | less
OOM Killer Kurbanı Nasıl Seçer?#
Çekirdek her süreç için bir oom_score hesaplar. Hesabın temeli basittir: süreç sistemin toplam belleğinin ne kadarını tutuyorsa puanı o kadar yüksektir. Buna sürece elle atanmış oom_score_adj değeri eklenir ve en yüksek puanlı süreç öldürülür.
Anlık puanları görebilirsiniz:
# En riskli 10 süreç
for p in /proc/[0-9]*; do
[ -r "$p/oom_score" ] && echo "$(cat $p/oom_score) $(basename $p) $(cat $p/comm)"
done | sort -rn | head -10
# Tek bir sürecin puanı ve ayarı (util-linux 2.32+)
choom -p 1487
Buradaki tasarım kararı, sorunun kaynağını gizleyen şeydir: çekirdek "suçluyu" değil, en çok belleği geri kazandıracak süreci seçer. Bir sunucuda MySQL genellikle tek başına en büyük süreçtir, çünkü innodb_buffer_pool_size ona öyle söylenmiştir. Belleği asıl tüketen taraf, her biri 120 MB tutan ve sayısı sınırsız büyüyen PHP işçileri olsa bile, tek tek küçük oldukları için puanları düşük kalır ve MySQL kurban gider. Bu yüzden "MySQL çok bellek kullanıyor, onu küçültelim" refleksi çoğu zaman yanlış kapıdır.
Kernel OOM mu, systemd-oomd mü, cgroup Limiti mi?#
Bir sürecin öldürülmesinin üç farklı yolu vardır ve çözümleri birbirinden tamamen ayrıdır. Karıştırmak, günlerce yanlış yerde arama yapmak demektir.
| Öldüren | Ayırt edici günlük izi | Sebep | Çözüm yönü |
|---|---|---|---|
| Çekirdek OOM killer | Out of memory: Killed process ... | Sistem geneli RAM tükendi | RAM artırımı veya servis sınırlama |
| cgroup bellek sınırı | Memory cgroup out of memory satırı | Servisin kendi sınırı doldu | Birimin MemoryMax değeri |
| systemd-oomd | systemd-oomd birimi tarafından basılan "Killed" satırı | PSI bellek baskısı eşiği aşıldı | Eşik ayarı veya asıl bellek sorunu |
Memory cgroup out of memory satırını görüyorsanız sistemde bol boş RAM olsa bile servis öldürülmüştür; sınırı siz ya da paket koymuştur. Şununla doğrulayın:
systemctl show mariadb -p MemoryMax -p MemoryHigh -p MemoryCurrent
systemd-oomd, çekirdek OOM killer devreye girmeden önce müdahale eden kullanıcı alanı bir servistir; PSI bellek baskısı verilerini izler ve eşik aşılırsa bir cgroup'u sonlandırır. Ubuntu masaüstü kurulumlarında varsayılan olarak etkindir, sunucu kurulumlarında genellikle yüklü gelmez — ama kurulu olup olmadığını kontrol etmek beş saniyelik iştir:
systemctl is-active systemd-oomd
journalctl -u systemd-oomd --since "3 days ago"
Swap Eklemek Çözüm mü?#
Bu, aramalarda en çok karşınıza çıkan öneridir ve doğruluk payı vardır, ama sınırlıdır. Doğru cümle şudur: swap, OOM killer'ı geciktirir; bellek yetersizliğini ortadan kaldırmaz.
Swap'in gerçekten faydalı olduğu durum, uzun süre dokunulmayan "soğuk" belleğin varlığıdır. Yılda üç kez çalışan bir arka plan servisinin tuttuğu 200 MB, swap'e taşınır ve RAM asıl işe kalır. Bu, sağlıklı ve arzu edilen bir davranıştır.
Swap'in çözüm olmadığı durum ise sık karşılaşılandır: aktif olarak kullanılan bellek RAM'e sığmıyorsa sistem sürekli takas yapmaya başlar. Buna "thrashing" denir ve sonucu çoğu zaman OOM killer'dan daha kötüdür — süreç ölmez ama her bellek erişimi disk erişimine döndüğü için sunucu yanıt veremez hâle gelir. SSH bile açılmaz. Bu durumda OOM killer'ın hızlıca bir süreci öldürüp sistemi kurtarması aslında daha temiz bir sonuçtur.
Pratik kural: swap'i tampon olarak ekleyin, kapasite olarak değil. 1-2 GB RAM'li bir sunucuda RAM kadar, 4 GB ve üzerinde 2 GB civarı bir swap ani sıçramaları yumuşatır. vm.swappiness değerini sunucularda 10-20 aralığına çekmek, çekirdeğin gereksiz yere takas yapmasını azaltır. Kurulum adımları için Linux'ta swap alanı oluşturma yazısına bakabilirsiniz.
Sağlık kontrolü basittir: vmstat 1 5 çıktısında si ve so sütunları sürekli sıfırdan büyükse swap tampon olmaktan çıkmış, sorunun parçası olmuştur.
oom_score_adj ile Oynamak Neden Yanlış Çözüm#
İkinci popüler öneri, kritik servisi OOM killer'dan korumaktır:
# MySQL'i kurban listesinin altına indir (kalıcı DEĞİL, süreç yeniden başlayınca sıfırlanır)
choom -n -500 -p $(pidof mariadbd)
Bu gerçekten işe yarar — ama yaptığı tek şey kurbanı değiştirmektir. Bellek yine yetmeyecek, çekirdek yine birini öldürecektir; sadece bu sefer sırada PHP-FPM, Redis, SSH sunucusu ya da yedekleme süreci vardır. Veritabanını kurtarıp SSH erişimini kaybetmek net bir kazanç değildir.
Değer aralığı -1000 ile 1000 arasındadır ve iki uç özellikle tehlikelidir. -1000 bir süreci OOM killer için fiilen dokunulmaz yapar; belleği kaçıran süreç oysa sistem, kurtulma şansı olmayan bir kilitlenmeye girer. 1000 ise süreci ilk kurban ilan eder.
Bu ayarın meşru kullanımı dardır ve şudur: sistemin ayakta kalması için kritik olan, belleği sabit ve küçük bileşenleri korumak — örneğin SSH sunucusunu makul bir negatif değerle koruyup uzaktan müdahale imkânını garanti altına almak. Kalıcı yapmak için systemd birimine yazılır:
# systemctl edit ssh
[Service]
OOMScoreAdjust=-500
Özetle oom_score_adj bir kurtarma tedbiridir, bir çözüm değildir. Asıl işi aşağıdaki iki adım yapar.
Kalıcı Çözüm 1: Bellek Tüketen Servisi Sınırlayın#
Çoğu OOM vakasında sunucunun RAM'i yetersiz değildir; bir servise ne kadar kullanabileceği söylenmemiştir. Varsayılan yapılandırmalar tek bir uygulamanın makinede yalnız olduğunu varsayar; sizin sunucunuzda ise MySQL, PHP-FPM, web sunucusu, Redis ve yedekleme betiği aynı RAM'i paylaşır.
MySQL/MariaDB. Tek büyük tüketici innodb_buffer_pool_size'dır. Yalnızca veritabanı sunucusu olan bir makinede RAM'in %60-70'i uygundur; web ve PHP ile aynı makinedeyse %25-30 daha gerçekçidir. max_connections da unutulmamalı: her bağlantı kendi tampon belleğini alır, dolayısıyla yüksek bir değer teorik bellek tavanını hızla yükseltir.
# /etc/mysql/mariadb.conf.d/60-bellek.cnf
[mysqld]
innodb_buffer_pool_size = 1G
max_connections = 80
Ayarları uygulamadan önce mevcut kullanımı ölçmek ve sorgu tarafını da düzeltmek için MySQL ve MariaDB performans optimizasyonu yazısı ayrıntılı bir kaynaktır.
PHP-FPM. Bu, en sık gözden kaçan taraftır çünkü tek bir süreç değil, çarpım hâlinde büyür. Önce ortalama işçi boyutunu ölçün:
ps -eo rss,comm | grep -i php-fpm | awk '{s+=$1; n++} END {printf "ortalama %.0f MB, %d surec\n", s/n/1024, n}'
Sonra üst sınırı buna göre belirleyin. Kaba hesap: pm.max_children = PHP için ayrılan RAM / ortalama işçi boyutu. 4 GB RAM'li, MySQL'e 1 GB verilmiş bir sunucuda PHP'ye kalan yaklaşık 2 GB ise ve işçi başına 100 MB ölçtüyseniz üst sınır 20 civarındadır — 50 değil. Havuz ayarlarının tamamı için PHP-FPM pool ayarları yazısına bakın.
systemd ile sert tavan koymak. En dayanıklı yöntem, servise cgroup düzeyinde tavan koymaktır. Böylece bir servis kaçtığında yalnızca kendi sınırına çarpar ve sistem geneli OOM tetiklenmez; komşu servisler ayakta kalır.
# systemctl edit mariadb
[Service]
MemoryHigh=1200M
MemoryMax=1500M
Restart=on-failure
RestartSec=5s
MemoryHigh yumuşak eşiktir: aşıldığında çekirdek süreci baskılar ve belleği geri almaya çalışır. MemoryMax sert tavandır; aşılırsa öldürme yalnızca o cgroup içinde olur. Birim dosyalarıyla çalışmaya alışkın değilseniz systemd servis yönetimi yazısı başlangıç için uygundur.
Kalıcı Çözüm 2: Sızıntı mı, Yetersiz RAM mi?#
Sınırları koyduktan sonra cevaplanması gereken asıl soru şudur: bu sunucu yetersiz mi, yoksa bir yerde sızıntı mı var? İkisi aynı belirtiyi verir ama çözümleri zıttır.
Ayrım ölçümle yapılır. Bir servisin bellek kullanımını zaman içinde izleyin:
# 5 dakikada bir, süreç bazında RSS kaydı
while true; do
date +'%F %T' >> /var/log/bellek-izleme.log
ps -eo rss,comm --sort=-rss | head -8 >> /var/log/bellek-izleme.log
sleep 300
done
İki desenden birini göreceksiniz:
- Yetersiz RAM: Kullanım gün içinde trafikle birlikte iniyor çıkıyor, tepe noktalarda sınıra dayanıyor, gece düşüyor. Kullanım yükle ilişkilidir. Çözüm kapasite artırımıdır; doğru boyutu seçmek için VDS için kaç CPU ve RAM gerekir yazısındaki hesap yöntemi işe yarar.
- Bellek sızıntısı: Kullanım yalnızca artar, trafiğe bakmaksızın düşmez ve servis yeniden başlatıldığında sıfırlanır, ardından aynı eğimle tırmanmaya devam eder. Bu tabloda RAM eklemek kazayı yalnızca erteler: iki gün yerine dört günde aynı yere gelirsiniz. Sorumlu genellikle bir uygulama eklentisi, sürekli çalışan bir kuyruk işçisi ya da hatalı bir kütüphanedir.
Sızıntı doğrulandıysa hızlı bir tampon olarak PHP-FPM'de pm.max_requests ayarını verip işçilerin belirli sayıda istekten sonra yenilenmesini sağlayabilirsiniz. Bu, sızıntıyı düzeltmez ama etkisini sınırlar ve asıl hatayı bulana kadar zaman kazandırır.
Tekrarını Önlemek: Alarm Kurmadan Bitirmeyin#
OOM olayının en can sıkıcı yanı, gece 03:00'te olması ve sabaha kadar kimsenin haberi olmamasıdır. Yapılandırmayı düzelttikten sonra son adım, bir sonraki olayın size gelmesini sağlamaktır.
En basit yöntem, olayı doğrudan çekirdek günlüğünden yakalayan bir kontroldür:
# Son 1 saatte OOM olayı olduysa çıktı üretir; cron ile çalıştırıp mail alabilirsiniz
journalctl -k --since "1 hour ago" --grep "Killed process" --no-pager
Daha kalıcı bir kurulum için available bellek eşiğine ve /proc/pressure/memory içindeki avg60 değerine alarm bağlamak, olayı gerçekleşmeden önce yakalar: bellek baskısı, ölüm anından dakikalar hatta saatler önce yükselmeye başlar. Grafik ve eşik uyarılarını hazır kurulumla almak isterseniz Netdata ile sunucu izleme bu iki metriği kutudan çıktığı gibi sunar. Değerleri elle yorumlamak için de free çıktısını doğru okumak yazısı iyi bir başvuru kaynağıdır.
Doğru sıra şudur: kanıtı bul, kurbanı değil tüketiciyi belirle, servislere tavan koy, sızıntı olup olmadığını ölçümle ayır, sonra alarm kur. Bu beş adımı tamamlamadan RAM yükseltmek çoğu zaman aynı sorunu daha pahalı bir sunucuda tekrar yaşamak olur.
Sıkça Sorulan Sorular#
OOM killer devreye girdiğinde sunucu yeniden mi başlar?#
Hayır. OOM killer yalnızca seçtiği süreci sonlandırır; işletim sistemi çalışmaya devam eder. Bu yüzden uptime değeri değişmez ve olay dışarıdan "servis çöktü" gibi görünür. Sunucunun gerçekten yeniden başladığını düşünüyorsanız uptime çıktısına bakın; süre sıfırlanmamışsa yaşanan şey yeniden başlatma değil, süreç öldürmedir.
MySQL neden hep o öldürülüyor, hatalı olan o mu?#
Genellikle değil. Çekirdek en çok bellek tutan süreci seçer ve MySQL, kendisine tanımlanan tampon havuzu nedeniyle çoğu sunucuda en büyük tek süreçtir. Belleği asıl tüketen taraf, tek tek küçük ama sayıca fazla olan PHP-FPM işçileri olabilir. Öldürme satırının üzerindeki aday tablosunda süreçlerin toplam bellek dağılımına bakmadan suçlu belirlemeyin.
Swap eklersem OOM killer bir daha devreye girmez mi?#
Girer. Swap yalnızca zaman kazandırır: bellek talebi kalıcı olarak RAM kapasitesini aşıyorsa sistem önce takas yapmaya başlar, yavaşlar ve sonunda yine OOM killer'a düşer. Swap'i ani sıçramaları yumuşatan bir tampon olarak düşünün. vmstat çıktısında si ve so sütunları sürekli sıfırdan büyükse swap artık çözüm değil, sorunun bir parçasıdır.
Bellek yeterli görünüyor ama servis yine öldürüldü, neden?#
Muhtemelen sistem geneli değil, cgroup düzeyinde bir sınıra çarptınız. Günlükte Memory cgroup out of memory satırını arayın ve systemctl show SERVISADI -p MemoryMax ile birime tanımlı tavanı kontrol edin. Konteyner ortamlarında da aynı durum geçerlidir: ana makinede boş RAM olsa bile konteynerin kendi bellek sınırı doluysa içerideki süreç öldürülür.
OOM olayının hangi saatte olduğunu nasıl öğrenirim?#
dmesg -T komutu çekirdek mesajlarını okunabilir zaman damgasıyla gösterir. Daha güvenilir yol journalctl -k --since "3 days ago" --grep "Killed process" kullanmaktır; bu, sunucu yeniden başlatılmış olsa bile kalıcı günlük tutuluyorsa kayıtları getirir. Saat bilgisi kritiktir çünkü olay sürekli aynı saatte tekrarlıyorsa suçlu genellikle zamanlanmış bir yedekleme veya rapor görevidir.
Uygulamayı hiç öldürülmeyecek şekilde ayarlayabilir miyim?#
Teknik olarak oom_score_adj değerini -1000 yaparak mümkündür, ancak bu tavsiye edilmez. Belleği tüketen sürecin kendisi dokunulmaz hâle gelirse çekirdek başka bir süreci, hatta SSH sunucusunu öldürür ve sisteme uzaktan müdahale edemez hâle gelebilirsiniz. Daha güvenli yaklaşım, servislere MemoryMax ile tavan koymak ve öldürmenin etkisini tek bir servisin içinde tutmaktır.