
Hallo zusammen! Mein Name ist Oleg Anastassiev, ich arbeite bei Odnoklassniki im Plattform-Team. Neben mir stehen viele Server zur Verfügung. Wir haben vier Rechenzentren mit etwa 500 Racks und mehr als 8000 Servern. Irgendwann wurde uns klar, dass die Implementierung eines neuen Managementsystems es uns ermöglichen würde, die Hardware effizienter auszulasten, den Zugang zu verwalten, die (Re)Verteilung von Rechenressourcen zu automatisieren, neue Dienste schneller zu starten und schneller auf große Störungen zu reagieren.
Was haben wir daraus gemacht?
Neben mir und der ganzen Hardware gibt es noch viele Menschen, die mit dieser Hardware arbeiten: Ingenieure vor Ort in den Rechenzentren, Netzwerkspezialisten, die Netzwerkausrüstung konfigurieren, Administratoren oder SREs, die für die Ausfallsicherheit der Infrastruktur sorgen, sowie Teams von Entwicklern, die jeweils für bestimmte Funktionen des Portals verantwortlich sind. Die von ihnen entwickelte Software funktioniert etwa so:

Benutzeranfragen gelangen wie auf die Fronten des Hauptportals , sowie auf andere, wie beispielsweise die API-Endpoints für Musik. Diese greifen auf Anwendungsserver zu, um Geschäftslogik zu verarbeiten, wobei bei der Bearbeitung einer Anfrage die erforderlichen spezialisierten Microservices aufgerufen werden – wie one-graph (Graf der sozialen Beziehungen), user-cache (Cache der Benutzerprofile) und so weiter.
Jeder dieser Dienste ist auf vielen Maschinen bereitgestellt, und jede hat verantwortliche Entwickler, die für die Funktionalität der Module, deren Betrieb und technologische Weiterentwicklung zuständig sind. Alle diese Dienste laufen auf physischen Servern, und bis vor kurzem haben wir genau eine Aufgabe auf einen Server ausgeführt, d. h. der Server war auf eine bestimmte Aufgabe spezialisiert.
Warum ist das so? Dieser Ansatz hatte mehrere Vorteile:
- Die massive Verwaltung wird erleichtert. Angenommen, die Aufgabe erfordert bestimmte Bibliotheken oder spezifische Einstellungen. In diesem Fall wird der Server genau einer bestimmten Gruppe zugeordnet, eine richtlinienbasierte Konfiguration für diese Gruppe (oder eine bereits bestehende) wird erstellt, und diese Konfiguration wird zentral und automatisch auf alle Server dieser Gruppe verteilt.
- Die Diagnose wird vereinfacht. Angenommen, Sie beobachten eine erhöhte CPU-Auslastung und stellen fest, dass diese Last nur durch die Aufgabe verursacht werden kann, die auf dieser Hardware läuft. Die Suche nach dem Schuldigen endet sehr schnell.
- Die Monitoring. Wenn mit dem Server etwas nicht stimmt, zeigt das Monitoring dies an, und Sie wissen genau, wer schuld ist.
Einem Dienst, der aus mehreren Replikaten besteht, werden mehrere Server zugewiesen – jeweils einer pro Replikat. Die Bereitstellung von Rechenressourcen für den Dienst gestaltet sich somit sehr einfach: So viele Server der Dienst hat, so viele Ressourcen kann er maximal beanspruchen. „Einfach“ heißt hier nicht, dass es leicht zu bedienen ist, sondern dass die Ressourcen manuell verteilt werden.
Dieser Ansatz ermöglichte es uns außerdem, spezialisierte Hardwarekonfigurationen Für Aufgaben, die auf diesem Server durchgeführt werden, verwenden wir einen 4U-Server mit einem Gehäuse für 38 Laufwerke, wenn diese Aufgaben große Datenmengen speichern. Bei rein rechenintensiven Aufgaben können wir einen günstigeren 1U-Server anschaffen. Dies ist in Bezug auf die Rechenressourcen effizient. Dieser Ansatz ermöglicht es uns auch, bei einer Last, die mit einem sozialen Netzwerk, das uns freundlich gesinnt ist, vergleichbar ist, viermal weniger Maschinen zu nutzen.
Eine solche Effizienz der Nutzung von Rechenressourcen sollte auch wirtschaftliche Effizienz gewährleisten, wenn man davon ausgeht, dass Server die teuersten Komponenten sind. Lange Zeit war die Hardware am teuersten, und wir haben viel Aufwand betrieben, um die Kosten der Hardware zu senken, indem wir Algorithmen zur Gewährleistung der Fehlertoleranz entwickelt haben, um die Anforderungen an die Zuverlässigkeit der Ausrüstung zu reduzieren. Heute sind wir an einem Punkt angekommen, an dem der Preis für einen Server nicht mehr entscheidend ist. Wenn man die neuesten exotischen Modelle außen vor lässt, spielt die spezifische Konfiguration der Server im Rack keine Rolle. Jetzt stehen wir vor einem neuen Problem — den Kosten des Platzes, den ein Server im Rechenzentrum einnimmt, also dem Platz im Rack.
Als wir uns dessen bewusst wurden, haben wir beschlossen, die Effizienz der Racknutzung zu berechnen.
Wir haben den Preis des leistungsstärksten Servers aus einer wirtschaftlich sinnvollen Perspektive genommen, berechnet, wie viele solcher Server wir in die Racks unterbringen können, welche Aufgaben wir basierend auf dem alten Modell "ein Server = eine Aufgabe" darauf ausführen könnten und wie gut diese Aufgaben die Hardware auslasten würden. Die Berechnungen waren erschütternd. Es stellte sich heraus, dass die Effizienz der Racknutzung bei uns nur etwa 11 % beträgt. Die Schlussfolgerung ist offensichtlich: Die Effizienz der Rechenzentren muss verbessert werden. Natürlich scheint die Lösung klar zu sein: Man sollte auf einem Server mehrere Aufgaben gleichzeitig ausführen. Doch hier beginnen die Herausforderungen.
Die Massenkonfiguration wird erheblich komplizierter – jetzt ist es unmöglich, einem Server eine bestimmte Gruppe zuzuweisen. Schließlich können auf einem Server mehrere Aufgaben verschiedener Teams ausgeführt werden. Zudem kann die Konfiguration für unterschiedliche Anwendungen konfliktreich sein. Auch die Diagnose wird schwieriger: Wenn Sie einen Anstieg der CPU- oder Festplattenauslastung auf einem Server feststellen, wissen Sie nicht, welche Aufgabe Probleme verursacht.
Das Wichtigste ist jedoch, dass es keine Isolation zwischen den auf einer Maschine laufenden Aufgaben gibt. Zum Beispiel zeigt das Diagramm die durchschnittliche Antwortzeit einer Serveraufgabe vor und nachdem eine andere, nicht miteinander verbundene Berechnungsanwendung auf demselben Server gestartet wurde – die Antwortzeit der Hauptaufgabe hat sich erheblich erhöht.

