Page 1 sur 1

php et process zombi

Posté : 03 janv. 2012, 22:24
par Pascal_zp
Bonjour,

J'ai un problème que je n'arrive pas à comprendre, donc à résoudre.
Mon hébergeur accuse un script PHP de créer des process zombi sur un hébergement linux.

Ce script a une seule particularité, celui d'être extrêmement court. Il n'utilise
- aucun appel systeme,
- aucune création de thread ou de fork
- aucun sleep ou wait ou équivalent
- aucun include
- même pas une boucle !

Il est composé d'une 50aine d'instructions de base, dont quelques appels à une BDD, toutes exécutées strictement de façon linéaire, avec des sorties par exit si des test échouent.

Est-ce qu'il y a des opérations connues en PHP pouvant empêcher une fin normale du script et favorables à la création de zombi ?

Si besoin je posterai ici le script en question.

Amitiés et ... Bonne année 2012 !
P.

Re: php et process zombi

Posté : 04 janv. 2012, 07:54
par xTG
Postes le nous oui, car à priori là je vois pas avec tout ce que tu élimines.

Re: php et process zombi

Posté : 04 janv. 2012, 09:10
par Pascal_zp
Bonjour,

Le voici, en intégralité. Les seuls changements sont les remplacement par xxxxxxxxx des infos sensibles.

Petite explication sur le fonctionnement :
Ce script sert à télécharger de très gros fichiers (de 1,5 à 8 Go) par blocs de 10 Mo via des requêtes HTTP "RANGE" émises par une applet Java. Pour chaque téléchargement elle émet :
- 1 requête avec le paramètre 't' pour vérifier la présence du fichier et récupérer sa taille
- 1 requête avec le paramètre 'i' pour initialiser le téléchargement et récupérer le n° de session
- autant que requêtes "RANGE" que le blocs de 10Mo dans le fichier
- 1 requête avec le paramètre 'x' pour terminer le téléchargement et clôturer la session
le paramètres "f" contient toujours le chemin du fichier à télécharger,
le paramètre "s" contient le numéro de session alloué par la commande "i"

La BDD contient 2 tables :
- zp_dl contient la liste des IP entrain de télécharger
- zp_stats contient la liste des fichiers téléchargés avec l'heure de dernier accès et le volume déjà téléchargé.
<?php
  if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE']))
  { $LNG =  StrToUpper(substr($_SERVER['HTTP_ACCEPT_LANGUAGE'],0,2));
    if ($LNG != 'FR')
      $LNG = 'EN';
  }
  else
    $LNG = 'EN';

  //Interdit appel depuis un navigateur autre que le downloadeur ou sans nom de fichier
  if (!isset($_GET['f']) || !isset($_SERVER['HTTP_USER_AGENT']) || ($_SERVER['HTTP_USER_AGENT'] != 'xxxxxxxx'))
  { header("HTTP/1.1 403 Forbidden");
    $Titre = 'Erreur'; 
    $InHead = '  <style>.erreur {font-size:16px; text-align:center; color:red; font-weight:bold}</style>'."\n";
    require 'menu.php';
    echo '<p class="TitrePage">'.(($LNG='FR') ? 'Erreur !' : 'Error!').'</p>';
    echo '<p class="erreur">'.(($LNG='FR') ? 'Page invalide !' : 'Invalid page!').'</p>'."\n";
    echo "</body>\n</html>\n";
    exit;
  }
  $fich = 'fichiers/'.stripslashes($_GET['f']);
  if (!file_exists($fich))
  { header("HTTP/1.1 404 Not Found");
    exit;
  }

  //Demande de taille du fichier
  $taille = filesize($fich);
  if (isset($_GET['t']))
  { echo $taille;
    exit;
  }

  //Connexion à la BDD indispensable pour la suite
  $sql_host = "xxxxxxxx";
  $sql_base = "xxxxxxxx";
  $sql_user = "xxxxxxxx";
  $sql_pwd  = "xxxxxxxx";
  if ((mysql_connect($sql_host, $sql_user, $sql_pwd) == false) || (mysql_select_db($sql_base) == false))
  { //La base de donné n'est pas joignable
    header("HTTP/1.1 500 Internal Server Error");
    exit;
  }
  $Attente = 30*60; //30 minutes avant annulation d'un téléchargement avorté sans prévenir
  $Temps = time();  //temps au moment de la demande
  $ip = addslashes($_SERVER['REMOTE_ADDR']);

  //Demande d'ID de session (inscription de début de téléchargement)
  if (isset($_GET['i']))
  { //inscrit le fichier en téléchargement
    $fichprot = mysql_real_escape_string($fich);
    mysql_query("INSERT zp_stats SET fichier='$fichprot', ip='$ip', debut=FROM_UNIXTIME($Temps), fin=0, taille=0");
    $id_stat = mysql_insert_id();
    
    //Inscrit l'IP téléchargeant
    mysql_query("INSERT zp_dl VALUES('$ip', FROM_UNIXTIME($Temps), $id_stat) ON DUPLICATE KEY UPDATE debut=FROM_UNIXTIME($Temps)");
    echo $id_stat;
    exit;
  }
  
  //Signale fin de téléchargement
  if (isset($_GET['x']))
  { mysql_query("DELETE FROM zp_dl WHERE ip='$ip'"); 
    echo '1';
    exit;
  }

  //Vérifie si demande valide
  $res = mysql_query("SELECT UNIX_TIMESTAMP(debut),session FROM zp_dl WHERE ip='$ip'");
  if ((mysql_num_rows($res) == 0) || !isset($_GET['s']))
  { header("HTTP/1.1 417 Expectation failed");
    exit; 
  }
  else
  { //L'IP est dans la table -> elle télécharge déjà (mais a pu abandonner)
    $enreg = mysql_fetch_row($res);
    $Delai = $Attente + $enreg[0] - time(); //Délai restant à attendre
    $id_stat = $enreg[1];

    if (($id_stat != $_GET['s']) && ($Delai > 0))
    { header('HTTP/1.1 503 Service Temporarily Unavailable');
      header('Status: 503 Service Temporarily Unavailable');
      header('Retry-After: '.$Delai);
      exit;
    }
  }

  //Pour le téléchargement, n'accepte que des demandes portant sur des tailles de moins de 20 Mo.
  if (!isset($_SERVER['HTTP_RANGE']))
  { header("HTTP/1.1 405 Method Not Allowed");
    exit;
  }
  
  //=== Téléchargement autorisé
  $datefich = date('r', filemtime($fich));
  $nomfich = (strstr($_SERVER["HTTP_USER_AGENT"], "MSIE") == false) ? basename($fich) : urlencode(basename($fich)); 
  $TailleFich = filesize($fich);

  preg_match('/bytes=(\d+)-(\d+)?/', $_SERVER['HTTP_RANGE'], $plage);
  $premier = intval($plage[1]);
  $dernier = intval($plage[2]);

  if ($dernier+1 == $TailleFich)
  { //Indque téléchargement fini pour cet utilisateur et dans les stats
    mysql_query("UPDATE zp_stats SET fin=FROM_UNIXTIME($Temps), taille=-1 WHERE id=$id_stat");
    mysql_query("DELETE FROM zp_dl WHERE ip='$ip'");
  }
  else
  { //Met à jour la date de dernier accès
    mysql_query("UPDATE zp_dl SET debut=FROM_UNIXTIME($Temps) WHERE ip='$ip'");
    //Met à jour le volume téléchargé
    $finmega = round($dernier/1024/1024);
    mysql_query("UPDATE zp_stats SET fin=FROM_UNIXTIME($Temps), taille=$finmega WHERE id=$id_stat");
  }
  mysql_close(); //Libère immédiatement BDD
  
  //Empêche Apache de compresser la sortie
  ini_set('zlib.output_compression', 0);

  header('Content-Transfer-Encoding: binary'); //Transfert en binaire (fichier)
  header('Content-Disposition: attachment; filename="'.$nomfich.'"; modification-date="'.$datefich.'";');
  header('Content-Length: '.$taille);
  header('Content-Type: application/force-download');
  header('Accept-Ranges: bytes');
  header('HTTP/1.1 206 Partial Content');
  header('Content-Range: bytes '.$premier.'-'.$dernier.'/'.$taille);
 
  $TailleBloc = min($dernier+1-$premier, $TailleFich-$premier, 40*1024*1024); //Taille maxi en octets acceptée

  $handle = fopen($fich, 'rb');
  fseek($handle, $premier);
  echo fread($handle, $TailleBloc);
  fclose($handle);
  exit; //Pour être sûr qu'aucun octet ne suit 
?>

Re: php et process zombi

