【PHP问题定位】线上机器打日志混乱问题定位分析

智数幽影
• 阅读 5044

顺风车运营研发团队 黄桃

现象

在线上脚本机器写入单条日志过长时会出现交叉打印的现象:

【PHP问题定位】线上机器打日志混乱问题定位分析
被交叉的日志很有规律,都是单条日志过长被截断的,建议优化下 /*/ruleanalysis.php:68 此处写入日志的字符串长度为: int(25909)

原因分析

脚本服务写入日志代码如下:

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 函数,里面有这样一处代码:

【PHP问题定位】线上机器打日志混乱问题定位分析
明确几个变量的含义:
count:需写入文件的字符串长度
stream->chunk_size :默认为8192 (8k)

从上面代码可以看出,当写入的字符串长度 大于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函数是能保证一次写入的完整的。

所以日志写串的原因也就能分析出来了,调用链接为: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倍数的位置,结果如下:

【PHP问题定位】线上机器打日志混乱问题定位分析

截断的位置非常接近8192的倍数值;但因为定位时间不是当时的时间点,期间数据库存在部分改动,所以出现偏移,那么也能验证我们之前的猜想,正是file_put_contents 多次调用write函数的时候出现交叉打印。

问题解决:

1、修改打日志处代码,这么巨大的日志写入文件是否合理?
2、需要写入巨大日志又不希望不被交叉打印,加上LOCK_EX 标识;

file_put_contents函数相关的知识点问答

1、file_put_contents(filename,msg ,FILE_APPEND ) 末尾追加实现?

FILE_APPEND文件追加的形式,这个是怎么实现的呢?
先标识 mode[0] =‘a’

【PHP问题定位】线上机器打日志混乱问题定位分析

紧接着转换成:O_CREAT|O_APPEND

【PHP问题定位】线上机器打日志混乱问题定位分析

调用 open函数 ,fd = open(realpath, open_flags, 0666);

【PHP问题定位】线上机器打日志混乱问题定位分析

通过 C底层函数保证,写入默认追加写;

2、file_put_contents(filename,msg ,FILE_APPEND|LOCK_EX ) 中的 LOCK_EX实现?作用?

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 保证了一个巨大字符串的完整,不会被写串;

3、多进程,file_put_contents()数据覆盖吗?

write调用路径:file_put_contents ->_php_stream_write_buffer ->php_stdiop_write(多次调用,每次最多写入8192字节) ->write()
file_put_contents($messageLogFile, $strLogMsg, FILE_APPEND);
write函数在O_APPEND模式下,偏移到文件末尾与写文件是原子性的,不存在被覆盖的情况;

4、以O_APPEND方式打开文件,然后使用lseek,定位到文件首部,然后调用write会怎样?是写在文件结尾吗?

还是写在文件尾部,参考文章: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);
}

5、进程内多次file_put_contents,open和close只有一次还是多次?

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

点赞
收藏
评论区
推荐文章
DevOpSec DevOpSec
2年前
限制linux用户访问特定目录-centos沙箱环境
背景研发需求:1.研发想查看线上服务日志2.研发要看线上部署代码是否正确3.研发想看业务运行时产生数据是否正确运维需求:1.运维不想让研发有太多权限2.只有产看某些特定目录的权限和产看日志问题:研发具有太多权限后,对线上风险影响较大,比如研发误操作删文件跑
Bahar76 Bahar76
4年前
用 Go 写个日志监控系统
日志收集系统架构1.项目背景a.每个系统都有日志,当系统出现问题时,需要通过日志解决问题b.当系统机器比较少时,登陆到服务器上查看即可满足c.当系统机器规模巨大,登陆到机器上查看几乎不现实2.解决方案
Stella981 Stella981
4年前
Android开发之,LogCat无故不打印隐藏深坑
最近在项目中遇见一个问题,在日志打印的过程中,发现某些日志不打印出来,这里给大家看看!(https://oscimg.oschina.net/oscnet/up6da5f696705ba41e4be14dfcbd53696d471.JPEG)看到这里
Stella981 Stella981
4年前
Dubbo链路追踪——生成全局ID(traceId)
全局traceId关于链路追踪,在微服务的趋势下,一次调用的日志信息分布在不同的机器上或目录下,当需要看一条链路调用所有的日志信息时,这是个比较困难的地方,我们虽然有ELK,Sentry等日志异常收集分析工具,但是如何把信息串起来也是一个关键的问题。我们一般的做法是在系统调用开始时生成一个traceId,并且它伴随着一
Wesley13 Wesley13
4年前
Java动态追踪技术探究
引子在遥远的希艾斯星球爪哇国塞沃城中,两名年轻的程序员正在为一件事情苦恼,程序出问题了,一时看不出问题出在哪里,于是有了以下对话:“Debug一下吧。”“线上机器,没开Debug端口。”“看日志,看看请求值和返回值分别是什么?”“那段代码没打印日志。”“改代码,加日志,重新发布一次。”“怀疑是线程池的问题,重启会破坏现场。”
Stella981 Stella981
4年前
Linux 日志分析
!CentOSLogo(https://img2018.cnblogs.com/blog/1746090/201908/1746090201908162240176241239542384.png)学会查看日志文件是一件很有意义的事,因为在Linux系统中运行的程序通常会把一些系统消息和错误消息写入对应的日志中,若是一旦出现问题,我们就可以
Stella981 Stella981
4年前
Python日志库logging总结
在部署项目时,不可能直接将所有的信息都输出到控制台中,我们可以将这些信息记录到日志文件中,这样不仅方便我们查看程序运行时的情况,也可以在项目出现故障时根据运行时产生的日志快速定位问题出现的位置。1、日志级别Python标准库logging用作记录日志,默认分为六种日志级别(括号为级别对应的数值),NOTSET(0)、DEBUG(10)
关于「日志采样」的一些思考及实践
一、背景:系统日志可用于追踪用户操作轨迹,异常情况下,合理的日志有助于快速排查、定位问题,毫无疑问,打印日志对于系统是很重要的。当业务规模较小时,大家都倾向于享受日志带来的便利,从而忽略日志带来的潜在的负面影响,缺乏对日志的管控。在JD当前用户量、业务规模
奇怪!应用的日志呢??
1.问题回顾问题背景是在进行中台应用中间件迁移过程中,发现存在项目启动失败或者项目正常启动(jsf正常挂载并正常运行,mq正常发送和消费)但是无任何日志打印现象。更奇怪的是不打印日志竟然是偶发的,在测试环境中多次部署都未出现项目启动但无日志打印情况,而且玄
MySQL 死锁日志分析方法
作者:京东物流张凯引言MySQL死锁是线上经常遇到的现象,但是死锁分析却并不总是件容易的事情,本文介绍MySQL死锁日志的分析方法,帮助研发从日志中快速提取有效信息,从而提高死锁原因分析的效率。死锁介绍触发条件死锁的触发条件包括四个:•互斥•占有且等待•不
HBase Sync功能导致HBase入库性能下降
本文分享自天翼云开发者社区《》,作者:5m问题背景与现象HBase入库慢,regionserver日志中大量打印slowsync。原因分析1.对比正常写入时间段监控,检查HBase服务整体CPU、内存以及NameNodeRPC在异常时间段是否增加;2.检查