Wychodząc od jednego z Praw Murphy’ego można by wyprowadzić twierdzenie, że programiści aplikacji webowych dzielą się na tych, którzy mieli już problem z przeciążeniem serwerów spowodowanych przez nieoptymalnie napisane skrypty lub źle skonfigurowany serwer oraz na tych, którzy dopiero natkną się na ten problem. Ten artykuł jest dla jednych i drugich.
Objawy przeciążenia są proste do zidentyfikowania – serwisy internetowe ładują się dłużej niż wynika to z jakości połączenia internetowego, występują błędy przekroczenia czasu wykonywania skryptu lub oczekiwania na odpowiedź, sporo narzędzi konsoli zwraca odpowiednie monity, niekiedy również internauci alarmują o rozmaitych problemach z integralnością danych, a w przypadku hostingu współdzielonego, administrator serwera dodatkowo wysyła złowróżbne ostrzeżenia o przekroczonych zasobach.
Problem z wydajnością aplikacji może mieć dwa podłoża – po pierwsze sama aplikacja jest napisana w sposób bardzo słaby i już przy niskich obciążeniach uwydatniają się problemy z tym związane. W drugim przypadku aplikacja może być przygotowana nieźle, jednak w związku ze sporym ruchem w serwisie internetowym i tak występują przeciążenia.
Gdy winna jest nieoptymalna aplikacja, należy przede wszystkim skoncentrować się na jej dopracowaniu. Ten artykuł nie jest jednak na temat sztuki poprawnego programowania. Chcemy zwrócić uwagę na kilka podstawowych działań, które można podjąć w celu usprawnienia całego środowiska. Stworzone skrypty nie są tu bowiem samotną wyspą i całość oprogramowania może mieć istotny wpływ na całościowy efekt. Cechą wspólną wszystkich porad jest to, że koncentrują się one wokół użycia pamięci operacyjnej we wszelakiego rodzaju buforach.
Do zastosowania porad będziesz potrzebował uprawnień administratora, dlatego są to wskazówki głównie dla osób posiadających serwery dedykowane lub VPS, a nie dla użytkowników zwykłego hostingu. Wskazówki adresowane są głównie do programistów PHP, jednak wiele z nich przydadzą się także programistom innych języków zorientowanych na aplikacje webowe.
Rada 1: Usprawnij bazę danych
Jeśli dysponujesz zapasem pamięci RAM, przeznacz go na bazę danych. W przypadku posiadania wielu baz danych, wielu tabel lub nawet jednej tylko bazy, ale z ogromną ilością zawartych w niej danych, zyskasz naprawdę spory skok wydajnościowy. Na wielu serwerach obsługujących większe systemy lub dużą ilość serwisów standardem jest to, że na bazę danych przeznacza się nawet połowę dostępnej pamięci operacyjnej (nie wspominając o wydzielaniu osobnych serwerów).
Jeżeli Twoja baza jest stosunkowo mała (kilkanaście MB) to nie musisz na ten cel rezerwować specjalnie 2 GB RAMu, jednak na pewno warto rozważyć dostrojenie parametrów związanych z pamięcią podręczną i buforami.
Poniżej prezentujemy przykładowe zmiany, które możesz dostosować do swoich potrzeb. Stosownych wpisów dokonaj w pliku {stala}/etc/mysql/my.cnf{/stala} (lub analogicznym – zależy od systemu operacyjnego) w sekcji „mysqld”, a następnie zrestartuj serwer MySQL.
key_buffer = 64M
table_cache = 512
query_cache_size = 256M
sort_buffer_size = 4M
Ważne jest przy tym, aby dostosować {stala}sort_buffer_size{/stala} do maksymalnej przewidywanej ilości połączeń do bazy danych, ponieważ wartość ta odnosi się do każdego połączenia z osobna. Jeśli więc zezwolisz na 1000 połączeń, możesz spodziewać się, że parametr ten zabierze łącznie nawet 4GB RAMu. Konieczne może być wówczas zmniejszenie jego wartości lub zmniejszenie ilości akceptowanych połączeń.
Możesz również po pewnym czasie podejrzeć zużycie pamięci „cache”:
show status like 'qcache%';
Dzieki wynikowi tego polecenia zorientujesz się w jakim stopniu cache zapytań jest wykorzystywany, czy warto go zwiększyć czy można zmniejszyć.
Na koniec jeszcze jedna uwaga. Skoro już zwiększasz bufory to zadbaj o to by właściwie wykorzystać nowe ustawienia bazy danych. Staraj się przerzucać obróbkę danych na silnik bazodanowy, który poradzi sobie z tym prawdopodobnie lepiej niż PHP czy inny język programowania. Dlatego złożone podzapytania i procedury składowane będą mile widziane (oczywiście napisane w sposób rozsądny).
Samą aktywację przechowywania wyników zapytań wykonujesz tutaj:
query-cache-type = 1
Pamiętaj, że w przypadku dokonywania aktualizacji danych (operacje INSERT, UPDATE itp.), pamięć podręczna w odpowiedniej części zostanie wyczyszczona. Jeśli operacji odczytu i zapisu dokonujesz z równą częstotliwością to ustawienie query-cache-type na 0 (wyłączenie zapamiętywania wyników zapytań) może przyspieszyć wykonywanie zapytań. Wyłączenia możesz również wykonać na poziomie sesji (danego połączenia), dla tych zapytań, dla których nie jest ono wskazane.
Nie zapomnij też aktywować slow_log i podglądać, z którymi zapytaniami jest problem:
log-slow-queries=/var/log/mysql_slow
Rada 2: Cache na poziomie aplikacji
Przy konstruowaniu dużych serwisów o sporej odwiedzalności warto wdrożyć dobre praktyki stosowane przez największe portale. Mówią one, że należy cache’ować na poziomie aplikacji tak wiele, ile się tylko da. Nawet jeżeli strona jest generowana w części dynamiczne (np. zmieniające się reklamy lub fragmenty spersonalizowane pod użytkownika) to spora część zawartości w określonym przedziale czasu jest niezmienna. Podziel swoją stronę na moduły i cache’uj je.
Mamy na myśli takie przygotowanie aplikacji, aby gotowy kod HTML z danym modułem lądował w pliku, a jeszcze lepiej – w bazie danych.
W ten sposób gotowa strona będzie sklejana z przetworzonych kawałków z dodaniem elementów, które muszą być przygotowane dla tej konkretnej odsłony. Jeśli jakaś akcja powoduje zdezaktualizowanie danych (np. dodanie nowego artykułu lub nowy komentarz zmienia zawartość strony głównej) to do metod obsługujących tę akcję należy dopisać czyszczenie tych elementów w pamięci podręcznej, na które zmiana ta wpływa.
Prostszą metodą jest ustawienie ważności czasowej pamięci podręcznej na adekwatny do częstotliwości aktualizacji odcinek czasu. Po upłynięciu zdefiniowanego czasu, strona lub jej fragment zostaną wygenerowane ponownie.
Rada 3: Nie bój się korzystać z crona
Są takie aplikacje. które przy każdym zapytaniu wykonują skomplikowane obliczenia lub operacje, choć wcale nie jest to konieczne w tej właśnie chwili. Spowalnia to wysłanie odpowiedzi do użytkownika oraz powoduje utratę zasobów, często w mało dogodnym momencie.
Jeżeli na przykład przyjmujesz zamówienie w systemie sklepowym i w związku z tym twój skrypt musi połączyć się z zewnętrznymi systemami (np. wysłać zamówienie do systemu księgowego lub zapisać zamówienie w systemie ERP), a połączenie z nimi długo trwa to rozważ czy koniecznie musisz robić to od razu, przed wysłaniem odpowiedzi do przeglądarki.
Czasem lepiej zapisać w bazie informacje o zdarzeniu i uruchamiać z crona skrypt, który będzie wykonywać te zadania hurtem, dla wszystkich zgłoszeń, które nadeszły od poprzedniego wykonania operacji.
Dodatkową zaletą takiego podejścia jest możliwość ustawienia obniżonego priorytetu w systemie dla takiego procesu, dzięki czemu procesy generujące strony dla użytkowników będą uprzywilejowane.
Inne zastosowanie crona to periodyczne wykonywanie obliczeń w miejsce wykonywania ich każdorazowo. Czy w każdym zapytaniu musisz wykonywać zliczanie z grupowaniem w obrębie kilku tabel naraz, gdzie każda posiada dziesiątki tysięcy rekordów tylko po to, aby wyświetlić listę towarów w twoim sklepie internetowym z częściami elektronicznymi? Może lepiej byłoby wykonać raz na 10 minut aktualizację tej listy poprzez skrypt odpalany z crona i zapisać wynik w pamięci podręcznej (np. plik statyczny, baza danych lub opisywany dalej mechanizm „memcache”) ?
Rada 4: Rozważ zalety RAM-dysku
Standardem przy przetwarzaniu danych jest korzystanie z systemów bazodanowych. Mają one bowiem własne mechanizmy zarządzania pamięcią RAM, co znacznie przyspiesza wykonywanie operacji. Niekiedy jednak sama baza danych nie wystarcza i konieczne jest sięganie do danych pobieranych i zapisywanych do plików. Drastycznie wpływa to na wydajność aplikacji, ponieważ operacje IO potrafią być słabym ogniwem całego systemu.
Jeśli masz taką sytuację, że przetwarzasz w swoich skryptach dużo plików (np. XML), rozważ zastosowanie tzw. „RAM-dysku”. W systemach unixowych możesz łatwo podmontować fragment pamięci RAM w taki sposób, że będzie on widoczny jak zwykły dysk. Odczyt i zapis takich danych będzie jednak bez porównania szybszy.
Takie rozwiązanie ma jednak dwie wady. Po pierwsze, jesteś ograniczony wielkością wolnej przestrzeni w pamięci operacyjnej (sytuacja, w której RAM-dysk trafia do swapu nie ma sensu). Po drugie zaś, zawartość RAM-dysku znika po odłączeniu zasilania. Jeśli wykonujesz z niego tylko operacje odczytu to nie ma problemu, wystarczy przy starcie systemu przekopiować dane z dysku fizycznego do RAM-dysku i niczym się nie przejmować. Jednak jeżeli oprócz odczytu także zapisujesz dane, zadbaj o częstą synchronizację danych z rzeczywistym dyskiem (np. poprzez skrypt cron) i zaakceptuj możliwość utraty danych zmodyfikowanych od czasu ostatniej synchronizacji.
Rada 5: Odkryj potęgę serwera MemCached
Także podczas samego pisania aplikacji PHP możesz łatwo korzystać z dobrodziejstw przechowywania struktur danych w pamięci RAM. Będziesz do tego potrzebować modułu PHP o nazwie „memcache” oraz uruchomionej instancji serwera memcached. Zobacz przykładowy program, który pokazuje jak proste jest korzystanie z tego narzędzia:
connect('127.0.0.1') or die("Błąd połączenia");
// Pobieramy dane z memcached
$dane = $memcache->get('dane');
// Czy dane są już w cache?
if ($dane)
{
echo "{$dane['ip']}
{$dane['time']}
";
}
else
{
// Tworzymy tablicę z danymi (można użyć dowolnej struktury danych)
$dane = Array( "ip" => $_SERVER['REMOTE_ADDR'], "time" => time() );
// Zapisujemy zmienną $dane do klucza "dane"
// Bufor będzie mieć czas "ważności" 30 sekund, po tym czasie
// mecached usunie klucz "dane"
$memcache->set('dane', $dane, false, 30) or die ("Problem z zapisem danych");
echo "Odśwież stronę, aby zobaczyć zapisane dane
";
}
?>
Program nie wymaga dużego komentarza. Przy pierwszym wejściu na taką stronę, do serwera memcache zapisywana jest tablica {stala}$dane{/stala}. Będzie ona widoczna pod kluczem o nazwie „dane”. Dodatkowo ustawiony został czas ważności danych w tym kluczu na 30 sekund. Jeśli wejdziesz na stronę po raz kolejny w ciągu tego odcinka czasu, zostanie wyświetlona pobrana z memcached zawartość klucza „dane”. Po 30 sekundach klucz zostanie zastąpiony przez nową tablicę i znów przez 30 sekund będzie wyświetlana jej zawartość z pamięci RAM.
Ze względów bezpieczeństwa pamiętaj, że stosując memcached, każdy twój skrypt ma swobodny dostęp do zapisanych w nim danych. Dlatego pojedyncza instancja serwera memcached raczej nie powinna być używana wspólnie przez kilku użytkowników.