Une analyse de cas d'anomalie de service PHP en ligne causée par Opcache

Je participe aux "Nuggets·Starting Plan"

1. Origines

2021-05-13 14:10 Après le lancement du service, un grand nombre d'erreurs "Appel à une méthode non définie" ont commencé à apparaître. Grâce à Grafana et à la vérification des journaux, vous pouvez trouver les éléments suivants

image

  • L'erreur se produit sur cinq serveurs
  • Le rapport d'erreur de chaque serveur dure environ 3 secondes

2. Analyse du problème

1. Calendrier de lancement

  • 14:10:50 Postulez pour aller en ligne
  • 14:10:57 Préparation des données terminée
  • 14:10:57 Aller en ligne
  • 14:10:57 Synchronisez les fichiers du répertoire avec le serveur cible via parallel-rsync
  • 14:11:26 La première synchronisation du serveur est terminée
  • 14:11:41 La dernière synchronisation du serveur est terminée
  • 14:11:41 Fin de connexion

2. Chronologie du serveur 1

  • 14:11:35 Synchronisation code-serveur terminée
  • 14:11:36 Le premier rapport d'erreur commence
  • 14:11:37 Fin du dernier message d'erreur (1 seconde, 220 messages au total)

3. Chronologie du serveur 2

  • 14:11:35 Synchronisation code-serveur terminée
  • 14:11:35 Le premier rapport d'erreur commence
  • 14:11:38 Le dernier rapport d'erreur se termine (dure 3 secondes, total 237)

4. Chronologie du serveur 3

  • 14:11:35 Synchronisation code-serveur terminée
  • 14:11:36 Le premier rapport d'erreur commence
  • 14:11:40 Le dernier rapport d'erreur se termine (4 secondes au total, 700 au total)

5. Chronologie du serveur 4

  • 14:11:39 Synchronisation code-serveur terminée
  • 14:11:45 Le premier rapport d'erreur commence
  • 14:11:45 Fin du dernier message d'erreur (1 seconde, 196 messages au total)

6. Chronologie du serveur 5

  • 14:11:28 Synchronisation code-serveur terminée
  • 14:11:30 Le premier rapport d'erreur commence
  • 14:11:32 Fin du dernier message d'erreur (dure 2 secondes, 399 messages au total)

3. Analyse du problème

1. Message d'erreur complet

Le message d'erreur appelle une méthode qui n'existe pas

{
    "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. Questions et doutes

Selon la pile d'appels imprimée, il a été constaté qu'elle avait été xxx.phpappelée dans . À en juger par l'enregistrement de soumission Git à ce moment-là, cette doSomething()méthode existe. En d'autres termes, il est impossible de trouver cette méthode.

3. Raisons possibles

Il y a deux raisons possibles au problème, l'une est le problème en ligne et l'autre est le problème d'opcache

(1) Problème en ligne

Lorsque le code de synchronisation en ligne est envoyé à la machine cible, le code du fichier de l'appelant a été synchronisé, mais doSomething()le fichier où se trouve la méthode n'a pas encore été synchronisé

(2) opcache问题

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

image

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

Échec de l'enregistrement, il est recommandé de télécharger directement le fichier image

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

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

image

④ 结论

由于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内容未更新

image

3、测试过程

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

image

4、测试结论

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

5、测试总结

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

四、问题总结

Étant donné qu'il faut 30 secondes pour synchroniser les fichiers en ligne, l'heure de la dernière mise à jour du fichier de l'appelant et du fichier de l'appelé doit être différente, et l'heure de la dernière mise à jour des deux fichiers correspondants dans opcache n'est pas non plus la même. Le QPS peut être déterminé en fonction au journal à ce moment-là Être au-dessus de 100 a provoqué le déclenchement d'erreurs PHP causées par opcache

image

Je suppose que tu aimes

Origine juejin.im/post/7250375753598435383
conseillé
Classement