0

Les Chaines Oracle

twitterlinkedinmail

Hello amis dba’s et bienvenue sur cet article !

Au menu aujourd’hui un cas concret d’utilisation d’un sujet assez peu abordé et pourtant bien utile: les chaines Oracle.

Les chaines sont un composant du planificateur Oracle qui permet de gérer une série d’actions déclenchées hiérarchiquement un peu à la manière d’un ordonnanceur. On réalise une action A si celle-ci échoue on part sur une action B sinon on lance C etc etc..

Dans le cas d’utilisation qui suit nous étudions un calcul de statistiques hebdomadaire sur une base de un peu plus de 1 To qui dure approximativement 28h et pour lequel un client nous demande une amélioration.

Nous partons d’une édition standard (donc pas de parallélisme) en 10.2.0.4 et passerons sur les modifications faites à la commande de calcul elle-même (auto-echantillonnage, exclusion des tables de travail applicatives etc)

Globalement partant de l’idée qu’un calcul de statistiques réalise un calcul séquentiel par objet nous allons “simuler” une forme de parallélisme en nous appuyant sur les chaines:

Une chaine est associée à un job qui est créé dynamiquement par Oracle ou défini explicitement, ce job lance lui-même des programmes ou d’autres chaines imbriquées qui créent elles-même d’autres jobs qui lancent des programmes. Si vous n’avez rien compris c’est normal moi non plus 🙂 donc voyons de quelle manière cela peut être utile pour notre cas.

Nous souhaitons avoir une procédure qui accepte en paramètre le “degré” de parallélisme et en paramètre optionnel le nom du schéma concerné par le calcul de statistique (par défaut la valeur est % donc tous les schémas seront embarqués s’il n’y en a pas un de précisé). En substance nous allons créer une première chaine “MASTER” qui lancera en simultanée autant de chaine “SLAVE” que le degré de parallélisme indiqué. Chaque chaine “SLAVE” se verra attribuer des tâches qui seront elles-mêmes lancées séquentiellement, chacune de ces tâches correspondantes a un calcul de stats pour une seule table. la procédure est là pour créer dynamiquement le jeu de chaine qui va bien pour arriver à cette organisation (merci paint pour la qualité irréprochable de ce schéma :)):

 

Dans le vocabulaire oracle une tâche correspond à un programme qui pour nous correspond à une commande de calcul de statistiques pour une seule table. Chaque étape ou step de chaque chaine slave lance un programme et donc un calcul de statistique pour une table.

Le début de la procédure se charge de créer le jeu de chaine complet (maitre/esclave):

(...)
   /* Debut phase de création des chaines maitres/esclaves */
   /* debut création de la chaine maitre */
   SYS.DBMS_SCHEDULER.CREATE_CHAIN (chain_name            => 'MASTER_CHAIN',
                                    rule_set_name         => NULL,
                                    evaluation_interval   => NULL,
                                    comments              => NULL);
   SYS.DBMS_SCHEDULER.ENABLE (name => 'MASTER_CHAIN');

   /* fin création de la chaine maitre */

   FOR I IN 1 .. P_FACTOR
   LOOP
      v_chain_start := v_chain_start || 'SLAVE_CHAIN_' || I || ',';
      v_chain_stop := v_chain_stop || 'SLAVE_CHAIN_' || I || ' COMPLETED AND ';

      /* debut creation des chaines esclaves qui seront lancées en paralléle par la chain maitre */
      SYS.DBMS_SCHEDULER.CREATE_CHAIN (chain_name            => 'SLAVE_CHAIN_' || I,
                                       rule_set_name         => NULL,
                                       evaluation_interval   => NULL,
                                       comments              => NULL);
      SYS.DBMS_SCHEDULER.ENABLE (name => 'SLAVE_CHAIN_' || I);
      /* pour chaque chaine slave un step dans la chaine master correspondant doit être créé */
      SYS.DBMS_SCHEDULER.DEFINE_CHAIN_STEP (
         chain_name     => 'MASTER_CHAIN',
         step_name      => 'SLAVE_CHAIN_' || I,
         program_name   => 'SLAVE_CHAIN_' || I);
   END LOOP;

 (...)

La variable P_FACTOR correspond à la valeur passée en paramètre de la procédure qui détermine le nombre de threads de chaines esclaves qui seront créés. Dans la boucle chaque chaine esclave est définie comme une étape de la chaine maitre. il s’agit de nested chain ou chaines imbriquées. c’est la commande DBMS_SCHEDULER.CREATE_CHAIN qui est utilisée pour ça.

Une chaine oracle doit comporter des étapes mais aussi des règles de déclenchement, par défaut si aucune règle n’existe aucun step n’est lancé, même s’ils sont déjà présents dans la chaine.

Pour définir ces règles on utilise DBMS_SCHEDULER.DEFINE_CHAIN_RULE

 
 (...)

  /* fin creation des chaines esclaves qui seront lancées en paralléle par la chain maitre */

   /* la ligne ci-dessous sert a créer la règle qui permet de lancer tous les process esclaves en parallèle */
   DBMS_SCHEDULER.DEFINE_CHAIN_RULE ('MASTER_CHAIN',
                                     'TRUE',
                                     TRIM (TRAILING ',' FROM v_chain_start));

   DBMS_SCHEDULER.DEFINE_CHAIN_RULE (
      'MASTER_CHAIN',
      SUBSTR (v_chain_stop, 1, LENGTH (v_chain_stop) - 5),
      'END');

(...)

Indépendamment des relations qui peuvent exister entre chaque step, la chaine doit avoir une condition qui se vérifie tout le temps pour permettre à au moins une première étape de se déclencher lorsqu’aucun step de départ n’est indiqué dans la commande de lancement. Elle doit également comporter une condition de sortie, de préférence une condition qui a 100% de chance de se produire. Dans le cas ci-dessus même si on ne voit pas très bien a quoi correspondent les variable v_chain_start & v_chain_stop ( je compose l’action à la volée car il dépend de P_FACTOR qui n’est pas fixe) j’indique à Oracle: “Lorsque TRUE renvoit vrai (tout le temps à priori) tu démarres toutes les chaines esclaves, et lorsqu’elles sont toutes completed la chaine maitre se termine”

A ce stade, nous devons créer un programme par table et les affecter en tant que step à chaque chaine esclave:

 
FOR table_courante
      IN (  SELECT table_name, owner
              FROM dba_tables
             WHERE     owner LIKE utilisateur
                   AND NOT REGEXP_LIKE (table_name, '^TMP_[0-9]+$')
                   AND table_name NOT LIKE 'TMP\_%\__\_________' ESCAPE '\'
                   AND table_name NOT LIKE '%TEST%'
                   AND table_name NOT LIKE 'BIN$%'
                   AND OWNER NOT IN
                          ('SYS',
                           'SYSTEM',
                           'TSMSYS',
                           'ORACLE_OCM',
                           'DIP',
                           'DBSNMP',
                           'DBSNMP',
                           'OUTLN',
                           'WMSYS')
          ORDER BY BLOCKS DESC NULLS LAST)
   LOOP
      SYS.DBMS_SCHEDULER.CREATE_PROGRAM (
         program_name          => 'SYS.PRG_PARA_' || v_cpt_prg,
         program_type          => 'PLSQL_BLOCK',
         program_action        => 'BEGIN 
                                dbms_stats.gather_table_stats(
                                    ownname=>'
                                 || DBMS_ASSERT.enquote_literal (
                                       table_courante.owner)
                                 || ',
                                    tabname=>'
                                 || DBMS_ASSERT.enquote_literal (
                                       table_courante.table_name)
                                 || ',
                                    granularity=>''ALL'',
                                    estimate_percent=>DBMS_STATS.AUTO_SAMPLE_SIZE,
                                    cascade=>TRUE,
                                    degree=>DBMS_STATS.AUTO_DEGREE);
                                END;',
         number_of_arguments   => 0,
         enabled               => TRUE,
         comments              => NULL);

      SYS.DBMS_SCHEDULER.DEFINE_CHAIN_STEP (
         chain_name     => 'SLAVE_CHAIN_' || v_curr_slave_chain,
         step_name      => 'STEP_PARA_' || v_cpt_prg,
         program_name   => 'SYS.PRG_PARA_' || v_cpt_prg);

      /* debut ajout du prog courant en tant que step du process slave courant */
      SYS.DBMS_SCHEDULER.DEFINE_CHAIN_STEP (
         chain_name     => 'SLAVE_CHAIN_' || v_curr_slave_chain,
         step_name      => 'STEP_PARA_' || v_cpt_prg,
         program_name   => 'SYS.PRG_PARA_' || v_cpt_prg);
      /* fin ajout du prog courant en tant que step du process slave courant */

      /* v_curr_slave_chain  :cette variable me sert à me positionner sur la chaine esclave suivante */
      v_curr_slave_chain := v_curr_slave_chain + 1;
      v_cpt_prg := v_cpt_prg + 1;

      IF v_curr_slave_chain > P_FACTOR
      THEN
         v_curr_slave_chain := 1;
      END IF;
   END LOOP;

   /* fin de la distribution des tâches entre les process slave */

