Un análisis de caso de anomalía en el servicio PHP en línea causada por Opcache

Estoy participando en el "Plan Nuggets·Starting"

1. Antecedentes

2021-05-13 14:10 Después de que se lanzó el servicio, comenzaron a aparecer una gran cantidad de errores de "Llamada a un método indefinido". A través de Grafana y revisando los registros, puede encontrar lo siguiente

imagen

  • El error ocurre en cinco servidores.
  • El informe de error de cada servidor tiene una duración de unos 3 segundos.

2. Análisis de problemas

1. Cronograma de lanzamiento

  • 14:10:50 Aplicar para ir en línea
  • 14:10:57 Preparación de datos completada
  • 14:10:57 Entrar en línea
  • 14:10:57 Sincronizar archivos de directorio con el servidor de destino mediante paralelo-rsync
  • 14:11:26 La primera sincronización del servidor está completa
  • 14:11:41 La última sincronización del servidor está completa
  • 14:11:41 Fin de la conexión

2. Línea de tiempo del servidor 1

  • 14:11:35 Se completó la sincronización del código con el servidor
  • 14:11:36 Comienza el primer informe de error
  • 14:11:37 Finaliza el último mensaje de error (1 segundo, 220 mensajes en total)

3. Línea de tiempo del servidor 2

  • 14:11:35 Se completó la sincronización del código con el servidor
  • 14:11:35 Comienza el primer informe de error
  • 14:11:38 Finaliza el último informe de error (dura 3 segundos, total 237)

4. Línea de tiempo del servidor 3

  • 14:11:35 Se completó la sincronización del código con el servidor
  • 14:11:36 Comienza el primer informe de error
  • 14:11:40 Finaliza el último informe de error (4 segundos en total, 700 en total)

5. Línea de tiempo del servidor 4

  • 14:11:39 Se completó la sincronización del código con el servidor
  • 14:11:45 Comienza el primer informe de error
  • 14:11:45 Finaliza el último mensaje de error (1 segundo, 196 mensajes en total)

6. Línea de tiempo del servidor 5

  • 14:11:28 Se completó la sincronización del código con el servidor
  • 14:11:30 Comienza el primer informe de error
  • 14:11:32 Finaliza el último mensaje de error (dura 2 segundos, 399 mensajes en total)

3. Análisis de problemas

1. Mensaje de error completo

El mensaje de error está llamando a un método que no existe

{
    "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. Preguntas y dudas

De acuerdo con la pila de llamadas impresa, se descubrió que se xxx.phpllamó en . A juzgar por los registros de envío de Git en ese momento, este doSomething()método existe. En otras palabras, es imposible encontrar este método.

3. Posibles razones

Hay dos razones posibles para el problema, una es el problema en línea y la otra es el problema de opcache

(1) Problema en línea

Cuando el código de sincronización en línea se envía a la máquina de destino, el código de archivo de la persona que llama se ha sincronizado, pero doSomething()el archivo donde se encuentra el método aún no se ha sincronizado.

(2) opcache问题

两个文件均已同步到目标机器,但Zend引擎解析代码时,opcache出现了如下的分布情况

imagen

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 执行原理

No se pudo guardar, se recomienda cargar el archivo de imagen directamente

③ opcache中文件的上次更新时间参差不齐

由于同步代码文件到服务器上需要30秒,所以在opcache中每个文件的上次更新时间会存在参差不齐的情况

imagen

④ 结论

由于opcache中每个文件的上次更新时间参差不齐,所以会出现如下情况

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

理论上会存在上述这种场景,但是需要测试并复现此场景,如果可以复现,则可以确认是opcache中AB文件的上次更新时间不一致,且不需要重新解析B文件

四、复现opcache导致PHP错误问题

1、测试准备

(1) opcache配置

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

(2) 7个测试脚本

  • online.sh :上线脚本,用于模拟上线操作
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文件,即回滚后的Service内容(没有getOpcacheStatus2方法)
<?php

class TestService
{

        public function getOpcacheStatus1(){
	          return 1;
        }

}
  • TestService.php :新的Service文件,即上线后的Service内容(有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) 测试方法

通过不间断执行上下线,可以加大opcache中不同文件上次更新时间的差异,在这种高概率的情况下且保持高QPS的访问,就比较容易复现 Call to undefined method 错误

  • 执行 loop 脚本 ,即不间断的依次执行上下线
  • 启动Go脚本并发请求接口 ,QPS 为 100

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

(2) 预期结果

日志中出现 Call to undefined method getOpcacheStatus2 的报错

(3) 预期结论

如果出现上述报错则说明PHP自身问题,导致在opcache中,Controller内容已更新,但Service内容未更新

imagen

3、测试过程

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

imagen

4、测试结论

  • 上线同步文件时间越长,每个文件的上次更新时间差异就越大
  • 每个文件的上次更新时间差异越大,对应到opcache中每个文件的有效期差异就越大
  • opcache中每个文件的有效期差异越大,PHP进程读取到非一致性版本文件内容(A文件中的新内容,B文件中的旧内容)的可能性就越大
  • PHP进程读取到非一致性版本文件内容的可能性越大,出现PHP错误的可能性就越取决于QPS
  • QPS越高(100以上),出现PHP错误的可能性就越大

5、测试总结

在可以确认所有文件是最新的情况下,同步文件需要的时间越长,每个文件的上次更新时间差异越大,上线后由opcache引起PHP错误的可能性就越大

四、问题总结

Dado que se tarda 30 segundos en sincronizar archivos en línea, la última hora de actualización del archivo de la persona que llama y el archivo de la persona que llama debe ser diferente, y la última hora de actualización de los dos archivos correspondientes en opcache tampoco es la misma. al registro en ese momento Estar por encima de 100 provocó que se activaran errores de PHP causados ​​por opcache

imagen

Supongo que te gusta

Origin juejin.im/post/7250375753598435383
Recomendado
Clasificación