İçeriğe geç
Muhammet Şafak
en

PHP ile Go'yu aynı OAuth2 API'de ölçtüm: 10.000 yazmada fark yok, 50.000 okumada var

Her istekte OAuth2 token'ı doğrulayıp PostgreSQL'e yazan ya da okuyan aynı API, dört çekirdekte PHP-FPM, FrankenPHP worker ve Go ile saniyede 10.000 yazmayı ve 50.000 okumayı kaç CPU'yla karşılıyor?

Bulgu

Saniyede 10.000 yazmada üç aday da hedefi beş tekrarın beşinde tutturdu, p99 üçünde de 2,5 ms'yi aşmadı: bu yükte dil bir kapasite kalemi değil. Saniyede 50.000 okumayı dört çekirdekte yalnız Go tutturdu (p99 6,45 ms); PHP-FPM 23.528'de, FrankenPHP 22.859'da kaldı. İstek başına CPU okumada Go'da 64, FrankenPHP'de 117, PHP-FPM'de 168 mikrosaniye. 50.000 okuma için instance hesabı Go'ya 3,4, iki PHP adayına 8,2–8,8 çekirdek yazdırıyor. FrankenPHP'nin CPU tasarrufu kapasiteye dönmüyor: doygunken dört çekirdeğin birini boşta bırakıyor.

10.000 yazma/sn · üç aday da tuttu
p99 ≤ 2,50 ms
50.000 okuma/sn · yalnız Go tuttu
p99 6,45 ms
Okuma başına CPU · Go → PHP-FPM
64 → 168 µs
Doğrulama tavanı · Go / en iyi PHP
83.526 / 32.953

Yöntem

Aynı sözleşme üç kez yazıldı ve her istekte RS256 bearer token doğrulandı (imza, iss, aud, exp, nbf, scope). Adaylar Go 1.27.1 (net/http, pgx, golang-jwt), nginx + php-fpm üzerinde PHP 8.5.10 ve FrankenPHP 1.12.7 worker modunda PHP 8.5.10; framework yok ve iki PHP adayı aynı sınıfı çalıştırıyor. Her aday dört sabitlenmiş çekirdek, 1 GiB bellek ve en fazla 32 veritabanı bağlantısı aldı; php-fpm'in nginx'i de bu bütçenin içinde. PostgreSQL 17.11 ayrı dört çekirdekte, oha 1.15.0 ayrı dört çekirdekte koştu. Her blok, önbelleğe çekilmiş 1.000.000 satırlık tablonun birebir kopyasıyla ve taze bir aday konteyneriyle başladı. Sabit hız fazı open loop modunda koştu (oha -q, latency correction, 256 bağlantı, 60 sn, 5 tekrar) ve medyan raporlandı. Tavan fazı closed loop modunda 16/64/128/256 bağlantıyla koştu (15 sn, 3 tekrar) ve en iyi tekrar raporlandı. CPU ve bellek her koşuda cgroup sayaçlarından okundu. Her bloktan önce uygulama kodu çalıştırmayan bir nginx, adayın çekirdeklerinde 50.000/sn ile sınandı; 24 bloğun hepsinde en az 49.980/sn verdi. Toplam 153 yük ölçümü, 123 milyon yanıt, sıfır non-2xx.

Orta güven Tekrarlı ölçüm, sınırlı ortam denetimi. Aynı düzendeki farklar anlamlıdır.
Ölçüm tarihi

bugün ölçüldü

Yayın

Ortam

Go
1.27.1 · net/http · pgx 5.11.0 · golang-jwt 5.3.1
PHP
8.5.10 · opcache açık · JIT kapalı · firebase/php-jwt 7.1.1
PHP-FPM
nginx 1.26.3 + php-fpm aynı konteynerde · pm=static · 32 worker
FrankenPHP
1.12.7 (Caddy 2.11.4) · worker modu · 32 worker · ZTS
Veritabanı
PostgreSQL 17.11 · 1.000.000 satır · en fazla 32 bağlantı · synchronous_commit açık
Yük üreteci
oha 1.15.0 · open loop + latency correction · 256 bağlantı
Donanım
Apple M4 Pro · 12 çekirdek · 24 GB · macOS 27.0
Sanallaştırma
Docker Desktop 29.8.0 · 12 vCPU / 7,75 GB · aarch64
Çekirdek ayrımı
aday 0-3 · PostgreSQL 4-7 · yük 8-11
Tekrar
sabit hız 5 × 60 sn (medyan) · tavan 3 × 15 sn (en iyisi)

Teknolojiler

Go PHP FrankenPHP PHP-FPM PostgreSQL OAuth2 JWT Docker nginx

Tekrarlamak için

./bench/build.sh && ./bench/verify.sh && STAMP=$(date -u +%F) ./bench/run.sh --all

“Go’ya geçersek kaç sunucu kazanırız?” sorusuna genellikle “Go daha hızlı” cevabı veriliyor. Bu cevapla kapasite tablosu doldurulamıyor. Bu kayıtta aynı OAuth2 korumalı API’yi PHP-FPM, FrankenPHP worker ve Go ile üç kez yazdım. Üçünü aynı dört çekirdekte, aynı PostgreSQL’e karşı ölçtüm ve farkı kendi hedefinize göre çekirdeğe çevirebileceğiniz bir sayıya indirdim: istek başına CPU. Ham koşuların hepsi php-go-bench deposunda duruyor.

Ne ölçtüm

Üç uç nokta, üç adayda da birebir aynı sözleşmeyle çalışıyor. Hepsi önce bearer token’ı doğruluyor: RS256 imzası, iss, aud, exp, nbf ve uç noktaya göre scope. Doğrulama hiçbir adayda önbelleğe alınmıyor.

Senaryo İstek Doğrulamadan sonra ne oluyor Hedef hız
Doğrulama GET /auth hiçbir şey — veritabanı yok 50.000/sn
Okuma GET /events/{id} 1.000.000 satırlık tablodan birincil anahtarla tek satır 50.000/sn
Yazma POST /events JSON gövde doğrulaması, tek INSERT … RETURNING 10.000/sn
Doğrulama senaryosu kendi başına bir hedef değil: OAuth2'nin veritabanından bağımsız maliyetini ayırmak için var.

Framework yok. İki PHP adayı aynı Api.php sınıfını çalıştırıyor, yalnız giriş dosyaları farklı. Her aday, süreç ya da istek ömrünün izin verdiği en hızlı veritabanı idiom’unu kullanıyor:

  • Go: pgx, prepared statement’ları bağlantı başına önbelleğe alıyor.
  • FrankenPHP worker: statement’ı bir kez hazırlayıp tekrar kullanıyor.
  • php-fpm: bir isteğin ömrü statement tutmaya yetmediği için sorguyu tek gidiş-dönüşlük parametreli çağrıyla gönderiyor.

Hedef hızda: 10.000 yazma herkes için kolay, 50.000 okuma değil

Sabit hedef hızda tutturulan istek/sn

Yazmada dört sütun aynı boyda. İki PHP adayı doğrulamada hedefin %61–66'sında, okumada yarısı civarında kalıyor; tutturamadıkları istekler yük üretecinin kuyruğunda bekliyor.

  • Hedef
  • Go
  • FrankenPHP (worker)
  • PHP-FPM

Kaynak: 5 tekrarın medyanı, open loop, 256 bağlantı, 60 sn — bench/report.mjs

Veri tablosu
5 tekrarın medyanı, open loop, 256 bağlantı, 60 sn — bench/report.mjs
Seri Yalnız doğrulamaDoğrulama + okumaDoğrulama + yazma
Hedef 50.00050.00010.000
Go 49.99949.99910.000
FrankenPHP (worker) 33.08922.85910.000
PHP-FPM 30.60623.52810.000

Grafik tarayıcıda çizilir; aşağıdaki tablo aynı veriyi taşır.

