分类: Python/Ruby
2011-06-20 09:41:52
问题背景:
一个新闻网站,其新闻的URL如下形式:
想要每天早上能得到前一天网站新闻的点击情况(PV和IP),以方便根据浏览者的喜好对新闻内容做出调整,新闻都已经是静态化成“.shtml”格式的文件。最一般的情况,可以在每个文章的静态文件里加入类似下面的代码来完成计数统计:
然后在相应的PHP文件里完成PV和IP的入库工作。
不过因为网站访问量巨大,数据库无法承受每次浏览时更新PV和IP数据的压力,所以需要换一种流量统计的方法。
解决思路:
首先,调整Apache日志的记录格式:
LogFormat "\"%h\",\"%U\"" NewsLog
这里,我为了简单,只记录了主机地址(%h)和文件路径(%U),实际为了更准确实用,你往往还需要加入别的设置。具体参数格式可以参考如下链接:
然后,我们还需要告诉Apache应该记录哪些访问的日志,免得记录了我们不想要的东西。
根据我们的URL样式:
SetEnvIf Request_URI "^/archive/" NewsEnv
具体的匹配模式不用写的分毫不差,只要根据客观情况能让Apache做出正确的判断就可以了,这样也可以提高效率。经过这样的配置后后日志就只会记录
的日志,而不会记录类似
的日志,这样保证了结果的客观性。
然后我们还需要在相应的VirtualHost里配置CustomLog选项,加入我们上面设定的规则,具体如下:
CustomLog "|/usr/local/sbin/cronolog /path/to/%Y/%m/%d/news" NewsLog env=NewsEnv
这里面加入cronolog是为了把Apache日志自动按年月日归档,方便统计。
现在准备工作基本就做好了,重启一下Apache日志就会按照我们要求的格式记录了,其结果可以入库,然后利用SQL来分析就方便多了,比如“SELECT COUNT(*) ...”就是总的PV数,而“SELECT DISTINCT ip ...”就是IP数,是不是方便多了,那这些日志记录怎么入库呢?是不是要先用PHP来分析日志文件,然后进行字符串处理组成SQL再入库?当然不是,那样的话这篇文章也就没有意义了,要说如何入库,还要先返回头来看看前面我们记录的日志格式:
LogFormat "\"%h\",\"%U\"" NewsLog
如此记录是有原因的,这是因为MySQL本身有一个“LOAD DATA INFILE”语法,它可以把符合格式的文本数据存入数据库,而其速度是相当高的,单纯的INSERT根本没法和它比。相应的,MySQL还提供了一个mysqlimport工具来实现命令行端的“LOAD DATA INFILE”语法,这样,我们就可以直接编写shell脚本用mysqlimport来完成日志的入库了,跳过了PHP分析日志的步骤,从而提高的效率。随便设定一下crontab,在系统最轻闲的时候,比如凌晨,跑一下前一天的数据入库,哈哈,苦难结束了!
回过头来,解决方法并没有新奇之处,只是我们的头脑里始终有一个惯性思维:Apache日志应该用PHP或者PERL什么的分析一下,然后再存入MySQL库,却不料这样反而割裂的Apache和MySQL的关系。
注意:
在大量数据入库的时候应该先在全部数据入库后再建索引,这样可以插入数据的提高速度。
其实也可以不入库,只是使用shell分析(wc -l, sort -nr, uniq -c...)
总结:
这里说的解决思路主要适用于非即时数据的统计,如果你要的是即时数据,那么上面的是否适合你呢?答案是要看你的即时性要求有多强,如果可以允许一小时一更新的话,那我们就可以把Apache的日志文件利用cronolog设定成每小时一个,然后每小时执行一次cron来更新数据,如果必须是绝对意义上的即时数据,那上面的方法就不适合你了,不过其实解决方法也很简单,只要你明确了瓶颈在哪里就容易了,在这样的问题背景中,系统瓶颈无疑是在大访问量下无法承受的数据库,只要我们能降低它的负载问题就迎刃而解了。最常见的做法是利用memcached这样的缓存系统,然后在更新PV的时候,不是直接更新数据库,而是先更新缓存,当缓存里的计数达到一个指定的数值后,再统一完成一次数据库更新,比如说,某篇新闻在memcached里的访问记录数达到50我们才做一次数据库的更新,同时把memcached里的计数清零,这样的话,数据库的负载就降低为原来的1/50,世界又变得美好了。
不过这里有一个小瑕疵,就是如果我们硬性的规定缓存计数达到50才更新一次数据库的话,那么当我们浏览新闻的时候,会发现新闻的PV都是一些类似“50, 100, 150...”这样的50的倍数,会显得不真实。所以,在设定memcached里计数达到多大才更新数据库时,一般选择一个随机数,比如,每次判断是否更新数据库的时候,先产生一个50~100之间的随机数,然后再判断memcached里的计数是否大于这个随机数,是的话就更新数据库,否的话就更新缓存。这样PV就会是一个很自然的数了。如果你想得到即时的数据,只要把数据库中的计数和缓存中的计数相加就可以了。
你可以将以下内容放到Apache配置文件中,更改Apache日志格式,使MySQL更容易读取:
LogFormat \ "\"%h\",%{%Y%m%d%H%M%S}t,%>s,\"%b\",\"%{Content-Type}o\", \ \"%U\",\"%{Referer}i\",\"%{User-Agent}i\""要想将该格式的日志文件装载到MySQL,你可以使用以下语句:
LOAD DATA INFILE '/local/access_log' INTO TABLE tbl_nameFIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' ESCAPED BY '\\'所创建的表中的列应与写入日志文件的LogFormat行对应。