
Het ontwikkelen van hoogbelaste projecten in elke programmeertaal vereist een speciale aanpak en het gebruik van specifieke hulpmiddelen, maar wanneer het gaat om toepassingen in PHP, kan de situatie zo ver verslechteren dat we bijvoorbeeld moeten ontwikkelen, . In dit artikel gaan we in op de bekende pijnpunten met gedistribueerde opslag van sessies en caching van gegevens in memcached, en hoe we deze problemen hebben opgelost in een project waar we in betrokken waren.
De hoofdrolspeler is een PHP-toepassing die is gebaseerd op het framework symfony 2.3, waarvan de upgrade niet op de agenda van de onderneming staat. Naast de vrij standaard opslag van sessies, werd in dit project volop gebruikgemaakt van de "alles cachen"-strategie in memcached: antwoorden op verzoeken aan databases en API-servers, verschillende vlaggen, blokkeringen voor synchronisatie van code-uitvoering en nog veel meer. In een dergelijke situatie kan een storing in memcached fataal zijn voor de werking van de applicatie. Bovendien leidt verlies van cache tot ernstige gevolgen: de database begint te haperen, API-services blokkeren verzoeken, enzovoort. Het stabiliseren van de situatie kan tientallen minuten duren, terwijl de service vreselijk traag wordt of zelfs helemaal niet bereikbaar is.
We moesten ervoor zorgen dat de mogelijkheid tot horizontale schaalbaarheid van de applicatie met minimale inspanning kon worden gerealiseerd,, d.w.z. met minimale wijzigingen in de oorspronkelijke code en volledige functionaliteit. We moesten de cache niet alleen foutbestendig maken, maar ook proberen de dataverliezen te minimaliseren.
Wat is er mis met memcached zelf?
In feite ondersteunt de memcached-extensie voor PHP "uit de doos" gedistribueerde opslag van gegevens en sessies. Het mechanisme van consistente hash van sleutels maakt het mogelijk om gegevens gelijkmatig over meerdere servers te verdelen door elke specifieke sleutel eenduidig aan een bepaalde server in de groep toe te wijzen, terwijl de ingebouwde failover-mechanismen zorgen voor een hoge beschikbaarheid van de cachingservice (maar, helaas, niet van de gegevens.).
Met de opslag van sessies gaat het iets beter: het is mogelijk om memcached.sess_number_of_replicas in te stellen., waardoor gegevens onmiddellijk op meerdere servers worden opgeslagen, en in het geval van een storing van één exemplaar van memcached, zullen de gegevens van andere servers worden geleverd. Als de server echter weer online komt zonder gegevens (zoals meestal het geval is na een herstart), zullen enkele sleutels in zijn voordeel opnieuw worden toegewezen. Dit zou in feite betekenen dataverlies van de sessie, omdat er geen mogelijkheid is om naar een andere replica te
De standaardtools van de bibliotheek zijn voornamelijk gericht op horizontale schaalvergroting: ze maken het mogelijk om de cache tot enorme afmetingen te vergroten en ervoor te zorgen dat deze toegankelijk is vanuit code die op verschillende servers is geplaatst. In onze situatie overschrijdt de hoeveelheid opgeslagen gegevens echter niet meer dan enkele gigabytes, en de prestaties van één of twee knooppunten zijn ruim voldoende. Derhalve zouden de standaardtools alleen de beschikbaarheid van memcached kunnen garanderen met ten minste één exemplaar van de cache in werking. Toch konden we zelfs deze mogelijkheid niet benutten... Hier moet de ouderdom van het framework dat in het project wordt gebruikt worden herinnerd, waardoor het ons niet lukte om de applicatie met een servergroep aan de praat te krijgen. We mogen ook de dataverliezen van sessies niet vergeten: bij massaal uitloggen van gebruikers kreeg de opdrachtgever de zenuwen.
Ideaal gezien was er behoefte aan replicatie van records in memcached en omzeiling van de replicas in het geval van een misser of fout. Om deze strategie te implementeren hielp ons .
mcrouter
Dit is een router voor memcached, ontwikkeld door Facebook om zijn problemen op te lossen. Het ondersteunt het tekstprotocol van memcached, waarmee memcached-installaties kunnen worden geschaald tot krankzinnige afmetingen. Een gedetailleerde beschrijving van mcrouter is te vinden in . Naast de andere kan het wat we nodig hebben:
- replicatie van records;
- fallback naar andere servers in de groep in het geval van een fout.
Aan de slag!
Configuratie van mcrouter
Ik ga direct 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 pools? Waarom worden servers herhaald? Laten we bekijken hoe dit werkt.
- In deze configuratie kiest mcrouter het pad waarlangs de aanvraag wordt verzonden op basis van het verzoektype. Dit wordt aangegeven door het type
OperationSelectorRoute. - GET-aanvragen komen terecht bij de handler
RandomRoute, die willekeurig een pool of route kiest uit de objecten van de arraychildren. Elk element van deze array is op zijn beurt een handlerMissFailoverRoute, die elk van de servers in de pool doorloopt totdat hij een antwoord met gegevens ontvangt, wat aan de client wordt teruggegeven. - Als we uitsluitend zouden werken
MissFailoverRoutemet een pool van drie servers, zouden alle aanvragen eerst naar het eerste exemplaar van memcached gaan, terwijl de anderen aanvragen zouden ontvangen volgens het resterende principe, wanneer gegevens ontbreken. Deze aanpak zou leiden tot overmatige belasting van de eerste server in de lijst, daarom is ervoor gekozen om drie pools te genereren met adressen in verschillende volgordes en ze willekeurig te kiezen. - Alle andere aanvragen (zoals schrijven) worden behandeld met behulp van
AllMajorityRoute. Deze handler verzendt aanvragen naar alle servers in de pool en wacht op antwoorden van ten minste N/2 + 1 van hen. Van het gebruik vanAllSyncRoutevoor schrijfoperaties moest worden afgezien, omdat deze methode een positieve respons vereist van van alle de servers in de groep — anders retourneert hijSERVER_ERROR. Hoewel mcrouter de gegevens in de beschikbare caches zal opslaan, zal de aanroepende PHP-functie een fout retourneren en een notice genereren.AllMajorityRouteis minder strikt en staat toe dat tot de helft van de knooppunten buiten gebruik wordt gesteld zonder de eerder beschreven problemen.
Het belangrijkste nadeel van dit schema is dat als er echt geen gegevens in de cache zijn, er voor elke aanvraag van de client feitelijk N aanvragen naar memcached worden uitgevoerd — naar met alles de servers in de pool. Het aantal servers in de pools kan worden verminderd, bijvoorbeeld tot twee: door de betrouwbaarheid van opslag op te offeren, verkrijgen we een verbeterde efficiëntie.groter publiek.een hogere snelheid en een lagere belasting van verzoeken aan ontbrekende sleutels.
NB: Nuttige links voor het bestuderen van mcrouter kunnen ook zijn en (inclusief gesloten), die een schat aan verschillende configuraties vertegenwoordigen.
Bouwen en uitvoeren van mcrouter
De applicatie (en memcached zelf) draait bij ons in Kubernetes - dus daar bevindt zich ook mcrouter. Voor het bouwen van de container maken we gebruik van , de configuratie voor waarvan er als volgt uitziet:
NB: De lijsten die in dit artikel worden gepresenteerd, zijn gepubliceerd in de 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: Installeer vereisten
apt:
name: [ 'apt-transport-https', 'tzdata', 'locales' ]
update_cache: yes
- name: Voeg mcrouter APT sleutel toe
apt_key:
url: https://facebook.github.io/mcrouter/debrepo/xenial/PUBLIC.KEY
- name: Voeg mcrouter Repo toe
apt_repository:
repo: deb https://facebook.github.io/mcrouter/debrepo/xenial xenial contrib
filename: mcrouter
update_cache: yes
- name: Stel tijdzone in
timezone:
name: "Europe/Moscow"
- name: Zorg ervoor dat een locale bestaat
locale_gen:
name: en_US.UTF-8
state: present
install:
- name: Installeer mcrouter
apt:
name: [ 'mcrouter' ]()
… en we schetsen Helm chart. Interessant is dat hier alleen de config-generator van het aantal replicas is (als iemand een meer beknopte en elegante variant heeft - deel deze in de opmerkingen):
{{- $count := (pluck .Values.global.env .Values.memcached.replicas | first | default .Values.memcached.replicas._default | int) -}}
{{- $pools := dict -}}
{{- $servers := list -}}
{{- 3 Fill the array with two copies of servers: "0 1 2 0 1 2" * / -}}
{{- range until 2 -}}
{{- range $i, $_ := until $count -}}
{{- $servers = append $servers (printf "mc-%d.mc:11211" $i) -}}
{{- end -}}
{{- end -}}
{{- 3 Shifting through the array, we get N slices: "[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 }}
}
}
}
}()
We rollen het uit naar 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 >Een zoekopdracht naar de fouttekst gaf geen resultaten, maar op het verzoek "" stond het oudste open probleem van het project bovenaan — het binaire protocol van memcached.
NB: Het ASCII-protocol in memcached is trager dan het binaire, en ook de standaardmethoden voor consistente hashing van sleutels werken alleen met het binaire protocol. Maar dit creëert geen problemen voor de specifieke case.
Dat is geregeld: we hoeven alleen maar over te schakelen naar het ASCII-protocol en alles werkt weer.... Echter, in dit geval heeft de gewoonte om antwoorden te zoeken in een slechte grap gespeeld. Het juiste antwoord zul je daar niet vinden… tenzij je natuurlijk helemaal naar beneden scrollt, waar in de sectie «User contributed notes» het juiste en .
Ja, de correcte naam van de optie is memcached.sess_binary_protocol. Deze moet je uitschakelen, waarna de sessies weer zullen werken. Je hoeft alleen maar de container met mcrouter in de pod met PHP te plaatsen!
Conclusie
Op deze manier hebben we met alleen infrastructurele wijzigingen de gestelde taak kunnen oplossen: de kwestie van de fouttolerantie van memcached is opgelost, de betrouwbaarheid van de cacheopslag is verhoogd. Naast de duidelijke voordelen voor de applicatie biedt dit ook ruimte voor manoeuvre tijdens het werken aan het platform: wanneer alle componenten een reserve hebben, wordt het leven van de beheerder veel eenvoudiger. Ja, deze methode heeft ook zijn nadelen en kan eruitzien als een 'oplossing uit een nood' maar als het geld bespaart, het probleem verbergt en geen nieuwe problemen veroorzaakt — waarom niet?
P.S.
Lees ook op onze blog:
- «Praktijk met dapp» (aan de hand van symfony-demo): en ;
- «».
Bron: habr.com
