
Lo sviluppo di progetti ad alta domanda in qualsiasi linguaggio richiede un approccio particolare e l'uso di strumenti specializzati, ma quando si tratta di applicazioni PHP, la situazione può aggravarsi a tal punto che bisogna sviluppare, ad esempio, . In questo articolo parleremo di un problema noto a tutti relativo allo stoccaggio distribuito delle sessioni e alla memorizzazione nella cache dei dati in memcached, e di come abbiamo risolto questi problemi in un progetto "sotto osservazione".
Il protagonista è un'applicazione PHP basata sul framework Symfony 2.3, il cui aggiornamento non rientra nei piani aziendali. Oltre al classico stoccaggio delle sessioni, in questo progetto era in atto una politica di "caching di tutto" in memcached: risposte alle richieste ai database e ai server API, vari flag, blocchi per la sincronizzazione dell'esecuzione del codice e molto altro. In questa situazione, un malfunzionamento di memcached diventa fatale per il funzionamento dell'applicazione. Inoltre, la perdita della cache porta a conseguenze gravi: il DBMS inizia a dare segnali di crisi, i servizi API bloccano le richieste, ecc. La stabilizzazione della situazione può richiedere decine di minuti, durante i quali il servizio rallenta drammaticamente o diventa del tutto inaccessibile.
Abbiamo dovuto garantire la possibilità di scalare horizontalmente l'applicazione con il minor sforzo possibile, cioè con modifiche minime al codice sorgente e mantenendo completamente la funzionalità. Rendere la cache non solo resistente ai guasti, ma anche cercare di minimizzare le perdite di dati da essa.
Cosa c'è che non va con il memcached stesso?
In generale, l'estensione memcached per PHP "out of the box" supporta lo stoccaggio distribuito di dati e sessioni. Il meccanismo di hashing consistente delle chiavi consente di distribuire i dati uniformemente su più server, indirizzando in modo univoco ogni chiave specifica a un determinato server del gruppo, mentre gli strumenti integrati di failover garantiscono un'elevata disponibilità del servizio di caching (ma, sfortunatamente, non dei dati).
. La gestione delle sessioni va leggermente meglio: è possibile configurare memcached.sess_number_of_replicas, di conseguenza i dati verranno memorizzati su più server, e in caso di guasto di un'istanza di memcached, i dati verranno restituiti da altri. Tuttavia, se il server tornerà operativo senza dati (cosa che accade di solito dopo un riavvio), parte delle chiavi verrà riassegnata a suo favore. In effetti, ciò significherà perdita dei dati di sessione, poiché non è possibile 'andare' in un'altra replica in caso di errore.
Gli strumenti standard della libreria sono principalmente indirizzati a scalabilità : consentono di espandere la cache a dimensioni enormi e di accedervi da codice distribuito su server diversi. Tuttavia, nella nostra situazione, il volume dei dati memorizzati non supera i pochi gigabyte, e le prestazioni di uno o due nodi sono più che sufficienti. Pertanto, le uniche funzionalità utili che gli strumenti standard potrebbero fornire sarebbero garantire l'accessibilità di memcached mantenendo almeno un'istanza della cache in uno stato operativo. Tuttavia, non siamo riusciti nemmeno a sfruttare questa possibilità... Vale la pena ricordare l'antichità del framework utilizzato nel progetto, per cui non siamo riusciti a far funzionare l'applicazione con un pool di server. Non dimentichiamo nemmeno la perdita dei dati di sessione: il cliente ha avvertito un ripetuto disconnettersi degli utenti, il che è stato piuttosto fastidioso.
In un'ideale era necessaria la replicazione della scrittura in memcached e il bypass delle repliche in caso di errore o fallimento. Per realizzare questa strategia ci ha aiutato .
mcrouter
Questo è un router per memcached, sviluppato da Facebook per risolvere i propri problemi. Supporta il protocollo testuale di memcached, che consente di escalare le installazioni di memcached fino a dimensioni incredibili. Una descrizione dettagliata di mcrouter può essere trovata in . Oltre ad altre può fare ciò di cui abbiamo bisogno:
- replicare le scritture;
- fare fallback su altri server del gruppo in caso di errore.
Mettiamoci al lavoro!
Configurazione di mcrouter
Passo direttamente alla configurazione:
{
"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"
]
}
}
}
}Perché tre pool? Perché i server si ripetono? Analizziamo come funziona.
- In questa configurazione, mcrouter seleziona il percorso attraverso il quale verrà inviato il riferimento in base al comando della richiesta. Questo è indicato dal tipo
OperationSelectorRoute. - Le richieste GET vengono gestite da
RandomRoute, che seleziona casualmente una pool o un percorso tra gli oggetti dell'arraychildren. Ogni elemento di questo array è a sua volta un gestoreMissFailoverRoute, che esamina ogni server nella pool fino a ricevere una risposta con i dati, che verrà restituita al cliente. - Se avessimo utilizzato esclusivamente
MissFailoverRouteuna pool di tre server, tutte le richieste arriverebbero prima al primo esemplare di memcached, mentre gli altri ricevono richieste in base alla disponibilità dei dati. Questo approccio porterebbe a un'eccessiva carica sul primo server della lista, quindi è stata presa la decisione di generare tre pool con indirizzi in sequenze diverse e sceglierli casualmente. - Tutte le altre richieste (cioè le operazioni di scrittura) vengono elaborate tramite
AllMajorityRoute. Questo gestore invia le richieste a tutti i server della pool e attende risposte da almeno N/2 + 1 di essi. È stato necessario rinunciare all'uso diAllSyncRouteper le operazioni di scrittura, poiché questo metodo richiede una risposta positiva da parte tutti dei server del gruppo — altrimenti restituisceSERVER_ERROR. Anche se mcrouter memorizza i dati nella cache disponibili, la funzione PHP chiamante restituirà un errore e genererà un avviso.AllMajorityRoutenon è così rigido e consente di escludere fino alla metà dei nodi senza i problemi sopra descritti.
Il principale svantaggio di questo schema è che se i dati non sono realmente presenti nella cache, per ogni richiesta del cliente verranno effettivamente eseguiti N richieste verso memcached — ai di tutto server nella pool. È possibile ridurre il numero di server nelle pool, ad esempio, a due: sacrificando l'affidabilità dell'archiviazione, otterremo bununa maggiore velocità e un minore carico dalle richieste a chiavi mancanti.
NB: I collegamenti utili per studiare mcrouter possono includere anche e (inclusi quelli chiusi), che rappresentano un vero tesoro di varie configurazioni.
Compilazione e avvio di mcrouter
L'applicazione (e lo stesso memcached) funziona in Kubernetes - pertanto, mcrouter si trova lì. Per la compilazione del contenitore utilizziamo , la configurazione apparirà come segue:
NB: Le liste riportate nell'articolo sono pubblicate nel 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: Installare i requisiti
apt:
name: [ 'apt-transport-https', 'tzdata', 'locales' ]
update_cache: yes
- name: Aggiungere la chiave APT di mcrouter
apt_key:
url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
- name: Aggiungere il repository di mcrouter
apt_repository:
repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
filename: mcrouter
update_cache: yes
- name: Impostare il fuso orario
timezone:
name: "Europe/Moscow"
- name: Assicurarsi che esista una località
locale_gen:
name: en_US.UTF-8
state: present
install:
- name: Installare mcrouter
apt:
name: [ 'mcrouter' ]()
… e stiamo abbozzando Helm-chart. Della cosa interessante — qui c'è solo il generatore di configurazione in base al numero di repliche (se qualcuno ha una versione più concisa ed elegante — condividete nei commenti):
{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{- \/\* Riempiamo l'array con due copie dei server: "0 1 2 0 1 2" \/\* -}}
{{- range until 2 -}}
{{- range $i, $_ := until $count -}}
{{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
{{- end -}}
{{- end -}}
{{- \/\* Spostando l'array, otteniamo N sezioni: "[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 }}
}
}
}
}()
Implementiamo nell'ambiente di test e verifichiamo:
# 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 >La ricerca del messaggio di errore non ha prodotto risultati, tuttavia con la query «» nelle prime posizioni c'era il problema più antico non risolto del progetto — per il protocollo binario di memcached.
NB: Il protocollo ASCII in memcached è più lento rispetto a quello binario e gli strumenti standard di hashing coerente delle chiavi funzionano solo con il protocollo binario. Tuttavia, questo non crea problemi per il caso specifico.
Il dado è tratto: basta passare al protocollo ASCII e tutto funzionerà… Tuttavia, in questo caso l'abitudine di cercare risposte nella ha giocato un brutto tiro. Non troverai la risposta giusta lì… a meno che tu non scorra fino in fondo, dove nella sezione «Note contribute dagli utenti» troverai la risposta corretta e .
Sì, il nome corretto dell'opzione è memcached.sess_binary_protocol. Deve essere disattivato, dopodiché le sessioni inizieranno a funzionare. Resta solo da posizionare il contenitore con mcrouter nel pod con PHP!
Conclusione
In questo modo, con sole modifiche infrastrutturali siamo riusciti a risolvere il compito assegnato: il problema della tolleranza agli errori di memcached è stato risolto, l'affidabilità dello storage della cache è aumentata. Oltre ai vantaggi evidenti per l'applicazione, questo ha dato spazio di manovra nel lavoro sulla piattaforma: quando tutti i componenti hanno un backup, la vita dell'amministratore è notevolmente semplificata. Sì, questo metodo ha i suoi svantaggi, può sembrare un 'palliativo', ma se risparmia denaro, nasconde il problema e non porta a nuove complicazioni — perché no?
P.S.
Leggi anche nel nostro blog:
- «Pratica con dapp» (esempio di symfony-demo): e ;
- «».
Fonte: habr.com
