İçeriğe geç
Muhammet Şafak
en

Bir çekirdek Go'da 14.330, PHP-FPM'de 5.152 OAuth2 isteği taşıyor

Her istekte RS256 bearer token doğrulayıp PostgreSQL'e bir satır yazan ya da okuyan aynı API, bir, iki ve dört çekirdekte Go, PHP-FPM ve FrankenPHP worker ile saniyede kaç karma istek taşıyor?

Bulgu

Dört çekirdekte Go 57.321, FrankenPHP 25.659, PHP-FPM 20.606 karma istek taşıdı; çekirdek başına 14.330, 6.415 ve 5.152. Kapasite planına giren sayı bu değil, istek başına uygulama CPU'su: Go 66,8, FrankenPHP 110,7, PHP-FPM 187,7 mikrosaniye. FrankenPHP doyduğunda dört çekirdeğin ancak 2,84'ünü kullanabiliyor — Go 3,83, PHP-FPM 3,87. Darboğaz veritabanı değil: aynı dört çekirdekte PostgreSQL tek başına saniyede 68.212 satır yazıyor, yani en hızlı adayın karma tavanının üstünde.

Çekirdek başına kapasite · Go
14.330 istek/sn
Çekirdek başına kapasite · PHP-FPM
5.152 istek/sn
İstek başına CPU · Go → PHP-FPM
66,8 → 187,7 µs
FrankenPHP · doyumda çekirdek kullanımı
%71
PostgreSQL · kendi INSERT tavanı
68.212 satır/sn

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, iki PHP adayı aynı sınıfı çalıştırıyor. Yük karma: iki open-loop üreteç aynı anda, biri okuyup biri yazarak, her biri kendi access token'ıyla. Her aday 1, 2 ve 4 sabitlenmiş çekirdekte ve o çekirdek bütçesindeki kendi en iyi havuz boyutuyla ölçüldü; havuz boyutu ayrı bir fazda 8/16/32/64 süpürülerek seçildi. Yayınlanan sayı kapalı çevrimde doyum tavanıdır: hücre başına 5 tekrar, her tekrardan önce uygulama kodu çalıştırmayan bir nginx aynı çekirdeklerde beklenen hızın %115'iyle sınandı ve 45 tekrarın 45'i bu kapıdan geçti, en iyi tekrar raporlandı. CPU ve bellek cgroup sayaçlarından, ana makine yükü her tekrarda ayrıca kaydedildi. Koşunun tamamı 333 yük ölçümü ve 110.617.180 yanıt; yayınlanan tavan fazı bunların 45'i ve 18.551.833 yanıtı. Hiçbir ölçümde non-2xx yanıt ya da transport hatası görülmedi.

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

dü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
FrankenPHP
1.12.7 (Caddy 2.11.4) · worker modu · ZTS
Veritabanı
PostgreSQL 17.11 · 1.000.000 satır · synchronous_commit açık
Yük üreteci
oha 1.15.0 · iki eşzamanlı üreteç · 64 bağlantı/üreteç
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
Tahminci
hücre başına 5 tekrar, üreteç yoklamasından geçenlerin en iyisi

Teknolojiler

Go PHP PostgreSQL Docker nginx

Tekrarlamak için

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

Saniyede on binlerce isteği karşılayacak bir OAuth2 servisi için Go önerdiğimde ekibin sorusu haklıydı: ne kadar kazanıyoruz? “Go daha hızlı” cevabı bu soruyu karşılamıyor, çünkü kapasite tablosuna yazılan şey saniyede istek değil, çekirdek. Bu yüzden aynı API’yi üç kez yazdım ve üçünü de bir, iki ve dört çekirdekte, yarısı okuma yarısı yazma olan aynı trafiğin altında ölçtüm. 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.

Aday HTTP Veritabanı idiom Havuz boyutu (1c / 2c / 4c)
Go net/http pgx, bağlantı başına statement önbelleği 16 / 64 / 64
FrankenPHP (worker) Caddy, gömülü PDO, worker ömrü boyunca hazırlanmış statement 8 / 8 / 8
PHP-FPM nginx, aynı çekirdek bütçesinde PDO, tek gidiş-dönüşlük parametreli çağrı 8 / 16 / 32
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.

Yük karma: iki open-loop üreteç aynı anda koşuyor, biri GET /events/{id} ile milyon satırlık tablodan birincil anahtarla tek satır okuyor, diğeri POST /events ile tek INSERT … RETURNING yazıyor. İkisi de kendi access token’ını gönderiyor.

