Utilizziamo mcrouter per la scalabilità orizzontale di memcached

Utilizziamo mcrouter per la scalabilità orizzontale di memcached

Lo sviluppo di progetti ad alta intensità su qualsiasi linguaggio richiede un approccio particolare e l'uso di strumenti specializzati, ma quando si parla di applicazioni PHP, la situazione può diventare così critica che è necessario sviluppare, ad esempio, un proprio server applicativo. In questo articolo parleremo di un problema ben noto riguardante la memorizzazione distribuita delle sessioni e la memorizzazione nella cache dei dati in memcached, e di come abbiamo affrontato queste sfide in un progetto 'sotto osservazione'.

Il protagonista è un'applicazione PHP basata sul framework symfony 2.3, il cui aggiornamento non è affatto nelle intenzioni del business. Oltre alla gestione standard delle sessioni, in questo progetto si stava applicando una politica di 'caching totale' in memcached: risposte a richieste ai database e server API, vari flag, lock per sincronizzare l'esecuzione del codice e molto altro. In una tale situazione, il malfunzionamento di memcached diventa fatale per il funzionamento dell'applicazione. Inoltre, la perdita della cache porta a conseguenze gravi: il DBMS inizia a dare segni di cedimento, i servizi API — a bloccare le richieste, ecc. Stabilizzare la situazione può richiedere decine di minuti, durante i quali il servizio rallenterà drasticamente o diventerà completamente non disponibile.

Abbiamo dovuto garantire la possibilità di scalare orizzontalmente l'applicazione senza grandi sforzi, 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 la perdita di dati da essa.

Cosa c'è che non va in memcached?

In realtà, l'estensione memcached per PHP supporta 'nativamente' lo storage distribuito di dati e sessioni. Il meccanismo di hashing consistente delle chiavi consente una distribuzione uniforme dei dati su più server, indirizzando in modo univoco ogni chiave specifica a un determinato server del gruppo. I sistemi di failover integrati garantiscono un'elevata disponibilità del servizio di caching (ma, sfortunatamente, non dei dati).

La gestione delle sessioni è un po' migliore: è possibile configurare memcached.sess_number_of_replicas, il che significa che i dati verranno memorizzati su più server contemporaneamente e, in caso di guasto di un'istanza di memcached, i dati saranno forniti da altri. Tuttavia, se il server torna online senza dati (come di solito accade dopo un riavvio), parte delle chiavi verrà redistribuita a suo favore. In effetti, questo significherà perdita dei dati di sessione, poiché non c'è modo di 'recuperare' un'altra replica in caso di errore.

Gli strumenti standard della libreria sono principalmente orientati a livello orizzontale scalabilità: consentono di aumentare la cache a dimensioni gigantesche e di accedervi da codice ospitato su diversi server. Tuttavia, nella nostra situazione, la quantità di dati memorizzati non supera alcuni gigabyte, e le prestazioni di uno o due nodi sono più che sufficienti. Pertanto, tra le opzioni utili, gli strumenti standard potrebbero garantire la disponibilità di memcached mantenendo almeno un'istanza della cache in stato operativo. Tuttavia, non siamo riusciti nemmeno a sfruttare questa possibilità… È importante ricordare l'età del framework utilizzato nel progetto, che ha reso difficile far funzionare l'applicazione con un pool di server. Non dimentichiamo neanche le perdite di dati delle sessioni: il cliente ha vissuto un aumento dei disconnessioni degli utenti.

Ideale era necessario replicazione dei dati in memcached e bypass delle repliche in caso di errore o di mancanza. Questa strategia è stata supportata da mcrouter.

mcrouter

Questo è un router per memcached, sviluppato da Facebook per risolvere i propri problemi. Supporta il protocollo testuale di memcached, che consente di scalare le installazioni di memcached 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 la registrazione;
  • effettuare il fallback su altri server del gruppo in caso di errore.

Iniziamo!

Configurazione di mcrouter

Passo subito 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? Vediamo come funziona.

  • In questa configurazione, mcrouter sceglie il percorso attraverso il quale inviare la richiesta in base al comando della richiesta. Questo è indicato dal tipo OperationSelectorRoute.
  • Le richieste GET vengono elaborate da RandomRoute, che seleziona casualmente un pool o un percorso tra gli oggetti dell'array children. Ogni elemento di questo array è a sua volta un gestore MissFailoverRoute, che controllerà ogni server nel pool fino a ricevere una risposta con i dati, che sarà restituita al cliente.
  • Se utilizzassimo esclusivamente MissFailoverRoute un pool di tre server, tutte le richieste andrebbero prima al primo esemplare di memcached, mentre gli altri riceverebbero le richieste in base alla disponibilità dei dati. Questo approccio comporterebbe un carico eccessivo sul primo server della lista, quindi è stata presa la decisione di generare tre pool con indirizzi in sequenze diverse e selezionarli in modo casuale.
  • Tutte le altre richieste (che sono scritture) vengono gestite tramite AllMajorityRoute. Questo gestore invia richieste a tutti i server del pool e attende risposte da almeno N/2 + 1 di essi. L'uso di AllSyncRoute per le operazioni di scrittura è stato abbandonato, poiché questo metodo richiede una risposta positiva da parte tutti dei server del gruppo — altrimenti restituirà SERVER_ERROR. Anche se mcrouter memorizza i dati nelle cache disponibili, la funzione PHP chiamante restituirà un errore e genererà un avviso. AllMajorityRoute è meno rigoroso e consente di escludere fino alla metà dei nodi senza i problemi descritti sopra.

Il principale svantaggio Il problema di questo schema è che, se non ci sono dati nella cache, per ogni richiesta del cliente verranno effettivamente eseguite N richieste a memcached — ai di tutto server nel pool. Possiamo ridurre il numero di server nei pool, ad esempio, a due: sacrificando l'affidabilità della memorizzazione, otterremo unaunmaggiore velocità e minore carico dalle richieste su chiavi assenti.

NB: Collegamenti utili per studiare mcrouter potrebbero essere la documentazione nella wiki e le issue del progetto (incluse quelle chiuse), che rappresentano un vero tesoro di varie configurazioni.

Compilazione e avvio di mcrouter

L'applicazione (e lo stesso memcached) funzionano su Kubernetes — di conseguenza, anche mcrouter è lì. Per compilare il contenitore utilizziamo werf, la configurazione per il quale sarà simile a questa:

NB: Gli elenchi presentati nell'articolo sono pubblicati 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: Installa i prerequisiti
   apt:
     name: [ 'apt-transport-https', 'tzdata', 'locales' ]
     update_cache: yes
 - name: Aggiungi la chiave APT di mcrouter
   apt_key:
     url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
 - name: Aggiungi il repository di mcrouter
   apt_repository:
     repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
     filename: mcrouter
     update_cache: yes
 - name: Imposta il fuso orario
   timezone:
     name: "Europe/Moscow"
 - name: Assicurati che esista una locale
   locale_gen:
     name: en_US.UTF-8
     state: present
 install:
 - name: Installa mcrouter
   apt:
     name: [ 'mcrouter' ]

(werf.yaml)

… e annotiamo Chart Helm. Di interessante — qui c'è solo il generatore di configurazione basato sul numero di repliche (se qualcuno ha una versione più concisa ed elegante, condividetela nei commenti):

{{- $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)

Distribuiamo nell'ambiente di test e controlliamo:

# 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 testo dell'errore non ha dato risultati, ma la richiesta «mcrouter php» nei primi risultati ha mostrato il problema più antico non risolto del progetto — mancanza di supporto del protocollo binario memcached.

NB: il protocollo ASCII in memcached è più lento di quello binario, e anche gli strumenti standard per l'hashing dei key funzionano solo con il protocollo binario. Ma questo non crea problemi per il caso specifico.

Tutto è a posto: resta solo da 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 scherzo. La risposta corretta non la troverete lì... a meno che, ovviamente, non scendiate in fondo, dove nella sezione «Note contribuite dagli utenti» troverete la risposta giusta e ingiustamente sottovalutata..

Sì, il nome corretto dell'opzione è memcached.sess_binary_protocol. Deve essere disattivata, dopodiché le sessioni inizieranno a funzionare. Rimane solo da posizionare il contenitore con mcrouter nel pod con PHP!

Conclusione

In questo modo, con solo modifiche all'infrastruttura, siamo riusciti a risolvere la questione: il problema con la tolleranza ai guasti di memcached è risolto, l'affidabilità dello storage della cache è aumentata. Oltre ai vantaggi evidenti per l'applicazione, questo ha fornito spazio di manovra durante i lavori sulla piattaforma: quando tutti i componenti hanno un backup, la vita dell'amministratore diventa molto più semplice. Sì, questo metodo ha anche i suoi svantaggi, potrebbe sembrare un "palliativo", ma se fa risparmiare soldi, risolve il problema e non genera nuovi problemi, perché no?

P.S.

Leggete 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