Na pewno sporo osób próbowało swoich sił w stworzeniu skryptu do zliczania odwiedzin na stronie na podstawie logów z Apache’a. W sumie nic trudnego, schemat logów jest w miarę prosty, odczytać i po sprawie. A jak wygląda sprawa przy plikach wielkości setek megabajtów lub gigabajtów ? Trzeba sprytnie to odczytywać linia po linii, przeanalizować i wywalić z pamięci. A jak mamy dwa lub więcej rdzeni w procesorze, to może by tak parę linii na raz analizować ?
Sama zasada jest dość prosta. Odczytujemy mały blok pliku i wyszukujemy gdzie jest znak końca linii. Jeśli nie znajdujemy to doczytujemy jeszcze kawałek. Jeśli już znaleźliśmy to odcinamy nasz kawałek i po sprawie. Przy wyszukiwaniu usuwamy wszystkie znaki powrotu karetki, jak by się znalazł jakiś plik z „enterami” z Windowsa.
$tresc = ''; //definiujemy zmienna
$uchwyt = fopen('pliczek.log', "rb");
$tresc .= fread($uchwyt, 300 ); //dopisujemy do zmiennej kawałek pliku
$tresc = str_replace("\r",'',$tresc);
if( strpos( $tresc, chr(10) ) === false ) {
//doczytujemy jeszcze kawalek i jeszcze raz szukamy i tak aż znajdziemy
} else {
$strpos = strpos( $tresc, chr(10) ); //znajdujemy pozycje entera
$linia = substr($tresc, 0, $strpos); //odcinamy interesujący nas odcinek
$tresc = substr($tresc, $strpos+1, strlen($tresc) ); //i usuwamy odcięty kawałek od pobranej treści wraz z enterem i zostawiamy do następnego odczytu
funkcjaAnalizujaca( $linia ); //możemy coś zwracać lub nie, to jest obojętne.
}
Oczywiście to trzeba ładnie ubrać w pętelkę gdzie będziemy się kręcić aż otrzymamy EOF (End Of File). Oczywiście można to wszystko ubrać w klasę, konfigurowalne zmienne, w dodatkowe zabawki typu zliczanie ilości linii, statystyka czasów analizy poszczególnych linii i inne wesołe rzeczy. Wszystkie takie rzeczy możecie podejrzeć w skrypcie który ja przygotowałem dla własnych celów, gdzie większość tych rzeczy jest już dodana.
Teraz apropo jednoczesnym przetwarzaniu więcej niż jednej linii jednocześnie. Niestety muszę zmartwić wielu z was, działa to jedynie pod systemami *unix oraz jedynie pod konsolą czyli wywoływane z linii poleceń. Windows oraz mod-php dla apache odpadają. To już jest ograniczenie od strony php. Będziemy korzystać z modułu Process Control, który nie jest domyślnie kompilowany do PHP. Żeby nie było nieścisłości to nie jest wielowątkowość ale fork czyli rozwidlenie procesu. Główny skrypt który jest rodzicem, tworzy dziecko które jest jego kopią. U nas tylko dzieci będą analizować pojedyncze linie logów, a rodzic będzie starał się nad tym „przedszkolem” zapanować. Zasady działania forków nie będę tłumaczył, jest Manual, jest Wikipedia no i Google.
Wiec ustalamy sobie maksymalną ilość dzieci. Niestety w zależności od sprzętu możecie zrobić ich więcej lub mniej. Musicie po testować różne ustawienia. Więc przykładowo będziemy pracowali z czwórką dzieci. Jako że procesy się nie komunikują między sobą wykorzystamy funkcję, która obsługuje otrzymane sygnały (pcntl_signal) a dokładniej chodzi nam o sygnał zakończenia procesu dziecka czyli SIGCHLD. Przy tworzeniu dziecka powiększamy licznik o 1 a gdy otrzymamy sygnał zamknięcia pomniejszamy. W ten sposób jesteśmy w stanie jakoś zapanować nad tym wszystkim. a oto przykład (brany gdzieś z manuala ale ten jest w miarę przejrzysty):
$child = 0;
$max=3;
function sig_handler($signo) {
global $child; //zmienna która trzyma ilość "wyprodukowanych" dzieci
switch ($signo) {
case SIGCHLD:
--$child;
break;
//tutaj możemy obsługiwać inne sygnały jak na przykład zamkniecie głównego skryptu, lub dowolnie wybrane przez nas sygnały
}
}
pcntl_signal(SIGCHLD, "sig_handler");
for( $a=0; $a < 20; $a++ ) {
$child++;
$pid=pcntl_fork();
if ($pid == -1) {
die("Nie mozna zrobić dziecka");
} elseif ($pid) {
if ( $child >= $max ) {
echo "Za dużo dzieci w przedszkolu \n";
pcntl_wait($status);
}
} else {
sleep( rand(1,10) );
file_put_contents('./file.log', $a.'.', FILE_APPEND ); //zadanie do wykonania
//dla przykładu wykonujemy zapis do pliku dla każdego dzieciaka, żeby pokazać że to naprawdę działa
// a sleep pokaże ze czasami w różnych kolejnościach będzie to wykonywane, ze względu na rożny czas wykonania zadania
exit;
}
}
Tyle mechanizmu, który czyta plik. Jeśli chodzi o analizę pliku to już zależy co my tam robimy. Jako że kiedyś przymierzałem się do jakiegoś dużego analizatora to wykombinowałem takie małe wyrażenie regularne. Znając życie albo posiada błędy albo można go jeszcze zoptymalizować. Może i kiedyś przy tym usiądę ale chyba nie dziś. A oto ten malutki tasiemiec.
preg_match('/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) ([^\s]+) ([^\s]+) \[(\d{2}\/[a-zA-Z]{3}\/\d{4}:\d{2}:\d{2}:\d{2} [+|-]\d{4})\] "(?:(POST|GET|PUT|DELETE|CONNECT|OPTIONS|HEAD|TRACE) ((?:http:\/\/|\/)?[^\s]*(?:\/[^\/\s]*)?) (HTTP\/\d\.\d))" ([1-5]\d{2}) (-|\d+) "([^"]*)" "([^"]*)"/', $linia, $match);
list($calosc, $ip, $niemampojecia, $http_user, $data, $method, $url, $http_protocol, $http_code, $transfer, $referrer, $user_agent) = $match;
Oczywiście można to po swojemu wykorzystać. Ja osobiście, najczęściej zliczam wykorzystany transfer przez daną domenę. A jeśli chcecie zobaczyć jak tą całą opowieść ubrałem w skrypt to zapraszam do ściągnięcia pliku gdzie jest spakowany skrypt główny oraz przykładowe rozszerzenie do analizy logów ‘Combined’ z Apache.
Należy pamiętać, że skrypt który zlicza transfer z logów, nie powinien działać w trybie „pseudo-wielowątkowy”, ponieważ transfer jest zliczany w pamięci php, czyli jest zapisany w zmiennych. Jeśli ktoś ma ochotę to może przerobić tak skrypt aby zliczał transfer w pliku, w bazie danych lub innym zewnętrznym nośniku.