One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

Aloha, les gens ! Je m'appelle Oleg Anastasyev, je travaille chez Odnoklassniki dans l'équipe Platform. Et à part moi, il y a beaucoup de matériel qui fonctionne à Odnoklassniki. Nous disposons de quatre centres de données avec environ 500 racks et plus de 8 XNUMX serveurs. À un moment donné, nous avons réalisé que l'introduction d'un nouveau système de gestion permettrait de charger les équipements plus efficacement, de faciliter la gestion des accès, d'automatiser la (re)distribution des ressources informatiques, d'accélérer le lancement de nouveaux services et d'accélérer les réponses. à des accidents de grande ampleur.

Qu’en est-il arrivé ?

Outre moi et un tas de matériel, il y a aussi des personnes qui travaillent avec ce matériel : des ingénieurs qui travaillent directement dans les centres de données ; les réseauteurs qui configurent des logiciels réseau ; les administrateurs, ou SRE, qui assurent la résilience de l'infrastructure ; et des équipes de développement, chacune d’elles est responsable d’une partie des fonctionnalités du portail. Le logiciel qu'ils créent fonctionne à peu près comme ceci :

One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

Les demandes des utilisateurs sont reçues à la fois sur les fronts du portail principal www.ok.ru, et sur d'autres, par exemple sur le front de l'API musicale. Pour traiter la logique métier, ils appellent le serveur d'applications qui, lors du traitement de la demande, appelle les microservices spécialisés nécessaires - one-graph (graphique des connexions sociales), user-cache (cache des profils utilisateur), etc.

Chacun de ces services est déployé sur de nombreuses machines, et chacun d'eux a des développeurs responsables chargés du fonctionnement des modules, de leur fonctionnement et de leur évolution technologique. Tous ces services fonctionnent sur des serveurs matériels et, jusqu'à récemment, nous lancions exactement une tâche par serveur, c'est-à-dire qu'elle était spécialisée pour une tâche spécifique.

Pourquoi donc? Cette approche présentait plusieurs avantages :

  • Soulagé gestion de masse. Disons qu'une tâche nécessite des bibliothèques, des paramètres. Ensuite, le serveur est attribué exactement à un groupe spécifique, la politique cfengine pour ce groupe est décrite (ou elle a déjà été décrite) et cette configuration est déployée de manière centralisée et automatique sur tous les serveurs de ce groupe.
  • Simplifié diagnostics. Disons que vous observez la charge accrue sur le processeur central et réalisez que cette charge ne peut être générée que par la tâche qui s'exécute sur ce processeur matériel. La recherche d’un coupable se termine très rapidement.
  • Simplifié surveillance. Si quelque chose ne va pas avec le serveur, le moniteur le signale et vous savez exactement qui est à blâmer.

Un service composé de plusieurs répliques se voit attribuer plusieurs serveurs, un pour chacun. Ensuite, la ressource informatique du service est allouée très simplement : le nombre de serveurs dont dispose le service, la quantité maximale de ressources qu'il peut consommer. « Facile » ne signifie pas ici qu'il est facile à utiliser, mais dans le sens où l'allocation des ressources se fait manuellement.

Cette approche nous a également permis de faire configurations de fer spécialisées pour une tâche exécutée sur ce serveur. Si la tâche stocke de grandes quantités de données, nous utilisons alors un serveur 4U avec un châssis de 38 disques. Si la tâche est purement informatique, nous pouvons alors acheter un serveur 1U moins cher. C’est efficace sur le plan informatique. Entre autres choses, cette approche nous permet d'utiliser quatre fois moins de machines avec une charge comparable à un réseau social convivial.

Une telle efficacité dans l'utilisation des ressources informatiques devrait également garantir l'efficacité économique, si l'on part du principe que ce qui coûte le plus cher, ce sont les serveurs. Pendant longtemps, le matériel était le plus cher, et nous avons déployé beaucoup d'efforts pour réduire le prix du matériel, en proposant des algorithmes de tolérance aux pannes pour réduire les exigences de fiabilité du matériel. Et aujourd’hui nous sommes arrivés au stade où le prix du serveur a cessé d’être déterminant. Si vous ne considérez pas les derniers exotiques, la configuration spécifique des serveurs dans le rack n'a pas d'importance. Nous avons maintenant un autre problème : le prix de l'espace occupé par le serveur dans le centre de données, c'est-à-dire l'espace dans le rack.

Réalisant que c'était le cas, nous avons décidé de calculer l'efficacité avec laquelle nous utilisions les racks.
Nous avons pris le prix du serveur le plus puissant parmi ceux économiquement justifiables, calculé combien de serveurs de ce type nous pourrions placer dans des racks, combien de tâches nous exécuterions dessus sur la base de l'ancien modèle « un serveur = une tâche » et combien de ces serveurs les tâches pourraient utiliser l’équipement. Ils ont compté et versé des larmes. Il s'est avéré que notre efficacité dans l'utilisation des racks est d'environ 11 %. La conclusion est évidente : nous devons accroître l’efficacité de l’utilisation des centres de données. Il semblerait que la solution soit évidente : vous devez exécuter plusieurs tâches sur un seul serveur à la fois. Mais c'est là que commencent les difficultés.

La configuration de masse devient considérablement plus compliquée : il est désormais impossible d'attribuer un groupe à un serveur. Après tout, plusieurs tâches de commandes différentes peuvent désormais être lancées sur un seul serveur. De plus, la configuration peut être conflictuelle pour différentes applications. Le diagnostic devient également plus compliqué : si vous constatez une augmentation de la consommation du processeur ou du disque sur un serveur, vous ne savez pas quelle tâche pose problème.

