dimhold.by
← Артыкулы

У README я напісаў, што H2 не ўзнаўляе мае лічбы з Postgres. Праз TCP ён лёг побач з лакальным Postgres

У жніўні я выклаў на GitHub невялікі замер, preview-cost. Ён пытае, колькі каштуе жывое прэв’ю, калі пабудаваць яго на адкаце транзакцыі. Прэв’ю запісвае значэнні з паўзунка, чытае мадэль назад, лічыць і адкочваецца. Другая стратэгія гэта копія ў памяці, якая базу не чапае зусім. На Postgres 16 у Docker адкат прайграў ва ўсіх 4500 парах, на 40 планах на 6,16 мс.

Раздзел README пра запуск дае 2 абяцанні. Па змаўчанні стаіць H2 у рэжыме Postgres, таму абяцаны прагон за 10 секунд “with nothing installed”. А лічбы ў табліцы зняты з сапраўднага Postgres, “because H2 in memory does not reproduce what a write and discard costs a real database”.

Другую фразу я напісаў, ні разу як след не паглядзеўшы на прагон з H2. Гэта тое, што ўсе кажуць пра H2 і я сам кажу тое ж, калі нехта хоча тэставаць на ім. Бярыце сапраўдную базу, убудаваная хлусіць. Выходзіць, рэжым па змаўчанні ў маім рэпазіторыі гэта рэжым, які, паводле таго ж README, лічбаў не ўзнаўляе. Гэтым разам я яго запусціў.

10 секунд

Я скланаваў рэпазіторый на каміце 26dd519 у пусты каталог з пустым кэшам Gradle, на віртуалцы з 4 vCPU і Gradle 9.8.0. Абгорткі Gradle у рэпазіторыі няма, так што “nothing installed” ужо значыць JDK 25 плюс Gradle. Дзеля справядлівасці, README кажа гэта ў сказе якраз перад абяцаннем.

Калі на машыне толькі JDK 21, зборка спыняецца праз 16 с. Тулчэйн просіць версію 25, а спампаваць яе Gradle няма адкуль. З Temurin 25.0.4.1 першы gradle run на пустым кэшы заняў 24 с. Наступныя 3 запускі занялі па 6-7 с. Значыць, абяцанне 10 секунд слушнае з другога запуску. Той, хто клануе рэпазіторый упершыню, чакае 24 с.

Што кажа H2

Прагон па змаўчанні гэта 5 свежых JVM па 300 парных прэв’ю ў кожнай. Я запусціў нязменены Bench на H2 2.3.232 у памяці і на PostgreSQL 16.14, усталяванага лакальна на тую ж віртуалку, з pgjdbc 42.7.4. Кожны памер прайшоў двойчы. У табліцы медыяна рознасці “адкат мінус копія”.

планыH2 у памяціPostgreSQL 16.14, лакальнаREADME, Postgres у Docker
100,50 / 0,54 мс0,78 / 0,76 мс6,54 мс
400,50 / 0,46 мс0,91 / 0,83 мс6,16 мс
1200,49 / 0,47 мс1,22 / 1,18 мс7,02 мс

На кожнай базе і кожным памеры адкат быў павольнейшы ў 99,4-100% пар. H2 дае той жа вердыкт, што і README, толькі лічба ў 12-15 разоў меншая. Лакальны Postgres адрозніваецца ад H2 у 1,4-2,6 раза, а Postgres з README ад лакальнага ў 6-9 разоў.

Я чакаў, што на H2 адкат стане амаль бясплатным, бо нічога не выходзіць за межы працэсу. Не стаў. Я расклаў прэв’ю з адкатам на крокі ў адной JVM. На H2 у памяці адно толькі абнаўленне 4 радкоў заняло 238-291 мкс. На 40 планах гэта прыкладна ў 4 разы больш за ўвесь разлік, які заняў 59 мкс.

Дзе сядзіць розніца

H2 умее працаваць і серверам. Гэта той жа рухавік з тым жа захоўваннем у памяці. Але драйвер размаўляе з ім праз TCP-сокет на localhost, гэтак жа, як pgjdbc размаўляе з Postgres. Я дадаў яго ў сетку да астатніх, разам з H2 з файлам на дыску. Варыянт з файлам супаў з H2 у памяці, 0,49-0,59 мс, таму на графіку яго няма.

10 планаў 0,50 / 0,54 H2 у памяці 0,84 / 0,83 H2 праз TCP 0,78 / 0,76 PostgreSQL, лакальна 6,54 README, Docker 40 планаў 0,50 / 0,46 H2 у памяці 0,82 / 0,82 H2 праз TCP 0,91 / 0,83 PostgreSQL, лакальна 6,16 README, Docker 120 планаў 0,49 / 0,47 H2 у памяці 1,22 / 1,25 H2 праз TCP 1,22 / 1,18 PostgreSQL, лакальна 7,02 README, Docker 0 1 2 3 4 5 6 7 адкат мінус копія, мс (па 2 праходы)
Слупок гэта сярэдняе 2 праходаў, у подпісе абодва. H2 праз TCP супадае з лакальным Postgres на кожным памеры. Слупок README гэта адна лічба са жніўня.

