Gebruik mcrouter om memcached horizontaal te schalen

Gebruik mcrouter om memcached horizontaal te schalen

Het ontwikkelen van projecten met een hoge belasting in welke taal dan ook vereist een speciale aanpak en het gebruik van speciale tools, maar als het om applicaties in PHP gaat, kan de situatie zo verergeren dat je bijvoorbeeld moet ontwikkelen: eigen applicatieserver. In deze notitie zullen we praten over de bekende pijn bij gedistribueerde sessieopslag en datacaching in memcached en hoe we deze problemen in één “ward”-project hebben opgelost.

De held van de gelegenheid is een PHP-applicatie gebaseerd op het symfony 2.3-framework, die helemaal niet is opgenomen in de te updaten businessplannen. Naast de vrij standaard sessie-opslag werd er bij dit project volop gebruik van gemaakt "Alles in cache plaatsen" beleid in memcached: reacties op verzoeken aan de database en API-servers, verschillende vlaggen, vergrendelingen voor het synchroniseren van code-uitvoering en nog veel meer. In een dergelijke situatie wordt het uitvallen van de memcached fataal voor de werking van de applicatie. Bovendien leidt cacheverlies tot ernstige gevolgen: het DBMS begint uit zijn voegen te barsten, API-services beginnen verzoeken te verbieden, enz. Het stabiliseren van de situatie kan tientallen minuten duren, en gedurende deze tijd zal de service vreselijk traag of volledig niet beschikbaar zijn.

Wij moesten voorzien de mogelijkheid om de applicatie met weinig moeite horizontaal te schalen, d.w.z. met minimale wijzigingen in de broncode en volledige functionaliteit behouden. Maak de cache niet alleen bestand tegen fouten, maar probeer ook gegevensverlies hierdoor te minimaliseren.

Wat is er mis met memcached zelf?

Over het algemeen ondersteunt de memcached-extensie voor PHP kant-en-klare gedistribueerde gegevens- en sessieopslag. Dankzij het mechanisme voor consistente sleutel-hashing kunt u gegevens gelijkmatig over veel servers verdelen, waarbij elke specifieke sleutel op unieke wijze wordt geadresseerd aan een specifieke server uit de groep, en ingebouwde failover-tools zorgen voor een hoge beschikbaarheid van de caching-service (maar helaas geen gegevens).

Met sessieopslag gaat het iets beter: u kunt configureren memcached.sess_number_of_replicas, waardoor de gegevens op meerdere servers tegelijk worden opgeslagen en bij het uitvallen van één memcached instance de gegevens van andere worden overgedragen. Als de server echter zonder gegevens weer online komt (zoals meestal gebeurt na een herstart), zullen sommige sleutels in zijn voordeel worden herverdeeld. In feite zal dit betekenen verlies van sessiegegevens, aangezien er geen manier is om naar een andere replica te "gaan" in geval van een misser.

Standaardbibliotheektools zijn voornamelijk gericht op horizontaal Schaling: ze stellen u in staat de cache tot gigantische afmetingen te vergroten en er toegang toe te bieden via code die op verschillende servers wordt gehost. In onze situatie is het volume aan opgeslagen gegevens echter niet groter dan enkele gigabytes en zijn de prestaties van een of twee knooppunten voldoende. Dienovereenkomstig zouden de enige bruikbare standaardhulpmiddelen kunnen zijn het garanderen van de beschikbaarheid van memcached terwijl ten minste één cache-instantie in werkende staat blijft. Het was echter niet mogelijk om zelfs maar van deze mogelijkheid gebruik te maken... Hier is het de moeite waard om te herinneren aan de ouderdom van het raamwerk dat in het project werd gebruikt, en daarom was het onmogelijk om de applicatie te laten werken met een pool van servers. Laten we ook het verlies van sessiegegevens niet vergeten: het oog van de klant trilde door het massale uitloggen van gebruikers.

Idealiter was het nodig replicatie van records in memcache en het omzeilen van replica's in geval van een vergissing of vergissing. Heeft ons geholpen deze strategie te implementeren microuter.

microuter

Dit is een memcached-router die door Facebook is ontwikkeld om de problemen op te lossen. Het ondersteunt het memcached-tekstprotocol, wat dit mogelijk maakt grootschalige memcached-installaties tot krankzinnige proporties. Een gedetailleerde beschrijving van mcrouter is te vinden in deze aankondiging. Onder andere brede functionaliteit het kan doen wat we nodig hebben:

  • record kopiëren;
  • terugvallen op andere servers in de groep als er een fout optreedt.

Ter zake komen!

configuratie van de microuter

Ik ga meteen naar de configuratie:

{
 "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"
       ]
     }
   }
 }
}

Waarom drie zwembaden? Waarom worden servers herhaald? Laten we uitzoeken hoe het werkt.

  • In deze configuratie selecteert mcrouter het pad waarnaar het verzoek wordt verzonden op basis van het verzoekcommando. De man vertelt hem dit OperationSelectorRoute.
  • GET-verzoeken gaan naar de handler RandomRoutedie willekeurig een pool of route tussen arrayobjecten selecteert children. Elk element van deze array is op zijn beurt een handler MissFailoverRoute, die door elke server in de pool gaat totdat deze een antwoord met gegevens ontvangt, die naar de client worden teruggestuurd.
  • Als we uitsluitend gebruikten MissFailoverRoute met een pool van drie servers, zouden alle verzoeken als eerste bij de eerste in de cache opgeslagen instantie terechtkomen, en zou de rest op resterende basis verzoeken ontvangen als er geen gegevens zijn. Een dergelijke aanpak zou leiden tot overmatige belasting van de eerste server in de lijst, dus werd besloten om drie pools met adressen in verschillende reeksen te genereren en deze willekeurig te selecteren.
  • Alle andere verzoeken (en dit is een record) worden verwerkt met behulp van AllMajorityRoute. Deze handler verzendt verzoeken naar alle servers in de pool en wacht op antwoorden van ten minste N/2 + 1 daarvan. Van gebruik AllSyncRoute want schrijfbewerkingen moesten worden opgegeven, omdat deze methode een positief antwoord vereist van Alle servers in de groep - anders komt het terug SERVER_ERROR. Hoewel mcrouter de gegevens aan beschikbare caches zal toevoegen, blijft de aanroepende PHP-functie zal een fout retourneren en zal een kennisgeving genereren. AllMajorityRoute is niet zo streng en staat toe dat tot de helft van de eenheden buiten dienst wordt gesteld zonder de hierboven beschreven problemen.

Het grootste nadeel: Dit schema houdt in dat als er werkelijk geen gegevens in de cache zitten, er voor elk verzoek van de client daadwerkelijk N verzoeken aan memcached zullen worden uitgevoerd - om alle servers in het zwembad. We kunnen het aantal servers in pools bijvoorbeeld terugbrengen tot twee: opslagbetrouwbaarheid opofferen, krijgen weоhogere snelheid en minder belasting van verzoeken tot ontbrekende sleutels.

NB: Mogelijk vindt u ook nuttige links voor het leren van mcrouter documentatie op wiki и projectvraagstukken (inclusief gesloten), die een heel pakhuis met verschillende configuraties vertegenwoordigen.

Microuter bouwen en gebruiken

Onze applicatie (en memcached zelf) draait in Kubernetes - dienovereenkomstig bevindt mcrouter zich daar ook. Voor montage van containers we gebruiken werf, waarvan de configuratie er als volgt uit zal zien:

NB: De vermeldingen in het artikel worden gepubliceerd in de repository flant/microuter.

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)

... en schets het Helmdiagram. Het interessante is dat er alleen een configuratiegenerator is op basis van het aantal replica's (als iemand een meer laconieke en elegante optie heeft, deel deze dan in de reacties):

{{- $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-microuter.yaml)

We rollen het uit in de testomgeving en controleren:

# 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 >

Zoeken naar de tekst van de fout leverde geen resultaten op, maar voor de zoekopdracht “microuter php"Op de voorgrond stond het oudste onopgeloste probleem van het project - gebrek aan ondersteuning opgeslagen binair protocol.

NB: Het ASCII-protocol in memcached is langzamer dan het binaire protocol, en standaardmiddelen voor consistente sleutelhashing werken alleen met het binaire protocol. Maar dit levert voor een specifiek geval geen problemen op.

De truc zit in de tas: je hoeft alleen maar over te schakelen naar het ASCII-protocol en alles werkt.... In dit geval is er echter de gewoonte om naar antwoorden te zoeken documentatie op php.net een wrede grap speelde. Daar zul je het juiste antwoord niet vinden... tenzij je natuurlijk naar het einde scrollt, waar in de sectie "Door gebruiker bijgedragen notities" zal trouw zijn en ten onrechte geminacht antwoord.

Ja, de juiste optienaam is memcached.sess_binary_protocol. Het moet worden uitgeschakeld, waarna de sessies beginnen te werken. Het enige dat overblijft is om de container met mcrouter in een pod met PHP te plaatsen!

Conclusie

Met alleen maar infrastructurele veranderingen konden we het probleem dus oplossen: het probleem met de fouttolerantie in de memcache is opgelost en de betrouwbaarheid van de cacheopslag is vergroot. Naast de voor de hand liggende voordelen voor de applicatie, gaf dit manoeuvreerruimte bij het werken op het platform: wanneer alle componenten een reserve hebben, wordt het leven van de beheerder enorm vereenvoudigd. Ja, deze methode heeft ook zijn nadelen, het ziet er misschien uit als een "kruk", maar als het geld bespaart, het probleem begraaft en geen nieuwe veroorzaakt - waarom niet?

PS

Lees ook op onze blog:

Bron: www.habr.com

Voeg een reactie