Es ist offensichtlich, dass Aufgaben entweder in Containern oder in virtuellen Maschinen gestartet werden müssen. Da praktisch alle unsere Aufgaben unter dem Betriebssystem Linux oder dafür angepasst laufen, müssen wir nicht viele verschiedene Betriebssysteme unterstützen. Daher ist Virtualisierung nicht erforderlich; aufgrund der zusätzlichen Overheads ist sie weniger effizient als Containerisierung.
Docker-Container sind eine hervorragende Lösung, um Aufgaben direkt auf Servern auszuführen: Die Dateisystem-Images lösen Konflikte in der Konfiguration effektiv. Da Images aus mehreren Schichten bestehen können, reduzieren wir erheblich den Datenumfang, der für die Bereitstellung auf der Infrastruktur erforderlich ist, indem wir gemeinsame Komponenten in separate Basis-Schichten auslagern. Dadurch werden die Basis- und voluminösen Schichten schnell in der gesamten Infrastruktur zwischengespeichert, und für die Bereitstellung verschiedener Anwendungsarten und Versionen sind nur noch kleine Schichten zu übertragen.
Außerdem bieten uns das bereitgestellte Registry und das Tagging von Images in Docker die notwendigen Werkzeuge für das Versionieren und die Bereitstellung von Code in die Produktion.
Docker bietet, wie jede ähnliche Technologie, eine gewisse Containerisolation direkt ab Werk. Zum Beispiel gibt es eine Speicherkapazitätsbegrenzung – jeder Container erhält ein Limit für den Speicherverbrauch auf der Maschine, das er nicht überschreiten kann. Auch die CPU-Nutzung der Container kann isoliert werden. Für uns war jedoch die standardmäßige Isolation nicht ausreichend. Aber dazu später mehr.
Das unmittelbare Starten von Containern auf Servern ist nur ein Teil der Herausforderung. Ein anderer Aspekt betrifft die Platzierung der Container auf den Servern. Wir müssen verstehen, welcher Container auf welchen Server gesetzt werden kann. Das ist keine einfache Aufgabe, da die Container so nah wie möglich auf den Servern platziert werden müssen, ohne die Geschwindigkeit ihrer Arbeit zu beeinträchtigen. Eine solche Platzierung kann auch in Bezug auf die Ausfallsicherheit kompliziert sein. Häufig möchten wir Replikate desselben Dienstes in verschiedenen Racks oder sogar in unterschiedlichen Hallen des Rechenzentrums unterbringen, um bei einem Ausfall eines Racks oder einer Halle nicht alle Replikate des Services zu verlieren.
Container manuell zu verteilen, ist keine Option, wenn man 8.000 Server und 8.000 bis 16.000 Container hat.
Darüber hinaus wollten wir den Entwicklern mehr Eigenverantwortung bei der Ressourcenzuteilung geben, sodass sie ihre Dienste selbstständig in der Produktion bereitstellen können, ohne die Hilfe eines Administrators. Dabei war es uns wichtig, die Kontrolle zu behalten, um sicherzustellen, dass kein unwesentlicher Dienst alle Ressourcen unserer Rechenzentren verbraucht.
Offensichtlich benötigen wir eine Verwaltungs-EBene, die dies automatisch übernimmt.
Hier kommen wir zu einem einfachen und klaren Bild, das alle Architekten lieben: drei Kästchen.

one-cloud masters – ein hochverfügbarer Cluster, der für die Orchestrierung der Cloud verantwortlich ist. Der Entwickler sendet ein Manifest an den Master, das alle notwendigen Informationen für die Bereitstellung des Dienstes enthält. Der Master gibt basierend darauf Befehle an die ausgewählten Minions (Rechner, die für das Starten von Containern vorgesehen sind). Auf den Minions befindet sich unser Agent, der den Befehl erhält, eigene Befehle an Docker ausgibt, und Docker konfiguriert den Linux-Kernel, um den entsprechenden Container zu starten. Neben der Ausführung von Befehlen informiert der Agent den Master kontinuierlich über Änderungen im Zustand sowohl des Minion-Rechners als auch der auf ihm betriebenen Container.
Ressourcenzuteilung
Jetzt betrachten wir die anspruchsvollere Aufgabe der Ressourcenverteilung für mehrere Minionen.
Rechenressourcen in one-cloud sind:
- Die Rechenleistung der CPU, die von einer bestimmten Aufgabe benötigt wird.
- Der verfügbare Speicher für die Aufgabe.
- Netzwerkverkehr. Jeder der Minionen hat eine spezifische Netzwerkschnittstelle mit begrenzter Bandbreite, daher können Aufgaben nicht ohne Berücksichtigung des dabei übertragenen Datenvolumens verteilt werden.
- Festplatten. Neben dem, naheliegenderweise, benötigten Platz für die Daten der Aufgabe, weisen wir auch den Festplattentyp zu: HDD oder SSD. Festplatten können eine endliche Anzahl von Anfragen pro Sekunde bedienen — IOPS. Daher reservieren wir für Aufgaben, die mehr IOPS erzeugen als ein einzelnes Laufwerk bewältigen kann, auch 'Spindeln' — d. h. Speichergeräte, die ausschließlich für die Aufgabe reserviert werden müssen.
Für einen bestimmten Dienst, zum Beispiel für user-cache, könnten wir die benötigten Ressourcen folgendermaßen angeben: 400 CPU-Kerne, 2,5 TB Speicher, 50 Gbit/s Verkehr in beide Richtungen, 6 TB Speicher auf HDD, verteilt auf 100 Spindeln. Oder in einer für uns vertrauteren Form so:
alloc:
cpu: 400
mem: 2500
lan_in: 50g
lan_out: 50g
hdd:100x6TDie Ressourcen des user-cache-Dienstes nutzen nur einen Teil der verfügbaren Ressourcen in der Produktionsinfrastruktur. Daher möchten wir sicherstellen, dass der user-cache, egal ob durch menschliches Versagen oder nicht, nicht mehr Ressourcen verbraucht als ihm zugewiesen sind. Das heißt, wir müssen die Ressourcen begrenzen. Aber woran könnten wir das Kontingent festmachen?
Lassen Sie uns zu unserem stark vereinfachten Schema der Komponenteninteraktion zurückkehren und es mit mehr Details neu zeichnen — so:

Was ins Auge fällt:
- Das Web-Frontend und die Musik nutzen isolierte Cluster desselben Anwendungsservers.
- Es lassen sich logische Schichten identifizieren, zu denen diese Cluster gehören: Frontends, Caches, Datenspeicher- und Verwaltungs-Schichten.
- Der Frontend ist heterogen, es handelt sich um verschiedene funktionale Teilsysteme.
- Caches können ebenfalls über die Teilsysteme verteilt werden, deren Daten sie zwischenspeichern.
Lassen Sie uns das Bild noch einmal neu zeichnen:

Wow! Wir sehen die Hierarchie! Das bedeutet, dass wir Ressourcen effizienter zuteilen können: einen verantwortlichen Entwickler für einen Knoten dieser Hierarchie zuweisen, der einer funktionalen Teilsystem entspricht (wie „music“ im Bild), und auch die Quote an dasselbe Hierarchie-Niveau binden. Eine solche Hierarchie ermöglicht es uns auch, die Dienste flexibler zu organisieren, um die Verwaltung zu erleichtern. Zum Beispiel teilen wir alle Web-Server, da es sich um eine sehr große Gruppe handelt, in mehrere kleinere Gruppen auf, die im Bild als group1 und group2 dargestellt sind.
Indem wir überflüssige Linien entfernen, können wir jeden Knoten unseres Bildes in einer flacheren Form darstellen: group1.web.front, api.music.front, user-cache.cache.
So kommen wir zum Konzept der "hierarchischen Warteschlange". Sie hat einen Namen, wie "group1.web.front". Eine Ressourcenquote und Benutzerrechte werden ihr zugewiesen. Einer Person aus dem DevOps-Team geben wir die Berechtigung, den Dienst in die Warteschlange zu senden, und diese Person kann etwas in der Warteschlange starten, während jemand aus dem OpsDev-Team Administratorrechte erhält und somit die Warteschlange verwalten, Personen zuweisen und ihnen Rechte erteilen kann usw. Die Dienste, die in dieser Warteschlange gestartet werden, werden im Rahmen der Warteschlangenquote ausgeführt. Wenn die Rechenressourcen der Warteschlange nicht ausreichen, um alle Dienste gleichzeitig auszuführen, werden sie nacheinander ausgeführt, wodurch tatsächlich eine Warteschlange entsteht.
Betrachten wir die Dienste näher. Ein Dienst hat einen vollständigen Namen, der immer den Namen der Warteschlange enthält. Somit wird der Dienst für das Web-Frontend den Namen ok-web.group1.web.fronttragen. Der Dienst des Anwendungsservers, auf den er zugreift, wird den Namen ok-app.group1.web.front. Jeder Service hat ein Manifest, das alle notwendigen Informationen für die Platzierung auf bestimmten Maschinen angibt: wie viele Ressourcen diese Aufgabe benötigt, welche Konfiguration erforderlich ist, wie viele Replikate vorhanden sein müssen und Eigenschaften zur Fehlerbehandlung dieses Services. Nach der Bereitstellung des Services auf den Maschinen entstehen seine Instanzen. Diese werden ebenfalls eindeutig benannt – durch die Instanznummer und den Servicenamen: 1.ok-web.group1.web.front, 2.ok-web.group1.web.front, …
Das ist sehr praktisch: Wenn man nur den Namen des laufenden Containers betrachtet, kann man sofort viele Informationen ableiten.
Kommen wir nun näher dazu, was diese Instanzen tatsächlich tun: die Aufgaben.
Isolation Klassen der Aufgaben
Alle Aufgaben in OK (und wahrscheinlich überall) lassen sich in Gruppen einteilen:
- Aufgaben mit kurzer Latenz — prod. Für solche Aufgaben und Services ist die Antwortzeit (latency) von großer Bedeutung, wie schnell jede Anfrage vom System verarbeitet wird. Beispiele für Aufgaben: Webfronts, Caches, Anwendungsserver, OLTP-Speicher usw.
- Rechenintensive Aufgaben — batch. Hier ist die Verarbeitungsgeschwindigkeit jeder einzelnen Anfrage weniger wichtig. Entscheidend ist, wie viele Berechnungen diese Aufgabe in einem bestimmten (großen) Zeitrahmen ausführen kann (Durchsatz). Solche Aufgaben betreffen MapReduce, Hadoop, maschinelles Lernen und Statistik.
- Hintergrundaufgaben — idle. Für solche Aufgaben sind weder Latenz noch Durchsatz von großer Bedeutung. Dazu gehören verschiedene Tests, Migrationen, Neuberechnungen und Konvertierungen von Daten aus einem Format in ein anderes. Sie sind einerseits ähnlich zu Berechnungen, andererseits spielt es keine Rolle, wie schnell sie abgeschlossen werden.
Lassen Sie uns betrachten, wie solche Aufgaben Ressourcen verbrauchen, beispielsweise von der zentralen Verarbeitungseinheit.
Aufgaben mit kurzer Latenz. Das Verbrauchsmuster der CPU für eine solche Aufgabe sieht folgendermaßen aus:

Ein Anfrage wird vom Benutzer verarbeitet, die Aufgabe beginnt, alle verfügbaren CPU-Kerne zu nutzen, verarbeitet, gibt eine Antwort zurück, wartet auf die nächste Anfrage und bleibt inaktiv. Die nächste Anfrage kommt — wieder werden alle Ressourcen genutzt, berechnet und wir warten auf die nächste.
Um minimale Latenz für diese Aufgabe zu garantieren, müssen wir die maximalen Ressourcen, die sie verbraucht, ermitteln und die erforderliche Anzahl von Kernen auf dem Minion (der Maschine, die die Aufgabe ausführt) reservieren. Die Reservierungsformel für unsere Aufgabe lautet somit:
alloc: cpu = 4 (max)Wenn wir also einen Minion mit 16 Kernen haben, können wir genau vier solcher Aufgaben darauf platzieren. Besonders bemerkenswert ist, dass der durchschnittliche CPU-Verbrauch solcher Aufgaben oft sehr niedrig ist – was offensichtlich ist, da die Aufgabe einen Großteil der Zeit in Warteschleife auf eine Anfrage verbringt und nichts tut.
Berechnungsaufgaben. Ihr Muster wird etwas anders sein:

Der durchschnittliche Ressourcenverbrauch der CPU für solche Aufgaben ist relativ hoch. Oft möchten wir, dass die Berechnungsaufgabe innerhalb eines bestimmten Zeitrahmens abgeschlossen wird, weshalb wir die minimale Anzahl an Prozessoren reservieren müssen, die erforderlich ist, damit die gesamte Berechnung in einem akzeptablen Zeitrahmen abgeschlossen werden kann. Die Reservierungsformel dafür lautet:
alloc: cpu = [1,*„Bitte platziere es auf dem Minion, wo mindestens ein freier Kern vorhanden ist, und alles weitere wird genutzt.“
Hier ist die Ressourcennutzung bereits deutlich besser als bei Aufgaben mit geringer Latenz. Der Gewinn wird jedoch viel größer sein, wenn beide Aufgabentypen auf einer Miniserver-Maschine kombiniert werden und ihre Ressourcen flexibel verteilt werden. Wenn eine Aufgabe mit geringer Latenz Prozessorleistung benötigt, erhält sie diese sofort, und wenn die Ressourcen nicht mehr gebraucht werden, werden sie an die Berechnungsaufgabe weitergegeben, also etwa so:

Aber wie macht man das?
Lass uns zunächst prod und seine Zuweisung anschauen: cpu = 4. Wir müssen vier Kerne reservieren. Das kann in Docker run auf zwei Arten gemacht werden:
- Mit der Option
--cpuset=1-4, d. h. vier bestimmte Kerne auf der Maschine der Aufgabe zuweisen. - Verwenden
--cpuquota=400_000 --cpuperiod=100_000, eine Quota für die Prozessorzeit festlegen, d. h. angeben, dass die Aufgabe in jedem realen Zeitintervall von 100 ms nicht mehr als 400 ms Prozessorzeit verbraucht. Das ergibt die gleichen vier Kerne.
Aber welcher dieser Ansätze ist geeignet?
Das cpuset sieht ziemlich ansprechend aus. Bei dieser Aufgabe stehen vier dedizierte Kerne zur Verfügung, was bedeutet, dass die Prozessor-Caches maximal effizient arbeiten. Es gibt jedoch einen Nachteil: Wir müssten die Verteilung der Berechnungen über die weniger belasteten Kerne der Maschine selbst übernehmen anstatt durch das Betriebssystem, und das ist eine recht anspruchsvolle Aufgabe, insbesondere wenn wir versuchen, auf einer solchen Maschine Batch-Jobs zu platzieren. Tests haben gezeigt, dass hier die Variante mit der Quotenregelung besser geeignet ist: So hat das Betriebssystem mehr Freiheit bei der Auswahl des Kerns zur Ausführung der Aufgabe zu einem bestimmten Zeitpunkt, und die Prozessorzeit wird effizienter verteilt.
Lassen Sie uns betrachten, wie im Docker eine Reservierung für die minimale Anzahl von Kernen erstellt werden kann. Die Quotenregelung für Batch-Jobs ist bereits nicht mehr anwendbar, da es nicht notwendig ist, ein Maximum zu beschränken; es reicht aus, nur ein Minimum zu garantieren. Hier eignet sich die Option gut. docker run --cpushares.
Wir haben uns darauf geeinigt, dass wenn ein Batch eine Garantie für mindestens einen Kern benötigt, wir angeben, --cpushares=1024, und wenn ein Minimum von zwei Kernen erforderlich ist, geben wir an, --cpushares=2048. CPU-Anteile beeinflussen die Verteilung der Prozessorzeit nicht, solange ausreichend vorhanden ist. Wenn also das Produkt momentan nicht alle vier Kerne nutzt, gibt es keine Einschränkungen für Batch-Aufgaben, und sie können zusätzliche Prozessorzeit verwenden. In einer Situation, in der die CPU jedoch überlastet ist, und das Produkt all seine vier Kerne verbraucht hat und auf die Quota stößt, wird die verbleibende Prozessorzeit proportional zu den CPU-Anteilen verteilt. Das bedeutet, dass bei drei verfügbaren Kernen eine Aufgabe mit 1024 CPU-Anteilen einen Kern erhält, während die anderen beiden einem Job mit 2048 CPU-Anteilen zugewiesen werden.
Doch die Verwendung von Quota und Shares reicht nicht aus. Wir müssen sicherstellen, dass Aufgaben mit kurzer Verzögerung Priorität vor Batch-Aufgaben bei der Verteilung der Prozessorzeit erhalten. Ohne diese Priorisierung würde die Batch-Aufgabe die gesamte Prozessorzeit in Anspruch nehmen, wenn sie für das Produkt benötigt wird. Bei Docker run gibt es keine Optionen zur Priorisierung von Containern, aber die CPU-Scheduler-Richtlinien in Linux helfen hier. Detaillierte Informationen hierzu sind verfügbar , und in diesem Artikel werden wir sie kurz durchgehen:
- SCHED_OTHER
Standardmäßig erhalten alle regulären Benutzerprozesse auf einer Linux-Maschine. - SCHED_BATCH
Ist für ressourcenintensive Prozesse vorgesehen. Bei der Platzierung einer Aufgabe im Prozessor wird eine sogenannte Aktivierungsstrafe eingeführt: Eine solche Aufgabe hat eine geringere Wahrscheinlichkeit, Ressourcen des Prozessors zu erhalten, wenn dieser momentan von einer Aufgabe mit SCHED_OTHER genutzt wird. - SCHED_IDLE
Ein Hintergrundprozess mit sehr niedriger Priorität, sogar niedriger als nice –19. Wir verwenden unsere Open-Source-Bibliothek , um die erforderliche Politik beim Start eines Containers durch den Aufruf
one.nio.os.Proc.sched_setscheduler( pid, Proc.SCHED_IDLE )Aber selbst wenn Sie nicht in Java programmieren, kann dasselbe mit dem Befehl chrt erreicht werden:
chrt -i 0 $pidLassen Sie uns alle unsere Isolationsstufen in einer Tabelle zur besseren Übersicht zusammenfassen:
Isolationsklasse
Beispiel alloc
Docker run Optionen
sched_setscheduler chrt*
Prod
cpu = 4
--cpuquota=400000 --cpuperiod=100000
SCHED_OTHER
Batch
Cpu = [1, * )
--cpushares=1024
SCHED_BATCH
Idle
Cpu= [2, *)
--cpushares=2048
SCHED_IDLE
*Wenn Sie chrt aus dem Container heraus ausführen, kann die Fähigkeit sys_nice erforderlich sein, da Docker diese Fähigkeit standardmäßig beim Start eines Containers entfernt.
Doch die Aufgaben beanspruchen nicht nur die CPU, sondern auch den Traffic, der die Verzögerung bei der Netzwerkauslastung noch stärker beeinflusst als eine falsche Verteilung der CPU-Ressourcen. Daher möchten wir selbstverständlich dasselbe auch für den Traffic erhalten. Das heißt, wenn die Produktionsaufgabe Datenpakete ins Netzwerk sendet, begrenzen wir die maximale Geschwindigkeit (Formel alloc: lan=[*,500mbps) ), mit der die Produktion dies tun kann. Für Batch-Aufgaben garantieren wir lediglich eine minimale Bandbreite, beschränken jedoch die maximale Bandbreite nicht (Formel alloc: lan=[10Mbps,*)) ) Dabei sollte der Traffic der Produktion Vorrang vor den Batch-Aufgaben haben.
Hier hat Docker keine primitiven Elemente, die wir nutzen könnten. Doch uns kommt zur Hilfe. Wir konnten das gewünschte Ergebnis mit der Disziplin erreichen. Damit schaffen wir zwei Traffic-Klassen: Hochpriorität für die Produktion und Niedrigpriorität für Batch/Idle. Schließlich ergibt sich folgende Konfiguration für den ausgehenden Traffic:
Hier 1:0 – der „Root qdisc“ der hsfc-Disziplinen; 1:1 – eine untergeordnete hsfc-Klasse mit einem Gesamtbegrenzung für die Bandbreite von 8 Gbit/s, unter der alle Container-Unterklassen zusammengefasst sind; 1:2 – eine untergeordnete hsfc-Klasse, die für alle Batch- und Idle-Aufgaben mit „dynamischen“ Limits, wie unten beschrieben, allgemein ist. Die übrigen untergeordneten hsfc-Klassen sind spezielle Klassen für aktuell ausgeführte Prod-Container mit Limits, die ihren Manifesten entsprechen – 450 und 400 Mbit/s. Jeder hsfc-Klasse ist eine qdisc-Warteschlange fq oder fq_codel zugewiesen, abhängig von der Kernelversion von Linux, um Paketverluste bei Traffic-Spitzen zu vermeiden.
Normalerweise werden tc-Disziplinen nur zur Priorisierung des ausgehenden Datenverkehrs verwendet. Aber wir möchten auch den eingehenden Datenverkehr priorisieren – denn irgendeine Batch-Aufgabe könnte den gesamten eingehenden Kanal für sich beanspruchen, indem sie beispielsweise ein großes Paket an Eingabedaten für Map & Reduce erhält. Zu diesem Zweck verwenden wir das Modul , der für jede Netzwerkschnittstelle ein virtuelles ifbX-Interface erstellt und den eingehenden Datenverkehr von der Schnittstelle auf das ifbX umleitet. Für ifbX gelten dann alle gleichen Disziplinen zur Steuerung des ausgehenden Datenverkehrs, wobei die hsfc-Konfiguration sehr ähnlich sein wird:
In unseren Experimenten haben wir festgestellt, dass hsfc die besten Ergebnisse erzielt, wenn die Klasse 1:2 des nicht priorisierten Batch-/Idle-Datenverkehrs auf Minion-Maschinen auf eine bestimmte Freiskapazität begrenzt wird. Andernfalls beeinflusst der nicht priorisierte Datenverkehr die Latenz der Prod-Aufgaben erheblich. Die aktuelle Freiskapazität wird von miniond jede Sekunde bestimmt, indem der durchschnittliche Datenverbrauch aller Prod-Aufgaben dieses Minions gemessen und von der Bandbreite des Netzwerkinterfaces abgezogen wird.
und von der Bandbreite des Netzwerkinterfaces abgezogen wird.
mit einem kleinen Puffer, d. h.

