piątek, 21 maja 2010

Optymalizacja Drupala - modyfikacja rdzenia

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:

  1. optymalizacja kodu (PHP)
  2. 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ć:

  1. derekursywacja procedur
  2. optymalizacja struktur danych
  3. 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

Brak komentarzy:

Prześlij komentarz