封装php的Log类
来源:互联网 发布:自动语音阅读软件 编辑:程序博客网 时间:2024/05/17 14:18
记录log,对于很多人而言是很简单或者低级的事情。但是,随着项目经验的增长,遇到生产环境中bug数的增多,至少对于我来说,日志的重要性日益增加。
这次,需要对项目中log类进行重构,主要希望实现4个目的:
- 建立日志监控机制,第一时间发现问题
- 协助定位问题,帮助快速解决问题
- 记录用户行为,协助解答客户疑问
- 记录用户行为,协助制定安全与个性化等策略
除了这些功能性的目的,由于log类在一次请求中的调用频率相对较高,且与基本业务无关,如果性能方面有问题的话,就太本末颠倒了,所以先从性能说起。
log一般记录在文件里,所以其本质上是写文件,使用php作为开发语言的话,主要考虑了3个方面:
- 选择fwrite还是file_put_contents?
- 是否使用debug_backtrace函数获取文件名和行号?
- 怎样保证并发写的原子性?
选择fwrite还是file_put_contents?
php有多种写文件的函数,fwrite需要先fopen获取到文件句柄,而file_put_contents直接使用文件名即可,且传入的data参数可以是字符串,也可以是数组,甚至stream,使用较简单。
zend框架的Zend_Log_Writer_Stream类,使用的是fwrite函数;而公司内部多个team的log封装都使用了file_put_contents函数。首先考虑,我们的log类,给谁用?内部使用,暂时没考虑开源。传入的参数,是否需要支持string,array or stream?记录log而已,支持string即可,而且log的基本样式是每次记录一行。所以,比较两者在写入多行数据之间的性能区别即可:
$str = "abc\n";$n = 10000;$filename = 'test_write.txt';$start_time = mytime();write_with_open($filename, $str, $n);$used_time = mytime() - $start_time;printf("%20s%s\n","fwrite","Time used: $used_time ");$start_time = mytime();write_without_open($filename, $str, $n);$used_time = mytime() - $start_time;printf("%20s%s\n","file_put_contents","Time used: $used_time ");$start_time = mytime();write_with_errorlog($filename, $str, $n);$used_time = mytime() - $start_time;printf("%20s%s\n","error_log","Time used: $used_time ");function write_with_open($filename, $str, $n){ $fp = fopen($filename, 'a') or die("can't open $filename for write"); while($n--){ fwrite($fp, $str); } fclose($fp);}function write_without_open($filename, $str, $n){ while($n--){ file_put_contents($filename, $str, FILE_APPEND); }}function write_with_errorlog($filename, $str, $n){ while($n--){ error_log($str, 3, $filename); }}
执行该测试脚本的结果是:
fwriteTime used: 0.018175840377808
file_put_contentsTime used: 0.22816514968872
error_logTime used: 0.2338011264801
可见fwrite的性能要远远大于另外两者,直观上看,fwrite仅关注于写,而文件句柄的获取仅由fopen做一次,关闭操作也尽有一次。如果修改write_with_open函数,把fopen和fclose函数放置到while循环里,则3者的性能基本持平。
以上结论,也可以通过查看PHP源代码得知,fwrite和file_put_contents的源码都在php-5.3.10/ext/standard/file.c里,file_put_contents不但逻辑较为负载,还牵涉到open/锁/close操作,对于只做一次写操作的请求来说,file_put_contents可能较适合,因为其减少了函数调用,使用起来较为方便。而对于log操作来说,fwrite从性能角度来说,较为适合。
2012-06-22补充:
以上只是单纯从“速度”角度考虑,但是在web的生产环境里,如果并发数很高,导致系统的open files数目成为瓶颈的话,情况就不同了!fwrite胜在多次写操作只用打开一次文件,持有file handler的时间较长;而file_put_contents每次都在需要的时候打开文件,写完就立即关闭,持有file handler的时间较短。如果open files的值已无法调高,那么使用file_put_contents在这种情况下,就会是另外一种选择了。
是否使用debug_backtrace函数获取文件名和行号?
在gcc,标准php出错信息里,都会给出出错的文件名和行号,我们也希望在log里加上这两个信息,那么是否使用debug_backstrace函数呢?
class A1{ public static $_use_debug=true; function run(){ # without debug_backtrace if (!self::$_use_debug){ #echo "Quit\n"; return; } # with debug_backtrace $trace = debug_backtrace(); $depth = count($trace) - 1; if ($depth > 1) $depth = 1; $_file = $trace[$depth]['file']; $_line = $trace[$depth]['line']; #echo "file: $_file, line: $_line\n"; }}class A2{ function run(){ $obj = new A1(); $obj->run(); }}class A3{ function run(){ $obj = new A2(); $obj->run(); }}class A4{ function run(){ $obj = new A3(); $obj->run(); }}class A5{ function run(){ $obj = new A4(); $obj->run(); }}class A6{ function run(){ $obj = new A5(); $obj->run(); }}$n = 1000;$start_time = mytime();A1::$_use_debug = true;$obj = new A6();while($n--){ $obj->run();}$used_time = mytime() - $start_time;printf("%30s:%s\n", "With Debug Time used", $used_time);$n = 1000;$start_time = mytime();A1::$_use_debug = false;$obj = new A6();while($n--){ $obj->run();}$used_time = mytime() - $start_time;printf("%30s:%s\n", "Without Debug Time used", $used_time);function mytime(){ list($utime, $time) = explode(' ', microtime()); return $time+$utime;}
运行的结果是:
flykobe@desktop test $ php test_debug_trace.php
With Debug Time used:0.005681037902832
Without Debug Time used:0.0021991729736328
但是若多次运行,少数情况下,with debug版本甚至与快于without版本。综合考虑,还是决定不用debug_backtrace,而由调用者传入__FILE__和__LINE__值。
怎样保证并发写的原子性?
写文件时,大致有这3种情况:
- 一次写一行,中间被插入其他内容
- 一次写多行,行与行之间被插入其他内容
- 多次写多行,行与行之间被插入其他内容
对于web访问这种高并发的写日志而言,一条日志一般就是一行,中间绝不允许被截断,覆盖或插入其他数据。但行与行之间,是否被插入其他内容,并不care。既然之前是决定采用fwrite,php手册上说的很清楚:
If
handle
was fopen()ed in append mode, fwrite()s are atomic (unless the size ofstring
exceeds the filesystem’s block size, on some platforms, and as long as the file is on a local filesystem). That is, there is no need to flock() a resource before callingfwrite(); all of the data will be written without interruption.
即当fwrite使用的handler是由fopen在append模式下打开时,其写操作是原子性的。不过也有例外,如果一次写操作的长度超过了文件系统的块大小,或者使用到NFS之类的非local存储时,则可能出问题。其中文件系统的块大小(在我的centos5虚拟机上是4096 bytes)可以通过以下命令查看:
sudo /sbin/tune2fs -l /dev/mapper/VolGroup00-LogVol00 | grep -i block
这同样可以通过模拟多种不同情况的fwrite操作来验证,由于比较简单不再赘述代码。
————————-
2012-07-03添加
《unix环境高级编程》里说:Unix系统提供了一种方法使这种操作成为原子操作,即打开文件时,设置O_APPEND操作,就使内核每次对这种文件进行读写之前,都将进程的当前偏移量设置到该文件的尾端处。
而php手册中,指的某些platform应该不包含*nix实现。故可认为,在我们的环境下,php的fwrite函数是原子性的。
在以上几种比较的基础上,初步完成了我们的Log类封装,进行了千行和万行级别的log写入测试,性能提高了3倍,但应该仍然有优化余地。
- 封装php的Log类
- 工具类的封装--Log封装
- 封装自己的LOG类
- 封装的log工具类
- android----Log类的封装
- 一个封装 android.util.Log 的Log类
- Cocos2d-x简单的Log类封装
- 好用的自定义Log封装类
- 对log工具类的方法封装
- 工具类的封装-Log日志的封装
- 简单的封装log
- Android Log的封装
- PHP类的封装
- Android Log类封装
- Android Log的简单封装
- php 数据库的封装类
- PHP数据库类的封装
- php 封装的日历类
- cocos2d-x 资源路径研究
- 显式调用构造函数产生的悲剧
- Win7系统下,VS2010关于OpenCV2.0和OpenCV2.4.3的配置
- org.hibernate.ObjectDeleteException
- 初学菜鸟的学习日记——二叉树(JAVA)
- 封装php的Log类
- HDU 4218 IMBA?
- POJ_2312_BFS:priority_queue -- Battle City
- linux查看命令
- Shell编程入门
- UVa 11995 I Can Guess the Data Structure!
- Android调用系统相机拍照后不进入预览确认
- UCDOS点阵字库
- 博客新家