Poštovani, pravim aplikacije za DBMS
Osjeti moć! (…aka uživajte u nastupu)
Sve navedeno čini Tarantool atraktivnom platformom za kreiranje aplikacija za baze podataka velikog opterećenja. U takvim aplikacijama često postoji potreba za replikacijom podataka.
Kao što je gore spomenuto, Tarantool ima ugrađenu replikaciju podataka. Princip njegovog rada je da uzastopno izvršava na replikama sve transakcije sadržane u glavnom dnevniku (WAL). Obično takva replikacija (dalje ćemo je zvati nizak nivo) se koristi za osiguranje tolerancije grešaka u aplikaciji i/ili za raspodjelu opterećenja čitanja između čvorova klastera.
Rice. 1. Replikacija unutar klastera
Primjer alternativnog scenarija bi bio prijenos podataka kreiranih u jednoj bazi podataka u drugu bazu podataka radi obrade/nadgledanja. U potonjem slučaju, pogodnije rješenje može biti korištenje visoki nivo replikacija - replikacija podataka na razini poslovne logike aplikacije. One. Ne koristimo gotova rješenja ugrađena u DBMS, već sami implementiramo replikaciju unutar aplikacije koju razvijamo. Ovaj pristup ima i prednosti i nedostatke. Hajde da navedemo prednosti.
1. Uštede u saobraćaju:
- Ne možete prenijeti sve podatke, već samo dio (na primjer, možete prenijeti samo neke tabele, neke njihove kolone ili zapise koji ispunjavaju određeni kriterijum);
- Za razliku od replikacije niskog nivoa, koja se izvodi kontinuirano u asinhronom (implementiranom u trenutnoj verziji Tarantool-a - 1.10) ili sinhronom (koji će se implementirati u narednim verzijama Tarantool-a) načinu, replikacija na visokom nivou može se izvoditi u sesijama (tj. aplikacija prvo sinhronizuje podatke - podatke sesije razmene, zatim dolazi do pauze u replikaciji, nakon čega dolazi do sledeće sesije razmene itd.);
- ako se zapis promijenio nekoliko puta, možete prenijeti samo njegovu najnoviju verziju (za razliku od replikacije niskog nivoa, u kojoj će se sve promjene napravljene na masteru reproducirati uzastopno na replikama).
2. Nema poteškoća sa implementacijom HTTP razmene, koja vam omogućava da sinhronizujete udaljene baze podataka.
Rice. 2. Replikacija preko HTTP-a
3. Strukture baze podataka između kojih se prenose podaci ne moraju biti iste (štaviše, u opštem slučaju čak je moguće koristiti različite DBMS, programske jezike, platforme itd.).
Rice. 3. Replikacija u heterogenim sistemima
Loša strana je u tome što je u prosjeku programiranje teže/skuplje od konfiguracije i umjesto prilagođavanja ugrađene funkcionalnosti, morat ćete implementirati vlastitu.
Ako su u vašoj situaciji gore navedene prednosti ključne (ili su neophodan uslov), onda ima smisla koristiti replikaciju na visokom nivou. Pogledajmo nekoliko načina za implementaciju replikacije podataka visokog nivoa u Tarantool DBMS.
Minimizacija saobraćaja
Dakle, jedna od prednosti replikacije na visokom nivou je ušteda saobraćaja. Da bi se ova prednost u potpunosti iskoristila, potrebno je minimizirati količinu podataka koji se prenose tokom svake sesije razmjene. Naravno, ne treba zaboraviti da na kraju sesije prijemnik podataka mora biti sinkroniziran sa izvorom (barem za onaj dio podataka koji je uključen u replikaciju).
Kako minimizirati količinu podataka koji se prenose tokom replikacije na visokom nivou? Jednostavno rješenje bi moglo biti odabir podataka prema datumu i vremenu. Da biste to učinili, možete koristiti polje datum-vrijeme koje već postoji u tabeli (ako postoji). Na primjer, dokument "narudžbe" može imati polje "potrebno vrijeme izvršenja naloga" - delivery_time
. Problem kod ovog rješenja je što vrijednosti u ovom polju ne moraju biti u redoslijedu koji odgovara kreiranju naloga. Dakle, ne možemo zapamtiti maksimalnu vrijednost polja delivery_time
, prenešene tokom prethodne sesije razmene, a tokom sledeće sesije razmene izaberite sve zapise sa višom vrednošću polja delivery_time
. Zapisi sa nižom vrijednošću polja su možda dodani između sesija razmjene delivery_time
. Takođe, poredak je mogao da pretrpi promene, koje ipak nisu uticale na teren delivery_time
. U oba slučaja, promjene se neće prenijeti sa izvora na odredište. Da bismo riješili ove probleme, morat ćemo prenijeti podatke koji se "preklapaju". One. u svakoj sesiji razmjene prenosit ćemo sve podatke sa vrijednošću polja delivery_time
, prelazeći neku tačku u prošlosti (na primjer, N sati od trenutnog trenutka). Međutim, očigledno je da je za velike sisteme ovaj pristup veoma redundantni i može smanjiti uštede u saobraćaju kojima težimo na ništa. Osim toga, tablica koja se prenosi možda nema polje povezano s datumom i vremenom.
Drugo rješenje, složenije u smislu implementacije, je potvrda prijema podataka. U tom slučaju, tokom svake sesije razmjene, prenose se svi podaci čiji prijem nije potvrdio primalac. Da biste ovo implementirali, morat ćete dodati Boolean stupac izvornoj tablici (na primjer, is_transferred
). Ako primalac potvrdi prijem zapisa, odgovarajuće polje uzima vrijednost true
, nakon čega unos više nije uključen u razmjene. Ova opcija implementacije ima sljedeće nedostatke. Prvo, za svaki preneseni zapis, mora se generirati i poslati potvrda. Grubo govoreći, ovo bi se moglo uporediti sa udvostručavanjem količine prenesenih podataka i dovesti do udvostručavanja broja povratnih putovanja. Drugo, ne postoji mogućnost slanja istog zapisa na više primatelja (prvi primalac će potvrditi prijem za sebe i za sve ostale).
Metoda koja nema gore navedene nedostatke je dodavanje stupca prenesenoj tabeli kako bi se pratile promjene u njenim redovima. Takva kolona može biti tipa datum-vrijeme i mora biti postavljena/ažurirana od strane aplikacije na trenutno vrijeme svaki put kada se zapisi dodaju/promijene (atomski sa dodavanjem/promjenom). Kao primjer, nazovimo kolonu update_time
. Čuvanjem maksimalne vrijednosti polja ove kolone za prenesene zapise, možemo započeti sljedeću sesiju razmjene sa ovom vrijednošću (odaberite zapise sa vrijednošću polja update_time
, premašuje prethodno pohranjenu vrijednost). Problem s potonjim pristupom je što se promjene podataka mogu dogoditi u serijama. Kao rezultat vrijednosti polja u koloni update_time
možda nije jedinstven. Stoga se ovaj stupac ne može koristiti za porcionirani (stranicu po stranicu) izlaz podataka. Da biste prikazali podatke stranicu po stranicu, morat ćete izmisliti dodatne mehanizme koji će najvjerovatnije imati vrlo nisku efikasnost (na primjer, preuzimanje iz baze podataka svih zapisa sa vrijednošću update_time
veći od datog i proizvodi određeni broj zapisa, počevši od određenog pomaka od početka uzorka).
Možete poboljšati efikasnost prijenosa podataka blagim poboljšanjem prethodnog pristupa. Da bismo to učinili, koristit ćemo cjelobrojni tip (dugi cijeli broj) kao vrijednosti polja stupca za praćenje promjena. Nazovimo kolonu row_ver
. Vrijednost polja ove kolone i dalje mora biti postavljena/ažurirana svaki put kada se zapis kreira/modi. Ali u ovom slučaju, polju neće biti dodijeljen trenutni datum-vrijeme, već vrijednost nekog brojača, uvećana za jedan. Kao rezultat toga, kolona row_ver
će sadržavati jedinstvene vrijednosti i može se koristiti ne samo za prikaz "delta" podataka (podaci dodani/promijenjeni od kraja prethodne sesije razmjene), već i za jednostavno i efikasno rastavljanje na stranice.
Posljednja predložena metoda minimiziranja količine podataka prenesenih u okviru replikacije na visokom nivou čini mi se najoptimalnijim i univerzalnim. Pogledajmo to detaljnije.
Prenošenje podataka pomoću brojača verzija reda
Implementacija serverskog/master dijela
U MS SQL Serveru postoji poseban tip kolone za implementaciju ovog pristupa - rowversion
. Svaka baza podataka ima brojač koji se povećava za jedan svaki put kada se zapis doda/promijeni u tablici koja ima stupac poput rowversion
. Vrijednost ovog brojača se automatski dodjeljuje polju ove kolone u dodanom/promijenjenom zapisu. Tarantool DBMS nema sličan ugrađeni mehanizam. Međutim, u Tarantoolu nije teško implementirati ga ručno. Pogledajmo kako se to radi.
Prvo, malo terminologije: tabele u Tarantoolu se zovu razmaci, a zapisi se zovu tuple. U Tarantoolu možete kreirati sekvence. Sekvence nisu ništa drugo do imenovani generatori uređenih cjelobrojnih vrijednosti. One. to je upravo ono što nam je potrebno za naše potrebe. U nastavku ćemo kreirati takav niz.
Prije izvođenja bilo koje operacije baze podataka u Tarantool-u, morate pokrenuti sljedeću naredbu:
box.cfg{}
Kao rezultat toga, Tarantool će početi pisati snimke baze podataka i dnevnike transakcija u trenutni direktorij.
Kreirajmo niz row_version
:
box.schema.sequence.create('row_version',
{ if_not_exists = true })
Opcija if_not_exists
dozvoljava da se skripta za kreiranje izvrši više puta: ako objekat postoji, Tarantool neće pokušati da ga ponovo kreira. Ova opcija će se koristiti u svim narednim DDL naredbama.
Napravimo prostor kao primjer.
box.schema.space.create('goods', {
format = {
{
name = 'id',
type = 'unsigned'
},
{
name = 'name',
type = 'string'
},
{
name = 'code',
type = 'unsigned'
},
{
name = 'row_ver',
type = 'unsigned'
}
},
if_not_exists = true
})
Ovdje postavljamo ime prostora (goods
), imena polja i njihove vrste.
Polja sa automatskim povećanjem u Tarantoolu se također kreiraju pomoću sekvenci. Kreirajmo primarni ključ koji se automatski povećava po polju id
:
box.schema.sequence.create('goods_id',
{ if_not_exists = true })
box.space.goods:create_index('primary', {
parts = { 'id' },
sequence = 'goods_id',
unique = true,
type = 'HASH',
if_not_exists = true
})
Tarantool podržava nekoliko tipova indeksa. Najčešće korišćeni indeksi su tipovi TREE i HASH, koji se zasnivaju na strukturama koje odgovaraju imenu. TREE je najsvestraniji tip indeksa. Omogućava vam da dohvatite podatke na organizovan način. Ali za odabir jednakosti, HASH je prikladniji. Shodno tome, preporučljivo je koristiti HASH za primarni ključ (što smo i uradili).
Za korištenje kolone row_ver
da biste prenijeli promijenjene podatke, trebate povezati vrijednosti sekvence za polja ove kolone row_ver
. Ali za razliku od primarnog ključa, vrijednost polja stupca row_ver
treba povećati za jedan ne samo prilikom dodavanja novih zapisa, već i prilikom promjene postojećih. Za ovo možete koristiti okidače. Tarantool ima dvije vrste okidača prostora: before_replace
и on_replace
. Okidači se aktiviraju kad god se podaci u prostoru promijene (za svaki tuple na koji promjene utiču, pokreće se funkcija okidača). Za razliku od on_replace
, before_replace
-okidači vam omogućavaju da modificirate podatke tuple-a za koji se okidač izvršava. U skladu s tim, posljednji tip okidača nam odgovara.
box.space.goods:before_replace(function(old, new)
return box.tuple.new({new[1], new[2], new[3],
box.sequence.row_version:next()})
end)
Sljedeći okidač zamjenjuje vrijednost polja row_ver
pohranjeni tuple na sljedeću vrijednost niza row_version
.
Da bi mogli izvući podatke iz svemira goods
po koloni row_ver
, napravimo indeks:
box.space.goods:create_index('row_ver', {
parts = { 'row_ver' },
unique = true,
type = 'TREE',
if_not_exists = true
})
Vrsta indeksa - stablo (TREE
), jer morat ćemo izdvojiti podatke u rastućem redoslijedu vrijednosti u koloni row_ver
.
Dodajmo malo podataka u prostor:
box.space.goods:insert{nil, 'pen', 123}
box.space.goods:insert{nil, 'pencil', 321}
box.space.goods:insert{nil, 'brush', 100}
box.space.goods:insert{nil, 'watercolour', 456}
box.space.goods:insert{nil, 'album', 101}
box.space.goods:insert{nil, 'notebook', 800}
box.space.goods:insert{nil, 'rubber', 531}
box.space.goods:insert{nil, 'ruler', 135}
Jer Prvo polje je brojač koji se automatski povećava; umjesto toga prosljeđujemo nulu. Tarantool će automatski zamijeniti sljedeću vrijednost. Slično, kao vrijednost polja kolone row_ver
možete proslijediti nil - ili uopće ne specificirati vrijednost, jer ova kolona zauzima posljednju poziciju u prostoru.
Provjerimo rezultat umetanja:
tarantool> box.space.goods:select()
---
- - [1, 'pen', 123, 1]
- [2, 'pencil', 321, 2]
- [3, 'brush', 100, 3]
- [4, 'watercolour', 456, 4]
- [5, 'album', 101, 5]
- [6, 'notebook', 800, 6]
- [7, 'rubber', 531, 7]
- [8, 'ruler', 135, 8]
...
Kao što vidite, prvo i posljednje polje se popunjavaju automatski. Sada će biti lako napisati funkciju za učitavanje stranica po stranicu promjena prostora goods
:
local page_size = 5
local function get_goods(row_ver)
local index = box.space.goods.index.row_ver
local goods = {}
local counter = 0
for _, tuple in index:pairs(row_ver, {
iterator = 'GT' }) do
local obj = tuple:tomap({ names_only = true })
table.insert(goods, obj)
counter = counter + 1
if counter >= page_size then
break
end
end
return goods
end
Funkcija uzima kao parametar vrijednost row_ver
, počevši od kojeg je potrebno izbaciti promjene, i vraća dio promijenjenih podataka.
Uzorkovanje podataka u Tarantoolu se vrši preko indeksa. Funkcija get_goods
koristi iterator po indeksu row_ver
za primanje izmijenjenih podataka. Tip iteratora je GT (Greater Than, larger than). To znači da će iterator uzastopno prelaziti vrijednosti indeksa počevši od proslijeđenog ključa (vrijednost polja row_ver
).
Iterator vraća tuple. Da bi se naknadno mogli prenositi podaci putem HTTP-a, potrebno je konvertirati tuple u strukturu pogodnu za naknadnu serijalizaciju. Primjer koristi standardnu funkciju za ovo tomap
. Umjesto korištenja tomap
možete napisati svoju vlastitu funkciju. Na primjer, možda bismo željeli preimenovati polje name
, ne prolazi kroz polje code
i dodajte polje comment
:
local function unflatten_goods(tuple)
local obj = {}
obj.id = tuple.id
obj.goods_name = tuple.name
obj.comment = 'some comment'
obj.row_ver = tuple.row_ver
return obj
end
Veličina stranice izlaznih podataka (broj zapisa u jednom dijelu) određena je varijablom page_size
. U primjeru vrijednost page_size
je 5. U pravom programu, veličina stranice je obično važnija. Zavisi od prosječne veličine prostornog tuple-a. Optimalna veličina stranice može se odrediti empirijski mjerenjem vremena prijenosa podataka. Što je veća veličina stranice, manji je broj povratnih putovanja između strane koja šalje i primaoca. Na ovaj način možete smanjiti ukupno vrijeme za preuzimanje promjena. Međutim, ako je veličina stranice prevelika, predugo ćemo provesti na serveru koji serializira uzorak. Kao rezultat toga, može doći do kašnjenja u obradi drugih zahtjeva koji dolaze na server. Parametar page_size
može se učitati iz konfiguracijske datoteke. Za svaki preneseni prostor možete postaviti vlastitu vrijednost. Međutim, za većinu prostora zadana vrijednost (na primjer, 100) može biti prikladna.
Izvršimo funkciju get_goods
:
tarantool> get_goods(0)
---
- - row_ver: 1
code: 123
name: pen
id: 1
- row_ver: 2
code: 321
name: pencil
id: 2
- row_ver: 3
code: 100
name: brush
id: 3
- row_ver: 4
code: 456
name: watercolour
id: 4
- row_ver: 5
code: 101
name: album
id: 5
...
Uzmimo vrijednost polja row_ver
iz zadnjeg reda i ponovo pozovite funkciju:
tarantool> get_goods(5)
---
- - row_ver: 6
code: 800
name: notebook
id: 6
- row_ver: 7
code: 531
name: rubber
id: 7
- row_ver: 8
code: 135
name: ruler
id: 8
...
Ponovo:
tarantool> get_goods(8)
---
- []
...
Kao što vidite, kada se koristi na ovaj način, funkcija vraća sve zapise o prostoru stranicu po stranicu goods
. Poslednju stranicu prati prazan izbor.
Učinimo promjene u prostoru:
box.space.goods:update(4, {{'=', 6, 'copybook'}})
box.space.goods:insert{nil, 'clip', 234}
box.space.goods:insert{nil, 'folder', 432}
Promijenili smo vrijednost polja name
za jedan unos i dodala dva nova unosa.
Ponovimo zadnji poziv funkcije:
tarantool> get_goods(8)
---
- - row_ver: 9
code: 800
name: copybook
id: 6
- row_ver: 10
code: 234
name: clip
id: 9
- row_ver: 11
code: 432
name: folder
id: 10
...
Funkcija je vratila izmijenjene i dodane zapise. Dakle, funkcija get_goods
omogućava vam da primite podatke koji su se promijenili od njegovog posljednjeg poziva, što je osnova metode replikacije koja se razmatra.
Izdavanje rezultata putem HTTP-a u obliku JSON-a ostavit ćemo izvan okvira ovog članka. O ovome možete pročitati ovdje:
Implementacija klijent/slave dio
Pogledajmo kako izgleda implementacija na strani koja prima. Napravimo prostor na strani primaoca za pohranjivanje preuzetih podataka:
box.schema.space.create('goods', {
format = {
{
name = 'id',
type = 'unsigned'
},
{
name = 'name',
type = 'string'
},
{
name = 'code',
type = 'unsigned'
}
},
if_not_exists = true
})
box.space.goods:create_index('primary', {
parts = { 'id' },
sequence = 'goods_id',
unique = true,
type = 'HASH',
if_not_exists = true
})
Struktura prostora podsjeća na strukturu prostora u izvoru. Ali pošto primljene podatke nećemo prenositi nigde drugde, kolonu row_ver
nije u prostoru primaoca. Na terenu id
izvorni identifikatori će biti snimljeni. Stoga, na strani prijemnika nema potrebe da se on automatski povećava.
Osim toga, potreban nam je prostor za spremanje vrijednosti row_ver
:
box.schema.space.create('row_ver', {
format = {
{
name = 'space_name',
type = 'string'
},
{
name = 'value',
type = 'string'
}
},
if_not_exists = true
})
box.space.row_ver:create_index('primary', {
parts = { 'space_name' },
unique = true,
type = 'HASH',
if_not_exists = true
})
Za svaki učitani prostor (polje space_name
) ovdje ćemo sačuvati posljednju učitanu vrijednost row_ver
(polje value
). Kolona se ponaša kao primarni ključ space_name
.
Kreirajmo funkciju za učitavanje podataka o prostoru goods
putem HTTP-a. Da bismo to uradili, potrebna nam je biblioteka koja implementira HTTP klijenta. Sljedeća linija učitava biblioteku i instancira HTTP klijenta:
local http_client = require('http.client').new()
Također nam je potrebna biblioteka za json deserializaciju:
local json = require('json')
Ovo je dovoljno za kreiranje funkcije učitavanja podataka:
local function load_data(url, row_ver)
local url = ('%s?rowVer=%s'):format(url,
tostring(row_ver))
local body = nil
local data = http_client:request('GET', url, body, {
keepalive_idle = 1,
keepalive_interval = 1
})
return json.decode(data.body)
end
Funkcija izvršava HTTP zahtjev na url adresu i šalje ga row_ver
kao parametar i vraća deserializirani rezultat zahtjeva.
Funkcija za spremanje primljenih podataka izgleda ovako:
local function save_goods(goods)
local n = #goods
box.atomic(function()
for i = 1, n do
local obj = goods[i]
box.space.goods:put(
obj.id, obj.name, obj.code)
end
end)
end
Ciklus pohranjivanja podataka u prostor goods
stavljen u transakciju (funkcija se koristi za to box.atomic
) za smanjenje broja operacija na disku.
Konačno, funkcija sinkronizacije lokalnog prostora goods
sa izvorom to možete implementirati ovako:
local function sync_goods()
local tuple = box.space.row_ver:get('goods')
local row_ver = tuple and tuple.value or 0
—— set your url here:
local url = 'http://127.0.0.1:81/test/goods/list'
while true do
local goods = load_goods(url, row_ver)
local count = #goods
if count == 0 then
return
end
save_goods(goods)
row_ver = goods[count].rowVer
box.space.row_ver:put({'goods', row_ver})
end
end
Prvo čitamo prethodno sačuvanu vrijednost row_ver
za prostor goods
. Ako nedostaje (prva sesija razmjene), onda to uzimamo kao row_ver
nula. Sljedeće u ciklusu vršimo preuzimanje stranica po stranicu promijenjenih podataka iz izvora na navedenom URL-u. Na svakoj iteraciji primljene podatke spremamo u odgovarajući lokalni prostor i ažuriramo vrijednost row_ver
(u svemiru row_ver
i u varijablu row_ver
) - uzeti vrijednost row_ver
iz zadnje linije učitanih podataka.
Za zaštitu od slučajnog petlje (u slučaju greške u programu), petlja while
može se zamijeniti sa for
:
for _ = 1, max_req do ...
Kao rezultat izvršavanja funkcije sync_goods
prostor goods
prijemnik će sadržavati najnovije verzije svih svemirskih zapisa goods
u izvoru.
Očigledno, brisanje podataka se ne može emitovati na ovaj način. Ako postoji takva potreba, možete koristiti oznaku za brisanje. Dodajte u prostor goods
boolean field is_deleted
i umjesto fizičkog brisanja zapisa, koristimo logičko brisanje - postavljamo vrijednost polja is_deleted
u značenje true
. Ponekad umjesto logičkog polja is_deleted
zgodnije je koristiti polje deleted
, koji pohranjuje datum i vrijeme logičkog brisanja zapisa. Nakon izvršenja logičkog brisanja, zapis označen za brisanje će se prenijeti od izvora do odredišta (prema gore opisanoj logici).
Slijed row_ver
može se koristiti za prijenos podataka iz drugih prostora: nema potrebe za kreiranjem zasebnog niza za svaki preneseni prostor.
Razmotrili smo efikasan način replikacije podataka visokog nivoa u aplikacijama koje koriste Tarantool DBMS.
nalazi
- Tarantool DBMS je atraktivan, obećavajući proizvod za kreiranje aplikacija visokog opterećenja.
- Replikacija podataka na visokom nivou ima brojne prednosti u odnosu na replikaciju niskog nivoa.
- Metoda replikacije visokog nivoa o kojoj se govori u članku omogućava vam da minimizirate količinu prenesenih podataka prenosom samo onih zapisa koji su se promijenili od posljednje sesije razmjene.
izvor: www.habr.com