gwift-book/chapters/infrastructure.tex

294 lines
15 KiB
TeX
Executable File

\chapter{Infrastructure et composants}
L'infrastructure est destinée à supporter deux types d'applications:
\begin{enumerate}
\item
Les applications consommant principalement des cycles processeur (\textit{Compute-intensive applications}).
\item
Les applications consommant intensivement des données (\textit{Data-intensive applications}).
Une application consommant principalement des données est rarement limitée par la puisse du CPU, mais plus souvent par la quantité et la complexité des structures de données, et la vitesse à laquelle celles-ci doivent être échangées. \cite[p. 3]{data_intensive}
\end{enumerate}
Au niveau des composants destinés à épauler ces applications, nous pouvons distinguer les types suivants:
\begin{enumerate}
\item
\textbf{Les composants fonctionnels}, ceux sans qui l'application fonctionnerait de manière partielle, inadéquate, ou avec des temps de réponse inadapté
\item
\textbf{Les composants périphériques}, c'est-à-dire les composants qui aident à avoir une architecture système résiliente, évolutive et maintenable
\item
\textbf{Les composants de supervision}, qui permettent de s'assurer que tout fonctionne correctement, et qu'aucun incident n'est prévu ou n'a été constaté récemment.
\end{enumerate}
\section{Composants fonctionnels}
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/diagrams/infrastructure.drawio.png}}
\caption{Une architecture possible pour un système de données moderne, qui combine plusieurs composants \cite[p. 5]{data_intensive}}
\end{figure}
Une application \textit{data-intensive} est généralement composée des blocs fonctionnels suivants \cite[p. 3]{data_intensive} \footnote{De manière non-exhaustive, mais assez complète quand même.}:
\begin{tabular}{|p{0.2\linewidth}|p{0.55\linewidth}|p{0.15\linewidth}|}
\hline
Type & Utilité & Exemples \\
\hline
Bases de données & Stocker des données, pour pouvoir les retrouver (elles-même ou une autre application) plus tard & PostgreSQL, MySQL, MariaDB, SQLite \\
\hline
Caches & Conserver en mémoire des résultats gourmands, afin de pouvoir les réexploiter plus rapidement, sans avoir à les recalculer & Redis, Memcache \\
\hline
Moteurs d'indexation & Autoriser la recherche de résultats sur base de mots-clés ou en utilisant des filtres particuliers & ElasticSearch, Solr \\
\hline
Traitements asynchrones & Exécution de traitements lourds ou pouvant être démarrés \textit{a posteriori} & Celery \\
\hline
\end{tabular}
\subsection{Bases de données}
\subsection{Mise en cache}
\subsection{Moteurs d'indexation}
\subsection{Tâches asynchrones}
\section{Composants périphériques}
Les composants périphériques gravitent autour du fonctionnement normal de l'application.
Ce sont les "petites mains" qui aident à ce que l'application soit résiliente ou que la charge soit amoindrie ou répartie entre plusieurs instances.
Il s'agit des composants comme le proxy inverse ou les distributeurs de charge: ce sont des composants qui sont fortement recommandés, mais pas obligatoires à au bon fonctionnement de l'application.
\begin{quote}
"Focusing on resilience often means that a firm can handle events that may cause crises for most organizations in a manner that is routine and mundane.
Specific architectural patterns that they implemented included fail fasts (settings aggressive timeouts such that failing components don't make the entire system crawl to a halt), fallbacks (designing each feature to degrade or fall back to a lower quality representation), and feature removal (removing non-critical features when they run slowly from any given page to prevent them from impacting the member experience).
Another astonishing example of the resilience that the netflix team created beyond preserving business continuity during the AWS outage, was that they went over six hours into the AWS outage before declaring a Sev 1 incident, assuming that AWS service would eventually be restored (i.e., "AWS will come back\ldots it usually does, right ?").
Only after six hours into the outage did they activate any business continuity procedures. \cite[p. 282]{devops_handbook}
\end{quote}
Avoir des composants périphériques correctement configurés permet d'anticiper ce type d'erreurs, et donc d'augmenter la résilience de l'application. \index{Résilience}
Certaines plateformes, comme Amazon Web Services \index{AWS}, favorisent la flexibilité et l'eslasticité plutôt que la disponibilité d'une seule machine. \cite[p. 8]{data_design}
C'est donc une bonne idée de faire en sorte que des erreurs d'indisponibilité peuvent arriver \footnote{Netflix a par exemple développer le \href{https://github.com/netflix/chaosmonkey}{Chaos Monkey}, qui s'occupe d'éteindre au hasard des machines virtuels et containers dans un environnement de production, juste pour faire en sorte que les équipes soient drillées à développer toutes sortes de mécanismes de remise en service - }.
\begin{tabular}{|p{0.2\linewidth}|p{0.55\linewidth}|p{0.15\linewidth}|}
\hline
Type & Utilité & Exemples \\
\hline
Firewall & & firewalld, UFW \\
\hline
Reverse Proxy & & Nginx \\
\hline
Load balancers & Distribution de la charge sur plusieurs serveurs, en fonction du nombre d'utilisateurs, du nombre de requêtes à gérer et du temps que prennent chacune d'entre elles & HAProxy \\
\hline
Serveurs d'application & & Gunicorn, Uvicorn \\
\hline
\end{tabular}
Si nous schématisons l'infrastructure et le chemin parcouru par une requête, nous pourrions arriver à la synthèse suivante :
\begin{enumerate}
\item
L'utilisateur fait une requête via son navigateur (Firefox ou Chrome)
\item
Le navigateur envoie une requête http, sa version, un verbe (GET, POST, \ldots), un port et éventuellement du contenu
\item
Le firewall du serveur (Debian GNU/Linux, CentOS, \dots vérifie si la requête peut être prise en compte
\item
La requête est transmise à l'application qui écoute sur le port (probablement 80 ou 443; et \emph{a priori} Nginx)
\item
Elle est ensuite transmise par socket et est prise en compte par un des \emph{workers} (= un processus Python) instancié par Gunicorn.
Sil'un de ces travailleurs venait à planter, il serait automatiquement réinstancié par Supervisord.
\item
Qui la transmet ensuite à l'un de ses \emph{workers} (= un processus Python).
\item
Après exécution, une réponse est renvoyée à l'utilisateur.
\end{enumerate}
\includegraphics{images/diagrams/architecture.png}
\subsection{Firewall}
\subsection{Reverse proxy}
Le principe du \textbf{proxy inverse} est de pouvoir rediriger du trafic entrant vers une application hébergée sur le système. Il serait tout à fait possible de rendre notre application directement accessible depuis l'extérieur, mais le proxy a aussi l'intérêt de pouvoir élever la sécurité du serveur (SSL) et décharger le serveur applicatif grâce à un mécanisme de cache ou en compressant certains résultats \footnote{\url{https://fr.wikipedia.org/wiki/Proxy_inverse}}
\subsection{Répartiteur de charge (\textit{Load balancer})}
Les répartiteurs de charges sont super utiles pour donner du mou à l'infrastructure:
\begin{enumerate}
\item
Maintenance et application de patches,
\item
Répartition des utilisateurs connectés,
\item
\ldots
\end{enumerate}
\subsection{Serveurs d'application (\textit{Workers})}
Processus et threads.
Au niveau logiciel (la partie mise en subrillance ci-dessus), la requête arrive dans les mains du processus Python, qui doit encore
\begin{enumerate}
\item
Effectuer le routage des données,
\item
Trouver la bonne fonction à exécuter,
\item
Récupérer les données depuis la base de données,
\item
Effectuer le rendu ou la conversion des données,
\item
Renvoyer une réponse à l'utilisateur.
\end{enumerate}
\section{Composants de supervision}
\begin{tabular}{|p{0.2\linewidth}|p{0.55\linewidth}|p{0.15\linewidth}|}
\hline
Type & Utilité & Exemples \\
\hline
Supervision des processus & & Supervisord \\
\hline
Flux d'évènements & & syslogd, journalctl \\
\hline
Notifications & & Zabbix, Nagios, Munin \\
\hline
Télémétrie & & \\
\hline
\end{tabular}
\subsection{Supervision des processus}
https://circus.readthedocs.io/en/latest/, https://uwsgi-docs.readthedocs.io/en/latest/, statsd
\subsection{Journaux d'évènements}
La présence de journaux, leur structure et de la définition précise de leurs niveaux est essentielle; ce sont eux qui permettent d'obtenir des informations quant au statut de l'application:
\begin{itemize}
\item Que fait-elle pour le moment ?
\item Qu'a-t-elle fait à un moment en particulier ?
\item Depuis quand n'a-t-elle plus émis de nouveaux évènements ?
\end{itemize}
\begin{quote}
When deciding whether a message should be ERROR or WARN, imagine being woken up at 4 a.m.
Low printer toner is not an ERROR.
--- Dan North, ancien consultant chez ToughtWorks
\end{quote}
Chaque évènement est associé à un niveau; celui-ci indique sa criticité et sa émet un premier tri quant à sa pertinence.
\begin{itemize}
\item
\textbf{DEBUG}: Il s'agit des informations qui concernent tout ce qui peut se passer durant l'exécution de l'application.
Généralement, ce niveau est désactivé pour une application qui passe en production, sauf s'il est nécessaire d'isoler un comportement en particulier, auquel cas il suffit de le réactiver temporairement.
\item
\textbf{INFO}: Enregistre les actions pilotées par un utilisateur - Démarrage de la transaction de paiement, \ldots
\item
\textbf{WARN}: Regroupe les informations qui pourraient potentiellement devenir des erreurs.
\item
\textbf{ERROR}: Indique les informations internes - Erreur lors de l'appel d'une API, erreur interne, \ldots
\item
\textbf{FATAL} (ou \textbf{EXCEPTION}): \ldots généralement suivie d'une terminaison du programme - Bind raté d'un socket, etc.
\end{itemize}
La configuration des \emph{loggers} est relativement simple, un peu plus complexe si nous nous penchons dessus, et franchement complète si nous creusons encore.
Il est ainsi possible de définir des formattages, différents gestionnaires (\emph{handlers}) et loggers distincts, en fonction de nos applications.
Sauf que comme nous l'avons vu avec les 12 facteurs, nous devons traiter les informations de notre application comme un flux d'évènements.
Il n'est donc pas réellement nécessaire de chipoter la configuration, puisque la seule classe qui va réellement nous intéresser concerne les \texttt{StreamHandler}, qui seront pris en charge par gunicorn \index{Gunicorn}.
La configuration que nous allons utiliser est celle-ci:
\begin{enumerate}
\item
\textbf{Formattage}: à définir - mais la variante suivante est complète, lisible et pratique:
\texttt{\{levelname\}\ \{asctime\}\ \{module\}\ \{process:d\}\ \{thread:d\}\ \{message\}}
\item
\textbf{Handler}: juste un, qui définit un \texttt{StreamHandler}
\item
\textbf{Logger}: pour celui-ci, nous avons besoin d'un niveau (\texttt{level}) et de savoir s'il faut propager les informations vers les sous-paquets, auquel cas il nous suffira de fixer la valeur de
\texttt{propagate} à \texttt{True}.
\end{enumerate}
Pour utiliser nos loggers, il suffit de copier le petit bout de code suivant:
\begin{minted}{python}
import logging
logger = logging.getLogger(__name__)
logger.debug('helloworld')
\end{minted}
\href{https://docs.djangoproject.com/en/stable/topics/logging/\#examples}{Par
exemples}.
\subsection{Télémétrie et exploitation des journaux}
Des erreurs sur un environnement de production arriveront, tôt ou tard, et seront sans doute plus compliquée à corriger qu'un morceau de code dans un coin du code.
L'exploitation des journaux permettra de comprendre, analyser, voire corriger, certains incidents.
Comme nous l'avons vu, en fonction de l'infrastructure choisie, il existe plusieurs types de journaux:
\begin{enumerate}
\item Les journaux applicatifs: ie. le flux d'évènements généré par votre application Django
\item Les journaux du serveur: Nginx, Gunicorn, \ldots
\item Les journaux des autres composants: base de données, service de mise en cache, ...
\end{enumerate}
Une manière de faire consiste à se connecter physiquement ou à distance à la machine pour analyser les logs.
En pratique, c'est impossible: entre les répartiteurs de charge, les différents serveurs, \ldots, il vous sera physiquement impossible de récupérer une information cohérente.
La solution consiste à agréger vos journaux à un seul et même endroit:
\includegraphics{images/infrastructure/mattsegal-logging.png}
CC https://mattsegal.dev/django-monitoring-stack.html
\subsection{Sumologic}
\subsection{Alternatives}
Il existe également \href{https://munin-monitoring.org}{Munin}, \href{https://www.elastic.co}{Logstash, ElasticSearch et Kibana (ELK-Stack)} ou \href{https://www.fluentd.org}{Fluentd}.
La récupération de métriques augmente la confiance que l'on peut avoir dans la solution.
L'analyse de ces métriques garantit un juste retour d'informations, sous réserve qu'elles soient exploitées correctement.
La première étape consiste à agréger ces données dans un dépôt centralisé, tandis que la seconde étape exigera une structuration correcte des données envoyées.
La collecte des données doit récupérer des données des couches métiers, applicatives et d'environnement.
Ces données couvrent les évènements, les journaux et les métriques - indépendamment de leur source - le pourcentage d'utilisation du processeur, la mémoire utilisée, les disques systèmes, l'utilisation du réseau, \ldots
\begin{enumerate}
\item
\textbf{Métier}: Le nombre de ventes réalisées, le nombre de nouveaux utilisateurs, les résultats de tests A/B, \ldots
\item
\textbf{Application}: Le délai de réalisation d'une transaction, le temps de réponse par utilisateur, \ldots
\item
\textbf{Infrastructure}: Le trafic du serveur Web, le taux d'occupation du CPU, \ldots
\item
\textbf{Côté client}: Les erreurs applicatives, les transactions côté utilisateur, \ldots
\item
\textbf{Pipeline de déploiement}: Statuts des builds, temps de mise à disposition d'une fonctionnalité, fréquence des déploiements, statuts des environnements, \ldots
\end{enumerate}
Bien utilisés, ces outils permettent de prévenir des incidents de manière empirique.
\begin{quote}
Monitoring is so important that our monitoring systems need to be more available and scalable than the systems being monitored.
-- Adrian Cockcroft \cite[p. 200]{devops_handbook}
\end{quote}
Histoire de schématiser, toute équipe se retrouve à un moment où un autre dans la situation suivante: personne ne veut appuyer sur le gros bouton rouge issu de l'automatisation de la chaîne de production et qui dit "Déploiement".
Et pour cause: une fois que nous aurons trouvé une joyeuse victime qui osera braver le danger, il y aura systématiquement des imprévus, des petits détails qui auront été oubliés sur le côté de la route, et qui feront lâchement planter les environnements.
Et puisque nous n'aurons pas (encore) de télémétrie, le seul moment où nous comprendrons qu'il y a un problème, sera quand un utilisateur viendra se plaindre.
\subsection{Statsd}
https://www.datadoghq.com/blog/statsd/