Большую часть прошлого года я работал с Hexvarium. Базирующаяся в Атертоне, штат Калифорния, компания строит и управляет оптоволоконными сетями. В настоящее время у них есть несколько сетей в районе залива, но у них есть планы по расширению в США.
Моя роль заключается в управлении платформой данных, которая содержит 30 миллиардов записей примерно из 70 источников информации. Эти данные используются инженерами, разрабатывающими оптимальные планы развертывания оптоволоконной сети с помощью LocalSolver. Далее приведен пример одного из таких планов.

Наша платформа данных в основном состоит из PostGIS, ClickHouse и BigQuery. Данные поступают в самых разных, но чаще не в оптимальных форматах. Затем мы формируем и обогащаем данные, часто с помощью PostGIS или ClickHouse, прежде чем отправлять их в BigQuery. Затем инженеры, работающие с LocalSolver, будут получать свои данные из очищенной и актуальной версии в BigQuery.
Анализ подпитывает воображение инженеров, поэтому мы часто визуализируем данные, полученные с помощью Unfolded. Ниже представлена визуализация, которую мы создали для Speedtest от Ookla прошлым летом. Мы сгруппировали записи по уровню масштабирования H3 9 и взяли самую высокую среднюю скорость загрузки из каждого шестиугольника, нанеся их на карту.

Ниже приведена еще одна визуализация, которую мы создали, показывающая разницу в максимальной скорости широкополосного доступа в период с июня по ноябрь прошлого года. Данные в FCC предоставили провайдеры широкополосного доступа из США.