Dans cette partie les programmes correspondent à des calcul de statistiques mais il pourrait s’agir de tout autre chose. Chaque step est affecté à une chaine de manière circulaire ajouté à l’order by BLOCK de la requête se sélection des tables ceci permet de ne pas affecter les tables les plus lourdes à une seule et même chaine esclave qui pourrait finir bien après les autres et limiter l’intérêt de paralléliser.

Comme on a vu précédemment une chaine a besoin d’étapes et de conditions. Pour créer ces conditions on utilise deux boucles imbriquées:

 
  FOR CHAINE_COURANTE
      IN (  SELECT CHAIN_NAME,
                   MAX (step_name) "STARTING_STEP",
                   MIN (step_name) "ENDING_STEP"
              FROM DBA_SCHEDULER_CHAIN_STEPS
             WHERE CHAIN_NAME LIKE 'SLAVE_CHAIN%'
          GROUP BY chain_name)
   LOOP
      DBMS_SCHEDULER.DEFINE_CHAIN_RULE (
         CHAINE_COURANTE.CHAIN_NAME,
         'TRUE',
         'START ' || CHAINE_COURANTE.STARTING_STEP); --je positionne mon step de depart
      DBMS_SCHEDULER.DEFINE_CHAIN_RULE (
         CHAINE_COURANTE.CHAIN_NAME,
         CHAINE_COURANTE.ENDING_STEP || ' COMPLETED',
         'END');                              -- je positionne mon step de fin
      v_last_step := CHAINE_COURANTE.STARTING_STEP; --cette variable mer sert a connaitre le dernier step exécuté donc le step de démarrage a ce stade la procedure

      FOR STEP_COURANT
         IN (  SELECT STEP_NAME
                 FROM DBA_SCHEDULER_CHAIN_STEPS
                WHERE CHAIN_NAME = CHAINE_COURANTE.CHAIN_NAME
                      AND STEP_NAME <> CHAINE_COURANTE.STARTING_STEP
             ORDER BY REPLACE (step_name, '_', 'Z') DESC)
      LOOP
         /* ici on balaye l'ensemble des steps de la chain en cours en triant le nom par ordre decroissant ce qui nous fera finir par le step de fin, j'exclue le step de démarrage
         car celui-ci a déjà été manuellement positionné ci-dessus*/

         --DBMS_OUTPUT.PUT_LINE('chaine: '||CHAINE_COURANTE.CHAIN_NAME||', STEP: '||STEP_COURANT.STEP_NAME); --affichage de controle oseb
         DBMS_SCHEDULER.DEFINE_CHAIN_RULE (
            CHAINE_COURANTE.CHAIN_NAME,
            v_last_step || ' COMPLETED',
            'START ' || STEP_COURANT.STEP_NAME);

         /*cette regle me permet d'indiquer à oracle que le dernier step complété lance le step courant dans la boucle qui devient alors le dernier step lancé et ainsi de suite jusqu'au dernier*/

         v_last_step := STEP_COURANT.STEP_NAME;
      END LOOP;
   END LOOP;