Saniyede 10.000 yazmada üç aday da hedefi beş tekrarın beşinde tutturdu. Üç adayın p99 değerleri yan yana: FrankenPHP 1,81 ms, PHP-FPM 1,88 ms, Go 2,50 ms. Bu yükte en düşük p99 bir PHP adayından geldi; Go ile aradaki fark da bir milisaniyenin altında. Bu hedefte dil, kapasite tablonuzda bir satır bile değil.

Hedef 50.000 okumaya çıkınca tablo değişiyor. Go beş tekrarın beşinde 50.000’i p99 6,45 ms ile tutturdu. PHP-FPM 23.528’de kaldı ve dört çekirdeğin 3,9’unu kullandı. FrankenPHP 22.859’da kaldı ama 2,7 çekirdekle; bu farka aşağıda döneceğim. Open loop ölçümde tutturulamayan istek kuyrukta bekliyor. Bu yüzden iki PHP adayının doğrulama ve okumadaki gecikmesi (p99 20–32 saniye) bir servis gecikmesi değil; yalnız “bu hedef bu bütçeyle karşılanamaz” anlamına geliyor.

Farkı belirleyen: istek başına CPU

İstek başına uygulama CPU'su

Adayın cgroup CPU sayacı, cevaplanan istek sayısına bölündü. Veritabanının CPU'su dahil değil.

  • Go
  • FrankenPHP (worker)
  • PHP-FPM

µs düşük olan iyi Kaynak: Sabit hız fazı, 5 tekrarın medyanı

Veri tablosu
Sabit hız fazı, 5 tekrarın medyanı
Seri Yalnız doğrulamaDoğrulama + okumaDoğrulama + yazma
Go 47 µs64 µs96 µs
FrankenPHP (worker) 88 µs117 µs146 µs
PHP-FPM 130 µs168 µs207 µs

Grafik tarayıcıda çizilir; aşağıdaki tablo aynı veriyi taşır.

Bu sayı hedef hızla çarpıldığında gereken çekirdeği veriyor. Adayın hedefi tutturduğu her ölçümde hesap, sayacın gösterdiği çekirdekle örtüşüyor. Go’da okuma için hesap 50.000 × 64 µs = 3,2 çekirdek, sayaç 3,20. PHP-FPM’de yazma için hesap 10.000 × 207 µs = 2,07 çekirdek, sayaç da 2,07.

50.000 okuma için iki farklı hesap yapılabiliyor ve iki PHP adayı bu iki hesapta farklı yere düşüyor:

50.000 okuma/sn CPU zamanı hesabı Go'ya oranı Instance hesabı Go'ya oranı
Go 3,2 çekirdek (ölçüldü) 1,0× 3,4 çekirdek 1,0×
FrankenPHP (worker) 5,8 çekirdek 1,8× 8,8 çekirdek 2,6×
PHP-FPM 8,4 çekirdek 2,6× 8,2 çekirdek 2,4×
CPU zamanı hesabı: hedef × istek başına CPU. Instance hesabı: hedef ÷ dört çekirdekli bir instance'ın tavanı × 4. Go'nun CPU zamanı hücresi dışındaki bütün hücreler ölçüm değil, ölçümden yapılan hesap; yatay ölçeklemenin doğrusal olduğunu varsayıyor. Veritabanı çekirdekleri dahil değil.

PHP-FPM’de iki hesap aynı yere çıkıyor, çünkü PHP-FPM dört çekirdeğin tamamını kullanıyor. FrankenPHP’de ise çıkmıyor.

CPU nereye gidiyor: dörtte üçü token’a

Doğrulama senaryosu veritabanına hiç gitmiyor, bu yüzden bir okuma isteğinin CPU’sunun ne kadarının OAuth2’ye gittiğini ayırmayı sağlıyor. Üç adayda da oran aynı bantta: token doğrulaması, okuma isteğinin uygulama CPU’sunun Go’da %73’ünü, FrankenPHP’de %75’ini, PHP-FPM’de %77’sini oluşturuyor. Oran yaklaşık, çünkü doğrulama yanıtı okuma yanıtından küçük; yine de kapasite açısından sonuç net. Bu API’de satırı okumak değil, RS256 imzasını doğrulamak pahalı. Dillerin arasındaki farkın büyük kısmı da imza doğrulamanın kendisinden geliyor: Go 47 µs, PHP-FPM 130 µs.

