[Высокая производительность, PostgreSQL, *nix, Администрирование баз данных] Поиск среди 10000 GitHub репозиториев на Postgres (используя только MacBook) (перевод)

Автор Сообщение
news_bot ®

Стаж: 6 лет 4 месяца
Сообщений: 27286

Создавать темы news_bot ® написал(а)
24-Июн-2021 14:37

Привет!  Меня зовут Никита Галушко, я R&D-разработчик в Lamoda. Специально для Хабра я сделал вольный перевод интересной статьи “Postgres regex search over 10,000 GitHub repositories (using only a Macbook)”. Ее автор провел эксперимент: собрал датасет из 10 тысяч GitHub-репозиториев и проверил, насколько Postgres подходит для поиска по документам на одной машине — MacBook Pro, а также измерил скорость поиска и подобрал подходящую конфигурацию.В этой статье подробно расписан ход эксперимента, чтобы его смогли повторить все желающие. Перевод опубликован с согласия автора.
В этой статье я поделюсь результатами эксперимента по использованию Postgres для индексирования и последующего поиска среди 10 000 GitHub-репозиториев с использованием pg_trgm только на MacBook.Это продолжение статьи “Postgres Trigram search learnings”, в которой я рассказывал о фишках и подводных камнях при использовании триграммных индексов в Postgres как альтернативе гугловому Zoekt. Я поделился итогами и точными шагами, чтобы вы могли воспроизвести результаты самостоятельно, если захотите.Цели экспериментаЯ хочу получить эмпирические измерения, насколько Postgres подходит для поиска по документам с помощью regexp в качестве альтернативы гугловому Zoekt. А именно:
  • сколько репозиториев можно проиндексировать на одном MacBook Pro 2019 года;
  • насколько быстрым будет поиск по корпусу данных с помощью различных регулярных выражений;
  • какая конфигурация Postgres 13 дает наилучший результат;
  • какие посторонние эффекты нужно учитывать, чтобы рассматривать Postgres как бэкенд поисковой системы на регулярных выражениях;
  • какую схему базы данных лучше всего использовать.
ЖелезоВсе тесты запускаются на MacBook Pro 2019 в конфигурации:
  • 2.3 GHz 8-Core Intel Core i9,
  • 16 GB 2667 MHz DDR4.
Во время выполнения тестов я почти не использовал другие приложения, так что  можно пренебречь их эффектом на потребление CPU и считать, что все CPU/RAM были отданы для Postgres.КорпусЯ собрал списки 1000 лучших репозиториев с GitHub, отсортированных по количеству звездочек для C++, C#, CSS, Go, HTML, Java, JavaScript, MatLab, ObjC, Perl, PHP, Python, Ruby, Rust, Shell, Solidity, Swift, TypeScript, VB .NET и Zig. Всего получилось примерно 20 500 репозиториев. Их клонирование заняло примерно 14 часов с соединением до серверов GitHub около 100Мб/с.Уменьшение размера датасетаЯ обнаружил, что объем дискового пространства, которое требуется для git clone --depth 1 только лишь для 12 148 репозиториев, составляет примерно 412 Гб. Я решил использовать пару приемов для уменьшения размера набора данных примерно на 66%:
  • Удаление директории .git дало снижение на 30% (412 Гб → 290 Гб для 12 148 репозиториев).
  • Удаление файлов > 1 Мб дало снижение еще на 51% (290 Гб → 142 Гб для 12148 репозиториев. Кстати, GitHub не индексирует файлы размером больше 384 Кб).
Вставка данныха Вставка производилась конкурентно в Postgres со следующей схемой:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE TABLE IF NOT EXISTS files (
    id bigserial PRIMARY KEY,
    contents text NOT NULL,
    filepath text NOT NULL
);
Это длилось примерно 8 часов, а на диске Postgres занял 101 Гб.Создание индекса
CREATE INDEX IF NOT EXISTS files_contents_trgm_idx ON files USING GIN (contents gin_trgm_ops);
  • В первый раз я поймал OOM после 11 с половиной часов индексирования. Это связано с резким всплеском потребления памяти в самом конце индексирования. Это не было неожиданностью, поскольку я использовал достаточно агрессивную конфигурацию Postgres с максимальным размером WAL.
