İç yapı
Bu belge FrankenPHP'nin iç mimarisini açıklar; odak noktası iş parçacığı yönetimi, durum makinesi ve Go ile C/PHP arasındaki CGO sınırıdır.
FrankenPHP mimarisine genel bakış
FrankenPHP, PHP yorumlayıcısını CGO aracılığıyla doğrudan Go’ya gömer. Her PHP yürütmesi gerçek bir POSIX iş parçacığında çalışır (goroutine değil); çünkü PHP’nin ZTS (Zend Thread Safety) modeli bunu gerektirir. Go bu iş parçacıklarını bir durum makinesi üzerinden yönetir; C ise PHP SAPI yaşam döngüsünü üstlenir.
Ana katmanlar şunlardır:
- Go katmanı (
frankenphp.go,phpthread.go,thread*.go,scaling.gogibi üst düzey*.godosyaları): İş parçacığı havuzu yönetimi, istek yönlendirme, otomatik ölçekleme - C katmanı (
frankenphp.c,frankenphp.h): PHP SAPI uygulaması, betik yürütme döngüsü, süper küresel yönetimi - Durum makinesi (
internal/state/): Go goroutine’leri ile C iş parçacıkları arasında eşzamanlama
FrankenPHP iş parçacığı türleri
Ana iş parçacığı (phpmainthread.go)
Ana PHP iş parçacığı (phpMainThread) PHP çalışma zamanını başlatır:
php.inigeçersiz kılmalarını uygular- Sanal alan için ortamın bir anlık görüntüsünü (
main_thread_env) alır - PHP SAPI modülünü başlatır
- Go tarafına hazır olduğunu bildirir
Sunucunun ömrü boyunca hayatta kalır. Diğer tüm iş parçacıkları o Ready sinyalini verdikten sonra başlatılır.
Düzenli iş parçacıkları (threadregular.go)
Klasik, çağrı başına bir istek PHP betiklerini işler. Her istek:
requestChanveya paylaşılanregularRequestChanüzerinden bir istek alırbeforeScriptExecution()ile betik dosya adını döndürür- C katmanı PHP betiğini yürütür
afterScriptExecution()istek bağlamını kapatır
Worker iş parçacıkları (threadworker.go)
Bir PHP betiğini birden fazla istek boyunca canlı tutar. PHP betiği bir döngüde frankenphp_handle_request() çağırır:
beforeScriptExecution()worker betik dosya adını döndürür- C katmanı PHP betiğini yürütmeye başlar
- PHP betiği
frankenphp_handle_request()çağırır; bu da Go’dawaitForWorkerRequest()çağırır - Go bir istek gelene kadar bekler, ardından istek bağlamını kurar
- PHP geri çağrısı isteği işler
go_frankenphp_finish_worker_request()istek bağlamını temizler- PHP betiği 3. adıma döner
Betik çıktıktan sonra, worker en az bir kez frankenphp_handle_request()’e ulaşmışsa (çıkış temiz olsa da ölümcül bir hatanın sonucu olsa da) hemen yeniden başlatılır. Üstel geri çekme yalnızca ardışık başlatma hatalarına uygulanır; burada betik frankenphp_handle_request()’e hiç ulaşmadan çıkar.
FrankenPHP iş parçacığı durum makinesi
Her iş parçacığının yaşam döngüsünü yöneten bir ThreadState’i vardır (internal/state/state.go içinde tanımlanır). Durum makinesi tüm durum geçişleri için bir sync.RWMutex ve engelleyen beklemeler için kanal tabanlı bir abone kalıbı kullanır.
FrankenPHP iş parçacığı durumları
Lifecycle: Reserved → BootRequested → Booting → Inactive → Ready ⇄ (processing)
↓
Shutdown: ShuttingDown → Done → Reserved
↑
Restart (admin/watcher): Restarting → Yielding → Ready
↑
ZTS reboot (max_requests): Rebooting → RebootReady → Ready
↑
Handler transition: TransitionRequested → TransitionInProgress → TransitionComplete
Durumların tam kümesi internal/state/state.go içinde tanımlanır:
| Durum | Açıklama |
|---|---|
Reserved |
İş parçacığı yuvası ayrılmıştır ancak henüz başlatılmamıştır. İstek üzerine başlatılabilir. |
BootRequested |
Başlatma kuyruğa alınmıştır (ör. ana iş parçacığı tarafından) ancak POSIX iş parçacığı henüz başlamamıştır. |
Booting |
Alttaki POSIX iş parçacığı başlıyordur. |
Inactive |
İş parçacığı canlıdır ancak atanmış bir işleyicisi yoktur. Bellek ayak izi küçüktür. |
Ready |
İş parçacığının bir işleyicisi vardır ve iş kabul etmeye hazırdır. |
ShuttingDown |
İş parçacığı kapanıyordur. |
Done |
İş parçacığı tamamen kapanmıştır. Olası yeniden kullanım için Reserved’a döner. |
Restarting |
Worker iş parçacığı yeniden başlatılıyordur (ör. admin API veya dosya izleyici aracılığıyla). |
Yielding |
Worker iş parçacığı denetimi bırakmıştır ve yeniden etkinleştirilmeyi bekliyordur. |
Rebooting |
Worker iş parçacığı tam bir ZTS yeniden başlatması için C döngüsünden çıkıyordur (ör. max_requests). |
RebootReady |
C iş parçacığı çıkmıştır ve ZTS durumu temizlenmiştir; yeni bir C iş parçacığı doğurmaya hazırdır. |
TransitionRequested |
Go tarafından bir işleyici değişikliği istenmiştir. |
TransitionInProgress |
C iş parçacığı geçiş isteğini kabul etmiştir. |
TransitionComplete |
Go tarafı yeni işleyiciyi kurmuştur. |
Temel durum makinesi işlemleri
RequestSafeStateChange(nextState): Dış goroutine’lerin durum değişikliği istemesinin birincil yoludur. Şunları yapar:
ReadyveyaInactive’ten atomik olarak başarılı olur (mutex altında)ShuttingDown,DoneveyaReserved’tan hemenfalsedöndürür- Diğer herhangi bir durumdan
Ready,InactiveveyaShuttingDownbekleyerek engeller ve yeniden dener
Bu karşılıklı dışlamayı güvence altına alır: belirli bir iş parçacığında aynı anda shutdown(), setHandler() veya drainWorkerThreads()’ten yalnızca biri başarılı olabilir.
WaitFor(states...): İş parçacığı belirtilen durumlardan birine ulaşana kadar engeller. Bekleyenlerin verimli bildirilmesi için kanal tabanlı bir abone kalıbı kullanır.
Set(nextState): Koşulsuz durum değişikliği. İş parçacığının kendisi (C geri çağrılarından) durum geçişlerini bildirmek için kullanır.
CompareAndSwap(compareTo, swapTo): Atomik karşılaştır-ve-değiştir. Başlatma ilklendirmesi için kullanılır.
İşleyici geçiş protokolü
Bir iş parçacığının işleyicisini değiştirmesi gerektiğinde (ör. inactive’den worker’a):
Go side (setHandler) C side (PHP thread)
───────────────── ─────────────────
RequestSafeStateChange(
TransitionRequested)
close(drainChan)
detects drain
Set(TransitionInProgress)
WaitFor(TransitionInProgress)
→ unblocked WaitFor(TransitionComplete)
handler = newHandler
drainChan = make(chan struct{})
Set(TransitionComplete)
→ unblocked
newHandler.beforeScriptExecution()
Bu protokol, işleyici işaretçisinin asla eşzamanlı okunup yazılmamasını sağlar.
Worker yeniden başlatma protokolü
Worker’lar yeniden başlatıldığında (ör. admin API aracılığıyla):
Go side (RestartWorkers) C side (worker thread)
───────────────── ─────────────────
RequestSafeStateChange(
Restarting)
close(drainChan)
detects drain in waitForWorkerRequest()
returns false → PHP script exits
beforeScriptExecution():
state is Restarting →
Set(Yielding)
WaitFor(Yielding)
→ unblocked WaitFor(Ready, ShuttingDown)
drainChan = make(chan struct{})
Set(Ready)
→ unblocked
beforeScriptExecution() recurse:
state is Ready → normal execution
Go ile PHP arasındaki CGO sınırı
Dışa aktarılan Go işlevleri
C kodu CGO dışa aktarımları üzerinden Go işlevlerini çağırır. Ana geri çağrılar şunlardır:
| İşlev | Ne zaman çağrılır |
|---|---|
go_frankenphp_before_script_execution |
C döngüsü yürütülecek sonraki betiğe ihtiyaç duyduğunda |
go_frankenphp_after_script_execution |
PHP betiği yürütmeyi bitirdiğinde |
go_frankenphp_worker_handle_request_start |
Worker’ın frankenphp_handle_request()’i çağrıldığında |
go_frankenphp_finish_worker_request |
Worker istek işleyicisi döndüğünde |
go_ub_write |
PHP çıktı ürettiğinde (echo vb.) |
go_read_post |
PHP POST gövdesini okuduğunda (php://input) |
go_read_cookies |
PHP çerezleri okuduğunda |
go_write_headers |
PHP yanıt başlıklarını gönderdiğinde |
go_sapi_flush |
PHP çıktıyı boşalttığında |
go_log_attrs |
PHP yapılandırılmış bir mesaj kaydettiğinde |
Tüm bu işlevler çağıran iş parçacığını tanımlayan bir threadIndex parametresi alır. Bu, iş parçacığı başlatılırken ayarlanan C’deki iş parçacığı yerel bir değişkendir (__thread uintptr_t thread_index).
C iş parçacığı ana döngüsü
Her PHP iş parçacığı frankenphp.c içinde php_thread() çalıştırır:
// frankenphp.c: php_thread() ana betik yürütme döngüsü
while ((scriptName = go_frankenphp_before_script_execution(thread_index))) {
php_request_startup();
php_execute_script(&file_handle);
php_request_shutdown();
go_frankenphp_after_script_execution(thread_index, exit_status);
}
Bailout’lar (ölümcül PHP hataları) zend_catch tarafından yakalanır; bu, iş parçacığını sağlıksız olarak işaretler ve temizliği zorlar.
CGO sınırı boyunca bellek yönetimi
- Go → C dizeleri:
C.CString()malloc()ile ayırır. Serbest bırakmak C tarafının sorumluluğundadır (ör.frankenphp_free_request_context()çerez verisini serbest bırakır). - Go dize sabitleme:
phpThread(phpthread.goiçinde) Go’nunruntime.Pinner’ını gömer.thread.Pin()/thread.Unpin()C’den başvurulan Go belleğini kopyalamadan canlı tutar. İş parçacığı her betik yürütmesinden sonra serbest bırakılır. - PHP belleği: Zend bellek yöneticisi (
emalloc/efree) tarafından yönetilir. İstek kapanışında otomatik serbest bırakılır.
FrankenPHP iş parçacığı otomatik ölçeklemesi
FrankenPHP, talebe göre PHP iş parçacığı sayısını otomatik ölçekleyebilir (scaling.go).
Otomatik ölçekleme yapılandırması
num_threads: Açılışta başlatılan başlangıç iş parçacığı sayısımax_threads: İzin verilen en fazla iş parçacığı sayısı (otomatik ölçeklenenler dahil)
Yukarı ölçekleme
Ayrılmış bir goroutine tamponlanmamış bir scaleChan’den okur:
- Bir istek işleyicisi müsait bir iş parçacığı bulamaz
- İstek bağlamını
scaleChan’e gönderir - Ölçekleme goroutine’i şunları kontrol eder:
- İstek yeterince uzun süre takılı kaldı mı? (en az 5 ms)
- CPU kullanımı eşiğin altında mı? (%80)
- İş parçacığı sınırına ulaşıldı mı?
- Tüm kontroller geçerse yeni bir iş parçacığı başlatılır ve atanır
Aşağı ölçekleme
Ayrı bir goroutine periyodik olarak (her 5 sn) boştaki otomatik ölçeklenmiş iş parçacıklarını kontrol eder. Ready durumunda maxIdleTime’dan (varsayılan 5 sn) daha uzun süre boşta kalan iş parçacıkları Inactive’e dönüştürülür (döngü başına en fazla 10). Tamamen durdurulmazlar: bunun için bir kod yolu vardır ancak şu anda devre dışıdır; çünkü bazı PECL eklentileri bellek sızdırır ve iş parçacıklarının temiz kapanmasını engeller.
İş parçacığı başına ortam sanal alanı
FrankenPHP ortam değişkenlerini iş parçacığı başına sanal alana alır:
- Başlangıçta ana iş parçacığı
os.Environ()’umain_thread_env(bir PHPHashTable) içine anlık görüntüler. $_SERVER,main_thread_env’in bir kopyası artı isteğe özgü değişkenlerden oluşturulur (frankenphp_register_server_varsiçinde). Her istek için, worker betiğinin her yinelemesi dahil, yeniden kurulur.$_ENVaynı anlık görüntüden PHP’ninphp_import_environment_variableskancası aracılığıyla doldurulur. Düzenli modda bu her betik yürütmesinde bir kez olur; worker modunda worker betiği başladığında bir kez olur ve worker istekleri arasında yeniden kurulmaz; bu yüzden$_ENVyazmaları istekler arasında sızar (bkz. Worker modu).frankenphp_putenv()/frankenphp_getenv()main_thread_env’den tembel başlatılan iş parçacığı yerelsandboxed_envüzerinde çalışır; global C ortamındaki yarış koşullarını önler.reset_sandboxed_environment()her PHP betik yürütmesinden sonrasandboxed_env’i serbest bırakır. Düzenli modda bu istek başınadır; worker modunda yalnızca worker betiğinin kendisi çıktığında çalışır, bu yüzdenputenv()yazmaları betik yeniden başlayana kadar aynı iş parçacığındaki sonraki worker isteklerinde görünür.
İstek akışı (düzenli mod)
- HTTP isteği Caddy’ye gelir
- FrankenPHP’nin Caddy modülü PHP betik yolunu çözer
- İstek ve betik bilgisiyle bir
frankenPHPContextoluşturulur - Bağlam
requestChanüzerinden müsait bir düzenli iş parçacığına gönderilir - İş parçacığının
beforeScriptExecution()’ı betik dosya adını döndürür - C katmanı PHP betiğini yürütür
- Yürütme sırasında Go geri çağrıları G/Ç’yi işler (
go_ub_write,go_read_postvb.) - Yürütmeden sonra
afterScriptExecution()tamamlanmayı bildirir - Yanıt istemciye gönderilir
İstek akışı (worker modu)
- HTTP isteği Caddy’ye gelir
- FrankenPHP’nin Caddy modülü bu istek için worker’ı çözer
- Bir
frankenPHPContextoluşturulur - Bağlam worker’ın
requestChan’ine veya belirli bir iş parçacığınınrequestChan’ine gönderilir - Worker iş parçacığının
waitForWorkerRequest()’i onu alır - PHP’nin
frankenphp_handle_request()geri çağrısı tetiklenir - Geri çağrı döndükten sonra
go_frankenphp_finish_worker_request()temizler - Worker
waitForWorkerRequest()’e döner