Het doen van GROTE imports en apache solr indexing met gebruik van drush, batch api en cron

Posted by: 
Dominique De Cooman

Met grote imports bedoel ik het creëren van miljoenen nodes en ze indexeren voor een apache solr search. We moesten 1 miljoen gegevens uit file importeren en we moesten 3 nodes maken voor elk record, een in alle 3 van onze talen. Dus dit gaf ons de taak om een script te maken voor het creëren, updaten en verwijderen van 3 miljoen nodes. Deze nodes hadden tevens 3 taxonomy woordenlijsten, rond de 30 cck velden en we moesten een andere tabel met ruimtelijke coördinaten voor ons geo veld invoegen.
Dus zo'n node opslaan is een dure operatie.
Het nam 72 uur in beslag om alle gegevens te in te voeren. Het proces was volledig geautomatiseerd en we gebruikte een handmatig geconfigureerde setup om de server op maximale capaciteit te gebruiken.

We ontvingen twee hoofdbestanden (en een andere set van 5 bestanden met key value pairs zoals mapping voor sommige van onze cck velden). Een die onze records met data bezat om onze andere cck velden en taxonomu te vullen, een tweede bestand met de coördinaten.
Allereerst hebben we alle bestanden in de database geladen. We gaven de eerste tabel een incremented id zodat we het later konden gebruiken om de voortgang te volgen (we gebruikte daarvoor een klein script). De tweede tabel voerde we in met gebruik van phpmyadmin. De lookup tabellen die we ook invoerde met phpmyadmin. Dit proces gaat heel snel. Binnen twee uur hadden we de files op de server en in de database.
Waarom we niet uit file lezen? We moesten het VAT id van het bedrijf in het tweede bestand opzoeken om de coördinaten voor het geschikte bedrijf te vinden. Dit is een taak die een database een stuk beter kan doen dan een file reading proces waarbij je het bestand regel voor regel moet scannen.

We creëerden tevens indexen op onze tabellen een primaire key op de auto inc veld en een unieke index op het VAT id in de eerste tabel en een primaire op het VAT id in de tweede tabel. Op de andere tabellen met lookups geven we de belangrijkste kollom een primaire key. We creëerden tevens een index op ons VAT id on onze cck tabel, daar we het zullen gebruiken om op te zoeken of een node gemaakt of geupdate moet worden. Indexes zijn erg belangrijk daar ze lookups versnellen in de database
(http://dev.mysql.com/doc/refman/5.0/en/mysql-indexes.html). Dus analyseer altijd uw database om te zien if een index geschikt is als je dingen importeert.

Nu we onze import script gecreëerd hebben dat in principe een regelin de database uit leest en een node object bouwt en een node_save om in drupal te komen. Daar is niks speciaals aan. Behalve dat wanneer u dat gewoon vanuit uw browser zou draaien het script time-out zal geven wanneer uw php max_execution_time bereikt is of wanneer php geen geheugen meer heeft. Zelfs als u memory_limit op maximaal instelt op uw server en max_execution_time op unlimited the script zou het nog steeds niet werken, daar het geheugen van de machines opgenomen wordt na een bepaalde tijd. Daarnaast is dit niet de manier om de voortgang op uw import te volgen. U kunt niet herstarten als iets mislukt...

Vanzelfsprekend denken we in drupal dat de batch api ons zal redden. Hier is de handboek pagina over hoe u een batch maakt
(http://drupal.org/node/180528) Nu hoeft u zich geen zorgen meer te maken over uw scripts die verlopen, de batch api zal ervoor zorgen dat het niet zoveel geheugen in beslag neemt. Er zijn twee manieren om een batch te maken, de eerste is op de handboek pagina behandeld. De tweede is iets anders daar het alleen een functie in de operation array gebruikt. We nemen de apachesolr reindex batch als voorbeeld. Waarom is dit nuttig, de batch api slaat zijn batch op in de batch tabel. Dus wanneer u een batch maakt die een miljoen operaties bevat en dat de array is geserialiseerd en in de database is gestopt, zullen er nare dingen gebeuren. Afhankelijk van uw mysql instellingen en server capaciteit zal dit mislukken. In mijn geval mislukte het rond 30k records.
U kunt nog steeds de eerste methode gebruiken, wat u doet is het in stukken opsplitsen van de batches en ze een voor een invoeren. Wat we deden maar niet met de reden van een te grote geserialiseerde array. Maar daar later meer over.

Bekijk deze snippet om te weten hoe u een batch api moet schrijven die alleen een functie gebruikt in de operations tabel. In ons geval is het enig wat u nodig heeft een variabele die volgt waar u bent.

<?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(), NULLWATCHDOG_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']) ? $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);
}
?>

Ok, dus nu kunnen we onze nodes invoeren zonder ons zorgen te maken over timeouts en het falen van onze batch. Maar we moeten nog steeds ons proces controleren voor het geval onze internet connectie verbreekt. We zouden de pagina moeten verversen en de batch laten doorgaan.
Dus het zou perfect zijn om een manier te hebben om een command te launchen en je nergens zorgen over te hoeven maken. Om dit te bereiken gebruiken we
http://drupal.org/project/drush en de cron functionality op de server.
U kunt uw eigen drush command kunnen schrijven dat een batch launches zoals je zou doen bij het oproepen van het script dat we reeds hadden. Echter werkt dit niet, uw geheugen zal uitgeput raken. Maar maakt u geen zorgen, er is een oplossing voor. U roept een drush command op dat in staat is om batches te doen. Dit drush command zal ervoor zorgen dat uw geheugen niet uitgeput raakt tijdens het uitvoeren van de batches. U kunt zien hoe het gebruikt is in de updatedb drush functie. Hier is mijn snippet over hoe ik het implementeer door middel van een custom drush command dat de functie oproept dat opgeroepen zou worden bij de "drush batch-process [batch-id]" command.

<?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_outTRUE17));   
  }
}
?>

