Construits en intégrant des éléments hétérogènes dans des systèmes multi-plateformes pour répondre aux exigences du marché, les systèmes modernes se construisent majoritairement par la réutilisation de différents composants développés indépendamment : systèmes d’exploitation, bibliothèques spécialisées, Frameworks, intergiciels. Pour ces systèmes, il est nécessaire d’assurer un degré de sûreté de fonctionnement adapté. Une défaillance du contrôle aérien, lorsqu’elle se produit, a des conséquences humaines et économiques catastrophiques, alors que celle d’une application de géolocalisation est moins critique. À la complexité inhérente à ces architectures logicielles hétérogènes, dans le contexte des systèmes ouverts, s’ajoute un certain nombre de problèmes liés à la sûreté de fonctionnement et à la résilience. La sûreté de fonctionnement est la propriété qui permet aux utilisateurs de placer une confiance justifiée dans la qualité du service que leur délivre un système. La résilience est la persistence de la sûreté de fonctionnement en dépit des changements.
Sûreté de fonctionnement et tolérance aux fautes
Nous allons dans un premier temps définir l’ensemble des notions permettant d’appréhender la sûreté de fonctionnement. Ces notions sont considérées indépendamment de la nature du système auquel elles s’appliquent. Pour ce faire, nous utilisons la taxonomie proposée par JEAN-CLAUDE LAPRIE et al. [9]. Ce paragraphe définit les notions de la sûreté de fonctionnement dans leurs grandes lignes, à partir des notions de faute, d’erreur et de défaillance. Une fois ces notions définies, nous pourrons aborder la problématique de cette thèse, qui porte sur l’implémentation au niveau logicielle des mécanismes de tolérance aux fautes. Nous décrivons à présent en quelques mots les moyens proposés pour réaliser des systèmes sûrs de fonctionnement, en détaillant plus particulièrement l’un d’entre eux, la tolérance aux fautes dans la section qui suit. D’une manière générale un système rend des services à des opérateurs humains ou à d’autres systèmes.
La sûreté de fonctionnement d’un système est définie comme :
“ La propriété qui permet à ses utilisateurs de placer une confiance justifiée dans la qualité du service qu’il leur délivre ”
JEAN-CLAUDE LAPRIE, 2004, [9]
Lorsque le service délivré par un système diverge du service attendu, le système est défaillant. La sûreté de fonctionnement cherche donc à éviter les défaillances, à prévenir les plus catastrophiques pour améliorer la survie du système. La prévention des défaillances s’appuie sur deux notions : faute et erreur. Une faute est la cause d’une défaillance. Les fautes étant les causes des défaillances, la sûreté de fonctionnement cherche à les combattre, si possible en évitant qu’elle se produisent (prévention), ou en les éliminant (élimination).
La sûreté de fonctionnement met donc à disposition quatre moyens pour éviter les fautes ou les éliminer :
– La prévention des fautes dont l’objectif est d’éviter l’introduction de fautes de développement tant au niveau logiciel qu’au niveau matériel.
– L’élimination des fautes consiste à vérifier la présence de fautes lors du processus de développement et à réaliser de la maintenance corrective et préventive sur le système en cours d’utilisation.
– La prévision des fautes est réalisée par une évaluation du comportement du système en présence de faute.
– La tolérance aux fautes est intégrée au système et agit en ligne en détectant et recouvrant les erreurs du système.
Les trois premiers moyens ne sont cependant pas suffisants pour deux raisons. Ces raisons sont inhérentes à la nature composite d’un système complexe dont les éléments peuvent être matériels ou logiciels. La première vient du fait que pour les logiciels il est impossible de concevoir des systèmes totalement exempts de fautes. La deuxième est liée à l’usure inévitable et à la détérioration des systèmes, agressés par leurs environnements d’exploitation. L’occurrence d’une faute reste donc possible. Vient alors la notion d’erreur, qui lie l’occurrence d’une faute à une défaillance. L’activation d’une faute amène le système dans un état erroné, un état anormal d’un système, qui peut potentiellement amener à une défaillance. Pour faire face à l’occurrence d’une faute, pour la tolérer, un quatrième moyen, la tolérance aux fautes doit être utilisé. La tolérance aux fautes, que nous détaillons maintenant, permet de casser cette chaîne de causalité, en empêchant une erreur de se propager jusqu’à l’utilisateur. On améliore donc la sûreté de fonctionnement d’un système en tolérant ses fautes.
La tolérance aux fautes
Dans les systèmes composites (systèmes de systèmes) la défaillance de sous-systèmes se propage au système global. Une faute est la cause adjugée ou supposée d’une erreur. Par propagation, une erreur crée de nouvelles erreurs. Une défaillance survient lorsque, par propagation, une erreur affecte le service délivré par le système. Cette défaillance peut alors apparaître comme une faute du point de vue d’un autre composant. On obtient ainsi la chaîne fondamentale suivante : →défaillance → faute → erreur → défaillance → faute →.
Les flèches dans cette chaîne expriment la relation de causalité entre fautes, erreurs et défaillances. Elles ne doivent pas être interprétées au sens strict : par propagation plusieurs erreurs peuvent être créées avant qu’une défaillance ne survienne. La tolérance aux fautes consiste à empêcher la propagation de l’erreur jusqu’aux frontières du système où celle-ci sera alors perçue comme une défaillance par l’utilisateur du système. La tolérance aux fautes au niveau logiciel s’appuie essentiellement sur deux grandes idées :
la détection d’erreur et le recouvrement d’erreur.
– La détection d’erreur permet d’identifier un état erroné comme tel ;
– Le recouvrement d’erreur, permet de substituer un état exempt d’erreur à l’état erroné ;
la substitution peut elle même prendre trois formes :
– La reprise, où le système est ramené dans un état survenu avant l’occurrence de l’erreur ; ceci passe par l’établissement de points de reprise, qui sont des instants de l’exécution où l’état courant peut ultérieurement nécessiter d’être restauré.
– La poursuite, où un nouvel état est trouvé à partir duquel le système peut fonctionner,
– La compensation d’erreur, où l’état erroné comporte suffisamment de redondance pour permettre la transformation de l’état erroné en un état exempt d’erreur.
Une stratégie de tolérance aux fautes est une combinaison de moyens permettant de couvrir le modèle de fautes souhaité. Une stratégie de tolérance aux fautes est choisie en fonction du modèle de fautes à couvrir, mais aussi en tenant compte de propriétés comme la disponibilité, fiabilité, robustesse, testabilité, maintenabilité, etc. Dans les sections qui suivent nous illustrons les difficultés à réaliser une implémentation résiliente de la tolérance aux fautes en utilisant les paradigmes de développement logiciel conventionnels. Nous discutons ensuite des travaux ayant mis en œuvre des solutions réflexives pour palier les problèmes liés à la résilience de la tolérance aux fautes.
Séparation des préoccupations
La résilience [44] est la persistance de la sureté de fonctionnement d’un système lors de l’évolution de ce dernier. L’essence de la résilience découle d’une vision évolutionnaire d’un système informatique ; un système évolue aux regards de ses fonctionnalités, des mécanismes de tolérance aux fautes garantissant la confiance placée dans ses fonctionnalités élémentaires (ou combinaison de fonctionnalités élémentaires), des ressources dont il dispose, de son environnement. Un système évolue selon trois axes. Un système fournit des services qui peuvent être sujets à différents types de fautes. Ces fautes peuvent être des fautes résiduelles de conception ou des fautes opérationnelles, logicielles ou matérielles. L’ensemble de ces fautes correspond au modèle de fautes à considérer. Le modèle de fautes doit être couvert par une stratégie de tolérance aux fautes. Puisque nous nous intéressons à la tolérance aux fautes, nous ne traitons pas les évolutions liées au code fonctionnel. Nous nous intéressons aux évolutions liées au changement des propriétés de sûreté de fonctionnement, aux changements dans l’environnement, à la disponibilité des ressources et aux hypothèses liées aux différents composants du système. Par exemple considérons un système distribué dans lequel un serveur fournit un service à plusieurs clients. Supposons que ce serveur soit un composant auto-testable parfait (self checking). Son seul mode de défaillance est donc le crash.
Un moyen intuitif de tolérer les fautes de type crash est de remplacer la réplique défaillante par une réplique non-défaillante. Plusieurs solutions sont possibles. Ces dernières s’appuient sur le principe de la réplication. Dans ce contexte, deux serveurs (approches duplex) sont capables de fournir le service, quand le premier défaille, le second prend le relais. Plusieurs stratégies de réplication peuvent être mises en œuvre :
– La réplication passive consiste à exécuter le service uniquement sur le serveur primaire, la synchronisation est alors faite par transfert d’état par des points de reprise.
– La réplication active consiste à exécuter le service sur le serveur primaire et le serveur secondaire.
Bien qu’un mécanisme puisse être défini syntaxiquement avec seulement quelques lignes de code, les techniques orientées objet exigent qu’une très grande quantité de code soit ajoutée dans le code applicatif pour les utiliser.
Par exemple, le control flow checking est utilisé pour détecter les exécutions de flot de contrôle erronées causées par l’exécution des branches illégales. Le mécanisme repose sur le principe selon lequel, si un programme entre dans un bloc de code, il doit sortir du bloc lors de sa prochaine sortie. Pour pouvoir le vérifier, un identificateur unique pour chaque bloc est placé au début et à la fin du bloc afin de tracer l’exécution. Ainsi le control flow checking affecte toutes les fonctions de chaque entité du système et est donc dispersé dans le code de ce dernier. Même si ce mécanisme peut être défini syntaxiquement avec seulement quelques lignes de code, une très grande quantité de code source doit être ajoutée au code applicatif pour le mettre en œuvre. La dispersion de code lié à une préoccupation aussi appelée code scattering est un effet du manque de modularisation de cette préoccupation. L’enchevêtrement de code ou code tangling en est une autre conséquence, c’est-à-dire que chaque module comporte le code associé à de nombreuses préoccupations. Le code lié au différentes préoccupations du système se mélange au code fonctionnel. Un certain nombre de problèmes liés à la qualité du logiciel et sa reconfiguration résultent de l’enchevêtrement et de la dispersion du code non-fonctionnel. Le premier est lié à la réutilisation du code fonctionnel. L’enchevêtrement du code rend difficile la réutilisation du code fonctionnel dans un autre système, puisque le code non fonctionnel est entrelacé avec ce dernier. La réutilisation du code non-fonctionnel, tel que du code pour la tolérance aux fautes, est encore plus difficile puisque le code est à la fois éparpillé autour du code applicatif et enchevêtré avec d’autres préoccupations. De la dispersion et de l’enchevêtrement du code nonfonctionnel résultent des systèmes complexes difficilement reconfigurables. Pour faire face à la complexité du logiciel, les programmeurs et les concepteurs ont naturellement essayé d’appliquer le principe « diviser pour mieux régner ». Cela s’est traduit par une division des applications en artefacts plus petits, plus faciles à comprendre, à maintenir et à faire évoluer qu’un système monolithique. Cette technique appelée séparation des préoccupations a été initialement décrite par PETER MICHAEL SMITH MELLIAR dans [52]. Le but de la séparation des préoccupations est de pouvoir analyser, développer et raisonner sur certaines parties d’un système sans avoir à prendre en compte les autres parties . Les paradigmes Orienté Objet (OO) [14] et de l’ingénierie logicielle à base de composants (Component-Based Software Enineering, CBSE) [83], permettent de réaliser partiellement la séparation et la modularisation des préoccupations en combinant des objets/composant simples pour construire des objets/composants plus complexes. La programmation OO et le CBSE offre les moyens nécessaires pour faire évoluer un système sur l’axe fonctionnel dans le temps. Cependant, le code des mécanismes de tolérance aux fautes implémentés à l’aide de ces paradigmes est dispersé dans le code des composants qui sont ainsi difficiles à faire évoluer. Finalement, qu’une application soit implémentée à l’aide d’objets ou de composants, les préoccupations transversales sont difficilement encapsulées et modularisées dans des entités. Ces préoccupations appelées préoccupations transversales (crosscutting concerns) sont des problèmes d’implémentation qui impactent plusieurs entités à différents points de leur code. Les préoccupations transversales constituent un vrai défit pour les paradigmes traditionnels puisque leur implémentation conduit typiquement à du code dupliqué et dispersé [52]. L’utilisation de langages à objet, ou d’approches à base de composants ne suffit pas pour séparer les préoccupations transversales comme la tolérance aux fautes du code applicatif.
|
Table des matières
1 Introduction
1.1 Problématique
1.2 Contributions
1.3 Organisation de ce mémoire
2 Tolérance aux fautes, séparation des préocupations et validation
2.1 Sûreté de fonctionnement et tolérance aux fautes
2.1.1 La tolérance aux fautes
2.2 Séparation des préoccupations
2.3 Approches réflexives pour la tolérance aux fautes
2.3.1 Approche réflexives
2.3.2 Implémentation réflexive de la tolérance aux fautes
2.3.3 Discussion
2.4 Programmation orientée aspect
2.4.1 Concepts et terminologie
2.4.1.1 Langage de point de coupe
2.4.1.2 Greffons
2.4.1.3 Définitions inter-types
2.4.2 Exemple
2.4.3 La programmation orientée aspect pour la tolérance aux fautes
2.4.3.1 Faisabilité et évaluation de la performance
2.4.3.2 Évaluation de la séparation des préoccupations
2.4.3.3 Discussion
2.5 Problèmes du point de vue de la validation
2.5.1 Modèle de fautes pour la POA
2.5.2 Niveaux de test pour la POA
2.5.2.1 Les tests unitaires
2.5.2.2 Les tests d’intégration
2.5.3 Génération et sélection de données de test
2.5.4 Oracles de test
2.5.5 Discussion
2.6 Conclusion
3 Interactions et interférence entre aspects
3.1 Contexte : Interactions, interférences entre aspects
3.2 Interactions et Interférences entre aspects à un point de jonction
3.2.1 Définition est classification des interactions
3.2.2 Interactions et interférences aux points de jonction partagés
3.2.3 Exemple d’interactions et d’interférences aux points de jonction partagés
3.2.4 Cadre pour la détection et la résolution des interactions
3.3 Interférences entre aspects au niveau du code
3.3.1 Détection
3.3.1.1 Détection par approches syntaxiques
3.3.1.2 Détection par approches sémantiques
3.3.1.3 Discussion
3.3.2 Résolution des interactions
3.3.2.1 Résolution des interactions dans ASPECTJ
3.3.2.2 Résolution des interactions dans REFLEX
3.3.2.3 Résolution des interactions dans AIRIA
3.3.2.4 Discussion
3.3.3 Validation des interactions entre aspects
3.4 Bilan
3.5 Motivations et objectifs de la thèse
4 Évitement et détection des interférences entre aspects
4.1 Introduction
4.2 Observabilité des propriétés de non-interférence
4.2.1 Propriétés de non-interférence
4.3 Composition des greffons à l’aide d’AIRIA
4.4 Détection des interférences
4.4.1 Placeholders pour exposer les transitions au moment de l’exécution
4.4.2 Détection des interferences de flot de données
4.4.2.1 Détection des interférences « changement avant » (CB)
4.4.2.2 Détection des interférences « changements après » (CA)
4.4.3 Détection de interférences de flot de contrôle
4.4.3.1 Détection des interférences « invalidation avant » (IB)
4.4.3.2 Détection des interférences « invalidation après » (IA)
4.5 Etude de faisabilité
4.5.1 Les objectifs de test
4.5.2 Les données d’entrée
4.5.3 L’oracle de test
4.5.3.1 Initialisation du modèle des propriétés
4.5.3.2 Prise en compte des informations contenues dans la trace d’exécution
4.5.3.3 Traitement de l’activation des assertions
4.5.3.4 Faux positifs et faux négatifs
4.6 Expérimentations et résultats
4.6.1 Vue globale de expérimentations
4.6.1.1 Les premières expériences avec des greffons before
4.6.1.2 Généralisation : greffons around et assertions multiples
4.6.1.3 Discussion des résultats
4.7 Bilan
5 Etude de cas : Implémentation orientée aspect d’un protocole PBR
5.1 Introduction
5.2 Cas d’étude et modèle de fautes
5.2.1 Cas d’étude
5.2.2 Modèle de fautes et mécanismes de tolérance aux fautes
5.3 Protocole de réplication duplex
5.3.1 Protocole client-serveur
5.3.1.1 Préoccupations transversales coté client
5.3.1.2 Aspects côté client
5.3.2 Protocole inter-répliques
5.3.2.1 Description générale des modes de fonctionnement du serveur
5.3.2.2 Comportement en mode duplex primaire
5.3.2.3 Comportement en mode secondaire
5.3.2.4 Comportement en mode single
5.3.3 Aspects du protocole inter-répliques
5.3.3.1 Aspects côté serveur primaire
5.3.3.2 Aspects côté serveur secondaire
5.3.3.3 Aspects côté serveur en mode single
5.4 Composition du protocole de réplication
5.4.1 Propriétés de l’implémentation
5.4.2 Composition du protocole duplex
5.4.2.1 Introduction inter-type
5.4.2.2 Aspects du protocole duplex et resolver
5.4.3 Discussion
5.5 Spécification des propriétés attendues
5.5.1 Processus de spécification
5.5.2 Spécification des aspects côté client
5.5.3 Spécification des aspects côté serveur primaire
5.5.4 Spécification des aspects côté serveur secondaire
5.6 Détection des interférences à l’assemblage
5.6.1 Détection d’interférences côté serveur primaire
5.6.2 Détection d’interférences côté client
5.7 Exemple de reconfiguration
5.7.1 Aspects côté serveur primaire sécurisé
5.7.2 Spécification des aspects de sécurité
5.7.3 Détection des interférences
5.7.4 Discussion
5.8 Conclusion
6 Conclusion