Burada yayınlanan “kapasite” bir doyum tavanıdır, bir servis seviyesi değildir. Yani adayın yanıtları yetiştirdiği en yüksek hızdır; “p99 şu eşiğin altında kalarak” güvencesi taşımaz. Bunu ayrıca ölçmeye üç kez çalıştım ve üçünde de bu tezgâhta güvenilmez sonuç aldım; nedeni aşağıda, sınırlar bölümünde. Tavanın yanına o tavandaki gecikmeyi de bastım, böylece sayının neye mal olduğunu görebilirsiniz.

Çekirdek başına kapasite

Karma OAuth2 trafiğinde doyum tavanı

Her sütun, o adayın o çekirdek bütçesinde yetiştirdiği toplam istek/sn. Okuma ve yazma birlikte.

  • Go
  • FrankenPHP (worker)
  • PHP-FPM

Kaynak: Hücre başına 5 tekrar, üreteç yoklamasından geçenlerin en iyisi — bench/capacity-report.mjs

Veri tablosu
Hücre başına 5 tekrar, üreteç yoklamasından geçenlerin en iyisi — bench/capacity-report.mjs
Seri 1 çekirdek2 çekirdek4 çekirdek
Go 16.88329.44957.321
FrankenPHP (worker) 6.54414.30625.659
PHP-FPM 6.21811.21120.606

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

Dört çekirdekte Go, PHP-FPM’in 2,78 katını, FrankenPHP’nin 2,23 katını taşıyor. Çekirdek başına çevirdiğinizde fark daha okunur hâle geliyor: Go 14.330, FrankenPHP 6.415, PHP-FPM 5.152 istek.

Üçü de çekirdek eklendikçe doğrusala yakın ölçekleniyor ama hiçbiri tam doğrusal değil. Bir çekirdekten dörde çıkarken Go teorik dördün %84,9’unu, PHP-FPM %82,8’ini alıyor. FrankenPHP’nin %98’i yanıltıcı: paydası, tek çekirdekte zaten düşük olan kendi rakamı.

Aday Tavan (4c) Çekirdek başına Tavandaki p50 (okuma/yazma) Tavandaki p99 Tepe RSS
Go 57.321/sn 14.330 1,81 / 2,42 ms 4,69 / 5,41 ms 30,4 MiB
FrankenPHP (worker) 25.659/sn 6.415 4,77 / 4,96 ms 8,80 / 9,06 ms 60,3 MiB
PHP-FPM 20.606/sn 5.152 6,03 / 6,24 ms 8,27 / 8,45 ms 60,0 MiB
Doyum tavanındaki gecikme — yani adayın en zorlandığı andaki hâli. Go tavanını gecikmeyle satın almıyor: en yüksek hızı verirken aynı zamanda en düşük p99'u veriyor.

Tabloya yazacağınız sayı bu değil

Dört çekirdekteki 57.321 yalnız dört çekirdek için doğru. Kendi hedefinize taşıyan sayı çekirdek başına istek değil, istek başına CPU:

İstek başına uygulama CPU'su, doyum tavanında

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: Doyum tavanı koşuları, en iyi geçerli tekrar

Veri tablosu
Doyum tavanı koşuları, en iyi geçerli tekrar
Seri 1 çekirdek2 çekirdek4 çekirdek
Go 57,8 µs63,65 µs66,77 µs
FrankenPHP (worker) 82,77 µs100,68 µs110,74 µs
PHP-FPM 157,07 µs173,83 µs187,74 µ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. Saniyede 30.000 karma istek için Go’da hesap 30.000 × 66,8 µs = 2,0 çekirdek, FrankenPHP’de 3,3, PHP-FPM’de 5,6.

Üç adayda da istek başına CPU çekirdek sayısı arttıkça yükseliyor — Go’da 57,8’den 66,77’ye, PHP-FPM’de 157,07’den 187,74’e. Bu, ölçeklemenin neden doğrusal olmadığının sayaç tarafındaki karşılığı: eşzamanlılık arttıkça bağlam değişimi, kilit ve önbellek kaçırma maliyeti istek başına payı büyütüyor. Planlama yaparken hedeflediğiniz instance boyutunun sütununu kullanın, tek çekirdeğin sayısını dörtle çarpmayın.

FrankenPHP’nin kullanamadığı çekirdekler

FrankenPHP’nin istek başına CPU’su PHP-FPM’inkinin %41 altında (110,74’e karşı 187,74 µs). Bu tasarruf kapasiteye tam dönüşmüyor, çünkü doyduğunda çekirdeklerini bitiremiyor:

