Proof of concept: demon fastcgi w PHP tudzież serwlet PHP

Z racji, iż w firmie dla której pracuję, zajmuję się sporo tematem optymalizacji kodu chodzą mi po głowie kuriozalne niekiedy pomysły. W ostatnim wpisie objaśniałem jak działa mechanizm FastCgi. Pośrednikiem pomiędzy skryptem PHP a klientem FastCGI jest SAPI php.cgi. Implementuje to główne założenie jak i niestety wadę PHP - konieczność uruchomienia skryptu dla każdego żądania HTTP.

A co gdyby napisać demona w PHP, który potrafiłby przyjmować żądania z klienta FastCgi, przekazać je do zainicjalizowanego już kontrolera (patrz MVC) odebrać wynik i odesłać? Pomysł fajny - odpada konieczność wielokrotnej uruchamiania, kompilacji kodu, inkludowania plików, bo przecież aplikacja cały czas działa. Odpada konieczność konstruowania klas, populowania w nich danych dla każdego żądania - wystarczy zrobić to raz, podczas uruchomienia.

Tak mi się ten pomysł spodobał, że postanowiłem się przekonać, czy to ma szanse zadziałać. Google? Cóż, nikt chyba wcześniej nie wpadł na taki pomysł, ponieważ nie znalazłem niczego konkretnego. Zacząłem zatem od specyfikacji protokołu FastCgi. Sam protokół do najprostszych nie należy, nie mniej jednak w implementacji serwera pomogła mi implementacja klienta stworzona na potrzeby aplikacji Nanoweb - The PHP Web Server. Dodatkowo posłużyłem się funkcjonalnością PCNTL w celu implementacji wielowątkowości.

Testowa aplikacja działała na zasadzie utworzenia nasłuchującego na porcie TCP socketa, w momencie nadejścia połączenia wersja 1) odsyłała odpowiedź, zamykała połaczenie z klientem i była gotowa do obsługi następnych wersja 2) dla każdego żądania tworzyła nowy wątek. Obie wersje, oraz skrypt referencyjny odpowiadały zawartością phpinfo().

Problemy jakie napotkałem po drodze:

  • problem z parsowaniem ramek FCGI_PARAMS - one są osadzane w standardowej ramce FastCgi, ale mają chyba krótszy niż 8 bajtów nagłówek, postanowiłem całkowicie je zignorować, nie mniej jednak od ich poprawnego sparsowania zależy czy w aplikacji pojawi się tablica $_SERVER, $_GET i $_POST
  • trzeba samodzielnie zaimplementować cały mechanizm dekodowania formularzy jak i plików z żądania POST
  • zmienne pomiędzy procesami utworzonymi via pcntl_fork() kopiują się, nie są referencjami, dlatego w wątku potomnym możemy odczytać wartość zmiennej, ale zmiana nie jest widoczna dla innych wątków. Nie dotyczy to oczywiście zmiennych typu resource.

Kiedy udało mi się już zaimplementować podstawowe minimum: demon potrafi odesłać prawidłową odpowiedź: przystąpiłem do benchmarkowania.

Wyniki mnie rozwaliły. Pierwsze referencyjny benchmark dla standardowej konfiguracji: apacz + php.fcgi -b 127.0.0.1:80800 + skrypt

[pink@poema ~]$ ab -n 100 -c 5 http://stateless.projekt.kurylowicz.info/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
 
Benchmarking stateless.projekt.kurylowicz.info (be patient).....done
 
Server Software:        Apache/2.2.14
Server Hostname:        stateless.projekt.kurylowicz.info
Server Port:            80
 
Document Path:          /
Document Length:        57346 bytes
 
Concurrency Level:      5
Time taken for tests:   0.859 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      5750300 bytes
HTML transferred:       5734600 bytes
Requests per second:    116.39 [#/sec] (mean)
Time per request:       42.960 [ms] (mean)
Time per request:       8.592 [ms] (mean, across all concurrent requests)
Transfer rate:          6535.76 [Kbytes/sec] received
 
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.3      0       3
Processing:    13   42   4.2     43      48
Waiting:        7   36   4.3     38      42
Total:         13   42   4.1     43      48
 
Percentage of the requests served within a certain time (ms)
  50%     43
  66%     43
  75%     43
  80%     43
  90%     44
  95%     44
  98%     44
  99%     48
 100%     48 (longest request)

Drugi wynik apacz + przygotowanego servera FastCGI bez forkowania, co oznacza, że każde żądanie przetwarzane jest pojedynczo:

[pink@poema ~]$ ab -n 100 -c 5 http://stateless.projekt.kurylowicz.info/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
 
Benchmarking stateless.projekt.kurylowicz.info (be patient).....done
 
Server Software:        Apache/2.2.14
Server Hostname:        stateless.projekt.kurylowicz.info
Server Port:            80
 
Document Path:          /
Document Length:        23082 bytes
 
Concurrency Level:      5
Time taken for tests:   0.622 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      2323900 bytes
HTML transferred:       2308200 bytes
Requests per second:    160.84 [#/sec] (mean)
Time per request:       31.088 [ms] (mean)
Time per request:       6.218 [ms] (mean, across all concurrent requests)
Transfer rate:          3650.07 [Kbytes/sec] received
 
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.6      0       5
Processing:     9   30   8.3     28      60
Waiting:        9   29   8.5     26      59
Total:          9   30   8.2     28      60
 
Percentage of the requests served within a certain time (ms)
  50%     28
  66%     30
  75%     33
  80%     35
  90%     38
  95%     56
  98%     58
  99%     60
 100%     60 (longest request)

Czas o parę procent krótszy dla zwykłego phpinfo(). W przypadku inicjalizacji połączeń z bazami, memcache, odczytywania identycznych zestawów danych (ustawień) ta różnica może być o wiele większa. Przy czym pojawia się tu jeden problem: im więcej równoczesnych żądań (parametr -c dla ab) tym wynik jest gorszy i zbliża się do tego poniżej. Oba benchmarki, referencyjny jak i testowy obsługiwane były przez pojedynczy backend FastCgi, przy czym ten w referencyjnej konfiguracji radzi sobie lepiej ze współbieżnymi żądaniami. Należy zadbać tutaj o odpowiednie parametry dla socket_listen() ponieważ zbyt mały backlog powoduje, iż połączenia współbieżne się kolejkują. Przy odpowiednio dużej wartości $backlog problem nie występuje.

[pink@poema ~]$ ab -n 100 -c 5 http://stateless.projekt.kurylowicz.info/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
 
Benchmarking stateless.projekt.kurylowicz.info (be patient).....done
 
Server Software:        Apache/2.2.14
Server Hostname:        stateless.projekt.kurylowicz.info
Server Port:            80
 
Document Path:          /
Document Length:        23047 bytes
 
Concurrency Level:      5
Time taken for tests:   3.327 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      2320400 bytes
HTML transferred:       2304700 bytes
Requests per second:    30.05 [#/sec] (mean)
Time per request:       166.372 [ms] (mean)
Time per request:       33.274 [ms] (mean, across all concurrent requests)
Transfer rate:          681.01 [Kbytes/sec] received
 
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.9      0       7
Processing:    11  165  62.5    166     391
Waiting:        8  132  60.7    129     288
Total:         11  165  62.3    166     391
 
Percentage of the requests served within a certain time (ms)
  50%    166
  66%    189
  75%    204
  80%    209
  90%    236
  95%    270
  98%    337
  99%    391
 100%    391 (longest request)

Jak łatwo zauważyć w tym przypadku wyniki są kilkanaście razy gorsze. Przypuszczam, że wynika to z niewydolności forkowania w PHP.

Konkludując: idea wygląda wielce obiecująco i wydaje mi się, że jeśli by rozwiązać jakoś problem spadku wydajności przy konkurencyjnych żądaniach (forkowanie skreślam całkowicie, wyniki mówią same za siebie) poprzez uruchomienie większej ilości instancji demona i rozproszoną dystrybucję żądań CGI można by osiągnąć całkiem przyzwoite wyniki. Dalszą poprawę wydajności jak i redundancję systemu można osiągnąć poprzez uruchomienie klastra serwletów i balansowanie ruchu apacz, klient fCGI ->serwlet z użyciem HAProxy (przetestowałem, z metodą balansowania round-robin, wyniki niżej)

Niestety inną sprawą jest stabilność takiego rozwiązania. PHP nie jest zaprojektowany do pracy w trybie demona i prawdopodobnie mogą pojawić się problemy z garbage collectorem lub wyciekami pamięci. Dodatkowo pisanie aplikacji serwletowych wymaga nieco innego podejścia do samej architektury - nie można sobie pozwolić na radosne używanie miliona zmiennych bez zawracania sobie głowy zwalnianiem pamięci, ponieważ trzeba mieć na uwadze, iż po zakończeniu żądania zaalokowana pamięć nie zostanie zwolniona samodzielnie. Dodatkowo zmienia się także koncepcja deployowania aplikacji, ponieważ każda zmiana kodu wymaga restartu całego klastra serwletów.

Update: wyniki z użyciem 3 wątków php.fcgi -b 127.0.0.1:8061, php.fcgi -b 127.0.0.1:8062, php.fcgi -b 127.0.0.1:8063 + haproxy z balansowaniem round-robin:

[pink@poema php cgi server]$ ab -n 100 -c 15 http://stateless.projekt.kurylowicz.info/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
 
Benchmarking stateless.projekt.kurylowicz.info (be patient).....done
 
Server Software:        Apache/2.2.14
Server Hostname:        stateless.projekt.kurylowicz.info
Server Port:            80
 
Document Path:          /
Document Length:        57373 bytes
 
Concurrency Level:      15
Time taken for tests:   0.535 seconds
Complete requests:      100
Failed requests:        33
   (Connect: 0, Receive: 0, Length: 33, Exceptions: 0)
Write errors:           0
Total transferred:      5754584 bytes
HTML transferred:       5738884 bytes
Requests per second:    186.75 [#/sec] (mean)
Time per request:       80.323 [ms] (mean)
Time per request:       5.355 [ms] (mean, across all concurrent requests)
Transfer rate:          10494.54 [Kbytes/sec] received
 
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    5   9.6      0      31
Processing:    24   70  24.0     68     117
Waiting:        6   57  25.4     55     106
Total:         30   75  22.6     77     117
 
Percentage of the requests served within a certain time (ms)
  50%     77
  66%     85
  75%     90
  80%     95
  90%    110
  95%    112
  98%    115
  99%    117
 100%    117 (longest request)

Wyniki z użyciem 3 instancji serwletu + haproxy z balansowaniem round-robin:

[pink@poema php cgi server]$ ab -n 100 -c 15 http://stateless.projekt.kurylowicz.info/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
 
Benchmarking stateless.projekt.kurylowicz.info (be patient).....done
 
Server Software:        Apache/2.2.14
Server Hostname:        stateless.projekt.kurylowicz.info
Server Port:            80
 
Document Path:          /
Document Length:        23173 bytes
 
Concurrency Level:      15
Time taken for tests:   0.479 seconds
Complete requests:      100
Failed requests:        67
   (Connect: 0, Receive: 0, Length: 67, Exceptions: 0)
Write errors:           0
Total transferred:      2329784 bytes
HTML transferred:       2314084 bytes
Requests per second:    208.65 [#/sec] (mean)
Time per request:       71.891 [ms] (mean)
Time per request:       4.793 [ms] (mean, across all concurrent requests)
Transfer rate:          4747.12 [Kbytes/sec] received
 
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    2   5.1      0      21
Processing:     9   64  45.3     49     180
Waiting:        2   61  45.0     46     179
Total:          9   66  44.8     52     180
 
Percentage of the requests served within a certain time (ms)
  50%     52
  66%     60
  75%     85
  80%    105
  90%    148
  95%    166
  98%    180
  99%    180
 100%    180 (longest request)

Jak widać wyniki serwletu nadal lepsze i zapewne jak w poprzednim przypadku róznica może powiększyć się parokrotnie przy kodzie wymagającym inicjalizacji stałych elementów (konfig, includy klas).

Update: wyniki z jednym watkiem serwletu, ale za to z nieco bardziej złożoną aplikacją: klasa, która w konstruktorze tworzy jedno połaczenie do memcache, jedno połaczenie z bazą danych i robi prepare zapytania SQL, i dalej we właściwej metodzie run() podbija wartość klucza memcache i wykonuje przygotowane wcześniej zapytanie.

Test php.cli -b 127.0.0.1:8060, klasa inicjalizowana i uruchamiana w pojedynczym pliku index.php

[pink@poema var]$ ab -n 100 -c 10 http://stateless.projekt.kurylowicz.info/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
 
Benchmarking stateless.projekt.kurylowicz.info (be patient).....done
 
Server Software:        Apache/2.2.14
Server Hostname:        stateless.projekt.kurylowicz.info
Server Port:            80
 
Document Path:          /
Document Length:        1990 bytes
 
Concurrency Level:      10
Time taken for tests:   0.483 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      214700 bytes
HTML transferred:       199000 bytes
Requests per second:    207.17 [#/sec] (mean)
Time per request:       48.270 [ms] (mean)
Time per request:       4.827 [ms] (mean, across all concurrent requests)
Transfer rate:          434.36 [Kbytes/sec] received
 
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   1.2      0       5
Processing:    12   46   6.6     48      60
Waiting:       11   45   6.6     47      60
Total:         13   46   6.4     48      65
 
Percentage of the requests served within a certain time (ms)
  50%     48
  66%     48
  75%     49
  80%     49
  90%     50
  95%     50
  98%     59
  99%     65
 100%     65 (longest request)

Test serwletu, klasa inicjalizowana przed otwarciem portu TCP, połączenie klienckie wykonuje metodę run() na zainicjalizowanej już klasie:

[pink@poema var]$ ab -n 100 -c 10 http://stateless.projekt.kurylowicz.info/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
 
Benchmarking stateless.projekt.kurylowicz.info (be patient).....done
 
Server Software:        Apache/2.2.14
Server Hostname:        stateless.projekt.kurylowicz.info
Server Port:            80
 
Document Path:          /
Document Length:        1994 bytes
 
Concurrency Level:      10
Time taken for tests:   0.245 seconds
Complete requests:      100
Failed requests:        0
Write errors:           0
Total transferred:      215100 bytes
HTML transferred:       199400 bytes
Requests per second:    407.70 [#/sec] (mean)
Time per request:       24.528 [ms] (mean)
Time per request:       2.453 [ms] (mean, across all concurrent requests)
Transfer rate:          856.41 [Kbytes/sec] received
 
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   2.5      0      13
Processing:     8   22   7.0     22      42
Waiting:        5   21   7.3     20      41
Total:          8   24   7.0     23      43
 
Percentage of the requests served within a certain time (ms)
  50%     23
  66%     25
  75%     28
  80%     31
  90%     33
  95%     37
  98%     41
  99%     43
 100%     43 (longest request)

Jak widac z dwóch powyższych testów średni czas przetwarzania pojedynczego żadania przez serwlet jest 50% krótszy.

Post a Comment

Security Code: