Code前端首页关于Code前端联系我们

Opcache 导致 PHP 在线服务异常的案例

terry 2年前 (2023-09-24) 阅读数 67 #后端开发

一、背景

2021-05-13 14:10 服务上线后,开始出现大量“Call to undefined method”错误。通过Grafana,通过查看日志文件,可以发现以下发现

Opcache导致线上PHP服务异常的案例分析

  • 错误发生在5台服务器上
  • 每台服务器上报的错误持续时间约为3秒

2。问题排序

1.上线时间轴

  • 14:10:50 登录上线
  • 14:10:57 数据准备完成
  • 14:10:57 开始上线
  • 14:10:57 同步目录文件通过并行 rsync 与目标服务器
  • 14:11:26 第一个服务器同步已完成
  • 14:11:41 最后一个服务器同步已完成
  • 14:11:41 在线完成

2.服务器 1 时间线

  • 14 :11:35 代码与服务器同步完成
  • 14:11:36 第一个 bug 报告开始
  • 14:11:37 最后一个 bug 报告结束(耗时 1第二,共220个帖子)

3。服务器 2 时间线

  • 14:11:35 代码同步到服务器完成
  • 14:11:35 第一个 bug 报告开始
  • 14:11:38 最后一个 bug 报告结束(耗时 3 秒,237 条消息)总共)

4。服务器 3 时间轴

  • 14:11:35 代码同步到服务器完成
  • 14:11:36 第一个 bug 报告开始
  • 14:11:40 最后一个 bug 报告结束(耗时 4 秒,700 条)共)

5,服务器 4 时间线

  • 14:11:39 代码同步到服务器完成
  • 14:11:45 第一个 bug 报告开始
  • 14:11:45 最终结束Bug 报告(耗时 1 秒,共 196 个)

6,服务器 5 时间轴

  • 14:11:28 代码与服务器同步完成
  • 14:11:30 第一个报错开始
  • 14:11:32 最后一个报错结束(持续2秒,共399条信息) )

3.问题分析

1.完整错误消息

错误消息是调用了一个不存在的方法

{
    "logtime":"2021-05-13 14:11:45",
    "Mode":"fpm-fcgi",
    "Msg":"Call to undefined method app\\xxx\\services\\xxx\\XXXService::doSomething()",
    "Trace":"\/home\/xxx\/xxxxxx\/xxxx.php(68)\n#0 {main}",
    "Uri":"\/xxx.php?xxxxxxxxxxxxxx",
    "Clientip":"xx.xx.xx.xx"
}

2。问题混乱

根据打印的调用堆栈,发现在xxx.php。从当时的Git提交记录来看,方法doSomething()是存在的。也就是说,这个方法不被发现也没有问题。 ? ,调用者的文件代码已同步,但包含方法 doSomething() 的文件尚未同步

(2) opcache 问题

两个文件都与目标机同步,但是当Zend引擎解析代码,opcache有如下分布情况

Opcache导致线上PHP服务异常的案例分析

4。根本原因分析

(1) 在线问题

看在线脚本,预计上传项目大约需要 32 秒。线上平台日志显示,从上线到所有机器同步完成,耗时30秒。

  • 问题时间线中,文件上传完成后开始出现错误信息
  • 实际上传时间超过30秒,与预估的32秒基本一致

从以上两个结论我们可以排除网上的问题,即代码文件。确实一切都正常同步了

(2)opcache问题

①opcache伪代码

$now = time();
if( isOpcached(file) ){
  
    // check opcache code
    if( $now - file.lastUpdatedTime < revalidate_freq ){
      
        // 读opcache
        return getOpcache(file);
    }
}

// 重新解析PHP文件
$result = reParse(file);
writeOpcache(file, $result);
return $result;

②opcache执行原理

③opcache中文件的最后更新时间不均匀

因为同步代码文件,到达服务器需要30秒,所以opcache中每个文件的最后更新时间会不均匀

Opcache导致线上PHP服务异常的案例分析

④ 结论

因为opcache中每个文件的最后更新时间会不均匀,所以会出现以下情况

  • 发送引擎检查A文件的opcache时,发现缓存已经过期,所以会解析新的A文件
  • B文件在opcache中的最后更新时间是很近的,即也就是说,opcache中的B文件。内容还在有效期内
  • Zend引擎直接读取opcache中B文件的内容,但是这个内容是旧的

理论上会存在上述场景,但需要测试复现。如果可以重现,则可以确认这是最后一次缓存的 AB 文件更新。无需重新解析B文件

