% INF5190 - Résilience et performance % Jean-Philippe Caissy % 20 novembre 2019
header-includes:
- \usepackage{fvextra}
- \DefineVerbatimEnvironment{Highlighting}{Verbatim}{breaklines,commandchars=\{}}
Définition : capacité d'un système à récupérer d'une défaillance et rester opérationnel
- Une défaillance dans un système (application Web) va se produire éventuellement
- Plus un système est complexe, plus les risques de défaillances sont élevées
- Une application avec 3-4 systèmes peut facilement être disponible 100% du temps vs. une application avec des centaines de composantes
- Un système est résilient s'il reste fonctionnel malgré une défaillance
Lors d'une défaillance, un système devrait être en mesure d'opérer en mode défaillance partiel.
Exemples :
- Amazon : la recherche ne fonctionne plus
- Netflix : les vidéos HD ne chargent plus
- Google : impossible de se connecter
La résilience d'une application Web se fait sur 4 niveaux différents :
- L'application elle-même
- Les données
- Le réseau
- Les gens et la culture organisationnelle
Il existe plusieurs patrons à utiliser pour rendre une application résiliente.
- Architecturer une application pour pouvoir rouler de manière redondante (plus d'une instance)
- La redondance s'applique à tous les niveaux :
- Application Web
- Base de donnée
- Réseau
- Employés
- La redondance permet d'éliminer les défaillances causés par un point de défaillance unique (SPOF, ou single point of failure)
La disponibilité d'un système en série est mesuré par la sommes de la disponibilité des deux systèmes.
| Composante | Disponibilité | Temps indisponible |
|---|---|---|
| A | 99% | 3 jours, 15 heures |
| B | 99.99% | 52 minutes |
| A et B | 98.99% | 3 jours, 16 heures et 33 minutes |
| Composante | Disponibilité | Temps indisponible |
|---|---|---|
| Un seul A | 99% | 3 jours, 15 heures |
| Deux A en parallèle | 99.99% | 52 minutes |
| Trois A en parallèle | 99.9999% | 31 secondes |
- Automatiquement augmenter, puis diminuer les capacités d'un système
- Mot clé : automatique, et non pas manuellement
Exemple : Diminuer le nombre de serveur applicatifs la nuit lorsque le trafique est très bas
- Lorsqu'une défaillance survient, il est très rare que ça soit causé par un seul élément précis.
- Les défaillances en cascade sont très courantes dans les systèmes distribués (application Web)
- Effet papillon : ce qui semble être une petite coquille opérationnelle se termine par une défaillance complète du système Exemple de défaillance en cascade : surcharge applicative (trop de trafique)
- Dans un système distribuée, une manière simple de traiter les erreurs est de réessayer.
- Par exemple : un appel à un API distant qui ne répond pas. En cas d'échec, on refais la requête.
- Peut facilement causé un problème de saturation sur le réseau (trop d'essais simultanés)
Pour empêcher une situation où un système surchargerait un autre en tentant de réessayer une requête, il est préférable d'utiliser un algorithme de recul exponentiel (backoff algorithms).
Objectif :
- Limiter le nombre de tentatives
- Plus le nombre de tentatives augmente, plus on attend entre chaque essais
Exemple :
- Après une tentative infructueuse, attendre 2 seconde
- À la 2e tentative, attendre 4 secondes
- À la 3e tentative attendre 8 secondes
- Retourner un erreur si la 4e tentative échoue
retries = 0
while:
sleep( (2 ** retries) * 100 milliseconds )
success = do_request()
if status == True:
break
else:
retries += 1
if retries >= 10:
raise Exception("Trop d'essais") sleep((2 ** retries) * 100 milliseconds)Il est important d'ajouter certain niveau d'aléatoire dans l'attente.
sleep((2 ** retries) * 100 milliseconds * rand())
# -------------------------------------------^Lorsqu'une application communique avec un autre système, il est important d'avoir des délais maximum (timeout).
Par exemple:
- Un appel à la BD ne devrait pas prendre plus de 1 seconde
- Communiquer avec une API REST devrait se faire en moins de 4 secondes
Chaque situation est différente. Il n'existe pas de délais maximum magique.
L'utilisation d'un disjoncteur (circuit breaker) permet de faire échouer des appels distants rapidement lors de défaillance.
Exemple :
- Un API distant ne répond plus depuis 30 secondes.
- Chaque appel d'API timeout après 10 secondes
Avec l'utilisation d'un disjoncteur, on peut faire échouer les appels subséquents sans attendre 10 secondes
Une fois qu'une défaillance est détecté, la dernière étape est d'adapter l'application pour avoir une défaillance partielle, ou graceful failure en anglais.
Lorsqu'une application est en défaillance partielle, seulement une partie des fonctionnalités est indisponible, mais le reste de l'application fonctionne.
Exemples:
- L'API distant qui récupère l'usager connecté est non disponible
- Temporairement affiché un profil en mode invité
- La recherche d'un site ne fonctionne pas
- Retirer la fonctionnalité de recherche
- Le système de paiement d'une boutique en ligne ne fonctionne pas
- Désactiver l'achat, mais permettre de naviguer sur la boutique
class User(object):
def get_connected_user(self):
try:
return User.get(id=request.user_id)
except ConnectionError:
return nil- Une requête n+1 survient lorsqu'on doit charger N objets associés
order = Order.get(Order.id == request.id)
images = []
for image in order.images:
images.append(image)Combien de requêtes SQL vont être effectuées?
Afin d'éviter les requêtes n+1, on peut tenter de charger les associations étrangères sur le champ.
Avec Peewee, on peut facilement faire un JOIN sql:
order = Order.select().Join(Images).get(Order.id == request.id)
images = []
for image in order.images:
images.append(image)-
L'objectif de mettre des données en cache est de désengorger des systèmes plus lourds.
-
Par exemple : récupérer des données d'une base de donnée qui ne changent pas souvent pour les mettre en cache.
-
On va surtout utiliser une base de donnée de type clé-valeur comme système de cache : Redis et Memcached
def get_user(user_id):
cache_key = f"user-{user_id}"
cached_user = redis.get(cache_key)
if cached_user:
user = dict_to_model(User, cached_user)
else:
user = User.get(user_id)
redis.set(cache_key, model_to_dict(user))
return userIl faut faire attention à l'utilisation de la cache :
- Les données doivent resté à jour
- Il est souvent préférable d'avoir une expiration des données
Un système de gestion de tâches en arrière plan (task queue) permet d'exécuter des méthodes à l'extérieur du cycle d'une requête HTTP
- Lorsqu'on ne veut pas bloquer une requête
- Une requête qui demande plus de temps à traiter (ex: appel API distant, requête SQL lourde, etc)
C'est la fin de la matière pour le cours. Bravo.