
Die Entwicklung hochbelasteter Projekte in beliebigen Programmiersprachen erfordert einen besonderen Ansatz und den Einsatz spezieller Werkzeuge. Doch wenn es um Anwendungen in PHP geht, kann die Situation so angespannt sein, dass beispielsweise ein entwickelt werden muss. In diesem Artikel geht es um das allbekannte Problem der verteilten Speicherung von Sitzungen und der Daten-Caching mit memcached sowie darum, wie wir diese Herausforderungen in einem 'Schützling'-Projekt gelöst haben.
Der Hauptakteur ist eine PHP-Anwendung, die auf dem Framework Symfony 2.3 basiert, dessen Aktualisierung für das Unternehmen überhaupt nicht auf der Agenda steht. Neben der durchaus üblichen Sitzungsspeicherung wurde in diesem Projekt verstärkt eine Politik des 'Cache alles' verfolgt. Bei memcached: Antworten auf Anfragen an Datenbanken und API-Server, verschiedene Flags, Synchronisationssperren zur Ausführung von Code und vieles mehr. In solch einer Situation kann ein Ausfall von memcached für die Funktionsfähigkeit der Anwendung katastrophal sein. Zudem führt der Verlust des Caches zu ernsthaften Konsequenzen: Die Datenbank beginnt zu instabil zu werden, und API-Dienste können Anfragen blockieren. Die Stabilisierung der Situation kann Minuten dauern, während der Dienst stark verlangsamt oder sogar vollständig unzugänglich ist.
Wir mussten sicherstellen die Möglichkeit eines horizontalen Skalierens der Anwendung mit geringem Aufwand, d.h. mit minimalen Änderungen des Quellcodes und vollständiger Bewahrung der Funktionalität. Den Cache nicht nur ausfallsicher zu machen, sondern auch zu versuchen, den Datenverlust zu minimieren.
Was stimmt nicht mit memcached selbst?
Das memcached-Erweiterung für PHP unterstützt "out of the box" die verteilte Speicherung von Daten und Sitzungen. Der Konsistenz-Hashing-Mechanismus erlaubt eine gleichmäßige Verteilung der Daten auf mehreren Servern, indem jeder spezifische Schlüssel eindeutig einem bestimmten Server in der Gruppe zugewiesen wird. Die integrierten Failover-Mechanismen gewährleisten eine hohe Verfügbarkeit des Caching-Dienstes (aber leider, nicht).
Die Verwaltung von Sitzungen gestaltet sich etwas besser: Sie können memcached.sess_number_of_replicas, wodurch die Daten gleichzeitig auf mehreren Servern gespeichert werden. Im Fall eines Ausfalls einer memcached-Instanz werden die Daten von anderen bereitgestellt. Allerdings, wenn der Server ohne Daten wieder in Betrieb genommen wird (wie es nach einem Neustart oft der Fall ist), werden einige Schlüssel zu seinen Gunsten neu verteilt. Das bedeutet im Grunde Datenverlust der Sitzung, da es nicht möglich ist, im Falle eines Fehlers auf eine andere Replik zuzugreifen.
Die Standardmittel der Bibliothek sind hauptsächlich auf horizontale Skalierung: Sie ermöglichen es, den Cache auf riesige Größen zu erhöhen und auf ihn aus Code zuzugreifen, der auf verschiedenen Servern gehostet ist. In unserer Situation überschreitet das Volumen der gespeicherten Daten jedoch nur einige Gigabyte, und die Leistung von ein oder zwei Knoten reicht vollkommen aus. Folglich könnten die bereitgestellten Standardmittel lediglich die Verfügbarkeit von memcached gewährleisten, indem sie mindestens eine Instanz des Caches in einem betriebsbereiten Zustand halten. Allerdings war es uns nicht möglich, diese Möglichkeit zu nutzen... Hier sei auf das Alter des im Projekt verwendeten Frameworks hinzuweisen, das es unmöglich machte, die Anwendung mit einem Server-Pool zum Laufen zu bringen. Auch die Datenverluste bei Sitzungen sollten wir nicht vergessen: Das massenhafte Abmelden von Benutzern brachte die Augen des Kunden zum Zucken.
Idealerweise erforderte dies die Replikation von Datensätzen in memcached und das Umgehen von Replikaten im Falle eines Fehlers oder Misserfolgs. Diese Strategie wurde uns durch .
mcrouter
ermöglicht, einem Router für memcached, der von Facebook entwickelt wurde, um deren Probleme zu lösen. Er unterstützt das textbasierte Protokoll von memcached, das es ermöglicht, memcached-Installationen zu skalieren. bis zu absurden Größen. Eine detaillierte Beschreibung von mcrouter finden Sie in . Neben weiteren kann er das, was wir brauchen:
- Aufzeichnungen replizieren;
- auf andere Server der Gruppe im Falle eines Fehlers fallbacken.
Legen wir los!
Konfiguration von mcrouter
Ich komme 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 wiederholen sich die Server? Lassen Sie uns verstehen, wie das funktioniert.
- In dieser Konfiguration wählt mcrouter den Pfad, über den die Anfrage gesendet wird, basierend auf dem Befehl der Anfrage. Das sagt ihm der Typ
OperationSelectorRoute. - GET-Anfragen gelangen zum Handler
RandomRoute, der zufällig einen Pool oder eine Route aus den Objekten des Arrayschildren. Jedes Element dieses Arrays ist wiederum ein HandlerMissFailoverRoute, der jeden Server im Pool durchläuft, bis er eine Antwort mit den Daten erhält, die an den Kunden zurückgegeben wird. - Wenn wir ausschließlich
MissFailoverRoutemit einem Pool von drei Servern arbeiten würden, würden alle Anfragen zuerst an die erste Instanz von memcached gesendet, und die anderen würden an die Reihe kommen, wenn die Daten fehlen. Dieser Ansatz würde zu einer übermäßigen Belastung des ersten Servers in der Liste führen, deshalb wurde beschlossen, drei Pools mit Adressen in unterschiedlicher Reihenfolge zu generieren und diese zufällig auszuwählen. - Alle anderen Anfragen (also Schreibvorgänge) werden über
AllMajorityRouteverarbeitet. Dieser Handler sendet Anfragen an alle Server im Pool und wartet auf Antworten von mindestens N/2 + 1 von ihnen. Von der Verwendung vonAllSyncRoutefür Schreiboperationen musste abgesehen werden, da diese Methode eine positive Antwort von alle den Servern der Gruppe erfordert – andernfalls gibt esSERVER_ERROR. Obwohl mcrouter die Daten in verfügbare Caches speichert, gibt die aufrufende PHP-Funktion einen Fehler zurück und erzeugt eine Mitteilung.AllMajorityRouteist nicht so streng und erlaubt es, bis zur Hälfte der Knoten außer Betrieb zu nehmen, ohne die oben beschriebenen Probleme.
Der Hauptnachteil Das Problem dieses Schemas ist, dass, wenn die Daten im Cache tatsächlich nicht vorhanden sind, für jede Anfrage des Clients effektiv N Anfragen an memcached durchgeführt werden müssen — an alle die Server im Pool. Man kann die Anzahl der Server in den Pools auf beispielsweise zwei reduzieren: indem man die Zuverlässigkeit der Speicherung opfert, gewinnen wir angrößtenGeschwindigkeit und verringern die Last durch Anfragen an nicht vorhandene Schlüssel.
NB: Nützliche Links, um mcrouter zu lernen, könnten auch und (einschließlich der geschlossenen), die eine Schatztruhe verschiedener Konfigurationen darstellen.
Bau und Start von mcrouter
Die Anwendung (und memcached selbst) läuft bei uns in Kubernetes — dementsprechend ist dort auch der Platz für mcrouter. Für den Bau des Containers verwenden wir , wobei die Konfiguration so aussehen wird:
NB: Die in diesem Artikel aufgeführten Listings sind im Repository .
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: Installieren von Voraussetzungen
apt:
name: [ 'apt-transport-https', 'tzdata', 'locales' ]
update_cache: yes
- name: Füge mcrouter APT-Schlüssel hinzu
apt_key:
url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
- name: Füge mcrouter-Repo hinzu
apt_repository:
repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
filename: mcrouter
update_cache: yes
- name: Zeitzone festlegen
timezone:
name: "Europe/Moscow"
- name: Sicherstellen, dass eine Sprache vorhanden ist
locale_gen:
name: en_US.UTF-8
state: present
install:
- name: Installiere mcrouter
apt:
name: [ 'mcrouter' ]()
… und wir skizzieren Helm-Chart. Interessant ist, dass hier nur der Konfigurationsgenerator basierend auf der Anzahl der Replikate verfügbar ist (Wenn jemand eine prägnantere und elegantere Variante hat – teilt sie in den Kommentaren):
{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{- /* Wir fügen dem Array zwei Serverkopien hinzu: "0 1 2 0 1 2" */ -}}
{{- range until 2 -}}
{{- range $i, $_ := until $count -}}
{{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
{{- end -}}
{{- end -}}
{{- /* Durch das Array verschoben, erhalten wir N Abschnitte: "[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|poold" $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 }}
}
}
}
}()
Wir rollen 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 Fehlertextsuche ergab kein Ergebnis, jedoch war bei der Anfrage „„ ein älteres, nicht geschlossenes Problem des Projekts prominent vertreten — für das binäre Protokoll von memcached.
NB: Das ASCII-Protokoll in memcached ist langsamer als das binäre Protokoll, und die Standardmethoden zur konsistenten Schlüssel-Hashing-Funktion arbeiten nur mit dem binären Protokoll. Für diesen speziellen Fall verursacht dies jedoch keine Probleme.
Alles klar: Es bleibt nur noch, auf das ASCII-Protokoll umzuschalten, und schon funktioniert alles.... In diesem Fall hat sich jedoch die Gewohnheit, Antworten in der zu suchen, als nachteilig erwiesen. Die richtige Antwort findet man dort nicht… es sei denn, man blättert bis zum Ende, wo in der Sektion „Benutzernotizen“ die korrekte und .
Ja, die korrekte Bezeichnung der Option ist memcached.sess_binary_protocol. Diese muss deaktiviert werden, danach funktionieren die Sitzungen wieder. Es bleibt nur noch, den mcrouter-Container in das PHP-Pod zu legen!
Fazit
So haben wir das gestellte Ziel allein durch infrastrukturelle Änderungen erreicht: Das Problem mit der Ausfallsicherheit von memcached ist gelöst, die Zuverlässigkeit der Cache-Speicherung wurde erhöht. Neben den offensichtlichen Vorteilen für die Anwendung hat dies auch Spielraum für die Arbeiten an der Plattform geschaffen: Wenn alle Komponenten eine Reserve haben, wird das Leben des Administrators erheblich einfacher. Ja, diese Methode hat auch ihre Nachteile und kann wie ein „Notbehelf“ erscheinen, aber wenn sie Geld spart, das Problem vertuscht und keine neuen verursacht – warum nicht?
P.S.
Lesen Sie auch in unserem Blog:
- „Praxis mit DApp“ (am Beispiel von symfony-demo): und ;
- «».
Quelle: habr.com
