İçeriğe geç
academia.sh

Ders 10 / 10

Dağıtım Akışı

Kodu üretime alırken sürüm dizinleri ve sembolik bağ geçişi: atomik geçişten sonra neden eski sürüm koşar, hangi çağrı yeni sürümü getirir.

Yayın Güncelleme

Bu derste

  • Sürüm dizinleri ve sembolik bağ ile atomik geçişin kesintisiz dağıtımı nasıl sağladığını açıklayabilmek
  • Üretim profilinde geçişten sonra eski sürümün koşabildiğini ölçerek göstermek
  • realpath(), opcache_reset() ve opcache_invalidate() çağrılarının yeni sürümü getirip getirmediğini karşılaştırmak
İçindekiler

Önceki beş ders tek tek ayar ve ölçüm yaptı: kod önbelleği, süreç yöneticisi, yapılandırma yönergeleri. Bir dağıtım hepsini aynı anda sınar, çünkü yeni kod yalnız diske yazılmakla üretime çıkmaz. Kesintisiz dağıtım (zero-downtime deployment) denen düzende her sürüm kendi dizinine kurulur ve istekleri karşılayan yol, tek bir adımda yeni dizine çevrilir. Bu ders o adımı kurar ve üretim ayarlarının onu nasıl etkilediğini ölçer.

Ölçü ekseni yine şu soru: bu davranış üretime çıkıyor mu? DevOps ve Platform Mühendisliği müfredatında geliştirme ile üretim arasındaki her sapma gizli kusur sayıldı ve kapatılması hedeflendi. Burada farkın çoğu kasıtlıdır: PHP iki ayar dosyasıyla dağıtılır ve fark o dosyalardan gelir. Bu ders farkı kapatmaz, adlandırır. Aşağıdaki yan.php kursun ortak çekirdeğidir; bu derste de 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;
}

Sürüm Dizinleri ve Atomik Geçiş

Düzen şöyledir: her sürüm surum-1, surum-2 gibi ayrı bir dizindedir ve isteklerin kullandığı guncel adı bir sembolik bağdır (symbolic link); bağ, o anda yayında olan sürüm dizinini gösterir. Yeni sürüm yanda hazırlanır, çalışan sürüme hiç dokunulmaz. Yayına alma, bağın başka bir dizini göstermesinden ibarettir.

Geçişin atomik (atomic) olması gerekir: bir istek bağı okurken ne eski ne yeni dizini gösteren bir ara durum görmemelidir. symlink() var olan bir adın üzerine yazmaz; bu yüzden önce guncel-yeni adıyla yeni bir bağ kurulur, sonra rename() onu guncel adının üstüne taşır. Aynı dosya sistemi içinde rename() tek adımda biter: guncel adı her an ya eski ya yeni dizini gösterir. İki yardımcı fonksiyon bu düzeni kurar. Dosya bu derste yalnız diske yazılır.

<?php
// kur.php — iki sürüm dizini, ilkini gösteren bağ ve atomik geçiş
declare(strict_types=1);

function surumleriKur(): void {
    foreach ([1, 2] as $n) {
        @mkdir(__DIR__ . "/surum-$n");
        $yol = __DIR__ . "/surum-$n/selam.php";
        file_put_contents($yol, "<?php return $n;");
        touch($yol, time() - 60);
    }
    @unlink(__DIR__ . '/guncel');
    symlink('surum-1', __DIR__ . '/guncel');
}

function gecir(): void {
    @unlink(__DIR__ . '/guncel-yeni');
    symlink('surum-2', __DIR__ . '/guncel-yeni');
    rename(__DIR__ . '/guncel-yeni', __DIR__ . '/guncel');
}

Her sürümün selam.php dosyası yalnız sürüm numarasını döndürür. touch() dosyaların zamanını geçmişe alır; önbellek son saniyelerde değişmiş dosyayı almadığı için yoklamalar bu adımı kodun başında yapar. İlk yoklama geçişin diskte işe yaradığını, ikincisi önbellek açıkken ne olduğunu sorar. Her yoklama kendi sürecinde ve surumleriKur() ile sıfırdan kurulan dizinlerde koşar.

<?php
// dagitim.php — geçişten sonra hangi sürüm koşuyor?
declare(strict_types=1);
require __DIR__ . '/yan.php';

file_put_contents('gecis.php', <<<'PHP'
<?php
// gecis.php — bağ geçişten sonra yeni dizini gösterir mi?
require __DIR__ . '/kur.php';
surumleriKur();
gecir();
echo is_link(__DIR__ . '/guncel') && readlink(__DIR__ . '/guncel') === 'surum-2' ? "evet\n" : "hayır\n";
PHP);

file_put_contents('eski.php', <<<'PHP'
<?php
// eski.php — geçişten sonra aynı yoldan eski sürüm mü koşar?
require __DIR__ . '/kur.php';
surumleriKur();
$once = include __DIR__ . '/guncel/selam.php';
gecir();
$sonra = include __DIR__ . '/guncel/selam.php';
echo $sonra === $once ? "evet\n" : "hayır\n";
PHP);

yanRaporu('bağ geçişten sonra yeni dizini gösterir', 'gecis.php');
yanRaporu('geçişten sonra aynı yoldan eski sürüm koşar', 'eski.php');
bağ geçişten sonra yeni dizini gösterir → iki yanda da
    geliştirme evet · üretim evet
geçişten sonra aynı yoldan eski sürüm koşar → yalnız üretimde
    geliştirme hayır · üretim evet

Birinci yoklama iki profilde de evet verdi: geçiş diskte her yerde aynı biçimde tamamlanır. İkinci yoklama yalnız üretimde evet verdi (BA8. Ölçüm komut satırı sürecinde, tek süreçle yapıldı; gerçek bir dağıtımda geçiş sürüyor olan işçi süreçlerin yanında olur ve her süreç kendi isteğini yeni yoldan görür, bu derste o düzen kurulmadı). Sembolik bağ yeni dizini gösterdiği hâlde guncel/selam.php yolundan gelen ikinci yükleme, birincisiyle aynı değeri döndürdü: önbellek bu yolu zaten tanıyordu ve diske bakmadı. Geliştirmede kod önbelleği kapalı olduğundan her yükleme diski okur ve yeni sürüm hemen gelir.

Yeni Sürümü Getiren Çağrılar

Sorun, önbelleğin bir yolu anahtar olarak tutması ve bu yolun bağdan geçmesidir. Dağıtım betiğinin elinde üç aday çağrı vardır: dosya sistemi denetim önbelleğini temizleyen clearstatcache(true), tüm kod önbelleğini boşaltan opcache_reset() ve tek dosyayı geçersiz kılan opcache_invalidate(). Dördüncüsü çağrı değil, yoldur: bağı kodda realpath() ile çözmek. Dört yoklama aynı kurulumla koşar; her birinde evet, adının söylediği şeyin gerçekleştiği anlamına gelir.

<?php
// cozumler.php — hangi çağrı geçişten sonra yeni sürümü getirir?
declare(strict_types=1);
require __DIR__ . '/yan.php';

file_put_contents('stat.php', <<<'PHP'
<?php
// stat.php — clearstatcache(true) sonrası eski sürüm kalır mı?
require __DIR__ . '/kur.php';
surumleriKur();
include __DIR__ . '/guncel/selam.php';
gecir();
clearstatcache(true);
$sonra = include __DIR__ . '/guncel/selam.php';
echo $sonra === 1 ? "evet\n" : "hayır\n";
PHP);

file_put_contents('sifirla.php', <<<'PHP'
<?php
// sifirla.php — opcache_reset() true döner ama aynı istekte eski sürüm kalır mı?
require __DIR__ . '/kur.php';
surumleriKur();
include __DIR__ . '/guncel/selam.php';
gecir();
$dondu = opcache_reset();
$sonra = include __DIR__ . '/guncel/selam.php';
echo $dondu === true && $sonra === 1 ? "evet\n" : "hayır\n";
PHP);

