Sunucu & Performans

OPcache Dolunca Neden 502 Alırsınız? (Vaka Analizi)

OPcache Dolunca Neden 502 Alırsınız? (Vaka Analizi)

Kısa cevap: Aynı PHP-FPM master process’ine bağlı tüm siteler, tek bir paylaşımlı OPcache belleğini kullanır — pool başına ayrı değil, sunucu başına tek havuzdur. Bu havuz (opcache.memory_consumption) dolarsa, en son isteği atan site değil, rastgele bir site yarım yüklenmiş bir bootstrap zinciriyle karşılaşabilir; WordPress’te bu genelde Call to undefined function gibi tuhaf, tekrar üretilemeyen bir fatal error olarak görünür ve dışarıdan 502 Bad Gateway gibi algılanır. Kalıcı çözüm: gerçek doluluğu opcache_get_status() ile ölçmek, sunucunun RAM bütçesine göre memory_consumption‘ı kademeli artırmak ve JIT arabelleğinin ayrı bir alan olduğunu unutmamak.

Çok kiracılı (multi-tenant) bir Virtualmin sunucusunda, 6 farklı sitenin PHP isteklerini tek bir PHP-FPM master process karşılıyordu. Bir öğleden sonra sitelerden biri Cloudflare arkasında aniden 502 vermeye başladı. systemctl restart php-fpm sorunu anında çözdü — ama bu, kök nedeni bulmadan geçiştirmek anlamına geliyordu. Bu yazı, belirtinin ardındaki gerçek mekanizmayı nasıl bulduğumuzu ve neden “restart atınca düzeldi”nin bir çözüm değil, sadece bir semptom baskılama olduğunu anlatıyor. Sunucu AlmaLinux + Virtualmin + PHP 8.5 (Remi), ama mantık paylaşımlı PHP-FPM kullanan her sunucuda geçerli.

Belirti: Cloudflare arkasında 502, ama sunucu ayakta

Cloudflare’in hata sayfası nettir: Frankfurt (Cloudflare) çalışıyor, Host hatalı. Yani sorun DNS’te ya da Cloudflare’de değil, doğrudan origin sunucuda bir servisin isteğe cevap vermemesinde. İlk refleks genelde doğrudur ama yanıltıcıdır: systemctl restart php-fpm çalıştırıldı, site anında geri geldi. Diğer 5 site hiç etkilenmemişti — bu da sorunu “sunucu genel çöküyor” ihtimalinden çıkarıp “bir şey belirli bir siteyi/pool’u tıkadı” ihtimaline yaklaştırdı.

Ama bir semptomun geçmesi, nedeninin anlaşıldığı anlamına gelmez. Aynı restart’ın birkaç gün sonra tekrar gerekip gerekmeyeceğini bilmiyorduk.

Teşhis: yanlış izler ve doğru iz

İlk şüpheliler elendi. Nginx error log’unda “upstream timed out” gibi bir kapasite işareti yoktu. pm.max_children reached uyarısı hiçbir FPM log’unda geçmiyordu — yani klasik “trafik worker sayısını doldurdu” senaryosu değildi. Dosya zaman damgaları, cron kayıtları ve disk doluluğu da temizdi; bir deploy ya da yarım dosya yazma da söz konusu değildi.

Asıl iz, domain’e özel PHP error log’undaydı:

domain’e özel PHP hata günlüğü
PHP Fatal error:  Uncaught Error: Call to undefined function get_locale()
in wp-admin/includes/admin.php:16

get_locale() WordPress’in çekirdek fonksiyonlarından biri ve normalde wp-settings.php her zaman admin.php‘den önce tam olarak yüklenir. Bu fonksiyonun “tanımsız” çıkması, PHP’nin bootstrap zincirini yarım çalıştırdığını gösteriyordu — dosya bozuk değildi, ama o an derlenen/cache’lenen script kümesi eksikti.

Bu iz bizi OPcache’e yönlendirdi. Ancak CLI üzerinden opcache_get_status() çağırmak false döndürdü — çünkü opcache.enable_cli kapalıydı, CLI’nin kendi ayrı (ve boş) bir opcache bağlamı var. Gerçek durumu görmek için FPM bağlamında, yani web üzerinden ölçmek gerekiyordu:

geçici ölçüm dosyası — FPM bağlamında opcache durumu
<?php
$s = opcache_get_status();
echo "used_memory: "        . $s['memory_usage']['used_memory']            . "n";
echo "free_memory: "        . $s['memory_usage']['free_memory']            . "n";
echo "num_cached_scripts: " . $s['opcache_statistics']['num_cached_scripts'] . "n";

