Utilizziamo mcrouter per la scalabilità orizzontale di memcached

Utilizziamo mcrouter per la scalabilità orizzontale di memcached

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, un proprio server per applicazioni. 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.

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 questo annuncio. Oltre ad altre ampia funzionalità 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'array children. Ogni elemento di questo array è a sua volta un gestore MissFailoverRoute, che esamina ogni server nella pool fino a ricevere una risposta con i dati, che verrà restituita al cliente.
  • Se avessimo utilizzato esclusivamente MissFailoverRoute una 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 di AllSyncRoute per le operazioni di scrittura, poiché questo metodo richiede una risposta positiva da parte tutti dei server del gruppo — altrimenti restituisce SERVER_ERROR. Anche se mcrouter memorizza i dati nella cache disponibili, la funzione PHP chiamante restituirà un errore e genererà un avviso. AllMajorityRoute non è 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 la documentazione su wiki e i problemi del progetto (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 werf, la configurazione apparirà come segue:

NB: Le liste riportate nell'articolo sono pubblicate nel repository flant/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: 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' ]

(werf.yaml)

… 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 }}
 }
 }
 }
 }

(10-mcrouter.yaml)

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 «mcrouter php» nelle prime posizioni c'era il problema più antico non risolto del progetto — mancanza di supporto 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 documentazione su php.net 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 ingiustamente bocciata.

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:

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster