Ders 07 / 10
Bellek ve Bağlantı Yönetimi
Bellek sınırı aşılınca isteğin nasıl durduğunu, bağlantı nesnesinin yıkıcısının ne zaman koştuğunu ve döngüsel referansın temizliği nasıl geciktirdiğini ölçmek.
Yayın Güncelleme
Bu derste
- memory_limit aşılınca isteğin durduğunu ve kapanış fonksiyonunun bunu error_get_last() ile gördüğünü göstermek
- Bağlantı nesnesinin yıkıcısının istek sonunda koştuğunu ve kalıcı bağlantı ile bağlantı havuzu kavramlarını ayırmak
- Döngüsel referansta unset() sonrası yıkıcının hemen koşmadığını, gc_collect_cycles() ile koştuğunu yorumlamak
İçindekiler
Bir PHP isteği ayrılmış bir bellek payıyla çalışır. Pay memory_limit yönergesiyle belirlenir; aşılırsa istek kurtarılamaz bir hatayla durur. Bellek kadar önemli bir kaynak da bağlantıdır: veritabanı ya da önbellek sunucusuna açılan her bağlantı, bir yerde kapatılana kadar karşı tarafta yer tutar. Bu ders, ikisinin de istek ömrüne nasıl bağlandığını ölçer. İstek ömrü kavramı önceki kursta kuruldu; burada yalnız ölçümün konusudur.
Ölçü ekseni yine ortak soru: bu davranış üretime çıkıyor mu? yan.php birebir aşağıdadır ve bu derste de yalnız diske yazılır.
<?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; }
Bellek sınırının davranışını anlamak için sınırın ne işe yaradığını hatırlayalım. PHP tek bir isteği kendi payı içinde tutar; pay, bir istekteki hatalı bir döngünün ya da beklenmedik büyük bir girdinin makinenin tamamını tüketmesini önleyen bir emniyettir. Sınır aşıldığında PHP işi yarım bırakır, çünkü devam etmenin güvenli bir yolu yoktur: ayrılacak bellek kalmamıştır. İstek durur, ama durmadan önce PHP kapanış fonksiyonlarını çağırır. Bu davranışı gözlemek için yoklama betiği kasıtlı olarak sonsuz bir döngüde bir megabaytlık dizeler biriktirir. Dizeler bir dizide tutulduğu için hiçbiri serbest kalmaz ve sınır kaçınılmaz olarak aşılır.
İlk ölçüm iki yoklama içerir. Birincisi belleği ini_set('memory_limit', '8M') ile sekiz megabayta çeker ve sınırı aşana kadar bellek ayırır. İstek durduğunda bir kapanış fonksiyonu (shutdown function) devreye girer ve error_get_last() ile son hatanın türüne bakar. İkincisi bir bağlantı nesnesi kurar ve yıkıcısının (destructor) betik bittikten sonra koşup koşmadığına bakar. Çıktıda hata iletisi metni ya da bayt sayısı yoktur; yoklamalar yalnız evet ya da hayır basar.
<?php // bellek.php — sınır aşılınca istek durur, yıkıcı istek sonunda koşar declare(strict_types=1); require __DIR__ . '/yan.php'; file_put_contents('sinir-asimi.php', <<<'PHP' <?php // sinir-asimi.php — memory_limit aşılınca kapanış fonksiyonu hatayı görür ini_set('memory_limit', '8M'); register_shutdown_function(function (): void { $son = error_get_last(); echo $son !== null && $son['type'] === E_ERROR ? "evet\n" : "hayır\n"; }); $parcalar = []; while (true) { $parcalar[] = str_repeat('a', 1024 * 1024); } PHP); file_put_contents('yikici.php', <<<'PHP' <?php // yikici.php — bağlantı nesnesinin yıkıcısı betikten sonra koşar final class Baglanti { public function __destruct() { echo "evet\n"; } } $baglanti = new Baglanti(); echo "betik bitti\n"; PHP); yanRaporu('bellek sınırı aşılınca istek durur', 'sinir-asimi.php'); yanRaporu('yıkıcı betik bittikten sonra koşar', 'yikici.php');
bellek sınırı aşılınca istek durur → iki yanda da
geliştirme evet · üretim evet
yıkıcı betik bittikten sonra koşar → iki yanda da
geliştirme evet · üretim evet
Sınır aşıldığında istek durdu ve kapanış fonksiyonu son hatanın E_ERROR olduğunu gördü; yıkıcı da betik bittikten sonra koştu. İkisi de iki profilde aynı: yan iki yanda da. Geliştirmede ekrana düşen hata iletisi üretimde gizlenir, ama isteğin durması ve kapanış fonksiyonunun çağrılması ayarlara bağlı değildir. Bu yüzden bir isteğin ölümünü yakalayıp günlüğe yazmak istiyorsanız kapanış fonksiyonu iki profilde de çalışan yoldur.
Bağlantılar bu ömre iki biçimde bağlanır. Sıradan bağlantı istekle doğar ve yıkıcısı koşunca, yani istek bitince kapanır. Kalıcı bağlantı (persistent connection) ise istek bittiğinde kapanmaz; süreç onu bir sonraki istek için saklar. Bağlantı havuzu (connection pool) bir adım öteye gider: bağlantıları süreçlerden bağımsız bir katmanda tutar ve istekler arasında paylaştırır. Bu derste ikisi de kurulmadı, yalnız kavram olarak anılıyor; ölçülen şey sıradan bağlantının yıkıcısıdır.
Kapanış Fonksiyonu ve Yıkıcı Sırası
Önceki bölümde iki ayrı mekanizma gördük: sınır aşımında devreye giren kapanış fonksiyonu ve nesne ömrünün sonunda koşan yıkıcı. Gerçek bir uygulamada ikisi birlikte bulunur ve hangisinin önce koştuğu önemlidir. Kapanış fonksiyonunda veritabanına son bir kayıt yazıyorsanız, bağlantı nesnesinin o ana kadar yıkılmamış olması gerekir. Bu bölüm sırayı ölçer. Bağlantı nesnesinin yıkıcısı, kapanış fonksiyonunun koşup koşmadığına bakar ve ona göre evet ya da hayır basar. İlk yoklamada nesne betik sonuna kadar yaşar; ikincisinde betik, kapanış fonksiyonunu kaydettikten sonra nesneyi unset() ile kendisi bırakır.
<?php // sira.php — kapanış fonksiyonu ile yıkıcı hangi sırayla koşar? declare(strict_types=1); require __DIR__ . '/yan.php'; $sinif = <<<'PHP' <?php final class Baglanti { public static bool $kapanisKostu = false; public bool $kapanisOnceMi = true; public function __destruct() { echo self::$kapanisKostu === $this->kapanisOnceMi ? "evet\n" : "hayır\n"; } } register_shutdown_function(function (): void { Baglanti::$kapanisKostu = true; }); PHP; file_put_contents('sonda.php', $sinif . <<<'PHP' $baglanti = new Baglanti(); PHP); file_put_contents('erken.php', $sinif . <<<'PHP' $baglanti = new Baglanti(); $baglanti->kapanisOnceMi = false; unset($baglanti); PHP); yanRaporu('betik sonunda kapanış fonksiyonu yıkıcıdan önce koşar', 'sonda.php'); yanRaporu('unset() ile bırakılan nesnenin yıkıcısı kapanıştan önce koşar', 'erken.php');
betik sonunda kapanış fonksiyonu yıkıcıdan önce koşar → iki yanda da
geliştirme evet · üretim evet
unset() ile bırakılan nesnenin yıkıcısı kapanıştan önce koşar → iki yanda da
geliştirme evet · üretim evet
Önce iki yoklamanın tasarımını açalım. Her ikisinde de kapanış fonksiyonu yalnız bir işi yapar: Baglanti::$kapanisKostu bayrağını true yapar. Yıkıcı ise bayrağın o anki değerine bakar ve nesnenin “kapanış önce mi koşmalı” beklentisiyle karşılaştırır; uyuşuyorsa evet, uyuşmuyorsa hayır basar. Böylece sıra bilgisi, yıkıcının çıktısına dönüşür ve ayrı bir günlük dosyası gerekmez. İlk yoklamada nesne betik sonuna kadar yaşar ve beklenti “kapanış önce”dir. İkincisinde betik nesneyi unset() ile bırakır, nesneyi tutan başka bir referans kalmadığı için yıkıcı hemen koşar; beklenti “kapanış sonra”dır ve bu beklenti kapanisOnceMi alanı false yapılarak iletilir.
İki satır da iki profilde evet verdi. Birinci satırın anlamı şudur: bir nesneyi betik sonuna kadar yaşatırsanız, önce kapanış fonksiyonları koşar, nesneler sonra yıkılır. Bu sıra, kapanış fonksiyonunda bağlantı nesnesini hâlâ kullanabileceğiniz anlamına gelir: günlüğe son kaydı yazmak, açık bir işlemi bitirmek, bir kuyruğa iş bırakmak gibi işleri güvenle yaparsınız. İkinci satırın anlamı da tersidir: nesneyi sizin elinizle bıraktıysanız yıkıcı kapanıştan önce koşar ve kapanış fonksiyonu o bağlantıyı artık bulamaz. İki davranış da dilin kuralıdır ve profil ayarına bağlı olmadığı için iki yanda da sınıfındadır.
Bunun üretimdeki karşılığı basit bir kuraldır. Kapanış fonksiyonunda kullanacağınız bir kaynağı, kaydettiğiniz anonim fonksiyona use ile bağlayarak ya da bir nesne özelliği olarak taşıyın; global bir değişkeni unset() ile bırakmayın. Bellek sınırı aşıldığında kapanış fonksiyonu yine koşar, ama bu noktada bellek tükenmiş olduğundan fonksiyonun yapabileceği iş küçüktür: yalnız hatayı günlüğe yazmak gibi. Büyük bir temizlik ya da yeni bir bağlantı açmak bu anda çoğu zaman başarısız olur. Sınır aşımı ölçümümüzdeki kapanış fonksiyonunun yalnız error_get_last() okuması bu yüzden bilinçli bir seçimdi.
Sınırlayıcı Ölçüm
Yıkıcının “istek sonunda koşar” olması, nesneyi bıraktığınız anda koşar anlamına gelmez. Bir nesne kendisine dönen bir referans taşıyorsa, yani iki nesne birbirini tutuyorsa, unset() ikisini de hemen yok etmez. Bu örnekte iki nesne birbirini tutuyor. Yoklamalar evet yönünde yazıldı: unset() sonrası yıkıcı hemen koşmaz mı, gc_collect_cycles() sonrası koşar mı?
<?php // dongu.php — döngüsel referansta yıkıcı ne zaman koşar? declare(strict_types=1); require __DIR__ . '/yan.php'; $govde = <<<'PHP' <?php class Dugum { public static int $yikilan = 0; public ?Dugum $komsu = null; public function __destruct() { self::$yikilan++; } } $a = new Dugum(); $b = new Dugum(); $a->komsu = $b; $b->komsu = $a; unset($a, $b); $unsetSonrasi = Dugum::$yikilan; gc_collect_cycles(); $toplayiciSonrasi = Dugum::$yikilan; PHP; file_put_contents('hemen.php', $govde . "\necho \$unsetSonrasi === 0 ? \"evet\\n\" : \"hayır\\n\";\n"); file_put_contents('toplayici.php', $govde . "\necho \$toplayiciSonrasi === 2 ? \"evet\\n\" : \"hayır\\n\";\n"); yanRaporu('unset() sonrası yıkıcı hemen koşmaz', 'hemen.php'); yanRaporu('gc_collect_cycles() sonrası yıkıcı koşar', 'toplayici.php');
unset() sonrası yıkıcı hemen koşmaz → iki yanda da
geliştirme evet · üretim evet
gc_collect_cycles() sonrası yıkıcı koşar → iki yanda da
geliştirme evet · üretim evet
İki yoklama da iki profilde evet verdi. unset() sonrasında yıkıcı koşmadı, çünkü iki nesne birbirini tuttuğu için referans sayısı sıfıra inmedi. gc_collect_cycles() çağrısı döngüyü bulup ikisini de yıktı ve yıkıcı iki kez koştu. Bunun pratik sonucu şudur: yıkıcıda bağlantıyı kapatan bir nesne döngüsel bir yapının parçasıysa, bağlantı unset() ile değil, ya açıkça yazdığınız bir kapat() çağrısıyla ya da çöp toplayıcı çalıştığında kapanır. Bağlantıyı yıkıcıya emanet etmek, nesne grafiğinizde döngü olmadığını varsaymaktır. (BA3. Çöp toplayıcıya ilişkin hiçbir yönerge değiştirilmedi; yoklamalar varsayılan davranışı ölçer.)
Bellek tarafında da aynı ders çıkar. Sınırı aşan istek, kapanış fonksiyonunu çağırarak durur; sınır her isteğin kendi payıdır ve istek bitince o pay serbest kalır. Uzun ömürlü bir süreçte ise, örneğin bir kuyruk işçi sürecinde, süreç biten isteklerle sıfırlanmadığı için döngüsel yapılar birikebilir ve memory_limit bu birikimi yakalayan son sigortadır.
Sınırlayıcı ölçümü bu kursun ortak yöntemiyle okuyalım: bir iddia kuruyoruz, iddiayı çürütebilecek bir ölçüm yapıyoruz. İddia “yıkıcı, nesneyi bıraktığınız anda koşar” idi. Çürütücü ölçüm, iki nesneyi birbirine bağlayıp unset() çağırmaktır. Yıkıcı koşmadı; yani iddia, bu koşulda doğru değildir. Düzeltilmiş iddia daha dardır: yıkıcı, nesneye giden son referans düştüğünde koşar. Döngüsel yapıda son referans düşmez, çünkü nesneler birbirini ayakta tutar. Bu yüzden bağlantıyı kapatma sorumluluğunu yıkıcıya yüklemek yerine, bağlantıyı açan kodun açıkça kapatması daha sağlam bir düzendir; yıkıcı yedek güvenlik ağı olarak kalır.
Ölçümün sınırı da açıktır: yoklamalar nesne sayar, bağlantı kurmaz. Gerçek bir veritabanı sunucusunda açık bağlantıların karşı taraftaki sayısını görmek ayrı bir araç ve ayrı bir ortam ister; bu dersin ölçümü ise yıkıcının koşup koşmadığını kanıtlar, koşmanın karşı tarafta ne yaptığını değil. İkisi birbirine bağlıdır ama aynı ölçüm değildir.
Bu bölümde gc_collect_cycles() çağrısını elle yaptık, çünkü ölçümün tekrar edilebilir olması gerekir. Gerçek bir uygulamada bu çağrıyı genellikle yapmazsınız; çöp toplayıcı kendi eşiğine ulaşınca çalışır. Uzun ömürlü işçi süreçlerde, büyük bir işin sonunda elle çağırmak bilinçli bir seçim olabilir, ama bunu bir kural olarak değil, ölçümle doğrulanmış bir istisna olarak uygulayın.
Bu iki kavramı birleştirelim. Bir kuyruk işçi süreci düşünün: uzun ömürlü bir süreç, her işte bir bağlantı nesnesi kuruyor ve işi bitince onu bırakıyor. Bağlantı nesnesi başka bir nesneyle birbirini tutuyorsa, örneğin bir “oturum” nesnesi bağlantıyı, bağlantı da oturumu tutuyorsa, unset() hiçbir bağlantıyı kapatmaz. Çöp toplayıcı çalışana kadar bağlantılar karşı tarafta açık kalır. Toplayıcı belirli bir birikim eşiğinde kendiliğinden çalışır, ama ne zaman çalışacağını kodunuz bilemez. Karşı tarafın bağlantı sınırı küçükse, bu belirsizlik bir sabah bağlantı hatası olarak görünür. Bu derste bağlantı sunucusu kurmadık; ama döngüsel referans yoklaması, aynı davranışı bir nesne sayacıyla ölçüyor.
Dersin üç ölçümünün sınadığı dört davranışı yan yana koyunca ortak bir yapı görülür. Sınır aşımı, yıkıcı zamanı, kapanış sırası ve döngüsel referans, hepsi dilin çalışma zamanı kurallarıdır ve php.ini ayarlarının profil farkına bağlı değildir. Bu, güven verici bir bulgudur, çünkü bellek ve bağlantı davranışını geliştirmede öğrenip üretimde aynen beklemenizi sağlar. Dikkat edilecek tek şey ayarın kendisidir: memory_limit değerini ayar dosyası belirler ve sınır aşımının hangi noktada gerçekleştiği o değere bağlıdır. İki hazır profilde değer 128M ile aynıdır, ama bir dağıtımın kendi ayar dosyası onu değiştirebilir. Bu derste sınırı betik içinde sekiz megabayta çektik ve iki profilin aynı noktaya gelmesini sağladık. Gerçek bir dağıtımda komut satırı süreci ile web süreci ayrı ayar dosyaları okuyabilir; o durumda kuyruk işçi sürecini web ayarıyla sınamak, işçi süreç için yanlış bir güven verir. Sınırı bilerek, ölçtüğünüz sürecin kendi değeriyle ölçün ve değeri dağıtım belgenize yazın; sınır aşımı bir gün geldiğinde ilk bakılacak yer orasıdır.
Özet
memory_limitaşılınca istek durur; kapanış fonksiyonuerror_get_last()ile son hatanınE_ERRORolduğunu görür.- Bu dersin ölçü sınıfı iki yanda dadır: sınır aşımı, yıkıcı davranışı ve döngüsel referans ölçümlerinin hepsi geliştirme ve üretim profillerinde aynı sonucu verdi.
- Bağlantı nesnesinin yıkıcısı betik bittikten sonra koşar; kalıcı bağlantı ve bağlantı havuzu bu ömrü isteklerin ötesine uzatan kavramlardır.
- Döngüsel referansta
unset()yıkıcıyı hemen koşturmaz;gc_collect_cycles()koşturur. - Bağlantıyı kapatma işini yıkıcıya bırakmak, nesne grafiğinde döngü olmadığına güvenmektir.
- Betik sonuna kadar yaşayan nesnenin yıkıcısı kapanış fonksiyonundan sonra,
unset()ile bırakılanın yıkıcısı ise kapanıştan önce koşar; iki sıra da iki profilde aynıdır (ölçü sınıfı: iki yanda da).
Sonraki Adım
Bellek ve bağlantı bir isteğin içinde yönetildi. Bu derste ölçülen her davranış çalışma zamanının kuralıydı; sıradaki derste ölçülen davranış ise ayarlara ve sürecin nasıl başlatıldığına bağlıdır. İsteklerin kendisini kimin karşıladığı ayrı bir sorudur: web sunucusu ve süreç yöneticisi, istekleri işçi süreçlere dağıtır. Sonraki ders yerleşik web sunucusunu iki işçiyle başlatır ve istekler arasında neyin taşınıp neyin taşınmadığını ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.