jeudi 4 avril 2013

[SSAS][AMO] Construire son propre outil de gestion dynamique de cubes

Le modèle Tabular est arrivé. Mais le modèle dimensionnel n’a pas dit son dernier mot. Bon nombre d’entre vous possède encore des cubes dans ce mode. Et il faudra encore un peu de temps avant que les cubes volumineux puissent monter intégralement en mémoire.

Et justement, dans le cas où les cubes sont volumineux, il est indispensable d’avoir une stratégie de partitionnement précise afin de stabiliser les performances de calcul des cubes mais aussi d’obtenir de bonnes performances d’accès aux données.

Le partitionnement doit être ajusté aux spécificités de chaque partition et doit être aussi dynamique. Il doit maîtriser le nombre de partitions afin de ne pas saturer l’instance par un nombre de fichiers trop important qui aurait un effet contraire à celui escompté.

ATTENTION : Un trop grand nombre de partitions peut avoir des inconvénients comme le ralentissement des mises en production en alourdissant le script XMLA de déploiement, ou comme un temps de démarrage plus long du service SSAS, ou encore un ralentissement du temps de traitement des dimensions, ou bien encore un discover plus long des partitions, etc …

La modération dans la création du nombre de partition est de mise. Une stratégie réfléchie doit être appliquée.

Je vous donne un lien vers le « Analysis Services 2008 R2 Performance Guide » http://sqlcat.com/sqlcat/b/whitepapers/archive/2011/10/10/analysis-services-2008-r2-performance-guide.aspx.

Ce guide vous permettra d’affiner votre stratégie de partitionnement ( et plus encore).

    Rendre la gestion de son partitionnement dynamique est essentiel :
  • Chaque partition possède des spécificités propres (clé de partitionnement et granularité (requête source), type de stockage (MOLAP, ROLAP, HOLAP), emplacement de stockage, design d’agrégation) 
  • Chaque partition nécessite un mode de rafraichissement spécifique: Recalcule sur une période glissante, calcul de données dans le futur.
  • Les exigences de performances peuvent être variables en fonction de l’ancienneté des données. Il faut pouvoir profiter de cette exigence pour fusionner les partitions et pourquoi pas changer le type de stockage de MOLAP vers ROLAP. 
  • Créer les partitions à l’avance n’est pas une bonne chose. Il faut que l’application soit capable de créer les partitions quand il en a besoin et seulement dans ce cas.

Le traitement des dimensions doit lui aussi être dynamique. Dans un mode de croisière en fonction du type de modification on préferera le mode de processing ProcessAdd au ProcessUpdate et réciproquement. Néanmoins, il faudra que l’application soit capable de détecter le statut de processing de la dimension pour faire un ProcessFull quand ce dernier est obligatoire.

Pour toutes ces raisons, il faut rendre dynamique la gestion des processing des cubes. La meilleur approche pour le faire c’est d’utiliser l’API AMO via un language tel que Powershell, C# ou VB. Je pense que pour des raisons de maintenabilité et de cohérence d’infrastructure avec le reste de l’application ETL, il est préférable d’embarquer les scripts dans un package SSIS. De ce fait, mieux vaut préférer C# ou VB qui peuvent s’écrire dans des Scripts Tasks SSIS. Pour des raisons d’affinité avec le language C#, je construirais mes exemples autour ce celui-çi.

L’idée n’est pas de vous assister totalement mais de vous donner les clés pour construire votre outil. Il va de soi que toutes les règles de développement s’appliquent à savoir gestion des erreurs, variabilisation, création de classes, de méthodes, d’utilisation (ou pas) de listes génériques, …