shared_buffers = 4GB → 2560MB
effective_cache_size = 12GB → 7680MB
maintenance_work_mem = 16GB → 1280MB
default_statistics_target = 100 → 500
work_mem = 5242kB → 16MB
min_wal_size = 50GB → 4GB
max_wal_size = 4GB → 16GB
max_parallel_workers_per_gather = 8 → 4
max_parallel_maintenance_workers = 8 → 4
  • В третий и последний раз датасет был обрезан наполовину, а индексирование заняло примерно 22 часа. Я удалил половину файлов (с 19 441 820 файлов / 178 Гб сократилось до 9 720 910 файлов / 82 Гб). Конфигурация Postgres была аналогична той, что использовалась во второй попытке.
Потребление памятиПри первой попытке контейнер с Postgres использовал целых 12 Гб, если верить docker stats:
Во второй и третьей попытке использование памяти сильно упало — примерно до 1.6 Гб:
Потребление CPUПостроение GIN-индекса в Postgres похоже на однопоточное — такой результат получен при индексировании одной таблицы, в дальнейшем протестируем несколько таблиц.В первой попытке использование CPU не превышало 156%:
Во второй попытке потребление CPU в среднем было 150–200%:
В третьей попытке потребление CPU было примерно таким же и составляло 150–200% с небольшим всплеском до 350% к концу:
Input/Output (Ввод/Вывод) Операции ввода-вывода для диска в процессе индексирования варьировались около 250 Мб/с для чтения (голубой цвет) и записи (красный). Бенчмарки диска, установленного в тестовый MacBook, показывают, что он способен достичь скорости примерно 860 Мб/с на чтение/запись с < 5% утилизацией CPU.
Примечание: Postgres вне контейнера показывает лучшую производительность при индексировании.Дисковое пространство
postgres=# select count(filepath) from files;
  count
---------
 9720910
(1 row)
postgres=# select SUM(octet_length(contents)) from files;
     sum
-------------
 88123563320
(1 row)
До индексирования Postgres занимал 54 Гб:
$ du -sh .postgres/
 54G    .postgres/
После CREATE INDEX:
$ du -sh .postgres/
 73G    .postgres/
Таким образом, размер индекса для 82 Гб текста составляет 19 Гб или 23% от объема данных.Время запуска базы данныхС эксплуатационной точки зрения стоит отметить, что при штатном завершении работы Postgres, время его запуска практически моментально: Postgres сразу начинает принимать соединения и загружает индекс по мере его использования. В противном случае потребуется около 10 минут на запуск, поскольку при старте происходит автоматическое восстановление.ЗапросыВ общей сложности я выполнил 19 936 поисковых запросов к индексу. Я выбрал запросы, которые, по моим ожиданиям, дают разное покрытие триграммного индекса. То есть те запросы, триграммы которых с большей или меньшей вероятностью встречаются во многих файлах:ЗапросСовпадения # файлов в датасетеvarunknown (2 000 000+)error1,479,45212345678959,841fmt.Error127,895fmt.Println22,876bytes.Buffer34,554fmt.Print.*37,319ac8ac5d63b66b83b90ce41a2d40616350d97f1d3ff91543[e-f]49.8b075175488770Производительность запросовВ общей сложности я выполнил 19 936 поисковых запросов к базе данных (линейно, не параллельно), которые завершились за следующее время:Временной промежутокПроцент запросовКоличество запросовдо 50 мс30%5933до 250 мс41%8088до 500 мс52%10 275до 750 мс63%12 473до 1 с68%13 481до 1.5 с74%14 697до 3 с79%15 706до 25 с79%15 708до 30 с99%19 788Реальная производительность vs. планировщикПриведенный ниже график показывает, что 79% запросов выполнялись менее чем за 3 секунды (ось Y в миллисекундах), в то время как планировщик запросов Postgres планировал их выполнение за 100–250 миллисекунд (ось X):
Если расширить график, чтобы в него помещались все запросы, то будет видно, насколько оставшийся 21% запросов выбивается от остальных. Обратите внимание, что небольшой блок точек в левом нижнем углу представляет собой диаграмму, показанную выше:
Потребление CPU/RAMСледующие графики показывают:
  • верхний — время запроса в миллисекундах,
  • средний — процент использования ЦП (например, 801% означает, что используется 8 из 16 виртуальных ядер),
  • нижний — потребление памяти в мегабайтах.

