A quoi sert l’undo (rollback segment), par Jonathan Lewis

Cet article est la traduction d’un article de Jonathan Lewis publié sur son blog. L’article original en anglais se trouve ici.

Merci à Mohamed Houri qui m’a aidé pour cette traduction.

L’article précédent Undo et Redo et Recovery en quelques mots nous montre que l’information undo est contenue dans le redo en tant que change vector pour les blocks d’undo. On peut donc se demander pourquoi Oracle a introduit les tablespaces d’undo (aussi connues comme tablespace des rollback segments) pour implémenter la lecture consistante, au lieu d’utiliser seulement les redo pour retrouver une image passée des données.

Un post sur le forum OTN pose la question suivante: Puisque le redo contient à la fois l’information passée et l’information courante, pourquoi donc Oracle n’utilise pas les redo logs pour récupérer cette information ? Pourquoi avoir besoin de l’undo alors que le redo a déjà cette information ?

Ce fil de discussion a généré des réponses intéréssantes, mais las plupart de ces réponses expliquent comment l’undo et le redo fonctionnent, plutôt que d’expliquer pourquoi les architectes d’Oracle Corp. ont choisi d’implémenter l’undo et le redo tel qu’ils l’ont fait. Comme je suis assis dans un aéroport (Zurich) à attendre un avion, je vais utiliser ce temps pour partager mon opinion sur le pourquoi.

Le redo est là pour deux raisons: pour la récupération des données en cas de panne (recoverability), et pour optimiser les performances (efficiency).
Si l’on journalise chaque modification que l’on fait sur la base de données, alors on peut toujours restaurer une ancienne copie de la base et réappliquer le journal des modifications pour amener la base de données à l’état courant. C’est la récupération en cas de panne.
Maintenant, si nous faisons en sorte que ces journaux soient le premier point de protection des modifications de nos données, alors on n’a plus besoin d’écrire sur disque chaque bloc de données au moment où on le modifie, mais seulement un faible volume d’information. C’est là qu’est l’optimisation. (Cependant, le fait de regrouper toutes les modifications dans un petit volume rends les I/O plus performants, mais on introduit aussi un point de contention vu que l’on concentre une grande activité sur un petit volume).

L’undo (d’une manière ou d’une autre) doit exister si l’on veut faire des lectures consistentes (read consistency). Personne n’est autorisé à voir les modifications de nos données tant que nous ne les avons pas comittées. Et mêmes commitées, nos modifications ne doivent pas être visibles des requêtes qui ont commencé avant notre commit. Il faut donc garder l’ancienne version des données quelque part. C’est où les choses deviennent moins intuitives, mais par contre très ingénieuses avec Oracle.

Observez l’enregistrement de redo (redo record) suivant. C’est un enregistrement de redo généré au milieu d’une transaction, et il représente le redo généré par Oracle lorsqu’on a modifié la quatrième colonne d’une ligne dans une table, de la valeur de 42 à la valeur 43.

REDO RECORD - Thread:1 RBA: 0x000056.00000028.0014 LEN: 0x0128 VLD: 0x01
SCN: 0x0000.0028efae SUBSCN: 3 02/09/2010 14:59:40
CHANGE #1 TYP:0 CLS:20 AFN:2 DBA:0x0080065f OBJ:4294967295 SCN:0x0000.0028efae SEQ: 24 OP:5.1
ktudb redo: siz: 112 spc: 5414 flg: 0x0022 seq: 0x0112 rec: 0x19
  xid: 0x0002.028.000009af
ktubu redo: slt: 40 rci: 24 opc: 11.1 objn: 49428 objd: 49428 tsn: 5
Undo type:  Regular undo       Undo type:  Last buffer split:  No
Tablespace Undo: No
             0x00000000
KDO undo record:
KTB Redo
op: 0x02  ver: 0x01
op: C uba: 0x0080065f.0112.18
Array Update of 1 rows:
tabn: 0 slot: 41(0x29) flag: 0x2c lock: 0 ckix: 11     <----- ligne 42 du bloc (slot indexé à partir de 0)
ncol: 4 nnew: 1 size: 0
KDO Op code:  21 row dependencies Disabled
  xtype: XAxtype KDO_KDOM2 flags: 0x00000080 bdba: 0x0180000a hdba: 0x01800009
itli: 2 ispac: 0 maxfr: 4863
vect = 9
col 3: [ 2] c1 2b     <----- la 4ème colonne revient à la valeur 42 (hexa:2b) (col indexée à partir de 0) 

CHANGE #2 TYP:0 CLS: 1 AFN:6 DBA:0x0180000a OBJ:49428 SCN:0x0000.0028efae SEQ: 41 OP:11.19
KTB Redo
op: 0x02  ver: 0x01
op: C  uba: 0x0080065f.0112.19
Array Update of 1 rows:
tabn: 0 slot: 41(0x29) flag: 0x2c lock: 2 ckix: 11     <----- ligne 42 du bloc
ncol: 4 nnew: 1 size: 0
KDO Op code:  21 row dependencies Disabled
  xtype: XAxtype KDO_KDOM2 flags: 0x00000080  bdba: 0x0180000a  hdba: 0x01800009
itli: 2  ispac: 0  maxfr: 4863
vect = 9
col 3: [ 2] c1 2c     <----- la 4ème colonne prends à la valeur 43 (hexa:2c)

Vous pouvez constater que cet enregistrement de redo comprend deux modifications (les change vectors CHANGE #1 and CHANGE #2) et j’ai annoté les deux lignes qui montrent que la valeur a été modifiée de 42 à 43. En principe, les redo logs d’Oracle contiennent effectivement l’information nécessaire à la génération de l’ancienne version des données. Alors, pourquoi Oracle a introduit les tablespaces d’undo pour la lecture consistente alors que les redo logs semblent avoir toute l’information nécessaire ?

Pour répondre à cette question, nous devons raisonner ‘multi-user‘. C’est très facile d’oublier qu’Oracle est un système multi-utilisateur, lorsqu’on essaie de comprendre certaines fonctionnalités. Mais si on ignore ce fait, on oublie de réfléchir à ce qui se passe du point de vue des autres sessions, et on oublie aussi de réfléchir sur leur impact.

1er point: Je suis en train d’exécuter une très longue transaction, et de nombreuses autres sessions exécutent des petites requêtes et des transactions très courtes. Les autres sessions doivent voir les données telles qu’elles étaient avant que je ne les aie modifiées. Si elles devaient lire le redo pour accéder à ces données, les fichiers redo logs devraient alors pouvoir atteindre la taille nécessaire pour contenir toute l’information de journalisation des modifications (nouvelles et anciennes valeurs) de toutes les sessions pour toutes les transactions qui se sont exécutées depuis que j’ai démarré ma transaction. Qe doit-on faire lorsque les fichier redo log sont pleins ?

2ème point: Si ma longue transaction se plante et doit faire un rollback, alors les informations des anciennes valeurs seront mélangées avec tous les changements (anciennes et nouvelles valeurs) que tout le monde a faits depuis que j’ai démarré ma transaction, et ma session va alors probablement devoir faire un grand nombre d’I/O aléatoires pour trouver les modifications antérieures. Bien sûr, on peut rendre cette tâche plus efficace (et aussi améliorer le point 1) en faisant en sorte que chaque transaction écrive son propre journal dans son propre fichier de redo. Mais cela rendrait plus complexe la gestion de fichiers (ouvrir et fermer des fichiers à chaque début et fin de transaction), ainsi que la récupération en cas de panne car il faut appliquer les redo dans le bon ordre.

3ème point: Oracle ne nettoie pas chaque bloc modifié lorsq’une transaction commite. Si je commite une transaction mais je ne nettoie pas le bloc X, comment la prochaine session qui va voir ce block saura que ma transaction a commité et quand est ce qu’elle a commité ? Si nous n’avions utilisé que les redo logs la prochaine session aurait du aller voir l’endroit du redo log où on a journalisé la modification du bloc X, et avancer jusqu’à ce qu’elle trouve la journalisation du commit (ou aller jusqu’à la fin du redo, ou trouver le SCN correspondant dans le redo et ainsi, en déduire que ma transaction n’a pas commitée ou n’a pas commité à temps). Mais alors, chaque enregistrement redo aurait du avoir un pointeur sur l’enregistrement redo qui le suit, ce qui veut dire qu’à chaque fois que nous aurions créé un enregistrement redo, nous aurions du aller voir le précédent enregistrement redo pour le modifier, ce qui ne serait pas très efficace. Le plan B serait d’avoir un mécanisme pour maintenir une longue liste infinie de toutes les transactions et de leur commit SCN quelque part dans la base de donnée – et c’est cet ‘infini’ qui n’est pas une idée séduisante.

Et cela peut se poursuivre ainsi. Vous pouvez trouver des opérations pour lesquelles il est parfaitement acceptable d’utiliser le redo comme source pour l’undo, mais si vous prenez en compte toutes les options d’un système multi-utilisateur avec une activité diverse, alors vous trouverez que certaines opérations auraient ou causeraient quelques problèmes.

La solution d’Oracle est de garder la partie ‘utilisable’ de l’information de retour arrière, l’undo, à un emplacement complètement différent de celui du redo. Plutôt que de créer une troisième structure de stockage de données, ils ont choisi de les mettre dans la base, en utilisant des blocs de données ordinaires. Vous pouvez réfléchir à la manière dont cela résout les points que j’ai soulevés ci-dessus.

Parce que les blocs d’undo sont des blocs de base de données ordinaires, ils sont protégés par les redo logs, et peuvent être récupérés en cas de panne, comme peut l’être le reste de la base de données, sans avoir à rajouter un code différent pour le recovery. Le change vector de retour arrière que nous avons vu précédemment est en réalité le change vector ‘vers l’avant’ pour rejouer la modification du bloc d’undo. Cela ajoute un overhead sur les redo log, car on écrit les undo deux fois, mais c’est ce que nous faisons de toute façon avec n’importe quelle modification (écriture dans le bloc et écriture dans le redo log). Les blocs d’undo bénéficient en plus du mécanisme standard de cache LRU: si un bloc d’undo est souvent utilisé, il reste en mémoire, et c’est parfait puisque l’undo généré récemment a plus de chance d’être celui dont on aura besoin pour la lecture consistante.

Comme nous gardons les flux d’undo dans la base de donnée, nous n’avons pas à nous préoccuper des sessions qui, constamment, ouvrent ou ferment des fichiers lorsqu’elles démarrent ou terminent des transactions. Nous avons juste à maintenir un catalogue qui liste les transactions actives et qui pointe sur l’adresse du tablespace d’undo où la transaction est en train de mettre ses enregistrements d’undo (c’est le rôle de la table des transactions de l’entête des segments d’undo – voir glossaire)

Parce que nous utilisons le bloc comme unité de stockage, et que l’undo de chaque transaction est à part, lorsque j’écris mes enregistrements d’undo ils sont regroupés ensembles (j’acquiers et je remplis un seul bloc à la fois). Donc, si je dois faire un rollback de ma transaction, la lecture d’un seul bloc ramènera beaucoup d’enregistrements d’undo, et cela minimisera les I/O aléatoires que je dois faire pour lire l’enchaînement d’undo.

Si je lance une très longue transaction alors que d’autres sessions sont aussi en train de générer du redo, il y a une certaine indépendance entre mon undo et celui de ces autres sessions. Dans un compromis entre ‘tous les undo dans un même fichier’ et ‘chaque transaction a son propre fichier’, Oracle a permis d’avoir des segments d’undo multiples, c’est à dire l’équivalent d’un relativement petit nombre de fichiers partagés. Ce qui veut dire que la longue transaction est cantonnée à un seul segment d’undo et que toutes les autres sessions peuvent réutiliser (en réécrivant) tout espace disponible dans le reste de la tablespace d’undo, de manière plus efficace. (Bien sûr on peut toujours se retrouver avec une erreur du type ‘fichier plein’, mais cette stratégie permet à la base de données de survivre plus longtemps lors d’activités concurrentes, sur le même volume d’espace disque).

Ce serait impossible d’expliquer tous les détails subtils des problèmes qui sont évités, ou minimisés, en stockant l’undo dans la base de données, et en l’utilisant comme Oracle le fait, mais l’idée est la suivante: si vous voulez savoir pourquoi Oracle utilise une tablespace d’undo plutôt que les redo logs pour ses lectures consistantes, pour ses rollbacks et pour calculer à quel moment le commit a eu lieu, alors il suffit uniquement de penser à tous les scénarios que vous auriez du prendre en compte et que vous auriez du résoudre seulement à l’aide des redo logs. Cela vous donnerait une idée de la surcharge et de la complexité que vous auriez du introduire, et vous aurait permis de voir à quel point les tablespaces d’undo rendent les choses (relativement) plus simple et plus efficaces.

Laisser un commentaire