Faire des importations importantes et Apache Solr indexing en utilisant drush, batch API et cron
Je sousentends par importation importante la création de millions de nœuds et de les indexer pour la recherche Apache Solr. On avait besoin d'importer 1 million d'enregistrements à partir d'un fichier et nous avions besoin de créer 3 noeuds pour chaque enregistrement, un dans chacune de nos trois langues. On devait créer ainsi des scripts pour créer, mettre à jour et supprimer 3 millions de nœuds. Ces nœuds ont également trois vocabulaires taxonomie, environ 30 champs CCK et nous avions également besoin d'une autre table de jointure contenant les coordonnées spatiales pour notre domaine géo.
Ceci dit, stocker un nœud comme ça est une opération coûteuse.
On a obtenu le temps d'arrêt à 72 heures pour importer l'ensemble de ces dossiers. Le processus a été entièrement automatisé et on a utilisé une installation manuellement paramétrée pour utiliser le serveur à sa capacité maximale.
On a reçu deux fichiers principaux (et une autre série de 5 photos avec des paires valeur-clé que la cartographie de certains de nos champs CCK). Un contenant nos dossiers avec des données pour remplir nos champs CCK les autres et la taxonomie, un second fichier avec les coordonnées.
Nous avons d'abord chargé tous les fichiers dans la base de données. Nous avons donné la première table un id incrémenté afin que nous puissions l'utiliser plus tard pour suivre l'évolution (nous avons utilisé un petit script pour faire cela). On a importé deuxième tableau en utilisant phpmyadmin. On a également importé les tables de recherche en utilisant phpmyadmin. Ce processus va très vite. En moins de deux heures, on a obtenu les fichiers sur le serveur et dans la base.
Pourquoi on ne lit pas directement des fichiers? On a dû chercher le numéro de TVA de l'entreprise dans le second fichier pour trouver les coordonnées de l'entreprise concernée. C'est une tâche qu'une base de données peut faire beaucoup mieux qu'un processus de lecture du fichier dans lequel vous aurez à scanner le fichier ligne par ligne.
Nous avons également créé des index sur nos tables, une clé primaire sur le champ Auto Inc et un index unique sur l'id TVA dans le premier tableau et d'un primaire sur le numéro de TVA dans le deuxième tableau. Sur les autres tables avec des recherches, on a désigné les colonnes clés en tant que clés primaires. Nous avons également créé un indice sur notre numéro de TVA dans notre tableau CCK, puisqu'on va l'utiliser pour rechercher si un noeud a besoin d'être créé ou mis à jour. Les index sont très importants car ils accélèrent les recherches dans la base de données (http://dev.mysql.com/doc/refman/5.0/en/mysql-indexes.html). Alors analyser toujours vos tables de la base pour voir si un index est approprié lorsque vous faites des importations.
Maintenant qu'on a créé notre script d'importation qui lit essentiellement sur une ligne dans la base de données et construit un objet de nœud et fait un node_save pour l'intégrer dans Drupal. Rien de spécial à ce sujet. Sauf que si vous exécutez simplement à partir de votre navigateur le script serait juste le temps quand votre max_execution_time php est atteinte ou lorsque PHP est hors de la mémoire. Même si vous réglez memory_limit au maximum sur votre serveur et max_execution_time à illimité le script serait encore échouer, car la mémoire des machines seraient consommés après un laps de temps. Aussi ce n'est pas la voie à suivre les progrès réalisés sur votre importation. Vous ne pouvez pas redémarrer quand quelque chose tourne mal...
Alors, naturellement dans Drupal nous pensons que les API de batch vont nous sauver. Voici la page de manuel sur la façon de créer un batch (http://drupal.org/node/180528).
Maintenant, vous n'avez pas à vous soucier de votre timing des scripts sur l'API de batch s'assurera, il l'habitude de utiliser beaucoup de mémoire. Il y a deux façons de construire un batch du premier qui est expliqué dans la page du manuel. Le second est un peu différent car il utilise une seule fonction dans le tableau de fonctionnement. On va prendre le batch réindexer apachesolr comme un exemple. Pourquoi est-ce utile, l'API de batch enregistre son batch dans le tableau batch. Alors, quand vous construisez un batch contenant un million d'opérations et ce tableau est sérialisé et mis dans la base de données des choses méchantes qui se passera. Selon vos paramètres de mysql et la capacité du serveur cela va échouer. Dans mon cas, il a échoué à environ 30k dossiers.
Vous pouvez toujours utiliser la première méthode, ce que vous faites est divisé les batchs en pièces. Et les importer un par un. Quel que nous avons fait, mais pas pour la raison d'un tableau sérialisé Tho grand. Mais plus à ce sujet dans un instant.
Vérifiez cet extrait pour savoir comment écrire une API de commandes qui utilise une seule fonction dans le tableau des opérations. Dans notre cas, la seule chose dont vous avez besoin est une variable qui garde la trace de l'endroit où vous vous trouvez.
<?php
/**
* Batch reindex functions.
*/
/**
* Submit a batch job to index the remaining, unindexed content.
*/
function apachesolr_batch_index_remaining() {
$batch = array(
'operations' => array(
array('apachesolr_batch_index_nodes', array()),
),
'finished' => 'apachesolr_batch_index_finished',
'title' => t('Indexing'),
'init_message' => t('Preparing to submit content to Solr for indexing...'),
'progress_message' => t('Submitting content to Solr...'),
'error_message' => t('Solr indexing has encountered an error.'),
'file' => drupal_get_path('module', 'apachesolr') . '/apachesolr.admin.inc',
);
batch_set($batch);
}
/**
* Batch Operation Callback
*/
function apachesolr_batch_index_nodes(&$context) {
if (empty($context['sandbox'])) {
try {
// Get the $solr object
$solr = apachesolr_get_solr();
// If there is no server available, don't continue.
if (!$solr->ping()) {
throw new Exception(t('No Solr instance available during indexing.'));
}
}
catch (Exception $e) {
watchdog('Apache Solr', $e->getMessage(), NULL, WATCHDOG_ERROR);
return FALSE;
}
$status = module_invoke('apachesolr_search', 'search', 'status');
$context['sandbox']['progress'] = 0;
$context['sandbox']['max'] = $status['remaining'];
}
// We can safely process the apachesolr_cron_limit nodes at a time without a
// timeout or out of memory error.
$limit = variable_get('apachesolr_cron_limit', 50);
// With each pass through the callback, retrieve the next group of nids.
$rows = apachesolr_get_nodes_to_index('apachesolr_search', $limit);
apachesolr_index_nodes($rows, 'apachesolr_search');
$context['sandbox']['progress'] += count($rows);
$context['message'] = t('Indexed @current of @total nodes', array('@current' => $context['sandbox']['progress'], '@total' => $context['sandbox']['max']));
// Inform the batch engine that we are not finished, and provide an
// estimation of the completion level we reached.
$context['finished'] = empty($context['sandbox']['max']) ? 1 : $context['sandbox']['progress'] / $context['sandbox']['max'];
// Put the total into the results section when we're finished so we can
// show it to the admin.
if ($context['finished']) {
$context['results']['count'] = $context['sandbox']['progress'];
}
}
/**
* Batch 'finished' callback
*/
function apachesolr_batch_index_finished($success, $results, $operations) {
$message = format_plural($results['count'], '1 item processed successfully.', '@count items successfully processed.');
if ($success) {
$type = 'status';
}
else {
// An error occurred, $operations contains the operations that remained
// unprocessed.
$error_operation = reset($operations);
$message .= ' '. t('An error occurred while processing @num with arguments :', array('@num' => $error_operation[0])) . print_r($error_operation[0], TRUE);
$type = 'error';
}
drupal_set_message($message, $type);
}
?>Alors maintenant on pourrait importer nos nœuds sans se soucier des délais d'expiration et notre batch défaillant. Mais on a encore à surveiller le processus dans le cas de notre connexion internet se coupe. On aura à actualiser la page et continuer l'éxécution du batch.
Donc, il serait formidable d'avoir juste moyen de lancer une commande et n'avez pas à vous soucier de rien. Pour réaliser cela, nous utilisons http://drupal.org/project/drush et la fonctionnalité cron sur le serveur.
Vous pouvez écrire votre propre commande drush qui lance un lot comme vous le feriez en appelant le script que nous avait deja. Cependant, il ne marchera pas, votre mémoire sera épuisée. Mais ne vous inquiétez pas, il y a une solution à cela. Vous allez appeler une commande drush qui est capable de faire des batchs. Avec cette commande drush nous nous assurerons que votre mémoire n'est pas entrain de s'épuiser tout en faisant des batchs. Vous pouvez voir comment il est utilisé dans la fonction drush updatedb. Voici mon extrait sur la façon dont je l'ai mis en œuvre en utilisant une commande personnalisée drush qui appelle la fonction qui sera appelée par le «drush traiter par lots [batch-id]" commande.
<?php
function import_drush_command() {
$items = array();
$items['import'] = array(
'callback' => 'import_drush_import',
'description' => dt('Import'),
'arguments' => array(
'start' => "start",
'stop' => "stop",
),
);
}
function import_drush_import($start, $stop) {
$result = db_query("SELECT * FROM {our_table_with_records} WHERE id > %d AND id < %d", $start, $stop);
import_drush_import_operations($batch, $result);
batch_set($batch);
$batch =& batch_get();
$batch['progressive'] = FALSE;
drush_backend_batch_process();
}
/**
* Creates operations for importing bedrijf nodes
*/
function import_drush_import_operations(&$batch, &$result) {
while ($fields = db_fetch_array($result)) {
array_shift($fields);
$fields_out = array();
foreach ($fields as $field) {
$fields_out[] = $field;
}
$batch['operations'][] = array('import_create_bedrijf_nodes', array($fields_out, TRUE, 17));
}
}
?>Ok donc maintenant dans le terminal, nous typ quelque chose comme: import drush 1 1000 et il va créer un lot, il le feu et l'importation les 1000 premiers enregistrements et de créer des nœuds pour elle.
Vous pourriez avoir cette fonction appelée par cron de sorte que vous n'avez même pas besoin d'avoir un terminal ouvert. Mais comme dit précédemment nous sommes encore la création d'une opération pour chaque enregistrement. Pourquoi? En faisant le tour d'une opération, je n'ai remarqué que 30% de la CPU a été utilisé (cocher cette tapant "top" dans une autre fenêtre de terminal). J'ai donc pensé que nous pourrions reproduire coquilles multiples et de les rendre tous faire le travail. Je l'ai fait et je trouvé que je pouvais lancer six coquilles avec la commande d'importation drush. Sur le septième de la CPU du serveur pointes jusqu'à 300% et fait le crash du serveur afin six obus était la limite. Il est probablement possible de mesurer les ressources et les commandes de lancement en fonction de cela. Mais pour l'instant j'ai pensé que le serveur utilise toutes ses ressources et de l'importation va aussi vite que possible en dépit de ce qui est un processus manuel.
La dernière chose que j'ai fait pour automatiser le processus a été mise en place d'une importcron.php dans la racine de l'installation de Drupal contenant ceci:
<?php
//Set the path correctly so drupal know how to include it's files during bootstrap
$path='/var/www/html/your_drupal';
chdir($path);
//Bootstrap drupal
include_once './includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
//Launch your function
import('importer_import_progress');
//IMPORT
function import($value) {
$amount = db_result(db_query("SELECT COUNT(bid) FROM {batch}"));
$start = (string) variable_get($value, 0);
if($amount < 7 && $start < 985558) {
//for some reason you need to cast everyhting to strings explicitly otherwise it wouldnt launch the command
$stop = (string) $start + 1000;
variable_set($value, $stop);
$command = "/var/www/html/your_drupal_site/sites/all/modules/contrib/drush/drush import ";
$start = (string) ($start+1);
$command .= $start . " " . $stop;
exec("$command" , $output , $return);
}
}
?>Puis dans le type crontab-e pour éditer la liste des cron, typ i pour insérer et mettre cette commande:
* * * * * /usr/bin/php /var/www/html/your_drupal_site/cronimport.php
Tapez le chemin complet de PHP et le chemin complet vers votre fichier. Ce sera d'exécuter la commande une fois toutes les minutes. Dans le script une commande drush sera exectuted avec les 300 prochains articles nécessaires à l'importation. Pour éviter que la commande de tir pour autant qu'il sera Allway vérifier si les lots précédents sont finis. Notre limite est de 6 lots au même moment. Si votre serveur est plus puissant que vous pouvez augmenter la 1000 articles et les 6 lots. Il serait agréable d'avoir ce processus contrôlé par une fonction qui calcule les ressources du serveur et lance des lots en conséquence, mais j'aurais à faire quelques recherches sur la façon de le faire.
Conclusion
Le script se déroule pendant environ 70 heures et le site contenait 3 millions de nœuds. Le même principe a été utilisé pour l'indexation qui a pris environ 50 heures à l'index tous les nœuds. Dans l'indexation, nous avons modifié quelques autres choses à faire aller plus vite mais c'est pour un autre billet de blog.
Related content
- Le problème d'organisation de drupal
- Automatiser l'installation de Drupal sur Ubuntu à des fins de formation Drupal
- Astuce Drupal 7 : Ajoutez des liens contextuels à n'importe quoi
- Une expérience de qualité en Drupal Entreprise
- Comment exécuter cron Drupal par CLI
- Drupal : règles personnalisées pour écrire vos propres conditions d'événements, actions et objets personnalisés (+ jetons personnalisés)
- Drupal : Astuce d'un formulaire web soumettant valeurs cachées
- Drupal : comment charger le contenu des champs en utilisant l'API de contenu CCK pour éviter le chargement des nœuds

