相关文章推荐
ps -ef | grep php-fpm ![](https://blog.junphp.com/public/upload/20201224171106_393.png) 发现有好些个进程被堵塞了很长时间,之后就用`strace`命令查看这些进程的跟踪状态 strace -p 进程ID ![](https://blog.junphp.com/public/upload/20201224171218_147.png) ![](https://blog.junphp.com/public/upload/20201224171231_292.png) 看样子像是在循环写什么文件,但又是写入空的,贼奇怪。 然后,可以通过`ll /proc`系统目录,查看各个进程的虚拟内存运行状态,进而得知这个进程现在在干些什么 ll /proc/进程ID/fd ![](https://blog.junphp.com/public/upload/20201224171436_810.png) 奇怪,怎么指向了一个session文件,想不通。 然后`yum install lsof`安装了`lsof`工具,该指令可以查看进程打开的文件,但该工具需要用root身份使用才能看到内存信息。 lsof 文件地址 ![](https://blog.junphp.com/public/upload/20201224171718_259.png) 卧槽,怎么都卡在了session文件读写这里?怎么还有个session触发了独占锁? 一头雾水,然后想到了之前ThinkPHP3.1不是默认开了`session_start();`吗,然后想了想,会不会是某个用户,在客户端异步并发时,产生了session排他锁,同时异步接口又在定时触发,导致一直无法释放锁呢? 因为之前的开发同事都是用的原生`$_SESSION`来读写session,所以就没办法直接在框架配置文件中,修改`session_start();`为不全局开启。 之前的方案是在单个控制器中,通过 public function __destruct() { session_destroy(); 解决了session文件创建过多的问题,但如果请求没正常释放,那`__destruct()`就有概率不会被触发到。 再三思量下没办法,还是决定直接修改框架源码,因为考虑到框架之后也没办法直接升级,所以小改动应该没有太大关系。 在`config.php`应用配置文件中,加入一个路由过滤参数 // +---------------------------------------------------------------------- // | 不需要加载session的路由前缀 - 全转小写 // +---------------------------------------------------------------------- 'SESSION_NO_URL' => array( '/index.php/Order', '/index.php/User', '/index.php/Car', 然后再修改ThinkPHP3.1的框架公告函数库`/ThinkPHP/Common/functions.php`文件中的`session()`方法 原代码第`551`行 // 启动session if(C('SESSION_AUTO_START')) session_start(); 整个框架,当配置`SESSION_AUTO_START`为`true`时,默认开启`session_start();`,所以为了兼容框架的原流程,我们只能读取新加的`SESSION_NO_URL`参数,来过滤掉一些,不想默认打开session的路由,将这行代码,修改成以下代码: // 启动session if(C('SESSION_AUTO_START')) {session_start(); $SESSION_NO_URL = C('SESSION_NO_URL'); $route_url = strtolower($_SERVER['SCRIPT_NAME'].'/'.MODULE_NAME); $session_start_status = true; foreach ($SESSION_NO_URL as $v) { if ($route_url == $v) { $session_start_status = false; break; if ($session_start_status) session_start(); 完成到这里,满心欢喜,TMD,这下应该没问题了吧! 心情愉悦,打开Xshell,清空LOG,重启PHP-FPM,又过1小时,突然Zabbix又彪了一下?What? 这不改了吗,怎么还有? 由于这个BUG出现了2天,前晚运维同事打开了PHP-FPM的status进程监控配置,就过去看了下,发现该功能只能抓取到当前尚还存活用户访问的文件,而且没办法记录到`PHP_INFO`的伪静态路由参数,也就是说,当我们URL为伪静态优化后的`index.php/控制器/操作方法`这类地址时,他只能记录到`/index.php` 又是头疼,这虽然抓到了那个堵塞的进程在什么时候,触发了框架入口文件,但没办法追踪到相关的业务文件,相关的调用栈,还是没办法定位到BUG在哪... 又是再三思量,没办法了,只能改框架的加载流程,在业务代码被初始化,但没被执行的前一步,将其进程ID连同调用栈,一起记录到文件中,等堵塞进程再次发生时,就能知道是哪个文件触发了堵塞的逻辑,毫无底线说干就干。 打开`/ThinkPHP/Lib/Core/App.class.php`文件,该文件是ThinkPHP3.1的框架流程加载文件。我们跳到其中的`exec()`方法,该方法是最终用于加载业务逻辑代码的调度器。 去到`136`行, //执行当前操作 $method = new ReflectionMethod($module, $action); 这行代码就行初始化业务代码,但不执行,实际上它是`new`了`class`,也表示它引入了最终的业务文件,我们只需要在他下面记录整个进程的调用栈就行,在这行代码下面插入以下代码。 // 记录当前进程调用栈 start $php_pid = posix_getpid(); $array = get_included_files(); $array[] = $action.'()'; $array[] = $_GET; $array[] = $_POST; // 注意这里要绝对路径写死,因为该文件最终会被加载到缓存中,如果写相对路径,就是相对于缓存路径了 $php_file = '/var/www/html/phppid/'.date('nd_H').'/'; if (!is_dir($php_file)){ mkdir($php_file, 0777); $php_file .= $php_pid.'.log'; error_log(date('H:i:s', time()).' | '.json_encode($array)."\r\n", 3, $php_file); // 记录当前进程调用栈 end 这下信心满满,就等那偷偷堵塞的家伙自己上门送死了。 不出所料,大概又过了一小时,又一个堵塞的进程出现,终于被上面的代码捕捉到,内容如下(因为同个进程可能触发很多次请求写,所以我们只需要看最后一条,那条就是堵塞的请求调用栈): "/Lib/Action/XXXAction.class.php",// 这个是业务控制器 "/Lib/Action/BackendAction.class.php", // 这个是父类控制器 .......中间省略一堆框架的加载文件 "index()", // 这个是访问的方法 {"work_card":"1","xxxx_id":"65349","_URL_":["XXX","index","work_card","1","xxx_id","65349"]}, // GET参数 [] // POST参数 看着很正常,一个普通请求的代码,没啥特别的,之后尝试着用GET参数,拼接到URL上模拟访问一次,奇迹发生,请求被堵塞也没结果返回,同时刷新页面也没办法刷新到。 打开`/Lib/Action/XXXAction.class.php`的`index()`方法,发现是通过`xxx_id`这个参数,查下了一个用户的资料,并调用PHP内置的`ZipArchive`类,将文件导出到`Zip`文件中。 以下为导出时的业务代码: $zip = new \ZipArchive(); $res = $zip->open($this->gzfilename, \ZipArchive::OVERWRITE); if ($res === true) { $Path = $arrayFile; foreach ($Path as $file) { //这里直接用原文件的名字进行打包,也可以直接命名,需要注意如果文件名字一样会导致后面文件覆盖前面的文件,所以建议重新命名 $aa = explode('/', $file); $new_filename = iconv('utf-8',"gbk//IGNORE", $aa[count($aa)-2].'/'.$aa[count($aa)-1]); $zip->addFile($file, $new_filename); //关闭文件 $zip->close(); //5、判断是否要求输出下载 if($Output === true){ $this->ZipDow($this->gzfilename); 很正常啊,看着没啥问题啊!然后想了下,如果上层没查出文件,又调用了这个生成会怎样,然后去翻了下官方对`ZipArchive`的介绍,发现: 当`ZipArchive`写入的压缩包内容为空时,当我们`close()`后,该扩展会自动把压缩包恢复到最初状态,也就是它会清掉这个压缩包。What? 那你清掉了文件,下面的`ZipDow()`下载怎么办? 看下`ZipDow()`的代码 * 输出压缩包下载 private function ZipDow($url){ sleep(1); clearstatcache(); header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename='.basename($url)); header('Content-Transfer-Encoding: binary'); header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); header('Pragma: public'); header('Content-Length: ' . filesize($url)); ob_clean(); flush();// 刷新内容 $file=fopen($url,"r"); while (!feof($file)){ // 限速发送当前部分文件给浏览者 print fread($file,round($this->download_rate*1024)); flush();// flush 内容输出到浏览器端 usleep(0.5 * 1000 * 1000);// 终端0.5秒后继续 fclose($file);// 关闭文件流 // 下载完成,删除文件 @unlink($url); 略微一看,没啥问题,仔细一看好像有点怪,认真一看不对劲!`while (!feof($file)) `? 这不是读取文件到末尾,就跳出循环吗。那就是说,如果`feof()`为`false`它就一直循环,为`true`才跳出循环,那问题来了,如果`fopen()`时就已经是`false`,那`feof()`就肯定一直都是`false`啊,这不是死循环了? 至此bug解决,将`ZipDow()`改成以下代码即可: private function ZipDow($url){ flush();// 刷新内容 $file=fopen($url,"r"); if ($file === false) return false; sleep(1); clearstatcache(); header('Content-Description: File Transfer'); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename='.basename($url)); header('Content-Transfer-Encoding: binary'); header('Expires: 0'); header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); header('Pragma: public'); header('Content-Length: ' . filesize($url)); ob_clean(); while (!feof($file)){ // 限速发送当前部分文件给浏览者 print fread($file,round($this->download_rate*1024)); flush();// flush 内容输出到浏览器端 usleep(0.5 * 1000 * 1000);// 终端0.5秒后继续 fclose($file);// 关闭文件流 // 下载完成,删除文件 @unlink($url);
文章努力渲染中,请稍等片刻....
 
推荐文章