Ders 05 / 10
Kod Önbellekleme
PHP'nin bayt kodunu önbellekte tutması üretimde dosya değişikliğini nasıl gizler: aynı yoldaki yeni sürüm ne zaman gelir, geçersiz kılma neyi çözer.
Yayın Güncelleme
Bu derste
- Bayt kodu ile kod önbelleğinin ne olduğunu ve üretimde dosya denetiminin neden kapatıldığını açıklayabilmek
- Üretimde yeniden yazılan bir dosyanın eski sürümle koşabileceğini ölçerek göstermek
- opcache_invalidate() ile geçersiz kılmanın iki profilde nasıl davrandığını yorumlamak
İçindekiler
PHP bir dosyayı çalıştırmadan önce kaynağı bir ara biçime, yani bayt koduna (bytecode) çevirir, sonra bu biçimi çalıştırır. Kod önbelleği (opcode cache) çevrilmiş bayt kodunu bellekte tutar ve sonraki istek çeviri adımını atlar. PHP’de bu işi OPcache eklentisi yapar. Önbelleğin bir bedeli var: bellekteki kopya ile diskteki dosya ayrışabilir. Kodu yayına aldığınızda hangisinin koştuğu, önbelleğin diske ne zaman baktığına bağlıdır.
Bu dersin ölçü ekseni şu soru: bu davranış üretime çıkıyor mu? Aşağıdaki yan.php kursun ortak çekirdeğidir. Bir betiği önce geliştirme, sonra üretim yönergeleriyle ayrı süreçlerde koşturur ve davranışın hangi yanda görüldüğünü söyler. Dosya bu derste yalnız diske yazılır, çıktısı yoktur.
<?php // yan.php — bu davranış üretime çıkıyor mu? declare(strict_types=1); // PHP'nin dağıttığı iki ayar dosyası (php.ini-development, php.ini-production) yedi // yönergede ayrışır; yedisi buraya birebir alındı. Üretimdeki son iki satır dağıtım // dosyasında yorum satırıdır: kod önbelleği komut satırında da açık, dosya denetimi kapalı. const PROFILLER = [ 'geliştirme' => [ 'display_errors' => '1', 'display_startup_errors' => '1', 'error_reporting' => '32767', 'mysqlnd.collect_memory_statistics' => '1', 'zend.assertions' => '1', 'zend.exception_ignore_args' => '0', 'zend.exception_string_param_max_len' => '15', ], 'üretim' => [ 'display_errors' => '0', 'display_startup_errors' => '0', 'error_reporting' => '24575', 'mysqlnd.collect_memory_statistics' => '0', 'zend.assertions' => '-1', 'zend.exception_ignore_args' => '1', 'zend.exception_string_param_max_len' => '0', 'opcache.enable_cli' => '1', 'opcache.validate_timestamps' => '0', ], ]; /** Betiği ayar dosyası olmadan, yalnız profilin yönergeleriyle ayrı bir süreçte koşturur. */ function calistir(string $betik, array $yonergeler): bool { $komut = ['php', '-n']; foreach ($yonergeler as $ad => $deger) { array_push($komut, '-d', "$ad=$deger"); } $komut[] = $betik; $surec = proc_open($komut, [1 => ['pipe', 'w'], 2 => ['file', '/dev/null', 'w']], $borular); $cikti = stream_get_contents($borular[1]); fclose($borular[1]); proc_close($surec); $satirlar = array_values(array_filter(array_map('trim', explode("\n", $cikti)))); return end($satirlar) === 'evet'; } /** Davranışın iki koşumda görülüp görülmediğinden yanını çıkarır. */ function yan(bool $gelistirmede, bool $uretimde): string { return match (true) { $gelistirmede && $uretimde => 'iki yanda da', $gelistirmede => 'yalnız geliştirmede', $uretimde => 'yalnız üretimde', default => 'koşuma girmiyor', }; } function yanRaporu(string $ad, string $betik): string { $gelistirmede = calistir($betik, PROFILLER['geliştirme']); $uretimde = calistir($betik, PROFILLER['üretim']); $sonYan = yan($gelistirmede, $uretimde); echo "$ad → $sonYan\n geliştirme ", $gelistirmede ? 'evet' : 'hayır', ' · üretim ', $uretimde ? 'evet' : 'hayır', "\n"; return $sonYan; }
Önbelleğin neden bu kadar sıkı çalıştığını anlamak için iki işi ayırın. Birincisi dosyayı bulup okumak ve bayt koduna çevirmektir; ikincisi bayt kodunu çalıştırmaktır. Önbellek yalnız birinciyi atlar, ikincisi her istekte yinelenir. Bir uygulama yüzlerce dosyayı yüklüyorsa atlanan iş büyür, bu yüzden üretimde önbellek açık tutulur. Geliştirmede ise tersine, dosyayı her kaydedişinizde sonucun değişmesini istersiniz; önbelleğin eski kopyayı tutması işinize engel olur.
opcache.validate_timestamps bu ikisi arasındaki anahtardır. Açıkken önbellek, dosyanın değişme zamanına bakıp kopyanın güncel olup olmadığına karar verir. Kapalıyken kopya, süreç yeniden başlatılana ya da dosya açıkça geçersiz kılınana (invalidation) kadar geçerlidir. Üretimde kapatmanın nedeni, çalışan koda dağıtım dışında kimsenin dokunmamasıdır: kod değişecekse bunun bir dağıtım adımı olması beklenir. Bunun doğal sonucu şudur: dağıtımı bilmeyen bir önbellek, dosya değişse bile eski kodu sunar.
Üretim profilinde opcache.validate_timestamps değeri 0dır: önbellek, dosyanın değişme zamanına bir daha bakmaz. Geliştirme profilinde kod önbelleği komut satırında kapalıdır, yani her koşum dosyayı diskten okur. Farkın görünür sonucunu ölçelim. Aşağıdaki betik iki yoklama dosyası yazar ve ikisini de yan.php ile iki profilde koşturur. OPcache son iki saniyede değişmiş bir dosyayı önbelleğe almadığı için (opcache.file_update_protection), yoklamalar dosyaların zamanını touch() ile geçmişe alır.
<?php // onbellek.php — yeniden yazılan dosya eski sürümle mi koşar? declare(strict_types=1); require __DIR__ . '/yan.php'; file_put_contents('eski.php', <<<'PHP' <?php // eski.php — aynı yolu iki sürümle yazar, iki kez yükler $yol = __DIR__ . '/surum.php'; file_put_contents($yol, '<?php return 1;'); touch($yol, time() - 60); $ilk = include $yol; file_put_contents($yol, '<?php return 2;'); touch($yol, time() - 30); $sonra = include $yol; echo $ilk === $sonra ? "evet\n" : "hayır\n"; PHP); file_put_contents('gecersiz.php', <<<'PHP' <?php // gecersiz.php — geçersiz kılma yeni sürümü getirir mi? $yol = __DIR__ . '/surum.php'; file_put_contents($yol, '<?php return 1;'); touch($yol, time() - 60); include $yol; file_put_contents($yol, '<?php return 2;'); touch($yol, time() - 30); $dondu = opcache_invalidate($yol, true); $sonra = include $yol; echo $dondu === true && $sonra === 2 ? "evet\n" : "hayır\n"; PHP); yanRaporu('yeniden yazılan dosya eski sürümle koşar', 'eski.php'); yanRaporu('geçersiz kılma true döner ve yeni sürüm gelir', 'gecersiz.php');
yeniden yazılan dosya eski sürümle koşar → yalnız üretimde
geliştirme hayır · üretim evet
geçersiz kılma true döner ve yeni sürüm gelir → yalnız üretimde
geliştirme hayır · üretim evet
Çıktıyı okumadan önce betiğin ne yaptığına satır satır bakalım. eski.php aynı yola surum.php adlı küçük bir dosya yazar; dosya yalnız bir sayı döndürür, ilk sürümde 1, ikincisinde 2. Her yazıştan sonra touch() ile dosyanın değişme zamanı geçmişe alınır, ilkinde altmış, ikincisinde otuz saniye öncesine. İki zamanın farklı olması önemlidir: aynı zamanı verseydik, denetim açık bir önbellek bile değişikliği ayırt edemezdi. Dosya iki kez include edilir ve iki dönüş değeri karşılaştırılır. Aynıysa, yani ikinci yükleme yeni sürümü görmediyse, yoklama evet yazar.
gecersiz.php ise ikinci yazıştan sonra, ikinci include öncesinde opcache_invalidate($yol, true) çağırır. İkinci parametre true, çağrının önbellekteki kopyayı dosyanın zamanına bakmadan geçersiz saymasını ister. Yoklama iki koşulu birlikte arar: çağrının true dönmesi ve ikinci yüklemenin 2 vermesi. Çağrı önbellek kapalıyken false döner, çünkü geçersiz kılınacak bir kopya yoktur; bu false bir hata değil, “yapılacak iş yok” cevabıdır.
Üretim yanında ilk yoklama evet verir (BA1. Ölçüm komut satırı sürecinde, tek süreçle yapıldı; bir süreç yöneticisi altında birden çok işçi süreç aynı kod önbelleğini paylaşır, bu derste o düzen kurulmadı): dosya yeniden yazıldığı hâlde ikinci yükleme birinciyle aynı değeri döndürdü, çünkü önbellek yolu zaten tanıyordu ve zamana bakmadı. Geliştirme yanında yeni sürüm hemen göründü. İkinci yoklama çıkış yolunu gösterir: opcache_invalidate($yol, true) üretimde true döner ve aynı istekte yeni sürüm gelir. Geliştirmede kod önbelleği kapalı olduğundan çağrı false döner ve dosya zaten hep yenidir. Yani bir dağıtım betiğinin değişen dosyaları geçersiz kılması gerekir; geliştirmede bu hiç fark edilmez.
Dosya Önbellekte mi?
Şimdiye kadar önbelleğin varlığını dolaylı gördük: dosya değişti ama sonuç değişmedi. Doğrudan sormanın yolu da var. opcache_is_script_cached($yol) verilen dosyanın şu anda önbellekte olup olmadığını bir mantıksal değerle söyler, opcache_get_status(false) ise eklentinin durumunu bir dizi olarak döndürür. Dizide sayı taşıyan alanlar da vardır, ama yoklama yalnız açık/kapalı bilgisini okur ve evet ya da hayır basar. Üç soru sorulur: eklenti açık mı, yüklenen dosya önbellekte mi, geçersiz kılınan dosya hâlâ önbellekte mi? Üçü de evet yönünde yazıldı. Üçüncüsünde evet, üretimde geçersiz kılmanın kopyayı önbellekten çıkardığı anlamına gelir; geliştirmede ne anlama geldiği çıktıdan sonra okunur.
<?php // durum.php — dosya önbellekte mi? declare(strict_types=1); require __DIR__ . '/yan.php'; file_put_contents('acik.php', <<<'PHP' <?php // acik.php — kod önbelleği bu süreçte açık mı? $durum = opcache_get_status(false); echo is_array($durum) && $durum['opcache_enabled'] === true ? "evet\n" : "hayır\n"; PHP); file_put_contents('yuklenen.php', <<<'PHP' <?php // yuklenen.php — include edilen dosya önbelleğe girer mi? $yol = __DIR__ . '/hedef.php'; file_put_contents($yol, '<?php return 1;'); touch($yol, time() - 60); include $yol; echo opcache_is_script_cached($yol) ? "evet\n" : "hayır\n"; PHP); file_put_contents('atilan.php', <<<'PHP' <?php // atilan.php — geçersiz kılınan dosya önbellekten çıkar mı? $yol = __DIR__ . '/hedef2.php'; file_put_contents($yol, '<?php return 1;'); touch($yol, time() - 60); include $yol; opcache_invalidate($yol, true); echo !opcache_is_script_cached($yol) ? "evet\n" : "hayır\n"; PHP); yanRaporu('kod önbelleği açık', 'acik.php'); yanRaporu('include edilen dosya önbelleğe girer', 'yuklenen.php'); yanRaporu('geçersiz kılınan dosya önbellekten çıkar', 'atilan.php');
kod önbelleği açık → yalnız üretimde
geliştirme hayır · üretim evet
include edilen dosya önbelleğe girer → yalnız üretimde
geliştirme hayır · üretim evet
geçersiz kılınan dosya önbellekten çıkar → iki yanda da
geliştirme evet · üretim evet
Çıktının ilk satırı, eklentinin bu süreçte açık olup olmadığını söyler. Geliştirme yanında hayır çıkması, önceki bölümde söylediğimizle uyumludur: komut satırı sürecinde kod önbelleği varsayılan olarak kapalıdır ve geliştirme profili ona dokunmaz. Üretim yanında evet çıkması, profildeki opcache.enable_cli=1 yönergesinin işidir. Bu satır yoklamanın zemin kontrolüdür: sonraki iki satırı okurken eklentinin gerçekten açık olduğunu bilmeniz gerekir, yoksa hayır yanıtlarını davranış sanabilirsiniz.
İkinci satır asıl soruya cevap verir: include ettiğimiz dosya önbelleğe girdi mi? Geliştirmede hayır, çünkü önbellek yok; üretimde evet. Burada ölçülen şey, dosyanın bellekte bir kopyası olduğudur. Bu kopya, birinci bölümdeki eski sürüm davranışının dayanağıdır: dosya bir kez girdikten sonra validate_timestamps=0 ile bir daha diske bakılmaz. İki ölçümü birlikte okuyunca zincir tamamlanır. Önce dosya önbelleğe girer, sonra disk değişir, sonra önbellek diske bakmadığı için eski sürüm koşar.
Üçüncü satır iki yanda da evet verir ve bu satırı dikkatle okumak gerekir. Üretimde evet, opcache_invalidate($yol, true) çağrısının kopyayı önbellekten çıkardığını gösterir. Geliştirmede ise evet çıkması hiçbir şeyin kanıtı değildir: önbellek kapalı olduğundan dosya zaten hiç girmemişti, dolayısıyla “çıktı mı” sorusunun cevabı baştan olumludur. Bu yüzden satırın ölçü sınıfı iki yanda dadır ve bu sınıf burada “iki yanda aynı davranış” demek değil, “ölçüm yalnız üretimde bir şey söyler” demektir. Sınıfı doğru adlandırmak için sınıfın neyi ölçtüğünü bilmek gerekir; yoklamanın iki yanda aynı sonucu vermesi, davranışın iki yanda aynı olduğunu her zaman göstermez.
Gerçek bir üretim kurulumunda bu üç soru, dağıtım sonrası doğrulamanın temelidir. Eklenti açık mı sorusu, yönergenin sürece ulaştığını doğrular; yapılandırma dosyası doğru yerde olsa bile eklenti yüklenmemişse yönergeler etkisiz kalır. Dosya önbellekte mi sorusu ısınmanın işe yarayıp yaramadığını söyler: ilk istekten önce kritik dosyalar önbelleğe alınmışsa ilk kullanıcı çeviri bedelini ödemez. Geçersiz kılınan dosya çıktı mı sorusu ise dağıtım betiğinizin eski sürümü gerçekten bıraktığını teyit eder. Bu ders komut satırında, tek süreçte ölçtü; bir süreç yöneticisi altında her işçi sürecin bu üç cevabı aynı kod önbelleğinden alacağı için sonuçlar paylaşılır.
Sınırlayıcı Ölçüm
Üretim profiline opcache.validate_timestamps=1 ve opcache.revalidate_freq=0 eklersek önbellek her istekte dosyaya bakar. Eski sürüm sorunu kalkar mı? Aşağıdaki betik eski.php yoklamasını bu iki yönergeyle koşturur. eski.php önceki bloktan diskte duruyor.
<?php // sinir.php — üretime dosya denetimi eklenirse eski sürüm kalır mı? declare(strict_types=1); require __DIR__ . '/yan.php'; $denetimli = array_replace(PROFILLER['üretim'], [ 'opcache.validate_timestamps' => '1', 'opcache.revalidate_freq' => '0', ]); $eskiKaldi = calistir('eski.php', $denetimli); echo 'eski sürüm kaldı mı: ', $eskiKaldi ? 'evet' : 'hayır', "\n";
eski sürüm kaldı mı: hayır
Yoklama hayır verdi: dosya denetimi açıkken önbellek her istekte değişme zamanına bakar, yeniden yazılan dosyanın zamanı değiştiği için yeni sürümü derler ve eski sürüm kalmaz. Eski sürüm sorunu bu ayarla kalkar. Kalkmasının bedeli başka yere taşınır: önbellek her istekte dosyanın değişme zamanına bakmak için diske gider. Bu bedel burada ölçülmedi, ölçülen yalnız davranıştır. Üretimde dosya denetimini kapalı tutup dağıtımda opcache_invalidate() çağırmak ya da süreci yeniden başlatmak, bedeli her isteğe değil dağıtım anına yükler. Hangisinin seçileceği, dağıtım düzeninizin neye izin verdiğine bağlıdır.
Bu ölçümün sınırını açıkça söylemek gerekir. Yoklama, dosya denetimi açıkken önbelleğin yeni sürümü gördüğünü gösterdi; ama yenileme sıklığı sıfır olduğu için her isteğin diske gitmesini istemiş olduk. Yenileme sıklığı sıfırdan büyük bir saniye değerine çekilirse, bir dağıtım sonrasında o süre boyunca eski sürümün koşması yeniden mümkün olur. Dersin sınıfı bu yüzden değişmez: eski sürüm davranışı yalnız üretim yanında, denetim kapalıyken ya da yenileme penceresi içindeyken görünür, geliştirme yanında hiç görünmez. Geliştirme profilinde sorunu hiç görmemiş bir ekip, üretime çıktığı gün ilk kez karşılaşır. Ölçümün öğrettiği alışkanlık şudur: yalnız üretimde görünen bir davranışı geliştirmede aramayın, dağıtım adımınıza o davranışı kapatan açık bir geçersiz kılma ya da yeniden başlatma koyun ve bu adımın çalıştığını yukarıdaki üç soruyla doğrulayın.
Bu dersin yoklamaları kasıtlı olarak küçük tutuldu. Dosyalar tek satırlık sayı döndürür, çünkü ölçülmek istenen şey dosyanın içeriği değil, hangi sürümün koştuğudur. Cevap evet ya da hayırdır. Bayt sayısı, bellek miktarı ya da zaman damgası makineden makineye değişir; hangi sürümün koştuğu ise değişmez, tekrar edilebilir olan da odur.
Son olarak sınırlayıcı ölçümün yanını gerekçelendirelim. Dosya denetimi eklenmiş üretim profili yine üretim yanındadır, ama ölçtüğü soru farklıdır: “eski sürüm kaldı mı” sorusuna hayır demesi, geliştirme profilinin davranışına yaklaştığı anlamına gelir. Bu yaklaşma bedelsiz değildir; ders boyunca söylediğimiz gibi bedel disk denetimine taşınır. Sınırlayıcı ölçümün işi, dersin ana iddiasının koşulunu göstermektir: eski sürüm sorunu, denetimin kapalı olmasına bağlıdır ve o koşul kalkınca sorun da kalkar.
Özet
- Bayt kodu, kaynağın çalıştırılmadan önce çevrildiği ara biçimdir; kod önbelleği bu biçimi bellekte tutar ve sonraki isteklerde çeviriyi atlar.
- Bu dersin ölçü sınıfı yalnız üretimdedir:
opcache.validate_timestamps=0iken yeniden yazılan dosya aynı yolda eski sürümle koşar, geliştirme profilinde bu hiç görülmez. opcache_invalidate($yol, true)üretimdetruedöner ve aynı istekte yeni sürümü getirir; geliştirmede önbellek kapalı olduğundanfalsedöner.- Üretime
opcache.validate_timestamps=1veopcache.revalidate_freq=0eklemek eski sürüm sorununu giderir, ama dosya denetimini her isteğe geri taşır. - Ölçümde dosyalar
touch()ile geçmişe alınır, çünkü OPcache son iki saniyede değişen dosyayı önbelleğe almaz. opcache_is_script_cached()yoklaması dosyanın önbelleğe girdiğini üretimde gösterir ve geliştirmedehayırverir; geçersiz kılınan dosyanın çıktığını gösteren satır ise geliştirmede anlamsız olduğundan yalnız üretim yanında bir şey söyler (ölçü sınıfı: iki yanda da, sınırla).
Sonraki Adım
Kod önbelleği, hangi sürümün koştuğunu belirler; ne kadar iş yaptığını söylemez. Bir isteğin zamanı ve belleği nereye gittiğini görmek için profil çıkarmak gerekir. Bir sonraki ders, aynı işin iki profilde nasıl sayıldığını ve sayma işinin kendisinin hangi profilde sonucu değiştirebileceğini ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.