H2 праз TCP даў 0,84 / 0,83, 0,82 / 0,82 і 1,22 / 1,25 мс. Гэта ў межах 0,1 мс ад лакальнага Postgres на кожным памеры, уключна з уздымам на 120 планах. Значыць, на гэтым харнэсе большая частка разрыву паміж H2 і Postgres прыпадае на сокет і сервер за ім. У маёй фразе не было ні таго, ні другога. Я праверыў яшчэ раз на той жа віртуалцы, калі яе грузіла іншая задача. Тады H2 праз TCP апынуўся на 0,04-0,23 мс вышэй за Postgres на 40 планах, так што 0,1 мс гэта лічба з ціхай машыны.

Запіс і скід адрозніваюцца менш, чым я думаў. На H2 у памяці абнаўленне заняло 238-291 мкс, на Postgres 312-389 мкс. Крок адкату заняў 78-90 мкс супраць 188-205 мкс, прычым Postgres плаціць за яго яшчэ і зваротам да сервера.

Большая частка ўздыму з ростам планаў прыпадае на чытанне назад. Кожнае прэв’ю чытае ўсе палі мадэлі, 42 радкі на 10 планах і 482 на 120. На Postgres гэтае чытанне заняло 387 мкс і 1006 мкс. H2 у памяці аддае тыя ж радкі за 128 і 174 мкс, думаю, таму што праз сокет не ідзе ні байта. Мой README назваў накладныя выдаткі “a constant” і зрабіў гэта галоўным вывадам. На лакальным Postgres яны выраслі прыкладна ў паўтара раза, з 0,76-0,78 мс на 10 планах да 1,18-1,22 мс на 120.

Звароты да сервера

Прэв’ю з адкатам на Postgres гэта 3 звароты да сервера: пачак з 4 абнаўленняў, select і адкат. Я палічыў іх з дапамогай маленькага проксі паміж драйверам і базай. 100 прэв’ю дадалі 300 запісаў кліента ў сокет да тых 12, якія без аніводнага прэв’ю спатрэбіліся падрыхтоўцы схемы і пачатковых радкоў.

Той жа проксі ўмее прытрымліваць кожны запіс кліента на зададзены час, перш чым перадаць яго далей. Гэта грубая мадэль даўжэйшага шляху да базы. Я прагнаў праз яго нязменены Bench на 40 планах з 5 рознымі затрымкамі.

0 2 4 6 8 0 250 500 1000 2000 затрымка на кожны зварот, мкс штраф, мс README, 40 планаў: 6,16 +3 × затрымка (без проксі: 0,83-0,91) 2,12 2,85 4,40 7,52 1,12
Пасля першай затрымкі вымераны штраф ідзе прыкладна на 0,25 мс вышэй за лінію 3 зваротаў. Узровень README дасягаецца паміж 1000 і 2000 мкс на зварот.

Без затрымкі сам проксі дадаў у тым прагоне каля 0,2-0,3 мс, 1,12 мс супраць 0,83-0,91 напрамую. Першыя 250 мкс затрымкі дадалі 1 мс, больш, чым 3 разы па 250. Далей лінія ідзе ўгору прыкладна на 3 мкс на кожную мікрасекунду затрымкі, гэта і ёсць 3 звароты. Лішнія 0,25 мс, хутчэй за ўсё, набягаюць таму, што проксі кожны раз спіць даўжэй за зададзенае, але асобна я гэта не мераў. Пры 2 мс на зварот штраф склаў 7,52 мс, крыху больш за лічбы README.

Паміж вымеранымі пунктамі на 1000 і 2000 мкс узровень 6,16 мс прыпадае прыкладна на 1,6 мс на зварот. Гэта толькі арыфметыка. Жнівеньскага стэнда ў мяне тут няма. Гэты харнэс выходзіць на лічбы README пры такой працягласці звароту, але сцвярджаць, што ў Docker на той машыне быў якраз такі, я не магу.

Што я змяніў бы ў README

10 секунд можна пакінуць, з заўвагай, што першы запуск займае каля 25. У фразе пра H2 памылковая прычына. H2 у памяці дае той жа вердыкт, што і Postgres, а лічба адрозніваецца ад лакальнага Postgres менш чым у 3 разы. У яго няма сокета, а H2 у рэжыме сервера яго вяртае. Табліца вынікаў павінна казаць, які Postgres мералі і наколькі ён быў далёка. Паводле маіх лічбаў гэтая адлегласць складае большую частку тых 6 мс.

Вывад зыходнага README не мяняецца. 1 мс ці 6 мс за дэбаўнсам у 400 мс карыстальнік не заўважыць. Значэнне мае кошт у радках, запісаных і заблакаваных на кожнае прэв’ю.

Чаго я не правяраў

Docker я не запускаў зусім, ні на якой машыне. Няма і канкурэнтнага доступу, які README ужо называе наступным крокам і дзе блакіроўкі пачынаюць абыходзіцца дорага. H2 быў толькі 2.3.232, драйвер толькі pgjdbc 42.7.4. Сетка прайшла двойчы, серыя з проксі адзін раз. Для разбору па кроках я браў адну JVM на клетку, так што яго мікрасекунды прыблізныя. На гэтай віртуалцы разлік яшчэ і да 2,6 раза хутчэйшы, чым на жнівеньскай машыне. Копія ў памяці займае 0,17 мс на 120 планах супраць 0,45 у README. Разлік у рознасці скарачаецца, бо яго выконваюць абедзве паловы пары. Але драйвер і база працуюць на тым жа працэсары, так што на больш павольнай машыне мае 0,9 мс, верагодна, таксама выраслі б.