Die Bandbreiten werden unabhängig für eingehenden und ausgehenden Datenverkehr definiert. Entsprechend den neuen Werten konfiguriert miniond das Limit der nicht priorisierten Klasse 1:2 neu.
So haben wir alle drei Isolationsklassen umgesetzt: prod, batch und idle. Diese Klassen beeinflussen stark die Ausführungseigenschaften der Aufgaben. Daher haben wir uns entschieden, dieses Merkmal an die Spitze der Hierarchie zu setzen, damit man auf den ersten Blick beim Namen der hierarchischen Warteschlange sofort erkennen kann, womit wir es zu tun haben:

Alle unsere Bekannten web und music werden dann in der Hierarchie unter prod eingeordnet. Zum Beispiel können wir unter batch den Dienst music catalogeinordnen, der regelmäßig einen Katalog von Tracks aus einer Sammlung von in "Одноклассники" hochgeladenen mp3-Dateien erstellt. Ein Beispiel für einen Dienst unter idle könnte sein music transformer, der die Lautstärke der Musik normalisiert.
Nachdem wir wieder überflüssige Zeilen entfernt haben, können wir die Namen unserer Dienste flacher aufzeichnen, indem wir die Isolationsklasse der Aufgabe am Ende des vollständigen Dienstnamens hinzufügen: web.front.prod, catalog.music.batch, transformer.music.idle.
Jetzt verstehen wir beim Blick auf den Dienstnamen nicht nur, welche Funktion er ausführt, sondern auch seine Isolationsklasse, was seine Kritikalität und Ähnliches bedeutet.
Das ist alles wunderbar, aber es gibt eine bittere Wahrheit. Eine vollständige Isolation von Aufgaben, die auf einer Maschine laufen, ist unmöglich.
Was wir erreicht haben: Wenn batch intensiv Ressourcen verbraucht, nur Die CPU-Ressourcen werden von dem eingebauten Linux-Prozessor-Planer gut verwaltet, was praktisch keinen Einfluss auf die Produktionsaufgaben hat. Beginnt jedoch diese Batch-Aufgabe aktiv mit dem Speicher zu arbeiten, zeigt sich eine gegenseitige Beeinflussung. Das liegt daran, dass die Cache-Speicher der Produktionsaufgabe „ausgewaschen“ werden – die Cache-Fehler steigen, und der Prozessor verarbeitet die Produktionsaufgabe langsamer. Eine solche Batch-Aufgabe kann die Latenz unseres typischen Produktionscontainers um 10 % erhöhen.
Die Isolierung des Traffics ist noch schwieriger, da moderne Netzwerkkarten eine interne Paketwarteschlange haben. Wenn ein Paket von der Batch-Aufgabe zuerst ankommt, wird es auch zuerst über das Kabel übertragen und da lässt sich nichts ändern.
Außerdem ist es uns bisher nur gelungen, die Priorisierung des TCP-Traffics zu lösen: Der Ansatz mit hsfc funktioniert nicht für UDP. Selbst im Falle von TCP-Traffic, wenn die Batch-Aufgabe viel Traffic generiert, führt dies ebenfalls zu einer Erhöhung der Latenz für die Produktionsaufgabe um etwa 10 %.
Ausfallsicherheit
Ein Ziel bei der Entwicklung von one-cloud war es, die Ausfallsicherheit von Odnoklassniki zu verbessern. Daher möchte ich im Folgenden die möglichen Ausfallszenarien und Notfälle näher betrachten. Lassen Sie uns mit einem einfachen Szenario beginnen – dem Ausfall eines Containers.
Ein Container kann auf verschiedene Weisen ausfallen. Dies kann durch ein Experiment, einen Bug oder einen Fehler im Manifest geschehen, wodurch die Produktionsaufgabe mehr Ressourcen verbraucht, als im Manifest angegeben. Wir hatten einen Fall: Ein Entwickler implementierte einen komplexen Algorithmus, überarbeitete ihn mehrfach und verlor dabei so den Überblick, dass die Aufgabe am Ende in einer nicht trivialisierbaren Schleife festhing. Da die Produktionsaufgabe jedoch eine höhere Priorität hat als alle anderen auf den gleichen Minions, begann sie, alle verfügbaren CPU-Ressourcen zu verbrauchen. In dieser Situation half die Isolation, genauer gesagt das Kontingent für die CPU-Zeit. Wenn der Aufgabe ein Kontingent zugewiesen wird, verbraucht sie nicht mehr. Daher bemerkten die Batch- und anderen Produktionsaufgaben, die auf demselben Rechner liefen, nichts davon.
Ein weiteres mögliches Problem ist der Abbruch des Containers. Hier helfen uns die Restart-Policies, die jeder kennt; Docker erledigt das hervorragend. Fast alle Produktionsaufgaben haben eine Always-Restart-Policy. Manchmal verwenden wir On-Failure für Batch-Jobs oder zur Fehlersuche bei Produktionscontainern.
Was kann man tun, wenn ein gesamter Minion nicht verfügbar ist?
Offensichtlich könnte man den Container auf einem anderen Rechner starten. Das Interessante ist hier: Was passiert mit der IP-Adresse (den IP-Adressen), die dem Container zugewiesen wurden?
Wir können Containern dieselben IP-Adressen zuweisen wie den Maschinen-Minions, auf denen diese Container laufen. Wenn der Container dann auf einem anderen Rechner gestartet wird, ändert sich seine IP-Adresse, und alle Clients müssen verstehen, dass der Container umgezogen ist; sie müssen jetzt eine andere Adresse ansprechen, was einen separaten Service für die Service Discovery erfordert.
Service Discovery ist praktisch. Auf dem Markt gibt es viele Lösungen mit unterschiedlichem Grad an Ausfallsicherheit zur Organisation eines Service-Registers. Oft beinhalten solche Lösungen auch Load-Balancer-Logik, die Speicherung zusätzlicher Konfigurationen in Form von KV-Speicher usw.
Wir möchten jedoch gerne auf die Notwendigkeit verzichten, ein separates Register einzuführen, da dies die Implementierung eines kritischen Systems bedeuten würde, das von allen Dienstleistungen in der Produktion genutzt wird. Das wäre eine potenzielle Fehlerquelle, und es müsste entweder eine sehr ausfallsichere Lösung gewählt oder entwickelt werden, was bekanntlich sehr kompliziert, zeitaufwendig und kostspielig ist.
Ein weiteres großes Manko: Um unsere alte Infrastruktur mit der neuen kompatibel zu machen, müsste man sämtliche Aufgaben für eine Service Discovery-System umschreiben. Die Aufgaben sind viel und an manchen Stellen nahezu unmöglich, insbesondere bei Low-Level-Geräten, die auf Betriebssystem-Kernel-Ebene oder direkt mit der Hardware arbeiten. Die Implementierung dieser Funktionalität durch etablierte Lösungsmuster, wie zum Beispiel würde teilweise zu zusätzlicher Belastung führen, teilweise die Nutzungskomplexität erhöhen und zusätzliche Ausfallszenarien schaffen. Da wir es nicht komplizierter machen wollten, haben wir entschieden, die Nutzung von Service Discovery optional zu gestalten.
In One-Cloud IP folgt dem Container, d. h. jede Instanz der Aufgabe hat ihre eigene IP-Adresse. Diese Adresse ist "statisch": Sie wird zum Zeitpunkt der ersten Bereitstellung des Dienstes in der Cloud einem jeden Exemplar zugeordnet. Wenn der Dienst während seiner Laufzeit eine unterschiedliche Anzahl von Exemplaren hatte, werden letztendlich so viele IP-Adressen zugewiesen, wie es maximal Exemplare gab.
Diese Adressen ändern sich anschließend nicht: Sie werden einmal zugewiesen und bestehen während der gesamten Lebensdauer des Dienstes in der Produktion. Die IP-Adressen folgen den Containern im Netzwerk. Wenn ein Container auf eine andere Minion migriert, wird die Adresse ebenfalls mit ihm übertragen.
Somit ändert sich die Zuordnung des Dienstnamens zu seiner Liste von IP-Adressen nur sehr selten. Werfen wir noch einmal einen Blick auf die Namen der Dienstinstanzen, die wir zu Beginn des Artikels erwähnt haben (1.ok-web.group1.web.front.prod, 2.ok-web.group1.web.front.prod, …), dann stellen wir fest, dass sie FQDN ähneln, die im DNS verwendet werden. Genau so ist es: Für die Anzeige von Instanznamen von Diensten in deren IP-Adressen nutzen wir das DNS-Protokoll. Dabei gibt dieses DNS alle reservierten IP-Adressen aller Container zurück – sowohl der laufenden als auch der gestoppten (angenommen, es sind drei Replikate in Verwendung und wir haben fünf Adressen reserviert – alle fünf werden zurückgegeben). Die Kunden, die diese Informationen erhalten, versuchen, eine Verbindung zu allen fünf Replikaten herzustellen – und erkennen somit die aktiven. Diese Methode der Verfügbarkeitsbestimmung ist deutlich zuverlässiger, da weder DNS noch Service Discovery involviert sind, was bedeutet, dass es auch keine schwierig zu lösenden Probleme hinsichtlich der Aktualität der Informationen und der Ausfallsicherheit dieser Systeme gibt. Darüber hinaus können wir in kritischen Diensten, von denen der gesamte Portalbetrieb abhängt, ganz auf DNS verzichten und einfach die IP-Adressen in die Konfiguration eintragen.
Die Implementierung eines solchen IP-Transfers für Container kann nicht trivial sein – und wir werden in folgendem Beispiel betrachten, wie das funktioniert:

Angenommen, der one-cloud Master gibt dem Minion M1 den Befehl, zu starten. 1.ok-web.group1.web.front.prod mit der IP-Adresse 1.1.1.1. Auf dem Minion läuft , der diese Adresse an spezielle Server ankündigt. . Letztere haben eine BGP-Sitzung mit der Netzwerkhardware, auf die die Route zur IP-Adresse 1.1.1.1 auf M1 übertragen wird. M1 routet die Pakete bereits mit Mitteln von Linux in den Container. Es gibt drei Route Reflectoren, da dies ein sehr kritischer Teil der Infrastruktur von one-cloud ist – ohne sie funktioniert das Netzwerk in one-cloud nicht. Wir platzieren sie in verschiedenen Racks, wenn möglich in unterschiedlichen Räumen des Rechenzentrums, um die Wahrscheinlichkeit eines gleichzeitigen Ausfalls aller drei zu verringern.
Angenommen, die Verbindung zwischen dem Master von one-cloud und dem Minion M1 ist abgerissen. Der Master von one-cloud wird nun unter der Annahme handeln, dass M1 vollständig ausgefallen ist. Das heißt, er wird den Minion M2 anweisen, zu starten. web.group1.web.front.prod Mit derselben Adresse 1.1.1.1 haben wir nun zwei konkurrierende Routen im Netzwerk für 1.1.1.1: auf M1 und M2. Um solche Konflikte zu lösen, verwenden wir den Multi Exit Discriminator, der im BGP-Ankündigung angegeben wird. Diese Zahl zeigt das Gewicht der angekündigten Route an. Aus den Konflikten wird die Route mit dem niedrigeren MED-Wert ausgewählt. Der one-cloud Master unterstützt MED als integralen Bestandteil der IP-Adressen der Container. Zunächst wird die Adresse mit einem ausreichend hohen MED = 1.000.000 ausgegeben. Bei einem solchen Notfall-Containerumzug reduziert der Master das MED, und M2 erhält den Befehl, die Adresse 1.1.1.1 mit MED = 999.999 anzukündigen. Instanz, die auf M1 arbeitet, bleibt dabei nicht verbunden, und ihre zukünftige Situation interessiert uns wenig, bis die Verbindung zum Master wiederhergestellt ist, wenn sie als alter Dublette gestoppt wird.
Ausfälle
Alle Systeme zur Verwaltung von Rechenzentren arbeiten in der Regel gut mit kleineren Ausfällen. Ein Containerabsturz ist praktisch überall die Norm.
Lassen Sie uns betrachten, wie wir mit einem Ausfall umgehen, beispielsweise einem Stromausfall in einem oder mehreren Räumen des Rechenzentrums.
Was bedeutet ein Ausfall für das Managementsystem des Rechenzentrums? Zunächst einmal ist es ein massiver gleichzeitiger Ausfall vieler Maschinen, und das Managementsystem muss gleichzeitig sehr viele Container migrieren. Wenn der Ausfall jedoch sehr umfangreich ist, kann es passieren, dass nicht alle Aufgaben auf andere Minions umverteilt werden können, da die Ressourcenfähigkeit des Rechenzentrums unter 100 % der Last fällt.
Ausfälle gehen oft mit dem Versagen der Steuerungsschicht einher. Dies kann aufgrund eines Ausfalls der Hardware geschehen, häufiger jedoch, weil Ausfälle nicht getestet werden und die Steuerungsschicht selbst aufgrund der erhöhten Last zusammenbricht.
Was kann man dagegen tun?
Massenauslagerungen bedeuten, dass in der Infrastruktur eine große Anzahl von Aktionen, Migrationen und Platzierungen stattfindet. Jede Migration benötigt Zeit für die Übertragung und Entpackung der Container-Images zu den Minions, das Starten und die Initialisierung der Container usw. Daher ist es wünschenswert, dass wichtigere Aufgaben vor weniger wichtigen gestartet werden.
Lassen Sie uns erneut einen Blick auf die vertraute Hierarchie der Dienste werfen und versuchen zu klären, welche Aufgaben wir zuerst starten möchten.

