awk扫描分析nginx访问日志
年底感觉没啥活干,尤其是在年底跳槽到新公司,更是没什么重要的项目可以做。老大布置任务,要做一个平台统计每个站点后台的nginx日志,针对url维度分析访问量,加载时间等。
当时设想是这样的,扫描任务部署在线上nginx机器上,定时执行。用shell脚本分析日志,然后找一台开发虚拟机部署管理后台,从shell脚本curl给php接口上报统计结果。
设想的挺好,但是等shell脚本写完以后放到nginx机器上一跑就吓尿了。一个500M的日志文件,用awk分析完需要1分多钟。期间用top看了下,直接CPU100%,占满了一个CPU的核。
后来同事用php写了一个相同逻辑的脚本,结果出人意料——shell要跑1分多钟的任务,php只需要10几秒。我至今不知道是什么原因,印象中用C写成的awk肯定速度要比php快啊?
还是先记录一下awk的扫描脚本主要思路。
## 省略前面获取日志文件路径,格式化时间等步骤
data=`cat $log_file | awk -v hour=$hour -v day=$day -v urls=$urls -v start_time=$start_time -v end_time=$end_time -v interval=$interval '
BEGIN {
split(urls,url_arr,"|");
arr_name=(day" "start_time); ##2016-01-18 16:00:00;
tmp_name=arr_name;
gsub(/:| /," ",tmp_name);
next_gap=strftime("%H:%M:%S",mktime(tmp_name)+interval); ##16:10:00
}
{
if(match($7,urls)){
##超过一个时间段
row_time=substr($4,index($4,":")+1,8);
if(row_time>=next_gap){
if(row>end_time){
exit; ##直接跳到END
}
arr_name=(day" "next_gap);
tmp_name=arr_name;
gsub(/:| /," ",tmp_name);
next_gap=strftime("%H:%M:%S",mktime(tmp_name)+interval); ##16:20:00
}
for(i in url_arr){
if(match($7,url_arr[i])){
sta[arr_name,url_arr[i],"visit"] ++;
sta[arr_name,url_arr[i],"total_time"] += $11; ##先计算总时间
if(match($9,/^"4[0-9][0-9]"$|^"5[0-9][0-9]"$/)){
sta[arr_name,url_arr[i],"badcode"] ++;
}
}
}
}
}
END {
for(i in sta){ ##i=day+url+visit day+url+time
split(i,key,SUBSEP); ##key[1]=day key[2]=url key[1]=visit
for(k in key){
if(key[3] == "total_time"){
printf("%s|%s %s %f\n",key[1],key[2],key[3],sta[i]);
}else{
printf("%s|%s %s %d\n",key[1],key[2],key[3],sta[i]);
}
}
}
}
'`
## 省略后面curl步骤
首先是-v
选项把shell里的参数赋值给awk变量。多个变量需要多次添加-v
选项。然后BEGIN
里做初始化。每次第一行的主循环里根据实际条件做统计。需要注意的是awk虽然支持关联数组,但是却不支持多维关联数组!
变相使其支持多维数组的常用的方法是将,
放在key中间。这样遍历的时候再使用split(i,key,SUBSEP);
方法,将逗号两遍的东西分隔开来。
比如PHP里的$arr["row"]["col"]=1
,在awk里的存储方式实际上是arr["row,col"]=1
,使用for(i in arr)
找到的i
实际上是诸如"row,col"
这种逗号隔开的字符串的key。这样如果我们想要表示第row行,第col列,值是1
,则应该把"row,col"
这个key用split(key,str,SUBSEP);
处理掉,之后输出第str[1]行,第str[2]列,值是arr[key]
。
说的很混乱。不过以后也不一定用的着。
还有,如果BEGIN和主循环里执行了exit;
awk并不会立即退出,而是跳到END里去执行。