PHP 7.4, 28 Kasım'da çıktı; bu satırları yazdığım gün itibarıyla üç haftalık taze sürüm. Değişiklik listesinde herkes typed property'lere ve arrow function'lara bakarken benim gözüm tek bir satıra takılıydı: opcache.preload. Vaat şu: uygulamanızın sınıflarını PHP-FPM başlarken bir kez yükleyin, her istekte yeniden bağlama maliyetinden kurtulun. RFC tartışmalarında yüzde 30-50'lik performans iddiaları uçuşuyordu. Filo platformumuzun API katmanı PHP'ydi ve her milisaniye, harita ekranındaki tazelik demekti. Hafta sonunu ayırıp gerçek iş yükümüzle ölçtük. Rakamlar aşağıda; önce beklentileri biraz törpüleyeyim.
OPcache Zaten Vardı; Preload Neyi Değiştiriyor?
Burada kavram kargaşası çok, netleştirmek şart. OPcache yıllardır PHP dosyalarının derlenmiş halini (opcode) paylaşımlı bellekte tutuyor; 7.4'ten önce de kimse her istekte dosyaları yeniden derlemiyordu. Preload'un eklediği şey derleme tasarrufu değil, bağlama (linking) tasarrufu. Normalde her istekte sınıf ilk kullanıldığında autoloader devreye girer, opcode önbellekten gelse bile sınıfın iç temsili kurulur: üst sınıfla birleştirilir, arayüzleri doğrulanır, trait'leri kopyalanır, sabitleri çözülür. Preload bu işi FPM master sürecinde bir kez yapıyor ve hazır bağlanmış sınıfları tüm worker'lara kalıcı olarak sunuyor. Autoloader o sınıflar için hiç çalışmıyor çünkü sınıf zaten "var".
Yani kazancınız, istek başına kaç sınıf dokunduğunuza ve o sınıfların kalıtım/trait karmaşıklığına bağlı. Üç sınıflık bir mikro serviste preload size hiçbir şey kazandırmaz. 400 sınıf yükleyen şişman bir istek yolunda ise fark edilir.
Kurulumun Dikenli Kısımları
php.ini tarafı iki satır: opcache.preload betiğin yolu, opcache.preload_user da FPM root olarak başladığı için zorunlu (root olarak preload çalıştırmayı reddediyor, www-data verdik). Asıl mesele preload betiğinin kendisi. İki strateji var: opcache_compile_file() ile dosyaları tek tek derletmek ya da require_once ile gerçekten yükletmek. İlkinde bağımlılık sırası umurunuzda olmaz ama sınıf "tam bağlanmış" sayılmayabilir; ikincisinde tam bağlama garantisi var ama üst sınıfı preload edilmemiş bir sınıf yüklemeye kalkarsanız betik patlar. Biz require_once yolunu seçtik ve composer'ın classmap'ini okuyup sınıfları topolojik sıraya dizen 80 satırlık bir betik yazdık. İlk üç denemede üç farklı "Class not found" yedik; suçlu her seferinde koşullu tanımlanan (if class_exists bloğu içindeki) eski usul sınıflardı. Onları preload listesinden kara listeye aldık.
Bir tuzak daha: preload edilen dosya değişince ne olur? Hiçbir şey. Worker'lar eski sürümle yaşamaya devam eder; değişikliği almanın tek yolu FPM'i yeniden başlatmak. opcache_reset() bile preload edilmiş kısma dokunmuyor. Deployment betiğimize zorunlu FPM reload eklendi ve bu, sıfır kesintili dağıtım kurgumuzu gözden geçirmemize sebep oldu. Preload, "kodu değiştir, F5'e bas" alışkanlığının resmî cenaze törenidir; geliştirme ortamında bu yüzden tamamen kapalı tutuyoruz.
Ölçüm Düzeni: Neyi, Nasıl Saydık
Benchmark hijyeni için üç şeyi sabitledik, çünkü ilk koşularda ikisi bizi yanılttı. CPU frekans yöneticisini performance moduna aldık; ondemand modunda ilk dakikanın sonuçları sonrakilerden yüzde 8 farklı çıkıyordu. opcache.validate_timestamps'ı iki senaryoda da kapattık; açık unuttuğumuz ilk koşuda OPcache her isteği stat çağrılarıyla süsleyip preload'suz tarafı haksız yere yavaş göstermişti. Ve testleri aynı makinede art arda değil, senaryoları dönüşümlü sırayla koştuk ki dosya önbelleği ve ısınma etkileri iki tarafa eşit dağılsın.
Test makinesi üretim eşleniği: 8 çekirdek, 32 GB RAM, PHP 7.4.0, nginx + FPM (static, 48 worker), OPcache her iki senaryoda da açık ve sıcak. Yani kıyas "OPcache'e karşı preload" değil, "sıcak OPcache'e karşı sıcak OPcache + preload" — dürüst kıyas bu. Yük olarak üretimden kaydedilmiş gerçek istek karışımını tekrar oynattık: yüzde 70 araç listesi/son konum uçları (hafif), yüzde 25 geçmiş sorguları (orta), yüzde 5 rapor tetikleme (ağır). wrk ile 10 dakikalık koşular, her senaryo üç tekrar, medyanları rapor ediyorum.
Preload kapsamı: framework çekirdeği + bizim domain sınıfları, toplam 1.410 dosya. FPM master'ın başlangıcı 1,9 saniye uzadı, paylaşılan bellek kullanımı 96 MB arttı. İki değer de umurumuzda olmayacak kadar küçük.
Rakamlar: Yüzde Elli Yok, Yüzde On Üç Var
Hafif uçlarda ortalama gecikme 11,2 ms'den 9,7 ms'ye indi (yüzde 13,4). p99 ise 34 ms'den 27 ms'ye — kuyruktaki iyileşme ortalamadan büyük, çünkü autoloader'ın dosya sistemi stat çağrıları en çok kuyruğu titretiyormuş. Verim 4.480 istek/sn'den 5.070 istek/sn'ye çıktı. Orta ağırlıklı uçlarda kazanç yüzde 7'ye düştü; ağır rapor ucunda yüzde 2'nin altında, ölçüm gürültüsünden ayırt edilemez. Mantıklı: rapor ucunun süresi veritabanında geçiyor, sınıf bağlamanın payı devede kulak.
Merak edip preload kapsamını da değişken yaptık: yalnızca framework çekirdeğini yükleyen dar liste, kazancın yaklaşık üçte ikisini tek başına getirdi. Domain sınıflarımızın eklenmesi kalan üçte biri ekledi ama kara liste bakımı gibi bir işletme yükü de getirdi. Kapsamı büyüttükçe getiri azalıyor; "her şeyi preload et" refleksi yerine, profiler'da autoload süresi görünen dosyalardan başlamak daha akıllıca.
Yüzde 30-50 iddiaları nereden çıkıyor peki? O ölçümler ya OPcache'siz kıyaslar ya da "hello world" seviyesinde, tüm süresi bootstrap olan isteklerdir. Gerçek bir uygulamada isteğin süresi I/O ile dolduğu ölçüde preload'un payı erir. Bizim yüzde 13'ümüz, günde 40 milyon API çağrısı işleyen bir sistem için yine de bedavaya yakın maliyetli, gayet somut bir kazanç — sadece pazarlandığı gibi bir devrim değil.
Küçük ama hoş bir yan bulgu: preload sonrası FPM worker'larının RSS bellek kullanımı worker başına ortalama 9 MB düştü. Bağlanmış sınıf tanımları artık her worker'da ayrı ayrı değil, paylaşılan bellekte tek kopya duruyor. 48 worker'da bu 400 MB'ın üzerinde tasarruf; o bellek doğruca dosya sistemi önbelleğine gitti.
Ufukta bir de PHP 8'in JIT'i görünüyor; RFC kabul edildi, 2020 sonunda gelecek. Şimdiden söyleyeyim: JIT de tipik web iş yükünde preload'la aynı kaderi yaşayacak bence, çünkü ikisi de CPU'da geçen zamanı kısaltıyor ve tipik web isteğinin zamanı CPU'da geçmiyor. Matematik ağırlıklı işlerde (bizim Kalman filtresi katmanı gibi) başka hikâye olabilir; çıktığında aynı düzenekle ölçeceğim.
7.4'ün diğer yeniliklerinden ikisini de aynı hafta sonu denedik madem, not düşeyim. Typed property'ler performans değil disiplin meselesi; ama FFI beni telematikçi olarak heyecanlandırıyor — CAN çerçevesi ayrıştıran C kütüphanemizi PHP'den uzantı yazmadan çağırabilmek, prototipleme süresini ciddi kısaltabilir. Onun ölçümü başka yazıya.
Özetlemeden bağlayayım: preload'u üretime aldık, açılışta bir kez ödenen 2 saniye ve 96 MB karşılığında API katmanında kalıcı yüzde 10-13 kazandık. Sizin iş yükünüzde bu oran bambaşka çıkabilir — ölçmeden inanmayın, ölçerken OPcache'i iki tarafta da sıcak tutmayı unutmayın. Kendi uygulamanız için preload betiği kurgusunu ya da ölçüm düzenini konuşmak isterseniz bana ulaşın; hafta sonu benchmark'ı benim için angarya değil, hobi.