MikrotjĂ€nstarkitektur, precis som allt annat i denna vĂ€rld, har sina fördelar och nackdelar. Vissa processer blir enklare med den, andra blir svĂ„rare. För att Ă„stadkomma snabbare förĂ€ndringar och bĂ€ttre skalbarhet mĂ„ste man göra vissa uppoffringar. En av dessa Ă€r kompliceringen av analysen. Om man i ett monolitiskt system kan reducera all operativ analys till SQL-frĂ„gor mot en analytisk replika, sĂ„ har varje tjĂ€nst i en multitjĂ€nstarkitektur sin egen databas, och det verkar som att man inte kan klara sig med en enda frĂ„ga (eller kanske kan?). För dem som Ă€r intresserade av hur vi löste problemet med operativ analys i vĂ„rt företag och hur vi har lĂ€rt oss att leva med denna lösning â vĂ€lkomna.

Jag heter Pavel Sivash, och pÄ DomKlik jobbar jag i teamet som ansvarar för underhÄll av det analytiska datalageret. VÄr verksamhet kan grovt kategoriseras som dataengineering, men egentligen Àr spektrum av uppgifter mycket bredare. Det handlar om standarder för dataengineering som ETL/ELT, stöd och anpassning av verktyg för dataanalys samt utveckling av egna verktyg. Specifikt för operativ rapportering har vi valt att "lÄtsas" att vi har ett monolitiskt system och ge analytikerna en databas dÀr all nödvÀndig information finns.
Vi har övervĂ€gt olika alternativ. Man skulle kunna bygga ett fullstĂ€ndigt lagringssystem â vi provade till och med det, men Ă€rligt talat lyckades vi aldrig fĂ„ tillrĂ€ckligt med frekventa förĂ€ndringar i logiken att stĂ€mma överens med den relativt lĂ„ngsamma processen för att bygga lagringen och göra Ă€ndringar i den (om nĂ„gon lyckades, skriv gĂ€rna i kommentarerna hur). Man kunde ha sagt till analytikerna: "Grabbar, lĂ€r er python och jobba med analytiska repliker", men det skulle innebĂ€ra ett extra krav pĂ„ att rekrytera personal, och det verkade som nĂ„got att undvika om möjligt. Vi beslöt oss för att prova teknologin FDW (Foreign Data Wrapper): i grunden Ă€r det en standard dblink som finns i SQL-standarden, men med ett mycket mer anvĂ€ndarvĂ€nligt grĂ€nssnitt. Baserat pĂ„ detta skapade vi en lösning som slutligen blev accepterad, och det Ă€r dĂ€r vi har stannat. Detaljerna kring detta Ă€r Ă€mnet för en separat artikel, och kanske för flera, eftersom vi har mycket att berĂ€tta: frĂ„n synkronisering av databasstrukturer till Ă„tkomsthantering och anonymisering av personuppgifter. Det bör ocksĂ„ nĂ€mnas att denna lösning inte ersĂ€tter verkliga analytiska databaser och lagringssystem; den löser bara en specifik uppgift.
Ăvergripande ser det ut sĂ„ hĂ€r:

