Billig synkronisering v2

Introduksjon

Etter hvert som Billig har modnet til, er det kommet fram at selv om systemet gjennomgående fungerer godt, ligger det noen svakheter i synkroniseringsrutinene mellom cirkus og dørklientene. Spesifikt hadde man under enkelte teltkonserter under UKA-09 problemer med at billetter ble satt som brukt to ganger; antageligvis av forskjellige grunner, men primærproblemet ligger i at synkroniseringen bare skjer hvert femte minutt.

Den nye synkroniseringsarkitekturen er tenkt å løse dette problemet, ved å lage et system hvor ende-til-ende-latensen er i størrelsesorden 50ms så lenge de aktuelle nodene har konnektivitet.

Teori

Overordnet strategi

Det er mange måter man kan tenke seg å løse problemet på:

  1. Synkronisere oftere, f.eks. hvert minutt. Hver synkronisering har en ikke-neglisjerbar kostnad på både cirkus og dørklienten, dog, så dette har sine begrensninger.
  2. Flytte synkroniseringslogikk inn i dørklienten, for eksempel ved å skrive til både cirkus og den lokale databasen i parallell. Dette er imidlertid svært komplekst og vanskelig å få riktig, ikke minst ettersom det kan være veldig vanskelig å finne ut om man har mistet tilkoblingen til cirkus, og i så fall om siste transaksjon gikk gjennom eller ikke. Det fører også til vesentlig dårligere modularisering, og flere forskjellige tilstander i systemet som kan være vanskelig å teste.
  3. Bruke en eller annen form for ferdig multimaster-replikering. Dette innfører imidlertid svært mye ekstra kompleksitet i databasesystemet vårt, og det er uklart hvordan det vil innvirke på semantikken i resten av databasen når ting går offline (det er per definisjon umulig å ha full ACID og offlinestøtte samtidig).
  4. Gjøre ad-hoc-synkronisering (f.eks. med multicast) mellom klientene for enkelte typer hendelser, f.eks. brukte billetter. I praksis ofte mer komplekst enn man tror, spesielt siden man ender opp med to separate synkroniseringssystemer som kan innvirke på hverandre.
  5. Lage inkrementell synkronisering.

Vi har valgt å basere oss på løsning 5. Det skal nevnes at inkrementell synkronisering har en del snags; flesteparten vil forhåpentligvis bli forklart her. Vi tar utgangspunkt i synkroniseringen fra cirkus og ned til dørklienten, men synkroniseringen den andre veien er relativt lik. Loggtabellen ligger i billig-skjemaet i databasen mdb2, og heter ticket_log.

Inkrementell synkronisering

Det er i hovedsak to måter å lage inkrementell synkronisering på:

  1. Alle relevante endringer (f.eks. «billett 1234 er brukt») lagres til en kø-/loggtabell i en eller annen form (via SQL-triggere), og dørklientene er selv ansvarlige for å lytte etter endringer på denne køen, finne ut hva som er nytt siden sist og gjøre en delvis sync.
  2. Som 1, men med én kø for hver klient, som dørklientene sletter fra når de har kopiert over en gitt endring.

Alternativ 2 er enklest å implementere, men vesentlig mindre fleksibelt; man må på forhånd vite akkurat hvilke dørklienter man har, for eksempel. (Om to klienter skulle få samme ID ved en feil, vil det føre til katastrofale resultater for synkroniseringen om de sletter hverandres data fra synkroniseringskøen.) Vi har derfor valgt å basere oss på alternativ 1.

SQL-transaksjoner gjør det hele litt mer komplisert, dog; et nøkkelproblem er å finne ut på en effektiv og korrekt måte hvilke endringer i loggen man har sett før, og hoppe over dem. Uansett om man bruker klokkeslett eller en sekvensteller (disse er for alle praktiske formål ekvivalente med mindre man bruker en låst teller og ødelegger skalerbarheten sin totalt) kan man komme borti en situasjon der en transaksjon gjør en endring, og så venter svært lenge før den faktisk committes og loggradene blir synlige. I mellomtiden kan en dørklient ha lest fra loggen, kommet fram til at den har lest opp til et gitt punkt forbi transaksjonen som ikke var committet ennå, og dermed gå glipp av den. Man kan jukse seg rundt dette ved å legge inn litt slakk, men det er i utgangspunktet ikke hverken en god eller effektiv løsning.

Vi løser problemet Postgres-spesifikt (inspirert av prosjektet PgQ), da ANSI SQL ikke har noen adekvate måter å løse problemet på. Postgres har et sett med funksjoner som lar deg få vite:

Den første verdien brukes av triggerne ved innsetting i loggtabellen, og den andre brukes under synkronisering. Om minste synlige transaksjons-ID f.eks. er 1000, betyr det at alle transaksjoner opp til og med nummer 999 har committet og er synlige for oss. I neste runde kan vi da se på kun loggrader som er merket med txid >= 1000 – vi vil ofte få noe overlapp (billetter vi ser flere ganger), men det er uproblematisk for synkroniseringslogikken. Vær obs på at synligheten kan endre seg utover i transaksjonen ettersom andre transaksjoner committer, så vi er nødt til å sjekke synligheten før vi begynner å lese data for å være sikre på å få med oss alt.

Praktisk implementasjon

Deteksjon av endringer

Det er i praksis tre interessante klasser av endringer sett fra dørklientenes synspunkt:

  1. En billett er blitt gyldig, som regel fordi ordren den hører til er blitt betalt (eller ordren ble opprettet direkte i betalt tilstand).
  2. En billett er blitt ugyldiggjort, som regel fordi den er blitt brukt i en dør.
  3. En endring har skjedd i MDB2 som innvirker på Billig, f.eks. at et kort med billett på er merket som mistet.

Hendelsestype #3 er i praksis umulig for Billig å holde orden på; viewene som kommer inn fra MDB2 er så komplekse og har så mange forskjellige ting som kan innvirke på dem at det ikke er realistisk å få det rett. Derfor baserer vi oss på en full sync nå og da for å rette opp i denne typen ting; den trenger dog ikke gå like ofte som den gjorde før. Man tenker seg rundt et kvarter mellom hver slik sync.

#1 og #2 er derimot relativt greit å løse med SQL-triggere. Tabellene purchase og ticket får hvert sitt sett med triggere som setter inn ting i ticket_log etter behov. For hver endring i ticket_log gjøres det dessuten en NOTIFY på ticket_log (man kan i prinsippet gjøre NOTIFY på hvilket som helst ord man vil, dog ikke med schemanavn foran) for å signalisere til alle dørklienter (som har gjort LISTEN) at det har skjedd endringer. NOTIFY vil ikke bli sendt før den aktuelle transaksjonen har committet, dog, så man risikerer ikke at klientene spør for tidlig.

En tilsvarende endring gjøres i dørklientens SQL-database, skjønt endringene er mindre komplekse der (eneste endring som gjøres i den databasen er å sette billetter til brukt).

Endringer i synkroniseringsscript

Overordnet logikk i synkroniseringsscriptet (migration.pl) er som før (inkludert offline-støtte), men det refaktoreres og utvides til å støtte et nytt flagg kalt --daemon. Om dette gis, vil scriptet etter å ha gjort full sync som vanlig gå inn i en evig løkke der den lytter etter endringer i hoved- og dørdatabasene (med LISTEN). Idet den får en slik endring, vil den gjøre en inkrementell sync som altså kun berører noen få billetter, og så gå tilbake igjen til å sove. Hvert N. minutt (der N altså per default er 15) vil den så gjøre en ny synkronisering som vanlig.

Daemonmodusen til migration.pl er ment å gå fra init.d eller lignende, så det å basere seg på cron for å sende epost om unormale situasjoner er ikke lenger mulig. I stedet sender den epost på egenhånd til en forhåndsoppsatt adresse.

Andre småfikser

Mens man allikevel skulle skrive ny syncarkitektur, fikset man noen småproblemer her og der:

Lenker: Start, billig, billig dør

Epost: itk@samfundet.no | Telefon: 992 15 925 | Sist endret: 2009-11-07 16:49 | Revisjon: 2 (historie, blame) | Totalt: 1886 kB | Rediger