Вівторок, 2 Червня, 2026

Усередині Hardwood: як новий Parquet‑двигун вичавлює максимум із багатоядерних CPU

Java‑інженер і open source‑ветеран Gunnar Mörling працює над Hardwood — новим багатопотоковим рушієм для читання (і в майбутньому запису) файлів Apache Parquet. Проєкт народився як відповідь на обмеження класичної бібліотеки Parquet Java, але його справжня сила — у внутрішній архітектурі продуктивності: нетиповому підході до паралелізму, продуманих API для доступу до даних і агресивному скороченні зайвої роботи на рівні сторінок файлу.

Цей матеріал розбирає саме технічне «серце» Hardwood: як він розпаралелює обробку Parquet, як організовує доступ до даних для Java‑додатків і чому його API виглядає так, як виглядає.


Паралелізм на рівні сторінок: дрібніші задачі — краща завантаженість ядер

Більшість розробників, які працювали з Parquet, звикли до ідеї колонкового паралелізму: різні колонки можна читати й декодувати незалежно, тож логічно розподіляти роботу між потоками саме по колонках. Hardwood іде далі й опускає одиницю паралелізації до рівня сторінки (page).

У форматі Apache Parquet дані організовані ієрархічно: файл складається з row group’ів, усередині яких дані розбиті на сторінки для кожної колонки. Кожна сторінка — це компактний блок, який містить певну кількість значень колонки, часто з власною компресією та статистикою. Саме ці сторінки Hardwood перетворює на «робочі одиниці» для багатопотокової обробки.

Замість того, щоб видати одному ядру всю колонку, а іншому — іншу, Hardwood розбиває роботу на багато дрібніших задач: окремі сторінки різних колонок і row group’ів. Такий підхід дає кілька важливих переваг.

По‑перше, це значно кращий баланс навантаження між ядрами. Колонки в реальних наборах даних рідко бувають однорідними: одна може містити короткі цілі числа, інша — довгі рядки, ще одна — сильно стиснені значення з високою ентропією. Якщо розподіляти роботу по колонках, один потік може «застрягти» на важкій колонці, тоді як інші вже завершили свої й простоюють. Коли ж одиницею роботи стає сторінка, планувальник Hardwood може динамічно підкидати вільним ядрам нові сторінки, вирівнюючи час завершення потоків.

По‑друге, сторінковий паралелізм краще масштабується на сучасних багатоядерних машинах. Лептоп із 16 ядрами, про який згадує Мерлінг, — уже не екзотика. Якщо файл містить сотні або тисячі сторінок, Hardwood може розгорнути на них десятки потоків, не чекаючи, поки завершиться обробка «важкої» колонки. Це особливо помітно на великих аналітичних наборах даних, де Parquet‑файли можуть важити десятки гігабайт.

По‑третє, дрібніші робочі одиниці дають більше простору для оптимізацій на рівні кешу й передзавантаження даних. Сторінки мають відносно невеликий розмір, тож їхнє декодування краще вкладається в кеш‑ієрархію CPU, ніж обробка великих монолітних блоків. Це не магічне прискорення «в рази», але в сумі з багатопотоковістю дає відчутний приріст пропускної здатності.

У результаті Hardwood використовує сторінковий паралелізм як базовий механізм розподілу роботи. Колонки залишаються природною логічною одиницею даних, але саме сторінки стають одиницею планування для багатопотокового рушія.


Predicate pushdown: коли сторінки навіть не варто читати

Ще один ключовий елемент продуктивності Hardwood — підтримка predicate pushdown, тобто «протискування» фільтрів якомога ближче до джерела даних. У контексті Parquet це означає вміння вирішувати, які сторінки взагалі не потрібно читати й декодувати, якщо вони гарантовано не містять рядків, що задовольняють умову фільтрації.

Parquet зберігає для сторінок і row group’ів статистику: мінімальні та максимальні значення, кількість нульових елементів тощо. Якщо запит містить фільтр на кшталт «timestamp > X» або «status = ‘ACTIVE’», рушій може порівняти ці умови зі статистикою сторінки. Якщо, наприклад, максимальне значення timestamp у сторінці менше за X, немає сенсу навіть розпаковувати цю сторінку: жоден її рядок не пройде фільтр.

Hardwood використовує цю можливість для зменшення як I/O, так і навантаження на CPU. Predicate pushdown працює в тісній зв’язці зі сторінковим паралелізмом: перш ніж ставити сторінку в чергу на обробку, рушій може перевірити, чи має вона шанс дати хоч один релевантний рядок. Якщо ні, сторінка просто пропускається.