file_put_contents('baglayol.php', <<<'PHP'
<?php
// baglayol.php — bağ yoluyla geçersiz kılmadan sonra yeni sürüm gelir mi?
require __DIR__ . '/kur.php';
surumleriKur();
$yol = __DIR__ . '/guncel/selam.php';
include $yol;
gecir();
opcache_invalidate($yol, true);
$sonra = include $yol;
echo $sonra === 2 ? "evet\n" : "hayır\n";
PHP);

file_put_contents('eskiyol.php', <<<'PHP'
<?php
// eskiyol.php — eski sürümün çözülmüş yoluyla geçersiz kılma yeni sürümü getirir mi?
require __DIR__ . '/kur.php';
surumleriKur();
$yol = __DIR__ . '/guncel/selam.php';
include $yol;
$eskiYol = realpath($yol);
gecir();
$dondu = opcache_invalidate($eskiYol, true);
$sonra = include $yol;
echo $dondu === true && $sonra === 2 ? "evet\n" : "hayır\n";
PHP);

file_put_contents('cozulmus.php', <<<'PHP'
<?php
// cozulmus.php — bağ include'dan önce realpath() ile çözülünce yeni sürüm gelir mi?
require __DIR__ . '/kur.php';
surumleriKur();
include realpath(__DIR__ . '/guncel') . '/selam.php';
gecir();
$sonra = include realpath(__DIR__ . '/guncel') . '/selam.php';
echo $sonra === 2 ? "evet\n" : "hayır\n";
PHP);

yanRaporu('clearstatcache(true) sonrası eski sürüm kalır', 'stat.php');
yanRaporu('bağ yoluyla geçersiz kılmadan sonra yeni sürüm gelir', 'baglayol.php');
yanRaporu('eski sürümün çözülmüş yoluyla geçersiz kılma yeni sürümü getirir', 'eskiyol.php');
yanRaporu('realpath() ile çözülen yoldan yeni sürüm gelir', 'cozulmus.php');
yanRaporu('opcache_reset() true döner ama aynı istekte eski sürüm kalır', 'sifirla.php');
clearstatcache(true) sonrası eski sürüm kalır → yalnız üretimde
    geliştirme hayır · üretim evet
bağ yoluyla geçersiz kılmadan sonra yeni sürüm gelir → yalnız geliştirmede
    geliştirme evet · üretim hayır
eski sürümün çözülmüş yoluyla geçersiz kılma yeni sürümü getirir → yalnız üretimde
    geliştirme hayır · üretim evet
realpath() ile çözülen yoldan yeni sürüm gelir → iki yanda da
    geliştirme evet · üretim evet
opcache_reset() true döner ama aynı istekte eski sürüm kalır → yalnız üretimde
    geliştirme hayır · üretim evet

Üç sonuç ayrılıyor. Birincisi, clearstatcache(true) işe yaramadı: üretimde ikinci yükleme yine eski sürümü verdi. Bu çağrı dosya sistemi sorgularının önbelleğini temizler, kod önbelleğine dokunmaz. İkincisi, opcache_reset() üretimde true döndü ama aynı istekte eski sürüm kaldı; çağrının başarılı görünmesi, o istekte yeni sürümün geleceği anlamına gelmiyor. Bir sonraki isteği bu ölçüm göstermedi. Üçüncüsü, geçersiz kılma çağrısının hangi yolu aldığı önemlidir: bağ yoluyla (guncel/selam.php) yapılan çağrıdan sonra üretimde eski sürüm kaldı; yeni sürümü yalnız önbelleği kapalı olan geliştirme gördü. Eski sürümün çözülmüş yoluyla (realpath() sonucu) yapılan çağrı ise üretimde yeni sürümü getirdi. Aynı biçimde, include etmeden önce realpath() ile çözülen yoldan yeni sürüm iki profilde de geldi. Bu üç sonuç, önbelleğin dosyayı bağın adıyla değil çözülmüş yolla tuttuğu varsayımıyla tutarlıdır. Geliştirmede opcache_reset() ve opcache_invalidate() önbellek kapalı olduğundan false döner, bu yüzden dağıtım betiğinin çağrıları geliştirmede sınanamaz.

