Ciao, creanu applicazioni per DBMS
Sentite u putere! (…aka gode di u spettaculu)
Tuttu ciò chì sopra face Tarantool una piattaforma attraente per creà applicazioni d'alta carica chì travaglianu cù basa di dati. In tali applicazioni, ci hè spessu bisognu di replicazione di dati.
Cumu l'esitatu sopra, Tarantool hà una replicazione di dati integrata. U principiu di u so funziunamentu hè di eseguisce sequenzialmente nantu à e rèpliche tutte e transazzione cuntenute in u logu maestru (WAL). Di solitu tali replicazione (avemu più chjamatu livellu bassu) hè aduprata per assicurà a tolleranza à i difetti di l'applicazione è / o per distribuisce a carica di lettura trà i nodi di cluster.
Risu. 1. Replicazione in un cluster
Un esempiu di un scenariu alternativu seria u trasferimentu di dati creati in una basa di dati à una altra basa di dati per u processu / monitoraghju. In l'ultimu casu, una suluzione più còmuda pò esse di utilizà altu livellu replicazione - replicazione di dati à u livellu di logica cummerciale di l'applicazione. Quelli. Ùn usemu micca una soluzione pronta integrata in u DBMS, ma implementemu a replicazione per noi stessu in l'applicazione chì sviluppemu. Stu approcciu hà dui vantaghji è svantaghji. Elenchemu i vantaghji.
1. Risparmiu di trafficu:
- Ùn pudete micca trasfiriri tutti i dati, ma solu una parte di questu (per esempiu, pudete trasfiriri solu qualchi tavulini, alcune di e so culonni o registri chì risponde à un certu criteriu);
- A cuntrariu di a replicazione di livellu bassu, chì hè realizatu continuamente in modu asincronu (implementatu in a versione attuale di Tarantool - 1.10) o sincronu (per esse implementatu in versioni successive di Tarantool), a replicazione di altu livellu pò esse realizatu in sessioni (vale à dì, l'applicazione prima sincronizza i dati - una data di sessione di scambiu, dopu ci hè una pausa in a replicazione, dopu chì a prossima sessione di scambiu si trova, etc.);
- se un registru hà cambiatu parechje volte, pudete trasferisce solu a so ultima versione (a cuntrariu di a replicazione di bassu livellu, in quale tutti i cambiamenti fatti nantu à u maestru seranu ghjucati in sequenza nantu à e rèpliche).
2. Ùn ci hè micca difficultà cù l'implementazione di u scambiu HTTP, chì permette di sincronizà e basa di dati remoti.
Risu. 2. Replicazione nantu à HTTP
3. E strutture di basa di dati trà quale a data hè trasferita ùn deve esse micca listessi (in più, in u casu generale, hè ancu pussibule di utilizà diverse DBMS, linguaggi di prugrammazione, piattaforme, etc.).
Risu. 3. Replicazione in sistemi eterogenei
U svantaghju hè chì, in media, a prugrammazione hè più difficiuli/costu chè a cunfigurazione, è invece di persunalizà a funziunalità integrata, avete da implementà u vostru propiu.
Se in a vostra situazione i vantaghji sopra sò cruciali (o sò una cundizione necessaria), allora hè sensu di utilizà a replicazione d'altu livellu. Fighjemu parechje manere di implementà a replicazione di dati d'altu livellu in u DBMS Tarantool.
Minimizazione di u trafficu
Dunque, unu di i vantaghji di a replicazione d'altu livellu hè u risparmiu di trafficu. Per fà stu vantaghju per esse realizatu cumplettamente, hè necessariu di minimizzà a quantità di dati trasferiti durante ogni sessione di scambiu. Di sicuru, ùn devemu micca scurdatu chì à a fine di a sessione, u receptore di dati deve esse sincronizatu cù a fonte (almenu per quella parte di e dati chì hè implicatu in a replicazione).
Cumu minimizzà a quantità di dati trasferiti durante a replicazione d'altu livellu? Una soluzione simplice puderia esse selezziunà e dati per data è ora. Per fà questu, pudete aduprà u campu di data-ora chì esiste digià in a tavula (se esiste). Per esempiu, un documentu "ordine" pò avè un campu "tempu di esecuzione di l'ordine necessariu" - delivery_time
. U prublema cù sta suluzione hè chì i valori in questu campu ùn anu micca esse in a sequenza chì currisponde à a creazione di ordini. Allora ùn pudemu micca ricurdà u valore massimu di u campu delivery_time
, trasmessa durante a sessione di scambiu precedente, è durante a prossima sessione di scambiu selezziunate tutti i registri cù un valore di campu più altu delivery_time
. I registri cù un valore di campu più bassu pò esse aghjuntu trà e sessioni di scambiu delivery_time
. Inoltre, l'ordine puderia avè subitu cambiamenti, chì però ùn anu micca affettatu u campu delivery_time
. In i dui casi, i cambiamenti ùn saranu micca trasferiti da a fonte à u destinazione. À scioglie sti prublemi, avemu bisognu di trasfiriri dati "overlapping". Quelli. in ogni sessione di scambiu avemu da trasfiriri tutti i dati cù u valore di u campu delivery_time
, sopra à qualchì puntu in u passatu (per esempiu, N ore da u mumentu attuale). In ogni casu, hè ovvi chì per i grandi sistemi stu approcciu hè assai redundante è pò riduce u risparmiu di trafficu chì avemu striving for à nunda. Inoltre, a tavula trasferita ùn pò micca avè un campu assuciatu cù una data-ora.
Una altra suluzione, più cumplessa in quantu à l'implementazione, hè di ricunnosce a ricezione di dati. In questu casu, durante ogni sessione di scambiu, tutte e dati sò trasmessi, a ricivuta di quale ùn hè micca stata cunfirmata da u destinatariu. Per implementà questu, avete bisognu di aghjunghje una colonna booleana à a tavola fonte (per esempiu, is_transferred
). Se u ricevitore ricunnosce u ricivutu di u recordu, u campu currispondente piglia u valore true
, dopu chì l'entrata ùn hè più implicata in scambii. Questa opzione di implementazione hà i seguenti svantaghji. Prima, per ogni record trasferitu, un ricunniscenza deve esse generatu è mandatu. À pocu pressu, questu puderia esse paragunabile à duppià a quantità di dati trasferiti è chì porta à radduppià u numeru di andata e ritorno. Siconda, ùn ci hè micca a pussibilità di mandà u stessu registru à parechji receptori (u primu ricevitore à riceve cunfirmà a ricivuta per ellu stessu è per tutti l'altri).
Un metudu chì ùn hà micca i disadvantages datu sopra hè di aghjunghje una colonna à a tavula trasmessa per seguità i cambiamenti in e so fila. Una tale colonna pò esse di tippu di data-ora è deve esse stabilita / aghjurnata da l'applicazione à l'ora attuale ogni volta chì i registri sò aghjuntu / cambiati (atomicamente cù l'aghjunzione / cambiamentu). Per esempiu, chjamemu a colonna update_time
. Salvendu u valore massimu di u campu di sta colonna per i registri trasferiti, pudemu inizià a prossima sessione di scambiu cù stu valore (selezziunà i registri cù u valore di u campu). update_time
, superendu u valore precedentemente almacenatu). U prublema cù l'ultimu approcciu hè chì i cambiamenti di dati ponu accade in batch. In u risultatu di i valori di u campu in a colonna update_time
pò micca esse unicu. Cusì, sta colonna ùn pò micca esse aduprata per l'output di dati porzionati (pagina per pagina). Per vede dati pagina per pagina, avete da inventà meccanismi supplementari chì probabilmente anu una efficienza assai bassa (per esempiu, ricuperà da a basa di dati tutti i registri cù u valore. update_time
più altu ch'è un datu è pruduce un certu nùmeru di registri, partendu da un certu offset da u principiu di u sample).
Pudete migliurà l'efficienza di u trasferimentu di dati migliurendu pocu l'approcciu precedente. Per fà questu, useremu u tipu interu (interu longu) cum'è i valori di u campu di a colonna per seguità i cambiamenti. Chjamemu a colonna row_ver
. U valore di u campu di sta colonna deve esse sempre stabilitu / aghjurnatu ogni volta chì un registru hè creatu / mudificatu. Ma in questu casu, u campu ùn serà micca attribuitu a data-ora attuale, ma u valore di qualchì contatore, aumentatu da unu. In u risultatu, a colonna row_ver
cuntene valori unichi è ponu esse aduprati micca solu per visualizà dati "delta" (dati aghjuntu / cambiatu da a fine di a sessione di scambiu precedente), ma ancu per sparghje in pagine in modu simplice è efficace.
L'ultimu mètudu prupostu di minimizzà a quantità di dati trasferiti in u quadru di replicazione d'altu livellu mi pari u più ottimali è universale. Fighjemu in più detail.
Passà Dati Utilizendu un Contatore di Versione di Fila
Implementazione di u servitore / parte maestru
In MS SQL Server, ci hè un tipu di colonna speciale per implementà stu approcciu - rowversion
. Ogni basa di dati hà un contatore chì aumenta da unu ogni volta chì un record hè aghjuntu / cambiatu in una tavula chì hà una colonna cum'è rowversion
. U valore di stu contatore hè automaticamente assignatu à u campu di sta colonna in u record aghjuntu / cambiatu. U DBMS Tarantool ùn hà micca un mecanismu integratu simili. Tuttavia, in Tarantool ùn hè micca difficiule di implementà manualmente. Fighjemu cumu si faci questu.
Prima, un pocu di terminologia: i tavule in Tarantool sò chjamati spazii, è i registri sò chjamati tuples. In Tarantool pudete creà sequenze. E sequenze ùn sò più cà generatori chjamati di valori interi ordinati. Quelli. questu hè esattamente ciò chì avemu bisognu per i nostri scopi. Sottu avemu da creà un tali sequenza.
Prima di fà qualsiasi operazione di basa di dati in Tarantool, avete bisognu di eseguisce u cumandimu seguente:
box.cfg{}
In u risultatu, Tarantool hà da cumincià à scrive snapshots di basa di dati è logs di transazzione in u cartulare attuale.
Creemu una sequenza row_version
:
box.schema.sequence.create('row_version',
{ if_not_exists = true })
Opzione if_not_exists
permette à u script di creazione per esse eseguitu parechje volte: se l'ughjettu esiste, Tarantool ùn pruvà micca di creà di novu. Questa opzione serà aduprata in tutti i cumandamenti DDL successivi.
Creemu un spaziu cum'è un esempiu.
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
})
Quì avemu stabilitu u nome di u spaziu (goods
), nomi di campu è i so tipi.
I campi auto-incrementanti in Tarantool sò ancu creati cù sequenze. Creemu una chjave primaria auto-incrementante per campu 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 supporta parechji tipi d'indici. L'indici più cumunimenti usati sò i tipi TREE è HASH, chì sò basati nantu à strutture currispondenti à u nome. TREE hè u tipu d'indici più versatile. Si permette à voi à ritruvà dati in una manera urganizata. Ma per a selezzione di ugualità, HASH hè più adattatu. Per quessa, hè cunsigliatu di utilizà HASH per a chjave primaria (chì hè ciò chì avemu fattu).
Per utilizà a colonna row_ver
per trasfiriri dati cambiati, avete bisognu di ligà i valori di sequenza à i campi di sta colonna row_ver
. Ma à u cuntrariu di a chjave primaria, u valore di u campu di a colonna row_ver
duverebbe aumentà da unu micca solu quandu aghjunghjenu novi registri, ma ancu quandu cambiate quelli esistenti. Pudete aduprà triggers per questu. Tarantool hà dui tipi di scatuli spaziali: before_replace
и on_replace
. I triggers sò sparati ogni volta chì i dati in u spaziu cambianu (per ogni tupla affettata da i cambiamenti, una funzione trigger hè lanciata). A cuntrariu on_replace
, before_replace
-triggers permettenu di mudificà i dati di a tupla per quale u trigger hè eseguitu. Per quessa, l'ultimu tipu di triggers ci cunvene.
box.space.goods:before_replace(function(old, new)
return box.tuple.new({new[1], new[2], new[3],
box.sequence.row_version:next()})
end)
U trigger seguente rimpiazza u valore di u campu row_ver
tuple guardatu à u prossimu valore di a sequenza row_version
.
Per esse in gradu di caccià dati da u spaziu goods
per colonna row_ver
, creemu un indice:
box.space.goods:create_index('row_ver', {
parts = { 'row_ver' },
unique = true,
type = 'TREE',
if_not_exists = true
})
Tipu d'indice - arbre (TREE
), perchè avemu bisognu di caccià i dati in ordine crescente di i valori in a colonna row_ver
.
Aghjunghjemu qualchi dati à u spaziu:
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}
Perchè U primu campu hè un contatore auto-incrementante; invece passemu nil. Tarantool sustituverà automaticamente u prossimu valore. In listessu modu, cum'è u valore di i campi di colonna row_ver
pudete passà nil - o micca specificà u valore in tuttu, perchè sta colonna occupa l'ultima pusizioni in u spaziu.
Cuntrollamu u risultatu di l'inserzione:
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]
...
Comu pudete vede, i primi è l'ultimi campi sò riempiti automaticamente. Avà serà faciule scrive una funzione per a carica pagina per pagina di cambiamenti di spaziu 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
A funzione piglia cum'è paràmetru u valore row_ver
, partendu da quale hè necessariu di scaricà cambiamenti, è torna una parte di i dati cambiati.
U campionamentu di dati in Tarantool hè fattu per mezu di indici. Funzione get_goods
usa un iteratore per indice row_ver
per riceve dati cambiati. U tipu di iteratore hè GT (Greater Than, più grande di). Questu significa chì l'iteratore traverserà in sequenza i valori di l'indice partendu da a chjave passata (valore di campu row_ver
).
L'iteratore torna tuple. Per pudè dopu trasfiriri dati via HTTP, hè necessariu di cunvertisce e tuple in una struttura cunvene per a serializazione successiva. L'esempiu usa a funzione standard per questu tomap
. Invece di utilizà tomap
pudete scrive a vostra propria funzione. Per esempiu, pudemu vulemu rinumate un campu name
, ùn passà u campu code
è aghjunghje un campu 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
A dimensione di a pagina di i dati di output (u nùmeru di registri in una parte) hè determinata da a variàbile page_size
. In l'esempiu u valore page_size
hè 5. In un veru prugramma, a dimensione di a pagina di solitu importa più. Dipende da a dimensione media di a tupla spaziale. A dimensione ottima di a pagina pò esse determinata empiricamente per mezu di u tempu di trasferimentu di dati. A più grande hè a dimensione di a pagina, u più chjucu u numeru di roundtrips trà i lati di l'inviu è u ricivutu. In questu modu, pudete riduce u tempu generale per scaricà i cambiamenti. In ogni casu, se a dimensione di a pagina hè troppu grande, passeremu troppu longu nantu à u servitore serializing the sample. In u risultatu, pò esse ritardi in u processu di altre dumande chì venenu à u servitore. Parametru page_size
pò esse caricatu da u schedariu di cunfigurazione. Per ogni spaziu trasmessu, pudete stabilisce u so propiu valore. In ogni casu, per a maiò parte di i spazii u valore predeterminatu (per esempiu, 100) pò esse adattatu.
Eseguimu a funzione 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
...
Pigliemu u valore di u campu row_ver
da l'ultima linea è chjamate a funzione di novu:
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
...
Torna una volta:
tarantool> get_goods(8)
---
- []
...
Comu pudete vede, quandu s'utilice in questu modu, a funzione torna tutti i registri spaziali pagina per pagina goods
. L'ultima pagina hè seguita da una selezzione viota.
Facemu cambiamenti à u spaziu:
box.space.goods:update(4, {{'=', 6, 'copybook'}})
box.space.goods:insert{nil, 'clip', 234}
box.space.goods:insert{nil, 'folder', 432}
Avemu cambiatu u valore di u campu name
per una entrata è aghjunghje dui novi entrate.
Ripitemu l'ultima funzione chjamata:
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
...
A funzione hà tornatu i registri cambiati è aghjunti. Allora a funzione get_goods
permette di riceve dati chì hà cambiatu da a so ultima chjamata, chì hè a basa di u metudu di replicazione in cunsiderà.
Lasceremu l'emissione di risultati via HTTP in forma di JSON fora di u scopu di stu articulu. Pudete leghje nantu à questu quì:
Implementazione di a parte client/slave
Fighjemu ciò chì l'implementazione di u latu ricevente pare. Creemu un spaziu in u latu di ricezione per almacenà i dati scaricati:
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
})
A struttura di u spaziu s'assumiglia à a struttura di u spaziu in a fonte. Ma siccomu ùn avemu da passà i dati ricevuti in ogni locu, a colonna row_ver
ùn hè micca in u spaziu di u destinatariu. In campu id
identificatori di fonte seranu registrati. Dunque, da u latu di u ricevitore ùn ci hè bisognu di fà l'autu-incrementing.
Inoltre, avemu bisognu di un spaziu per salvà i valori 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
})
Per ogni spaziu caricatu (campu space_name
) salveremu l'ultimu valore caricatu quì row_ver
(campu value
). A colonna agisce cum'è a chjave primaria space_name
.
Creemu una funzione per carica dati spaziali goods
via HTTP. Per fà questu, avemu bisognu di una biblioteca chì implementa un cliente HTTP. A seguente linea carica a biblioteca è instantiate u cliente HTTP:
local http_client = require('http.client').new()
Avemu ancu bisognu di una biblioteca per a deserializazione json:
local json = require('json')
Questu hè abbastanza per creà una funzione di carica di dati:
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
A funzione eseguisce una dumanda HTTP à l'indirizzu url è u manda row_ver
cum'è un paràmetru è torna u risultatu deserializatu di a dumanda.
A funzione per salvà i dati ricevuti hè cusì:
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
Ciclu di salvezza di dati à u spaziu goods
postu in una transazzione (a funzione hè aduprata per questu box.atomic
) per riduce u numeru di operazioni di discu.
Infine, a funzione di sincronizazione spaziale lucale goods
cù una fonte pudete implementà cusì:
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
Prima avemu lettu u valore salvatu prima row_ver
per u spaziu goods
. S'ellu manca (a prima sessione di scambiu), allora u pigliemu cum'è row_ver
zeru. In seguitu in u ciculu facemu un scaricamentu pagina per pagina di i dati cambiati da a fonte à l'url specificata. À ogni iterazione, salvemu i dati ricevuti in u spaziu lucale appropritatu è aghjurnà u valore row_ver
(in u spaziu row_ver
è in a variabile row_ver
) - piglià u valore row_ver
da l'ultima linea di dati caricati.
Per pruteggiri contru à u looping accidintali (in casu di un errore in u prugramma), u ciclu while
pò esse rimpiazzatu da for
:
for _ = 1, max_req do ...
Cum'è u risultatu di eseguisce a funzione sync_goods
spaziu goods
u ricevitore cuntene l'ultime versioni di tutti i registri spaziali goods
in a fonte.
Ovviamente, a cancellazione di dati ùn pò esse trasmessa in questu modu. Se un tali bisognu esiste, pudete aduprà una marca di eliminazione. Aghjunghjite à u spaziu goods
campu booleanu is_deleted
è invece di sguassà fisicamente un registru, usemu l'eliminazione logica - avemu stabilitu u valore di u campu is_deleted
in significatu true
. Calchì volta invece di un campu booleanu is_deleted
hè più còmuda à aduprà u campu deleted
, chì guarda a data-ora di l'eliminazione logica di u record. Dopu avè realizatu una eliminazione logica, u registru marcatu per a cancellazione serà trasferitu da a fonte à a destinazione (sicondu a logica discutata sopra).
Sequenza row_ver
pò esse usatu per trasmette dati da altri spazii: ùn ci hè bisognu di creà una sequenza separata per ogni spaziu trasmessu.
Avemu vistu un modu efficace di replicazione di dati d'altu livellu in l'applicazioni chì utilizanu u DBMS Tarantool.
scuperti
- Tarantool DBMS hè un pruduttu attraente è promettente per a creazione di applicazioni d'alta carica.
- A replicazione di dati d'altu livellu hà una quantità di vantaghji nantu à a replicazione di livellu bassu.
- U metudu di replicazione d'altu livellu discutitu in l'articulu permette di minimizzà a quantità di dati trasferiti trasfirendu solu quelli registri chì anu cambiatu da l'ultima sessione di scambiu.
Source: www.habr.com