Mais l’essentiel est qu’il n’y ait pas d’isolation entre les tâches exécutées sur la même machine. Voici, par exemple, un graphique du temps de réponse moyen d'une tâche serveur avant et après le lancement d'une autre application informatique sur le même serveur, sans aucun rapport avec la première - le temps de réponse de la tâche principale a considérablement augmenté.

One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

Évidemment, vous devez exécuter des tâches soit dans des conteneurs, soit dans des machines virtuelles. Étant donné que presque toutes nos tâches s'exécutent sous un seul système d'exploitation (Linux) ou sont adaptées à celui-ci, nous n'avons pas besoin de prendre en charge de nombreux systèmes d'exploitation différents. Par conséquent, la virtualisation n'est pas nécessaire ; en raison de la surcharge supplémentaire, elle sera moins efficace que la conteneurisation.

En tant qu'implémentation de conteneurs pour exécuter des tâches directement sur les serveurs, Docker est un bon candidat : ​​les images du système de fichiers résolvent bien les problèmes de configurations conflictuelles. Le fait que les images puissent être composées de plusieurs couches nous permet de réduire considérablement la quantité de données nécessaires à leur déploiement sur l'infrastructure, en séparant les parties communes en couches de base distinctes. Ensuite, les couches de base (et les plus volumineuses) seront mises en cache assez rapidement dans toute l'infrastructure, et pour fournir de nombreux types d'applications et de versions différentes, seules de petites couches devront être transférées.

De plus, un registre prêt à l'emploi et le balisage d'images dans Docker nous fournissent des primitives prêtes à l'emploi pour la gestion des versions et la livraison du code en production.

Docker, comme toute autre technologie similaire, nous offre un certain niveau d'isolation des conteneurs dès le départ. Par exemple, l'isolation de la mémoire - chaque conteneur se voit attribuer une limite d'utilisation de la mémoire machine, au-delà de laquelle il ne consommera pas. Vous pouvez également isoler les conteneurs en fonction de l'utilisation du processeur. Mais pour nous, une isolation standard ne suffisait pas. Mais plus à ce sujet ci-dessous.

L’exécution directe de conteneurs sur les serveurs n’est qu’une partie du problème. L'autre partie est liée à l'hébergement des conteneurs sur les serveurs. Vous devez comprendre quel conteneur peut être placé sur quel serveur. Ce n'est pas une tâche si facile, car les conteneurs doivent être placés sur les serveurs aussi densément que possible sans réduire leur vitesse. Un tel placement peut également être difficile du point de vue de la tolérance aux pannes. Nous souhaitons souvent placer des répliques du même service dans différents racks ou même dans différentes salles du centre de données, de sorte qu'en cas de panne d'un rack ou d'une salle, nous ne perdions pas immédiatement toutes les répliques de service.

La distribution manuelle des conteneurs n'est pas une option lorsque vous disposez de 8 8 serveurs et de 16 à XNUMX XNUMX conteneurs.

De plus, nous souhaitions donner aux développeurs plus d'indépendance dans l'allocation des ressources afin qu'ils puissent héberger eux-mêmes leurs services en production, sans l'aide d'un administrateur. Dans le même temps, nous souhaitions garder le contrôle afin qu'un service mineur ne consomme pas toutes les ressources de nos centres de données.

Évidemment, nous avons besoin d’une couche de contrôle qui ferait cela automatiquement.

Nous sommes donc arrivés à une image simple et compréhensible que tous les architectes adorent : trois carrés.

One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

one-cloud masters est un cluster de basculement responsable de l'orchestration du cloud. Le développeur envoie un manifeste au maître, qui contient toutes les informations nécessaires pour héberger le service. Sur cette base, le maître donne des commandes aux serviteurs sélectionnés (machines conçues pour exécuter des conteneurs). Les serviteurs ont notre agent, qui reçoit la commande, envoie ses commandes à Docker et Docker configure le noyau Linux pour lancer le conteneur correspondant. En plus d'exécuter des commandes, l'agent signale en permanence au maître les changements d'état de la machine minion et des conteneurs qui y sont exécutés.

Affectation des ressources

Examinons maintenant le problème de l'allocation de ressources plus complexe pour de nombreux serviteurs.

Une ressource informatique dans un cloud est :

  • La quantité de puissance du processeur consommée par une tâche spécifique.
  • La quantité de mémoire disponible pour la tâche.
  • Trafic réseau. Chacun des minions possède une interface réseau spécifique avec une bande passante limitée, il est donc impossible de répartir les tâches sans tenir compte de la quantité de données qu'ils transmettent sur le réseau.
  • Disques. En plus, évidemment, de l'espace pour ces tâches, nous attribuons également le type de disque : HDD ou SSD. Les disques peuvent traiter un nombre fini de requêtes par seconde : IOPS. Par conséquent, pour les tâches qui génèrent plus d'IOPS qu'un seul disque ne peut en gérer, nous allouons également des « broches », c'est-à-dire des périphériques de disque qui doivent être exclusivement réservés à la tâche.

Ensuite pour certains services, par exemple pour le cache utilisateur, on peut enregistrer les ressources consommées de cette manière : 400 cœurs de processeur, 2,5 To de mémoire, 50 Gbit/s de trafic dans les deux sens, 6 To d'espace disque dur réparti sur 100 broches. Ou sous une forme plus familière comme celle-ci :

alloc:
    cpu: 400
    mem: 2500
    lan_in: 50g
    lan_out: 50g
    hdd:100x6T