Aday 1 çekirdekte kullanılan 2 çekirdekte 4 çekirdekte
Go 0,98 (%98) 1,87 (%93,5) 3,83 (%95,8)
PHP-FPM 0,98 (%98) 1,95 (%97,5) 3,87 (%96,8)
FrankenPHP (worker) 0,54 (%54) 1,44 (%72) 2,84 (%71)
Doyum anında adayın cgroup'unun gerçekten harcadığı çekirdek. Go ve PHP-FPM verilen bütçeyi bitiriyor; FrankenPHP tek çekirdekte yarısını, dört çekirdekte 1,16'sını boşta bırakıyor.

Tek çekirdekte FrankenPHP, PHP-FPM’den yalnız %5 daha fazla istek taşıyor (6.544’e karşı 6.218) — oysa istek başına CPU’su neredeyse yarısı. Aradaki kazancın tamamı boşta kalan çekirdekte kayboluyor. İki ve dört çekirdekte darboğaz gevşiyor ve fark açılıyor (%28 ve %25), ama üçte birlik bütçe hâlâ kullanılmıyor.

Darboğazın nerede olduğunu bu kayıt ölçmedi. Havuz boyutu değil: 8, 16, 32 ve 64 worker denendi ve FrankenPHP her çekirdek bütçesinde 8’i seçti, yani daha fazla eşzamanlılık ona yardım etmiyor. Caddy ile PHP thread’leri arasındaki geçişte ya da ZTS derlemesinde olabilir. Pratik sonuç şu: FrankenPHP’ye geçmek aynı işi daha az CPU zamanıyla yapıyor, ama ödediğiniz çekirdeğin üçte birini size geri vermiyor.

Darboğaz veritabanı değil

FrankenPHP’nin boşta bıraktığı 1,16 çekirdek uygulama tarafında. Peki aynı yükü altta taşıyan PostgreSQL nerede duruyor?

Ölçüm Değer Kullanılan çekirdek (4 üzerinden)
PostgreSQL tek başına, pgbench INSERT 68.212 satır/sn 3,45
Go doyarken veritabanının yaptığı iş 24.930 yazma + 32.392 okuma/sn 2,34
FrankenPHP doyarken 12.575 yazma + 13.084 okuma/sn 1,11
PHP-FPM doyarken 10.128 yazma + 10.478 okuma/sn 1,46
pgbench satırı HTTP ve token olmadan, aynı satırı aynı tabloya aynı dayanıklılık ayarıyla yazıyor. Üç adayın hiçbiri veritabanını doyurmuyor.

En hızlı aday tavanındayken bile PostgreSQL dört çekirdeğinin 2,34’ünü kullanıyor ve kendi tavanı o noktanın epey üstünde. Yani bu iş yükünde seçim tamamen uygulama runtime’ında: veritabanını değiştirmek kapasite tablosunu kıpırdatmıyor, dili değiştirmek 2,78 kat kıpırdatıyor.

Bir ayrıntı yine de veritabanı tarafında görünüyor. İstek başına PostgreSQL CPU’su Go’da 40,75 µs, FrankenPHP’de 43,2 µs, PHP-FPM’de 71,07 µs. Statement’ı tekrar kullanabilen iki aday birbirine yakın; kullanamayan PHP-FPM veritabanına istek başına %74 daha fazla iş yaptırıyor. Muhtemel sebep her sorgunun yeniden ayrıştırılıp planlanması; bu kayıt bunu ayrıca ölçmedi.

Her adaya kendi en iyi havuzu

Bu tür karşılaştırmalara gelen ilk itiraz “PHP’yi kötü ayarlamışsın” olur. Bu yüzden havuz boyutu ölçümün öncesinde ayrı bir faz olarak süpürüldü: her aday, her çekirdek bütçesinde 8, 16, 32 ve 64 ile ikişer kez tam gaz koşturuldu ve kendi en iyisiyle ölçüme girdi. Havuz boyutu aynı zamanda veritabanı bağlantı tavanıdır, yani üç adayda da tek bir düğme.

Aday 1 çekirdek 2 çekirdek 4 çekirdek En kötü seçimin bedeli
Go 16 64 64 %70,6 (4c: 8 worker 32.666, 64 worker 55.715)
PHP-FPM 8 16 32 %30,7 (1c: 64 worker 4.734, 8 worker 6.188)
FrankenPHP (worker) 8 8 8 %29,6 (2c: 32 worker 11.252, 8 worker 14.580)
Seçilen havuz boyutları ve o hücrede en iyi ile en kötü seçim arasındaki fark. Üç adayda da en yüksek throughput'u veren boyut aynı zamanda en iyi p99'u verdi, yani seçim ölçütü gecikmeyi feda etmiyor.

