Sur Internet (service mesh), et en voici un autre. Hourra ! Mais pourquoi ? Parce que je veux exprimer mon opinion, qui est qu'il aurait été préférable que les services mesh apparaissent il y a 10 ans, avant l'émergence de plateformes conteneurisées telles que Docker et Kubernetes. Je ne prétends pas que mon point de vue soit meilleur ou pire que d'autres, mais vu que les services mesh sont des éléments assez complexes, une multiplicité de perspectives aidera à mieux les comprendre.
Je vais parler de la plateforme dotCloud, qui a été construite sur plus d'une centaine de microservices et a soutenu des milliers d'applications en conteneurs. Je vais expliquer les problèmes que nous avons rencontrés lors de son développement et de son lancement, et comment les services mesh auraient pu aider (ou pas).
L'histoire de dotCloud
J'ai déjà écrit sur l'histoire de dotCloud et le choix de l'architecture pour cette plateforme, mais j'ai peu parlé du niveau réseau. Si vous ne souhaitez pas vous plonger dans la lecture sur dotCloud, en résumé, voici l'essentiel : c'est une plateforme en tant que service PaaS, permettant aux clients de lancer un large éventail d'applications (Java, PHP, Python…), avec le soutien d'une large gamme de services de données (MongoDB, MySQL, Redis…) et un flux de travail similaire à celui de Heroku : vous téléchargez votre code sur la plateforme, elle construit des images de conteneur et les déploie.
Je vais expliquer comment le trafic était dirigé vers la plateforme dotCloud. Ce n'est pas parce que c'était particulièrement génial (bien que pour son époque, le système fonctionnait assez bien !), mais surtout parce qu'avec les outils modernes, un design de ce type peut facilement être mis en œuvre en peu de temps par une petite équipe, si elle a besoin d'un moyen de router le trafic entre un tas de microservices ou une multitude d'applications. Ainsi, on peut comparer les options : que se passe-t-il si l'on développe tout soi-même ou si l'on utilise un service mesh existant. Le choix standard : faire soi-même ou acheter.
Routage du trafic pour les applications hébergées
Les applications sur dotCloud peuvent fournir des points de terminaison HTTP et TCP.
Points de terminaison HTTP ajoutés dynamiquement à la configuration du cluster de balanceurs de charge . Cela ressemble à ce que font aujourd'hui les ressources dans Kubernetes et un équilibrage de charge comme .
Les clients se connectent aux points de terminaison HTTP via les noms de domaine correspondants, à condition que le nom de domaine pointe vers les balanceurs de charge de dotCloud. Rien de spécial.
Points de terminaison TCP sont liées au numéro de port, qui est ensuite transmis à tous les conteneurs de cette pile via des variables d'environnement.
Les clients peuvent se connecter aux points de terminaison TCP en utilisant le nom d'hôte approprié (quelque chose comme gateway-X.dotcloud.com) et le numéro de port.
Ce nom d'hôte se résout sur un cluster de serveurs « nats » (sans rapport avec ), qui va router les connexions TCP entrantes vers le bon conteneur (ou, dans le cas des services avec équilibrage de charge, vers les bons conteneurs).
Si vous êtes familier avec Kubernetes, cela vous rappellera probablement les services .
Sur la plateforme dotCloud, il n'y avait pas d'équivalent aux services : pour simplifier, l'accès aux services se faisait de la même façon à l'intérieur et à l'extérieur de la plateforme.
Tout était organisé assez simplement : les premières mises en œuvre de réseaux de routage HTTP et TCP ne faisaient probablement que quelques centaines de lignes Python. Des algorithmes simples (je dirais même naïfs) qui ont été améliorés à mesure que la plateforme grandissait et que des exigences supplémentaires apparaissaient.
Une refonte complète du code existant n'était pas nécessaire. En particulier, peuvent directement utiliser l'adresse obtenue via les variables d'environnement.
En quoi cela diffère-t-il d'un service mesh moderne ?
Visibilité limitée de la chaîne.. Nous n'avions aucune métrique pour la grille de routage TCP. En ce qui concerne le routage HTTP, les versions ultérieures ont introduit des métriques HTTP détaillées avec des codes d'erreur et des temps de réponse, mais les services meshes modernes vont encore plus loin, en intégrant des systèmes de collecte de métriques comme Prometheus, par exemple.
La visibilité est importante non seulement du point de vue opérationnel (pour aider à résoudre les problèmes), mais aussi lors du déploiement de nouvelles fonctionnalités. Il s'agit d'un déploiement et .
Efficacité de routage était également limitée. Dans le réseau de routage de dotCloud, tout le trafic devait passer par un cluster de nœuds de routage dédiés. Cela impliquait un croisement potentiel de plusieurs zones de disponibilité (AZ) et une augmentation considérable de la latence. Je me souviens avoir résolu des problèmes avec un code qui exécutait plus d'une centaine de requêtes SQL par page et ouvrait une nouvelle connexion avec le serveur SQL pour chaque requête. Lors d'une exécution locale, la page se charge instantanément, mais dans dotCloud, le chargement prend plusieurs secondes, car chaque connexion TCP (et requête SQL subséquente) nécessite des dizaines de millisecondes. Dans ce cas précis, le problème a été résolu par des connexions persistantes.
Les maillages de services modernes gèrent mieux de tels problèmes. Tout d'abord, ils vérifient que les connexions sont routées à la source. Le flux logique reste le même : client → maillage → service, mais maintenant le maillage fonctionne localement, et non sur des nœuds distants, donc la connexion client → maillage est locale et très rapide (microsecondes au lieu de millisecondes).
Les maillages de services modernes mettent également en œuvre des algorithmes de répartition de charge plus intelligents. En surveillant la disponibilité des backends, ils peuvent diriger plus de trafic vers les backends plus rapides, ce qui améliore les performances globales.
Sécurité également mieux. Le réseau de routage de dotCloud fonctionnait entièrement sur EC2 Classic et ne chiffrait pas le trafic (supposant que si quelqu'un parvenait à installer un sniffer sur le trafic réseau EC2, vous aviez déjà de gros problèmes). Les maillages de services modernes protègent de manière transparente tout notre trafic, par exemple, avec une authentification mutuelle TLS suivie d'un chiffrement.
Routage du trafic pour les services de la plateforme
Bien, nous avons parlé du trafic entre les applications, mais qu'en est-il de la plateforme elle-même de dotCloud ?
La plateforme elle-même se composait d'environ une centaine de microservices, chacun responsable de diverses fonctions. Certains prenaient des requêtes d'autres, et certains étaient des travailleurs en arrière-plan qui se connectaient à d'autres services mais ne prenaient pas eux-mêmes de connexions. Quoi qu'il en soit, chaque service doit connaître les points de terminaison des adresses auxquelles il doit se connecter.
De nombreux services de haut niveau peuvent utiliser le réseau de routage décrit ci-dessus. En réalité, beaucoup des plus de cent microservices de dotCloud ont été déployés comme des applications ordinaires sur la plateforme dotCloud. Mais un petit nombre de services de bas niveau (en particulier ceux qui implémentent ce réseau de routage) nécessitaient quelque chose de plus simple, avec moins de dépendances (car ils ne pouvaient pas dépendre d'eux-mêmes pour fonctionner - le vieux problème du poulet et de l'œuf).
Ces services de bas niveau, importants, ont été déployés en exécutant des conteneurs directement sur plusieurs nœuds clés. Aucune des services standards de la plateforme n'a été impliquée : le planificateur, le compresseur et le runner. Si vous voulez comparer avec les plateformes modernes de conteneurs, cela ressemble à l'exécution de la couche de gestion directement sur les nœuds, au lieu de déléguer la tâche à Kubernetes. docker run Cela ressemble beaucoup au concept de , utilisé par ou lorsqu'il charge un cluster autonome.
Ces services étaient exposés de manière simple et rudimentaire : leurs noms et adresses étaient listés dans un fichier YAML; chaque client devait prendre une copie de ce fichier YAML pour le déploiement.
D'une part, c'est extrêmement fiable, car cela ne nécessite pas de stockage externe de clé/valeur, comme Zookeeper (n'oubliez pas qu'à l'époque, etcd ou Consul n'existaient pas encore). D'autre part, cela compliquait le déplacement des services. Chaque fois qu'un service était déplacé, tous les clients devaient obtenir un fichier YAML mis à jour (et potentiellement redémarrer). Ce n'était pas très pratique !
Par la suite, nous avons commencé à mettre en œuvre un nouveau schéma, où chaque client se connectait à un serveur proxy local. Au lieu de l'adresse et du port, il lui suffisait de connaître simplement le numéro de port du service et de se connecter via localhost. Le serveur proxy local gère cette connexion et la redirige vers le serveur réel. Désormais, lors du déplacement du backend vers une autre machine ou lors de l'évolution, au lieu de mettre à jour tous les clients, il suffit de mettre à jour tous ces proxys locaux; et le redémarrage n'est plus nécessaire.
(Il a également été prévu d'encapsuler le trafic dans des connexions TLS et d'installer un autre serveur proxy du côté réception, ainsi que de vérifier les certificats TLS sans l'implication du service de réception, qui est configuré pour accepter les connexions uniquement sur localhost. À propos de cela plus tard).
Cela ressemble beaucoup à d'Airbnb, mais la différence essentielle est que SmartStack est implémenté et déployé en production, tandis que le système de routage interne de dotCloud a été rangé dans un tiroir lorsque dotCloud est devenu Docker.
Je considère personnellement SmartStack comme l'un des précurseurs de systèmes comme Istio, Linkerd et Consul Connect, car ils suivent tous un même modèle :
- Lancer un proxy sur chaque nœud.
- Les clients se connectent au proxy.
- La couche de contrôle met à jour la configuration du serveur proxy lorsque les backends changent.
- … Profit !
La mise en œuvre moderne d'un maillage de services
Si nous devions mettre en œuvre un tel maillage aujourd'hui, nous pourrions utiliser des principes similaires. Par exemple, configurer une zone DNS interne en faisant correspondre les noms de services aux adresses dans l'espace 127.0.0.0/8. Ensuite, lancer HAProxy sur chaque nœud du cluster, acceptant des connexions sur chaque adresse de service (dans ce sous-réseau 127.0.0.0/8) et rediriger/balancer la charge vers les backends appropriés. La configuration de HAProxy peut être gérée , permettant de stocker des informations sur le backend dans etcd ou Consul et de pousser automatiquement la configuration mise à jour sur HAProxy lorsque nécessaire.
C'est à peu près comme ça qu'Istio fonctionne ! Mais avec quelques différences :
- Un des fournisseurs de cloud local aux États-Unis utilise au lieu de HAProxy.
- Il conserve la configuration du backend via l'API Kubernetes au lieu de etcd ou Consul.
- Les services se voient attribuer des adresses dans le sous-réseau interne (adresses Kubernetes ClusterIP) au lieu de 127.0.0.0/8.
- Il a un composant supplémentaire (Citadel) pour ajouter une authentification mutuelle TLS entre le client et les serveurs.
- Il prend en charge de nouvelles fonctionnalités, telles que la rupture de circuit, le traçage distribué, le déploiement de canary, etc.
Examinons brièvement certaines différences.
Envoy Proxy
Envoy Proxy a été écrit par la société Lyft [concurrent d'Uber sur le marché des taxis - note du traducteur]. Il ressemble beaucoup à d'autres proxies (comme HAProxy, Nginx, Traefik…), mais Lyft a écrit le leur car ils avaient besoin de fonctionnalités manquantes dans d'autres proxies, et il leur a semblé plus raisonnable de créer un nouveau proxy que d'élargir un existant.
Envoy peut être utilisé seul. Si j'ai un service spécifique qui doit se connecter à d'autres services, je peux le configurer pour se connecter à Envoy, puis configurer et reconfigurer dynamiquement Envoy avec l'emplacement d'autres services, tout en bénéficiant de nombreuses fonctionnalités supplémentaires, comme la visibilité. Au lieu d'une bibliothèque cliente personnalisée ou d'une intégration dans le code de traçage des appels, nous dirigeons le trafic vers Envoy, qui collecte des métriques pour nous.
Mais Envoy est également capable de fonctionner comme un plan de données (data plane) pour le service mesh. Cela signifie que maintenant, pour ce service mesh, Envoy est configuré par le plan de contrôle (control plane).
Le plan de contrôle
Dans le plan de contrôle, Istio s'appuie sur l'API Kubernetes. Ce n'est pas très différent de l'utilisation de confd, qui s'appuie sur etcd ou Consul pour visualiser un ensemble de clés dans le magasin de données. Istio consulte l'ensemble des ressources Kubernetes via l'API Kubernetes.
En passant: personnellement, j'ai trouvé utile cette , qui dit :
Le serveur API Kubernetes est un « serveur stupide » qui offre stockage, gestion des versions, validation, mise à jour et sémantique des ressources API.
Istio est conçu pour fonctionner avec Kubernetes ; et si vous souhaitez l'utiliser en dehors de Kubernetes, vous devez exécuter une instance du serveur API Kubernetes (et du service auxiliaire etcd).
Adresses des services
Istio s'appuie sur les adresses ClusterIP attribuées par Kubernetes, donc les services Istio obtiennent une adresse interne (pas dans la plage 127.0.0.0/8).
Le trafic vers l'adresse ClusterIP pour un service spécifique dans le cluster Kubernetes sans Istio est intercepté par kube-proxy et envoyé à la partie serveur de ce proxy. Si vous êtes intéressé par les détails techniques, kube-proxy établit des règles iptables (ou des équilibreurs de charge IPVS, selon la façon dont il est configuré) pour réécrire les adresses IP de destination des connexions se dirigeant vers l'adresse ClusterIP.
Après l'installation d'Istio dans le cluster Kubernetes, rien ne change tant qu'il n'est pas explicitement activé pour ce consommateur ou même pour tout l'espace de noms, en introduisant le conteneur sidecar dans des pods personnalisés. Ce conteneur lancera une instance d'Envoy et installera un ensemble de règles iptables pour intercepter le trafic allant vers d'autres services et rediriger ce trafic vers Envoy.
Lors de l'intégration avec le DNS Kubernetes, cela signifie que notre code peut se connecter par le nom du service, et tout « fonctionne simplement ». En d'autres termes, notre code émet des requêtes de type http://api/v1/users/4242, alors api il résout la requête à 10.97.105.48, les règles iptables interceptent les connexions avec 10.97.105.48 et les redirigent vers le proxy local Envoy, et ce proxy local dirigera la requête vers le véritable backend API. Ouf !
Des fonctionnalités supplémentaires
Istio fournit également un chiffrement et une authentification de bout en bout via mTLS (mutual TLS). Cela est géré par un composant appelé Citadel.
Il y a aussi un composant Mixer, que Envoy peut interroger pour chaque la requête, afin de prendre une décision spéciale à propos de cette requête en fonction de divers facteurs tels que les en-têtes, la charge du backend, etc. (ne vous inquiétez pas : il existe de nombreux moyens d'assurer le bon fonctionnement de Mixer, et même s'il tombe en panne, Envoy continuera de fonctionner normalement en tant que proxy).
Et bien sûr, nous avons mentionné la traçabilité : Envoy collecte une énorme quantité de métriques tout en fournissant une traçabilité distribuée. Dans l'architecture des microservices, si une requête API doit passer par les microservices A, B, C et D, alors lorsqu'elle entre dans le système, la traçabilité distribuée ajoutera un identifiant unique à la requête et conservera cet identifiant à travers les sous-requêtes vers tous ces microservices, permettant d'enregistrer tous les appels associés, leurs délais, etc.
Développer ou acheter
Istio a la réputation d'être un système complexe. En revanche, la construction du maillage de routage que j'ai décrit au début de cet article est relativement simple avec les outils existants. Alors, cela vaut-il la peine de créer votre propre service mesh ?
Si nous avons des besoins modestes (pas besoin de traçabilité, disrupteur de chaîne et autres subtilités), alors des pensées sur le développement de notre propre outil surgissent. Mais si nous utilisons Kubernetes, il se peut même qu'il ne soit pas nécessaire, car Kubernetes fournit déjà des outils de base pour la découverte de services et l'équilibrage de charge.
Mais si nous avons des exigences avancées, alors « acheter » un service mesh semble être une bien meilleure option. (Ce n'est pas toujours vraiment un « achat », car Istio est fourni avec un code source ouvert, mais nous devons tout de même investir du temps d'ingénierie pour comprendre son fonctionnement, le déployer et le gérer).
Que choisir : Istio, Linkerd ou Consul Connect ?
Jusqu'à présent, nous avons uniquement parlé d'Istio, mais ce n'est pas le seul service mesh. Une alternative populaire est , et il y a aussi .
Que choisir ?
Honnêtement, je ne sais pas. Pour l'instant, je ne me considère pas suffisamment compétent pour répondre à cette question. Il existe plusieurs comparaisons de ces outils et même .
L'une des approches prometteuses consiste à utiliser un outil comme . Il implémente une couche d'abstraction pour simplifier et unifier les API fournies par les service meshes. Au lieu d'étudier les API spécifiques (et, à mon avis, relativement complexes) de différents service meshes, nous pouvons utiliser des constructions plus simples de SuperGloo - et passer facilement d'un à l'autre, comme si nous avions un format intermédiaire de configuration décrivant des interfaces HTTP et des backends, capable de générer la configuration réelle pour Nginx, HAProxy, Traefik, Apache...
J'ai un peu joué avec Istio et SuperGloo, et dans le prochain article, je veux montrer comment ajouter Istio ou Linkerd dans un cluster existant à l'aide de SuperGloo, et dans quelle mesure ce dernier s'acquitte de sa tâche, c'est-à-dire permettre de passer d'un service mesh à un autre sans réécrire les configurations.
Source : habr.com