Posté : 04 janv. 2012, 09:47
par xTG
Hum bah hormis le fait que tu ne testes pas $handle je ne vois pas...
Aucune boucle en effet ou effet de bord pouvant entrainer une attente.

Donc à moins que cela soit une primitive système en rapport avec le système de fichier qui ne se termine pas...
Est-ce que ton appli java réceptionne des fois des erreurs ou bien récupère-t-elle toujours ses paquets ?

Leur as-tu demandé le nom du processus zombie ? Car il est clair que ce n'est pas PHP avec tes exit()...

Re: php et process zombi

Posté : 04 janv. 2012, 11:51
par Mazarini
Bonjour,

Je suppose que ton script est appelé plusieurs fois par le client pour faire le transfert. Le nombre d'appel et la durée de chaque transfert de fichier peut faire croire à des processus zombi. Le chargement de fichier de 1,5Go à 8Go par tranche de 10Mo doit faire une sacré activité sur le serveur.
Les hébergeurs (même illimité) ont une facheuse tendance à faire la chasse aux gros consommateurs de ressources machines. Je ne suis pas sur qu'ils aillent beaucoup plus loin que la consommation de ressources dans leur analyse.

Re: php et process zombi

Posté : 04 janv. 2012, 12:48
par stealth35
t'as pas x-sendfile sur ton serveur ?

Re: php et process zombi

Posté : 04 janv. 2012, 17:54
par Pascal_zp
Merci pour vos réponses, je commence à désespérer....
Le site est suspendu et je vais bientôt me pendre moi aussi. :-(

Le service technique de BlueHost (puis que c'est chez eux qu'est hébergé le script) n'en démord pas :
The zombie processes here were only occurring for PHP processes that had executed your dlm.php script. There's really no question about it, only PHP processes that had executed your dlm.php script were exhibiting this behavior, clearly the issue at the heart of this lies in that script somewhere else we would see the bad behavior in other cases as well.
@xTG :
Non je n'ai pas eu en clair le nom du process, mais d'après la réponse ci-dessus ce serait PHP.

@Mazarini :
Oui, le script est appelé souvent, mais ce ne devrait pas atteindre unniveau très élevé : il y a rarement plus de 15 personnes qui téléchargent à la fois et surtout le débit global sortant du serveur est limité à 15 Mb/s (soit 1,5 Moctet/s) donc télécharger 10 Mo ne devrait pas être possible en moins de 6,6 secondes. Donc au pire 15 appels simultanés toutes les 6,6 secondes, on devrait être loin d'une charge importante.

@Stealth35:
De mémoire il me semble que x-sendfile n'est pas installé par défaut chez Bluehost, mais surtout, à cause de la durée de téléchargement très longue, de nombreux usagers avaient des coupures et devaient recommencer, les navigateurs gérant très mal la reprise. L'applet gère une reprise avec une très solide vérification de concordance et un retour en arrière optimisée en cas de blocs défectueux.

En outre, l'ensemble permet d'éviter qu'un usager indélicat ouvre plusieurs téléchargements à la fois et monopolise la bande passante.

Je vais poursuivre les demandes et vous tiens au courant.
Merci

P.

Re: php et process zombi

Posté : 04 janv. 2012, 20:23
par stealth35
En outre, l'ensemble permet d'éviter qu'un usager indélicat ouvre plusieurs téléchargements à la fois et monopolise la bande passante
ce que fait nativement x-sendfile :wink:
dommage, après si c'est pour du pro, tu peux rajouter ce module

Re: php et process zombi

Posté : 07 janv. 2012, 21:31
par Pascal_zp
Bonjour,

Quelques nouvelles sur le problème que j'ai réussi à mieux cerner :

Chaque fois que Apache reçoit une requête pour un bloc de données, il crée un nouveau process "PHP5" qui traite le script PHP extrêmement rapidement, place les 10 MB dans le buffer de sortie puis meurt et est marqué "Defunct".

Les données sont ensuite envoyées par Apache à la vitesse de réception de l'utilisateur, mais Apache ne retire pas le process PHP de la liste des process tant que le buffer n'est pas vide ou que la connexion n'est pas coupée.

Existe-t il une option de configuration pour dire à Apache de retirer les process PHP terminées plus rapidement ? Une façon de déconnecter le buffer de PHP ?

----
Je confirme, X-SendFile n'est pas installé sur le serveur. S'il était installé, est-ce qu'il serait possible de lui demander de n'envoyer d'une partie de fichier ?

P.