26 août 2011

HTTP request loop in NodeJS

Comment envoyer 200000 requêtes HTTP avec NodeJS ? A priori cette question semble facile ? Voyons si c'est le cas ?

Commençons petit

Avant d'envoyer plusieurs centaines de requêtes voyons déjà comment en envoyer une seule :

#!/usr/bin/env node

var http = require('http');

var options = {
  host: '127.0.0.1',
  port: 80,
  path: '/'
};

http.get(options, function(response) {
    console.log(response.statusCode);
});

On boucle !

#!/usr/bin/env node

var http = require('http');

var options = {
  host: '127.0.0.1',
  port: 80,
  path: '/'
};

for(var i = 0; i < 2000000; i++) {
	http.get(options, function(res) {
   	    console.log(res.statusCode);
	});
}

Résultat :


FATAL ERROR: JS Allocation failed - process out of memory

Économiser la mémoire

Ok, on y a été un peu fort, on va optimiser notre boucle en sortant le client HTTP de la boucle, histoire de l'instancier qu'une seule fois.

#!/usr/bin/env node

var http = require('http');


var client = http.createClient(80, '127.0.0.1');


for(var i = 0; i < 200000; i++) {
	var request = client.request("GET", '/')
	request.end();
	request.on('response', function(response) {
			console.log(response.statusCode);
	});
}

Résultat :


node.js:205
        throw e; // process.nextTick error, or 'error' event on first tick
              ^
Error: EMFILE, Too many open files
    at net_legacy.js:741:19
    at Object.lookup (dns_legacy.js:159:5)
    at Socket.connect (net_legacy.js:729:20)
    at Object.createConnection (net_legacy.js:268:5)
    at new ClientRequest (http2.js:1041:23)
    at Client.request (http2.js:1475:11)
    at Object.<anonymous> (/home/touv/tests/node/loop02.js:11:23)
    at Module._compile (module.js:416:26)
    at Object..js (module.js:434:10)
    at Module.load (module.js:335:31)

Oups, NodeJS est-il incapable d'exécuter un traitement aussi simple ? Ou mon code est-il mauvais ?

Pool HTTP Request

Pour lancer nos 200000 requetes, il va falloir économiser la mémoire et les descripteurs, mais surtout s'adapter au mode asynchrone et éviter de vouloir balancer 200000 requêtes en parallèle !

Pour nous aider, il existe le module node-pool qui propose :
"a small but usefull resource pooling/limiting/throttling library".
Voilà donc la solution à notre question toute simple ...

#!/usr/bin/env node

var http = require('http');

var pool = require('generic-pool').Pool({
		name     : 'http_request',
		create   : function(callback) {
			var c = http.createClient(80, '127.0.0.1');
			callback(null, c);
		},
		destroy  : function(client) { },
		max      : 10,
		idleTimeoutMillis : 300,
		log : false
});


for(var i = 0; i < 200000; i++) {
	pool.acquire(function(err, client) {
			var request = client.request("GET", '/');
			request.end();
			request.on('response', function(response) {
					console.log(response.statusCode);
					pool.release(client);
			});
	});

}

19 août 2011

Mapping XML - Array - JSON en PHP

Vouloir établir une correspondance entre JSON et XML n'est pas nouveau. (cf. Converting Between XML and JSON). D'ailleurs, le Zend Framework propose sa solution XML to JSON conversion.
Cependant,

  • le format est-il adapté ?
  • l’opération inverse est-elle possible ?