Natürlich sind dies die Prozesse, die direkt an der Bearbeitung von Benutzeranfragen beteiligt sind, also prod. Wir geben dies an mit dem Platzierungspriorität — eine Zahl, die einer Warteschlange zugewiesen werden kann. Wenn eine Warteschlange eine höhere Priorität hat, werden ihre Dienste zuerst bereitgestellt.
Für prod weisen wir höhere Prioritäten zu, 0; für batch etwas niedrigere, 100; und für idle noch niedrigere, 200. Die Prioritäten werden hierarchisch angewendet. Alle Aufgaben unterhalb dieser Hierarchie haben die entsprechende Priorität. Wenn wir möchten, dass im Bereich prod die Caches vor den Frontends starten, setzen wir die Prioritäten für cache auf 0 und für die Front-Subwarteschlangen auf 1. Wenn wir beispielsweise möchten, dass von den Frontends zuerst das Hauptportal gestartet wird und der Musik-Frontend danach, können wir letzterem eine niedrigere Priorität von 10 zuweisen.
Das nächste Problem ist der Mangel an Ressourcen. Wir haben eine große Menge an Geräten verloren, ganze Hallen im Rechenzentrum sind betroffen, während wir so viele Dienste gestartet haben, dass die Ressourcen nun nicht für alle ausreichen. Wir müssen entscheiden, welche Aufgaben wir opfern können, damit die wesentlichen kritischen Dienste weiterhin funktionieren.