/* FIN phase de création des chaines maitres/esclaves */

Une fois ces règles définies il ne reste plus qu’à lancer la chaine maitre qui lancera en cascade toutes les chaines esclaves et avec le système poussé de logging du scheduler oracle nous allons pouvoir connaitre quel temps on a mis pour chaque table individuellement.

Comme avec le parallélisme classique il faut être très vigilant sur la consommation cpu que cela peut engendrer, donc ne pas exagérer le nombre de thread créés…

Pour ceux qui souhaitent faire le test voici le code complet de la procédure:

 

 
CREATE OR REPLACE PROCEDURE SYS.CALCUL_STATS_PARALLEL (
   P_FACTOR       NUMBER,
   Utilisateur    VARCHAR2 DEFAULT '%')
IS
   v_code               NUMBER;
   v_errm               VARCHAR2 (64);
   v_chain_start        VARCHAR2 (30000) := 'START ';
   v_chain_stop         VARCHAR2 (30000) := NULL;
   v_curr_slave_chain   NUMBER := 1;
   v_last_step          VARCHAR2 (30);
   v_cpt_prg            NUMBER (5) := 0;
   CLEAN                VARCHAR2 (32);
   cpt                  NUMBER (10) := 1;
BEGIN
   /*********************************************************************************************/
   /* C@pdata LE 17 mai 2016
   /********************************************************************************************/

   /* Debut phase de création des chaines maitres/esclaves */
   /* debut création de la chaine maitre */
   SYS.DBMS_SCHEDULER.CREATE_CHAIN (chain_name            => 'MASTER_CHAIN',
                                    rule_set_name         => NULL,
                                    evaluation_interval   => NULL,
                                    comments              => NULL);
   SYS.DBMS_SCHEDULER.ENABLE (name => 'MASTER_CHAIN');

   /* fin création de la chaine maitre */

   FOR I IN 1 .. P_FACTOR
   LOOP
      v_chain_start := v_chain_start || 'SLAVE_CHAIN_' || I || ',';
      v_chain_stop := v_chain_stop || 'SLAVE_CHAIN_' || I || ' COMPLETED AND ';

      /* debut creation des chaines esclaves qui seront lancées en paralléle par la chain maitre */
      SYS.DBMS_SCHEDULER.CREATE_CHAIN (chain_name            => 'SLAVE_CHAIN_' || I,
                                       rule_set_name         => NULL,
                                       evaluation_interval   => NULL,
                                       comments              => NULL);
      SYS.DBMS_SCHEDULER.ENABLE (name => 'SLAVE_CHAIN_' || I);
      /* pour chaque chaine slave un step dans la chaine master correspondant doit être créé */
      SYS.DBMS_SCHEDULER.DEFINE_CHAIN_STEP (
         chain_name     => 'MASTER_CHAIN',
         step_name      => 'SLAVE_CHAIN_' || I,
         program_name   => 'SLAVE_CHAIN_' || I);
   END LOOP;

   /* fin creation des chaines esclaves qui seront lancées en paralléle par la chain maitre */

   /* la ligne ci-dessous sert a créer la règle qui permet de lancer tous les process esclaves en parallèle */
   DBMS_SCHEDULER.DEFINE_CHAIN_RULE ('MASTER_CHAIN',
                                     'TRUE',
                                     TRIM (TRAILING ',' FROM v_chain_start)); /* le trim sert a retirer la derniere virgule qui casse les c.. a la fin de la variable v_chain_start */
   /* la ligne ci-dessous sert à créer la regle de fin d'exécution de la chaine maitre on remanipule un peu la chaîne au passage */

   DBMS_SCHEDULER.DEFINE_CHAIN_RULE (
      'MASTER_CHAIN',
      SUBSTR (v_chain_stop, 1, LENGTH (v_chain_stop) - 5),
      'END');

   --dbms_output.put_line('chaine: '||substr(v_chain_stop,1,length(v_chain_stop)-5));
   /* debut de la distribution des tâches entre les process slave */

   FOR table_courante
      IN (  SELECT table_name, owner
              FROM dba_tables
             WHERE     owner LIKE utilisateur
                   AND NOT REGEXP_LIKE (table_name, '^TMP_[0-9]+$')
                   AND table_name NOT LIKE 'TMP\_%\__\_________' ESCAPE '\'
                   AND table_name NOT LIKE '%TEST%'
                   AND table_name NOT LIKE 'BIN$%'
                   AND OWNER NOT IN
                          ('SYS',
                           'SYSTEM',
                           'TSMSYS',
                           'ORACLE_OCM',
                           'DIP',
                           'DBSNMP',
                           'DBSNMP',
                           'OUTLN',
                           'WMSYS')
          ORDER BY BLOCKS DESC NULLS LAST)
   LOOP
      SYS.DBMS_SCHEDULER.CREATE_PROGRAM (
         program_name          => 'SYS.PRG_PARA_' || v_cpt_prg,
         program_type          => 'PLSQL_BLOCK',
         program_action        => 'BEGIN 
                                dbms_stats.gather_table_stats(
                                    ownname=>'
                                 || DBMS_ASSERT.enquote_literal (
                                       table_courante.owner)
                                 || ',
                                    tabname=>'
                                 || DBMS_ASSERT.enquote_literal (
                                       table_courante.table_name)
                                 || ',
                                    granularity=>''ALL'',
                                    estimate_percent=>DBMS_STATS.AUTO_SAMPLE_SIZE,
                                    cascade=>TRUE,
                                    degree=>DBMS_STATS.AUTO_DEGREE);
                                END;',
         number_of_arguments   => 0,
         enabled               => TRUE,
         comments              => NULL);

      SYS.DBMS_SCHEDULER.DEFINE_CHAIN_STEP (
         chain_name     => 'SLAVE_CHAIN_' || v_curr_slave_chain,
         step_name      => 'STEP_PARA_' || v_cpt_prg,
         program_name   => 'SYS.PRG_PARA_' || v_cpt_prg);

      /* debut ajout du prog courant en tant que step du process slave courant */
      SYS.DBMS_SCHEDULER.DEFINE_CHAIN_STEP (
         chain_name     => 'SLAVE_CHAIN_' || v_curr_slave_chain,
         step_name      => 'STEP_PARA_' || v_cpt_prg,
         program_name   => 'SYS.PRG_PARA_' || v_cpt_prg);
      /* fin ajout du prog courant en tant que step du process slave courant */

      /* v_curr_slave_chain  :cette variable me sert à me positionner sur la chaine esclave suivante */
      v_curr_slave_chain := v_curr_slave_chain + 1;
      v_cpt_prg := v_cpt_prg + 1;

      IF v_curr_slave_chain > P_FACTOR
      THEN
         v_curr_slave_chain := 1;
      END IF;
   END LOOP;

   /* fin de la distribution des tâches entre les process slave */

   FOR CHAINE_COURANTE
      IN (  SELECT CHAIN_NAME,
                   MAX (step_name) "STARTING_STEP",
                   MIN (step_name) "ENDING_STEP"
              FROM DBA_SCHEDULER_CHAIN_STEPS
             WHERE CHAIN_NAME LIKE 'SLAVE_CHAIN%'
          GROUP BY chain_name)
   LOOP
      DBMS_SCHEDULER.DEFINE_CHAIN_RULE (
         CHAINE_COURANTE.CHAIN_NAME,
         'TRUE',
         'START ' || CHAINE_COURANTE.STARTING_STEP); --je positionne mon step de depart
      DBMS_SCHEDULER.DEFINE_CHAIN_RULE (
         CHAINE_COURANTE.CHAIN_NAME,
         CHAINE_COURANTE.ENDING_STEP || ' COMPLETED',
         'END');                              -- je positionne mon step de fin
      v_last_step := CHAINE_COURANTE.STARTING_STEP; --cette variable mer sert a connaitre le dernier step exécuté donc le step de démarrage a ce stade la procedure

      FOR STEP_COURANT
         IN (  SELECT STEP_NAME
                 FROM DBA_SCHEDULER_CHAIN_STEPS
                WHERE CHAIN_NAME = CHAINE_COURANTE.CHAIN_NAME
                      AND STEP_NAME <> CHAINE_COURANTE.STARTING_STEP
             ORDER BY REPLACE (step_name, '_', 'Z') DESC)
      LOOP
         /* ici on balaye l'ensemble des steps de la chain en cours en triant le nom par ordre decroissant ce qui nous fera finir par le step de fin, j'exclue le step de démarrage
         car celui-ci a déjà été manuellement positionné ci-dessus*/

         --DBMS_OUTPUT.PUT_LINE('chaine: '||CHAINE_COURANTE.CHAIN_NAME||', STEP: '||STEP_COURANT.STEP_NAME); --affichage de controle oseb
         DBMS_SCHEDULER.DEFINE_CHAIN_RULE (
            CHAINE_COURANTE.CHAIN_NAME,
            v_last_step || ' COMPLETED',
            'START ' || STEP_COURANT.STEP_NAME);

         /*cette regle me permet d'indiquer à oracle que le dernier step complété lance le step courant dans la boucle qui devient alors le dernier step lancé et ainsi de suite jusqu'au dernier*/

         v_last_step := STEP_COURANT.STEP_NAME;
      END LOOP;
   END LOOP;
