Dobraniania.pl unleashed

Wczoraj po długich trudach udało się wreszcie wypuścić pierwszy oficjalny release projektu, nad którym z doskoku siedziałem niemalże rok: serwis ogłoszeń opiekunek www.dobraniania.pl. Serwis wykonany jest w oparciu o mój framework, o którym może nieco później coś wspomnę, na łamach bloga. Strona wykonana jest w technologii PHP 5 i oparta na bazie danych MySQL. Serwis ma zastapić istniejący portal prowadzony przez firmę, dla której oprogramowanie było pisane.

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.

Read the rest of this entry »

Apache + PHP+ FastCGI + Spawn FastCGI? Wydajność, migracja.

Mijający już weekend upłynął mi pod znakiem migracji serwera www.poema.art.pl na FastCgi. Sporo alternatyw, sporo, ale niepełnych informacji - trudno się w tym połapać komuś, kto nie jest zaznajomiony z tą technologią. Ale od początku. Dlaczego warto się przerzucić  na FastCgi? Otóż powody są co najmniej dwa:

  1. wydajność
  2. bezpieczeństwo

Rozwinięcie wątku wydajności należałoby zacząć od wyjaśnienia w jaki sposób FastCgi działa. Otóż dzięki FastCgi separujemy wykonanie kodu PHP od wątków Apacza. Apacz w modelu FastCgi służy wyłącznie do komunikacji z przeglądarką klienta. W momencie kiedy klient zażąda uruchomienia skryptu PHP - żądanie to przekazywane jest do 'serwera FastCGI' który używając interpretera PHP uruchamia skrypt i odsyła wynik do Apacza. Wygląda to mniej więcej tak:

+----------+  TCP 80  +----------+ Unix socket
|  Klient  | <----->  |  Apache  | <-----+
+----------+          +----------+       |
                       user: http        |
+----------+          +----------+       |      +-------------+        +--------------+
|  Klient  | <----->  |  Apache  | <-----+----> | PHP FastCgi | <----> | Server MySQL |
+----------+          +----------+       |      +-------------+        +--------------+
                       user: http        |        user: john
+----------+          +----------+       |
|  Klient  | <----->  |  Apache  | <-----+
+----------+          +----------+
                       user: http

Przy użyciu spawn-fastcgi uruchamiamy dla każdego z użytkowników własny serwer PHP FastCgi który nasłuchuje na Unixowym sockecie. Apache komunikuje się po sockecie z wybranym zgodnie z ustawieniami (per vhost) serwerem żądając wykonania skryptu. Dzięki temu otrzymujemy izolację wątków PHP od wątków Apache. Dodatkowo PHP FastCgi działając z uprawnieniami użytkownika, nie zaś serwera Apache ma możliwość zapisu do katalogów użytkownika, a także ograniczenia odczytu plików do których posiada uprawnienia.

Ok, ale co z wydajnością?

Po pierwsze Apache, dzięki temu że nie parsuje w każdym swoim wątku PHP zabiera o wiele mniej pamięci. Przy dużej ilości żądań jest  to sprawa kluczowa. Dzięki modelowi FastCgi można w prawidłowy sposób wykorzystać mechanizm database persistent connections (eg. w MySQL). W przypadku wielu wątków Apacza parsujących PHP nawiązywanych jest tyle połączeń ile wątków. Nowe wątki nie potrafią użyć ponownie połączeń utworzonych przez zabite wątki. Dzięki temu w parę minut osiągamy maksymalny limit połączeń z bazą.  W przypadku FastCgi nawiązanych zostaje tyle połączeń ile wątków i połączenia te są cały czas utrzymywane. To jest także sprawa kluczowa dla serwisów o wysokim obciążeniu.

Bezpieczeństwo?

Wspomniałem wcześniej - wykonanie kodu PHP z uprawnieniami użytkownika. Ale nie tylko - separacja cache APC to kolejny element bezpieczeństwa. Dalej: możliwość pozbycia się safe mode w PHP - aktualnie to mechanizm z którego PHP się wycofuje. W wersji 5 ma już go nie być.

fCgi, FastCgi, fCgid?

Jest sporo zamieszania jeśli chodzi o mechanizm FastCgi. Istnieje mnóstwo alternatyw. Osoba zaznajamiająca się z tematem za pomocą poldek search *cgi*   może czyć się nieco zagubiona. Dlatego opiszę co potrzebujemy aby to wszystko właściwie uruchomić.

Zacznijmy od końca, czyli od PHP. Potrzebna będzie paczka zawierająca interpreter fcgi dla PHP - jest to dodatkowy moduł SAPI (oprócz eg. zwyczajnego cli, lub modułu do Apacha) które można skompilować, tudzież zainstalować. Pod PLD jest to pakiet "php-fcgi". Dalej, przyda się coś co będzie potrafiło utworzyć nam serwery FastCgi dla użytkowników. Pakiet "spawn-fcgi" jest całkiem dobry do tego. I wreszcie musimy nauczyć Apache komunikować się z naszymi serwerami. Doinstalujmy moduł "apache-mod_fastcgi".

Wszystkie moduły można bez obaw zainstalować na serwerze który używa modułu "apache-mod_php"  do parsowania kodu PHP. Generalnie można używać obu modeli parsowania jednocześnie - część vhostów i aplikacji parsować modułem Apacza, najbardziej obciążone vhosty puścić przez FastCgi.

Read the rest of this entry »

Cpuset dla Linux 2.6

Ostatnio, z ciekawości chciałem przetestować sobie Cupset w Linux kernel 2.6. Standardowo pod PLD jest to wkompilowane w Kernel oraz wspierane (mniej więcej) w RC-scripts. Generalnie Cpusets  przydatne jest dla maszyn wieloprocesorowych. Służy do tworzenia wirtualnych procesorów, w których skład może wchodzić dowolna ilość procesorów fizycznych. Następnie, można przypisać wybrany proces do takiego wirtualnego procesora, tak, aby wykonywał się wyłącznie na nim. Potencjalne korzyści wydają się oczywiste:

  • przypisanie zasobów konkretnych procesorów dla wybranych usług, tak aby nie zajmowały sobie wzajemnie czasu procesora, eg. apache + mysql
  • przypisanie procesów wybranego użytkownika do konkretnego procesora
  • etc

Dla mnie ciekawym wydało się zastosowanie numer 1, z uwagi na fakt, iż mam Apache i MySql na tej samej maszynie. Postanowiłem zatem przypisać obie usługi do osobnych grup procesorów. Jako, iż dysponuje maszyną 2 x Intel(R) XEON(TM) CPU 2.20GHz każdej usłudze przypadło po 1 procesorze.

Ogólne wnioski są takie, ze przy 2 procesorach nie warto. Apache zżera o wiele więcej czasu procesora niż MySQL, co skutkuje ciągłym przeciążeniem jednego procesora, i co za tym idzie obniżeniem wydajności. Procesor przypisany dla MySQL jest obciążany w tym czasie na poziomie 20-40%.

Jedyną korzyścią, jaką widzę w tej konfiguracji, jest to, że przy dużym ruchu, Apache nie zablokuje całkowicie maszyny, ponieważ pozostałe usługi będą mogły wykonać się na mniej obciążonym procesorze.

Przy większej ilości procesorów miało by to być może sensowne zastosowanie. Linki:

Read the rest of this entry »

Stored procedures czasem sie przydają

Podczas konwersji bazy danych serwisu Poema napotkałem pewien problem. Otóż dane dotyczące przypisania użytkownika do grup dostępu nie były zapisywane w osobnej tabeli jako indywidualne rekordy, lecz w rekordzie użytkownika, w pojedynczym polu o nazwie GID,  jako identyfikatory grup oddzielanych przecinkami, np. '1,3,19,3' lub pojedyncze wartości '8'. Z oczywistych względów dane takie są mało użyteczne. Zatem trzeba je przenieść do osobnej tabeli i powkładać w indywidualne rekordy. Na pierwszy rzut oka MySQL sobie nie poradzi. No to skrypt PHP, ale... leniwy jestem - nie chce mi się pisać skryptu, może zatem jednak MySQL. Rozwiązaniem jest napisanie własnej funkcji. Wygląda ona mniej więcej tak:

delimiter //
CREATE FUNCTION create_records_in_poema_uid_group(p_id INT)
RETURNS INT
BEGIN
  DECLARE gid_set VARCHAR(255) DEFAULT '';
  DECLARE gid INT;
  DECLARE pos INT;
 
  SET @done = 0;
  SET @num = 0;
 
  -- Pobieramy wartość pola do zmiennej gid_set, pole może wyglądać
  -- '1,5,12,16' lub być pojedyncza wartością '10'
  -- dodatkowy warunek w WHERE zapobiega duplikatom
     SELECT u.GID
       INTO gid_set
       FROM poema_users u
  LEFT JOIN poema_uid_group g ON (g.intUid = u.ID)
      WHERE u.ID = p_id
        AND g.intUidGroupId IS NULL;
 
  IF gid_set <> '' THEN
    REPEAT
 
      -- Ustalamy pozycję znaku przecinka
      SELECT LOCATE(',', gid_set) INTO pos;
 
      IF pos > 0 THEN
        -- jest przecinek, odetnij pierwszą wartość, i usuń ją ze zmiennej gid_set
        SELECT CAST(SUBSTR(gid_set, 1, pos) AS UNSIGNED) INTO gid;
        SELECT SUBSTR(gid_set, pos + 1) INTO gid_set;
      ELSE
        -- nie znaleziono przecinka, gid_set zawiera wyłącznie cyfrę,
        -- zzutuj ją do zmiennej gid jako
        -- INT i ustaw flagę zakończenia pętli
        SELECT CAST(gid_set AS UNSIGNED) INTO gid;
        SET @done = 1;
      END IF;
 
      -- Można dodać rekord do bazy
      INSERT INTO poema_uid_group (intUid, intGid, intGrantorUid, dtmGranted)
      VALUES (p_id, gid, 1, NOW());
      SET @num = @num + 1;
 
    UNTIL @done END REPEAT;
  END IF;
 
  -- Zwróć ilość dodanych rekordów
  RETURN @num;
END;
//
delimiter ;

Ok, funkcja dodana, teraz wystarczy wywołać ją dla każdego ID użytkownika:

SELECT create_records_in_poema_uid_group(ID) FROM poema_users;

Przy odrobinie inwencji będziesz w stanie wykorzystać funkcję do rozwiązania podobnego problemu w Twojej bazie danych.