Ok, dus nu typen we in de terminal iets als: drush import 1
1000 en het zal een batch creëeren, fire it en zal de eerste 1000 records importeren en er nodes voor creëren.
U kunt deze functie op laten roepen door cron zodat u niet eens een terminal open hoeft te hebben. Maar zoals eerder gezegd zijn we nog steeds bezig met het creëren van een operation voor elk record. Waarom? Wanneer de one operation truc gedaan wordt viel mij op dat slechts 30% van de cpu gebruikt werd (controleer dit door "top" te typen in een ander terminal venster). Dus ik dacht dat we meerdere shells kunnen voortbrengen en ze allemaal laten werken. Ik deed het en kwam erachter dat ik zes shells kon launchen met de drush import command. Bij de zevende de server cpu schoot naar 300% en liet de server crashen, dus zes shells is het limiet. Het is waarschijnlijk mogelijk om resources te meten en aan de hand daarvan de commands te launchen. Maar voor nu denk ik dat de server al zijn resources gebruikt en het importeren gaat zo snel mogelijk voor een handmatig proces.
Het laatste wat ik deed om het proces te automatiseren was het opzetten van een importcron.php in de drupal root installatie dat dit bevatte:

<?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($value0);
 
  if(
$amount && $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);         
  }                       
}                                                                  
?>

Typ vervolgens crontab -e om de cron listing te bewerken, typ i om dit command in te voeren en in te stellen:

* * * * * /usr/bin/php /var/www/html/your_drupal_site/cronimport.php

Typ het volledige path naar php en het volledige path naar uw bestand. Dit zal het command elke minuut laten uitvoeren. In het script zal een drush command uitgevoerd worden met de volgende 300 items die ingevoerd moeten worden. Om te voorkomen dat de command te vaak fired dient u altijd te controleren of de vorige batches afgerond zijn. Ons limiet was 6 batches tegelijkertijd. Als uw server krachtiger is kunt u de 1000 items en de 6 batches uitbreiden. Het zou fijn zijn om dit proces gecontroleerd te hebben door een functie die de server resources berekent en aan de hand daarvan de batches launched, maar daar moet ik wat onderzoek naar doen hoe dat moet.

Conclusie
Het script liep voor ongeveer 70 uur en de site bevatte 3 miljoen nodes. Hetzelfde principe werd gebruikt om te indexeren wat ongeveer 50 uur in beslag nam om alle nodes te indexeren. In de indexering hadden we enkele andere dingen aangepast om het sneller te laten gaan maar dat is iets voor een andere blog post.

Reactie toevoegen