昨天群里有人问怎么处理超大规模的nginx访问日志(50G),有人回答直接用awk,有的人回答用hadoop,那么到底有谁真的尝试过了呢?
对于小文件,awk自然是十分方便的,一条命令就能得到访问top N的ip,可是很明显内存不够,故这个方法行不通。
对于hadoop,肯定是十分合适的,但是也不是每个公司都会配置hadoop的,既然没有hadoop,我们可不可以自己来实现一个简易版的mapreduce呢?本人昨天还真的试了试。
其实整个过程并不复杂,用fgets扫描整个日志,这个过程是可以开多线程处理的,也可以先切割这个日志文件,然后分到多台电脑上去处理,完了进行一次或多次合并。每扫一行就可以得到一个ip,如果日志不大,自然是可以直接在内存里构建hash表来存储的,但是如果日志大,ip多的话就不行了,我们把ip分为4段,每段当作文件夹名新建4个文件夹,最终的文件是以ip命名的,里面存的就是ip的访问次数,每扫描一次我们去判断一次这个文件是否存在,如果存在就取出这个文件里的文件内容,+1,再覆盖进去,如果不存在,则创建之,内容为1.
这样扫描完了后,得到了一批文件,每个文件存的ip对应的访问数量。现在需要把他们读取出来并排序,然后取出top N就行了,对于取top N,可以采取一个N节点的环形双链表来获得。
当然上述的步骤并非一成不变,可是事实变通,局部可以采取内存硬盘映射而不是纯硬盘处理来加快处理速度,比如最后的排序,我在中间加了一层文件映射文件。我是全部采用php处理的,可以处理,但是过程非常非常慢。
第一步是扫描并归类,我没有分4层,而是把ip转换成一个无符号整数了,然后划了1000个文件夹来存储,文件名就是这个ip转成的整数,代码如下:
<?php
$file = fopen("access.log","r");
function aHash($key) {
$base = \_\_DIR\_\_;
$dir = $base.'/storage';
!is_dir($dir) && mkdir($dir);
$dir .= '/'.$key%1000;
!is_dir($dir) && mkdir($dir);
return $dir.'/'.$key;
}
$arr = array();
while(! feof($file)) {
preg_match('|\[\\d\]{1,3}(\\.\[\\d\]{1,3}){3}|', fgets($file),$tmp);
if(empty($tmp\[0\])) continue;
$key = (int)sprintf("%u", ip2long($tmp\[0\]));
if(is_file(aHash($key))) {
$n = file\_get\_contents(aHash($key));
file\_put\_contents(aHash($key), $n+1);
} else {
file\_put\_contents(aHash($key), 1);
}
}
fclose($file);
然后我中间多加了一步,把所有文件的地址归到一个文件夹里了
function traverse($path = '.') {
$current_dir = opendir($path);
while(($file = readdir($current_dir)) !== false) {
$sub\_dir = $path . DIRECTORY\_SEPARATOR . $file;
if($file == '.' || $file == '..') {
continue;
} else if(is\_dir($sub\_dir)) { //如果是目录,进行递归
traverse($sub_dir);
} else {
file\_put\_contents(\_\_DIR\_\_.'/storage/dir\_list',$path.'/'.$file."\\n",FILE\_APPEND);
}
}
}
//此时生成了一批文件,每个文件文件名就是ip(整数形式),然后遍历每个文件夹,把数据又收归一起
traverse(\_\_DIR\_\_.'/storage');
接下来要做的,就是根据这个dir_list文件里的地址,分别把每个ip和对应的访问次数找出来,分到一个新的文件里,其实这一步是不必要的,可以直接进行分堆排序,但是我还是多分了这一步,
$file = fopen(\_\_DIR\_\_.'/storage/dir_list',"r");
while(! feof($file)) {
$path = trim(fgets($file));
if(!is_file($path)) {
//echo $path,"-\\n";
continue;
}
$iplong = pathinfo($path,PATHINFO_BASENAME);
$n = file\_get\_contents($path);
file\_put\_contents(\_\_DIR\_\_.'/storage/result',$iplong.'_'.$n."\\n",FILE_APPEND);
}
fclose($file);
到现在,ip对应的访问次数就在一个文件夹里了,我们对他们进行排序,然后取出top N就行了,但是既然是取top N,就没必要排序了,我用php模拟的双向循环链表,节点数就是N。代码如下:
function show($head) {
$curr = $head;
$arr = array();
do {
$arr\[\] = $curr->n;
$curr = $curr->next;
} while($curr->sign != $head->sign);
return implode('-', $arr);
//echo "\\n";
}
$head = new stdClass();
$head->ip = 0;
$head->n = 0;
$head->next = null;
$head->pre = null;
$head->sign = uniqid();
$last = $head;
for($i = 1;$i <= 9;$i++) {
$new = new stdClass();
$new->ip = 0;
$new->n = 0;
$new->next = $head;
$new->pre = $last;
$new->sign = uniqid();
$last->next = $new;
$head->pre = $new;
$last = $new;
}
//show($head);
//
//
//
//$curr = $head;
$file = fopen(\_\_DIR\_\_.'/storage/result',"r");
while(! feof($file)) {
$str = fgets($file);
if(empty($str)) {
continue;
}
$arr = explode('_', $str);
$iplong = (int)$arr\[0\];
$num = (int)$arr\[1\];
//如果比尾节点都小,肯定舍去咯
if($num <= $head->pre->n) {
echo '-\[舍去'.$num.'\]'.show($head)."\\n";
continue;
}
//如果比头节点大,则将头指针指向尾指针,然后把尾指针当成头指针,
if($num >= $head->n) {
$head = $head->pre;
$head->n = $num;
$head->ip = $iplong;
echo 'O\[轮转'.$num.'\]'.show($head)."\\n";
continue;
}
//剩下的必然是在中间咯,这个情况好复杂啊......按道理讲,应该是插入,然后舍去尾巴,但是php申请内存malloc也是有消耗的,这样就不必了吧。我们先把尾巴拿出来,然后插入指定的位置。
$tmp = $head->pre;
$tmp->n = $num;
$tmp->ip = $iplong;
$head->pre = $tmp->pre;
$tmp->pre->next = $head;
echo '=状态\['.$num.'\]'.show($head)."\\n";
$curr = $head->next;
$status = 1;
do {
if($num >= $curr->n) {
$tmp->pre = $curr->pre;
$tmp->next = $curr;
$curr->pre->next = $tmp;
$curr->pre = $tmp;
echo '+插入\['.$num.'\]'.show($head)."\\n";
$status = 0;
}
$curr = $curr->next;
} while($status && $curr->sign != $head->sign);
//如果循环完了,临时的那个节点还没归位,则需要把它放到尾部
if($status === 1) {
echo '$归尾\[前\]'.show($head)."\\n";
echo '$归尾\['.$num.'\]'.show($head)."\\n";
}
}
fclose($file);
$curr = $head;
$arr = array();
do {
echo long2ip($curr->ip).'---------'. $curr->n."\\n";
$curr = $curr->next;
} while($curr->sign != $head->sign);
我中间加了几个处理过程的说明,主要是为了调试,实际可以去掉。理论上,这个是可以处理很大的文件的,但是我刚才试了一个4G的日志文件,真尼玛慢啊,主要是第一步,太慢了,实际处理,这个应该先切割,然后开多进程处理甚至多pc处理。题外话,如果要获取某个指定的ip的访问次数咋办呢?那么我们就不能分堆了,我们得分梯次了。我们依次建立几个文件,100,1000,10000,1000000...,然后根据访问次数归类到不同的梯次文件里,然后我们根据一个ip对应的点击次数,只要统计他前几个梯次,以及自身梯次里比他大的就行了,如果数量实际比较多,梯次可以精确到个位的。
以上只是纯文件处理,如果实际内存比较大的话,有部分操作可以在内存操作,这样速度会大大加快。