Preskoči na sadržaj

Kardinalnost u InfluxDB-u: ID-jevi korisnika i uređaja ne idu u tagove

ID-jevi u tagovima povećavaju indeks InfluxDB-a sa svakim novim posjetiocem. Uz ID-jeve u poljima i dnevni sažetak u MySQL-u, godina za preko 900 oglasa vraća se za manje od sekunde.

6 min čitanja8 sekcija

Dizajnirao sam analitički sloj za marketplace oglasa. Primao je preko dva miliona upisa dnevno, slao podatke na dashboard koji se osvježavao uživo i na upit nad cijelom godinom historije odgovarao za manje od sekunde.

Prva odluka o šemi u tom sistemu pokazala se kao najvažnija. Za svaki identifikator svodila se na jedno pitanje: ide li ova vrijednost u tag ili u polje?

Svaki identifikator stavio sam u polje, uključujući ID-jeve oglasa, korisnika i uređaja, i nijedan nije postao tag. U nastavku objašnjavam zašto, šta je to koštalo i čime se to nadoknadilo.

01

Šema koja izgleda ispravno

Evo događaja onako kako bi ga većina ljudi prvo napisala: neko je na nekom uređaju pogledao oglas.

snippet.txt
Tekst · 1 linija
listing_event,listing_id=48213,user_id=u_9f2c,device_id=d_71ab,event=view count=1i 1717430400000

Lijepo se čita. Sve po čemu biste mogli filtrirati nalazi se među tagovima, a jedino polje je ono što brojite. Većina primjera koje ćete naći ima ovaj oblik, s host ili region u tagovima.

Problem je što host i region uzimaju vrijednosti iz malih, fiksnih skupova. ID-jeva oglasa, korisnika i uređaja stalno je sve više.

02

Svaka kombinacija vrijednosti tagova je jedna serija

InfluxDB indeksira tagove, a polja ne. Cijena zavisi od toga šta je u tom indeksu, a u njemu su serije.

Serija je measurement zajedno s jednom tačnom kombinacijom vrijednosti tagova. Svaka različita kombinacija koju je baza ikad vidjela posebna je serija i ima svoj unos u indeksu. Polja s tim nemaju veze. U jednu seriju možete upisati milion različitih vrijednosti polja i ona ostaje jedna serija.

Zato je broj serija ograničen proizvodom broja različitih vrijednosti svakog taga:

snippet.txt
Tekst · 1 linija
series(listing_event) <= |listing_id| x |user_id| x |device_id| x |event|

U praksi nećete doći do punog proizvoda, jer ne dira svaki korisnik svaki oglas, ali to i nije potrebno da bi nastao problem. Kad su korisnik i uređaj među tagovima, svaki novi posjetilac pravi nove serije, a platforma za oglašavanje i postoji zbog novih posjetilaca.

Taj broj se zove kardinalnost serija i može samo rasti. Tag s malim, fiksnim skupom vrijednosti dodaje poznat broj serija. Identifikator dodaje nove kako saobraćaj raste, pa kad su identifikatori u tagovima, indeks raste sa saobraćajem umjesto sa šemom.

Indeks koji raste troši sve više memorije i usporava upise, a upiti koji su nekad bili trenutni počinju čitati mnogo veći dio indeksa nego što bi trebali. Na početku sve radi, a pukne kad saobraćaj krene, u najgorem mogućem trenutku. Ovakve instalacije najčešće propadnu baš tako.

03

Šema koju sam koristio

Identifikatori idu u polja. Tagovi su za vrijednosti iz malog, poznatog skupa po kojima filtrirate:

snippet.js
JavaScript · 20 linija
import { InfluxDB, Point } from '@influxdata/influxdb-client' const writeApi = new InfluxDB({ url: INFLUX_URL, token: INFLUX_TOKEN })  .getWriteApi(INFLUX_ORG, INFLUX_BUCKET, 'ms') export function recordEvent(e) {  const point = new Point('listing_event')    // tags: a closed set, known before the first write    .tag('event', e.type)    // fields: anything that grows with traffic    .stringField('listing_id', String(e.listingId))    .stringField('user_id', e.userId)    .stringField('device_id', e.deviceId)    .stringField('session_id', e.sessionId)    .stringField('page_url', e.pageUrl)    .intField('count', 1)    .timestamp(e.at)   writeApi.writePoint(point)}

Isti događaj u line protocolu:

snippet.txt
Tekst · 1 linija
listing_event,event=view listing_id="48213",user_id="u_9f2c",device_id="d_71ab",count=1i 1717430400000

Na svakog kandidata za tag primjenjujem isti kratki test. Mogu li zapisati svaku vrijednost koju će ikad imati? Ako mogu, može biti tag. Ako se lista povećava svaki put kad se neko registruje, objavi oglas za firmu ili otvori sajt na novom telefonu, to je polje.

Vrsta događaja prolazi. Taj skup određujemo mi, u kodu, i saobraćaj ga ne mijenja. ID oglasa pada, jer je svaki novi oglas nova vrijednost, a firma upravo želi što više novih oglasa.

04

Cijena: u time-series bazi nema grupisanja po oglasu

Kad su ID-jevi oglasa u poljima, u time-series bazi ne možete efikasno grupisati po oglasu. U InfluxQL-u GROUP BY prima tagove i vrijeme, ali ne i polja. Drugi načini postavljanja upita mogu grupisati po bilo kojoj koloni, ali tek kad pročitaju redove, pa skeniraju cijeli vremenski prozor umjesto da odmah odu na seriju. Izvještaj po oglasu za cijelu godinu najgori je slučaj za takav posao.

Dakle, očigledan prigovor stoji. Pitanje koje se najčešće postavlja, „kako mi ide oglas?“, upravo je ono na koje je s ovom šemom skupo odgovoriti iz time-series baze. To je bilo prihvatljivo, jer time-series baza nikad nije ni trebala odgovarati na njega.

05

Dnevni sažetak u MySQL-u odgovara na pitanja po oglasu

Dizajn radi zahvaljujući drugoj odluci, donesenoj zajedno s prvom. Sirovi događaji ostaju u time-series bazi sedam dana. Jednom dnevno sažetak po oglasu upisuje se u relacionu bazu, a dashboard i fakture čitaju taj sažetak. Sirovi podaci idu u object storage kao arhiva.

Time-series baza ima jedan zadatak: prima upise i vraća podatke za nedavne vremenske prozore. Nikad ne mora odgovarati na pitanje grupisano po oglasu za dug period, a to je jedina vrsta pitanja koju identifikatori u poljima usporavaju.

Sažetak se upisuje u običnu MySQL tabelu:

query.sql
SQL · 8 linija
CREATE TABLE listing_daily (  listing_id  BIGINT UNSIGNED NOT NULL,  day         DATE            NOT NULL,  event       VARCHAR(64)     NOT NULL,  total       INT UNSIGNED    NOT NULL,  uniques     INT UNSIGNED    NOT NULL,  PRIMARY KEY (listing_id, day, event));

Dnevni cron job čita jedan dan sirovih događaja, gdje je skeniranje jeftino jer je prozor mali, i upisuje jedan red po oglasu, danu i događaju. Može se napisati, na primjer, ovako:

snippet.js
JavaScript · 31 linija
async function rollUp(day) {  const flux = `    from(bucket: "${INFLUX_BUCKET}")      |> range(start: ${day.start}, stop: ${day.stop})      |> filter(fn: (r) => r._measurement == "listing_event")      |> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")      |> keep(columns: ["_time", "event", "listing_id", "session_id", "page_url"])  `   const buckets = new Map()  for await (const { values, tableMeta } of queryApi.iterateRows(flux)) {    const row = tableMeta.toObject(values)    const key = `${row.listing_id}|${row.event}`    const b = buckets.get(key) ?? { total: 0, seen: new Set() }    b.total += 1    b.seen.add(`${row.session_id}|${row.page_url}`)    buckets.set(key, b)  }   const rows = [...buckets].map(([key, b]) => {    const [listingId, event] = key.split('|')    return [listingId, day.date, event, b.total, b.seen.size]  })   await mysql.query(    `INSERT INTO listing_daily (listing_id, day, event, total, uniques)     VALUES ?     ON DUPLICATE KEY UPDATE total = VALUES(total), uniques = VALUES(uniques)`,    [rows],  )}

Job odvojeno čuva ukupan broj događaja i broj jedinstvenih. Jedinstveni se broje po sesiji i URL-u stranice, pa se neko ko traži u dva grada broji dvaput, jer je tražio dvije različite stvari. Ukupan broj je veći i ljepše izgleda u izvještaju, a broj jedinstvenih govori oglašivaču koliko je različitih ljudi vidjelo njegove oglase. Oba broja čuvamo u tabeli, pa nijedan ne treba skrivati.

Zbog upserta se job može bez problema pokrenuti ponovo. Ako se isti dan sažme dvaput, drugo pokretanje prepiše prvo umjesto da ga udvostruči.

I sesija i URL stranice su u sirovoj bazi polja. Brojanje jedinstvenih ih treba, a obje vrijednosti rastu sa saobraćajem.

06

Godišnji izvještaj za više od 900 oglasa vraća se za manje od sekunde

Godišnji izvještaj ne čita nijedan sirovi red, nego 365 sažetih redova po oglasu:

query.sql
SQL · 6 linija
SELECT d.listing_id, d.event, SUM(d.total) AS totalFROM listing_daily dJOIN listings l ON l.id = d.listing_idWHERE l.account_id = ?  AND d.day >= CURDATE() - INTERVAL 1 YEARGROUP BY d.listing_id, d.event;

Mjerio sam najgori slučaj. Za klijente s više od 900 oglasa godina dnevnih sažetaka je otprilike 330.000 redova, koje jedan upit agregira, a rezultat stiže za manje od jedne sekunde. „Manje od sekunde“ ne znači ništa kod klijenta s tri oglasa, pa vrijedi mjeriti one najveće.

PodaciGdje se čuvajuŠta ih čita
Sirovi događaji, identifikatori u poljimatime-series baza, 7 danadashboard uživo, dnevni sažetak
Jedan red po oglasu, danu i događajurelaciona bazagodišnji izvještaj, fakture
Sirova arhivaobject storageniko, osim ako nešto treba ponovo obraditi

Ista podjela bila je važna i za račun za hosting, o čemu pišem u posebnom tekstu. Indeks je ostao mali, jer u njemu ništa nije raslo sa saobraćajem.

07

Duga historija jednog entiteta ide u relacionu bazu

Time-series baza dobro odgovara na pitanje „šta se nedavno desilo, po nekoliko fiksnih presjeka“, a loše na „šta se desilo s ovim jednim entitetom u dužem periodu“. Drugo je relaciono pitanje i ima relacioni odgovor: tabelu sažetaka s primarnim ključem.

Kad pitanja o dugim periodima idu u relacionu bazu, odluka o tagovima je laka. Tagovi pokrivaju fiksne presjeke. Identifikatori pripadaju pitanjima o dugim periodima, pa im ne treba indeks u bazi koja na ta pitanja ne odgovara.

Kad vidim identifikatore u tagovima, razlog je rijetko to što neko nije znao bolje. Češće je time-series baza jedina baza u dizajnu, pa mora odgovarati na svako pitanje, i na ono s kojim se najlošije snalazi, a cijenu plaća indeks, koji raste sa saobraćajem.

08

Šta bih uradio drugačije

Ponovo bih razmotrio prozor od sedam dana. Izabran je rano i nikad nije provjeren. Gotovo sigurno je bio dobar, ali nikad nisam pogledao da li je ikome trebalo ponovo obraditi period duži od sedmice, niti koliko bi koštalo trideset dana. Dizajn sažetaka zavisi od te brojke, a ja sam je izabrao i krenuo dalje.

Redovno bih pregledao vrste događaja. Analitika je za četiri godine pratila oko 180 vrsta događaja, bez redovne provjere koje od njih još iko gleda. Neke su se sigurno prestale koristiti, a i dalje su se upisivale. Test za tagove iznad drži indeks malim, ali ne sprječava da zatvoreni skup raste, a iz ovog skupa niko nije uklanjao vrste događaja.

Zapisao bih i test za tagove, i to prije prvog upisa. Šemu je jeftino promijeniti prvog dana, a skupo kad serije već postoje. Odluka stane u jedno pitanje, a to pitanje vrijedi zapisati tamo gdje će ga vidjeti sljedeća osoba koja dodaje tag.

Kako je nastao ovaj tekstAnalitički sloj koji sam dizajnirao za američki marketplace usluga za kamione, s preko dva miliona upisa dnevno.Pročitajte više o projektu →Povezana uslugaSistemi s velikim brojem upisaPogledajte uslugu →Sljedeći tekstGdje sam u našem dizajn sistemu razdvojio web i React Native kodPročitajte →

Blog

Ako radite na nečemu sličnom, javite nam se s detaljima.

Prvi poziv traje 30 minuta i ni na šta vas ne obavezuje.

Opišite nam svoj projekatcontact@microtech.consulting