Sınırlayıcı Ölçüm

realpath() çözümü, yolun ne zaman çözüldüğüne bağlıdır. Yoklama yolu geçişten önce bir kez çözer ve saklar; geçişten sonra saklanan yoldan yükler. Uzun ömürlü bir süreç yolu başlangıçta çözüp bellekte tutsaydı bu olurdu.

<?php
// sinir.php — geçişten önce çözülen yol geçişten sonra da eski sürümü verir mi?
declare(strict_types=1);
require __DIR__ . '/yan.php';

file_put_contents('saklanan.php', <<<'PHP'
<?php
// saklanan.php — çözülmüş yol geçişten önce saklanır, geçişten sonra kullanılır
require __DIR__ . '/kur.php';
surumleriKur();
$saklanan = realpath(__DIR__ . '/guncel');
$once = include $saklanan . '/selam.php';
gecir();
$sonra = include $saklanan . '/selam.php';
echo $sonra === $once ? "evet\n" : "hayır\n";
PHP);

yanRaporu('geçişten önce çözülen yol geçişten sonra da eski sürümü verir', 'saklanan.php');
geçişten önce çözülen yol geçişten sonra da eski sürümü verir → iki yanda da
    geliştirme evet · üretim evet

Yoklama iki profilde de evet verdi: saklanan yol eski dizini gösterdiği için geçişten sonra da eski sürüm geldi, kod önbelleği açık olsun olmasın. Demek ki realpath() tek başına bir çözüm değil, bir zamanlama kuralıdır: çözümleme her istekte, include’dan hemen önce yapılmalıdır. Ölçümün sınırı da burada: yoklama bir süreç ve iki sürümle sınırlıdır, saklanan yolun gerçek bir işçi süreçte ne kadar yaşadığı ölçülmedi.

Özet

  • Kesintisiz dağıtım, her sürümü ayrı dizine koyar ve symlink() ile kurulan yeni bağı rename() ile guncel adının üstüne taşır; geçiş (atomik geçiş) diskte iki profilde de tamamlanır.
  • Bu dersin ölçü sınıfı yalnız üretimdedir: opcache.validate_timestamps=0 iken geçişten sonra aynı bağ yolundan eski sürüm koşar; geliştirmede kod önbelleği kapalıdır ve yeni sürüm gelir.
  • clearstatcache(true) bunu çözmez; opcache_reset() true döner ama aynı istekte eski sürüm kalır; geliştirmede iki opcache_* çağrısı false döner.
  • Yeni sürümü getiren iki yol ölçüldü: include’dan önce realpath() ile çözülmüş yol ve eski sürümün çözülmüş yoluyla opcache_invalidate($yol, true). Bağ yoluyla geçersiz kılmadan sonra üretimde eski sürüm kaldı.
  • Sınır: realpath() sonucu geçişten önce saklanırsa eski sürüm iki yanda da gelir; çözümleme her istekte yapılmalıdır.

Kurs Kapanışı

Kurs, kodu üretime taşıyan yedi yönergenin ve dört davranış sınıfının ölçülmesiyle kapanıyor. Her dersin ölçülen davranışı, yanı ve sınırlayıcı ölçümü aşağıdadır.