Из этого можно сделать следующие выводы:
  • Значительное увеличение использования ресурсов к концу — это когда я начал выполнять запросы без LIMIT.
  • Использование CPU не превышает 138% до всплеска в конце.
  • Потребление памяти не превышает 42 Мб до всплеска в конце.
Есть подозрение, что pg_trgm является однопоточным в пределах одной таблицы, но также я предполагаю, что лучшего параллелизма (и, следовательно, лучших результатов) можно добиться при партиционировании, то есть разбиении данных на несколько таблиц.Исследование медленных запросовГрафик зависимости количества повторных проверок индекса (ось X) от времени выполнения (ось Y) показывает, что один из значимых аспектов замедления выполнения запроса — большее количество повторных проверок индекса:
И если взглянуть на EXPLAIN ANALYZE одного из таких запросов, можно подтвердить, что Parallel Bitmap Heap Scan работает медленно из-за Rows Removed by Index Recheck.ПартиционированиеРазделение на несколько небольших таблиц кажется очевидным подходом к тому, чтобы заставить pg_trgm использовать несколько ядер процессора. Я попробовал это сделать: взял тот же набор данных, разделил его на 200 таблиц и обнаружил многочисленные преимущества.№1: Инкрементальное индексированиеВесь прогресс индексирования не будет потерян, если в какой-то момент оно упадет или будет остановлено, как это случилось ранее дважды.№2: Параллельное индексированиеВ отличие от первого подхода, показавшего, что при построении индекса использовались всего 1,5-2 виртуальных ядра CPU, с несколькими таблицами я смог утилизировать 8-9 ядер CPU:
№3: Индексирование на 84% быстрееВ отличие от первого подхода, где индексирование занимало 22 часа, параллельное индексирование завершилось всего за 3 часа 27 минут.№4: Индексирование потребляет на 69% меньше RAMВ подходе с одной таблицей пиковое значение потребление памяти составляло 12 Гб. С такой же конфигурацией Postgres можно рассчитывать на пиковое значение потребления памяти всего в 3,7 Гб:
№5: Параллельная обработка запросовРанее загрузка процессора составляла всего 138% (1,3 виртуальных ядра CPU), а с разделением таблиц — 1600% во время запросов (16 виртуальных ядер CPU). Это показывает, что я выполняю работу полностью параллельно:
Аналогично дело обстоит и с потреблением памяти. Среднее значение потребления памяти увеличилось до 380 Мб в отличие от 42 Мб в подходе с одной таблицей:
№6: Производительность запросовЯ повторно выполнил тот же набор поисковых запросов, но меньше: 350 запросов вместо 19,9 тысяч, что, по-моему, достаточно представительная выборка. Разделение таблиц в целом ускорило выполнение запросов на 200–300% для более тяжелых запросов. Раньше они занимали 20–30 секунд, а теперь всего 7–5 секунд благодаря параллельному выполнению запросов (верхний график — до, нижний — после, оба в миллисекундах):
Также я сгруппировал запросы на основе LIMIT, указанного в запросе, и распределил их по временным интервалам — сколько запросов завершилось менее чем за 50 мс. Сравнение этих двух показателей показывает, что менее сложные запросы или запросы с меньшим количеством результатов пострадали незначительно, в то время как более крупные запросы получили существенный прирост в производительности:ИзменениеОграничение по результатамБакетЗапросов в бакете доЗапросов в бакете после-33%10<50мс33%0%+13%10<250мс44%57%+33%10<1с77%100%-29%100<100мс29%0%+20%100<500мс50%70%+19%100<10с80%99%-12%1000<250мс12%0%-13%1000<2.5с77%64%+23%1000<20s77%100%+4%none<20с0%4%+18%none<60с0%18%Docker vs. нативный PostgresСначала я не думал о влиянии производительности при запуске Postgres в Docker. Thorsten Ball задавался вопросом о потенциальном источнике разницы в производительности IO-операций.Все тесты, приведенные выше, были произведены на Postgres, запущенном в Docker с использованием драйвера osxfs, а не на экспериментальном драйвере FUSE gRPC. Я дополнительно прогнал те же тесты на собственном сервере Postgres и обнаружил следующие ключевые изменения.Потребление CPU/RAMПотребление CPU и памяти было похожим на то, что я наблюдал с Postgres в Docker.Индексирование стало на 88% быстрееРазбиение одной большой таблицы на несколько маленьких происходило следующим образом:
CREATE TABLE files_000 AS SELECT * FROM files WHERE id > 0 AND id < 50000;
CREATE TABLE files_001 AS SELECT * FROM files WHERE id > 50000 AND id < 100000;
...
Процесс разбиения был куда быстрее в нативно запущенном Postgres. На построение каждой таблицы уходило от 2 до 8 секунд, тогда как при запуске в Docker на это уходило 20–40 секунд.Параллельное создание триграммных индексов CREATE INDEX IF NOT EXISTS files_000_contents_trgm_idx ON files USING GIN (contents gin_trgm_ops); также было быстрым — всего 23 минуты вместо примерно 3 часов в Docker.Скорость обработки запросов увеличилась на 12–99%Я запустил те же 350 запросов с прошлого теста и обнаружил несколько существенных улучшений:
  • Запросы, которые ранее выполнялись очень медленно, улучшились на ~12%. Вероятно, это связано с операциями ввода-вывода, необходимыми при взаимодействии с 200 отдельными таблицами.
  • Для средних по времени запросов прирост составил примерно 5%.
  • Запросы, которые ранее были очень быстрыми (вероятнее всего, поиск осуществлялся по одной-двум таблицам), улучшились на 16–99%.
