php打印warning日志引发的core追查
内容
春节期间线上出了两个php-cgi的core,具体追查过程如下:
一、 Core信息
file core.xxx
bug.php-cgi.3611.1296586902: ELF 64-bit LSB core file AMD x86-64, version 1 (SYSV), SVR4-style, from ‘php-cgi’
gdb ~/php5/bin/php-cgi core.xxx
Core was generated by `~/php5/bin/php-cgi –fpm –fpm-config ~/php5/etc/php-fpm.co’.
Program terminated with signal 4, Illegal instruction.
(gdb) bt
#0 0×0000000001000707 in ?? ()
#1 0×00000000006b1402 in zend_hash_destroy (ht=0×7fbffff4f8)
at ~/self/xxx/soft/source/src/php/php-5.2.8/Zend/zend_hash.c:526
#2 0×0000000000732b2e in fcgi_close (req=0×7fbfffd4c0, force=0, destroy=Variable “destroy” is not available.
)
at ~/self/xxx/soft/source/src/php/php-5.2.8/sapi/cgi/fastcgi.c:894
#3 0×0000000000732d24 in fcgi_finish_request (req=0×7fbfffd4c0)
at ~/self/xxx/soft/source/src/php/php-5.2.8/sapi/cgi/fastcgi.c:1248
#4 0×0000000000732d49 in fcgi_accept_request (req=0×7fbfffd4c0)
at ~/self/xxx/soft/source/src/php/php-5.2.8/sapi/cgi/fastcgi.c:944
#5 0×00000000007352b8 in main (argc=4, argv=0×7fbffff698)
at ~/self/xxx/soft/source/src/php/php-5.2.8/sapi/cgi/cgi_main.c:2224
根据堆栈可以看出core发生在php-fpm在accept一个新请求时,在对上一个请求(请求异常终止?)进行资源释放时core掉的,线上的php访问模式是apache+fastcgi+php的模式。一层层堆栈往下看:
1) f 0
已经被写坏了,没有什么有用信息
2) f 1
打印zend_hash_destroy函数的参数
(gdb) p *ht
$5 = {nTableSize = 16779009, nTableMask = 0, nNumOfElements = 16779009, nNextFreeElement = 16779009,
pInternalPointer = 0×1000701, pListHead = 0×1000701, pListTail = 0×1000701, arBuckets = 0×1000701,
pDestructor = 0×1000701, persistent = 1 ‘\001′, nApplyCount = 7 ‘\a’, bApplyProtection = 0 ‘\0′}
PHP HashTbale的数据结构可以上网上搜一下,有很多介绍。这个hashtable已经被写坏了,各个节点指向的内存0×1000701,该内存地址在gdb中都是一个不能访问的内存。依然没有什么有用信息。
3) f 2
查看源码,打印fcgi_close的参数
(gdb) p *req
$6 = {listen_socket = 0, fd = 11, id = 1, keep = 0, in_len = 0, in_pad = 0, out_hdr = 0×0,
out_pos = 0×7fbffffcf8 “\001\003″,
out_buf = “\001\a\000\001\037鳿000\000PHP Warning: simplexml_load_string() [<a href='function.simplexml-load-string'>function.simplexml-load-string</a>]: Entity: line 1: parser error : Start tag expected, ‘<’ not found in /hom”…, reserved = “\001\a\000\001\000\000\000\000\001\a\000\001\000\000\000″, env = {nTableSize = 16779009,
nTableMask = 0, nNumOfElements = 16779009, nNextFreeElement = 16779009, pInternalPointer = 0×1000701,
pListHead = 0×1000701, pListTail = 0×1000701, arBuckets = 0×1000701, pDestructor = 0×1000701,
persistent = 1 ‘\001′, nApplyCount = 7 ‘\a’, bApplyProtection = 0 ‘\0′}}
(gdb) ptype req
type = struct _fcgi_request {
int listen_socket;
int fd;
int id;
int keep;
int in_len;
int in_pad;
fcgi_header *out_hdr;
unsigned char *out_pos;
unsigned char out_buf[8192];
unsigned char reserved[16];
HashTable env;
} *
调用zend_hash_destroy(&req->env)进行销毁的是req的成员env,这个成员变量是一个hashtable,该hashtable已经被上一个请求写坏了,导致新请求在释放上一个请求时core掉。
req->out_buf数组是php-cgi和apache进行交互的内存缓冲区,简单看了一下,目前out_buf中的内容全部为simple_xml_load…这个PHP WARNNING,类似的错误信息出现在out_buf中的原因是PHP需要通过fastcgi协议打印错误信息到apache的error_log中。req->out_pos指针则指向当前buf末尾。
gdb) p req->out_pos – req->out_buf
$2 = 8312
BUF的末尾位置已经超过了声明的大小8192,所以可以判断后面的env成员变量已经在写out_buf的过程中被写坏了。PHP中有一个重要的全局变量sapi_globals,通过阅读PHP源码得知,新请求的sapi_globals请求数据填充在fcgi_accept_request完成之后的init_request_info函数中,所以当前内存中的sapi_globals仍然是上次请求的残留信息
(gdb) p sapi_globals
从数据中得知导致core的罪魁祸首是线上某个功能的URL
二、 fastcgi源码分析
(1) 源码位置
fastcgi源码位置:php5/sapi/cgi/fastcgi.c
cgi_main源码位置:php5/sapi/cgi/cgi_main.c
(2) 结构体介绍
首先关注一下fcgi_request这个结构体
typedef struct _fcgi_request {
int listen_socket;
#ifdef _WIN32
int tcp;
#endif
int fd;
int id;
int keep;
int in_len;
int in_pad;
fcgi_header *out_hdr;
unsigned char *out_pos;
unsigned char out_buf[1024*8];
unsigned char reserved[sizeof(fcgi_end_request_rec)];
HashTable env;
} fcgi_request;
这个结构体贯穿整个fastcgi请求的处理流程。我们这次需要关注的是out_hdr、out_pos、out_buf这三个成员变量,fastcgi对apache交互的缓存使用out_buf数组,缓存写满后就会flush出去。但不管是正常输出,还是错误信息输出,所有类型的输出全部会缓存到同一段out_buf中,而这些内容输出的时候需要写到不同的fd中。所以fastcgi采用的方法是在每一种输出内容前加入一个8字节的fcgi_header
typedef struct _fcgi_header {
unsigned char version;
unsigned char type;
unsigned char requestIdB1;
unsigned char requestIdB0;
unsigned char contentLengthB1;
unsigned char contentLengthB0;
unsigned char paddingLength;
unsigned char reserved;
} fcgi_header;
fcgi_header的用途是用来标示header之后输出的内容长度(类似于Nshead中的body_len的作用)、内容类型等等,每一段内容都是fcgi_header+content这种形式。out_buf中允许缓存多对fcgi_header+content,然后在flush的时候写到apache的不同fd中。req->out_hdr指针用来保存当前buf中正在使用的head地址,req->out_pos指针指向当前BUF的末尾位置,req->out_buf指针指向当前buf的起始位置。
(2) 函数介绍
a. fcgi_write函数
fcgi_write函数会通过判断out_hdr指针对当前buf中的fcgi_header进行检查,如果没有header(即out_hdr指针为空)就会调用open_packet函数插入一个新的header。
req->out_hdr = (fcgi_header*) req->out_pos;
req->out_hdr->type = type;
req->out_pos += sizeof(fcgi_header);
注意:这段代码并没有对out_pos做越界检查,这为之后的数组越界埋下了隐患。
如果遇到一种跟当前head类型不同的输出,则会调用close_packet函数填充当前header中的数据,然后重新开启一个新的header。需要写的内容会写到out_pos指针之后。当out_buf全部写满之后,就会调用fcgi_flush函数把out_buf中的内容写出去。
b. fcgi_flush函数
每次调用fcgi_flush函数首先会调用close_packet函数填充fcgi_header中的数据,并把req->out_hdr指针置为NULL。
问题发生在fcgi_flush函数的异常分支上
close_packet(req);//会导致req->out_hdr指针被置为NULL。
…
if (safe_write(req, req->out_buf, len) != len) {
req->keep = 0;
//这里out_pos = out_buf+8192
return 0;
}
req->out_pos = req->out_buf; //写成功后会重置out_pos
return 1;
}
假如第一次fcgi_flush失败后(失败的原因很多,比如客户端主动断开连接)
这时候三个指针的值分别是:
out_buf = 缓冲区初始
out_pos = out_buf+8192
out_hdr = NULL
如果下一次再调用fcgi_write首先会判断req->out_hdr是否为NULL,由于上次调用失败的fcgi_flush已经把out_hdr指针置为NULL,所以这个地方就会越过out_buf数组下标写一个8字节的fcgi_header。
三个指针的值就变成了
out_buf = 缓冲区初始
out_pos = out_buf+8192+8
out_hdr = out_buf +8192
out_pos的越界就从此开始了。由于目前out_buf仍然是满的,所以会继续调用fcgi_flush函数。而该函数会首先会通过close_packet把req->out_hdr置为NULL。
out_buf = 缓冲区初始
out_pos = out_buf+8192+8
out_hdr = NULL
后续每次调用fcgi_write都会先写一个8字节header,从而进入fcgi_write和fcgi_flush的循环,每次调用fcgi_write都导致out_pos向后越界8个字节。我们core中的out_pos-8192正好是8的整数倍,证明了这个猜想。
(3) 问题分析
fcgi_wrire函数调用fcgi_flush失败后是会return -1的
if (!fcgi_flush(req, 0)) {
return -1;
}
那为什么fcgi_write失败之后,PHP依然会继续调用该函数呢。调用fcgi_wtite的函数有两个地方。
第一个地方是sapi_cgibin_ub_write+ sapi_cgibin_single_write:
函数sapi_cgibin_single_write:
if (fcgi_is_fastcgi()) {
fcgi_request *request = (fcgi_request*) SG(server_context);
long ret = fcgi_write(request, FCGI_STDOUT, str, str_length);
if (ret <= 0) {
return 0;
}
return ret;
}
函数sapi_cgibin_ub_write:
ret = sapi_cgibin_single_write(ptr, remaining TSRMLS_CC);
if (!ret) {
php_handle_aborted_connection();
return str_length – remaining;
}
正常的PHP内容输出调用的是sapi_cgibin_ub_write函数,如果写失败,该函数会直接断开PHP请求。所以问题不会出现在这里。
第二个是地方函数sapi_cgi_log_message
memcpy(buf, message, len);
memcpy(buf + len, “\n”, sizeof(“\n”));
fcgi_write(request, FCGI_STDERR, buf, len+1);
free(buf);
这里没有判断fcgi_write函数的返回值。这个函数的用途是PHP通过fastcgi打印错误信息到apache的error_log中。如果PHP持续的出Warning,没有正常的内容输出。Fcgi_wtite函数就会一直被调用,如果在写的过程中客户端断开连接等原因导致fcgi_flush失败。就会复现上面发现的问题。
分析到这里,问题已经比较明了了。我们出core的请求需要与后端HTTP Service进行27次HTTP交互获取xml数据。假设每次访问请求响应都超时(500ms),解析空的返回结果就会触发simple xml语法解析错误导致出PHP warning。27次交互*2次重试会变为54次HTTP交互。如果全部超时则会触发54次PHP Warning,即需要调用54次fcgi_write。大约30次出错后out_buf就会被写满,然后进行fcgi_flush。如果这时候客户端早已断开连接(用户受不了慢,自己关掉),就会出现out_buf越界的问题。
于是等下一次请求为上一次请求收尸时,杯具就发生了^_^
出core的必要条件有两个:
1. PHP脚本持续触发PHP Warning
出错函数调用的是sapi_cgi_log_message函数。该函数中没有判断fcgi_write的返回值,所以即使flush出错,PHP脚本依然会继续运行。
2. PHP持续出错过程中,客户端主动断开连接。
三、 线下复现
写一个简单的PHP脚本
<?php
$i = 200;
while($i –){
usleep(100000);
$str = ‘afadasdfad >x’;
$xml = simplexml_load_string($str, null, LIBXML_NOCDATA);
}
使用压力工具开启大压力进行访问,等apache进程满了就停掉压力(主动断开连接),然后重新开启压力,后续的新请求就会全部出core。Core的堆栈和线上的core完全一样。
四、 解决方案
方案一:修改fastcgi代码和cgi_main代码
- 修改sapi_cgi_log_message,增加对返回值的判断,出错就断开php连接
- 修改fcgi_flush函数,写失败的情况下重置out_pos到buf的初始位置
if (safe_write(req, req->out_buf, len) != len) {
req->keep = 0;
req->out_pos = req->out_buf;
return 0;
}
虽然该core在理论上很多请求都可能触发,比较容易复现,但该core的触发条件比
较极端,不太容易触发。且修改修改源码的代价过高,不利于后续PHP版本升级。
方案二:线上的php错误信息全部是打印到apache的错误日志中的,其实在php.ini
中可以指定error_log的文件位置,这样就不会调用sapi_cgi_log_message函数了?
为了证实这个猜想,阅读了PHP的出错部分源代码:
PHPAPI void php_log_err(char *log_message TSRMLS_DC)
{
…
/* Try to use the specified logging location. */
if (PG(error_log) != NULL) {
…
if (!strcmp(PG(error_log), “syslog”)) {
php_syslog(LOG_NOTICE, “%.500s”, log_message);
return;
}
…
return;
}
if (sapi_module.log_message) {
sapi_module.log_message(log_message);
}
代码首先会判断error_log配置是否有效,如果该配置存在,则直接打到该配置指向的日志文件中,不再调用SAPI中可能会出问题的sapi_cgi_log_message。
PG(error_log) = core_globals.error_log
之前的core
(gdb) p core_globals.error_log
$3 = 0×0
而gdb attach 一个正在运行的PHP进程(修改了php.ini)
(gdb) p core_globals.error_log
$1 = 0xb66b30 “~/php5/logs/php_error.log”
最后采用了方案二,并将其作为了线上的PHP环境标准。
转自:http://stblog.baidu-tech.com/?p=752
相关文章:
BIZTALK项目中WEB引用WEBSERVICES服务时候报错
近期工作中须要完毕通过BIZTALK完毕调用WEBLOGIC公布的WebServices服务,环境搭建好后,打开VS开发工具新建一个BIZTALK项目,加入WEB引用将对方公布的地址拷贝上去,能够正常浏览到,然后点击加入引用button,这…

百度“知识增强的跨模态语义理解技术”获国家技术发明奖
11月3日,2020年度国家科学技术奖励大会在京举行,百度“知识增强的跨模态语义理解关键技术及应用”获国家技术发明二等奖。 该技术旨在通过构建大规模知识图谱,关联跨模态信息,通过知识增强的自然语言语义表示方法,解决…

Objective C浅拷贝和深拷贝
##浅拷贝 浅拷贝就是对内存地址的复制,让目标对象指针和源对象指向同一片内存空间。如: char* str (char*)malloc(100); char* str2 str; 复制代码浅拷贝只是对对象的简单拷贝,让几个对象共用一片内存,当内存销毁的时候…

我常用的那些linux命令
我常用的那些linux命令 用linux也有些年头了,说来也忏愧,说是有些年头了,其实也还是个不长进的主。记得第一次接触linux是boss跟我说的怎么操作,什么编辑模式,按i,a,o进入编辑模式。在一个黑乎乎…

2021腾讯数字生态大会:腾讯安全聚焦安全共建,护航数字经济发展
11月3日,以“数实融合 绽放新机”为主题的2021腾讯数字生态大会在武汉开幕。在首日的主峰会上,多位腾讯高管及行业领袖、企业家对数字时代如何建设安全底座,发表了看法。 腾讯高级执行副总裁、云与智慧产业事业群CEO汤道生指出,没…

Oauth认证协议
原文地址腾讯QQ第三方登录的实现原理? Oauth当中的角色: 1.Service Provider(服务提供方): 服务提供方通常是网站,在这些网站当中存储着一些受限制的资源,如照片、视频、联系人列表等。这些网站…

“分布式哈希”和“一致性哈希”的概念与算法实现
分布式哈希和一致性哈希是分布式存储和p2p网络中说的比较多的两个概念了。介绍的论文很多,这里做一个入门性质的介绍。 分布式哈希(DHT) 两个key point:每个节点只维护一部分路由;每个节点只存储一部分数据。从而实现整个网络中的寻址和存…

7000 字 23 张图,Pandas一键生成炫酷的动态交互式图表
作者 | 俊欣来源 | 关于数据分析与可视化今天小编来演示一下如何用pandas一行代码来绘制可以动态交互的图表,并且将绘制的图表组合到一起,组成可视化大屏,本次小编将要绘制的图表有折线图散点图直方图柱状图饼图面积图地图组合图准备工作我们…

手把手教你使用zabbix监控nginx
zabbix监控nginx,多亏了容哥(杨容)的帮忙,为了感谢容哥的帮助,写了这篇文章。环境介绍:服务器系统版本:CentOSrelease 6.6 (Final)内核版本:Linux hk_nginx2.6.32-504.3.3.el6.x86_64ZabbixServer版本&…

理解多线程设计模式
多线程设计模式:1.Single Threaded Execution Pattern [同一时刻只允许一个线程操作] 比喻:三个挑水的和尚,只能同一时间一个人过桥,不然都掉河里喂鱼了。 总结:在多个线程同时要访问的方法上加上synchronized关键…

Linux内核之旅
内核模块是Linux内核向外部提供的一个插口,其全称为动态可加载内核模块(Loadable Kernel Module,LKM),我们简称为模块。Linux内核之所以提供模块机制,是因为它本身是一个单内核(monolithic kern…

qq腾讯第三方登陆
html页面:<html> <head> <meta charset"utf-8" /> <title>第三方登录</title> <meta property"qc:admins" content"1541324001721762700063671645060454" /> </h…

如何利用 Python 爬取 LOL 高清精美壁纸?
作者 | 阿拉斯加 来源 | 杰哥的IT之旅 一、背景介绍 随着移动端的普及出现了很多的移动 APP,应用软件也随之流行起来。最近看到英雄联盟的手游上线了,感觉还行,PC 端英雄联盟可谓是爆火的游戏,不知道移动端的英雄联盟前途如何&…

生产环境主从数据同步不了?
生产环境主从数据同步不了?经历过程: 一般我们常常在做主从复制的时候,可能是很少遇到到错误,那都是因为,你做主从基本用的是,本地虚拟机做,或者一些测试环境做。但是当我们把主从复制部署…

用 YOLOv5模型识别出表情!
作者 | 闫永强来源 | Datawhale本文利用YOLOV5对手势进行训练识别,并识别显示出对应的emoji,如同下图:本文整体思路如下。提示:本文含完整实践代码,代码较长,建议先看文字部分的实践思路,代码先…

Linux操作系统中内存buffer和cache的区别
我们一开始,先从Free命令说起。 free 命令相对于top 提供了更简洁的查看系统内存使用情况: $ freetotal used free shared buffers cachedMem: 255268 238332 16936 0 85540 126384-/ buffers/cache: 26408 228860Swap: 265000 …

sort cut 命令的常用用法
sort命令介绍:sort是在Linux里非常常用的一个命令,管排序的,集中精力,五分钟搞定sort,现在开始!1 sort的工作原理sort将文件的每一行作为一个单位,相互比较,比较原则是从首字符向后&…

使用 dockerfile 创建镜像
dockerfile 是一个文本格式的配置文件,可以使用 dockerfile 快速创建自定义的镜像。 dockerfile 一般包含4部分信息:基础镜像信息、维护者信息、镜像操作指令、容器启动时执行指令 创建镜像命令:docker build [选项] 路径,会读取指…

wireshark的使用教程--用实践的方式帮助我们理解TCP/IP中的各个协议是如何工作的
wireshark的使用教程 --用实践的方式帮助我们理解TCP/IP中的各个协议是如何工作的 wireshark是一款抓包软件,比较易用,在平常可以利用它抓包,分析协议或者监控网络,是一个比较好的工具,因为最近在研究这个,…

设计师你们还坐的住吗?2021 PS 进入人工智能 P 图时代
与每年一样,Adobe 的 Max 2021 活动顺利开展。本次活动主要是以产品展示以及其他创新产品。 这个活动最有趣的特点之一是,Adobe 不断将人工智能集成到其产品或是功能中。在过去的几年里,人工智能一直是这家公司不断探索的领域。 与许多其他公…

图像处理之噪声---椒盐,白噪声,高斯噪声三种不同噪声的区别
白噪声是指功率谱密度在整个频域内均匀分布的噪声。 所有频率具有相同能量的随机噪声称为白噪声。白噪声或白杂讯,是一种功率频谱密度为常数的随机信号或随机过程。换句话说,此信号在各个频段上的功率是一样的,由于白光是由各种频率ÿ…

发现一个“佛系记账本”
因为这是一款微信小程序,张小龙大力推崇的“用完即走”完美地适合记账应用。 不用下载、不用安装、不用注册、不用各种授权,只要从微信进入,就能记账,账本只与微信关联。 换手机、换PAD都无所谓,只要登录微信ÿ…

YSLOW法则中,为什么yahoo推荐用GET代替POST?
原文:http://www.cnxct.com/use-get-for-ajax-requests-why/ 背景:上上周五,公司前端工程师培训,提到前端优化的一些技巧,当然不能少了yahoo yslow的优化法则。其中有这么一条“Use GET for AJAX Requests”࿰…

Python 多进程、协程异步抓取英雄联盟皮肤并保存在本地
作者 | 俊欣来源 | 关于数据分析与可视化就在11月7日晚间,《英雄联盟》S11赛季全球总决赛决斗,在冰岛拉开“帷幕”,同时面向全球直播。在经过了5个小时的鏖战,EDG战队最终以3:2战胜来自韩国LCK赛区的DK战队,获得俱乐部…

QT 5.4.1 for Android Ubuntu QtWebView Demo
QT 5.4.1 for Android Ubuntu QtWebView Demo 2015-5-15 目录 一、说明: 二、参考文章: 三、QtWebView Demo在哪里? 四、Qt Creator 3.4.0能打开QtWebView Demo? 五、Qt Creator如何生成AndroidManifest.xml? 一、…

硬改TP-Link WR841N v8刷breed和OpenWrt
找到了以前的路由器,想刷OpenWrt但版本是TP-Link的WR841N v8版,上网查过才知道,是专门面向国内发布的严重缩水版国际版的Flash是4M,内存RAM是32M,国内版是2M/16M,不过论坛上也有人说到手的Flash是4M的。(Op…

Facebook的实时Hadoop系统
原文地址: http://blog.solrex.org/articles/facebook-realtime-hadoop-system.html作者:杨文博Facebook 在今年六月 SIGMOD 2011 上发表了一篇名为“Apache Hadoop Goes Realtime at Facebook”的会议论文 (pdf),介绍了 Facebook 为了打造一…

Ka的回溯编程练习 Part1|整划什么的。。
1 #include<stdio.h>2 int search(int s,int t);3 void op(int k);4 int res[1001]{1},n;5 int main()6 {7 //scanf("%d",&n);8 n10;9 search(n,1); 10 return 0; 11 } 12 int search(int s,int t) //当前数的大小s,个数n 13 …

开发者关心的十个数据库技术问题
作者 | 雷海林 责编 | 田玮靖出品 | 《新程序员》如今,数据库越来越受到业界的广泛关注,许多高校毕业生及资深技术人也逐渐投身于数据库产业。《新程序员002》经过用户、专家调研,收集汇总了十个开发者关心的数据库技术问题,…

使用T-SQL语句操作数据表-更新数据
使用update语句更新表中的数据。也就是修改表中的数据。update语法格式:update <表名> set <列名更新值> [where <更新条件>] 解释:update 是更新数据名, 表明是更新数据set 是必要的, 后面可以紧随多个数据列的…