Fehlertolerante Datenstrukturen

Werbung
Fehlertolerante Datenstrukturen
Henning Kühn
Technische Universität Dortmund
[email protected]
Zusammenfassung In dieser Ausarbeitung werden Verfahren zur Fehlererkennung und Möglichkeiten zu deren Behebung mit Hilfe fehlertoleranter Datenstrukturen beschrieben. Dabei wird genauer auf modified(2)
double-linked lists als spezielle Datenstruktur und die Spezifikation von
Consistency Constraints zum Umgang mit Fehlern eingegangen. Zum
Schluss werden mit Resilient Data Structures Datenstrukturen vorgestellt, die auch in Gegenwart von nicht korrigierten Fehlern noch sinnvoll
zu benutzen sind.
1
Motivation
Warum ist es sinnvoll, Fehlertoleranz zu Datenstrukturen hinzuzufügen? Im Idealfall sollten Fehler natürlich gar nicht erst auftreten, doch lässt sich dies nicht
immer vermeiden. Es gibt im realen Betrieb eine Vielzahl möglicher Fehlerquellen. Hardware kann durch immer weiter voranschreitende Miniaturisierung unzuverlässiger werden, Umgebungsbedingungen wie ionisierende Strahlung können
Fehler hervorrufen. Eine Erkennung auf Hardwareebene ist zwar möglich aber
aufgrund hoher Komplexität und der damit verbundenen Kosten nicht immer
zweckmäßig.
Auch wenn die Hardware zuverlässig arbeiten sollte, ist immer noch die darrauf laufende Software selber als Fehlerquelle nicht auszuschließen. So kann ein
Algorithmus ganz einfach fehlerhaft sein und in manchen Fällen ein inkorrektes Ergebnis produzieren. Es können irgendwo ungültige Annahmen gemacht
worden sein, es wird falsches Locking bei mehreren Threads, die auf die gleiche
Datenstruktur zugreifen, verwendet, oder ein Speicherbereich kann aus Versehen
überschrieben werden.
In diesen Fällen ist Fehlertoleranz günstig und flexibel mit Software zu realisieren. Ein Minimalziel hierbei ist zumindest das Erkennen von Fehlern, um
darauf reagieren zu können. Wenn möglich wird eine Wiederherstellung des Originalzustands versucht, andernfalls kann möglicherweise ein beliebiger gültiger
Zustand hergestellt werden wenn die Ausführungsbedingungen dies zulassen.
Notfalls muss die Existenz von Fehlern hingenommen werden, dabei sollten zumindest die noch gültigen Daten von den Fehlerhaften unbeeinflusst bleiben.
2
Begriffsklärung
Versagen
Beobachtbares Ereignis das auftritt, wenn ein System nicht seinen Spezifikationen entspricht
fehlerhafter Zustand
Zustand, der zu einem Versagen des Systems führen
kann
Fehler
Teil eines Zustands, der zum Versagen führen kann
Störung
Ursache eines Fehlers
Fehlertolerantes System System, welches verhindert, dass fehlerhafte
Zustände zum Versagen führen
3
3.1
Möglicher Umgang mit Fehlern
Fehlererkennung und -behebung durch Redundanz
Um Fehler erkennen zu können, müssen genügend Informationen in der Datenstruktur vorhanden sein, um durch sich ergebende Widersprüche aus mehrfach
vorhandenen Informationen auf Inkonsistenz schließen zu können. Ein einfaches
Beispiel ist die Angabe der erwarteten Anzahl der Elemente im Header der Datenstruktur im Vergleich zur Anzahl der tatsächlich vorhandenen oder erreichbaren Elemente.
Statt auf Informationen über den Zustand der Datenstruktur kann Redundanz auch auf die Datenelemente selber angewendet werden. Trivialerweise
müssen hierzu bei maximal δ zu erwartenden Fehlern 2δ+1 Kopien eines Datums
gespeichert werden. So können alle Kopien auf Gleichheit überprüft werden, bei
sich widersprechenden Daten wird die größte Anzahl an gleichen Daten als richtig angenommen. Dies bringt jedoch einen von δ abhängigen, unter Umständen
nicht zu vertretend großen zusätzlichen Aufwand bei jedem Elementzugriff mit
sich [3].
3.2
Consistency Constraints
Eine weitere Möglichkeit mit Fehlern umzugehen besteht darin, für die betrachteten Daten Consistency Constraints anzugeben, deren Einhaltung hin und wieder
überprüft wird. Wird eine Inkonsistenz festgestellt, kann versucht werden diese
zu beheben, um mit der Programmausführung fortsetzen zu können. Die wiederhergestellten Daten müssen dabei nicht den ursprünglichen, fehlerfreien Daten
entsprechen. Wichtig ist nur die Konsistenz der Datenstruktur.
3.3
Resilient Data Structures
Wenn man Fehler nicht beheben will oder kann, weil dies vielleicht zu kostspielig
wäre, kann man Resilient Data Structures benutzen. Diese garantieren auch in
Gegenwart von Fehlern ein bestimmtes Verhalten bezüglich der noch gültigen
Daten.
4
4.1
Redundanz
Arten von Redundanz
Neben der eingangs bereits erwähnten Möglichkeit der trivialen mehrfachen Speicherung der zu schützenden Daten selber, kann auch strukturelle Redundanz innerhalb einer Datenstruktur zur Anwendung kommen. Häufig angewandte Techniken, um Redundanz zu Datenstrukturen hinzuzufügen:
– Speichern der Anzahl der enthaltenen Elemente
Ein Feld mit der Anzahl der Elemente kann zum Header der Datenstruktur
hinzugefügt werden.
– Eindeutiges Identifikationsfeld
Jeder Datenstruktur wird eine eindeutige Identifikationsnummer zugewiesen welche in jedem Element gespeichert wird um dessen Zugehörigkeit zur
Struktur anzugeben. Es können auch Informationen zum erwarteten Typ
des Elements gespeichert werden, bei einem Baum z.B. “Blatt” oder “innerer Knoten”.
– Zusätzliche Zeiger
Es können voneinander unabhängige Zeiger gespeichert werden wie z.B. die
next- und back-Zeiger in doppelt verketteten Listen mit denen sich jeweils
die Datenstruktur rekonstruieren lässt.
4.2
Erkennbarkeit, Korrigierbarkeit
Es ist möglich, Datenstrukturen durch bestimmte Änderungen von einem gültigen Zustand in einen anderen gültigen, aber möglicherweise fehlerhaften Zustand
zu überführen. Wird der next-Zeiger eines Elements einer einfach verketteten
Liste ohne Speicherung der Elementanzahl auf NULL gesetzt, ergibt sich wieder
eine gültige Liste, es wurden jedoch durch nur eine einzige Änderung Daten
verloren ohne dass dies erkannt oder behoben werden kann.
Wird die zu erwartende Elementanzahl der Liste in einem Header gespeichert,
kann zumindest dieser Datenverlust erkannt werden, auch wenn nichts dagegen
unternommen werden kann.
Fehler können erkannt werden, wenn eine Datenstruktur durch eine oder
mehrere Änderungen in einen ungültigen Zustand gelangt.
Definition 1. Sind N + 1 Änderungen notwendig, um von einem gültigen Zustand in einen anderen gültigen Zustand zu gelangen, ist die Struktur N -erkennbar.
Definition 2. Ist es möglich, aus einer vormals gültigen Datenstruktur, welche
höchstens N Änderungen erfahren hat, wieder die ursprüngliche Datenstruktur
herzustellen, so ist diese N -korrigierbar.
Demnach ist eine einfach verkettete Liste ohne Speicherung der Elementanzahl 0-erkennbar und 0-korrigierbar, mit Speicherung der Elementanzahl immerhin 1-erkennbar aber immer noch 0-korrigierbar. Durch Hinzufügen weiterer
Redundanz in Form von back-Zeigern erhält man eine doppelt verkettete Liste.
Dadurch kann 1-Korrigierbarkeit erreicht werden und es erhöht sich die Erkennbarkeit auf 2 [6].
4.3
Verkettete Listen
Durch eine einfache Änderung kann die Erkennbarkeit einer doppelt verketteten
Liste noch weiter erhöht werden. Bei einer modified(2) double-linked list wird der
back-Zeiger eines Elements auf das Element zwei Elemente vor diesem gespeichert. Dabei gibt die Zahl in Klammern die Distanz zum gespeicherten Element
an und kann auch noch weiter erhöht werden. Diese Änderung erzielt bei modified(2) double-linked lists 3-Erkennbarkeit, bei modified(3) double-linked lists
4-Erkennbarkeit. Eine weitere Erhöhung der überbrückten Distanz führt zu keiner weiteren Verbesserung, jedoch wird das Modifizieren der Liste immer teurer
da immer mehr back-Zeiger angepasst werden müssen [6].
Abbildung 1. Löschen eines Elements aus einer doppelt verketteten Liste
Header
count=3
1
2
3
Es soll nun das zweite Element einer Liste gelöscht werden. Bei einer doppelt
verketteten Liste reicht die Veränderung von zwei Zeigern und die Anpassung
der im Header gespeicherten Elementanzahl um wieder eine gültige Liste herzustellen.
Bei einer modified(2) double-linked list ist noch eine Zeigerveränderung mehr
erforderlich. Die nach der Löschoperation geänderten Zeiger sind rot dargestellt.
4.4
Binärbäume
Eine ähnliche Situation ergibt sich bei Binärbäumen. Ohne Speicherung der
Anzahl der enthaltenen Elemente sind diese wie einfach verkettete Listen 0erkennbar. Doch auch wenn in jedem Knoten die Größe der Bäume seiner Kinder gespeichert wird, lassen sich manche Veränderungen nicht erkennen, nämlich
wenn ein Zeiger durch einen anderen Zeiger auf einen Baum gleicher Größe
ersetzt wird. Mit einem eindeutigen Identifikationsfeld kann zumindest sichergestellt werden, dass auf einen Knoten im richtigen Baum gezeigt wird. Um
Abbildung 2. Löschen eines Elements aus einer modified(2) double-linked list
Header
count=3
1
2
3
nun erkennen zu können, ob in einem Baum ein Knoten mehrfach referenziert
wird, kann beim Durchlaufen ein Flag im Knoten gesetzt werden, welches anzeigt, dass dieser schon besucht wurde. Falls eine Veränderung des Baums nicht
möglich oder gewollt ist, muss entweder eine Liste mit schon besuchten Knoten
verwaltet werden, oder es muss der gesamte Teil des Baum bis zum betrachteten
Knoten jeweils erneut durchlaufen werden.
Wie bei einer doppelt verketteten Liste können auch zu einem Baum redundante Zeiger hinzugefügt werden. Bei einem right threaded tree werden in
Knoten, die kein rechtes Kind besitzen, Zeiger auf deren in-order Nachfolger gespeichert. Damit es möglich ist, bei einer beliebigen Veränderung eines Zeigers
den ursprünglichen Baum wiederherzustellen, muss jeder Knoten allerdings mindestens zwei eingehende Zeiger besitzen, was bei threaded trees nicht der Fall ist.
Werden jetzt noch alle Knoten, die kein linkes Kind besitzen, in in-order miteinander verbunden, erhält man einen chained and threaded binary tree (CT-Tree).
Dieser ist 2-erkennbar und 1-korrigierbar [6].
5
Consistency Constraints
Durch die Angabe von Consistency Constraints einer Datenstruktur ist es möglich, automatisch Inkonsistenzen zu erkennen und zu beheben. Demsky und Rinard stellen ein System vor, das die Beschreibung solcher Constraints und die
darauf aufbauende automatische Reparatur von Datenstrukturen erlaubt [2].
5.1
Ziel
Es wird nicht davon ausgegangen, dass genügend Daten zur Rekonstruktion des
Ausgangszustands zur Verfügung stehen. Statt dessen wird die Herstellung eines beliebigen gültigen Zustands, um die Programmausführung fortsetzen zu
können, verfolgt. Dies kann in bestimmten Fällen einem Programmabbruch vorzuziehen sein, wenn dies z.B. katastrophale Folgen hätte. Die angenommenen
Daten können mit der Zeit durch neue ersetzt und irrelevant werden und sich
das System so, wenn es weiterhin lange genug erfolgreich ausgeführt wird, von
selbst wieder in einen fehlerfreien Zustand versetzen.
Es kann mehrere Möglichkeiten geben die Reparatur durchzuführen. Um die
auszuführenden Reparaturoperationen auszuwählen, können diesen Operationen
Kosten zugewiesen werden. So kann man andere Operationen Löschoperationen
vorziehen, um Datenverlust zu vermeiden. Außerdem sollte die Anzahl der notwendigen Operationen möglichst klein gehalten werden.
5.2
Spezifikation von Consistency Constraints
Zur Angabe von Consistency Constraints können Datenstrukturen eines Programms in einer C-ähnlichen Sprache beschrieben werden. Hier zum Beispiel ein
Auszug aus der Definition der Karte eines Spielfeldes eines Strategiespiels, auf
das später noch genauer eingegangen wird [1].
structure city {
int x;
int y;
//[...]
}
structure tile {
int terrain;
city *city;
//[...]
}
structure tilegrid {
tile grid[map.xsize*map.ysize];
}
Städte haben eine Position, Felder einen Terraintyp sowie möglicherweise einen
Zeiger auf eine Stadt, die Karte besteht aus einem Array von Feldern.
Diese Strukturdefinitionen können nun verwendet werden, um Mengen von
Objekten und deren Verhältnis zueinander anzugeben.
set GRID(tilegrid):
set TILE(tile): STILE
set STILE(tile):
set TERRAINTYPES(int): water
set water(int):
set CITY(city):
CITYMAP: STILE -> CITY (1->1)
– Die Menge GRID enthält Objekte vom Typ tilegrid.
– Die Mengen TILE und STILE enthalten Objekte vom Typ tile, also Felder
der Karte. STILE ist eine Untermenge von TILE.
– Die Mengen TERRAINTYPES und water enthalten Integer. water ist eine Untermenge von TERRAINTYPES.
– CITY enthält Städte, also Objekte vom Typ city. CITYMAP ist eine Relation,
die Feldern aus STILE Städten zuordnet.
[forall t in TILE], !t.city = NULL => t.city in CITY
Es werden alle Elemente der Menge TILE und damit die Felder der Karte betrachtet. Wenn der city-Zeiger eines Feldes nicht NULL ist, so muss die Stadt, auf
die gezeigt wird, auch tatsächlich existieren und somit Element der Menge CITY
sein.
[],sizeof(GRID)=1
[forall t in GRID,
for x=literal(0) to map.xsize-literal(1),
for y=literal(0) to map.ysize-literal(1)], true =>
t.grid[x+(y*map.xsize)] in TILE
An allen Positionen des einzigen Elements aus GRID müssen sich tatsächlich
existierende Felder aus TILE befinden.
[for i=literal(0) to literal(6)], true => i in TERRAINTYPES
[for i=literal(8) to literal(13)], true => i in TERRAINTYPES
[for i=literal(7) to literal(7)], true => i in water
Die Werte 0 bis 6 und 8 bis 13 sind gültige, nicht näher angegebene Terraintypen.
7 ist der Wert für Wasser. Da water eine Untermenge von TERRAINTYPES ist,
zählt auch Wasser als Terraintyp.
Einige Strukturdefinitionen für ein einfaches Dateisystem [1]:
structure Inode {
int referencecount;
//[...]
}
structure DirectoryEntry {
int inodenumber;
//[...]
}
Inodes haben einen Referenzzähler, Verzeichniseinträge verweisen auf einen Inode.
Darauf aufbauende Mengen- und Relationsangaben:
set Inode(int): partition UsedInode | FreeInode
set FreeInode(int):
set UsedInode(int): partition FileInode | DirectoryInode
set FileInode(int):
set DirectoryInode(int): RootDirectoryInode
set RootDirectoryInode(int):
set DirectoryEntry(DirectoryEntry):
inodeof: DirectoryEntry -> UsedInode (many->1)
inodestatus: Inode -> token (many->1)
referencecount: Inode -> int (many->1)
– Die Menge aller Inodes wird in disjunkte Teilmengen benutzter und noch freier Inodes aufgeteilt. Die benutzten Inodes wiederum teilen sich in Datei- und
Verzeichnis-Inodes auf. Der Inode des Wurzelverzeichnisses ist ein VerzeichnisInode.
– Die Relation inodeof ordnet einem Verzeichniseintrag einen benutzten Inode
zu.
– inodestatus gibt den Status eines Inodes an, referencecount die Anzahl
der Referenzen auf einen Inode.
[for j=literal(0) to d.s.NumberofInodes-literal(1)],
!(j in UsedInode) => j in FreeInode
Wenn ein Inode nicht benutzt ist, so ist er frei.
[forall <de, u> in inodeof], true => de.inodenumber=u
Jeder Verzeichniseintrag, der mit einem benutzten Inode in Relation steht, enthält
dessen Inodenummer.
[forall u in UsedInode], u.inodestatus=literal(Used)
[forall f in FreeInode], f.inodestatus=literal(Free)
Benutzte und freie Inodes haben einen entsprechenden Status.
[forall i in UsedInode], i.referencecount=sizeof(i.~inodeof)
Der Referenzzähler eines Inodes ist gleich der Anzahl der Verzeichniseinträge,
mit denen dieser in Relation steht.
[],sizeof(RootDirectoryInode)=1
Es gibt nur ein einziges Wurzelverzeichnis.
5.3
Anwendung von Consistency Constraints
Die Fehlererkennung und gegebenenfalls Reparatur kann zu unterschiedlichen
Zeiten der Programmausführung vorgenommen werden. Bei automatischer Ausführung muss darauf geachtet werden, dass keine zu überprüfende Datenstruktur
gerade von dem Programm verändert wird, da es dadurch zu temporären Inkonsistenzen kommen kann, die bei korrekter Beendigung der Operation behoben
werden. So können beim Einfügen in eine Liste schon die Zeiger angepasst worden
sein, aber die Aktualisierung der Elementanzahl im Header steht noch aus. Dies
darf natürlich nicht von einer Überprüfungsroutine, die gerade den normalen
Programmablauf unterbrochen hat, fälschlicherweise erkannt und vermeintlich
repariert werden.
Einerseits kann durch eine geeignete Auswahl von Stellen, an denen Konsistenz angenommen wird, eine automatische regelmäßige Kontrolle durchgeführt
werden. Indem die Überprüfungen an manchen dieser Stellen je nach Bedarf deaktiviert werden, kann zwischen höherer Zuverlässigkeit und mehr Performanz
abgewogen werden.
Weiterhin ist es möglich, erst bei Erkennen eines Problems den Überprüfungsund Reparaturvorgang zu starten und dadurch das System in einen lauffähigen
Zustand zurückzuversetzen, um danach die problematische Operation von einem
vorher gespeicherten Konsistenzpunkt zu wiederholen. Natürlich können beide
Techniken auch kombiniert werden.
Auch können so z.B. auf Festplatte befindliche Daten von einem eigenständigen Programm überprüft werden, ohne dass diese Daten Teil eines gerade in
Ausführung befindlichen Programms sind.
Ein Problem, das bei der Reparatur auftreten kann, sind zyklische Abhängigkeiten. Veränderungen zur Konsistenzwiederherstellung an einer Stelle können
Beschränkungen an einer anderen Stelle verletzen, deren Behebung wiederum
selbst Beschränkungen verletzt. Diese Abhängigkeiten sind zyklisch, wenn durch
eine spätere Veränderung die Verletzung einer schon früher behobenen Beschränkung wieder hervorgerufen wird. Eine Spezifikation muss also vor ihrer Anwendung auf diese Abhängigkeiten überprüft werden, um sicherzustellen, dass die
Reparatur auch wirklich terminiert.
5.4
Beispiele der erfolgreichen Anwendung
Um die Praxistauglichkeit dieses Ansatzes zu untersuchen, wurden Spezifikationen für mehrere sich täglich in Benutzung befindender Datenstrukturen sowie
geeignete Fehlerinjektionsstrategien, um diese auf ihre Funktionalität zu testen,
entwickelt.
CTAS Als erstes Beispiel dient das Center-TRACON Automation System, kurz
CTAS, ein Flugverkehrskontrollsystem, das Fluglotsen eine grafische Darstellung der Position und des vorhergesagten Weges der in der Luft befindlichen
Flugzeuge bietet. Dazu werden Informationen der Flugpläne mit Start- und
Zielflughäfen der einzelnen Flugzeuge an CTAS übermittelt, wobei es aufgrund
der hohen strukturellen Komplexität der übertragenen Daten zu Verarbeitungsfehlern kommen kann. Die möglichen Flughäfen werden dabei in einem Array
gespeichert. Wird versucht, aufgrund eines fehlerhaft gespeicherten und damit
ungültigen Flughafens auf ein Element außerhalb des Arrays zuzugreifen, stürzt
das System ab. Dies kann abgefangen und der ungültige Wert durch einen beliebigen Gültigen ersetzt werden. Danach kann die Ausführung an einem vorher
festgelegten Konsistenzpunkt fortgesetzt werden. Nun wird zwar ein Start- oder
Zielflughafen eines einzelnen Flugzeugs falsch angezeigt, dennoch ist das System weiterhin benutzbar. Außerdem werden die angenommenen Daten, sobald
das Flugzeug landet oder den Überwachungsbereich der Kontrollstation verlässt,
irrelevant. Dies ist einem Totalausfall klar vorzuziehen, da auch nach einem Neustart des Systems mit den alten, fehlerhaften Daten gearbeitet werden und so
der Fehler wieder auftreten würde.
vereinfachtes ext2-Dateisystem Weiterhin wurde eine vereinfachte Version
des ext2-Dateisystems betrachtet. Dazu wurde bei Schreiboperationen ein Systemabsturz simuliert und das Verhalten bei anschließender weiterer Benutzung des
Dateisystems mit und ohne Ausführung der Reparaturfunktionen verglichen. Dabei zeigte sich, dass ohne Reparatur Inkonsistenzen bestehen bleiben wie falsche
Referenzzähler in Inodes oder ungültige Zustände für Verzeichniseinträge. Dies
kann zu mehrfach verwendeten Blöcken und falschen Dateiinhalten führen. Das
verwendete Reparaturverfahren war in der Lage, diese Probleme zu verhindern
und eine weitere fehlerfreie Benutzung des Dateisystems zu ermöglichen.
Freeciv Als Nächstes wurde das Kartenformat des Spiels Freeciv analysiert. Bei
diesem Spiel kommen mehrere Arten von Terrain zum Einsatz und es können
sich Städte auf der Karte befinden. Es soll sichergestellt werden, dass jedes Feld
auf der Karte einen gültigen Eintrag für den Terraintyp besitzt, dass jede Stadt
eine eindeutige Position besitzt, dass eine Stadt sich nicht auf einem Ozeanfeld
befindet und dass die in der Stadt gespeicherte Position mit der in der Karte
Gespeicherten übereinstimmt. Zum Testen wurden in vom Spiel erstellten Karten die Werte für das Terrain einiger Felder auf zufällige Werte gesetzt. Dadurch
kann es zu zwei Arten von Fehlern kommen: der Terraintyp eines Feldes kann
ungültig sein oder eine Stadt kann sich auf einem Ozeanfeld befinden. Zur Reparatur wurde Feldern mit ungültigen Terrainwerten gültige Werte zugewiesen
und Städte wenn nötig auf Landfelder verschoben. Beim Verschieben der Städte
muss sowohl die in der Karte gespeicherte Position der Stadt als auch die in
der Stadt selber gespeicherte Position angapasst werden damit es nicht zu neuen Inkonsistenzen kommt. Durch diese Änderungen war es möglich, ansonsten
abstürzende Spiele erfolgreich auszuführen.
Word-Dokumente Zuletzt wurde die Reparatur beschädigter Word-Dokumente
in Angriff genommen. Dieses Dateiformat benutzt Blockzuweisungstabellen wie
beim FAT-Dateisystem, das eine einfach verkettete Liste verwendet, um eine
Kette der einer Datei zugewiesenen Blöcke zu speichern. In der Liste können
statt der Adresse des nächsten Blocks spezielle Werte stehen um z.B. das Ende
einer Kette zu kennzeichnen. Hierbei kann es zu Problemen wie mehrfach zugewiesenen Blöcken oder Zeiger auf nicht existierende Blöcke kommen. Um Fehler
einzufügen, wurden Endmarkierungen von Ketten durch Zeiger auf vorhandene Blöcke anderer Ketten ersetzt, Zeiger auf nicht existierende Blöcke eingefügt
und benutzte Blöcke als nicht zugewiesen markiert. Während es mit Word noch
möglich war, Dateien zu laden in denen Blöcke fälschlicherweise als nicht benutzt
markiert waren, schlug dies bei Dateien mit anderen Defekten fehl. Nach erfolgter Reparatur war es wieder möglich diese Dateien zu laden, auch wenn dadurch
das ursprüngliche Dokument verändert wurde, da es nicht zur Wiederherstellung
von Daten sondern nur zur Anpassung der Struktur der Blockzuweisungstabelle
kam.
5.5
Probleme der automatischen Reparatur
Durch die Annahme von beliebigen gültigen Daten ist das Verfahren nicht unbedingt für alle Zwecke geeignet. Bei manchen kritischen Anwendungen ist eine Beendigung des Programms unumgänglich. Auch können manche Beschränkungen
durch das Verbot von zyklischen Abhängigkeiten gar nicht erst ausgedrückt werden. Es ist nicht auszuschließen, dass Datenstrukturen, die vom Reparaturalgorithmus selber verwendet werden, beschädigt werden und dieser dann bestenfalls nicht mehr richtig funktioniert oder sogar selber Fehler verursacht. Auch
wenn der Algorithmus nach Spezifikation arbeitet, ist es schwer vorherzusagen,
welche Veränderungen tatsächlich vorgenommen werden. Durch manche Reparaturoperationen können zusätzliche Daten verloren gehen welche bei besserer
Auswahl der auszuführenden Operationen hätten erhalten bleiben können. Kaskadierende Reparaturen können große Teile der Datenstruktur und somit vom
ursprünglichen Fehler eigentlich unbeeinflusste Daten verändern.
6
Resilient Data Structures
Da Fehlererkennung und -korrektur mit teilweise nicht zu tolerierend hohen Kosten an zusätzlich verwendetem Speicher und längerer Ausführungszeit verbunden
sind, kann es sich in bestimmten Fällen anbieten, auf diese zu verzichten, wenn
mit den noch fehlerfreien Daten weiterhin korrekt gearbeitet werden kann. Für
einen solchen Umgang mit Fehlern existieren Resilient Data Structures, die unter fehleranfälligen Ausführungsbedingungen bestimmte Garantien für ihre unterstützten Operationen bieten. Dabei stellen diese Datenstrukturen einige Anforderungen, so wird eine Obergrenze δ an möglichen Fehlern angenommen und
es muss eine bestimmte Menge an garantiert fehlerfreiem Speicher zur Verfügung
stehen, um interne Daten verwalten zu können. Ein nützliches Modell zur Analyse dieser Datenstrukturen ist die faulty-memory random access machine. In diesem Modell können beliebige Speicherzellen jederzeit falsche Werte annehmen,
es existiert jedoch eine konstante Menge des geforderten fehlerfreien Speichers
[3].
6.1
Priority Queue
Unterstützte Operationen:
– insert
Fügt ein Element hinzu.
– deletemin
Gibt entweder den kleinsten gültigen oder einen fehlerhaften Wert zurück
und löscht diesen.
Es werden mehrere Puffer verwendet, deren Größe und Anzahl von der Nummer
der Elemente in der Queue abhängt. In diesen Puffern werden die Elemente
der Queue sortiert gespeichert. Diese Puffer werden in einer doppelt verketteten
Liste verwaltet, deren Zeiger redundant mit 2δ + 1 Kopien gespeichert werden.
Zeiger auf den Anfang dieser Liste sowie auf einen Einfügepuffer zusammen
mit der Anzahl an Elementen in diesem Puffer werden in fehlerfreiem Speicher
gehalten. Bei Über- oder Unterschreiten einer festgelegten Anzahl an Elementen
in den Puffern werden diese mit fehlertoleranten Algorithmen aufgeteilt oder
zusammengefasst. Dabei wird die Sortierung der nicht fehlerhaften Elemente
beibehalten. Die Puffergröße erhöht sich zum Ende der Queue hin, damit die
teuren Operationen zur Anpassung der Puffergröße seltener stattfinden müssen
[5].
6.2
Dictionary
Unterstützte Operationen:
– insert
Fügt ein Element hinzu.
– delete
Löscht ein Element.
– search
Sucht nach einem Schlüssel κ. Gibt bei Vorhandensein eines gültigen Wertes
mit diesem Schlüssel entweder diesen Wert oder einen fehlerhaften mit dem
gleichen Schlüssel zurück. Gibt bei Abwesenheit eines Wertes mit Schlüssel κ
entweder einen leeren Wert oder einen fehlerhaften Wert mit diesem Schlüssel
zurück.
In einem Suchbaum wird eine Partition der Menge der möglichen Schlüssel gespeichert. Ein Knoten darf dabei zwischen δ/2 und 2δ Schlüssel enthalten. Bei
Einfüge- oder Löschoperationen müssen Knoten gegebenenfalls aufgeteilt oder
zusammengefasst werden. Jeder Knoten speichert redundant mit 2δ + 1 Kopien
die Endpunkte des abgedeckten Intervalls, die Anzahl der enthaltenen Schlüssel,
das von seinem Unterbaum abgedeckte Intervall, Zeiger auf die Kinder- und den
Elternknoten sowie möglicherweise benötigte Informationen um den Baum balanciert zu halten. Die Menge der enthaltenen Schlüssel wird nicht redundant
gespeichert. Der Baum selber wird dabei in einem Array gespeichert, um das
Erkennen ungültiger Zeiger zu vereinfachen. Wird die maximale Anzahl an Elementen im Array überschritten oder wird weniger als ein Viertel der möglichen
Elemente verwendet, wird ein neues Array mit der doppelten beziehungsweise
halben Größe angelegt und der alte Inhalt hineinkopiert. Zeiger auf das Array
und den Wurzelknoten sowie die momentane Anzahl an Elementen im Array
werden in fehlerfreiem Speicher gehalten [4].
6.3
Verbleibende Probleme
Die vorgestellten Resilient Data Structures machen einige Annahmen, die nicht
unbedingt realistisch sind. Auch wenn nur eine konstante Menge an fehlerfreiem
Speicher benötigt wird, kann es schwer sein, die Fehlerfreiheit tatsächlich zu
garantieren. Außerdem ist die Anzahl der auftretenden Fehler nicht unbedingt
nach oben beschränkt.
Mit der faulty-memory random access machine wird ein sehr simples Modell eines fehleranfälligen Systems betrachtet. Da Resilient Data Structures vor
allem bei sehr großen Datenmengen und damit hohem Bedarf an viel billigem
Speicher sinnvoll sind, wäre eine Unterstützung komplexerer Speicherhierarchien
wünschenswert.
Die Performanz der hier betrachteten Queue ist stark von der angenommenen
Obergrenze an Fehlern abhängig und wird mit steigendem δ schnell schlechter
[3].
Literatur
1. http://www.cag.lcs.mit.edu/∼bdemsky/repair.
2. Brian Demsky and Martin Rinard. Automatic detection and repair of errors in data
structures. Proceedings of the 18th annual ACM SIGPLAN conference on Objectoriented programing, systems, languages, and applications, 2003.
3. Umberto Ferraro-Petrillo, Irene Finocchi, and Giuseppe F. Italiano. Experimental study of resilient algorithms and data structures. In Proceedings of the 9th
international conference on Experimental Algorithms, SEA’10, pages 1–12, Berlin,
Heidelberg, 2010. Springer-Verlag.
4. Irene Finocchi, Fabrizio Grandoni, and Giuseppe F. Italiano. Resilient dictionaries.
ACM Trans. Algorithms, 6(1):1:1–1:19, December 2009.
5. Allan Grønlund Jørgensen, Gabriel Moruz, and Thomas Mølhave. Priority queues
resilient to memory faults. In Proceedings of the 10th international conference on
Algorithms and Data Structures, WADS’07, pages 127–138, Berlin, Heidelberg, 2007.
Springer-Verlag.
6. David J. Taylor, D. E. Morgan, and J. P. Black. Redundancy in data structures:
Improving software fault tolerance. IEEE Transactions on Software Engineering,
1980.
Herunterladen