Använda mcrouter för att skala memcached horisontellt

Använda mcrouter för att skala memcached horisontellt

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. egen applikationsserver. 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.

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 detta tillkännagivande. Bland annat bred funktionalitet 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 arrayobjekt children. Varje element i denna array är i sin tur en hanterare MissFailoverRoute, 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 MissFailoverRoute med 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ändning AllSyncRoute för skrivoperationer måste överges, eftersom denna metod kräver ett positivt svar från Alla servrar i gruppen - annars kommer den tillbaka SERVER_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 dokumentation på wiki и projektfrågor (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 werf, konfigurationen för vilken kommer att se ut så här:

NB: Listorna i artikeln publiceras i arkivet 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: 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)

... 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 }}
         }
       }
     }
   }

(10-mcrouter.yaml)

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 "mikrouter php"I förgrunden var det äldsta olösta problemet med projektet - Brist på stöd 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 dokumentation på php.net 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 orättvist nedröstad svar.

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:

Källa: will.com

Lägg en kommentar