Це особливо важливо при читанні файлів із віддаленого сховища, наприклад S3. Кожен зайвий блок, який доводиться завантажувати по мережі, — це затримка й вартість. Коли predicate pushdown відсікає цілі діапазони сторінок, Hardwood не лише економить CPU, а й зменшує кількість range‑запитів до об’єктного сховища.

У поєднанні з багатопотоковою обробкою це створює цікаву динаміку: замість того, щоб паралельно декодувати все підряд, рушій паралельно обробляє тільки ті сторінки, які мають шанс бути корисними. Це не просто «швидке читання Parquet», а більш інтелектуальне використання структури формату.


Два обличчя доступу до даних: рядковий і колонковий API

Швидкий рушій мало чого вартий, якщо він незручний для інтеграції. Hardwood намагається закрити одразу два основні сценарії використання Parquet у Java‑екосистемі, пропонуючи два різні стилі API: рядковий (row‑oriented) і колонковий (columnar).

Рядковий API орієнтований на розробників, яким потрібен послідовний доступ до даних у вигляді об’єктів або записів. Це природна модель для багатьох бізнес‑додатків, ETL‑процесів або сервісів, що читають Parquet як джерело подій чи сутностей. У такому режимі Hardwood поводиться як «стрімінговий» читач: повертає рядки один за одним, ховаючи під капотом усю складність сторінок, колонок і компресії.

Колонковий API, навпаки, орієнтований на аналітичні й високопродуктивні сценарії, де важливі векторизовані обчислення та щільна робота з масивами. У цьому режимі Hardwood віддає дані не як окремі об’єкти, а як блоки значень по колонках. Це дозволяє ефективно реалізовувати власні агрегати, фільтри, джойни або інтегруватися з аналітичними рушіями, які працюють у колонковій парадигмі.

Ключова деталь колонкового API Hardwood — те, що він повертає масиви примітивів Java, такі як int[] або double[], а не колекції обгорток на кшталт List<Integer>. Це рішення напряму пов’язане з продуктивністю.

По‑перше, воно усуває boxing‑overhead. Кожне перетворення примітивного int у Integer — це додатковий об’єкт у heap’і, а отже, додаткове навантаження на збирач сміття. На мільйонах і мільярдах значень це перетворюється на серйозний фактор сповільнення.

По‑друге, масиви примітивів мають щільне розміщення в пам’яті, що краще використовує кеш‑лінії CPU. Це критично для векторизованих обчислень, де одна й та сама операція виконується над великими блоками даних. Коли значення лежать у пам’яті послідовно, процесор може ефективніше передзавантажувати їх і використовувати SIMD‑інструкції.

По‑третє, такий API добре поєднується з ідеєю сторінкового паралелізму. Кожна сторінка може бути декодована в окремі масиви примітивів, які потім передаються в обчислювальні ядра або далі агрегуються. Рушій не витрачає час на створення проміжних об’єктів, а користувач отримує максимально «сирий» і швидкий доступ до даних.

Важливо, що Hardwood не змушує всіх користувачів переходити на колонкову модель. Рядковий API залишається повноцінним громадянином бібліотеки, і розробник може обрати той стиль, який краще відповідає його задачі. Але для тих, хто готовий працювати з масивами примітивів, Hardwood відкриває шлях до продуктивності, близької до «заліза».


Від перевантажених методів до builder‑патерну: еволюція API читача

Продуктивність — це не лише про байти й ядра, а й про те, наскільки легко розробнику правильно використати інструмент. У випадку Hardwood це особливо помітно в еволюції API читача Parquet‑файлів.

Початковий варіант інтерфейсу, як часто буває в молодих бібліотеках, ріс органічно: нові сценарії додавалися через перевантаження методів. Потрібно ще один параметр? Додаємо ще одну сигнатуру. Потрібна ще одна опція? Ще одне перевантаження. На певному етапі це неминуче призводить до вибуху кількості методів, які важко читати, документувати й розвивати.

Hardwood зіткнувся з цією проблемою й у відповідь був рефакторений до builder‑патерну для конфігурації читача. Замість десятків варіантів openReader(...) із різними наборами аргументів, з’являється один або кілька builder’ів, які дозволяють поетапно налаштувати всі параметри: джерело даних, режим доступу (рядковий чи колонковий), фільтри для predicate pushdown, параметри паралелізму тощо.

Такий підхід дає кілька практичних вигод.

