İçeriğe geç
academia.sh

Ders 08 / 10

Süreç Yöneticisi ve Web Sunucusu

İstekleri kimin karşıladığını ve işçi süreçlerin istekler arasında neyi paylaşmadığını yerleşik web sunucusuyla ölçmek: durumu süreç dışında tutmak neden gerekir.

Yayın Güncelleme

Bu derste

  • Web sunucusu, süreç yöneticisi ve işçi süreç kavramlarını ayırt edebilmek
  • Yerleşik web sunucusunu iki işçiyle başlatıp bir sayfanın iki profilde aynı durum kodunu verdiğini ölçmek
  • İşçi sürecin belleğinde tutulan durumun istekler arasında taşınmadığını, dosyaya yazılan durumun taşındığını yorumlamak
İçindekiler

Bir tarayıcı PHP uygulamasına istek gönderdiğinde araya iki katman girer. Web sunucusu (web server) bağlantıyı kabul eder ve isteği okur. Süreç yöneticisi (process manager) PHP kodunu çalıştıran işçi süreçleri (worker process) başlatır, izler ve istekleri aralarında dağıtır. Yönetici işçi sayısını artırıp azaltabilir, çöken bir işçinin yerine yenisini koyabilir. Ayrıntıların hepsi üretim kurulumuna bağlıdır; bu derste kurulum yerine bu işin küçük bir modeli olan yerleşik web sunucusu (built-in web server) ölçülür.

Yerleşik sunucu yalnız php -S ile koşar ve üretim için tasarlanmamıştır; birden çok işçi süreç desteği PHP 7.4.0 ile gelen deneysel bir özelliktir ve Windows’ta desteklenmez. Üretimde web sunucusu ve süreç yöneticisi ayrı bir yapılandırmayla kurulur. Yerleşik sunucu bu yapıyı ölçülebilir bir kapsama indirir: PHP_CLI_SERVER_WORKERS ortam değişkeni işçi süreç sayısını belirler ve her işçi ayrı bir süreçtir. Ölçü ekseni bu kursun ortak sorusudur: bu davranış üretime çıkıyor mu? yan.php birebir aşağıdadır, 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;
}

Önce sunucuyu iki profilde başlatan betiği yazalım. Sunucu her zaman -d display_errors=0 ile ve yalnız 127.0.0.1 adresinde açılır. Profil yönergeleri yan.php içindeki PROFILLER tablosundan alınmıştır; tek bilinçli ayrım display_errors yönergesidir: bu derste yanıt gövdesi değil günlük ölçüldüğü için iki profilde de 0 verilir, tablodaki geliştirme değeri 1 burada kullanılmaz. İki profil de aynı işçi sayısıyla başlar. Betik iki sayfa yazar: sayac.php her istekte bellekteki bir sayacı artırıp yazdırır, uyari.php tanımsız bir değişken okuyarak uyarı üretir. Sunucu hazır olana kadar curl ile beklenir, sonunda kill ile durdurulur. Betik sonuçları sunucu-sonuc.txt dosyasına da yazar.

# sunucu.sh — yerleşik sunucuyu iki işçiyle iki profilde başlatır
cat > sayac.php <<'EOF'
<?php
$GLOBALS['sayac'] = ($GLOBALS['sayac'] ?? 0) + 1;
echo $GLOBALS['sayac'];
EOF
cat > uyari.php <<'EOF'
<?php
echo $tanimsiz;
echo 'tamam';
EOF

GELISTIRME="-d display_startup_errors=1 -d error_reporting=32767 -d mysqlnd.collect_memory_statistics=1 -d zend.assertions=1 -d zend.exception_ignore_args=0 -d zend.exception_string_param_max_len=15"
URETIM="-d display_startup_errors=0 -d error_reporting=24575 -d mysqlnd.collect_memory_statistics=0 -d zend.assertions=-1 -d zend.exception_ignore_args=1 -d zend.exception_string_param_max_len=0 -d opcache.enable_cli=1 -d opcache.validate_timestamps=0"
: > sunucu-sonuc.txt