Интересные особенности DuckDB
DuckDB — это прежде всего работа Марка Раасвельдта и Ханнеса Мюлейзена. Он состоит из миллиона строк C++ и работает как отдельный двоичный файл. Разработка идет очень активно: количество коммитов в репозитории GitHub удваивается почти каждый год с момента его запуска в 2018 году. DuckDB использует синтаксический анализатор SQL PostgreSQL, механизм регулярных выражений RE2 от Google и оболочку SQLite.
SQLite поддерживает пять типов данных: NULL, INTEGER, REAL, TEXT и BLOB. Меня всегда это расстраивало, поскольку работа со временем требовала преобразований в каждом операторе SELECT, а невозможность описать поле как логическое означает, что программное обеспечение для анализа не могло автоматически распознавать и предоставлять определенные элементы управления пользовательского интерфейса и визуализацию этих полей.
К счастью, DuckDB поддерживает 25 типов данных из коробки, а дополнительные можно добавить с помощью расширений. Ведется работа над расширением, которое перенесет геопространственные функции PostGIS Пола Рэмси в DuckDB. Исаак Бродский, главный инженер Foursquare, материнской компании Unfolded, также недавно опубликовал расширение H3 для DuckDB. И хотя еще ничего не опубликовано, предпринимаются попытки встроить GDAL через его интерфейс на основе Arrow в DuckDB.
Min/Max индекс создается для каждого сегмента столбца в DuckDB. Именно благодаря этому типу индекса большинство баз данных OLAP так быстро отвечают на запросы агрегирования. Расширения Parquet и JSON поставляются в официальной сборке, и их использование хорошо задокументировано. Для файлов Parquet поддерживаются сжатие Snappy и ZStandard.
Документация DuckDB хорошо организована и очень лаконична, с примерами рядом с большинством описаний.
Запуск DuckDB
Официальная версия DuckDB не содержит расширений Geospatial и H3, используемых в этом посте, поэтому я скомпилирую DuckDB с этими расширениями.
Приведенные ниже команды были выполнены на экземпляре e2-standard-4 в Google Cloud под управлением Ubuntu 20 LTS. Эта виртуальная машина содержит 4 виртуальных ЦП и 16 ГБ ОЗУ. Он был запущен в Лос-Анджелесе, в регионе us-west2-b, и имеет сбалансированный персистентный диск объемом 100 ГБ. Запуск стоил 0,18 доллара в час.
Ниже будут установлены пакеты ПО, используемые в этом посте.
$ sudo apt update $ sudo apt install \ awscli \ build-essential \ libssl-dev \ pigz \ unzip
Для компиляции расширения H3 требуется CMake версии 3.20+, поэтому я сначала соберу его.
$ cd ~ $ wget -c https://github.com/Kitware/CMake/releases/download/v3.20.0/cmake-3.20.0.tar.gz $ tar -xzf cmake-3.20.0.tar.gz $ cd cmake-3.20.0 $ ./bootstrap --parallel=$(nproc) $ make -j$(nproc) $ sudo make install
Следующие команды будет собирать как DuckDB, так и расширение H3 для него. Расширение geo привязано к коммиту DuckDB c817201, поэтому я также прикреплю к нему расширение H3.
$ git clone https://github.com/isaacbrodsky/h3-duckdb ~/duckdb_h3 $ cd ~/duckdb_h3 $ git submodule update --init $ cd duckdb $ git checkout c817201 $ cd .. $ CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) \ make release
Далее будет создано расширение, которое перенесет части функциональности PostGIS в DuckDB.
$ git clone https://github.com/handstuyennn/geo ~/duckdb_geo $ cd ~/duckdb_geo $ git submodule init $ git submodule update --recursive --remote $ mkdir -p build/release $ cmake \ ./duckdb/CMakeLists.txt \ -DEXTERNAL_EXTENSION_DIRECTORIES=../geo \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DEXTENSION_STATIC_BUILD=1 \ -DBUILD_TPCH_EXTENSION=1 \ -DBUILD_PARQUET_EXTENSION=1 \ -B build/release $ CMAKE_BUILD_PARALLEL_LEVEL=$(nproc) \ cmake --build build/release
Создам шелл скрипт, который запустит двоичный файл DuckDB с поддержкой неподписанных расширений.
$ echo "$HOME/duckdb_geo/build/release/duckdb -unsigned \$@" \ | sudo tee /usr/sbin/duckdb $ sudo chmod +x /usr/sbin/duckdb
Настрою файл конфигурации DuckDB для загрузки обоих расширений по умолчанию.
$ vi ~/.duckdbrc
.timer on LOAD '/home/mark/duckdb_h3/build/release/h3.duckdb_extension'; LOAD '/home/mark/duckdb_geo/build/release/extension/geo/geo.duckdb_extension';
Запущу DuckDB и проверю, работают ли расширения.
$ duckdb
SELECT h3_cell_to_parent(CAST(586265647244115967 AS ubigint), 1) test; ┌────────────────────┐ │ test │ │ uint64 │ ├────────────────────┤ │ 581764796395814911 │ └────────────────────┘
SELECT ST_MAKEPOINT(52.347113, 4.869454) test; ┌────────────────────────────────────────────┐ │ test │ │ geography │ ├────────────────────────────────────────────┤ │ 01010000001B82E3326E2C4A406B813D26527A1340 │ └────────────────────────────────────────────┘
Мне кажется полезным, что по умолчанию тип данных отображается под каждым именем поля.
Набор данных Ookla Speedtest
Ookla публикует свои данные Speedtest в AWS в формате Parquet. Ниже я скачаю набор данных 2022 года для мобильных устройств.
$ aws s3 sync \ --no-sign-request \ s3://ookla-open-data/parquet/performance/type=mobile/year=2022/ \ ./
Структура путей к папкам предназначена для поддержки секционирования Apache Hive. Я буду использовать только четыре файла Parquet, поэтому я перемещу их в одну папку.
$ mv quarter\=*/*.parquet ./
Ниже приведены размеры для каждого из файлов. Всего они содержат 15 738 442 строки данных.
$ ls -lht *.parquet 173M 2022-10-01_performance_mobile_tiles.parquet 180M 2022-07-01_performance_mobile_tiles.parquet 179M 2022-04-01_performance_mobile_tiles.parquet 174M 2022-01-01_performance_mobile_tiles.parquet
Ниже приведена схема, используемая в этих файлах Parquet.
$ echo "SELECT name, type, converted_type FROM parquet_schema('2022-10*.parquet');" \ | duckdb ┌────────────┬────────────┬────────────────┐ │ name │ type │ converted_type │ │ varchar │ varchar │ varchar │ ├────────────┼────────────┼────────────────┤ │ schema │ BOOLEAN │ UTF8 │ │ quadkey │ BYTE_ARRAY │ UTF8 │ │ tile │ BYTE_ARRAY │ UTF8 │ │ avg_d_kbps │ INT64 │ INT_64 │ │ avg_u_kbps │ INT64 │ INT_64 │ │ avg_lat_ms │ INT64 │ INT_64 │ │ tests │ INT64 │ INT_64 │ │ devices │ INT64 │ INT_64 │ └────────────┴────────────┴────────────────┘
Ookla использовала сжатие Snappy для каждого столбца. DuckDB может проводить диагностику того, как расположены данные, и предоставлять статистическую информацию по каждому столбцу. Ниже приведены сведения о столбце тайла.
$ echo ".mode line SELECT * EXCLUDE (stats_min_value, stats_max_value) FROM parquet_metadata('2022-10-01_*.parquet') WHERE path_in_schema = 'tile' AND row_group_id = 0;" \ | duckdb file_name = 2022-10-01_performance_mobile_tiles.parquet row_group_id = 0 row_group_num_rows = 665938 row_group_num_columns = 7 row_group_bytes = 31596007 column_id = 1 file_offset = 24801937 num_values = 665938 path_in_schema = tile type = BYTE_ARRAY stats_min = stats_max = stats_null_count = 0 stats_distinct_count = compression = SNAPPY encodings = PLAIN_DICTIONARY, PLAIN, RLE, PLAIN index_page_offset = 0 dictionary_page_offset = 3332511 data_page_offset = 3549527 total_compressed_size = 21469426 total_uncompressed_size = 124569336
Импорт Parquet данных
Теперь могу импортировать все четыре файла Parquet в DuckDB с помощью одного оператора SQL. Команда добавит дополнительный столбец с указанием имени файла, из которого происходит каждая строка данных. Из исходных 706 МБ Parquet и дополнительная колонка превратились в файл DuckDB размером 1,4 ГБ.
$ echo "CREATE TABLE mobile_perf AS SELECT * FROM read_parquet('*.parquet', filename=true);" \ | duckdb ~/ookla.duckdb
В качестве альтернативы, используя расширение DuckDB HTTPFS, я могу загружать файлы Parquet непосредственно из S3.
$ echo "INSTALL httpfs; CREATE TABLE mobile_perf AS SELECT * FROM parquet_scan('s3://ookla-open-data/parquet/performance/type=mobile/year=2022/*/*.parquet', FILENAME=1);" \ | duckdb ~/ookla.duckdb
Если его еще нет, расширение HTTPFS будет загружено и установлено автоматически. Имейте в виду, что иногда сборки еще не готовы, и вы можете увидеть следующее:
Error: near line 1: IO Error: Failed to download extension "httpfs" at URL "http://extensions.duckdb.org/7813eea926/linux_amd64/httpfs.duckdb_extension.gz" Extension "httpfs" is an existing extension. Are you using a development build? In this case, extensions might not (yet) be uploaded.
Структура таблицы DuckDB
Ниже приведена схема таблицы, которую автоматически составляет DuckDB.
$ echo '.schema --indent' \ | duckdb ~/ookla.duckdb
CREATE TABLE mobile_perf( quadkey VARCHAR, tile VARCHAR, avg_d_kbps BIGINT, avg_u_kbps BIGINT, avg_lat_ms BIGINT, tests BIGINT, devices BIGINT, filename VARCHAR );
Ниже приведен пример записи.
$ echo '.mode line SELECT * FROM mobile_perf LIMIT 1' \ | duckdb ~/ookla.duckdb
quadkey = 0022133222330023 tile = POLYGON((-160.043334960938 70.6363054807905, -160.037841796875 70.6363054807905, -160.037841796875 70.6344840663086, -160.043334960938 70.6344840663086, -160.043334960938 70.6363054807905)) avg_d_kbps = 15600 avg_u_kbps = 14609 avg_lat_ms = 168 tests = 2 devices = 1 filename = 2022-10-01_performance_mobile_tiles.parquet
Ниже приведены схемы сжатия, используемые в каждом из первых сегментов полей в первой группе строк.
$ echo "SELECT column_name, segment_type, compression, stats FROM pragma_storage_info('mobile_perf') WHERE row_group_id = 0 AND segment_id = 0 AND segment_type != 'VALIDITY';" \ | duckdb ~/ookla.duckdb ┌─────────────┬──────────────┬─────────────┬───────────────────────────────────┐ │ column_name │ segment_type │ compression │ stats │ │ varchar │ varchar │ varchar │ varchar │ ├─────────────┼──────────────┼─────────────┼───────────────────────────────────┤ │ quadkey │ VARCHAR │ FSST │ [Min: 00221332, Max: 02123303, … │ │ tile │ VARCHAR │ FSST │ [Min: POLYGON(, Max: POLYGON(, … │ │ avg_d_kbps │ BIGINT │ BitPacking │ [Min: 1, Max: 3907561][Has Null… │ │ avg_u_kbps │ BIGINT │ BitPacking │ [Min: 1, Max: 917915][Has Null:… │ │ avg_lat_ms │ BIGINT │ BitPacking │ [Min: 0, Max: 2692][Has Null: f… │ │ tests │ BIGINT │ BitPacking │ [Min: 1, Max: 1058][Has Null: f… │ │ devices │ BIGINT │ BitPacking │ [Min: 1, Max: 186][Has Null: fa… │ │ filename │ VARCHAR │ Dictionary │ [Min: 2022-10-, Max: 2022-10-, … │ └─────────────┴──────────────┴─────────────┴───────────────────────────────────┘
Обогащение данных
Я запущу DuckDB с только что созданной базой данных, добавлю три столбца для уровней масштабирования 7, 8 и 9, а затем приготовлю значения шестиугольников из столбца «геометрия» тайла .
$ duckdb ~/ookla.duckdb
ALTER TABLE mobile_perf ADD COLUMN h3_7 VARCHAR(15); ALTER TABLE mobile_perf ADD COLUMN h3_8 VARCHAR(15); ALTER TABLE mobile_perf ADD COLUMN h3_9 VARCHAR(15); UPDATE mobile_perf SET h3_7 = printf('%x', h3_latlng_to_cell( ST_Y(ST_CENTROID(ST_GEOMFROMTEXT(tile))), ST_X(ST_CENTROID(ST_GEOMFROMTEXT(tile))), 7)::bigint), h3_8 = printf('%x', h3_latlng_to_cell( ST_Y(ST_CENTROID(ST_GEOMFROMTEXT(tile))), ST_X(ST_CENTROID(ST_GEOMFROMTEXT(tile))), 8)::bigint), h3_9 = printf('%x', h3_latlng_to_cell( ST_Y(ST_CENTROID(ST_GEOMFROMTEXT(tile))), ST_X(ST_CENTROID(ST_GEOMFROMTEXT(tile))), 9)::bigint);
Ниже приведен пример расширенной записи.
.mode line SELECT h3_7, h3_8, h3_9, tile FROM mobile_perf LIMIT 1 h3_7 = 8730e0ae9ffffff h3_8 = 8830e0ae9dfffff h3_9 = 8930e0ae9d3ffff tile = POLYGON((126.837158203125 37.5576424267952, 126.842651367188 37.5576424267952, 126.842651367188 37.5532876459577, 126.837158203125 37.5532876459577, 126.837158203125 37.5576424267952))
Когда я попытался выполнить описанное выше на 4-ядерной виртуальной машине GCP e2-standard-4, я оставил задание работать на ночь, и оно еще не было завершено, когда проверил его утром. Позже запустил описанное выше на 16-ядерной виртуальной машине GCP e2-standard-16, и 15,7 млн записей были обогащены за 2 минуты 21 секунду. Использование ОЗУ имело верхнюю границу ~ 7 ГБ. Я поделился этим с Исааком, чтобы понять, есть ли какие-то очевидные исправления, которые можно было бы применить.
Некоторые из баз данных могут создавать новые таблицы быстрее, чем изменять существующие. Не обнаружил, что это относится к DuckDB. Следующая попытка была значительно медленнее, чем прошлый подход.
CREATE TABLE mobile_perf2 AS SELECT quadkey, tile, avg_d_kbps, avg_u_kbps, avg_lat_ms, tests, devices, filename, printf('%x', h3_latlng_to_cell( ST_Y(ST_CENTROID(ST_GEOMFROMTEXT(tile))), ST_X(ST_CENTROID(ST_GEOMFROMTEXT(tile))), 7)::bigint) h3_7, printf('%x', h3_latlng_to_cell( ST_Y(ST_CENTROID(ST_GEOMFROMTEXT(tile))), ST_X(ST_CENTROID(ST_GEOMFROMTEXT(tile))), 8)::bigint) h3_8, printf('%x', h3_latlng_to_cell( ST_Y(ST_CENTROID(ST_GEOMFROMTEXT(tile))), ST_X(ST_CENTROID(ST_GEOMFROMTEXT(tile))), 9)::bigint) h3_9 FROM mobile_perf;
Экспорт CSV
Далее создается CSV-файл сжатый с помощью GZIP для содержимого таблицы mobile_perf. Это было сделано для версии таблицы, которая не содержала шестиугольников H3 из приведенного выше обогащения.
$ echo "COPY (SELECT * FROM mobile_perf) TO 'mobile_perf.csv.gz' WITH (HEADER 1, COMPRESSION gzip);" \ | duckdb ~/ookla.duckdb
Имейте в виду, что только один процесс может одновременно использовать DuckDB в режиме записи, а описанная выше операция требует режима записи. Если вы видите что-то вроде следующего, вам нужно сначала дождаться другого процесса, использующего базу данных.
Error: unable to open database "/home/mark/mobile_perf.duckdb": IO Error: Could not set lock on file "/home/mark/mobile_perf.duckdb": Resource temporarily unavailable
Если вы создаете файл для массового распространения, возможно, стоит повторно сжать вышеуказанное с помощью pigz. Его уровень сжатия настраивается, он может использовать преимущества нескольких ядер и создавать файлы меньшего размера, чем GNU GZIP.
Следующая команда завершилась в 1,12 раза быстрее, чем предыдущая, с файлом результата на 2,5 МБ меньше.
$ echo ".mode csv SELECT * FROM mobile_perf;" \ | duckdb ~/ookla.duckdb \ | pigz -9 \ > mobile_perf.csv.gz
Если размер файла не является такой проблемой, использование параметра -1 создаст файл в 1,6 раза быстрее, только в 1,25 раза больше.
Порядок полей и ключи сортировки сильно влияют на способность GZIP к сжатию. С 8 полями существует 40 320 перестановок порядка полей, поэтому попытка найти наиболее сжимаемый вариант займет слишком много времени. Тем не менее, переупорядочить строки по каждому столбцу и посмотреть, какие из них сжимаются больше всего, можно сделать достаточно быстро.
$ COLUMNS=`echo ".mode list SELECT column_name FROM information_schema.columns WHERE table_name = 'mobile_perf';" \ | duckdb ~/ookla.duckdb \ | tail -n +2` $ for COL in $COLUMNS; do echo $COL echo "COPY (SELECT * FROM mobile_perf ORDER BY $COL) TO 'mobile_perf.$COL.csv.gz' WITH (HEADER 1, COMPRESSION gzip);" \ | duckdb ~/ookla.duckdb done
Для приведенного выше требуется доступ для записи в базу данных, поэтому я буду выполнять команды последовательно, а не параллельно с xargs.
DuckDB может использовать несколько ядер для большей части вышеуказанной работы, но некоторые задачи должны выполняться последовательно на одном ядре. Возможно, стоит продублировать исходную базу данных DuckDB и протестировать каждый ключ сортировки в своей собственной базе данных, если вы хотите максимально использовать доступные вам процессорные ядра.
При сортировке по avg_u_kbps был получен файл CSV, сжатый с помощью GZIP, размером 841 МБ, а при сортировке по quadkey — файл размером 352 МБ.
Вышеупомянутое потребовало почти всех 16 ГБ ОЗУ, которые у меня были в этой системе. Помните об этом для больших наборов данных и/или сред с ограниченным объемом оперативной памяти.
ZStandard — это более новый компрессор для сжатия без потерь, который должен сжимать так же, как GZIP, но примерно в 3-6 раз быстрее. Я подробно рассказывал про это в своем минималистском руководстве по сжатию без потерь. Если потребители ваших данных могут открывать файлы, сжатые ZStandard, их создание будет намного быстрее.
Следующая команда закончилось в 3,4 раза быстрее, чем его аналог GZIP, при этом вывод был примерно на 6% больше.
$ echo "COPY (SELECT * FROM mobile_perf) TO 'mobile_perf.csv.zstd' WITH (HEADER 1, COMPRESSION zstd);" \ | duckdb ~/ookla.duckdb
Опять же, если вышеперечисленное было отсортировано по avg_u_kbps, будет создан файл CSV со сжатием ZStandard размером 742 МБ, а сортировка по quadkey даст файл размером 367 МБ.
Экспорт Parquet
В моей публикации «Faster PostgreSQL To BigQuery Transfers» в блоге обсуждалось преобразование Microsoft Buildings, набора данных GeoJSON на 130M записей в файл Parquet, сжатый Snappy. В том посте ClickHouse справился с этой задачей в 1,38 раза быстрее, чем его ближайший конкурент. Теперь запускаю ту же рабочую нагрузку через DuckDB.
Используя внешний SSD-накопитель на моем MacBook Pro с процессором Intel 2020 года выпуска, ClickHouse создал файл за 35,243 секунды. После работы над оптимизацией совместно с командой DuckDB, я смог выполнить ту же рабочую нагрузку за 30,826 секунды, и, если бы я был готов обработать восемь файлов Parquet вместо одного, это время сократилось до 25,146 секунды. Это был SQL, который работал быстрее всего.
$ echo "COPY (SELECT * FROM read_ndjson_objects('California.jsonl')) TO 'cali.duckdb.pq' (FORMAT 'PARQUET', CODEC 'Snappy', PER_THREAD_OUTPUT TRUE);" \ | duckdb
Обратите внимание на использование read_ndjson_objects вместо read_json_objects, это значительно улучшило производительность. Кроме того, PER_THREAD_OUTPUT TRUE создаст несколько файлов. Уберите этот параметр, если вы хотите создать один файл Parquet.
Но с учетом сказанного, та же рабочая нагрузка, выполняемая на виртуальной машине Google Cloud e2-highcpu-32 с Ubuntu 20 LTS, показала, что ClickHouse превзошел DuckDB в 1,44 раза. Марк Раасвелдт предположил, что на этапе анализа сжатия DuckDB могут быть дальнейшие улучшения, поэтому есть шанс, что в ближайшем будущем мы увидим уменьшение разрыва в производительности по сравнению с ClickHouse.
PS. Ну а если вам не хочется тратить время на настройку компиляции и хотите легко собрать расширения для более свежей версии duckdb с новыми фичами(как например map_entries, map_values, map_keys из #6522), то можете взять этот Docker файл за основу.
ссылка на оригинал статьи https://habr.com/ru/post/721832/
Добавить комментарий