Utilizzo di mcrouter per ridimensionare memcached orizzontalmente

Utilizzo di mcrouter per ridimensionare memcached orizzontalmente

Lo sviluppo di progetti ad alto carico in qualsiasi lingua richiede un approccio speciale e l'uso di strumenti speciali, ma quando si tratta di applicazioni in PHP, la situazione può diventare così aggravata da dover sviluppare, ad esempio, proprio server delle applicazioni. In questa nota parleremo dei problemi familiari legati all'archiviazione di sessioni distribuite e alla memorizzazione nella cache dei dati in memcached e di come abbiamo risolto questi problemi in un progetto "reparto".

L'eroe dell'occasione è un'applicazione PHP basata sul framework symfony 2.3, che non è affatto inclusa nei piani aziendali da aggiornare. Oltre all'archiviazione di sessioni abbastanza standard, questo progetto ha sfruttato appieno Politica di "memorizzare tutto nella cache". in memcached: risposte alle richieste al database e ai server API, vari flag, blocchi per la sincronizzazione dell'esecuzione del codice e molto altro. In una situazione del genere, un guasto di memcached diventa fatale per il funzionamento dell'applicazione. Inoltre, la perdita della cache porta a gravi conseguenze: il DBMS inizia a scoppiare, i servizi API iniziano a vietare le richieste, ecc. La stabilizzazione della situazione potrebbe richiedere decine di minuti e durante questo periodo il servizio sarà terribilmente lento o completamente non disponibile.

Avevamo bisogno di fornire la possibilità di scalare orizzontalmente l'applicazione con poco sforzo, cioè. con modifiche minime al codice sorgente e piena funzionalità preservata. Rendi la cache non solo resistente ai guasti, ma cerca anche di minimizzare la perdita di dati da essa.

Cosa c'è di sbagliato in memcached stesso?

In generale, l'estensione memcached per PHP supporta immediatamente i dati distribuiti e l'archiviazione delle sessioni. Il meccanismo per l'hashing delle chiavi coerente consente di posizionare uniformemente i dati su molti server, indirizzando in modo univoco ciascuna chiave specifica a un server specifico del gruppo, e gli strumenti di failover integrati garantiscono un'elevata disponibilità del servizio di caching (ma, sfortunatamente, nessun dato).

Le cose vanno un po' meglio con l'archiviazione delle sessioni: puoi configurare memcached.sess_number_of_replicas, a seguito della quale i dati verranno archiviati su più server contemporaneamente e, in caso di guasto di un'istanza di memcached, i dati verranno trasferiti da altri. Tuttavia, se il server torna online senza dati (come di solito accade dopo un riavvio), alcune chiavi verranno ridistribuite a suo favore. In effetti questo significherà perdita dei dati della sessione, poiché non è possibile “andare” su un'altra replica in caso di errore.

Gli strumenti di libreria standard sono rivolti principalmente a orizzontale ridimensionamento: consentono di aumentare la cache fino a dimensioni gigantesche e di fornirne l'accesso da codice ospitato su server diversi. Tuttavia, nella nostra situazione, il volume dei dati archiviati non supera diversi gigabyte e le prestazioni di uno o due nodi sono sufficienti. Di conseguenza, gli unici strumenti standard utili potrebbero essere quelli di garantire la disponibilità di memcached mantenendo almeno un'istanza della cache funzionante. Tuttavia, non è stato possibile sfruttare nemmeno questa opportunità... Qui vale la pena ricordare l'antichità del framework utilizzato nel progetto, motivo per cui era impossibile far funzionare l'applicazione con un pool di server. Non dimentichiamoci inoltre della perdita dei dati di sessione: gli occhi del cliente si muovevano per il massiccio logout degli utenti.

Idealmente era necessario replica dei record in memcached e bypassando le repliche in caso di errore o sbaglio. Ci ha aiutato a implementare questa strategia mcrouter.

mcrouter

Questo è un router memcached sviluppato da Facebook per risolvere i suoi problemi. Supporta il protocollo di testo memcached, che consente installazioni memcached su larga scala a proporzioni folli. Una descrizione dettagliata di mcrouter può essere trovata in questo annuncio. Tra le altre cose ampia funzionalità può fare ciò di cui abbiamo bisogno:

  • replicare il record;
  • eseguire il fallback su altri server del gruppo se si verifica un errore.

Per la causa!

configurazione del microuter

Vado 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 piscine? Perché i server vengono ripetuti? Scopriamo come funziona.

  • In questa configurazione, mcrouter seleziona il percorso a cui verrà inviata la richiesta in base al comando request. Il ragazzo glielo dice OperationSelectorRoute.
  • Le richieste GET vanno al gestore RandomRouteche 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 passerà attraverso ciascun server nel pool finché non riceverà una risposta con i dati, che verranno restituiti al client.
  • Se usassimo esclusivamente MissFailoverRoute con un pool di tre server, tutte le richieste arriverebbero prima alla prima istanza memcached e il resto riceverebbe richieste su base residua quando non ci sono dati. Un simile approccio porterebbe a carico eccessivo sul primo server nell'elenco, quindi si è deciso di generare tre pool con indirizzi in sequenze diverse e selezionarli in modo casuale.
  • Tutte le altre richieste (e questo è un record) vengono elaborate utilizzando AllMajorityRoute. Questo gestore invia richieste a tutti i server nel pool e attende risposte da almeno N/2 + 1 di essi. Dall'uso AllSyncRoute per le operazioni di scrittura è stato necessario abbandonare, poiché questo metodo richiede una risposta positiva da parte tutti server nel gruppo, altrimenti verrà restituito SERVER_ERROR. Sebbene mcrouter aggiungerà i dati alle cache disponibili, la funzione PHP chiamante restituirà un errore e genererà un avviso. AllMajorityRoute non è così rigido e consente di mettere fuori servizio fino alla metà delle unità senza i problemi sopra descritti.

Meno negativo Questo schema prevede che se in realtà non ci sono dati nella cache, allora per ogni richiesta del client verranno effettivamente eseguite N richieste a memcached - per tutti server nel pool. Possiamo ridurre il numero di server nei pool, ad esempio, a due: sacrificando l'affidabilità dello storage, otteniamoоmaggiore velocità e minor carico dalle richieste alle chiavi mancanti.

NB: Potresti anche trovare link utili per imparare mcrouter documentazione su wiki и questioni di progetto (compresi quelli chiusi), che rappresentano un intero magazzino di varie configurazioni.

Costruire ed eseguire mcrouter

La nostra applicazione (e lo stesso memcached) viene eseguita in Kubernetes, di conseguenza anche mcrouter si trova lì. Per assemblaggio del contenitore noi usiamo werf, la cui configurazione sarà simile a questa:

NB: Gli elenchi forniti nell'articolo sono pubblicati nel repository piatto/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)

...e disegnalo Grafico del timone. La cosa interessante è che esiste solo un generatore di configurazione basato sul numero di repliche (se qualcuno ha un'opzione più laconica ed elegante, condividila 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)

Lo implementiamo 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 per il testo dell'errore non ha dato risultati, ma per la query “microuter php"In primo piano c'era il più antico problema irrisolto del progetto - mancanza di supporto protocollo binario memcached.

NB: Il protocollo ASCII in memcached è più lento di quello binario e i mezzi standard di hashing delle chiavi coerenti funzionano solo con il protocollo binario. Ma questo non crea problemi per un caso specifico.

Il trucco è nella borsa: basta passare al protocollo ASCII e tutto funzionerà.... Tuttavia, in questo caso, l’abitudine di cercare risposte documentazione su php.net fatto uno scherzo crudele. Non troverai la risposta corretta lì... a meno che, ovviamente, non scorri fino alla fine, dove nella sezione "Note fornite dall'utente" sarà fedele e risposta ingiustamente sottovalutata.

Sì, il nome dell'opzione corretto è memcached.sess_binary_protocol. Deve essere disabilitato, dopodiché le sessioni inizieranno a funzionare. Non resta che inserire il contenitore con mcrouter in un pod con PHP!

conclusione

Pertanto, con solo modifiche infrastrutturali siamo riusciti a risolvere il problema: il problema con la tolleranza agli errori di memcached è stato risolto e l'affidabilità dell'archiviazione della cache è stata aumentata. Oltre agli ovvi vantaggi per l'applicazione, questo ha dato spazio di manovra quando si lavora sulla piattaforma: quando tutti i componenti hanno una riserva, la vita dell'amministratore è notevolmente semplificata. Sì, questo metodo ha anche i suoi svantaggi, può sembrare una "stampella", ma se fa risparmiare denaro, seppellisce il problema e non ne causa di nuovi, perché no?

PS

Leggi anche sul nostro blog:

Fonte: habr.com

Aggiungi un commento