Sonuç:

ölçüm çıktısı
used_memory: 268435440
free_memory: 16
num_cached_scripts: 4030

256 MB’lık havuzda sadece 16 byte boş yer vardı. num_cached_scripts: 4030 ise tek bir sitenin dosya sayısı değildi — sunucudaki 6 sitenin toplamı. Çünkü OPcache belleği pool başına değil, PHP-FPM master process başına paylaşılır; 6 site aynı master’a bağlıysa hepsi aynı 256 MB’lık havuzu paylaşır.

Kaynağı bulduk: /etc/php.d/99-hardened-performance.ini içinde, sunucu ilk kurulduğunda tespit edilen RAM’e göre otomatik hesaplanmış bir opcache.memory_consumption=256 değeri vardı. Sorun tek bir sitenin trafiğinde değil, 6 sitenin toplam kod tabanının 256 MB’a artık sığmamasındaydı. Havuz kronik olarak doluyken, opcache.revalidate_freq=60 her 60 saniyede dosya değişikliklerini kontrol ederken ya da herhangi bir site yeni bir script cache’lemeye çalıştığında, eviction/yeniden yazma anlarında bir worker’ın yarım bir bootstrap zinciriyle karşılaşma riski oluşuyordu.

Ölçüm scriptini iş bitince silin

opcache_get_status() gibi fonksiyonlar sunucunun iç durumunu (dosya sayısı, bellek haritası, script yolları) ifşa eder. Bu tür geçici ölçüm dosyaları production’da kalıcı bırakılmamalı; ölçüm biter bitmez silinmeli.

Çözüm: önce doğru dosyayı bul, sonra RAM bütçesine göre büyüt

Değeri değiştirmeden önce bir adım atlanmamalı: opcache.memory_consumption‘ı gerçekten hangi dosyanın belirlediğini bulmak. Bu, sunucudan sunucuya değişir — bizim vakamızda sunucuya özel bir “hardening” scriptinin oluşturduğu /etc/php.d/99-hardened-performance.ini dosyasıydı; ama sizde bu doğrudan /etc/php.ini olabilir, panel tarafından yönetilen ayrı bir .ini olabilir ya da PHP sürümüne özel bir Remi/SCL yolu (/etc/opt/remi/phpXX/php.d/) olabilir. Dosya adını tahmin etmek yerine önce arayın:

ayarı hangi dosya veriyor, hangisi gerçekten etkin?
# Olası tüm konumlarda ara
grep -rn "opcache.memory_consumption" 
  /etc/php.ini /etc/php.d/*.ini /etc/opt/remi/*/php.d/*.ini 2>/dev/null

# Hangisinin GERÇEKTEN devrede olduğunu çalışan process'e sor
php-fpm -i 2>/dev/null | grep "opcache.memory_consumption"

Birden fazla dosyada satır çıkabilir — bazıları yorum satırı (; ile başlayan) olabilir, gerçek etkin olan değil. php-fpm -i size etkin değeri ve varsayılan değeri gösterir; hangi .ini dosyasının kazandığını netleştirir. Değişikliği o dosyada yapın.

Değeri belirlerken önce sunucunun RAM bütçesine baktık. Sunucuda 3.5 GB RAM + 2 GB swap vardı ve swap’ın zaten 1.3 GB’ı doluydu — yani “bol RAM var, istediğin kadar büyüt” durumu değildi. vm.swappiness daha önceden 10’a ayarlanmıştı (iyi bir başlangıç), bu da mevcut swap kullanımının gereksiz erken swap’lamadan değil, gerçek bellek baskısından kaynaklandığını gösteriyordu. Bu yüzden körlemesine 512 MB’a çıkmak yerine tedbirli bir adım seçildi: 256 → 384 MB.

kademeli artırım + servis yeniden başlatma
# etkin .ini dosyasında:
opcache.memory_consumption=384

systemctl restart php-fpm

Doğrulama tuzağı: ölçüm sayfanız da cache’lenmiş olabilir

Değişikliği uygulayıp restart ettikten sonra aynı test scriptini tekrar çalıştırdık ve birebir aynı, 256 MB’lık eski sonucu aldık — sanki hiçbir şey değişmemiş gibi. Bu, düzeltmenin çalışmadığını düşündürecek kadar ikna ediciydi. Oysa php-fpm -i çıktısı 384 diyordu, config dosyası 384 yazıyordu — çelişki gerçek değil, görünürdü.

Sebep basitti: test dosyasının URL’i Cloudflare ya da bir önbellek katmanı tarafından cache’lenmişti. .php uzantılı olması bunu otomatik olarak önbellek dışı bırakmıyor — bir “Cache Everything” kuralı, bir sayfa önbellek eklentisi ya da Nginx’in geniş bir location ~ .php$ cache bloğu, dosya adına bakmaksızın her yanıtı yakalayabilir. Önce yanıt header’larına bakmak işe yarar:

önce header’a bak, sonra cache anahtarını kır
# Yanıt önbellekten mi geliyor?
curl -sI https://site.com/opcache-check.php | 
  grep -i "cf-cache-status|x-fastcgi-cache|cache-control|age"

# HIT görüyorsanız: yeni dosya adı + cache-busting parametresi
curl -s "https://site.com/opcache-check-XXXX.php?x=$(date +%s)"

HIT gibi bir değer görürseniz, o anki yanıt PHP’yi hiç çalıştırmamış demektir. Kesin çözüm hem dosya adını değiştirmek hem de sorguya bir cache-busting parametresi eklemektir — ikisi birlikte cache anahtarını kırar.

Bu, gerçek ve taze sonucu verdi: script cache’i tam 384 MB’a çıkmıştı. Ayrıca JIT arabelleğinin (opcache.jit_buffer_size) bundan tamamen ayrı, 128 MB’lık bağımsız bir alan olduğu ve neredeyse boş kaldığı görüldü. Yani gerçek toplam opcache ayak izi 384 + 128 ≈ 512 MB’tı — bunu RAM planlamasına dahil etmek gerekiyordu.

Bu vakanın en az OPcache kadar önemli dersi

Bir düzeltmeyi doğrularken kullandığınız ölçüm aracının kendisi de cache’lenmiş olabilir. Canlı bir sunucuda debug scripti çalıştırıyorsanız, her seferinde benzersiz bir dosya adı ve cache-busting parametresi kullanmadan “değişmedi” sonucuna güvenmeyin — aksi hâlde çalışan bir düzeltmeyi “başarısız” sanıp gereksiz yere daha agresif (ve riskli) adımlara yönelebilirsiniz.

Sonuç: ölçülmüş fark

Metrik Önce Sonra
OPcache boş alan (free_memory) 16 byte (pratik olarak dolu) Rahat seviyede
Script cache boyutu 256 MB 384 MB
Cache’lenen script sayısı (6 site toplamı) 4030 4030 (artık rahat sığıyor)
Kullanılabilir RAM (available) 1.5 Gi 1.7 Gi
Boş RAM (free) 594 Mi 794 Mi
JIT arabelleği Ölçülmemiş 128 MB ayrı, neredeyse boş

Not: Bu düzeltme aynı gün uygulandı; “undefined function” hatasının tekrarlanıp tekrarlanmadığını görmek için domain error log’unu birkaç gün izlemeye devam ettik. Restart’ın semptomu geçici olarak maskelediğini, asıl kanıtın havuzun artık dolmaması olduğunu unutmamak gerekir.

Hangi sunucuda hangi reçete?

Bu vakanın dersi tek bir sabit sayı değil, doğru ölçüm alışkanlığı:

  • Tek site barındıran sunucular: OPcache havuzu genelde o sitenin dosya sayısına göre rahat kalır; varsayılan 128–256 MB çoğu zaman yeterlidir. Yine de opcache_get_status() ile bir kez doğrulamak ucuza mal olur.
  • Çok kiracılı (birden fazla site/pool) sunucular: Her yeni site eklendiğinde toplam script sayısı artar ama opcache havuzu büyümez — bunu elle takip etmek gerekir. num_cached_scripts ve free_memory‘yi periyodik kontrol edin; havuz doluyorsa büyütün.
  • Dar RAM bütçeli sunucular (4 GB ve altı): Körlemesine büyütme yapmayın. Önce free -h ile gerçek boşluğu, sonra JIT’in ayrı bir alan olduğunu hesaba katın. Kademeli artırıp izlemek, tek seferde büyük sıçramadan daha güvenlidir.

Aynı paylaşımlı havuz mantığının JIT tarafında nasıl bir segfault zincirine dönüştüğünü PHP-FPM coredump vaka analizinde ayrıca ele aldık — ikisi aynı sunucu profilinin iki farklı yüzü.

Sıkça sorulan sorular

OPcache neden pool başına değil, sunucu başına paylaşılıyor?

OPcache, derlenmiş PHP bytecode’unu bir shared memory segmentinde tutar ve bu segment PHP-FPM master process’i başlatıldığında bir kez ayrılır. Aynı master’a bağlı tüm pool’lar (siteler) aynı worker havuzundan türediği için aynı shared memory’yi de paylaşır — pool bazlı izolasyon yoktur.

Bir sitenin dosya sayısı artınca diğer siteler neden etkilenebilir?

Havuz ortak olduğu için, bir sitenin kod tabanı büyüdükçe (yeni eklenti, yeni tema dosyası, güncelleme) toplam doluluk artar. Havuz sınıra yaklaştığında, o anda cache’e yazmaya çalışan hangi site ise o etkilenebilir — sorunun kaynağı ile sonucu aynı site olmak zorunda değildir.

OPcache doluluğunu nasıl kontrol ederim?

opcache_get_status() fonksiyonunu FPM bağlamında (CLI’de değil, bir web isteği üzerinden) çalıştırın ve memory_usage.free_memory ile opcache_statistics.num_cached_scripts değerlerine bakın. Ölçüm için kullandığınız geçici dosyayı iş bitince mutlaka silin.

Bende 99-hardened-performance.ini gibi bir dosya yok, ne yapmalıyım?

Sorun değil — bu dosya adı sunucuya özeldi, evrensel bir standart değil. Aynı ayarı sizde /etc/php.ini, panel tarafından yönetilen ayrı bir dosya ya da PHP sürümüne özel bir yol belirliyor olabilir. Dosya adını tahmin etmeyin: grep -rn "opcache.memory_consumption" /etc/php.ini /etc/php.d/*.ini ile tüm olası konumları tarayın, sonra php-fpm -i | grep opcache.memory_consumption ile hangisinin gerçekten etkin olduğunu doğrulayın.

Ne kadar OPcache belleği yeterli?

Sabit bir sayı yok — sunucudaki toplam site sayısına, her sitenin dosya sayısına ve mevcut RAM bütçesine bağlı. Tahmin etmek yerine ölçün: free_memory sıfıra yakınsa büyütme zamanı gelmiştir.

JIT, OPcache belleğini etkiler mi?

Evet ama ayrı bir havuzdan: opcache.jit_buffer_size, memory_consumption‘dan bağımsız ek bir alan ayırır. RAM planlaması yaparken ikisini toplamak gerekir.

Restart atmak neden yeterli bir çözüm değil?

Restart, paylaşımlı bellek segmentini sıfırdan ayırır; havuz bir anlığına boşalır ve site geri gelir. Ama kod tabanı hâlâ havuza sığmıyorsa doluluk kısa sürede geri gelir. Restart semptomu maskeler, kapasite sorununu çözmez.

DKS pratikte ne yapıyor?

Çok kiracılı bir sunucuyu devraldığımızda, “hardening script otomatik ayarladı” diye opcache/PHP ayarlarını sorgusuz kabul etmiyoruz — her sitenin gerçek dosya sayısını ve mevcut doluluğu ölçüp sunucunun RAM bütçesine göre kademeli, ölçülmüş bir değer belirliyoruz. Ortak tema hep aynı: paylaşımlı bir kaynak, doğru izlenmezse tüm sunucuyu etkileyebilir. Bu tür sürekli izleme ve doğrulama işini bakım hizmetimizde, sıfırdan doğru kurulum ve kapasite planlamasını ise performans optimizasyonu ve sunucu kurulumu tarafında üstleniyoruz.

İlgili yazılar

OPcache’i genel tuning bağlamında (PHP-FPM pool modu ve Redis object cache ile birlikte) WordPress Sunucu Performansı: PHP-FPM, OPcache, Redis yazısında kapsamlı ele aldık. Aynı sunucu profilinde paylaşımlı bir kaynağın nasıl OOM’a yol açtığını ClamAV OOM vaka analizinde, JIT’in segfault ve disk şişmesine dönüşen yüzünü ise PHP-FPM coredump vaka analizinde anlattık.

Sitesi ara sıra, açıklanamayan sebeplerle 502 veren bir sunucunuz mu var?

Çok kiracılı sunucularda bu tür “bir site diğerini etkiliyor” senaryoları oldukça yaygın ama teşhisi zaman alır. Bizimle iletişime geçin; paylaşımlı kaynakları ölçüp kalıcı bir kapasite planı çıkaralım.

Digital Kernel Software

Digital Kernel Software

WordPress tema geliştirme, hosting ve sunucu performansı üzerine yazıyoruz. İstanbul / Türkiye.

Projeniz mi var?

Okuduğunuzu
projenize uygulayalım

WordPress, hosting ve performans tarafında yazdıklarımızı sizin işinizde hayata geçirelim. Net konuşuruz — pazarlama dili değil, uygulanabilir çözüm.