Optymalizacja rdzenia ( oraz modułów pobranych z repozytorium )
Z mojego, firmowego doświadczenia wynika, że jest to rozwiązanie o największym potencjale. Przykładów i miejsc optymalizacji można by szukać (i skutecznie znajdować) bardzo dużo, ale w zależności od platformy i konkretnej instalacji rozwiązaniem dającym nam największy zysk będzie prawdopodobnie co innego.
Mimo tego, jest kilka elementów na które warto by zwrócić uwagę niezależnie od platformy działania aplikacji:
- optymalizacja kodu (PHP)
- keszowanie statyczne (SQL)
w nawiasach podane są podsystemy na które dana optymalizacja ma największy wpływ: SQL - baza danych
PHP - szybkość wykonywania kodu
ad. 1 optymalizacja kodu
- wpływa na: szybkość parsowania PHP
- możliwa ogólna poprawa wydajności systemu: niska
Co można poprawić:
- derekursywacja procedur
- optymalizacja struktur danych
- optymalizacja przebiegu programu (czyli przepisanie nieefektywnych pętli i konstrukcji warunkowych)
Potencjalnie najbardziej czasochłonne zadanie, uzasadnione jedynie w przypadku bardzo intensywnego wykorzystania kodu, lub w systemach wymagających bardzo szybkich obliczeń. Nie będę wdawał się w szczegóły odnośnie tego zagadnienia, polecę natomiast książkę Piotra Wróblewskiego: "Algorytmy, struktury danych i techniki programowania", gdzie znajdziemy wiele informacji o różnych typach struktur danych wraz z analizą wydajności. Gorąco polecam!
ad. 2 keszowanie statyczne
- wpływa na: bazę danych
- możliwa poprawa wydajności systemu: duża
Jest to technika wypływająca z samej architektury języka, polegająca na zapisywaniu w lokalnych zmiennych statycznych informacji pobranych z bazy danych.
Fakt że musimy stosować ten mechanizm jest bezpośrednim skutkiem tego, że Drupal z systemami czysto obiektowymi nie ma wiele wspólnego. Jest on napisany i utrzymywany w oparciu o techniki programowania strukturalnego, chociaż sama architektura (i pewne założenia koncepcyjne) posiadają znamiona obiektowości. W środowisku obiektowym posługiwalibyśmy się tzw. modelami
Przykładem zastosowania keszowania statycznego jest funkcja node_load, która jedynie przy pierwszym wywołaniu faktycznie "kręci" bazą danych i systemem hooków, natomiast przy każdym następnym, zwraca wygenerowany obiekt zapisany w zmiennej statycznej.
Jak optymalizować nieefektywny kod pod tym kątem ?
Po pierwsze instalacja modułu devel. Następnie włączenie w nim wyświetlania logów z bazy danych i odpalenie najbardziej newralgicznych ( potencjalnie najbardziej obciążonych, lub posiadających najbardziej rozległe funkcje ) stron w witrynie.
Obserwowane wyniki powinny dać nam bardzo wyraźny sygnał o tym, gdzie występują nadprogramowe zapytania (sam devel podpowie nam zaznaczając liczbę zapytań kolorem czerwonym ),
przykład: ( case z naszej firmowej strony )
Mamy zaznaczone na czerwono dwie pozycje. Są to, w obu przypadkach, identyczne zapytania które zostały wykonane przez system więcej niż raz - dobre miejsca aby przeprowadzić próbę optymalizacji z użyciem zmiennych statycznych. Spójrzmy gdzie zostały one wywołane:
"SELECT * FROM users u WHERE uid = 1"
odpalany jest w funkcji user_load, natomiast druga kwerenda w funkcji cache_get ( czyli z systemu Drupalowego kesza ). W pierwszym przypadku ze zlokalizowaniem funkcji nie ma problemów, w drugim potrzebne będzie małe dochodzonko; zajmę się więc pierwszym zapytaniem.
Funkcja user_load jest niekrótka - nie będę jej zamieszczał w orginale, a jedynie przedstawię swoje uwagi odnośnie potencjalnych problemów z wdrożeniem tej metody optymalizacji i ich rozwiązaniem:
Parametry funkcji
Całkiem oczywisty jest fakt, że jedna, dobra dla każdego wywołania tej funkcji zmienna statyczna nie wystarczy. Możemy mieć różne parametry określające sposób ładowania użytkowników, a więc i prawdopodobnie (choć w tym wypadku na pewno) różne zapytania.
Metoda która rozwiązuje nam ten problem jest prosta, ale wiąże się z nią dodatkowy czas obliczeniowy.
Tworzymy zmienną statyczną będącą tablicą i zapisujemy do jej kluczy parametry wywołania, a do wartości obiekty użytkowników. Klucze obliczamy przy pomocy funkcji serialize ( i tu jest ten minus wydajnościowy ).
Przykładowa implementacja funkcji user_load, z zastosowaniem wyżej opisanej metody:
(wersja 6.16 Drupala)
function user_load($array = array()) {
static $staticCache = array();
// obliczamy klucz w keszu statycznym
$cacheKey = serialize($array);
// jeśli nie posiadamy informacji zapisanej pod tym kluczem
// w keszu statycznym
if(!isset($staticCache[$cacheKey])) {
// tu zapiszemy sobie wynikowy obiekt USER
$res;
// to lecimy z całą procedurą obliczającą obiekt USER
// Dynamically compose a SQL query:
$query = array();
$params = array();
// flaga, dzieki ktorej ominiemy niefajny "return" w środku kodu
$canComputeFlag = TRUE;
if (is_numeric($array)) {
$array = array('uid' => $array);
} elseif (!is_array($array)) {
// w tym miejscu powinnismy wyjsc z funkcji, ale
// wtedy obsluga kesza nie będzie bezproblemowa
// korzystamy więc z canCompute'a
$res = FALSE;
$canComputeFlag = FALSE;
}
if($canComputeFlag) { // wszystko gra
foreach ($array as $key => $value) {
if ($key == 'uid' || $key == 'status') {
$query[] = "$key = %d";
$params[] = $value;
} else if ($key == 'pass') {
$query[] = "pass = '%s'";
$params[] = md5($value);
} else {
$query[]= "LOWER($key) = LOWER('%s')";
$params[] = $value;
}
}
$result = db_query('SELECT * FROM {users} u WHERE '. implode(' AND ', $query), $params);
if ($user = db_fetch_object($result)) {
$user = drupal_unpack($user);
$user->roles = array();
if ($user->uid) {
$user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
} else {
$user->roles[DRUPAL_ANONYMOUS_RID] = 'anonymous user';
}
$result = db_query('SELECT r.rid, r.name FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid
WHERE ur.uid = %d', $user->uid);
while ($role = db_fetch_object($result)) {
$user->roles[$role->rid] = $role->name;
}
user_module_invoke('load', $array, $user);
} else {
$user = FALSE;
}
$res = $user;
}
// ustawiamy obiekt w kluczu kesza statycznego
$staticCache[$cacheKey] = $res;
}
// zwracamy to co mamy pod kluczem
return $staticCache[$cacheKey];
}
Efekt prezentuje się następująco:
Podobną procedurę można stosować z dużym powodzeniem do wielu istniejących w jądrze Drupala funkcji oraz do modułów dodatkowych. Rozumiem zamierzenie twórców, którzy zdecydowali kosztem bazy danych, zachować większą szybkość i czytelność kodu, ale jednak z doświadczenia wynika, że dużo łatwiej zapchać jest bazę niż proces PHP, a każda dodatkowa kwerenda w Drupalu (bo i tak jest ich nie mało), stanowi potencjalne wąskie gardło całego systemu. To jest moim zdaniem droga, która powinna przyświecać gronu twórców Drupala z każdym następnym wydaniem