Les ressources du service de cache utilisateur ne consomment qu'une partie de toutes les ressources disponibles dans l'infrastructure de production. Par conséquent, je veux m'assurer que du coup, en raison d'une erreur de l'opérateur ou non, le cache utilisateur ne consomme pas plus de ressources que celles qui lui sont allouées. Autrement dit, nous devons limiter les ressources. Mais à quoi pourrait-on lier le quota ?

Revenons à notre diagramme grandement simplifié de l'interaction des composants et redessinons-le avec plus de détails - comme ceci :

One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

Ce qui saute aux yeux :

  • L'interface Web et la musique utilisent des clusters isolés du même serveur d'applications.
  • On peut distinguer les couches logiques auxquelles appartiennent ces clusters : fronts, caches, couche de stockage des données et de gestion.
  • Le frontend est hétérogène ; il est constitué de différents sous-systèmes fonctionnels.
  • Les caches peuvent également être dispersés dans le sous-système dont ils mettent en cache les données.

Redessinons à nouveau le tableau :

One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

Bah ! Oui, on voit une hiérarchie ! Cela signifie que vous pouvez répartir les ressources en morceaux plus grands : affectez un développeur responsable à un nœud de cette hiérarchie correspondant au sous-système fonctionnel (comme « musique » dans l'image), et attachez un quota au même niveau de la hiérarchie. Cette hiérarchie nous permet également d'organiser les services de manière plus flexible pour en faciliter la gestion. Par exemple, nous divisons l'ensemble du Web, puisqu'il s'agit d'un très grand groupe de serveurs, en plusieurs groupes plus petits, représentés dans l'image par groupe1, groupe2.

En supprimant les lignes supplémentaires, nous pouvons écrire chaque nœud de notre image sous une forme plus plate : groupe1.web.front, api.music.front, cache-utilisateur.cache.

C’est ainsi qu’on arrive au concept de « file d’attente hiérarchique ». Il porte un nom comme "group1.web.front". Un quota de ressources et de droits d'utilisation lui est attribué. Nous donnerons à la personne de DevOps le droit d'envoyer un service dans la file d'attente, et un tel employé pourra lancer quelque chose dans la file d'attente, et la personne d'OpsDev aura des droits d'administrateur, et maintenant elle pourra gérer la file d'attente, y affecter des personnes, donnez à ces personnes des droits, etc. Les services exécutés sur cette file d'attente s'exécuteront dans les limites du quota de la file d'attente. Si le quota de calcul de la file d'attente n'est pas suffisant pour exécuter tous les services en même temps, ils seront alors exécutés séquentiellement, formant ainsi la file d'attente elle-même.

Regardons de plus près les services. Un service possède un nom complet, qui inclut toujours le nom de la file d'attente. Le service Web frontal portera alors le nom ok-web.group1.web.front. Et le service du serveur d'applications auquel il accède sera appelé ok-app.group1.web.front. Chaque service dispose d'un manifeste qui spécifie toutes les informations nécessaires au placement sur des machines spécifiques : combien de ressources cette tâche consomme, quelle configuration est nécessaire pour cela, combien de répliques il doit y avoir, propriétés de gestion des échecs de ce service. Et une fois le service placé directement sur les machines, ses instances apparaissent. Ils sont également nommés sans ambiguïté - comme le numéro d'instance et le nom du service : 1.ok-web.group1.web.front, 2.ok-web.group1.web.front, …

C'est très pratique : en regardant uniquement le nom du conteneur en cours d'exécution, on peut immédiatement en savoir beaucoup.

Examinons maintenant de plus près ce que ces instances effectuent réellement : des tâches.

Classes d'isolement des tâches

Toutes les tâches dans OK (et probablement partout) peuvent être divisées en groupes :

  • Tâches à latence courte - prod. Pour de telles tâches et services, le délai de réponse (latence) est très important, à quelle vitesse chacune des demandes sera traitée par le système. Exemples de tâches : fronts web, caches, serveurs d'applications, stockage OLTP, etc.
  • Problèmes de calcul - batch. Ici, la vitesse de traitement de chaque demande spécifique n'a pas d'importance. Pour eux, il est important de savoir combien de calculs cette tâche effectuera sur une certaine (longue) période de temps (débit). Il s'agira de toutes les tâches de MapReduce, Hadoop, d'apprentissage automatique, de statistiques.
  • Tâches en arrière-plan - inactives. Pour de telles tâches, ni la latence ni le débit ne sont très importants. Cela inclut divers tests, migrations, recalculs et conversions de données d'un format à un autre. D'une part, ils sont similaires aux calculs, d'autre part, la rapidité avec laquelle ils sont réalisés n'a pas vraiment d'importance pour nous.

Voyons comment de telles tâches consomment des ressources, par exemple le processeur central.

Tâches à court délai. Une telle tâche aura un modèle de consommation CPU similaire à celui-ci :

One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

Une demande de l'utilisateur est reçue pour traitement, la tâche commence à utiliser tous les cœurs de processeur disponibles, la traite, renvoie une réponse, attend la demande suivante et s'arrête. La demande suivante est arrivée - encore une fois, nous avons choisi tout ce qui était là, l'avons calculé et attendons la suivante.

Pour garantir la latence minimale pour une telle tâche, il faut prendre le maximum de ressources qu'elle consomme et réserver le nombre requis de cœurs sur le minion (la machine qui exécutera la tâche). Alors la formule de réservation pour notre problème sera la suivante :

alloc: cpu = 4 (max)

et si nous avons une machine minion avec 16 cœurs, alors exactement quatre de ces tâches peuvent y être confiées. On note surtout que la consommation moyenne du processeur de telles tâches est souvent très faible - ce qui est évident, puisqu'une partie importante du temps la tâche attend une requête et ne fait rien.

Tâches de calcul. Leur motif sera légèrement différent :

One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

La consommation moyenne des ressources CPU pour de telles tâches est assez élevée. Souvent, nous souhaitons qu'une tâche de calcul soit terminée dans un certain temps, nous devons donc réserver le nombre minimum de processeurs dont elle a besoin pour que l'ensemble du calcul soit terminé dans un délai acceptable. Sa formule de réservation ressemblera à ceci :

alloc: cpu = [1,*)

"S'il vous plaît, placez-le sur un serviteur où il y a au moins un noyau libre, et autant qu'il y en aura, il dévorera tout."

Ici, l'efficacité d'utilisation est déjà bien meilleure que sur des tâches à court délai. Mais le gain sera bien plus important si vous combinez les deux types de tâches sur une seule machine serviteur et distribuez ses ressources en déplacement. Lorsqu'une tâche avec un court délai nécessite un processeur, celui-ci le reçoit immédiatement, et lorsque les ressources ne sont plus nécessaires, elles sont transférées à la tâche de calcul, c'est-à-dire quelque chose comme ceci :

One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

Mais comment le faire?

Tout d'abord, regardons prod et son allocation : cpu = 4. Nous devons réserver quatre cœurs. Dans Docker, cela peut être fait de deux manières :

  • Avec option --cpuset=1-4, c'est-à-dire allouer quatre cœurs spécifiques sur la machine à la tâche.
  • À utiliser --cpuquota=400_000 --cpuperiod=100_000, attribuez un quota de temps processeur, c'est-à-dire indiquez que toutes les 100 ms de temps réel, la tâche ne consomme pas plus de 400 ms de temps processeur. Les quatre mêmes noyaux sont obtenus.

Mais laquelle de ces méthodes est la plus adaptée ?

cpuset semble assez attrayant. La tâche dispose de quatre cœurs dédiés, ce qui signifie que les caches du processeur fonctionneront aussi efficacement que possible. Cela a aussi un inconvénient : il faudrait se charger de répartir les calculs sur les cœurs déchargés de la machine au lieu du système d'exploitation, et c'est une tâche plutôt non triviale, surtout si l'on essaie de placer des tâches batch sur un tel système. machine. Les tests ont montré que l'option avec quota est ici mieux adaptée : de cette façon, le système d'exploitation a plus de liberté dans le choix du cœur pour effectuer la tâche au moment présent et le temps processeur est réparti plus efficacement.

Voyons comment effectuer des réservations dans Docker en fonction du nombre minimum de cœurs. Le quota pour les tâches batch n'est plus applicable, car il n'est pas nécessaire de limiter le maximum, il suffit de garantir simplement le minimum. Et ici, l'option convient bien docker run --cpushares.

Nous avons convenu que si un lot nécessite une garantie pour au moins un noyau, alors nous indiquons --cpushares=1024, et s'il y a au moins deux cœurs, alors nous indiquons --cpushares=2048. Les partages CPU n'interfèrent en rien avec la répartition du temps processeur tant qu'il y en a suffisamment. Ainsi, si prod n'utilise pas actuellement ses quatre cœurs, rien ne limite les tâches par lots et elles peuvent utiliser du temps processeur supplémentaire. Mais dans une situation de pénurie de processeurs, si prod a consommé ses quatre cœurs et a atteint son quota, le temps processeur restant sera divisé proportionnellement aux parts de processeur, c'est-à-dire que dans une situation de trois cœurs libres, un sera attribués à une tâche avec 1024 2048 parts de processeur, et les deux autres seront attribués à une tâche avec XNUMX XNUMX parts de processeur.

Mais utiliser des quotas et des partages ne suffit pas. Nous devons nous assurer qu'une tâche avec un court délai est prioritaire sur une tâche par lots lors de l'allocation du temps processeur. Sans une telle priorisation, la tâche batch prendra tout le temps processeur au moment où elle est nécessaire à la production. Il n'y a pas d'options de priorisation des conteneurs dans l'exécution de Docker, mais les politiques du planificateur de processeur Linux sont utiles. Vous pouvez les lire en détail ici, et dans le cadre de cet article nous les passerons brièvement en revue :

  • SCHED_OTHER
    Par défaut, tous les processus utilisateur normaux sur une machine Linux reçoivent.
  • SCHED_BATCH
    Conçu pour les processus gourmands en ressources. Lors du placement d'une tâche sur un processeur, une pénalité d'activation est introduite : une telle tâche est moins susceptible de recevoir des ressources processeur si elle est actuellement utilisée par une tâche avec SCHED_OTHER
  • SCHED_IDLE
    Un processus en arrière-plan avec une très faible priorité, même inférieure à nice -19. Nous utilisons notre bibliothèque open source un-nio, afin de définir la politique nécessaire lors du démarrage du conteneur en appelant

one.nio.os.Proc.sched_setscheduler( pid, Proc.SCHED_IDLE )

Mais même si vous ne programmez pas en Java, la même chose peut être faite en utilisant la commande chrt :

chrt -i 0 $pid

Résumons tous nos niveaux d'isolement dans un seul tableau pour plus de clarté :

Classe d'isolation
Exemple d'allocation
Options d'exécution de Docker
sched_setscheduler chrt*

Prod
processeur = 4
--cpuquota=400000 --cpuperiod=100000
SCHED_OTHER

Lot
Processeur = [1, *)
--cpushares=1024
SCHED_BATCH

Idle
Processeur = [2, *)
--cpushares=2048
SCHED_IDLE

*Si vous effectuez chrt depuis l'intérieur d'un conteneur, vous aurez peut-être besoin de la fonctionnalité sys_nice, car par défaut, Docker supprime cette fonctionnalité lors du démarrage du conteneur.

Mais les tâches consomment non seulement le processeur, mais aussi le trafic, ce qui affecte encore plus la latence d'une tâche réseau qu'une allocation incorrecte des ressources du processeur. C’est pourquoi nous souhaitons naturellement obtenir exactement la même image du trafic. Autrement dit, lorsqu'une tâche prod envoie des paquets au réseau, nous limitons la vitesse maximale (formule allouer : lan=[*,500 Mbps) ), avec lequel prod peut faire cela. Et pour les lots, nous garantissons uniquement le débit minimum, mais ne limitons pas le maximum (formule allouer : lan=[10Mbps,*) ) Dans ce cas, le trafic de production doit être prioritaire sur les tâches par lots.
Ici, Docker n'a aucune primitive que nous pouvons utiliser. Mais cela nous vient en aide Contrôle du trafic Linux. Nous avons pu obtenir le résultat souhaité grâce à la discipline Courbe de service équitable hiérarchique. Avec son aide, nous distinguons deux classes de trafic : prod à haute priorité et batch/inactif à faible priorité. En conséquence, la configuration du trafic sortant est la suivante :

One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

ici 1:0 est le « qdisc racine » de la discipline hsfc ; 1:1 - classe enfant hsfc avec une limite de bande passante totale de 8 Gbit/s, en dessous de laquelle les classes enfants de tous les conteneurs sont placées ; 1:2 - la classe enfant hsfc est commune à toutes les tâches batch et inactives avec une limite « dynamique », décrite ci-dessous. Les classes enfants hsfc restantes sont des classes dédiées aux conteneurs de production en cours d'exécution avec des limites correspondant à leurs manifestes - 450 et 400 Mbit/s. Chaque classe hsfc se voit attribuer une file d'attente qdisc fq ou fq_codel, selon la version du noyau Linux, pour éviter la perte de paquets lors des rafales de trafic.

En règle générale, les disciplines tc servent à donner la priorité uniquement au trafic sortant. Mais nous voulons également donner la priorité au trafic entrant - après tout, certaines tâches par lots peuvent facilement sélectionner l'intégralité du canal entrant, recevant, par exemple, un grand lot de données d'entrée pour map&reduce. Pour cela nous utilisons le module ifb, qui crée une interface virtuelle ifbX pour chaque interface réseau et redirige le trafic entrant de l'interface vers le trafic sortant sur ifbX. De plus, pour ifbX, toutes les mêmes disciplines travaillent pour contrôler le trafic sortant, pour lequel la configuration hsfc sera très similaire :

One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

Au cours des expériences, nous avons découvert que hsfc affiche les meilleurs résultats lorsque la classe 1:2 de trafic batch/inactif non prioritaire est limitée sur les machines minions à une certaine voie libre au maximum. Sinon, le trafic non prioritaire a trop d'impact sur la latence des tâches de prod. miniond détermine la quantité actuelle de bande passante libre chaque seconde, mesurant la consommation moyenne de trafic de toutes les tâches de production d'un minion donné One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki et en le soustrayant de la bande passante de l'interface réseau One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki avec une petite marge, c'est-à-dire

One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

Les bandes sont définies indépendamment pour le trafic entrant et sortant. Et selon les nouvelles valeurs, miniond reconfigure la limite de classe non prioritaire 1:2.

Ainsi, nous avons implémenté les trois classes d'isolation : prod, batch et ralenti. Ces classes influencent grandement les caractéristiques de performance des tâches. Par conséquent, nous avons décidé de placer cet attribut en haut de la hiérarchie, de sorte que lorsque l'on regarde le nom de la file d'attente hiérarchique, il soit immédiatement clair à quoi nous avons affaire :

One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

Tous nos amis web и la musique les façades sont ensuite placées dans la hiérarchie sous prod. Par exemple, sous batch, plaçons le service catalogue de musique, qui compile périodiquement un catalogue de morceaux à partir d'un ensemble de fichiers mp3 téléchargés sur Odnoklassniki. Un exemple de service en veille serait transformateur de musique, qui normalise le niveau de volume de la musique.

Une fois les lignes supplémentaires supprimées, nous pouvons écrire nos noms de service de manière plus plate en ajoutant la classe d'isolation des tâches à la fin du nom complet du service : web.front.prod, catalogue.musique.batch, transformer.music.idle.

Et maintenant, en regardant le nom du service, on comprend non seulement quelle fonction il remplit, mais aussi sa classe d'isolation, c'est-à-dire sa criticité, etc.

Tout est génial, mais il y a une vérité amère. Il est impossible d’isoler complètement les tâches exécutées sur une seule machine.

Ce que nous avons réussi à réaliser : si le lot consomme intensément seulement Ressources CPU, le planificateur CPU Linux intégré fait très bien son travail et il n'y a pratiquement aucun impact sur la tâche de production. Mais si cette tâche par lots commence à travailler activement avec la mémoire, alors une influence mutuelle apparaît déjà. Cela se produit parce que la tâche de production est « effacée » des caches mémoire du processeur. En conséquence, les échecs de cache augmentent et le processeur traite la tâche de production plus lentement. Une telle tâche par lots peut augmenter la latence de notre conteneur de production typique de 10 %.

L'isolation du trafic est encore plus difficile en raison du fait que les cartes réseau modernes disposent d'une file d'attente interne de paquets. Si le paquet de la tâche par lots arrive en premier, il sera alors le premier à être transmis via le câble et rien ne peut être fait à ce sujet.

De plus, nous n'avons jusqu'à présent réussi à résoudre que le problème de la priorisation du trafic TCP : l'approche hsfc ne fonctionne pas pour UDP. Et même dans le cas du trafic TCP, si la tâche batch génère beaucoup de trafic, cela donne aussi une augmentation d'environ 10% du délai de la tâche prod.

tolérance aux pannes

L'un des objectifs du développement d'un cloud unique était d'améliorer la tolérance aux pannes d'Odnoklassniki. Par conséquent, je voudrais ensuite examiner plus en détail les scénarios possibles de pannes et d'accidents. Commençons par un scénario simple : une panne de conteneur.

Le conteneur lui-même peut échouer de plusieurs manières. Il peut s'agir d'une sorte d'expérience, d'un bug ou d'une erreur dans le manifeste, à cause duquel la tâche de production commence à consommer plus de ressources que ce qui est indiqué dans le manifeste. Nous avons eu un cas : un développeur a implémenté un algorithme complexe, l'a retravaillé plusieurs fois, a trop réfléchi et est devenu tellement confus que finalement le problème a été bouclé d'une manière très non triviale. Et comme la tâche de production a une priorité plus élevée que toutes les autres sur les mêmes serviteurs, elle a commencé à consommer toutes les ressources processeur disponibles. Dans cette situation, l’isolement, ou plutôt le quota de temps CPU, a sauvé la mise. Si un quota est attribué à une tâche, elle ne consommera pas davantage. Par conséquent, les tâches par lots et autres tâches de production exécutées sur la même machine n’ont rien remarqué.

Le deuxième problème possible est la chute du conteneur. Et ici, les politiques de redémarrage nous sauvent, tout le monde les connaît, Docker lui-même fait un excellent travail. Presque toutes les tâches de production ont une politique de redémarrage permanent. Parfois, nous utilisons on_failure pour les tâches par lots ou pour le débogage des conteneurs de production.

Que pouvez-vous faire si un serviteur entier n'est pas disponible ?

Évidemment, exécutez le conteneur sur une autre machine. La partie intéressante ici est ce qui arrive à la ou aux adresses IP attribuées au conteneur.

Nous pouvons attribuer aux conteneurs les mêmes adresses IP que les machines minions sur lesquelles ces conteneurs s'exécutent. Ensuite, lorsque le conteneur est lancé sur une autre machine, son adresse IP change et tous les clients doivent comprendre que le conteneur a été déplacé et doivent désormais accéder à une adresse différente, ce qui nécessite un service de découverte de services distinct.

La découverte de services est pratique. Il existe de nombreuses solutions sur le marché avec différents degrés de tolérance aux pannes pour organiser un registre de services. Souvent, ces solutions implémentent une logique d'équilibrage de charge, stockent une configuration supplémentaire sous forme de stockage KV, etc.
Cependant, nous aimerions éviter de devoir mettre en place un registre séparé, car cela impliquerait l'introduction d'un système critique utilisé par tous les services en production. Cela signifie qu'il s'agit d'un point de défaillance potentiel et que vous devez choisir ou développer une solution très tolérante aux pannes, ce qui est évidemment très difficile, long et coûteux.

Et un autre gros inconvénient : pour que notre ancienne infrastructure fonctionne avec la nouvelle, nous devrions réécrire absolument toutes les tâches pour utiliser une sorte de système de découverte de services. Il y a BEAUCOUP de travail, et dans certains endroits, c'est presque impossible lorsqu'il s'agit de périphériques de bas niveau qui fonctionnent au niveau du noyau du système d'exploitation ou directement avec le matériel. Implémentation de cette fonctionnalité à l'aide de modèles de solutions établis, tels que side-car cela signifierait à certains endroits une charge supplémentaire, à d'autres - une complication de fonctionnement et des scénarios de défaillance supplémentaires. Nous ne voulions pas compliquer les choses, nous avons donc décidé de rendre facultative l’utilisation de Service Discovery.

Dans un cloud unique, l'IP suit le conteneur, c'est-à-dire que chaque instance de tâche a sa propre adresse IP. Cette adresse est « statique » : elle est attribuée à chaque instance lors de la première envoi du service vers le cloud. Si un service a eu un nombre différent d'instances au cours de sa vie, alors à la fin, il se verra attribuer autant d'adresses IP qu'il y a d'instances maximales.

Par la suite, ces adresses ne changent pas : elles sont attribuées une seule fois et continuent d'exister pendant toute la durée de vie du service en production. Les adresses IP suivent les conteneurs sur le réseau. Si le conteneur est transféré à un autre serviteur, l'adresse le suivra.

Ainsi, le mappage d’un nom de service à sa liste d’adresses IP change très rarement. Si vous regardez à nouveau les noms des instances de service que nous avons mentionnés au début de l'article (1.ok-web.group1.web.front.prod, 2.ok-web.group1.web.front.prod, …), on remarquera qu’ils ressemblent aux FQDN utilisés dans les DNS. C'est vrai, pour mapper les noms des instances de service à leurs adresses IP, nous utilisons le protocole DNS. De plus, ce DNS renvoie toutes les adresses IP réservées de tous les conteneurs - à la fois en cours d'exécution et arrêtés (disons que trois répliques sont utilisées et que nous y avons cinq adresses réservées - toutes les cinq seront renvoyées). Les clients, ayant reçu ces informations, tenteront d'établir une connexion avec les cinq répliques - et ainsi déterminer celles qui fonctionnent. Cette option de détermination de la disponibilité est beaucoup plus fiable : elle n'implique ni DNS ni Service Discovery, ce qui signifie qu'il n'y a pas de problèmes difficiles à résoudre pour garantir la pertinence des informations et la tolérance aux pannes de ces systèmes. De plus, dans les services critiques dont dépend le fonctionnement de l'ensemble du portail, nous ne pouvons pas du tout utiliser DNS, mais simplement saisir des adresses IP dans la configuration.

Implémenter un tel transfert IP derrière des conteneurs peut être non trivial - et nous verrons comment cela fonctionne avec l'exemple suivant :

One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

Disons que le maître du one-cloud donne l'ordre au minion M1 de s'exécuter 1.ok-web.group1.web.front.prod avec l'adresse 1.1.1.1. Fonctionne sur un serviteur OISEAU, qui annonce cette adresse à des serveurs spéciaux réflecteur d'itinéraire. Ces derniers ont une session BGP avec le matériel réseau, dans laquelle est traduite la route d'adresse 1.1.1.1 sur M1. M1 achemine les paquets à l’intérieur du conteneur à l’aide de Linux. Il existe trois serveurs de réflecteur de route, car il s'agit d'une partie très critique de l'infrastructure one-cloud - sans eux, le réseau dans one-cloud ne fonctionnera pas. Nous les plaçons dans des racks différents, si possible situés dans des pièces différentes du centre de données, afin de réduire le risque de panne des trois en même temps.

Supposons maintenant que la connexion entre le maître one-cloud et le minion M1 soit perdue. Le maître du one-cloud agira désormais en partant du principe que M1 est complètement tombé en panne. C'est-à-dire qu'il donnera l'ordre au serviteur M2 de lancer web.group1.web.front.prod avec la même adresse 1.1.1.1. Nous avons désormais deux routes conflictuelles sur le réseau pour la 1.1.1.1 : sur M1 et sur M2. Afin de résoudre de tels conflits, nous utilisons le Multi Exit Discriminator, spécifié dans l'annonce BGP. Il s'agit d'un nombre qui indique le poids de l'itinéraire annoncé. Parmi les itinéraires en conflit, l'itinéraire avec la valeur MED la plus faible sera sélectionné. Le maître one-cloud prend en charge MED en tant que partie intégrante des adresses IP des conteneurs. Pour la première fois, l'adresse est écrite avec un MED suffisamment grand = 1 000 000. Dans la situation d'un tel transfert de conteneur d'urgence, le maître réduit le MED, et M2 recevra déjà la commande d'annoncer l'adresse 1.1.1.1 avec MED = 999 999. L'instance fonctionnant sur M1 restera dans ce cas, il n'y a pas de connexion, et son sort ultérieur ne nous intéresse guère jusqu'à ce que la connexion avec le maître soit rétablie, quand il sera arrêté comme une vieille prise.

Accidents

Tous les systèmes de gestion de centres de données gèrent toujours les pannes mineures de manière acceptable. Le débordement des conteneurs est la norme presque partout.

Voyons comment nous gérons une urgence, telle qu'une panne de courant dans une ou plusieurs pièces d'un centre de données.

Que signifie un accident pour un système de gestion de centre de données ? Tout d’abord, il s’agit d’une panne ponctuelle massive de nombreuses machines et le système de contrôle doit migrer de nombreux conteneurs en même temps. Mais si la catastrophe est à très grande échelle, il peut arriver que toutes les tâches ne puissent pas être réaffectées à d'autres serviteurs, car la capacité en ressources du centre de données tombe en dessous de 100 % de la charge.

Les accidents s'accompagnent souvent d'une défaillance de la couche de contrôle. Cela peut se produire en raison d'une défaillance de son équipement, mais le plus souvent en raison du fait que les accidents ne sont pas testés et que la couche de contrôle elle-même tombe en raison de la charge accrue.

Que pouvez-vous faire contre tout cela ?

Les migrations de masse signifient qu'un grand nombre d'activités, de migrations et de déploiements ont lieu dans l'infrastructure. Chacune des migrations peut prendre un certain temps, nécessaire pour livrer et décompresser les images de conteneur aux minions, lancer et initialiser les conteneurs, etc. Par conséquent, il est souhaitable que les tâches les plus importantes soient lancées avant les moins importantes.

Examinons à nouveau la hiérarchie des services que nous connaissons et essayons de décider quelles tâches nous voulons exécuter en premier.

One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

Bien entendu, ce sont les processus qui participent directement au traitement des demandes des utilisateurs, c'est-à-dire la prod. Nous indiquons cela avec priorité de placement — un numéro qui peut être attribué à la file d'attente. Si une file d'attente a une priorité plus élevée, ses services sont placés en premier.

En production, nous attribuons des priorités plus élevées, 0 ; sur lot - un peu plus bas, 100 ; au repos - encore plus bas, 200. Les priorités sont appliquées hiérarchiquement. Toutes les tâches inférieures dans la hiérarchie auront une priorité correspondante. Si nous voulons que les caches à l'intérieur de prod soient lancés avant les frontends, alors nous attribuons des priorités à cache = 0 et aux sous-files d'attente front = 1. Si, par exemple, nous voulons que le portail principal soit lancé depuis les frontaux en premier, et le front musical uniquement alors, nous pouvons alors attribuer une priorité inférieure à ce dernier - 10.

Le prochain problème est le manque de ressources. Ainsi, une grande quantité d'équipements, des halls entiers du centre de données sont tombés en panne, et nous avons relancé tellement de services qu'il n'y a désormais plus assez de ressources pour tout le monde. Vous devez décider quelles tâches sacrifier afin de maintenir le fonctionnement des principaux services critiques.

One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

Contrairement à la priorité de placement, on ne peut pas sacrifier indistinctement toutes les tâches batch ; certaines d'entre elles sont importantes pour le fonctionnement du portail. Nous avons donc souligné séparément priorité de préemption Tâches. Lorsqu'elle est placée, une tâche de priorité plus élevée peut préempter, c'est-à-dire arrêter, une tâche de priorité inférieure s'il n'y a plus de serviteurs libres. Dans ce cas, une tâche avec une faible priorité restera probablement non placée, c'est-à-dire qu'il n'y aura plus de serviteur approprié avec suffisamment de ressources libres pour cette tâche.

Dans notre hiérarchie, il est très simple de spécifier une priorité de préemption telle que les tâches de production et par lots préemptent ou arrêtent les tâches inactives, mais pas les unes les autres, en spécifiant une priorité d'inactivité égale à 200. Tout comme dans le cas de la priorité de placement, nous peut utiliser notre hiérarchie afin de décrire des règles plus complexes. Par exemple, indiquons que nous sacrifions la fonction musique si nous n'avons pas suffisamment de ressources pour le portail Web principal, en définissant la priorité des nœuds correspondants plus bas : 10.

Accidents entiers à DC

Pourquoi l’ensemble du centre de données pourrait-il tomber en panne ? Élément. C'était un bon article l'ouragan a affecté le travail du centre de données. Les éléments peuvent être considérés comme des sans-abri qui ont autrefois brûlé l'optique du collecteur et le centre de données a complètement perdu le contact avec d'autres sites. La cause de la panne peut également être un facteur humain : l'opérateur émettra une commande telle que l'ensemble du centre de données tombera. Cela pourrait arriver à cause d'un gros bug. En général, l’effondrement des centres de données n’est pas rare. Cela nous arrive une fois tous les quelques mois.

Et c’est ce que nous faisons pour empêcher quiconque de tweeter #alive.

La première stratégie est l’isolement. Chaque instance d'un cloud est isolée et peut gérer des machines dans un seul centre de données. Autrement dit, la perte d'un cloud en raison de bugs ou de commandes incorrectes de l'opérateur équivaut à la perte d'un seul centre de données. Nous y sommes prêts : nous avons une politique de redondance dans laquelle des répliques de l'application et des données sont situées dans tous les centres de données. Nous utilisons des bases de données tolérantes aux pannes et testons périodiquement les pannes.
Puisque nous disposons aujourd’hui de quatre centres de données, cela signifie quatre instances distinctes et complètement isolées d’un seul cloud.

Cette approche protège non seulement contre les pannes physiques, mais peut également protéger contre les erreurs de l’opérateur.

Que peut-on faire d’autre avec le facteur humain ? Lorsqu'un opérateur donne au cloud une commande étrange ou potentiellement dangereuse, il peut soudainement lui être demandé de résoudre un petit problème pour voir s'il a bien réfléchi. Par exemple, s'il s'agit d'une sorte d'arrêt massif de nombreuses répliques ou simplement d'une commande étrange - réduire le nombre de répliques ou modifier le nom de l'image, et pas seulement le numéro de version dans le nouveau manifeste.

One-cloud - système d'exploitation au niveau du centre de données à Odnoklassniki

Les résultats de

Particularités du one-cloud :

  • Schéma de dénomination hiérarchique et visuel pour les services et les conteneurs, qui permet de savoir très rapidement quelle est la tâche, à quoi elle se rapporte et comment elle fonctionne et qui en est responsable.
  • Nous appliquons notre technique de combinaison de production et de lotstâches sur les serviteurs pour améliorer l’efficacité du partage de machines. Au lieu de cpuset, nous utilisons des quotas de CPU, des partages, des politiques de planification de CPU et Linux QoS.
  • Il n'a pas été possible d'isoler complètement les conteneurs fonctionnant sur la même machine, mais leur influence mutuelle reste inférieure à 20 %.
  • L'organisation des services dans une hiérarchie facilite la reprise automatique après sinistre à l'aide de priorités de placement et de préemption.

FAQ

Pourquoi n’avons-nous pas adopté une solution toute faite ?

  • Différentes classes d'isolation de tâches nécessitent une logique différente lorsqu'elles sont placées sur des serviteurs. Si les tâches de production peuvent être placées en réservant simplement des ressources, alors les tâches par lots et inactives doivent être placées, en suivant l'utilisation réelle des ressources sur les machines minions.
  • La nécessité de prendre en compte les ressources consommées par les tâches, telles que :
    • bande passante du réseau ;
    • types et « broches » de disques.
  • La nécessité d'indiquer les priorités des services lors des interventions d'urgence, les droits et les quotas de commandes de ressources, qui est résolue à l'aide de files d'attente hiérarchiques dans un seul cloud.
  • La nécessité de donner un nom humain aux conteneurs pour réduire le temps de réponse aux accidents et incidents
  • L'impossibilité d'une mise en œuvre ponctuelle et généralisée de Service Discovery ; la nécessité de coexister pendant longtemps avec des tâches hébergées sur des hôtes matériels - ce qui est résolu par des adresses IP « statiques » suivant les conteneurs et, par conséquent, la nécessité d'une intégration unique avec une grande infrastructure réseau.

Toutes ces fonctions nécessiteraient des modifications significatives des solutions existantes pour nous convenir et, après avoir évalué la quantité de travail, nous avons réalisé que nous pouvions développer notre propre solution avec à peu près les mêmes coûts de main d'œuvre. Mais votre solution sera beaucoup plus facile à exploiter et à développer - elle ne contient pas d'abstractions inutiles prenant en charge des fonctionnalités dont nous n'avons pas besoin.

A ceux qui liront les dernières lignes, merci pour votre patience et votre attention !

Source: habr.com

Ajouter un commentaire