Im Gegensatz zur Priorität der Bereitstellung können wir nicht einfach alle Batch-Aufgaben opfern, da einige von ihnen wichtig für den Betrieb des Portals sind. Deshalb haben wir separat выделены Priorität des Verdrängens von Aufgaben. Bei der Bereitstellung kann eine Aufgabe mit höherer Priorität eine Aufgabe mit niedrigerer Priorität verdrängen, das heißt, sie kann gestoppt werden, wenn keine freien Minions mehr vorhanden sind. In diesem Fall wird die Aufgabe mit niedriger Priorität wahrscheinlich unzugeordnet bleiben, da es keinen geeigneten Minion mit ausreichend freien Ressourcen mehr geben wird.
In unserer Hierarchie ist es sehr einfach, einen solchen Prioritätsersatz anzugeben, sodass prod- und batch-Aufgaben idle-Aufgaben verdrängen oder stoppen, sich jedoch nicht gegenseitig stören, indem der idle-Aufgabe eine Priorität von 200 zugewiesen wird. Genau wie beim Zuweisung von Prioritäten können wir unsere Hierarchie nutzen, um komplexere Regeln zu beschreiben. Zum Beispiel können wir angeben, dass wir die Musikfunktion opfern, wenn die Ressourcen für unser Haupt-Webportal nicht ausreichen, indem wir die Priorität der entsprechenden Knoten niedriger setzen: 10.
Ausfälle des Rechenzentrums insgesamt
Warum kann das gesamte Rechenzentrum ausfallen? Naturgewalten. Es gab einen interessanten Beitrag darüber, wie Als Naturgewalt kann man auch obdachlose Menschen betrachten, die einmal die Glasfaser in einem Abfluss angezündet haben, wodurch das Rechenzentrum die Verbindung zu anderen Standorten komplett verlor. Eine Ursache für den Ausfall kann auch menschliches Versagen sein: Ein Operator kann einen Befehl ausgeben, der das gesamte Rechenzentrum zum Absturz bringt. Dies kann durch einen großen Fehler geschehen. Allgemein gesagt, Rechenzentren fallen aus — das ist keine Seltenheit. Bei uns passiert das alle paar Monate.
Und das machen wir, damit niemand #okzhivi in Twitter postet.
Die erste Strategie ist die Isolation. Jede Instanz von one-cloud ist isoliert und kann nur Maschinen in einem einzigen Rechenzentrum verwalten. Das bedeutet, dass der Verlust eines Cloud-Dienstes aufgrund von Bugs oder falschen Befehlen des Betreibers nur den Verlust dieses einen Rechenzentrums zur Folge hat. Wir sind darauf vorbereitet: Es gibt eine Reservierungspolitik, bei der die Replikate von Anwendungen und Daten in allen Rechenzentren untergebracht werden. Wir nutzen ausfallsichere Datenbanken und testen regelmäßig die Ausfälle.
Da wir heute über vier Rechenzentren verfügen, haben wir auch vier separate, vollständig isolierte Instanzen von one-cloud.
Dieser Ansatz schützt nicht nur vor physischen Ausfällen, sondern kann auch vor Fehlern des Betreibers schützen.
Was kann man noch im Umgang mit dem menschlichen Faktor tun? Wenn ein Betreiber der Cloud einen merkwürdigen oder potenziell gefährlichen Befehl erteilt, könnte von ihm plötzlich verlangt werden, eine kleine Aufgabe zu lösen, um zu überprüfen, wie gut er nachgedacht hat. Zum Beispiel, wenn es um eine Massenstilllegung vieler Replikate geht oder wenn es sich einfach um einen seltsamen Befehl handelt – die Reduzierung der Anzahl der Replikate oder das Ändern des Namens des Images, nicht nur der Versionsnummer im neuen Manifest.
Ergebnisse
Besondere Merkmale von one-cloud:
- Hierarchisches und anschauliches Schema zur Benennung von Services und Containern, das es ermöglicht, sehr schnell zu erkennen, um welche Aufgabe es sich handelt, zu welchem Bereich sie gehört, wie sie funktioniert und wer dafür verantwortlich ist.
- Wir wenden unsere Technik zur Kombination von Prod- und Batch-Aufgaben auf Minion-Instanzen an, um die Effizienz der gemeinsamen Nutzung von Maschinen zu erhöhen. Statt cpuset verwenden wir CPU-Quoten, Shares, CPU-Planungspolitiken und Linux QoS.
- Eine vollständige Isolation der Container, die auf einer Maschine arbeiten, konnten wir nicht erreichen, aber ihr gegenseitiger Einfluss bleibt innerhalb von 20 %.
- Die Organisation der Services in einer Hierarchie hilft bei der automatischen Behebung von Störungen durch Platzierungsprioritäten und Verdrängung..
FAQ
Warum haben wir keine fertige Lösung gewählt.
- Verschiedene Klassen von Aufgabasolationen erfordern unterschiedliche Logik bei der Platzierung auf Minion-Instanzen. Während Prod-Aufgaben einfach durch Ressourcensicherung platziert werden können, müssen Batch- und Idle-Aufgaben unter Berücksichtigung der tatsächlichen Ressourcenauslastung auf den Minion-Maschinen platziert werden.
- Die Notwendigkeit, solche von Aufgaben genutzten Ressourcen zu berücksichtigen, wie:
- Netzwerkbandbreite;
- Typen und „Spindeln“ der Festplatten.
- Die Notwendigkeit, Prioritäten für Dienste bei der Beseitigung von Störungen, die Rechte und Quoten der Teams für Ressourcen anzugeben, wird durch hierarchische Warteschlangen in one-cloud gelöst.
- Die Notwendigkeit, Container menschenlesbar zu benennen, um die Reaktionszeiten auf Störungen und Vorfälle zu verkürzen.
- Die Unfähigkeit, Service Discovery gleichzeitig flächendeckend einzuführen; die Notwendigkeit, eine lange Zeit mit Aufgaben, die auf physischen Hosts betrieben werden, zu koexistieren – das wird durch 'statische' IP-Adressen gelöst, die den Containern folgen, und in der Folge die Notwendigkeit einer einzigartigen Integration mit einer großen Netzwerk-Infrastruktur.
All diese Funktionen würden bedeutende Änderungen der bestehenden Lösungen erfordern, und nachdem wir den Arbeitsaufwand bewertet haben, haben wir festgestellt, dass wir unsere eigene Lösung mit ungefähr dem gleichen Aufwand entwickeln können. Doch diese eigene Lösung wird erheblich einfacher zu betreiben und weiterzuentwickeln sein – sie enthält keine überflüssigen Abstraktionen, die Funktionen unterstützen, die wir nicht benötigen.
Vielen Dank an alle, die diese letzten Zeilen gelesen haben, für Ihre Geduld und Aufmerksamkeit!
Quelle: habr.com