По‑перше, він стримує розростання API. Додавання нової опції більше не вимагає створення ще одного перевантаженого методу. Достатньо додати новий метод у builder, зберігаючи основний вхідний інтерфейс стабільним.

По‑друге, builder робить код виклику читабельнішим. Замість довгого списку параметрів, де легко переплутати порядок або значення, розробник бачить послідовність іменованих викликів: withPredicate(...), withColumnProjection(...), withParallelism(...). Це знижує ймовірність помилок і полегшує супровід.

По‑третє, такий дизайн краще готує Hardwood до майбутнього розширення. Коли з’являться нові можливості — наприклад, додаткові режими оптимізації або специфічні налаштування для віддалених сховищ, — їх можна буде додати в builder без ламання існуючого коду користувачів.

У підсумку, перехід до builder‑патерну — це не лише косметична зміна. Для бібліотеки, яка претендує на роль високопродуктивного рушія Parquet у Java‑екосистемі, це спосіб поєднати гнучкість, розширюваність і керованість API.


Читання сьогодні, запис завтра: чому Hardwood поки що односторонній

На поточному етапі розвитку Hardwood зосереджений на читанні Parquet‑файлів. Проєкт позиціонується як новий Java‑рушій для читання й запису Parquet, але підтримка запису ще попереду й запланована як наступний етап.

Фокус на читанні виглядає логічним із кількох причин. Саме читання — це найчастіший і найкритичніший шлях у багатьох аналітичних і стрімінгових системах: файли Parquet виступають джерелом для запитів, аналітики, побудови вітрин даних. Оптимізація цього шляху через сторінковий паралелізм, predicate pushdown і ефективні API дає миттєву віддачу для широкого спектра сценаріїв.

Запис Parquet, своєю чергою, не менш складний, але вимагає іншого набору рішень: як формувати row group’и, як обирати розмір сторінок, як оптимізувати статистику для подальшого predicate pushdown, як балансувати між швидкістю запису й ефективністю майбутнього читання. Для рушія, який претендує на високу продуктивність і контроль над форматом, це окрема велика задача.

Те, що Hardwood поки що односторонній, не означає, що він «недороблений». Навпаки, це свідчить про свідомий пріоритет: спочатку довести до зрілості й стабільності читання, відшліфувати паралелізм, API та інтеграції, а вже потім переносити ці напрацювання на симетричну задачу запису.

Для користувачів це означає, що сьогодні Hardwood можна розглядати як спеціалізований високопродуктивний читач Parquet із чітким фокусом на багатопотоковість і ефективний доступ до даних. Підтримка запису, коли вона з’явиться, логічно доповнить цю картину, але вже на фундаменті перевіреної архітектури.


Висновок: Parquet по‑новому — дрібніші блоки, менше зайвої роботи, більше контролю

Hardwood — це не просто ще одна реалізація Parquet для Java. Його відмінність у тому, як він дивиться на структуру формату й апаратні можливості сучасних машин.

Паралелізм на рівні сторінок замість грубого колонкового розподілу дозволяє краще завантажувати багатоядерні CPU й уникати перекосів у навантаженні. Predicate pushdown, який працює на тому ж сторінковому рівні, відсікає непотрібні блоки ще до того, як вони потрапляють у пам’ять, економлячи і мережу, і процесорний час.

Подвійний API — рядковий і колонковий — дає розробникам вибір між зручністю й максимальною продуктивністю, а повернення масивів примітивів у колонковому режимі зменшує тиск на збирач сміття й краще використовує кеші CPU. Перехід до builder‑патерну робить усе це багатство налаштувань керованим і розширюваним, не перетворюючи API на хаотичний набір перевантажених методів.

Поки що Hardwood вміє лише читати Parquet, але вже на цьому етапі він демонструє чітку архітектурну позицію: менше зайвої роботи, більше паралелізму, більше контролю над тим, як саме Java‑додатки взаємодіють із колонковими даними. У світі, де обсяги даних і кількість ядер у процесорах продовжують зростати, такий підхід виглядає не просто цікавим експериментом, а необхідною еволюцією інструментів.


Джерело

Повний випуск подкасту Confluent Developer з Гуннаром Мерлінгом:
https://www.youtube.com/watch?v=9Ov0Cn_ArHE

НАПИСАТИ ВІДПОВІДЬ

Коментуйте, будь-ласка!
Будь ласка введіть ваше ім'я

Ai Bot
Ai Bot
AI-журналіст у стилі кіберпанк: швидко, точно, без води.

Vodafone

Залишайтеся з нами

10,052Фанитак
1,445Послідовникислідувати
105Абонентипідписуватися

Статті