4. 重现opcache导致PHP错误

1.测试准备

(1) opcache 配置

opcache 配置如下,有效期为 5 秒

Opcache导致线上PHP服务异常的案例分析

(2) 7 个测试脚本

  • online.sh:在线脚本,用于模拟在线actions
cat TestController.php > /home/TestController.php
cat TestService.php > /home/TestService.php
  • rollback.sh:回滚脚本,用于模拟回滚操作
cat TestControllerOld.php > /home/TestController.php
cat TestServiceOld.php > /home/TestService.php
  • TestControllerOld.php:旧的Controller文件,即回滚后的Controller内容(调用getOpcacheStatus1方法)
<?php

class TestController {

    public function test(){
         phpinfo();
    }

    public function test2(int $number){

	// opcache_invalidate(__FILE__, true);
        $opcache = TestService::getInstance()->getOpcacheStatus1();
	      $this->result = [
	        'number' => $number,
	        'opcache' => $opcache,
    	    'time' => date('Y-m-d H:i:s'),
        ];

    }
}
  • TestController.php:新的Controller文件,即上线后的Controller内容(调用getOpcacheStatus2方法)
<?php

class TestController {

    public function test(){
         phpinfo();
    }

    public function test2(int $number){

	// opcache_invalidate(__FILE__, true);
        $opcache = TestService::getInstance()->getOpcacheStatus2();
	      $this->result = [
	        'number' => $number,
	        'opcache' => $opcache,
    	    'time' => date('Y-m-d H:i:s'),
        ];

    }
}
  • TestServiceOld.php:旧服务文件,即回滚后的Service内容(不调用getOpcacheStatus2方法)
<?php

class TestService
{

        public function getOpcacheStatus1(){
	          return 1;
        }

}
  • TestService.php :新服务文件,即上线后的服务内容(通过getOpcacheStatus2方法)
<?php

class TestService
{

        public function getOpcacheStatus1(){
	          return 1;
        }

        public function getOpcacheStatus2(){
		        sleep(1);
		        return 2;
        }
}
  • loop.php:循环脚本,用于连续按顺序执行上线/回滚操作
<?php

while(1){
    sleep(1);
    echo "上线\n";
    system('bash ./online.sh');
    sleep(1);
    echo "回滚\n";
    system('bash ./rollback.sh');
}

2。测试计划

(1) 测试方法

通过在线和离线不断执行,可以增大不同缓存文件的最后更新时间的差异。在这种高层环境下,在概率和保持高QPS访问的情况下,更容易重现Call to undefined method错误

  • 运行循环脚本,即按顺序运行线上线下操作,无需中断
  • 启动Go脚本并发请求接口,QPS为100

执行前两步,然后观察tail -f /home/log/sys_fatal.log日志

( 2) 预期结果

出现在日志中调用未定义的方法 getOpcacheStatus2 错误

(3) 预期结论

如果出现上述错误,则说明 PHP 有问题本身。结果在opcache中,控制器的内容更新了,但是服务的内容却没有

Opcache导致线上PHP服务异常的案例分析

3。测试流程

执行完所有挂起的操作后,开始观察日志,很快就会不断出现预期的 Call to undefined method 错误,如下图如图

Opcache导致线上PHP服务异常的案例分析

4。测试结论 在线同步文件时间越长,每个文件最后更新时间相差越大

  • 每个文件最后更新时间相差越大,对应缓存中每个文件的有效期越大差异
  • opcache 中每个文件的有效期差异越大,PHP 进程就越有可能读取到不一致版本的文件内容(文件 A 中新内容,文件 B 中旧内容)该值越大
  • PHP进程越有可能读取版本不一致的文件内容,PHP出错的几率取决于QPS
  • QPS越高(100以上),PHP出错的可能性就越大是
  • 5.测试总结

    当可以确认所有文件都是最新的时,同步文件的时间越长,每个文件的最后更新时间差异就越大,这是PHP opcache造成的上线后出错的几率越高

    4.问题总结

    因为在线同步文件需要30秒,所以调用方文件和被调用文件对应的最后更新时间两者必须不同opcache 中的文件。最后更新时间不一致。根据当时的日志文件可以确定QPS在100以上,导致由于opcache导致PHP错误

    Opcache导致线上PHP服务异常的案例分析作者:Wgrape
    来源:稀土掘金

    版权声明

    本文仅代表作者观点,不代表Code前端网立场。
    本文系作者Code前端网发表,如需转载,请注明页面地址。

    发表评论:

    ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

    热门