Det finns en PostgreSQL-databas dÀr anvÀndarna kan lagra sina arbetsdata, och det viktigaste Àr att denna databas Àr ansluten till analytiska repliker av alla tjÀnster via FDW. Detta ger möjlighet att skriva en frÄga till flera databaser, oavsett om det handlar om: PostgreSQL, MySQL, MongoDB eller nÄgot annat (en fil, API, om det inte finns nÄgon passande wrapper kan man skriva sin egen). SÄ, allt verkar bra! Ska vi avsluta hÀr?
Om allt slutade sÄ snabbt och enkelt, skulle det antagligen inte finnas nÄgon artikel.
Det Àr viktigt att tydligt förstÄ hur Postgres hanterar begÀrningar till fjÀrrservrar. Det verkar logiskt, men mycket ofta uppmÀrksammar man inte detta: Postgres delar upp begÀran i delar som körs pÄ fjÀrrservrar oberoende av varandra, samlar dessa data och gör de slutgiltiga berÀkningarna sjÀlv, sÄ hastigheten för att utföra en begÀran beror starkt pÄ hur den Àr skriven. Det bör ocksÄ pÄpekas att nÀr data kommer frÄn en fjÀrrserver har de redan inga index, och inget som kan hjÀlpa planeringsverktyget; dÀrför kan vi endast hjÀlpa och vÀgleda det sjÀlva. Och det Àr just detta jag vill berÀtta mer om.
Ett enkelt förfrÄgan och planen med den
För att visa hur PostgreSQL utför en frÄga mot en tabell med 6 miljoner rader pÄ en avlÀgsen servern, lÄt oss titta pÄ den enkla planen.
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 msAnvÀndningen av VERBOSE-instruktionen möjliggör att se den frÄga som kommer att skickas till den avlÀgsna servern och vars resultat vi fÄr för vidare behandling (rad RemoteSQL).
LÄt oss gÄ lite lÀngre och lÀgga till nÄgra filter i vÄr frÄga: ett pÄ boolean fÀltet, ett pÄ förekomsten timestamp inom intervallet och ett pÄ 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 msDet Àr precis hÀr som det finns en punkt som behöver uppmÀrksammas vid skrivning av frÄgor. Filtren överfördes inte till den avlÀgsna servern, vilket betyder att PostgreSQL hÀmtar alla 6 miljoner rader för att sedan lokalt filtrera (rad Filter) och utföra aggregationen. Nyckeln till framgÄng Àr att skriva frÄgan sÄ att filtren överförs till den avlÀgsna maskinen, sÄ att vi fÄr och aggregerar endast de nödvÀndiga raderna.
Det Àr en del booleanshit
Med booleanfÀlt Àr allt enkelt. Problemet i den ursprungliga frÄgan uppstod pÄ grund av operatorn is. Om vi byter ut den mot =, fÄr vi följande resultat:
förklara analysera detaljerat
VĂLJ rĂ€kna(1)
FRĂ
N fdw_schema.tabell
DĂR is_active = Sant
OCH created_dt MELLAN NU_DAG - INTERVAL '7 mÄnader'
OCH NU_DAG - INTERVAL '6 mÄnader'
OCH meta->>'source' = 'test';
Aggregat (kostnad=508010.14..508010.15 rader=1 bredd=8) (faktisk tid=19064.314..19064.314 rader=1 loop=1)
Utmatning: rÀkna(1)
-> UtlÀndsk skanning pÄ fdw_schema."tabell" (kostnad=100.00..507988.44 rader=8679 bredd=0) (faktisk tid=33.035..18951.278 rader=1360025 loop=1)
Utmatning: "tabell".id, "tabell".is_active, "tabell".meta, "tabell".created_dt
Filter: ((("tabell".meta ->> 'source'::text) = 'test'::text) OCH ("tabell".created_dt >= (('nu'::cstring)::datum - '7 mÄn'::intervall)) OCH ("tabell".created_dt <= ((('nu'::cstring)::datum)::tidsstÀmpel med tidszon - '6 mÄn'::intervall)))
Rader borttagna av filter: 3567989
FjĂ€rr SQL: VĂLJ created_dt, meta FRĂ
N fdw_schema.tabell DĂR (is_active)
Planeringstid: 0.834 ms
Exekveringstid: 19064.534 msSom ni ser har filtret gÄtt till den fjÀrranslutna servern, och exekveringstiden har minskat frÄn 27 till 19 sekunder.
Det Ă€r vĂ€rt att notera att operatorn is skiljer sig frĂ„n operatorn = i att den kan hantera vĂ€rdet Null. Detta innebĂ€r att is not True i filtret lĂ€mnar vĂ€rdena Falskt och Null, medan != True bara lĂ€mnar vĂ€rdena Falskt. DĂ€rför, vid byte av operatorn is not bör man ange tvĂ„ villkor i filtret med operatorn OR, till exempel, DĂR (col != True) ELLER (col is null).
Vi har kommit överens om boolean, lÄt oss gÄ vidare. Under tiden ÄterstÀller vi filtret för booleska vÀrden till sitt ursprungliga tillstÄnd för att oberoende övervÀga effekten av andra förÀndringar.
timestamptz? hz
Ăverlag mĂ„ste man ofta experimentera med hur man korrekt skriver en frĂ„ga som involverar fjĂ€rrservrar, och först dĂ€refter söka förklaringar till varför saker och ting fungerar sĂ„. Mycket lite information finns att hitta om detta pĂ„ Internet. I vĂ„ra experiment har vi upptĂ€ckt att ett filter för ett fast datum gĂ„r undan till den fjĂ€rranslutna servern, men nĂ€r vi vill stĂ€lla in ett datum dynamiskt, till exempel now() eller CURRENT_DATE, sĂ„ hĂ€nder inte detta. I vĂ„rt exempel lade vi till ett sĂ„dant filter för att kolumnen created_at skulle innehĂ„lla data exakt en mĂ„nad tillbaka i tiden (MELLAN NU_DAG - INTERVAL '7 mĂ„nader' OCH NU_DAG - INTERVAL '6 mĂ„nader'). Vad gjorde vi i det hĂ€r fallet?
förklara analysera detaljerat
VĂLJ count(1)
FRĂ
N fdw_schema.tabell
DĂR is_active Ă€r Sant
OCH created_dt >= (VĂLJ CURRENT_DATE::timestamptz - INTERVAL '7 mĂ„nader')
OCH created_dt >'source' = 'test';
Aggregat (kostnad=306875.17..306875.18 rader=1 bredd=8) (faktisk tid=4789.114..4789.115 rader=1 loopar=1)
Utdata: count(1)
InitPlan 1 (returnerar $0)
-> Resultat (kostnad=0.00..0.02 rader=1 bredd=8) (faktisk tid=0.007..0.008 rader=1 loopar=1)
Utdata: ((('nu'::cstring)::datum)::timestamp med tidszon - '7 mÄn'::intervall)
InitPlan 2 (returnerar $1)
-> Resultat (kostnad=0.00..0.02 rader=1 bredd=8) (faktisk tid=0.002..0.002 rader=1 loopar=1)
Utdata: ((('nu'::cstring)::datum)::timestamp med tidszon - '6 mÄn'::intervall)
-> Utlandsundersökning pÄ fdw_schema."tabell" (kostnad=100.02..306874.86 rader=105 bredd=0) (faktisk tid=23.475..4681.419 rader=1360025 loopar=1)
Utdata: "tabell".id, "tabell".is_active, "tabell".meta, "tabell".created_dt
Filter: (("tabell".is_active ĂR SANT) OCH (("tabell".meta ->> 'source'::text) = 'test'::text))
Rader borttagna av filter: 76934
FjĂ€rr SQL: VĂLJ is_active, meta FRĂ
N fdw_schema.tabell DĂR ((created_dt >= $1::timestamp med tidszon)) OCH ((created_dt < $2::timestamp med tidszon))
Planeringstid: 0.703 ms
Utförandetid: 4789.379 msVi föreslog för planeringsverktyget att berÀkna datumet i underfrÄgan och skicka den fÀrdiga variabeln till filtret. Och denna rekommendation gav oss ett utmÀrkt resultat, förfrÄgan blev nÀstan sex gÄnger snabbare!
à terigen, det Àr viktigt att vara uppmÀrksam: datatypen i underfrÄgan mÄste vara densamma som för fÀltet vi filtrerar pÄ, annars kommer planeringsverktyget att besluta att eftersom typerna Àr olika mÄste alla data först hÀmtas och sedan filtreras lokalt.
Vi ÄtergÄr till datumfiltret till sitt ursprungliga vÀrde.
Freddy mot Jsonb
Ăverlag har booleska fĂ€lt och datum redan accelererat vĂ„r förfrĂ„gan ganska mycket, men det fanns fortfarande en datatyp kvar. Striden med filtreringen pĂ„ den, Ă€rligt talat, Ă€r fortfarande inte avslutad, Ă€ven om dĂ€r finns framsteg. SĂ„ hĂ€r lyckades vi skicka filtret för jsonb fĂ€ltet till den fjĂ€rrserver.
förklara analysera detaljerat
VĂLJ count(1)
FRĂ
N fdw_schema.tabell
DĂR is_active Ă€r Sant
OCH created_dt mellan CURRENT_DATE - INTERVAL '7 mÄnader'
OCH CURRENT_DATE - INTERVAL '6 mÄnader'
OCH meta @> '{"source":"test"}'::jsonb;
Aggregat (kostnad=245463.60..245463.61 rader=1 bredd=8) (faktisk tid=6727.589..6727.590 rader=1 loopar=1)
Utdata: count(1)
-> Utlandsundersökning pÄ fdw_schema."tabell" (kostnad=1100.00..245459.90 rader=1478 bredd=0) (faktisk tid=16.213..6634.794 rader=1360025 loopar=1)
Utdata: "tabell".id, "tabell".is_active, "tabell".meta, "tabell".created_dt
Filter: (("tabell".is_active ĂR SANT) OCH ("tabell".created_dt >= (('nu'::cstring)::datum - '7 mĂ„n'::intervall)) OCH ("tabell".created_dt '{"source": "test"}'::jsonb))
Planeringstid: 0.747 ms
Utförandetid: 6727.815 msIstÀllet för filtreringsoperatorer ska en operator för nÀrvaro anvÀndas. jsonb i en annan. 7 sekunder istÀllet för ursprungliga 29. Detta Àr hittills det enda framgÄngsrika sÀttet att överföra filter genom jsonb till en fjÀrrserver, men hÀr Àr det viktigt att ta hÀnsyn till en begrÀnsning: vi anvÀnder version 9.6 av databasen, men fram till slutet av april planerar vi att slutföra de sista testerna och uppgradera till version 12. SÄ snart vi har uppdaterat kommer vi att skriva om hur detta pÄverkade oss, eftersom det finns mÄnga förÀndringar som vi har stora förhoppningar pÄ: json_path, nytt beteende för CTE, push down (som funnits sedan version 10). Det skulle vara bra att prova snart.
Finish him
Vi har kontrollerat hur varje Àndring pÄverkar hastigheten för förfrÄgan var för sig. LÄt oss nu se vad som hÀnder nÀr alla tre filter Àr korrekt skrivna.
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 '{"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 '{"source": "test"}'::jsonb))
Planning time: 0.820 ms
Execution time: 2279.087 msJa, förfrÄgan verkar mer komplex, det Àr en oundviklig kostnad, men exekveringstiden Àr 2 sekunder, vilket Àr mer Àn 10 gÄnger snabbare! Och detta handlar om en enkel förfrÄgan mot en relativt liten datamÀngd. Vid verkliga förfrÄgningar har vi fÄtt en ökning med upp till flera hundra gÄnger.
Sammanfattningsvis: Om du anvÀnder PostgreSQL med FDW, kontrollera alltid att alla filter skickas till fjÀrrservern, sÄ blir du lycklig⊠Ätminstone tills du nÄr join mellan tabeller frÄn olika servrar. Men det Àr redan en historia för en annan artikel.
Tack för din uppmÀrksamhet! Jag ser fram emot frÄgor, kommentarer och Àven berÀttelser om din erfarenhet i kommentarerna.
KĂ€lla: habr.com
