Guide til Apache BookKeeper

1. Oversikt

I denne artikkelen presenterer vi BookKeeper, en tjeneste som implementerer en distribuert, feiltolerant lagringssystem.

2. Hva er? Bokholder?

BookKeeper ble opprinnelig utviklet av Yahoo som et ZooKeeper-underprosjekt og ble uteksaminert til å bli et toppnivåprosjekt i 2015. I sin kjerne har BookKeeper som mål å være et pålitelig og høytytende system som lagrer sekvenser av Logg inn (aka Records) i datastrukturer som kalles Ledgers.

Et viktig trekk ved hovedbøker er det faktum at de bare er vedlegg og uforanderlige. Dette gjør BookKeeper til en god kandidat for visse applikasjoner, som distribuerte loggingsystemer, Pub-Sub-meldingsapplikasjoner og sanntids strømbehandling.

3. BookKeeper-konsepter

3.1. Logg inn

En loggoppføring inneholder en udelelig enhet av data som et klientprogram lagrer til eller leser fra BookKeeper. Når det er lagret i en hovedbok, inneholder hver oppføring de medfølgende dataene og noen få metadatafelt.

Disse metadatafeltene inkluderer et entryId, som må være unik innenfor en gitt hovedbok. Det er også en autentiseringskode som BookKeeper bruker for å oppdage når en oppføring er ødelagt eller har blitt tuklet med.

BookKeeper tilbyr ingen serialiseringsfunksjoner i seg selv, så klienter må lage sin egen metode for å konvertere konstruksjoner på høyere nivå til / fra byte arrays.

3.2. Ledgers

En hovedbok er den grunnleggende lagringsenheten som administreres av BookKeeper, og lagrer en ordnet sekvens av loggoppføringer. Som nevnt tidligere har hovedbøker kun semantikk, noe som betyr at poster ikke kan endres når de er lagt til dem.

Når en klient slutter å skrive til en hovedbok og lukker den, BookKeeper sel det, og vi kan ikke lenger legge til data i det, selv ikke på et senere tidspunkt. Dette er et viktig poeng å huske på når du designer en applikasjon rundt BookKeeper. Ledgers er ikke en god kandidat til å implementere konstruksjoner på høyere nivå direkte, for eksempel en kø. I stedet ser vi hovedbøker som brukes oftere for å lage mer grunnleggende datastrukturer som støtter disse begrepene på høyere nivå.

For eksempel bruker Apaches distribuerte loggprosjekt hovedbøker som loggsegmenter. Disse segmentene er samlet i distribuerte logger, men de underliggende hovedbøkene er gjennomsiktige for vanlige brukere.

BookKeeper oppnår storresistens ved å replikere loggoppføringer over flere serverforekomster. Tre parametere styrer hvor mange servere og kopier som beholdes:

  • Ensemble størrelse: antall servere som brukes til å skrive reskontordata
  • Skriv quorumstørrelse: antall servere som brukes til å replikere en gitt loggoppføring
  • Ack quorum size: antall servere som må bekrefte en gitt loggoppføring

Ved å justere disse parametrene kan vi justere ytelsen og motstandsdyktighetene til en gitt reskontro. Når du skriver til en hovedbok, vil BookKeeper bare betrakte operasjonen som vellykket når et minimumsgruppe av klyngemedlemmer anerkjenner det.

I tillegg til de interne metadataene støtter BookKeeper også å legge til tilpassede metadata i en reskontro. Dette er et kart over nøkkel- / verdipar som klienter passerer ved opprettelsestid, og BookKeeper-butikker i ZooKeeper sammen med sine egne.

3.3. Bookies

Bookies er servere som har en eller hovedbok. En BookKeeper-klynge består av en rekke bookmakere som kjører i et gitt miljø, og tilbyr tjenester til klienter via vanlige TCP- eller TLS-tilkoblinger.

Bookies koordinerer handlinger ved hjelp av klyngetjenester levert av ZooKeeper. Dette innebærer at hvis vi ønsker å oppnå et fullt feiltolerant system, trenger vi minst et 3-instans ZooKeeper og et 3-instans BookKeeper-oppsett. Et slikt oppsett vil kunne tåle tap hvis en enkelt forekomst mislykkes og fortsatt kan fungere normalt, i det minste for standardoppsett: 3-noders ensemblestørrelse, 2-noders skrivekvorum og 2-node ack-kvorum.

4. Lokalt oppsett

De grunnleggende kravene for å kjøre BookKeeper lokalt er ganske beskjedne. Først trenger vi en ZooKeeper-forekomst som er i gang, som gir store metadatalagring for BookKeeper. Deretter distribuerer vi en bookmaker som gir kundene de faktiske tjenestene.

Selv om det absolutt er mulig å gjøre disse trinnene manuelt, her bruker vi en docker-compose fil som bruker offisielle Apache-bilder for å forenkle denne oppgaven:

$ cd $ docker-komponere opp

Dette docker-compose oppretter tre bookmaker og en ZooKeeper-forekomst. Siden alle bookmakere kjører på samme maskin, er det bare nyttig for testformål. Den offisielle dokumentasjonen inneholder de nødvendige trinnene for å konfigurere en fullstendig feiltolerant klynge.

La oss gjøre en grunnleggende test for å kontrollere at den fungerer som forventet, ved hjelp av bokholderens skallkommando listebøker:

$ docker exec -it apache-bookkeeper_bookie_1 / opt / bookkeeper / bin / bookkeeper \ shell listbookies -readwrite ReadWrite Bookies: 192.168.99.101 (192.168.99.101): 4181 192.168.99.101 (192.168.99.101): 4182 192.168.99.101 (192.168. 99.101): 3181 

Utgangen viser listen over tilgjengelige bookmakere, bestående av tre bookmakere. Vær oppmerksom på at IP-adressene som vises vil endres avhengig av detaljene til den lokale Docker-installasjonen.

5. Bruke Ledger API

Ledger API er den mest grunnleggende måten å grensesnitt med BookKeeper på. Det lar oss samhandle direkte med Hovedbok objekter, men på den annen side mangler direkte støtte for høyere nivå abstraksjoner som strømmer. For de brukstilfellene tilbyr BookKeeper-prosjektet et annet bibliotek, DistributedLog, som støtter disse funksjonene.

Å bruke Ledger API krever å legge til bokholder-server avhengighet av prosjektet vårt:

 org.apache.bookkeeper bokholder-server 4.10.0 

MERKNAD: Som nevnt i dokumentasjonen, vil bruk av denne avhengigheten også omfatte avhengigheter for protobuf- og guava-bibliotekene. Skulle prosjektet vårt også trenge disse bibliotekene, men i en annen versjon enn de som brukes av BookKeeper, kan vi bruke en alternativ avhengighet som skygger for disse bibliotekene:

 org.apache.bookkeeper bokholder-server-skyggelagt 4.10.0 

5.1. Koble til Bookies

De Bokholder klasse er hovedinngangsstedet for Ledger API, som gir noen få metoder for å koble til BookKeeper-tjenesten vår. I sin enkleste form er alt vi trenger å gjøre å opprette en ny forekomst av denne klassen, som sender adressen til en av ZooKeeper-serverne som brukes av BookKeeper:

BookKeeper-klient = ny BookKeeper ("zookeeper-host: 2131"); 

Her, dyrepleier-vert bør settes til IP-adressen eller vertsnavnet til ZooKeeper-serveren som har BookKeepers klyngekonfigurasjon. I vårt tilfelle er det vanligvis "localhost" eller verten som DOCKER_HOST-miljøvariabelen peker på.

Hvis vi trenger mer kontroll over de forskjellige parametrene som er tilgjengelige for å finjustere klienten vår, kan vi bruke en ClientConfiguration forekomst og bruk den til å opprette vår klient:

ClientConfiguration cfg = ny ClientConfiguration (); cfg.setMetadataServiceUri ("zk + null: // zookeeper-host: 2131"); // ... sett andre egenskaper BookKeeper.forConfig (cfg) .build ();

5.2. Opprette en hovedbok

Når vi har en Bokholder for eksempel er det enkelt å opprette en ny reskontro:

LedgerHandle lh = bk.createLedger (BookKeeper.DigestType.MAC, "passord" .getBytes ());

Her har vi brukt den enkleste varianten av denne metoden. Det vil opprette en ny reskontro med standardinnstillinger, ved hjelp av MAC-fordøyelsestypen for å sikre inngangsintegritet.

Hvis vi vil legge til tilpassede metadata i hovedboken vår, må vi bruke en variant som tar alle parametrene:

LedgerHandle lh = bk.createLedger (3, 2, 2, DigestType.MAC, "password" .getBytes (), Collections.singletonMap ("name", "my-ledger" .getBytes ()));

Denne gangen har vi brukt den fullstendige versjonen av createLedger () metode. De tre første argumentene er henholdsvis ensemblestørrelsen, skrive-kvorumet og ack-kvorumverdiene. Deretter har vi de samme fordøyelsesparametrene som før. Til slutt passerer vi a Kart med våre tilpassede metadata.

I begge tilfeller ovenfor, createLedger er en synkron operasjon. BookKeeper tilbyr også oppretting av asynkron reskontro ved hjelp av tilbakeringing:

bk.asyncCreateLedger (3, 2, 2, BookKeeper.DigestType.MAC, "passwd" .getBytes (), (rc, lh, ctx) -> {// ... bruk lh for å få tilgang til hovedbokshandlinger}, null, samlinger .emptyMap ()); 

Nyere versjoner av BookKeeper (> = 4.6) støtter også et flytende API og Fullførbar fremtid for å oppnå det samme målet:

CompletableFuture cf = bk.newCreateLedgerOp () .withDigestType (org.apache.bookkeeper.client.api.DigestType.MAC) .withPassword ("password" .getBytes ()) .execute (); 

Merk at i dette tilfellet får vi en WriteHandle i stedet for en LedgerHandle. Som vi får se senere, kan vi bruke hvilken som helst av dem til å få tilgang til hovedboken vår som LedgerHandle redskaper WriteHandle.

5.3. Skrive data

Når vi har anskaffet en LedgerHandle eller WriteHandle, skriver vi data til tilhørende hovedbok ved hjelp av en av legg til () metode varianter. La oss starte med den synkrone varianten:

for (int i = 0; i <MAX_MESSAGES; i ++) {byte [] data = new String ("message-" + i) .getBytes (); lh.append (data); } 

Her bruker vi en variant som tar en byte array. API-et støtter også Netty's ByteBuf og Java NIO-er ByteBuffer, som gir bedre minnehåndtering i tidskritiske scenarier.

For asynkrone operasjoner, varierer API-en litt avhengig av den spesifikke håndtakstypen vi har anskaffet. WriteHandle bruker Fullførbar fremtid mens LedgerHandle støtter også tilbakeringingsbaserte metoder:

// Tilgjengelig i WriteHandle og LedgerHandle CompletableFuture f = lh.appendAsync (data); // Bare tilgjengelig i LedgerHandle lh.asyncAddEntry (data, (rc, ledgerHandle, entryId, ctx) -> {// ... tilbakeringingslogikk utelatt}, null);

Hvilken man skal velge er i stor grad et personlig valg, men generelt, ved hjelp av Fullførbar fremtid-baserte API-er har en tendens til å være lettere å lese. Dessuten er det sidefordelen som vi kan konstruere en Mono direkte fra det, noe som gjør det lettere å integrere BookKeeper i reaktive applikasjoner.

5.4. Lese data

Å lese data fra en BookKeeper-hovedbok fungerer på samme måte som å skrive. Først bruker vi vår Bokholder forekomst for å opprette en LedgerHandle:

LedgerHandle lh = bk.openLedger (ledgerId, BookKeeper.DigestType.MAC, ledgerPassword); 

Bortsett fra ledgerId parameter, som vi vil dekke senere, ser denne koden ut som createLedger () metoden vi har sett før. Det er imidlertid en viktig forskjell; denne metoden returnerer en skrivebeskyttet LedgerHandle forekomst. Hvis vi prøver å bruke noe av det tilgjengelige legg til () metoder, alt vi får er et unntak.

Alternativt er en tryggere måte å bruke API-et med flytende stil:

ReadHandle rh = bk.newOpenLedgerOp () .withLedgerId (ledgerId) .withDigestType (DigestType.MAC) .withPassword ("password" .getBytes ()) .execute () .get (); 

ReadHandle har de nødvendige metodene for å lese data fra hovedboken vår:

long lastId = lh.readLastConfirmed (); rh.read (0, lastId) .forEach ((oppføring) -> {// ... gjør noe});

Her har vi ganske enkelt bedt om alle tilgjengelige data i denne hovedboken ved hjelp av synkron lese variant. Som forventet er det også en asynkroniseringsvariant:

rh.readAsync (0, lastId) .thenAccept ((entries) -> {entries.forEach ((entry) -> {// ... process entry});});

Hvis vi velger å bruke den eldre openLedger () metode, finner vi flere metoder som støtter tilbakeringingsstil for asynkroniseringsmetoder:

lh.asyncReadEntries (0, lastId, (rc, lh, entries, ctx) -> {while (entries.hasMoreElements ()) {LedgerEntry e = ee.nextElement ();}}, null);

5.5. Listing Ledgers

Vi har tidligere sett at vi trenger hovedboken id for å åpne og lese dataene. Så hvordan får vi en? En måte er å bruke LedgerManager grensesnitt, som vi kan få tilgang til fra vårt Bokholder forekomst. Dette grensesnittet handler i utgangspunktet om hovedmetadata, men har også asyncProcessLedgers () metode. Ved å bruke denne metoden - og litt hjelp til å danne samtidige primitiver - kan vi telle opp alle tilgjengelige hovedbøker:

public List listAllLedgers (BookKeeper bk) {List ledgers = Collections.synchronizedList (new ArrayList ()); CountDownLatch processDone = ny CountDownLatch (1); bk.getLedgerManager () .asyncProcessLedgers ((ledgerId, cb) -> {ledgers.add (ledgerId); cb.processResult (BKException.Code.OK, null, null);}, (rc, s, obj) -> { processDone.countDown ();}, null, BKException.Code.OK, BKException.Code.ReadException); prøv {processDone.await (1, TimeUnit.MINUTES); returbokser; } catch (InterruptedException ie) {throw new RuntimeException (ie); }} 

La oss fordøye denne koden, som er litt lenger enn forventet for en tilsynelatende triviell oppgave. De asyncProcessLedgers () metoden krever to tilbakeringinger.

Den første samler alle ledgers ID-er i en liste. Vi bruker en synkronisert liste her fordi denne tilbakeringingen kan ringes fra flere tråder. Foruten hovedbok-ID, mottar denne tilbakeringingen også en tilbakeringingsparameter. Vi må kalle det processResult () metode for å erkjenne at vi har behandlet dataene og for å signalisere at vi er klare til å få mer data.

Den andre tilbakeringingen blir ringt når alle hovedbokene er sendt til prosessorens tilbakeringing, eller når det oppstår en feil. I vårt tilfelle har vi utelatt feilhåndteringen. I stedet reduserer vi bare a CountDownLatch, som igjen vil fullføre avvente drift og la metoden komme tilbake med en liste over alle tilgjengelige reskontroer.

6. Konklusjon

I denne artikkelen har vi dekket Apache BookKeeper-prosjektet, ser på kjernekonseptene og bruker API-et på lavt nivå for å få tilgang til Ledgers og utføre lese / skrive-operasjoner.

Som vanlig er all kode tilgjengelig på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found