Veritabanının tarafı ayrı bir satır. Aynı sorgu için PostgreSQL’in harcadığı CPU’yu istek başına böldüğümde üç aday iki gruba ayrıldı:

PostgreSQL CPU / istek Okuma Yazma
Go 34 µs 48 µs
FrankenPHP (worker) 35 µs 51 µs
PHP-FPM 53 µs 81 µs
Veritabanı konteynerinin cgroup CPU sayacı, cevaplanan istek sayısına bölündü; sabit hız fazı, 5 tekrarın medyanı. Veritabanı her adayda aynı dört çekirdekte ve aynı ayarlarla koştu.

Statement’ı tekrar kullanan iki aday (Go ve FrankenPHP) birbirine çok yakın. Kullanamayan PHP-FPM ise veritabanına istek başına Go’ya göre okumada %56, yazmada %69 daha fazla iş yaptırıyor. Muhtemel sebep, her sorgunun her seferinde yeniden ayrıştırılıp planlanması; bu kayıt bunu ayrıca ölçmedi. Kapasite tablosunda bunun anlamı şu: PHP-FPM’den Go’ya geçiş, uygulama çekirdeklerinin yanında veritabanı çekirdeğini de azaltıyor. FrankenPHP’ye geçiş de aynı kazancın veritabanı tarafını büyük ölçüde sağlıyor.

FrankenPHP’nin tasarrufu kapasiteye dönmüyor

Worker modu PHP’nin istek başına CPU’sunu üç senaryoda da PHP-FPM’e göre üçte bir civarında düşürüyor (okumada 168’den 117 µs’ye). Tavan ise neredeyse kıpırdamıyor:

Dört çekirdekte tavan: saniyede karşılanan istek

Go, veritabanı olmadan 83.526'ya çıkıyor, iki PHP adayı 33.000'in altında kalıyor. Yazma satırı veritabanının I/O'suna bağlı ve gürültülü; aşağıdaki nota bakın.

  • Go
  • FrankenPHP (worker)
  • PHP-FPM

Kaynak: 16/64/128/256 bağlantılık süpürmenin en iyi hücresi, 3 tekrarın en iyisi, 15 sn

Veri tablosu
16/64/128/256 bağlantılık süpürmenin en iyi hücresi, 3 tekrarın en iyisi, 15 sn
Seri Yalnız doğrulamaDoğrulama + okumaDoğrulama + yazma
Go 83.52659.00443.942
FrankenPHP (worker) 32.95322.61322.874
PHP-FPM 31.15724.43721.857

Grafik tarayıcıda çizilir; aşağıdaki tablo aynı veriyi taşır.

FrankenPHP doygunken bile dört çekirdeğin ancak 2,2 ile 2,9’unu kullanıyor; PHP-FPM aynı koşularda 3,3 ile 4,0 arasında. Yani darboğaz CPU değil, başka bir yerde: 32 worker’lık havuzda, Caddy ile PHP thread’leri arasındaki geçişte ya da ZTS derlemesinde olabilir. Bu kayıt darboğazın nerede olduğunu ölçmedi. Pratik sonuç şu: FrankenPHP’ye geçmek aynı işi daha az CPU zamanıyla yapıyor, ama bu bütçede aynı instance’tan daha fazla istek almıyor.

Bellek tarafında sıralama başka:

Aday Tepe RSS Not
Go ~27 MiB tek süreç, 32 bağlantılık havuz
PHP-FPM ~63 MiB 32 worker + 2 nginx worker
FrankenPHP (worker) ~137 MiB 32 worker thread ve Caddy, tek süreç
Adayın cgroup'undaki anonim belleğin tepe değeri, sabit hız fazı. Hiçbiri 1 GiB'lık sınırın yanına yaklaşmadı.

