Verwenden von mcrouter zur horizontalen Skalierung von Memcached

Verwenden von mcrouter zur horizontalen Skalierung von Memcached

Die Entwicklung hochlastiger Projekte in einer beliebigen Sprache erfordert eine besondere Herangehensweise und den Einsatz spezieller Tools. Bei Anwendungen in PHP kann sich die Situation jedoch so verschärfen, dass Sie beispielsweise Folgendes entwickeln müssen: eigener Anwendungsserver. In diesem Hinweis werden wir über die bekannten Probleme mit der verteilten Sitzungsspeicherung und dem Daten-Caching in Memcached sprechen und darüber, wie wir diese Probleme in einem „Ward“-Projekt gelöst haben.

Der Held des Anlasses ist eine PHP-Anwendung auf Basis des Symfony 2.3-Frameworks, die in den zu aktualisierenden Business-Plänen überhaupt nicht enthalten ist. Dieses Projekt nutzte nicht nur den ganz normalen Sitzungsspeicher, sondern auch den vollen Nutzen Richtlinie „Alles zwischenspeichern“. in Memcached: Antworten auf Anfragen an die Datenbank- und API-Server, verschiedene Flags, Sperren zur Synchronisierung der Codeausführung und vieles mehr. In einer solchen Situation kann sich ein Ausfall des Memcached negativ auf den Betrieb der Anwendung auswirken. Darüber hinaus führt ein Cache-Verlust zu schwerwiegenden Folgen: Das DBMS beginnt aus allen Nähten zu platzen, API-Dienste beginnen, Anfragen zu verbieten usw. Die Stabilisierung der Situation kann mehrere zehn Minuten dauern, und während dieser Zeit ist der Dienst furchtbar langsam oder überhaupt nicht verfügbar.

Wir mussten etwas bereitstellen die Möglichkeit, die Anwendung mit geringem Aufwand horizontal zu skalieren, d.h. mit minimalen Änderungen am Quellcode und voller Funktionalität. Machen Sie den Cache nicht nur fehlersicher, sondern versuchen Sie auch, den daraus resultierenden Datenverlust zu minimieren.

Was ist mit Memcached selbst los?

Im Allgemeinen unterstützt die Memcached-Erweiterung für PHP standardmäßig verteilte Daten- und Sitzungsspeicherung. Der Mechanismus für konsistentes Schlüssel-Hashing ermöglicht es Ihnen, Daten gleichmäßig auf vielen Servern zu platzieren und jeden spezifischen Schlüssel eindeutig an einen bestimmten Server aus der Gruppe zu adressieren, und integrierte Failover-Tools sorgen für eine hohe Verfügbarkeit des Caching-Dienstes (aber leider keine Daten).

Beim Sitzungsspeicher sieht es etwas besser aus: Sie können ihn konfigurieren memcached.sess_number_of_replicasDadurch werden die Daten auf mehreren Servern gleichzeitig gespeichert und bei Ausfall einer zwischengespeicherten Instanz werden die Daten von anderen übertragen. Wenn der Server jedoch ohne Daten wieder online geht (was normalerweise nach einem Neustart der Fall ist), werden einige der Schlüssel zu seinen Gunsten neu verteilt. Tatsächlich wird dies bedeuten Verlust von Sitzungsdaten, da es im Falle eines Fehlschlags keine Möglichkeit gibt, zu einer anderen Replik zu „gehen“.

Hauptsächlich sind Standard-Bibliothekstools gedacht horizontal Skalierung: Sie ermöglichen es Ihnen, den Cache auf gigantische Größen zu vergrößern und den Zugriff darauf über Code zu ermöglichen, der auf verschiedenen Servern gehostet wird. In unserer Situation überschreitet das gespeicherte Datenvolumen jedoch nicht mehrere Gigabyte und die Leistung von einem oder zwei Knoten reicht völlig aus. Dementsprechend könnten die einzigen nützlichen Standardtools darin bestehen, die Verfügbarkeit von Memcached sicherzustellen und gleichzeitig mindestens eine Cache-Instanz funktionsfähig zu halten. Es war jedoch nicht einmal möglich, diese Gelegenheit zu nutzen... An dieser Stelle sei an die Antike des im Projekt verwendeten Frameworks erinnert, weshalb es unmöglich war, die Anwendung mit einem Serverpool zum Laufen zu bringen. Vergessen wir auch nicht den Verlust von Sitzungsdaten: Das Auge des Kunden zuckte wegen der massiven Abmeldung der Benutzer.

Im Idealfall war es erforderlich Replikation von Datensätzen in zwischengespeicherten und umgangenen Replikaten im Falle eines Fehlers oder Irrtums. Hat uns bei der Umsetzung dieser Strategie geholfen mcrouter.

mcrouter

Dies ist ein zwischengespeicherter Router, der von Facebook entwickelt wurde, um seine Probleme zu lösen. Es unterstützt das Memcached-Textprotokoll, das dies ermöglicht Skalieren Sie zwischengespeicherte Installationen zu wahnsinnigen Ausmaßen. Eine ausführliche Beschreibung von mcrouter finden Sie in diese Ankündigung. Unter anderem große Funktionalität Es kann tun, was wir brauchen:

  • Datensatz replizieren;
  • Fallback auf andere Server in der Gruppe durchführen, wenn ein Fehler auftritt.