Sabit bir sayı üçünden hiçbirinin en iyisi değil ve yön de aynı değil: Go çekirdek arttıkça daha fazla eşzamanlılık istiyor, iki PHP adayı istemiyor. Tek çekirdekte 64 worker’lı php-fpm, 8 worker’lıdan %24 daha az iş çıkarıyor.

Hedefinizi çekirdeğe çevirmek

Hedef hız Go FrankenPHP PHP-FPM
10.000 istek/sn 1 1 1
20.000 istek/sn 1 1 1
30.000 istek/sn 1 2 2
50.000 istek/sn 1 2 3
100.000 istek/sn 2 4 5
Dört çekirdekli instance sayısı, ölçülen tavana bölünüp yukarı yuvarlanarak. Yatay ölçeklemenin doğrusal olduğunu ve baş üstü boşluk bırakmadığınızı varsayıyor; veritabanı çekirdekleri dahil değil.

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 bütçesinin içinde; Go ve FrankenPHP HTTP’yi kendileri karşılıyor. Bellek her çekirdek bütçesinde 1 GiB sabit, böylece eksen yalnız çekirdek.
  • Her hücre aynı yerden başlıyor. Her hücre, tohumlanmış veritabanının CREATE DATABASE … TEMPLATE ile alınmış bir kopyasıyla başlıyor; kopya pg_prewarm ile önbelleğe çekiliyor, CHECKPOINT alınıyor ve taze bir aday konteyneri açılıp ısıtılıyor.
  • Üreteç yoklaması. Her tekrardan önce, uygulama kodu çalıştırmayan bir nginx adayın kendi çekirdeklerine konuyor ve aynı iki üreteçle adayın beklenen tavanının %115’i isteniyor. Yoklamanın altında kalan tekrar adayı değil üreteci ölçmüş olur ve elenir. 45 tekrarın 45’i geçti.
  • Tahminci en iyi tekrar. Ölçüm bir dizüstünde koşuyor ve makinedeki her iş ölçümden düşüyor; girişim yalnız aşağı çekebilir. Bu yüzden her tekrarın yanına macOS’un o anki yükü de kaydedildi.
  • Bağımsız doğrulama. Aynı tavanlar, on iki saat önce farklı bir bağlantı sayısıyla koşan havuz süpürmesinde de ölçüldü ve iki bağımsız ölçüm birkaç yüzde içinde örtüştü.

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

  1. Servis seviyeli kapasite ölçülemedi. “p99 10 ms’nin altında kalarak en fazla kaç istek” sorusunu üç ayrı yöntemle ölçmeye çalıştım; üçü de bu tezgâhta güvenilmez çıktı ve elendi. Sebep, open-loop ölçümde sınırın bir eğim değil uçurum olması: servis yavaşlayınca yük üreteci takvimden geri kalıyor ve gecikme düzeltmesi bunu üste yazıyor; uçurumun yeri de tekrarlar arasında geziyor. Aynı hücrenin tekrarları beş kata kadar ayrıştı. Elenen üç denemenin ham verisi ve teşhisi EXCLUDED.md dosyasında duruyor. Bu yüzden yayınlanan sayı doyum tavanıdır ve confidence alanı medium.
  2. Tek makine, dizüstü. 12 vCPU’nun tamamı Docker VM’ine verildiği için macOS’ta koşan her iş doğrudan ölçümden düşüyor. Bu bir varsayım değil, ölçülmüş bir olay: bir koşu sırasında ana makinede headless Chromium süreçleri on çekirdeğin üstünü kullandı ve aynı Go servisi 57.321 yerine 17.490 istek/sn ölçüldü. O koşu atıldı, yoklama kapısı ve ana makine yükü kaydı bu yüzden eklendi.
  3. Karışım %50/%50. Kendi trafiğiniz okuma ağırlıklıysa üç adayın da tavanı yükselir, çünkü okuma, yazmadan ucuz. Oran sizin profilinizle değişir; istek başına CPU’yu okuma ve yazma için ayrı ayrı ölçmek bu kaydın kapsamında değildi.
  4. 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.
  5. Kapsam dışı. Framework’ler, PHP JIT, Swoole ile RoadRunner, PostgreSQL’e alternatif veritabanları, 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ı.

29 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.

27 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.

27 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