Oui même en BI on soigne son code.

  1. Référence à la librairie.
  2. La première chose est de référencer notre librairie.
    using AMO = Microsoft.AnalysisServices;
    
  3. Connexion à la base :
  4. AMO.Server srv = new AMO.Server();
    srv.Connect("MonServeur");
    AMO.Database db = srv.Databases.FindByName(initialcatalog);
    
  5. Capture de la commande XMLA au serveur :
  6. Pour générer une commande à lancer au serveur il suffit de capturer un ensemble d’action entre 2 bornes marquées par la propriété CaptureXml. La valeur true signifie que la capture commence. La valeur false que la capture s’arrête.

    srv.CaptureXml = true;
    … (Voir dans le 3)
    srv.CaptureXml = false;
    
    
  7. Les différentes commandes :
  8. Voici quelques commandes possibles pour gérer vos cubes.

    1. suppression des partitions:
    2. Soit il est possible de supprimer l’intégralité des partitions de tous les cubes comme suit :
      foreach (AMO.Cube Cube in db.Cubes){
           foreach (AMO.MeasureGroup Meg in Cube.MeasureGroups){
                foreach (AMO.Partition part in Cube.Partitions){
                        part.Drop();     
                }
           }
      }
      
      

      Soit il est possible de supprimer une partition en fonction d’une liste :

      AMO.Cube cube = db.Cubes.FindByName("BaseASNomCube");
      AMO.MeasureGroup Meg = cube.MeasureGroups.FindByName("BaseASNomGroupeMesure");
      AMO.Partition part = Meg.Partitions.FindByName("BaseASNomPartition");
      part.Drop();
      
    3. La création des partitions :
    4. AMO.Partition part = Meg.Partitions.FindByName("NomPartition");
      if (part == null)
      {
      part = Meg.Partitions.Add("NomPartition");
      switch ("MonModeStockage")
      {
      case "molap":
      part.StorageMode = AMO.StorageMode.Molap;
      break;
      case "holap":
      part.StorageMode = AMO.StorageMode.Holap;
      break;
      case "rolap":
      part.StorageMode = AMO.StorageMode.Rolap;
      break;
      default:
      part.StorageMode = AMO.StorageMode.Molap;
      break;
      }
      
      datasource = db.DataSources.FindByName("MonDataSourceName");
      part.Source = new AMO.QueryBinding(datasource.ID, "MaRequête"]);
      
      // affectation du premier design d'aggregation trouve
                         
      if (Meg.AggregationDesigns.Count > 0)
      {
      part.AggregationDesignID = Meg.AggregationDesigns[0].ID;
      }
      }
      
      part.Update(AMO.UpdateOptions.ExpandFull);
      
    5. Le processing des dimensions :
    6. AMO.Dimension dim = db.Dimensions.FindByName("NomDimension");
      
      if (dim == null)
      {
      
      // Si les partitions ne sont pas processer alors on les process FULL sinon on applique le paramétrage.
      if (dim.State.ToString() == "Processed")
      {
      switch (row["ProcessingType"].ToString())
      {
      case "U":
      dim.Process(AMO.ProcessType.ProcessUpdate);
      break;
      case "F":
      dim.Process(AMO.ProcessType.ProcessFull);
      break;
      case "A":
      dim.Process(AMO.ProcessType.ProcessAdd);
      break;
      default:
      dim.Process(AMO.ProcessType.ProcessDefault);
      break;
      }
      }
      else
      dim.Process(AMO.ProcessType.ProcessFull);
      
      
    7. Le processing des partitions :
    8. AMO.Partition part = Meg.Partitions.FindByName(p.NomPartition);
      part.Process(AMO.ProcessType.ProcessFull);
      
      

      Il est possible de faire une ProcessData + un ProcessIndex, qui reviendrait au même que le process Full et serait même plus performant.

    9. Le processing des groupes de mesures liés :
    10. Le processing des groupes de mesure liés n’est pas automatique. Traiter le groupe de mesures auquel il est lié ne suffit pas.

      // parcours des cubes à la recherche des groupes de mesures liés
      foreach (AMO.Cube cube2 in db.Cubes)
      {
      foreach (AMO.MeasureGroup Meg2 in cube2.MeasureGroups)
      {
      if (Meg2.IsLinked)
      Meg2.Process(AMO.ProcessType.ProcessDefault);
      }
      }
      
      
  9. Lancement des commandes :
  10. Les commande d’exécute grâce à la méthode ExecuteCaptureLog qui prends 2 arguments booléen : Transaction et Parallel afin de jouer sur la parallélisation et sur la gestion des transactions (par lot ou à la fin).

    Exemple de code pour l’exécution. Attention j’ai utilisé une méthode et une variable non définit dans cet article.

    AMO.XmlaResultCollection results = srv.ExecuteCaptureLog(, parallel);
    
    foreach (AMO.XmlaResult result in results)
    {
    foreach (AMO.XmlaMessage message in result.Messages)
    {
    if (message.GetType().Name == "XmlaError")
    {
    statutexecution = false;
    ScriptResult(srv, statutexecution,message.Description);
    }
    else
    {
    statutexecution = true;
    ScriptResult(srv, statutexecution, message.Description);
    }
    }
    }
    
    
  11. Récupération du contenu d’une variable de type object dans un script task :
  12. Il peut s’avérer utile dans ce type de tâche de ramener des informations de listes dans un script et de les récupérer.

    Pour se faire :

    DataTable dt = new DataTable();
    OleDbDataAdapter adapter = new OleDbDataAdapter();
    adapter.Fill(dt, Dts.Variables["ListePartitionASupprimer"].Value);
    
    //Puis on fini par un dispose
    adapter.Dispose();
    dt.Dispose();
    
    

    Ensuite vous pouvez récupérer les valeurs des colonnes pour chaque ligne comme suit :

    foreach (DataRow row in dt.Rows)
    {
          row["BaseASNomCube"].ToString())
    //etc …
    }
    
    
  13. Ajout de journalisation dans votre package :
  14. Si vous souhaitez récupérer des informations de jounalisation dans votre table de log (sys.ssislog). Voilà ce que vous pouvez ajouter dans votre code :

    Dts.Log("Création de la partition machin", 0, null);
    Dts.Events.FireWarning(0,Dts.Variables["System::TaskName"].Value.ToString(),"Mon message", "", 0);
    

    Si vous souhaitez faire échouer votre package vous pouvez utiliser cette commande :

    Dts.Events.FireError(0, Dts.Variables["System::TaskName"].Value.ToString(), description, "", 0);
    
  15. Ecriture de la commande XMLA dans un fichier :
  16. Il peut être intéressant de générer le fichier XMLA lancé au serveur dans une optique de Debug.

    Voici un code qui pourrait vous être utile.

    StringCollection CapturedXmla = new StringCollection();
    CapturedXmla = srv.CaptureLog;
    if (CapturedXmla.Count > 1){
    StreamWriter sw = new StreamWriter(FileName);
    sw.AutoFlush = true;
    sw.Write(srv.ConcatenateCaptureLog(true, true));
    sw.Close();
    }
    
    

J’espère que tous ces petits bout de code pourront vous permettre d’aller au bout de vos projets ou bien que cet article vous aura permis de réfléchir autour de votre stratégie d’exploitation de vos cubes SSAS.