Für die Sache!

mcrouter-Konfiguration

Ich gehe direkt zur Konfiguration:

{
 "pools": {
   "pool00": {
     "servers": [
       "mc-0.mc:11211",
       "mc-1.mc:11211",
       "mc-2.mc:11211"
   },
   "pool01": {
     "servers": [
       "mc-1.mc:11211",
       "mc-2.mc:11211",
       "mc-0.mc:11211"
   },
   "pool02": {
     "servers": [
       "mc-2.mc:11211",
       "mc-0.mc:11211",
       "mc-1.mc:11211"
 },
 "route": {
   "type": "OperationSelectorRoute",
   "default_policy": "AllMajorityRoute|Pool|pool00",
   "operation_policies": {
     "get": {
       "type": "RandomRoute",
       "children": [
         "MissFailoverRoute|Pool|pool02",
         "MissFailoverRoute|Pool|pool00",
         "MissFailoverRoute|Pool|pool01"
       ]
     }
   }
 }
}

Warum drei Pools? Warum werden Server wiederholt? Lassen Sie uns herausfinden, wie es funktioniert.

  • In dieser Konfiguration wählt mcrouter den Pfad, an den die Anfrage gesendet wird, basierend auf dem Anfragebefehl aus. Der Typ erzählt ihm das OperationSelectorRoute.
  • GET-Anfragen gehen an den Handler RandomRoutedie zufällig einen Pool oder eine Route unter Array-Objekten auswählt children. Jedes Element dieses Arrays ist wiederum ein Handler MissFailoverRoute, der jeden Server im Pool durchläuft, bis er eine Antwort mit Daten erhält, die an den Client zurückgegeben werden.
  • Wenn wir ausschließlich verwendet haben MissFailoverRoute Bei einem Pool von drei Servern würden dann alle Anfragen zuerst an die erste zwischengespeicherte Instanz gehen, und der Rest würde Anfragen auf Restbasis erhalten, wenn keine Daten vorhanden wären. Ein solcher Ansatz würde dazu führen Überlastung des ersten Servers in der ListeDaher wurde beschlossen, drei Pools mit Adressen in unterschiedlicher Reihenfolge zu generieren und diese zufällig auszuwählen.
  • Alle anderen Anfragen (und dies ist ein Datensatz) werden mit verarbeitet AllMajorityRoute. Dieser Handler sendet Anfragen an alle Server im Pool und wartet auf Antworten von mindestens N/2 + 1 von ihnen. Vom Gebrauch AllSyncRoute für Schreibvorgänge musste aufgegeben werden, da diese Methode eine positive Antwort von erfordert alle Server in der Gruppe - andernfalls wird es zurückgegeben SERVER_ERROR. Obwohl mcrouter die Daten zu verfügbaren Caches hinzufügt, ist dies die aufrufende PHP-Funktion wird einen Fehler zurückgeben und wird Aufmerksamkeit erregen. AllMajorityRoute ist nicht so streng und ermöglicht die Außerbetriebnahme von bis zu der Hälfte der Einheiten ohne die oben beschriebenen Probleme.

Hauptminus Dieses Schema besteht darin, dass, wenn wirklich keine Daten im Cache vorhanden sind, für jede Anfrage des Clients tatsächlich N Anfragen an Memcached ausgeführt werden – bis alle Server im Pool. Wir können die Anzahl der Server in Pools beispielsweise auf zwei reduzieren: Das bedeutet Einbußen bei der Speicherzuverlässigkeitоhöhere Geschwindigkeit und weniger Belastung durch Anfragen bis hin zu fehlenden Schlüsseln.

NB: Möglicherweise finden Sie auch nützliche Links zum Erlernen von mcrouter Dokumentation im Wiki и Projektfragen (einschließlich geschlossener), die ein ganzes Lagerhaus in verschiedenen Konfigurationen darstellen.

Erstellen und Ausführen von mcrouter

Unsere Anwendung (und Memcached selbst) läuft in Kubernetes – dementsprechend befindet sich dort auch mcrouter. Für Behältermontage wir gebrauchen Hof, die Konfiguration sieht folgendermaßen aus:

NB: Die im Artikel angegebenen Auflistungen werden im Repository veröffentlicht flach/mcrouter.

configVersion: 1
project: mcrouter
deploy:
 namespace: '[[ env ]]'
 helmRelease: '[[ project ]]-[[ env ]]'
---
image: mcrouter
from: ubuntu:16.04
mount:
- from: tmp_dir
 to: /var/lib/apt/lists
- from: build_dir
 to: /var/cache/apt
ansible:
 beforeInstall:
 - name: Install prerequisites
   apt:
     name: [ 'apt-transport-https', 'tzdata', 'locales' ]
     update_cache: yes
 - name: Add mcrouter APT key
   apt_key:
     url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
 - name: Add mcrouter Repo
   apt_repository:
     repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
     filename: mcrouter
     update_cache: yes
 - name: Set timezone
   timezone:
     name: "Europe/Moscow"
 - name: Ensure a locale exists
   locale_gen:
     name: en_US.UTF-8
     state: present
 install:
 - name: Install mcrouter
   apt:
     name: [ 'mcrouter' ]

(werf.yaml)

... und skizziere es Helmkarte. Das Interessante ist, dass es nur einen Konfigurationsgenerator gibt, der auf der Anzahl der Replikate basiert (Wenn jemand eine lakonischere und elegantere Option hat, teilen Sie sie in den Kommentaren):

{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{- /* Заполняем  массив двумя копиями серверов: "0 1 2 0 1 2" */ -}}
{{- range until 2 -}}
 {{- range $i, $_ := until $count -}}
   {{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
 {{- end -}}
{{- end -}}
{{- /* Смещаясь по массиву, получаем N срезов: "[0 1 2] [1 2 0] [2 0 1]" */ -}}
{{- range $i, $_ := until $count -}}
 {{- $pool := dict "servers" (slice $servers $i (add $i $count)) -}}
 {{- $_ := set $pools (printf "MissFailoverRoute|Pool|pool%02d" $i) $pool -}}
{{- end -}}
---
apiVersion: v1
kind: ConfigMap
metadata:
 name: mcrouter
data:
 config.json: |
   {
     "pools": {{- $pools | toJson | replace "MissFailoverRoute|Pool|" "" -}},
     "route": {
       "type": "OperationSelectorRoute",
       "default_policy": "AllMajorityRoute|Pool|pool00",
       "operation_policies": {
         "get": {
           "type": "RandomRoute",
           "children": {{- keys $pools | toJson }}
         }
       }
     }
   }

(10-mcrouter.yaml)

Wir rollen es in die Testumgebung aus und prüfen:

# php -a
Interactive mode enabled

php > # Проверяем запись и чтение
php > $m = new Memcached();
php > $m->addServer('mcrouter', 11211);
php > var_dump($m->set('test', 'value'));
bool(true)
php > var_dump($m->get('test'));
string(5) "value"
php > # Работает! Тестируем работу сессий:
php > ini_set('session.save_handler', 'memcached');
php > ini_set('session.save_path', 'mcrouter:11211');
php > var_dump(session_start());
PHP Warning:  Uncaught Error: Failed to create session ID: memcached (path: mcrouter:11211) in php shell code:1
Stack trace:
#0 php shell code(1): session_start()
#1 {main}
  thrown in php shell code on line 1
php > # Не заводится… Попробуем задать session_id:
php > session_id("zzz");
php > var_dump(session_start());
PHP Warning:  session_start(): Cannot send session cookie - headers already sent by (output started at php shell code:1) in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Failed to write session lock: UNKNOWN READ FAILURE in php shell code on line 1
PHP Warning:  session_start(): Unable to clear session lock record in php shell code on line 1
PHP Warning:  session_start(): Failed to read session data: memcached (path: mcrouter:11211) in php shell code on line 1
bool(false)
php >

Die Suche nach dem Fehlertext ergab keine Ergebnisse, wohl aber die Abfrage „Mikrouter PHP„Im Vordergrund stand das älteste ungelöste Problem des Projekts – Mangel an Unterstützung zwischengespeichertes Binärprotokoll.

NB: Das ASCII-Protokoll in Memcached ist langsamer als das Binärprotokoll und Standardmittel für konsistentes Schlüssel-Hashing funktionieren nur mit dem Binärprotokoll. Im konkreten Fall stellt dies jedoch keine Probleme dar.

Der Trick liegt in der Tasche: Sie müssen nur auf das ASCII-Protokoll umsteigen und alles wird funktionieren.... In diesem Fall besteht jedoch die Gewohnheit, nach Antworten zu suchen Dokumentation auf php.net einen grausamen Scherz gespielt. Dort finden Sie nicht die richtige Antwort ... es sei denn, Sie scrollen natürlich bis zum Ende des Abschnitts „Benutzer hat Notizen beigetragen“ wird treu sein und zu Unrecht herabgestimmte Antwort.

Ja, der korrekte Optionsname lautet memcached.sess_binary_protocol. Es muss deaktiviert werden, danach beginnen die Sitzungen zu funktionieren. Jetzt muss nur noch der Container mit mcrouter in einen Pod mit PHP gestellt werden!

Abschluss

Somit konnten wir das Problem allein durch infrastrukturelle Änderungen lösen: Das Problem mit der Memcached-Fehlertoleranz wurde behoben und die Zuverlässigkeit der Cache-Speicherung wurde erhöht. Neben den offensichtlichen Vorteilen für die Anwendung ergab sich dadurch auch Handlungsspielraum bei der Arbeit auf der Plattform: Wenn alle Komponenten über eine Reserve verfügen, vereinfacht sich das Leben des Administrators erheblich. Ja, diese Methode hat auch ihre Nachteile, sie mag wie eine „Krücke“ aussehen, aber wenn sie Geld spart, das Problem vergräbt und keine neuen verursacht – warum nicht?

PS

Lesen Sie auch auf unserem Blog:

Source: habr.com

Kommentar hinzufügen