for profil in gelistirme uretim; do
  if [ "$profil" = gelistirme ]; then yonergeler=$GELISTIRME; else yonergeler=$URETIM; fi
  rm -f gunluk-$profil.txt
  PHP_CLI_SERVER_WORKERS=2 php -n -d display_errors=0 -d log_errors=1 -d error_log=gunluk-$profil.txt \
    $yonergeler -S 127.0.0.1:8471 >/dev/null 2>&1 &
  pid=$!
  for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8471/sayac.php && break; sleep 0.1; done
  ilk=$(curl -s http://127.0.0.1:8471/sayac.php)
  ikinci=$(curl -s http://127.0.0.1:8471/sayac.php)
  ucuncu=$(curl -s http://127.0.0.1:8471/sayac.php)
  durum=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8471/uyari.php)
  kill $pid; wait $pid 2>/dev/null
  uyarilar=$(grep -c Warning gunluk-$profil.txt)
  echo "$profil $ilk $ikinci $ucuncu $durum $uyarilar" >> sunucu-sonuc.txt
  echo "$profil: sayaç $ilk $ikinci $ucuncu · uyarılı sayfa $durum · günlükte uyarı $uyarilar"
done
gelistirme: sayaç 1 1 1 · uyarılı sayfa 200 · günlükte uyarı 1
uretim: sayaç 1 1 1 · uyarılı sayfa 200 · günlükte uyarı 1

Şimdi iki profilin sonucunu okuyup yanını çıkaralım. Üç ölçüm sorusu var: sayaç her istekte 1 mi, uyarılı sayfa 200 mü, uyarı günlüğe düştü mü? Betik her birinin iki profilde aynı olup olmadığına bakar ve yan.php içindeki yan() fonksiyonuyla adlandırır.

<?php
// sunucu-yan.php — sunucu ölçümlerinin yanını çıkarır
declare(strict_types=1);
require __DIR__ . '/yan.php';

$satirlar = array_map(
    fn(string $satir): array => explode(' ', trim($satir)),
    file('sunucu-sonuc.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES),
);
[$gelistirme, $uretim] = $satirlar;

$olcumler = [
    'sayaç her istekte 1' => fn(array $s): bool => array_slice($s, 1, 3) === ['1', '1', '1'],
    'uyarılı sayfa 200 verir' => fn(array $s): bool => $s[4] === '200',
    'uyarı günlüğe düşer' => fn(array $s): bool => (int) $s[5] === 1,
];

foreach ($olcumler as $ad => $olcum) {
    echo $ad, ' → ', yan($olcum($gelistirme), $olcum($uretim)), "\n";
}
sayaç her istekte 1 → iki yanda da
uyarılı sayfa 200 verir → iki yanda da
uyarı günlüğe düşer → iki yanda da

Sayaç iki profilde de her istekte 1 çıktı; iki işçi süreç ayrı bellek taşıdığı için bellekteki sayaç istekler arasında birikmedi. Uyarılı sayfa iki profilde 200 döndü ve uyarı ekrana değil günlüğe yazıldı. Bu yüzden üretimde bir sayfa uyarı verdiği hâlde yanıt sağlıklı görünür: durum kodu uyarıyı haber vermez, onu görmenin yolu günlüktür. (BA4. Her profil sunucuyu kendi ayarlarıyla baştan başlattı; çalışan bir sunucunun ayarını değiştirmek ölçülmedi.)

İstek Başına Durum

Bir önceki ölçüm bellekteki sayacın istekler arasında birikmediğini gösterdi. Bu bölüm aynı soruyu dört farklı durum türü için sorar. PHP’de durumun yaşayabileceği yerler bellidir: bir sınıfın statik özelliği, global değişken, fonksiyon içindeki static değişken ve bir dosyanın include_once ile yüklenmesinin yan sonucu. Dört yerin her biri bir sayaç tutar ve sayfa dördünü birden yazdırır. Beklentimiz hepsinin 1 çıkmasıdır, çünkü PHP her istekte yürütme ortamını baştan kurar. Üç istek gönderilir ve yalnız üçüncünün yanıtı yazdırılır: birikme olsaydı sayılar orada 3 görünürdü.

# durum.sh — istek başına hangi durum taşınır?
cat > yuklu.php <<'EOF'
<?php
$GLOBALS['yuklu'] = ($GLOBALS['yuklu'] ?? 0) + 1;
EOF
cat > durum.php <<'EOF'
<?php
final class Tutucu {
    public static int $n = 0;
}
function say(): int {
    static $c = 0;
    return ++$c;
}
Tutucu::$n++;
$GLOBALS['g'] = ($GLOBALS['g'] ?? 0) + 1;
include_once __DIR__ . '/yuklu.php';
echo 'statik özellik ', Tutucu::$n, ' · global ', $GLOBALS['g'],
    ' · fonksiyon içi static ', say(), ' · include_once ', $GLOBALS['yuklu'];
EOF

GELISTIRME="-d display_startup_errors=1 -d error_reporting=32767 -d mysqlnd.collect_memory_statistics=1 -d zend.assertions=1 -d zend.exception_ignore_args=0 -d zend.exception_string_param_max_len=15"
URETIM="-d display_startup_errors=0 -d error_reporting=24575 -d mysqlnd.collect_memory_statistics=0 -d zend.assertions=-1 -d zend.exception_ignore_args=1 -d zend.exception_string_param_max_len=0 -d opcache.enable_cli=1 -d opcache.validate_timestamps=0"

for profil in gelistirme uretim; do
  if [ "$profil" = gelistirme ]; then yonergeler=$GELISTIRME; else yonergeler=$URETIM; fi
  PHP_CLI_SERVER_WORKERS=2 php -n -d display_errors=0 $yonergeler -S 127.0.0.1:8471 >/dev/null 2>&1 &
  pid=$!
  for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8471/durum.php && break; sleep 0.1; done
  curl -s -o /dev/null http://127.0.0.1:8471/durum.php
  curl -s -o /dev/null http://127.0.0.1:8471/durum.php
  son=$(curl -s http://127.0.0.1:8471/durum.php)
  kill $pid; wait $pid 2>/dev/null
  echo "$profil: $son"
done
gelistirme: statik özellik 1 · global 1 · fonksiyon içi static 1 · include_once 1
uretim: statik özellik 1 · global 1 · fonksiyon içi static 1 · include_once 1

Betiğin kurduğu düzeni okuyalım. durum.php dört ayrı yerde bir sayaç artırır. Birincisi Tutucu sınıfının statik özelliğidir; statik özellik sınıfa aittir, nesneye değil, ve bu yüzden “süreç boyunca yaşar” diye düşünülebilir. İkincisi global dizideki g anahtarıdır. Üçüncüsü say() fonksiyonunun içindeki static $c değişkenidir; bu değişken fonksiyon çağrıları arasında değerini korur. Dördüncüsü yuklu.php dosyasıdır: include_once dosyayı bir kez yükler ve dosyanın içindeki kod bir global sayaç artırır. include_once adındaki “bir kez”, bir istek içinde bir kez demektir; sorumuz yeni bir istekte dosyanın yine yüklenip yüklenmediğidir.

Çıktının iki satırı da dört sayının hepsini 1 gösterir. Bu, PHP’nin istek modelinin ölçülmüş hâlidir: her istek, işçi süreç aynı kalsa bile, kullanıcı koduna göre sıfırdan kurulur. Statik özellik sıfırlanır, çünkü sınıf tanımı da değerleriyle birlikte her istekte yeniden oluşturulur. Global dizi istek bitince boşalır. Fonksiyon içi static değişken, fonksiyon her istekte yeniden tanımlandığı için sıfırdan başlar. include_once dosyayı her istekte yeniden yükler ve içindeki sayacı sıfırdan artırır. Dördü de aynı kuralın sonucudur ve iki profilde aynı olması, kuralın ayarlara bağlı olmadığını gösterir; bu yüzden bölümün sınıfı iki yanda dadır.

Bu sonucun gerçek üretimde iki yönü vardır. Olumlu yönü, bir isteğin bıraktığı kirin sonrakini bozmamasıdır: sızıntı yapan bir istek, bir sonraki isteğin değişkenlerini etkilemez. Bu özelliği ucuza veren model, bedelini başka yerden alır: her istek uygulamayı baştan kurar. Web uygulama çerçevesinin yapılandırmayı okuması, sınıfların yüklenmesi, bağlantıların açılması her istekte yinelenir. Kod önbelleği bu bedelin yalnız bir parçasını, dosyaları çevirme işini kaldırır. Kalan parça, yani nesnelerin kurulması, istek başına durumun doğal sonucudur. Uzun ömürlü çalışma modelleri bu bedeli azaltmak için durumu istekler arasında tutar ve bunun karşılığında sızıntı riskini kendileri yönetmek zorunda kalır. Bu derste o modeller kurulmadı; ölçülen yalnız işçi süreç başına bir isteğin klasik modelidir.

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

Bellekteki durum istekler arasında taşınmıyorsa durumu dosyaya yazmak çözüm olur mu? Aşağıdaki betik sayacı bir dosyada tutan sayfayı iki profilde başlatır ve üç isteğin döndürdüğü değerleri yazdırır.

# sinir.sh — durum dosyaya yazılırsa istekler arasında taşınır mı?
cat > dosyali.php <<'EOF'
<?php
$yol = __DIR__ . '/sayac.txt';
$n = (int) @file_get_contents($yol) + 1;
file_put_contents($yol, (string) $n);
echo $n;
EOF

GELISTIRME="-d display_startup_errors=1 -d error_reporting=32767 -d mysqlnd.collect_memory_statistics=1 -d zend.assertions=1 -d zend.exception_ignore_args=0 -d zend.exception_string_param_max_len=15"
URETIM="-d display_startup_errors=0 -d error_reporting=24575 -d mysqlnd.collect_memory_statistics=0 -d zend.assertions=-1 -d zend.exception_ignore_args=1 -d zend.exception_string_param_max_len=0 -d opcache.enable_cli=1 -d opcache.validate_timestamps=0"

for profil in gelistirme uretim; do
  if [ "$profil" = gelistirme ]; then yonergeler=$GELISTIRME; else yonergeler=$URETIM; fi
  rm -f sayac.txt
  PHP_CLI_SERVER_WORKERS=2 php -n -d display_errors=0 $yonergeler -S 127.0.0.1:8472 >/dev/null 2>&1 &
  pid=$!
  for i in $(seq 1 50); do curl -s -o /dev/null http://127.0.0.1:8472/dosyali.php && break; sleep 0.1; done
  rm -f sayac.txt
  ilk=$(curl -s http://127.0.0.1:8472/dosyali.php)
  ikinci=$(curl -s http://127.0.0.1:8472/dosyali.php)
  ucuncu=$(curl -s http://127.0.0.1:8472/dosyali.php)
  kill $pid; wait $pid 2>/dev/null
  echo "$profil: dosyadaki sayaç $ilk $ikinci $ucuncu"
done
gelistirme: dosyadaki sayaç 1 2 3
uretim: dosyadaki sayaç 1 2 3

Dosyadaki sayaç iki profilde de 1 2 3 çıktı: iki işçi aynı dosyayı gördü ve istekler arasında durum taşındı. Bellekteki sayaç ise 1 1 1 kalmıştı. Fark işçinin belleğinde değil, durumun nerede tutulduğundadır. İşçi süreçlere bağlı değişkenler, statik özellikler ve önbelleğe alınmış nesneler istekler arasında güvenilir biçimde paylaşılmaz; paylaşılması gereken durum süreç dışında, bir dosyada, veritabanında ya da bir önbellek sunucusunda tutulur. Burada dosyayı kullandık, çünkü ölçüm için en küçük dış depodur; sıralı üç istek çakışmaz. Aynı anda yazan iki işçi için dosya kilidi ya da gerçek bir depo gerekir, bu da ölçülmedi.

Bu ölçümler birlikte işçi sayısı kararına ışık tutar. Her işçi süreç, kendi istek belleğiyle çalışır; iki işçi, aynı anda iki isteği karşılayabilir ama birbirlerinin belleğini görmez. Bu yüzden işçi süreç sayısını artırmak, aynı anda karşılanabilecek istek sayısını artırır ve toplam bellek kullanımını da artırır. Bir işçi sürecin tipik bellek tutarını bilmeden sayıyı artırmak, makinenin belleğini aşıp sistemi yavaşlatmaya götürebilir. Eşzamanlılık sayıyla gelir, ama sayının üst sınırını bellek koyar. Dağıtım belgenize “iki işçi” yerine “işçi başına şu kadar bellek, toplamda şu kadar işçi” yazmak, kararı sonradan açıklanabilir kılar.

(BA6. Sunucunun profil yönergeleri yan.php içindeki PROFILLER tablosuyla aynıdır, yalnız display_errors iki profilde de 0 verilir; tablodaki geliştirme değeri 1dir. Bu derste yanıt gövdesi değil yalnız günlük ve durum kodu ölçüldü; display_errors ayrımının yanıta etkisi ölçülmedi, sonraki yapılandırma ayarı dersinde ele alınır.) Yerleşik sunucunun işçi süreçleri ölçüm için seçildi. Üretimde süreç yöneticisinin işçi sayısını nasıl belirlediği, işçilerin kaç istekten sonra yeniden başlatıldığı ve bir işçi çöktüğünde ne olduğu yapılandırma konularıdır ve bu ölçümün kapsamında değildir. Ölçüm, işçi sayısı iki iken bile iki profilin aynı durumu verdiğini ve durumun istekler arasında taşınmadığını gösterdi. Bu iki bilgi, sonraki derste yapılandırma farklarını konuşurken zemin olacak. Özetle: durum işçi süreçte değil, onun dışında yaşar; işçi süreç sayısı da bu yüzden eşzamanlılığı belleğe karşı tartan bir dağıtım kararıdır.

Özet

  • Web sunucusu bağlantıyı alır, süreç yöneticisi PHP kodunu çalıştıran işçi süreçleri başlatır, izler ve istekleri aralarında dağıtır.
  • Yerleşik web sunucusu yalnız php -S ile koşar, üretim için değildir; bu derste iki işçili küçük bir model olarak kullanıldı.
  • Bu dersin ölçü sınıfı iki yanda dadır: bellekteki sayaç iki profilde de her istekte 1 verdi, uyarılı sayfa iki profilde 200 döndü ve uyarı günlüğe düştü.
  • Durum kodu uyarıyı haber vermez; üretimde uyarıyı görmenin yolu günlüktür.
  • Bellekteki durum istekler arasında taşınmaz, dosyadaki taşınır: paylaşılacak durum süreç dışında tutulur.
  • Statik özellik, global değişken, fonksiyon içi static değişken ve include_once sayaçlarının dördü de üçüncü istekte 1 verdi; istek başına durum iki profilde aynıdır (ölçü sınıfı: iki yanda da).

Sonraki Adım

İşçi süreç ayarlarıyla başlar: bellek sınırı, hata raporlama ve kod önbelleği yönergeleri süreç başlarken verilir. Son ders iki profilin yedi yönergede nasıl ayrıştığını yazar ve çalışırken bir yönergeyi ini_set() ile değiştirmenin hangi profilde kabul edildiğini ölçer.

İ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