Nasıl ölçtüğüm

  • Bütçe. Docker VM’inin 12 vCPU’su ayrık üç kümeye bölündü: aday 0-3, PostgreSQL 4-7, yük üreteci 8-11. php-fpm’in nginx’i de adayın dört çekirdeğinin içinde; Go ve FrankenPHP HTTP’yi kendileri karşılıyor.
  • Her blok aynı yerden başlıyor. Tablo CREATE DATABASE … TEMPLATE ile birebir kopyalanıyor ve pg_prewarm ile önbelleğe çekiliyor. Ardından CHECKPOINT alınıyor, taze aday konteyneri açılıyor ve her senaryo 5 saniye ısıtılıyor. Tabloyu değiştiren tek senaryo yazma olduğu için hep sonda koşuyor.
  • Referans ölçümü. Her bloktan önce, uygulama kodu çalıştırmayan bir nginx adayın çekirdeklerine konup saniyede 50.000 istekle sınandı. Yük üreteci bu hızı bile tutturamasaydı hiçbir aday tutturamazdı. En düşük değer 49.980/sn oldu.
  • İki tahminci. Sabit hız fazında hedef sabit olduğu için tekrarların medyanı raporlandı. Tavan fazında makine sakinleştirilemediği ve girişim throughput’u yalnız aşağı çekebildiği için en iyi tekrar raporlandı. İki faz birbirini doğruluyor: PHP-FPM doğrulamada tavanda 31.157, sabit hızın medyanında 30.606 verdi; FrankenPHP 32.953 ve 33.089.

Sınırlar ve dürüstlük notları

  1. Tek makine, dizüstü. Aday, veritabanı ve yük üreteci ayrı çekirdeklerde ama tek bir VM’de ve tek bir Linux kernel’inde koşuyor; ağ Docker bridge’i, gerçek bir kablo değil. 12 vCPU’nun performans çekirdeğine ya da verimlilik çekirdeğine denk gelmesini macOS belirliyor.
  2. Yazma tavanı gürültülü. Go’nun yazma tavanı tekrarlar arasında 9.649 ile 43.942 arasında gezdi. O koşularda ne aday ne veritabanı CPU’da doymuştu; sınır büyük olasılıkla VM diskindeki WAL yazımı, ama bu kayıt onu doğrudan ölçmedi. Bu yüzden yazma tavanını karşılaştırma için değil, bağlam için okuyun. Sabit 10.000 yazma sonuçları bu gürültüden etkilenmiyor: 15 koşunun 15’i hedefi tuttu.
  3. Disk garantisi doğrulanmadı. Docker Desktop VM diskindeki fsync’in çıplak donanımla aynı garantiyi verip vermediğini kontrol etmedim. Mutlak yazma rakamları iyimser olabilir; üç adayın karşılaştırması etkilenmiyor, çünkü üçü aynı veritabanına yazıyor.
  4. Tek token. Her istekte aynı token gönderildi. Adaylar doğrulamayı önbelleğe almıyor, ama gerçek bir resource server farklı token’lar görür.
  5. Bir blok yeniden ölçüldü. Tavan fazında FrankenPHP’nin 3. tekrarı sürerken makine uykuya geçti. Blok baştan ölçüldü. Kesilen bloğun ham dosyaları kenara alınamadı ve yeniden ölçüm aynı adlarla onların üzerine yazdı. Bunun nasıl olduğu EXCLUDED.md dosyasında yazıyor.
  6. Kapsam dışı. Framework’ler, Swoole ile RoadRunner, JIT, token üreten authorization server ve kodu Go’ya taşımanın ekip maliyeti ölçülmedi. Framework’lerin kendi maliyeti PHP framework yük testi kaydında.

Kendi sayınızı üretmek için php-go-bench deposunu kendi token biçiminiz ve kendi sorgunuzla, kendi donanımınızda koşturun. Tablonuza yazacağınız değer istek başına CPU; gerisi çarpma.

