
Att utveckla högbelastningsprojekt på vilket språk som helst kräver ett speciellt förhållningssätt och användning av specialverktyg, men när det kommer till applikationer i PHP kan situationen bli så förvärrad att man måste utveckla t.ex. . I den här anteckningen kommer vi att prata om den välbekanta smärtan med distribuerad sessionslagring och datacache i memcached och hur vi löste dessa problem i ett "ward"-projekt.
Tillfällets hjälte är en PHP-applikation baserad på ramverket symfony 2.3, som inte alls ingår i affärsplanerna att uppdatera. Förutom ganska standard sessionslagring, utnyttjade detta projekt fullt ut "cache allt" policy i memcached: svar på förfrågningar till databasen och API-servrar, olika flaggor, lås för synkronisering av kodexekvering och mycket mer. I en sådan situation blir ett sammanbrott av memcached ödesdigert för applikationens funktion. Dessutom leder cacheförlust till allvarliga konsekvenser: DBMS börjar spricka i sömmarna, API-tjänster börjar förbjuda förfrågningar, etc. Att stabilisera situationen kan ta tiotals minuter, och under denna tid kommer tjänsten att vara fruktansvärt långsam eller helt otillgänglig.
Vi behövde tillhandahålla förmågan att horisontellt skala applikationen med liten ansträngning, dvs. med minimala ändringar av källkoden och full funktionalitet bevarad. Gör cachen inte bara motståndskraftig mot fel, utan försök också minimera dataförlusten från den.
Vad är det för fel på memcached själv?
I allmänhet stöder den memcachade tillägget för PHP distribuerad data och sessionslagring direkt. Mekanismen för konsekvent nyckelhashning gör att du kan placera data jämnt på många servrar, unikt adressera varje specifik nyckel till en specifik server från gruppen, och inbyggda failover-verktyg säkerställer hög tillgänglighet för cachningstjänsten (men, tyvärr, inga data).
Saker och ting är lite bättre med sessionslagring: du kan konfigurera memcached.sess_number_of_replicas, som ett resultat av vilket data kommer att lagras på flera servrar samtidigt, och i händelse av ett fel i en memcachad instans, kommer data att överföras från andra. Men om servern kommer tillbaka online utan data (som vanligtvis händer efter en omstart), kommer några av nycklarna att omfördelas till dess fördel. I själva verket kommer detta att betyda förlust av sessionsdata, eftersom det inte finns något sätt att "gå" till en annan replik i händelse av en miss.
Standardbiblioteksverktyg syftar främst till horisontell skalning: de låter dig öka cachen till gigantiska storlekar och ge tillgång till den från kod som finns på olika servrar. Men i vår situation överstiger inte volymen lagrad data flera gigabyte, och prestandan för en eller två noder är tillräckligt. Följaktligen kan de enda användbara standardverktygen vara att säkerställa tillgängligheten av memcached samtidigt som åtminstone en cache-instans bibehålls i fungerande skick. Det var dock inte möjligt att dra nytta av ens denna möjlighet... Här är det värt att påminna om urgamla ramverk som användes i projektet, varför det var omöjligt att få applikationen att fungera med en pool av servrar. Låt oss inte heller glömma förlusten av sessionsdata: kundens öga rycktes av den massiva utloggningen av användare.
Helst krävdes det replikering av poster i memcachade och förbigående repliker vid ett misstag eller misstag. Hjälpte oss att implementera denna strategi .
mcrouter
Detta är en memcachad router utvecklad av Facebook för att lösa dess problem. Den stöder det memcachade textprotokollet, vilket tillåter skala memcachade installationer till galna proportioner. En detaljerad beskrivning av mcrouter finns i . Bland annat den kan göra vad vi behöver:
- replikera post;
- gör fallback till andra servrar i gruppen om ett fel uppstår.
Gå igång!
mcrouter-konfiguration
Jag går direkt till konfigurationen:
{
"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"
]
}
}
}
}Varför tre pooler? Varför upprepas servrar? Låt oss ta reda på hur det fungerar.
- I den här konfigurationen väljer mcrouter sökvägen som förfrågan ska skickas till baserat på kommandot förfrågan. Killen berättar detta för honom
OperationSelectorRoute. - GET-förfrågningar går till hanteraren
RandomRoutesom slumpmässigt väljer en pool eller rutt bland arrayobjektchildren. Varje element i denna array är i sin tur en hanterareMissFailoverRoute, som kommer att gå igenom varje server i poolen tills den får ett svar med data, som kommer att returneras till klienten. - Om vi enbart använde
MissFailoverRoutemed en pool av tre servrar, då skulle alla förfrågningar komma först till den första memcachade instansen, och resten skulle få förfrågningar på resterande basis när det inte finns några data. Ett sådant synsätt skulle leda till överdriven belastning på den första servern i listan, så det beslutades att generera tre pooler med adresser i olika sekvenser och välja dem slumpmässigt. - Alla andra förfrågningar (och detta är en post) behandlas med
AllMajorityRoute. Denna hanterare skickar förfrågningar till alla servrar i poolen och väntar på svar från minst N/2 + 1 av dem. Från användningAllSyncRouteför skrivoperationer måste överges, eftersom denna metod kräver ett positivt svar från Alla servrar i gruppen - annars kommer den tillbakaSERVER_ERROR. Även om mcrouter kommer att lägga till data till tillgängliga cacher, den anropande PHP-funktionen kommer att returnera ett fel och kommer att generera meddelande.AllMajorityRouteär inte så strikt och tillåter att upp till hälften av enheterna tas ur drift utan de ovan beskrivna problemen.
Största nackdelen Detta schema är att om det verkligen inte finns någon data i cachen, så kommer N förfrågningar till memcached att utföras för varje begäran från klienten - till alla servrar i poolen. Vi kan minska antalet servrar i pooler, till exempel, till två: vi kan offra lagringssäkerheten.оhögre hastighet och mindre belastning från förfrågningar till saknade nycklar.
NB: Du kan också hitta användbara länkar för att lära dig mcrouter и (inklusive slutna), som representerar ett helt förråd av olika konfigurationer.
Bygger och kör en mcrouter
Vår applikation (och memcachade sig själv) körs i Kubernetes - följaktligen finns mcrouter också där. För containermontering vi använder , konfigurationen för vilken kommer att se ut så här:
NB: Listorna i artikeln publiceras i arkivet .
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' ]()
... och skissa upp det Rordiagram. Det intressanta är att det bara finns en konfigurationsgenerator baserad på antalet repliker (om någon har ett mer lakoniskt och elegant alternativ, dela det i kommentarerna):
{{- $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 }}
}
}
}
}()
Vi rullar ut det i testmiljön och kontrollerar:
# 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 >Att söka efter texten till felet gav inga resultat, men för frågan ""I förgrunden var det äldsta olösta problemet med projektet - memcachat binärt protokoll.
NB: ASCII-protokollet i memcached är långsammare än det binära, och standardmetoder för konsekvent nyckelhashing fungerar bara med det binära protokollet. Men detta skapar inga problem för ett specifikt fall.
Tricket ligger i bagaget: allt du behöver göra är att byta till ASCII-protokollet så fungerar allt.... Men i det här fallet, vanan att leta efter svar i spelade ett grymt skämt. Du hittar inte det rätta svaret där... om du inte, naturligtvis, bläddrar till slutet, var i avsnittet "Anteckningar från användare" kommer att vara trogen och .
Ja, det korrekta alternativnamnet är memcached.sess_binary_protocol. Den måste inaktiveras, varefter sessionerna börjar fungera. Allt som återstår är att lägga behållaren med mcrouter i en pod med PHP!
Slutsats
Således, med bara infrastrukturella förändringar, kunde vi lösa problemet: problemet med memcachad feltolerans har lösts, och tillförlitligheten för cachelagring har ökat. Förutom de uppenbara fördelarna för applikationen gav detta manöverutrymme vid arbete på plattformen: när alla komponenter har en reserv förenklas administratörens liv avsevärt. Ja, den här metoden har också sina nackdelar, den kan se ut som en "krycka", men om den sparar pengar, begraver problemet och inte orsakar nya - varför inte?
PS
Läs även på vår blogg:
- "Öva med dapp" (med symfony-demo som exempel): и ;
- «".
Källa: will.com
