Статьи

Широкий Искатель в… errr… PHP

Долгое время не было блога — в эти дни я очень занят поиском в Швейцарии, но сначала захожу, чтобы сказать поздравления команде 2-го издания, а во-вторых, за явное злорадство, представив позднюю реализацию PHP для захватывающего проекта Wide Finder Тима Брея.

Тим поставил простую, но очень сложную задачу; написать приложение, которое определяет топ-10 самых популярных блогов из его журнала доступа Apache. Он должен быть быстрым и читаемым, с подтекстом, иллюстрирующим, как «язык X» справляется с точки зрения параллельной обработки и использования «более широких» (многопроцессорных) систем.

Технически, PHP практически не имеет возможности выполнять вещи параллельно, кроме pcntl_fork — (не пытайтесь делать это на вашем живом веб-сервере — только CLI!), Если вы не учитываете тики , а я надеюсь, что это не так.

Но мы не позволим маленьким деталям остановить нас! Потому что у нас есть curl_multi_exec (), который позволяет нам общаться с несколькими URL-адресами и обрабатывать их ответы параллельно. И просто подумайте, сколько запросов Apache + mod_php может обслуживать одновременно. Идеально подходит для какой-то карты и уменьшить

Итак, сначала картограф;


<?php
if ( isset($argv[1]) ) $_GET['f'] = $argv[1];

if ( !isset($_GET['f']) || !is_readable($_GET['f']) ) {
    header("HTTP/1.0 404 Not Found");
	die("Cant read file ".htmlspecialchars($_GET['f']));
}

$s = isset($_GET['s']) && ctype_digit($_GET['s']) ? $_GET['s'] : 0;
$e = isset($_GET['s']) && ctype_digit($_GET['e']) ? $_GET['e'] : filesize($_GET['f']);

$f = fopen($_GET['f'], 'r');
fseek($f, $s);

$counts = array();
while ( !feof($f) && ftell($f) < $e ) {
	$pattern = '#GET /ongoing/When/dddx/(dddd/dd/dd/[^ .]+) #';
	if ( preg_match($pattern, fgets($f), $m) ) {
		isset($counts[$m[1]]) ? $counts[$m[1]]++ : $counts[$m[1]] = 1;
	}

}
fclose($f);

$out = serialize($counts);
header("Content-Length: ".strlen($out));
echo $out;

Вы указываете на файл, к которому он может получить доступ через свою файловую систему, а также задаете диапазон байтов (начало и конец) для анализа. Он возвращает сериализованный хэш, ключами которого являются заголовки записей в блоге, а значениями — количество вхождений. URL-адрес для вызова может выглядеть как http: //localhost/~harry/wf/wf_map.php? F = / tmp / o10k.ap & s = 257664 & e = 524288. Также для бенчмаркинга он может быть вызван из командной строки для обработки всего файла в одном потоке, например, $ php ./wf_map.php /tmp/o10k.ap

Затем у нас есть редуктор, который фактически вызывает преобразователь (и) с помощью curl;


#!/usr/bin/php
<?php
function wf_reduce($urls) {

	$counts = array();
	
	$mh = curl_multi_init();
	
	foreach ( $urls as $url ) {
		$ch = curl_init($url);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		curl_multi_add_handle($mh, $ch);
	}

	do {
		curl_multi_exec($mh,$running);
		while ( FALSE !==($msg = curl_multi_info_read($mh)) ) {
			
			if ( "200" != ( $status = curl_getinfo ( $msg['handle'], CURLINFO_HTTP_CODE ) ) ) {
				die("HTTP Status: $status!");
			}
			
			// Here we can "reduce" as soon as we get a response...
			foreach ( unserialize( curl_multi_getcontent ( $msg['handle'] )  ) as $page => $hits ) {
				isset($counts[$page]) ? $counts[$page] += $hits : $counts[$page] = $hits;
			}
			
		}
	} while ($running > 0);

	return $counts;
	
}

$mapurl = "http://localhost/~harry/wf/wf_map.php";
$file = "/tmp/o10k.ap";

if (!file_exists($file) ) {
	print "First run: downloading sample data!";
	file_put_contents($file, file_get_contents("http://www.tbray.org/tmp/o10k.ap"));
	die();
}

$partitions = isset($argv[1]) && ctype_digit($argv[1]) ? $argv[1] : 1;

$end = filesize($file);
$partition = floor($end / $partitions);

$s = 0;
$e = $partition;
$urls = array();

while ( $e <= $end ) {
	$urls[] = sprintf("%s?f=%s&s=%s&e=%s", $mapurl, urlencode($file), $s, $e);
	$s = $e;
	$e += $partition;
}

$counts = wf_reduce($urls);
arsort($counts, SORT_NUMERIC);

$top10 = array_slice($counts, 0, 10, TRUE);
print_r( array_reverse($top10) );

Он предназначен исключительно для выполнения командной строки, принимая один аргумент, который представляет собой число параллельных «потоков», которые должны выполняться, например, $ ./wf_reducer.php 10 Вам нужно будет изменить $mapurl

Сомневаюсь, что это получит какие-либо награды за красоту, и тесты с образцом файла Тима не впечатляют — быстрее запустить однопоточный — возможно, вы начнете получать с гораздо большим файлом? Мое желание узнать уменьшается.

Но в любом случае — PHP тоже был там. Интересно, Тим сейчас скомпилирует его на этом T5120? 😉

Обновление: только что обнаруженный Рассел Битти первым пришел с однопоточным решением.