Оперативна аналитика у архитектури микросервиса: помоћ и брзи Постгрес ФДВ

Микросервисна архитектура, као и све на овом свету, има своје предности и мане. Неки процеси постају лакши са њим, други тежи. А зарад брзине промена и боље скалабилности, потребно је да се жртвујете. Једна од њих је све већа сложеност аналитике. Ако се у монолиту сва оперативна аналитика може свести на СКЛ упите до аналитичке реплике, онда у мултисервисној архитектури сваки сервис има своју базу података и чини се да један упит не може да се уради (или можда може?). За оне које занима како смо решили проблем оперативне аналитике у нашој компанији и како смо научили да живимо са овим решењем - добродошли.

Оперативна аналитика у архитектури микросервиса: помоћ и брзи Постгрес ФДВ
Моје име је Павел Сиваш, у ДомЦлицк-у радим у тиму који је одговоран за одржавање складишта аналитичких података. Уобичајено, наше активности се могу класификовати као инжењеринг података, али, у ствари, опсег задатака је много шири. Постоје ЕТЛ/ЕЛТ стандард за инжењеринг података, подршку и прилагођавање алата за анализу података и развој сопствених алата. Конкретно, за оперативно извештавање, одлучили смо да се „претварамо” да имамо монолит и дамо аналитичарима једну базу података која ће садржати све податке који су им потребни.

Генерално, разматрали смо различите опције. Било је могуће направити пуноправно спремиште - чак смо и покушали, али, да будемо искрени, нисмо били у могућности да комбинујемо прилично честе промене у логици са прилично спорим процесом изградње спремишта и уношења измена у њега (ако је неко успео , напишите у коментарима како). Аналитичарима је било могуће рећи: „Момци, научите питон и идите на аналитичке реплике“, али то је додатни услов за регрутовање и чинило се да то треба избегавати ако је могуће. Одлучили смо да покушамо да користимо ФДВ (Фореигн Дата Враппер) технологију: у суштини, ово је стандардни дблинк, који је у СКЛ стандарду, али са сопственим много погоднијим интерфејсом. На основу тога смо направили решење, које се на крају ухватило и на њему смо се определили. Његови детаљи су тема посебног чланка, а можда и више од једног, пошто желим да причам о много чему: од синхронизације шема базе података до контроле приступа и деперсонализације личних података. Такође, потребно је резервисати да ово решење није замена за праве аналитичке базе података и репозиторијума, оно решава само конкретан проблем.

На највишем нивоу то изгледа овако:

Оперативна аналитика у архитектури микросервиса: помоћ и брзи Постгрес ФДВ
Постоји ПостгреСКЛ база података у којој корисници могу да чувају своје радне податке, а што је најважније, аналитичке реплике свих сервиса су повезане са овом базом података преко ФДВ-а. Ово омогућава писање упита за неколико база података, и није важно шта је то: ПостгреСКЛ, МиСКЛ, МонгоДБ или нешто друго (датотека, АПИ, ако одједном нема одговарајућег омотача, можете написати свој). Па, све изгледа одлично! Да ли се растајемо?

Да се ​​све завршило тако брзо и једноставно, онда, вероватно, не би било ни чланка.

Важно је да буде јасно како Постгрес обрађује захтеве удаљеним серверима. Ово изгледа логично, али људи често не обраћају пажњу на то: Постгрес дели захтев на делове који се извршавају независно на удаљеним серверима, прикупља ове податке и сам обавља коначне прорачуне, тако да ће брзина извршења упита у великој мери зависити од како је написано. Такође треба напоменути: када подаци стигну са удаљеног сервера, они више немају индексе, не постоји ништа што ће помоћи планеру, стога само ми сами можемо да му помогнемо и саветујемо. И управо о овоме желим да причам детаљније.

Једноставан упит и план са њим

Да бисмо показали како Постгрес поставља упите према табели од 6 милиона редова на удаљеном серверу, погледајмо једноставан план.

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

Коришћење наредбе ВЕРБОСЕ нам омогућава да видимо упит који ће бити послат удаљеном серверу и чије ћемо резултате добити на даљу обраду (РемотеСКЛ линија).

Идемо мало даље и нашем захтеву додамо неколико филтера: један за боолеан поље, по једном појављивању Време у интервалу и један по јсонб.

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