Vouloir passer du XML au JSON revient en fait à transformer du XML en une structure mémoire de type tableau. Toutes les solutions en PHP que j'ai trouvé passe par cette étape. Aucune ne transforme la chaîne de caractères XML en chaîne de caractères JSON. La véritable question est donc :
Comment faire un mapping entre XML et un tableau PHP ?
Le format JSON n'étant plus qu'une sérialisation du tableau. Du coup, l'opération inverse n'est pas de vouloir transformer du JSON en XML mais bien de vouloir transformer un tableau en XML. Le problème est donc de passer d'une structure mémoire à un fichier XML et réciproquement. CQFD ;-)
La belle affaire me direz-vous ? C'est pareil ! Oui sauf qu'en abordant le problème de cette manière, on doit produire un code capable de transformer toute structure mémoire en XML, et cela a une influence directe sur le format d’équivalence que l'on va choisir. Et à ce petit jeu là, c'est le format proposé par Google qui me semble le plus adapté ! cf. Using JSON in the Google Data Protocol

XML_Array

Voici donc une petite classe PHP, disponible au travers d'un package PEAR qui permet de transformer n'importe quel document XML en tableau PHP. Mais qui permet également l'opération inverse à savoir transformer n'importe quel tableau PHP en fichier XML.

array2xml.php

<?php
require_once 'XML/Array.php';

$array_input = array (
  'rdf:RDF' => 
  array (
    'xmlns:rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
    'xmlns' => 'http://www.exemple.com/fake#',
    'rdf:Description' => 
    array (
      0 => 
      array (
        'rdf:about' => 'urn:id:aaa',
        'name' => 
        array (
          '#text' => 'Dupont T.',
        ),
        'email' => 
        array (
          'type' => 'main',
          '#text' => 'dupont@example.com',
        ),
      ),
      1 => 
      array (
        'rdf:about' => 'urn:id:bbb',
        'name' => 
        array (
          '#text' => 'Dupond D.',
        ),
        'email' => 
        array (
          'type' => 'main',
          '#text' => 'dupond@exeample.com ',
        ),
      ),
    ),
  ),
);

$xml = XML_array::export($array_input);
$array_output = XML_array::import($xml);

assert($array_input == $array_output);

?>

xml2array.php

<?php
require_once 'XML/Array.php';
$xml_input = <<<EOT
<rdf:RDF
	xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
	xmlns="http://www.exemple.com/fake#">
	<rdf:Description rdf:about="urn:id:aaa">
		<name>Dupont T.</name>
		<email type="main">dupont@example.com</email>
	</rdf:Description>
	<rdf:Description rdf:about="urn:id:bbb">
		<name>Dupond D.</name>
		<email type="main">dupond@exeample.com </email>
	</rdf:Description>
</rdf:RDF>
EOT;

$array = XML_Array::import($xml_input);
$xml_output = XML_array::export($array);
 
$xml_input = preg_replace(',\s+,', '', $xml_input);
$xml_output = preg_replace(',\s+,', '', $xml_output);

assert($xml_input == $xml_output);
?>

Tables de correspondance

Règles principales
ARRAY XML
key ⇒ array( key1 ⇒ value, ) <key key1="value/>
key ⇒ array( #text ⇒ value1, ) <key>value1</key>
key ⇒ array( array(…), array(…), ) <key>…</key><key>…</key>
0 ⇒ value <![CDATA[value]]>
key ⇒ value <row key="value" />
#comment ⇒ value <!--value-→
Clefs réservées
XML values key
Text node $t, _t, _text, #text
comment node $c, _c, _comment, #comment
xml:lang xmllang, xml:lang, xml$lang
xml:space xmlspace, xml:space, xml$space
xml:id xmlid, xml:id, xml$id
xml:idref xmlidref, xml:idref, xml$idref

Benchmark

Voici une petite comparaison de performance en chargeant en mémoire séquentiellement 196867 documents XML pour un volume total de 250 Mo.


xml2str   => 53,292876 Seconds 
--------------------------------
xml2array => 178,356342 Seconds 
--------------------------------
xml2dom   => 64,135433 Seconds 

Téléchargement et code source

Le code source est disponible sur GitHub : http://github.com/touv/xml_array

ou on peut directement l'installer avec PEAR en s'abonnant au Channel Respear :


% pear channel-discover pear.respear.net
% pear install respear/xml_array

Webographie