İlgili yazılar

Paylaş:

Diğer Kayıtlar

Tüm kayıtlar

Yedi PHP framework'ü aynı yükte ölçtüm: fark, istek gerçek iş yaptıkça küçülüyor

Aynı donanımda, aynı PHP sürümünde ve aynı yedi rotayla, Laravel, Symfony, CodeIgniter, Yii2, Phalcon, Laminas ve Slim saniyede kaç isteği ne gecikmeyle karşılıyor?

Bulgu

Boş bir rotada en hızlı ile en yavaş arasında 4,4 kat var (Slim 25.975, Laravel 5.966 istek/sn). Ama istek gerçek iş yapmaya başlayınca fark kapanıyor: veritabanından tek satır çeken bir istekte 3,7 kata, yirmi satır çekende 3,5 kata iniyor. Phalcon boş rotada üçüncüyken veritabanı isteğinde beşinciye düşüyor — C eklentisi olmak, sorgu beklerken bir işe yaramıyor. Asıl pahalı olan şey framework seçimi değil: Laravel'in kendi varsayılan web middleware grubu, aynı yanıtı 5.858'den 2.176 istek/sn'ye düşürüyor — yani tek bir varsayılan, framework'ler arasındaki farkın çoğundan daha pahalı.

27 gün önce ölçüldü

Orta güven

Laravel'in preload eğrisi: 123 dosya, 1.912 dosyadan sekiz kat fazla kazandırıyor

Laravel için elle seçilmiş bir preload nereye kadar iner, ve her dilim ne kadar açılış bedeline mal olur?

Bulgu

Eğri hacimle orantılı değil. İlk 1.592 dosya (Laravel çekirdeği) 30 ms kazandırıyor ve açılışa 1,2 saniye ekliyor. Sonraki 1.094 Symfony dosyası 9,5 ms kazandırıyor, bedava. Ondan sonraki **123 dosya** (psr, carbon) 15,7 ms kazandırıyor — kendinden önceki 1.094 dosyadan fazla. Ve son 1.912 dosya yalnız 1,8 ms kazandırıp açılışa 1,2 saniye daha yazıyor. Yani önceki kaydın tavan olarak ölçtüğü "hepsini derle", eğrinin başlangıç noktası dışındaki en kötü fiyat/performans bölgesi: 2.809 dosyada durmak 12,77 ms ve 1.514 ms açılış verirken, 4.721 dosya 10,96 ms için 2.691 ms istiyor.

25 gün önce ölçüldü

Yüksek güven

opcache preload deploy faturasını on dört kata kadar siliyor — ama yedi framework'ün beşi onu size vermiyor

`opcache.preload` açıkken yedi PHP framework'ünün deploy sonrası ilk isteği ne kadar sürüyor, bu kazanç neye mal oluyor, ve kimler ona erişebiliyor?

Bulgu

Preload, soğuk ilk isteği 3,5 ile 14,2 kat arasında kısaltıyor: Symfony 35,58 ms'den 2,50 ms'ye, yani Phalcon'un çıplak seviyesine iniyor. Ama yedi adayın yalnız ikisi (Symfony, CodeIgniter) resmî bir preload dosyası yayınlıyor; kalan beşinde kazanç masada duruyor ve kullanıcının kendi yazmasını bekliyor. Yazmak da göründüğü kadar kolay değil: classmap'ten körlemesine üretilen preload Symfony'yi hiç ayağa kaldırmıyor, CodeIgniter'da ise elle seçilmiş resmî dosyadan (3,13 ms) daha kötü sonuç veriyor (5,29 ms). Ve bedel kaybolmuyor: Laravel'in classmap preload'ı ziyaretçiden aldığı 62 ms'yi php-fpm'in ayağa kalkışına 2.340 ms olarak yazıyor.

25 gün önce ölçüldü

Yüksek güven

Sitede Ara

Yazı, proje ve sayfalarda arama yapmak için yazmaya başlayın.

Esc ile kapat Pagefind ile güçlendirildi