PostgreSQL-де JIT-компиляция
Бүгін JIT-компиляцияның көмектеспей, керісінше зиян келтіретін жағдайға тап болдым! JIT-компиляция қосылғанда сұрау уақытының сегіз есе артуы қалай мүмкін? Мұны түсініп көрейік.
Аздап тарихқа шолу
PostgreSQL-дегі JIT (Just-In-Time compilation) қымбат сұрауларды орындауды жеделдету мақсатында орындалу жоспарының бір бөлігін нативті машиналық кодқа компиляциялау арқылы пайда болды. Идеясы — өрнектерді (фильтрлер, есептеулер, агрегаттар) интерпретациялаудан бас тартып, оларды «темірге» барынша жақын орындау. Бұл үшін PostgreSQL LLVM-ді қолданады, ол нақты сұрауға арналған машиналық кодты жылдам генерациялап, оңтайландырады.
JIT ең алдымен CPU-ға жүктеме түсіретін аналитикалық сценарийлер үшін ойластырылған: күрделі өрнектер, үлкен деректер көлемі, ұзақ сұраулар, мұнда компиляцияның құны орындау уақытымен ақталады. Мұндай жағдайларда интерпретатордың шығынын азайту және алдын ала жасау мүмкін емес немесе тиімсіз агрессивті оңтайландырулар арқылы ұтысқа жетуге болады. OLAP жүктемесінде бұл шынымен де айтарлықтай өсім бере алады — бірақ тек JIT өзін ақтайтындай «жеткілікті ауыр» сұрау болса ғана.
JIT типтік OLTP үшін жобаланбаған.
JIT PostgreSQL 11-де эксперименталды және әдепкіде өшірілген мүмкіндік ретінде пайда болды. PostgreSQL 12-ден бастап JIT әдепкіде қосылған (jit = on), бірақ маңызды ескертумен: ол тек шығын шектерінен асқанда іске қосылады (jit_above_cost, jit_inline_above_cost, jit_optimize_above_cost).
PostgreSQL әзірлеушілері PostgreSQL-ді ұзақ уақыт бойы және белсенді түрде OLAP немесе аналитикалық ДБ ретінде қолданатынын ескерді: есептер, ad-hoc аналитика, ауыр SELECT агрегацияларымен және т.б. Мұндай сұраулар секундтар мен минуттар бойы орындалады, күрделі өрнектерді қамтиды, сирек іске қосылады, бірақ процессорды «күйдіреді». Дәл осы сценарийде JIT-компиляцияның құны жалпы орындау уақытында жоғалады және өрнектер мен фильтрлерді жеделдету мағынасы бар.
Міне, осы жерде шындықпен алшақтық басталады.
Іс жүзінде PostgreSQL көбінесе веб-қосымшалар үшін OLTP-дерекқоры ретінде қолданылады: CRUD, API, бэкендтер, микросервистер. Мұндай жүктемелердің басым сипаттамалары — қысқа сұраулар (миллисекундтар), жоғары жиілік, көптеген қарапайым SELECT / INSERT / UPDATE. Бұл жүктеме класы үшін JIT шығыны жиі ақталмайды және тіпті planner cost бойынша «қымбат» сұрау шын мәнінде қысқа болуы мүмкін, ал компиляция + LLVM орындау уақытымен салыстырмалы уақыт алуы мүмкін.
PostgreSQL-дің әмбебаптығы өзіне қарсы жұмыс істеді.
PostgreSQL бір уақытта OLTP және OLAP, аналитикада белсенді қолданылады және жиі «әмбебап ДБ» ретінде орналастырылады. Сондықтан JIT-ті жаһандық қосу туралы шешім қабылданды, бірақ оны шығынға негізделген шектер арқылы шектеу және соңғы баптауды нақты жүктемеге инженерлерге тапсыру. Бұл ДББЖ ядросы тұрғысынан ақылға қонымды, бірақ EXPLAIN ANALYZE-ды қарамасаңыз, өндірісте қауіпті.
PostgreSQL-де бір нәрсені түсіну маңызды: planner cost — бұл уақыт емес және оны болжауға тырысу да емес. Бұл жоспарды таңдауға ғана қолданылатын жоспардың салыстырмалы «қымбаттығының» абстрактілі моделі, кідірісті бағалау үшін емес.
Planner cost шартты бірліктермен өлшенеді, миллисекундтарға байланбаған, CPU-дың нақты өнімділігін ескермейді және жүйенің ағымдағы жүктемесі туралы ештеңе білмейді. Бұл оның өрескел эвристика екенін, өлшемдер емес екенін білдіреді.
Planner Cost жоспар ұзақ орындалғанда және жоспар нұсқалары арасындағы айырмашылық айтарлықтай болғанда жақсы жұмыс істейді, бірақ орындау 5–20 мс алатын қысқа сұраулар үшін JIT 10–30 мс қосады, ал planner бұл «қымбат» сұрау деп санайды. Шығын тұрғысынан бұл «ақталады», бірақ пайдаланушы тұрғысынан объективті түрде баяулады.
PostgreSQL-де JIT-ті қалай дұрыс шектеу немесе өшіру керек
Маңызды нәрсені бірден бекіту керек: JIT — бұл бинарлық таңдау емес. Оны міндетті түрде «барлығына қосу» немесе «мәңгілікке өшіру» қажет емес. PostgreSQL-де бірнеше басқару деңгейі бар — мұқият баптаудан бастап толық өшіруге дейін.
Ең қауіпсіз нұсқа: JIT-ті сессия деңгейінде өшіру. OLTP-сервистер, API, фондық жұмысшылар және болжамды сұраулармен batch-тапсырмалар үшін қолайлы. Бұл жақсы, себебі басқа қосылымдарға әсер етпейді, оны оңай қайта қосуға болады және оны нүктелі түрде қолдануға болады (қосылым пулы, рөл, қосымша). Көбінесе бұл backend-сервистер үшін оңтайлы әдепкі.
JIT — бұл бір батырма емес, бірнеше кезеңдер жиынтығы: генерация, inlining, оңтайландыру, emission. Кейбір сценарийлерде JIT-ті қосулы қалдырып, ең қымбат кезеңдерді өшіру орынды. Бұл үлкен сұраулар үшін ұтыстың бір бөлігін сақтай отырып, шығынды азайтады.
Толық жаһандық өшіру — шеткі нұсқа және аналитикалық сұраулар болмаса, CPU қымбат ресурс болса немесе база таза OLTP болса мағынасы бар. Бұл адал және жарамды шешім, «антипаттерн» емес.
Жеке тәжірибе
Бүгін PostgreSQL-де көрсеткіш жағдайға тап болдық: бірнеше JOIN, көптеген CASE, round() және numeric-арифметикасы бар күрделі есептеу VIEW шамамен 52 секунд орындалды. Алғашқы көзқараста — типтік «ауыр» сұрау. Алайда EXPLAIN ANALYZE нақты мәселе деректерде немесе сұрыптауда емес, JIT-компиляцияда екенін көрсетті: шамамен 47 секунд тек LLVM JIT-ке кетті (мыңдаған генерацияланған функциялар, оңтайландыру мен emission-ге ондаған секундтар). Орындау жалпы уақыттың тек аз бөлігін алды.
Шешім қарапайым болды: SET jit = off. Сұрауды қайта жазбай, индекстерсіз және логиканы өзгертпей орындау уақыты ~52 секундтан ~7 секундқа дейін қысқарды. Бұл жағдай cost-based JIT қосудың уақыт бойынша ұтысқа кепілдік бермейтінін жақсы көрсетеді: planner cost — бұл абстрактілі модель, кідіріс метрикасы емес.
PostgreSQL-дегі JIT — қуатты құрал, бірақ әдепкіде ол нақты уақытты емес, абстрактілі құнды оңтайландырады. Өндірісте мұны түсініп, саналы түрде басқаратын адам жеңеді.