Исчерпывающие детали сравнения: негативные изменения —  это хорошо.Выводы
  • Директория .git , даже с клонированием --depth=1, составляет 30% от размера репозитория на диске (по крайней мере, в 10 000 лучших репозиториях GitHub).
  • Файлы более 1 Мб (часто бинарные файлы) составляют 51% от объема данных на диске.
  • Используя только MacBook, можно построить GIN-индексы Postgres по 10 000 репозиториев GitHub и выполнять большинство разумных запросов менее чем за 5 секунд. Дело пойдет гораздо быстрее при использовании более мощного железа.
  • pg_trgm выполняет индексирование и поиск однопоточно, если не разделить данные на несколько таблиц.
  • По умолчанию Postgres сжимает колонки с типом text, что в результате приводит к уменьшению размера на 23%.
  • pg_trgm индексы занимают около 26% размера данных на диске. Таким образом, если индексируется 1 Гб необработанного текста, Postgres может потребовать примерно 827 Мб для хранения данных, а для индекса — около 279 Мб.
  • Однозначно стоит разделять данные на несколько таблиц при использовании pg_trgm. Такой подход позволяет уменьшить время на построение индекса (в нашем случае — с 22 до 4 часов).
  • Bind mount — довольно медленная технология за пределами хост-окружения Linux.

===========
Источник:
habr.com
===========

===========
Автор оригинала: Stephen Gutekanst
===========
Похожие новости: Теги для поиска: #_vysokaja_proizvoditelnost (Высокая производительность), #_postgresql, #_*nix, #_administrirovanie_baz_dannyh (Администрирование баз данных), #_postgresql, #_bazy_dannyh (базы данных), #_proizvoditelnost (производительность), #_benchmarki (бенчмарки), #_macbook, #_indeksy (индексы), #_blog_kompanii_lamoda (
Блог компании Lamoda
)
, #_vysokaja_proizvoditelnost (
Высокая производительность
)
, #_postgresql, #_*nix, #_administrirovanie_baz_dannyh (
Администрирование баз данных
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 25-Июн 11:05
Часовой пояс: UTC + 5