Овде лежи тачка на коју треба да обратите пажњу када пишете упите. Филтери нису пребачени на удаљени сервер, што значи да да би га извршио, Постгрес извлачи свих 6 милиона редова да би затим локално филтрирао (Филтер ров) и извршио агрегацију. Кључ успеха је писање упита тако да се филтери пренесу на удаљену машину, а ми примамо и агрегирамо само потребне редове.

То је глупост

Са логичким пољима све је једноставно. У првобитном захтеву, проблем је настао због оператера is. Ако га замените са =, онда добијамо следећи резултат:

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

Као што видите, филтер је одлетео на удаљени сервер, а време извршења је смањено са 27 на 19 секунди.

Вреди напоменути да оператер is различито од оператера = јер може да ради са нултом вредношћу. То значи да није истина ће оставити вредности Фалсе и Нулл у филтеру, док != Истина оставиће само лажне вредности. Стога, приликом замене оператера није два услова са оператором ОР треба проследити филтеру, нпр. ВХЕРЕ (кол != Тачно) ИЛИ (колона је нула).

Средили смо боолеан, идемо даље. За сада, вратимо Булов филтер у првобитни облик како бисмо самостално размотрили ефекат других промена.

тиместамптз? хз

Генерално, често морате да експериментишете како да правилно напишете захтев који укључује удаљене сервере, а тек онда тражите објашњење зашто се то дешава. На Интернету се може наћи врло мало информација о томе. Дакле, у експериментима смо открили да филтер фиксног датума лети на удаљени сервер са праском, али када желимо да динамички подесимо датум, на пример, нов() или ЦУРРЕНТ_ДАТЕ, то се не дешава. У нашем примеру смо додали филтер тако да колона цреатед_ат садржи податке за тачно 1 месец у прошлости (ИЗМЕЂУ ЦУРРЕНТ_ДАТЕ - ИНТЕРВАЛ '7 месеци' И ЦУРРЕНТ_ДАТЕ - ИНТЕРВАЛ '6 монтхс'). Шта смо урадили у овом случају?

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

Рекли смо планеру да унапред израчуна датум у потупиту и проследи готову променљиву у филтер. И овај наговештај нам је дао одличан резултат, захтев је постао скоро 6 пута бржи!

Опет, овде је важно да будете пажљиви: тип података у потупиту мора бити исти као у пољу на којем филтрирамо, иначе ће планер одлучити да пошто су типови различити, потребно је прво добити све податке и филтрирати их локално.

Вратимо филтер датума на првобитну вредност.

Фредди вс. Јсонб

Генерално, Булова поља и датуми су већ довољно убрзали наш упит, али је остао још један тип података. Битка са филтрирањем по њој, да будем искрен, још увек није завршена, иако и овде има успеха. Дакле, овако смо успели да прођемо филтер јсонб поље на удаљени сервер.

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

Уместо оператора филтрирања, морате користити присуство једног оператора јсонб у другачијем. 7 секунди уместо оригиналних 29. До сада је ово једина успешна опција за пренос филтера преко јсонб на удаљени сервер, али овде је важно узети у обзир једно ограничење: користимо верзију 9.6 базе података, али до краја априла планирамо да завршимо последње тестове и пређемо на верзију 12. Када ажурирамо, писаћемо о томе како је то утицало, јер има доста промена за које постоји много наде: јсон_патх, ново ЦТЕ понашање, пусх довн (постоје од верзије 10). Стварно желим да пробам ускоро.

Завршити га

Тестирали смо како свака промена утиче на брзину захтева појединачно. Хајде сада да видимо шта се дешава када су сва три филтера исправно написана.

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

Да, захтев изгледа компликованије, ово је принудна накнада, али брзина извршења је 2 секунде, што је више од 10 пута брже! А ми говоримо о једноставном упиту према релативно малом скупу података. На стварне захтеве добили смо повећање и до неколико стотина пута.

Да резимирамо: ако користите ПостгреСКЛ са ФДВ, увек проверите да ли су сви филтери послати на удаљени сервер, и бићете срећни... Барем док не дођете до спајања табела са различитих сервера. Али то је прича за други чланак.

Хвала на пажњи! Волео бих да чујем питања, коментаре и приче о вашим искуствима у коментарима.

Извор: ввв.хабр.цом

Додај коментар