Ders Ölçülen davranış Yan Sınırlayıcı ölçüm
Statik Çözümleme çağrılmayan koldaki tanımsız fonksiyon koşmaz, söz dizimi denetimi yalnız söz dizimine bakar, belirteç denetleyicisi ozetle çağrısını bulur koşuma girmiyor adı değişkenden çağrılırsa denetleyici 0 buldu, doğrudan koşum Error attı
Kod Stili Denetimi biçimsiz kod iki profilde aynı çalışır, belirteç tabanlı denetleyici satır sonu, çift boşluk ve girintiyi sayar koşuma girmiyor nowdoc içindeki satır sonu boşluğunu belirteçsiz düzeltme dizeyi kısaltır, yalnız T_WHITESPACE yeniden yazan biçimlendirici uzunluğu 13 bırakır
Test Çerçeveleri assert() tabanlı sınama geliştirmede yakalar, üretimde (zend.assertions=-1) derlenmez yalnız geliştirmede zend.assertions=0 da yakalamaz ve çalışırken 1’e çevrilebilir, -1’de yol kapalıdır
Hata Ayıklayıcı kart-0042 istisna izinde geliştirmede görünür, üretimde gizlidir yalnız geliştirmede değer iletiye yazılırsa getMessage() iki profilde de kart-0042 taşır
Kod Önbellekleme validate_timestamps=0 ile yeniden yazılan dosya eski sürümle koşar, opcache_invalidate($yol, true) yeni sürümü getirir yalnız üretimde validate_timestamps=1 ve revalidate_freq=0 eski sürüm sorununu giderir
Profil Çıkarma 500 çağrı, toplam 249500 ve eşit bellek farkı; üç fonksiyonlu zincirde oku 400, donustur 200, ozetle 5 iki yanda da assert() içindeki sayaç geliştirmede 500, üretimde 0
Bellek ve Bağlantı Yönetimi memory_limit aşılınca istek durur, yıkıcı istek sonunda koşar iki yanda da döngüsel referansta unset() yıkıcıyı koşturmaz, gc_collect_cycles() koşturur
Süreç Yöneticisi ve Web Sunucusu iki işçi süreçte bellekteki sayaç her istekte 1 kalır, uyarılı sayfa iki profilde 200 döner iki yanda da sayaç dosyada tutulursa 1 2 3, bellekte 1 1 1
Yapılandırma Ayarı yedi yönergelik fark; ini_set('zend.assertions','1') geliştirmede kabul, üretimde false yalnız üretimde display_errors üretimde ini_set() ile geri açılabildi
Dağıtım Akışı sembolik bağ geçişinden sonra aynı yoldan eski sürüm koşar, realpath() ile çözülen yoldan yeni sürüm gelir yalnız üretimde geçişten önce çözülüp saklanan yoldan iki profilde de eski sürüm gelir

Derslerin dağılımı: 3 iki yanda da · 2 yalnız geliştirmede · 3 yalnız üretimde · 2 koşuma girmiyor. Çekirdek (davranislar.php, Statik Çözümleme dersinde koşturuldu): 4 · 4 · 2 · 2. İki sayı ayrı şeyleri sayar: ilki on dersin ana ölçümünün yanını, ikincisi on iki ayrı davranışın yanını sayar; bir ders birden çok davranış ölçtüğü ve bir davranış birden çok dersi ilgilendirdiği için iki dağılımın aynı çıkması beklenmez.

M12 burada kapanır; beş kurs PHP’yi bir dilden bir üretim sistemine kadar yürüttü. PHP Temelleri kursu, bir sorunun hangi yoldan duyulduğunu ölçtü (bu sorun hangi kanaldan duyuluyor: sessiz · dönüş değeri · uyarı · durdurur, 3/4/4/3). Nesneye Dayalı PHP ve Standartlar kursu, bir değişikliğin nereye ulaştığını ölçtü (bu değişiklik nereye ulaşıyor: yerinde · bağlantıda · kıran · örtük, 3/3/3/3). Web ile PHP: Oturum, Veri ve Güvenlik kursu, sonucu kimin değiştirebildiğini ölçtü (bu sonucu kim değiştirebiliyor: sunucu · ortam · istemci · üçüncü taraf, 5/4/4/2). Web Uygulama Çerçevesi kursu, bir parçanın o istekte koşup koşmadığını ölçtü (bu parça bu istekte koştu mu: koştu · ertelendi · atlandı · bağlanmadı, 6/5/5/5). PHP’de Kalite, Başarım ve Dağıtım kursu, bir davranışın üretime çıkıp çıkmadığını ölçtü (bu davranış üretime çıkıyor mu: iki yanda da · yalnız geliştirmede · yalnız üretimde · koşuma girmiyor, 3/2/3/2). Sıradaki müfredat Ruby’dir.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat