Toimintaanalytiikka mikropalveluarkkitehtuurissa: miten auttaa ja neuvoa Postgres FDW:tä

Mikropalveluarkkitehtuurilla, kuten kaikella tässä maailmassa, on hyvät ja huonot puolensa. Jotkut prosessit helpottuvat sen kanssa, toiset vaikeampia. Ja muutoksen nopeuden ja paremman skaalautuvuuden vuoksi sinun on tehtävä uhrauksia. Yksi niistä on analytiikan monimutkaistuminen. Jos monoliitissa kaikki operatiivinen analytiikka voidaan pelkistää SQL-kyselyiksi analyyttiseksi replikiksi, niin monipalveluarkkitehtuurissa jokaisella palvelulla on oma tietokanta ja näyttää siltä, ​​että yhtä kyselyä ei voi tehdä (tai ehkä voi?). Niille, jotka ovat kiinnostuneita siitä, kuinka ratkaisimme operatiivisen analytiikan ongelman yrityksessämme ja kuinka opimme elämään tämän ratkaisun kanssa - tervetuloa.

Toimintaanalytiikka mikropalveluarkkitehtuurissa: miten auttaa ja neuvoa Postgres FDW:tä
Nimeni on Pavel Sivash, DomClickissä työskentelen tiimissä, joka vastaa analyyttisen tietovaraston ylläpidosta. Perinteisesti toimintamme voidaan luokitella tietotekniikaksi, mutta itse asiassa tehtävävalikoima on paljon laajempi. Tietojen suunnitteluun on olemassa ETL/ELT-standardi, työkalujen tuki ja mukauttaminen tietojen analysointiin ja omien työkalujen kehittämiseen. Erityisesti operatiivisen raportoinnin osalta päätimme "teeskellä", että meillä on monoliitti ja antaa analyytikoille yhden tietokannan, joka sisältää kaikki heidän tarvitsemansa tiedot.

Yleisesti ottaen pohdimme erilaisia ​​vaihtoehtoja. Täysi arkiston rakentaminen oli mahdollista - jopa yritimme, mutta rehellisesti sanottuna emme pystyneet yhdistämään melko tiheitä logiikan muutoksia melko hitaaseen arkiston rakentamisprosessiin ja muutosten tekemiseen siihen (jos joku onnistui , kirjoita kommentteihin miten). Analyytikoille oli mahdollista sanoa: "Kaverit, opettele python ja mene analyyttisiin replikoihin", mutta tämä on lisävaatimus rekrytoinnissa, ja näytti siltä, ​​​​että tätä tulisi välttää, jos mahdollista. Päätimme kokeilla FDW-teknologiaa (Foreign Data Wrapper): pohjimmiltaan tämä on standardi dblink, joka on SQL-standardin mukainen, mutta omalla paljon kätevämmällä käyttöliittymällään. Sen perusteella teimme ratkaisun, joka lopulta tarttui, ja päädyimme siihen. Sen yksityiskohdat ovat erillisen artikkelin aiheena, ja ehkä useamman kuin yhden, koska haluan puhua paljon: tietokantakaavioiden synkronoinnista pääsyn hallintaan ja henkilötietojen depersonalisointiin. On myös tarpeen tehdä varaus, että tämä ratkaisu ei korvaa todellisia analyyttisiä tietokantoja ja arkistoja, vaan se ratkaisee vain tietyn ongelman.

Ylimmällä tasolla se näyttää tältä:

Toimintaanalytiikka mikropalveluarkkitehtuurissa: miten auttaa ja neuvoa Postgres FDW:tä
Siellä on PostgreSQL-tietokanta, johon käyttäjät voivat tallentaa työtietonsa, ja mikä tärkeintä, kaikkien palveluiden analyyttiset kopiot on yhdistetty tähän tietokantaan FDW:n kautta. Tämä mahdollistaa kyselyn kirjoittamisen useisiin tietokantoihin, eikä sillä ole väliä mikä se on: PostgreSQL, MySQL, MongoDB vai jokin muu (tiedosto, API, jos yhtäkkiä ei ole sopivaa käärettä, voit kirjoittaa oman). No, kaikki näyttää hienolta! Eroammeko?

Jos kaikki päättyisi niin nopeasti ja yksinkertaisesti, artikkelia ei todennäköisesti olisi.

On tärkeää tietää, kuinka Postgres käsittelee pyynnöt etäpalvelimille. Tämä vaikuttaa loogiselta, mutta usein ihmiset eivät kiinnitä siihen huomiota: Postgres jakaa pyynnön osiin, jotka suoritetaan itsenäisesti etäpalvelimilla, kerää nämä tiedot ja suorittaa lopulliset laskelmat itse, joten kyselyn suoritusnopeus riippuu suuresti miten se on kirjoitettu. On myös huomattava: kun tiedot saapuvat etäpalvelimelta, sillä ei ole enää indeksejä, mikään ei auta ajoittajaa, joten vain me itse voimme auttaa ja neuvoa häntä. Ja juuri tästä haluan puhua tarkemmin.

Yksinkertainen kysely ja suunnitelma sen kanssa

Katsotaanpa yksinkertaista suunnitelmaa näyttääksesi, kuinka Postgres tekee kyselyitä 6 miljoonan rivin taulukosta etäpalvelimella.

explain analyze verbose  
SELECT count(1)
FROM fdw_schema.table;

Aggregate  (cost=418383.23..418383.24 rows=1 width=8) (actual time=3857.198..3857.198 rows=1 loops=1)
  Output: count(1)
  ->  Foreign Scan on fdw_schema."table"  (cost=100.00..402376.14 rows=6402838 width=0) (actual time=4.874..3256.511 rows=6406868 loops=1)
        Output: "table".id, "table".is_active, "table".meta, "table".created_dt
        Remote SQL: SELECT NULL FROM fdw_schema.table
Planning time: 0.986 ms
Execution time: 3857.436 ms

VERBOSE-käskyn avulla voimme nähdä kyselyn, joka lähetetään etäpalvelimelle ja jonka tulokset saamme jatkokäsittelyä varten (RemoteSQL-rivi).

Mennään hieman pidemmälle ja lisätään useita suodattimia pyyntöömme: yksi varten boolean kenttä, yksi tapahtuman mukaan aikaleima välissä ja yksi kerrallaan jsonb.

explain analyze verbose
SELECT count(1)
FROM fdw_schema.table 
WHERE is_active is True
AND created_dt BETWEEN CURRENT_DATE - INTERVAL '7 month' 
AND CURRENT_DATE - INTERVAL '6 month'
AND meta->>'source' = 'test';

Aggregate  (cost=577487.69..577487.70 rows=1 width=8) (actual time=27473.818..25473.819 rows=1 loops=1)
  Output: count(1)
  ->  Foreign Scan on fdw_schema."table"  (cost=100.00..577469.21 rows=7390 width=0) (actual time=31.369..25372.466 rows=1360025 loops=1)
        Output: "table".id, "table".is_active, "table".meta, "table".created_dt
        Filter: (("table".is_active IS TRUE) AND (("table".meta ->> 'source'::text) = 'test'::text) AND ("table".created_dt >= (('now'::cstring)::date - '7 mons'::interval)) AND ("table".created_dt <= ((('now'::cstring)::date)::timestamp with time zone - '6 mons'::interval)))
        Rows Removed by Filter: 5046843
        Remote SQL: SELECT created_dt, is_active, meta FROM fdw_schema.table
Planning time: 0.665 ms
Execution time: 27474.118 ms

Tässä on se kohta, johon sinun on kiinnitettävä huomiota kyselyitä kirjoittaessasi. Suodattimia ei siirretty etäpalvelimelle, mikä tarkoittaa, että sen suorittamiseksi Postgres vetää pois kaikki 6 miljoonaa riviä suodattaakseen sitten paikallisesti (Filter row) ja suorittaakseen aggregoinnin. Menestyksen avain on kirjoittaa kysely niin, että suodattimet siirretään etäkoneeseen ja vastaanotamme ja aggregoimme vain tarvittavat rivit.

Tuo on järjetöntä paskaa

Boolen kentillä kaikki on yksinkertaista. Alkuperäisessä pyynnössä ongelma johtui operaattorista is. Jos korvaat sen =, niin saamme seuraavan tuloksen:

explain analyze verbose
SELECT count(1)
FROM fdw_schema.table
WHERE is_active = True
AND created_dt BETWEEN CURRENT_DATE - INTERVAL '7 month' 
AND CURRENT_DATE - INTERVAL '6 month'
AND meta->>'source' = 'test';

Aggregate  (cost=508010.14..508010.15 rows=1 width=8) (actual time=19064.314..19064.314 rows=1 loops=1)
  Output: count(1)
  ->  Foreign Scan on fdw_schema."table"  (cost=100.00..507988.44 rows=8679 width=0) (actual time=33.035..18951.278 rows=1360025 loops=1)
        Output: "table".id, "table".is_active, "table".meta, "table".created_dt
        Filter: ((("table".meta ->> 'source'::text) = 'test'::text) AND ("table".created_dt >= (('now'::cstring)::date - '7 mons'::interval)) AND ("table".created_dt <= ((('now'::cstring)::date)::timestamp with time zone - '6 mons'::interval)))
        Rows Removed by Filter: 3567989
        Remote SQL: SELECT created_dt, meta FROM fdw_schema.table WHERE (is_active)
Planning time: 0.834 ms
Execution time: 19064.534 ms

Kuten näet, suodatin lensi etäpalvelimelle, ja suoritusaika lyheni 27 sekunnista 19 sekuntiin.

On syytä huomata, että operaattori is erilainen kuin operaattori = koska se voi toimia Null-arvon kanssa. Se tarkoittaa sitä ei ole totta jättää arvot False ja Null suodattimeen, kun taas != Totta jättää vain vääriä arvoja. Siksi operaattoria vaihdettaessa ei ole kaksi ehtoa OR-operaattorilla tulee välittää suodattimelle, esimerkiksi WHERE (sarake != tosi) TAI (sarake on tyhjä).

Olemme käsitelleet Boolen, jatketaan. Palautetaan toistaiseksi Boolen suodatin alkuperäiseen muotoonsa, jotta voidaan itsenäisesti tarkastella muiden muutosten vaikutusta.

aikaleima? hz

Yleensä joudut usein kokeilemaan, kuinka kirjoittaa oikein etäpalvelimia sisältävä pyyntö, ja vasta sitten etsimään selitystä, miksi näin tapahtuu. Internetistä löytyy hyvin vähän tietoa tästä. Joten kokeissa havaitsimme, että kiinteä päivämääräsuodatin lentää etäpalvelimelle räjähdysmäisesti, mutta kun haluamme asettaa päivämäärän dynaamisesti, esimerkiksi now() tai CURRENT_DATE, niin tämä ei tapahdu. Esimerkissämme lisäsimme suodattimen, jotta Create_at -sarake sisälsi tiedot täsmälleen 1 kuukauden ajalta (BETWEEN CURRENT_DATE - INTERVAL '7 kuukautta' JA CURRENT_DATE - INTERVAL '6 kuukautta'). Mitä teimme tässä tapauksessa?

explain analyze verbose
SELECT count(1)
FROM fdw_schema.table 
WHERE is_active is True
AND created_dt >= (SELECT CURRENT_DATE::timestamptz - INTERVAL '7 month') 
AND created_dt <(SELECT CURRENT_DATE::timestamptz - INTERVAL '6 month')
AND meta->>'source' = 'test';

Aggregate  (cost=306875.17..306875.18 rows=1 width=8) (actual time=4789.114..4789.115 rows=1 loops=1)
  Output: count(1)
  InitPlan 1 (returns $0)
    ->  Result  (cost=0.00..0.02 rows=1 width=8) (actual time=0.007..0.008 rows=1 loops=1)
          Output: ((('now'::cstring)::date)::timestamp with time zone - '7 mons'::interval)
  InitPlan 2 (returns $1)
    ->  Result  (cost=0.00..0.02 rows=1 width=8) (actual time=0.002..0.002 rows=1 loops=1)
          Output: ((('now'::cstring)::date)::timestamp with time zone - '6 mons'::interval)
  ->  Foreign Scan on fdw_schema."table"  (cost=100.02..306874.86 rows=105 width=0) (actual time=23.475..4681.419 rows=1360025 loops=1)
        Output: "table".id, "table".is_active, "table".meta, "table".created_dt
        Filter: (("table".is_active IS TRUE) AND (("table".meta ->> 'source'::text) = 'test'::text))
        Rows Removed by Filter: 76934
        Remote SQL: SELECT is_active, meta FROM fdw_schema.table WHERE ((created_dt >= $1::timestamp with time zone)) AND ((created_dt < $2::timestamp with time zone))
Planning time: 0.703 ms
Execution time: 4789.379 ms

Käskimme suunnittelijaa laskea alikyselyssä päivämäärän etukäteen ja välittää valmiin muuttujan suodattimelle. Ja tämä vihje antoi meille erinomaisen tuloksen, pyynnöstä tuli melkein 6 kertaa nopeampi!

Tässäkin on tärkeää olla varovainen: alikyselyn tietotyypin on oltava sama kuin sen kentän tietotyypin, jota suodatamme, muuten suunnittelija päättää, että koska tyypit ovat erilaisia, on ensin hankittava kaikki tiedot ja suodattaa ne paikallisesti.

Palautetaan päivämääräsuodatin alkuperäiseen arvoonsa.

Freddy vs. Jsonb

Yleensä Boolen kentät ja päivämäärät ovat jo nopeuttaneet kyselyämme riittävästi, mutta yksi tietotyyppi oli vielä jäljellä. Taistelu sen suodatuksen kanssa, rehellisesti sanottuna, ei ole vieläkään ohi, vaikka menestystä on täälläkin. Joten näin onnistuimme ohittamaan suodattimen jsonb kenttään etäpalvelimelle.

explain analyze verbose
SELECT count(1)
FROM fdw_schema.table 
WHERE is_active is True
AND created_dt BETWEEN CURRENT_DATE - INTERVAL '7 month' 
AND CURRENT_DATE - INTERVAL '6 month'
AND meta @> '{"source":"test"}'::jsonb;

Aggregate  (cost=245463.60..245463.61 rows=1 width=8) (actual time=6727.589..6727.590 rows=1 loops=1)
  Output: count(1)
  ->  Foreign Scan on fdw_schema."table"  (cost=1100.00..245459.90 rows=1478 width=0) (actual time=16.213..6634.794 rows=1360025 loops=1)
        Output: "table".id, "table".is_active, "table".meta, "table".created_dt
        Filter: (("table".is_active IS TRUE) AND ("table".created_dt >= (('now'::cstring)::date - '7 mons'::interval)) AND ("table".created_dt <= ((('now'::cstring)::date)::timestamp with time zone - '6 mons'::interval)))
        Rows Removed by Filter: 619961
        Remote SQL: SELECT created_dt, is_active FROM fdw_schema.table WHERE ((meta @> '{"source": "test"}'::jsonb))
Planning time: 0.747 ms
Execution time: 6727.815 ms

Operaattoreiden suodattamisen sijaan sinun on käytettävä yhden operaattorin läsnäoloa jsonb toisessa. 7 sekuntia alkuperäisen 29 sijaan. Tämä on toistaiseksi ainoa onnistunut vaihtoehto suodattimien lähettämiseen kautta jsonb etäpalvelimelle, mutta tässä on tärkeää ottaa huomioon yksi rajoitus: käytämme tietokannan versiota 9.6, mutta huhtikuun loppuun mennessä aiomme suorittaa viimeiset testit ja siirtyä versioon 12. Kun päivitämme, kirjoitamme kuinka se vaikutti, koska on olemassa melko paljon muutoksia, joihin on paljon toivoa: json_path, uusi CTE-käyttäytyminen, push down (olemassa versiosta 10 lähtien). Haluan todella kokeilla sitä pian.

Viimeistele hänet

Testasimme, miten kukin muutos vaikutti pyyntönopeuteen yksitellen. Katsotaan nyt, mitä tapahtuu, kun kaikki kolme suodatinta on kirjoitettu oikein.

explain analyze verbose
SELECT count(1)
FROM fdw_schema.table 
WHERE is_active = True
AND created_dt >= (SELECT CURRENT_DATE::timestamptz - INTERVAL '7 month') 
AND created_dt <(SELECT CURRENT_DATE::timestamptz - INTERVAL '6 month')
AND meta @> '{"source":"test"}'::jsonb;

Aggregate  (cost=322041.51..322041.52 rows=1 width=8) (actual time=2278.867..2278.867 rows=1 loops=1)
  Output: count(1)
  InitPlan 1 (returns $0)
    ->  Result  (cost=0.00..0.02 rows=1 width=8) (actual time=0.010..0.010 rows=1 loops=1)
          Output: ((('now'::cstring)::date)::timestamp with time zone - '7 mons'::interval)
  InitPlan 2 (returns $1)
    ->  Result  (cost=0.00..0.02 rows=1 width=8) (actual time=0.003..0.003 rows=1 loops=1)
          Output: ((('now'::cstring)::date)::timestamp with time zone - '6 mons'::interval)
  ->  Foreign Scan on fdw_schema."table"  (cost=100.02..322041.41 rows=25 width=0) (actual time=8.597..2153.809 rows=1360025 loops=1)
        Output: "table".id, "table".is_active, "table".meta, "table".created_dt
        Remote SQL: SELECT NULL FROM fdw_schema.table WHERE (is_active) AND ((created_dt >= $1::timestamp with time zone)) AND ((created_dt < $2::timestamp with time zone)) AND ((meta @> '{"source": "test"}'::jsonb))
Planning time: 0.820 ms
Execution time: 2279.087 ms

Kyllä, pyyntö näyttää monimutkaisemmalta, tämä on pakotettu maksu, mutta suoritusnopeus on 2 sekuntia, mikä on yli 10 kertaa nopeampi! Ja puhumme yksinkertaisesta kyselystä suhteellisen pienessä tietojoukossa. Todellisista pyynnöistä saimme jopa useita satoja kertoja lisäyksen.

Yhteenvetona: jos käytät PostgreSQL:ää FDW:n kanssa, tarkista aina, että kaikki suodattimet lähetetään etäpalvelimelle, niin olet tyytyväinen... Ainakin siihen asti kunnes pääset liitoksiin eri palvelimien taulukoiden välillä. Mutta se on tarina toiselle artikkelille.

Kiitos huomiostasi! Haluaisin kuulla kommenteissa kysymyksiä, kommentteja ja tarinoita kokemuksistasi.

Lähde: will.com

Lisää kommentti