顺风车运营研发团队 黄桃php
在线上脚本机器写入单条日志过长时会出现交叉打印的现象:mysql
被交叉的日志颇有规律,都是单条日志过长被截断的,建议优化下 /*/ruleanalysis.php:68 此处写入日志的字符串长度为: int(25909)sql
脚本服务写入日志代码以下:数据库
if ($this->isCli == true) { return file_put_contents($messageLogFile, $strLogMsg, FILE_APPEND); //在调用file_put_contents 方法写入,为何在写入超长字符串是交叉写呢? //跟进下file_put_contents函数的实现? }
查看file_put_contents 的源码实现,最终写文件会执行到_php_stream_write_buffer 函数,里面有这样一处代码:json
明确几个变量的含义:
count:需写入文件的字符串长度
stream->chunk_size :默认为8192 (8k)vim
从上面代码能够看出,当写入的字符串长度 大于8192时,则拆为屡次<=8192的字符串,而后调用php_stdiop_write函数写入文件,php_stdiop_write函数实现以下:并发
static size_t php_stdiop_write(php_stream *stream, const char *buf, size_t count) { php_stdio_stream_data *data = (php_stdio_stream_data*)stream->abstract; assert(data != NULL); if (data->fd >= 0) { #ifdef PHP_WIN32 int bytes_written; if (ZEND_SIZE_T_UINT_OVFL(count)) { count = UINT_MAX; } bytes_written = _write(data->fd, buf, (unsigned int)count); #else int bytes_written = write(data->fd, buf, count); #endif if (bytes_written < 0) return 0; return (size_t) bytes_written; } else { #if HAVE_FLUSHIO if (!data->is_pipe && data->last_op == 'r') { zend_fseek(data->file, 0, SEEK_CUR); } data->last_op = 'w'; #endif return fwrite(buf, 1, count, data->file); } }
php_stdiop_write 则调用的 write函数 写入文件;write函数是能保证一次写入的完整的。app
因此日志写串的缘由也就能分析出来了,调用连接为:file_put_contents ->_php_stream_write_buffer ->php_stdiop_write(屡次调用,每次最多写入8192字节) ->write(),是在 屡次调用php_stdiop_write 函数时出的问题;第一次写完,紧接着在高并发的状况下,被其余进程的 write 函数追着写,此时就出现写串,也就是前面示例中日志;函数
为了证明此观点,我对报错的代码 /**/ruleanalysis.php:68 写了以下伪代码:高并发
public function run() { $this->mysqlConnect(); $sql = "select * from *** where ***=1"; $pidRet = $this->db->run($sql); Utils\Logger::notice('tiger_project_info:'.json_encode($pidRet)); die; } vim ~/*****/logger.php if ($this->isCli == true) { var_dump(substr($strLogMsg ,16084 ,400 )); //在字符串的8192倍数的位置打出附近的字符串 16384 = 8192 * 2 return file_put_contents($messageLogFile, $strLogMsg, FILE_APPEND); }
执行代码看打串日志的地方是否为8192倍数的位置,结果以下:
截断的位置很是接近8192的倍数值;但由于定位时间不是当时的时间点,期间数据库存在部分改动,因此出现偏移,那么也能验证咱们以前的猜测,正是file_put_contents 屡次调用write函数的时候出现交叉打印。
一、修改打日志处代码,这么巨大的日志写入文件是否合理?
二、须要写入巨大日志又不但愿不被交叉打印,加上LOCK_EX 标识;
FILE_APPEND文件追加的形式,这个是怎么实现的呢?
先标识 mode[0] =‘a’
紧接着转换成:O_CREAT|O_APPEND
调用 open函数 ,fd = open(realpath, open_flags, 0666);
经过 C底层函数保证,写入默认追加写;
file_put_contents在调用_php_stream_write_buffer 前加一个锁 php_stream_supports_lock(stream) ->flock()
获得文件锁定后 调用_php_stream_write_buffer->屡次 write();
写完后释放文件锁
php_stream_close(stream)->close(data->fd); //直接关闭
总结:LOCK_EX 保证了一个巨大字符串的完整,不会被写串;
write调用路径:file_put_contents ->_php_stream_write_buffer ->php_stdiop_write(屡次调用,每次最多写入8192字节) ->write()
file_put_contents($messageLogFile, $strLogMsg, FILE_APPEND);
write函数在O_APPEND模式下,偏移到文件末尾与写文件是原子性的,不存在被覆盖的状况;
仍是写在文件尾部,参考文章:https://blog.csdn.net/dog250/...
write函数代码以下:
+static inline loff_t file_pos_read_lock(struct file *file) { + if (file->f_mode & FMODE_LSEEK) + mutex_lock(&file->f_pos_lock); return file->f_pos; } +static inline void file_pos_write_unlock(struct file *file, loff_t pos) { file->f_pos = pos; + if (file->f_mode & FMODE_LSEEK) + mutex_unlock(&file->f_pos_lock); } 修改sys_write系统调用: file = fget_light(fd, &fput_needed); if (file) { - loff_t pos = file_pos_read(file); + loff_t pos = file_pos_read_lock(file); ret = vfs_write(file, buf, count, &pos); - file_pos_write(file, pos); + file_pos_write_unlock(file, pos); fput_light(file, fput_needed); }
open调用路径:file_put_contents->php_stream_open_wrapper_ex->php_plain_files_stream_opener->php_stream_fopen_rel->fd = open(realpath, open_flags, 0666);
close调用路径:file_put_contents->php_stream_close->php_stdiop_close->ret = close(data->fd);
每次都会执行 open和close