/* FIN phase de création des chaines maitres/esclaves */

/* Debut phase de lancement et monitoring */
DBMS_SCHEDULER.RUN_CHAIN
   (
    chain_name => 'SYS.MASTER_CHAIN'
    ,start_steps => null
   );--lancement de la chaine maitre

WHILE cpt <> 0
   LOOP
      SELECT COUNT (*)
        INTO cpt
        FROM dba_scheduler_running_chains
       WHERE chain_name = 'MASTER_CHAIN';
       DBMS_LOCK.SLEEP (30);
   END LOOP;

/* FIN phase de lancement et monitoring */

/* DEBUT nettoyage des chaines */

FOR CLEAN in (
SELECT   chain_name
  FROM SYS.DBA_SCHEDULER_CHAINS
WHERE chain_name LIKE 'MASTER_%' OR chain_name LIKE 'SLAVE_%'
                    ) LOOP
dbms_scheduler.drop_chain(CLEAN.CHAIN_NAME,force  => TRUE);
END LOOP;
/* FIN nettoyage des chaines */
/* DEBUT nettoyage des programmes */
FOR CLEAN in (
SELECT  P.PROGRAM_NAME
  FROM SYS.DBA_OBJECTS O, SYS.DBA_SCHEDULER_PROGRAMS P
 WHERE     O.OWNER = P.OWNER
       AND O.OBJECT_NAME = P.PROGRAM_NAME
       AND O.OBJECT_TYPE = 'PROGRAM'
       AND O.OWNER = 'SYS'
       AND PROGRAM_TYPE = 'PLSQL_BLOCK'
       AND PROGRAM_NAME LIKE 'PRG_PARA_%') LOOP
DBMS_SCHEDULER.DROP_PROGRAM(CLEAN.PROGRAM_NAME);
END LOOP;
/* FIN nettoyage des programmes */

EXCEPTION
   WHEN OTHERS
   THEN
      v_code := SQLCODE;
      v_errm := SUBSTR (SQLERRM, 1, 64);
      DBMS_OUTPUT.PUT_LINE (
         'Sortie de la procedure en erreur: ORA-' || v_code || '- ' || v_errm);
END;
/

 

N’hésitez pas à nous faire des retours 😉

Continuez votre lecture sur le blog :

twitterlinkedinmail

Capdata team

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.