Сравнительный анализ представлений CouchDB

в реальном мире

Kanceľarśka Sotńa (Сотня белых воротничков) - это украинская неправительственная организация, цель которой - способствовать изменениям в нашем гражданском обществе с помощью технологий. Это означает, что мы сталкиваемся с различными техническими проблемами в рамках наших усилий в таких проектах, как (большинство сайтов без английской версии, извините) оцифровка старых бумажных деклараций с помощью краудсорсинга, поиск владельцев элитной недвижимости , Реестр политически значимых лиц, реестр помощников политиков. Некоторые из этих работ перекрестно опыляются с другими инициативами, такими как открытый исходный код обработки естественного языка для украинского языка.

Apache CouchDB 2.0 - это наш выбор в качестве основной части нашего последнего (находящегося на стадии разработки) проекта, в котором мы стремимся в конечном итоге обработать миллионы налоговых деклараций и деклараций о праве собственности, периодически поданных украинскими государственными служащими, и передавать их в бизнес-аналитику. инструмент для анализа или действий на основе определенных правил. Целью CouchDB будет запуск ряда нетривиальных картографических функций для каждого документа, которые, скорее всего, сами по себе будут большими и вложенными.

CouchDB - это хранилище документов со встроенными мощными инструментами MapReduce. Каждая функция карты, представленная для базы данных, создает постоянное представление, заполняемое ленивым применением этой функции ко всем затронутым документам по запросу и постепенно с новыми добавлениями. Это очень удобная концепция для такой задачи, как наша, поскольку мы не будем часто обновлять функции, а мы, возможно, захотим часто запрашивать результаты и быстро их извлекать. В дополнение к этому, CouchDB поддерживает JavaScript и Erlang как функциональные языки из коробки с возможностью добавления любых других через внешний сервер запросов (например, Python).

Однако важно измерять производительность с помощью реальных наборов данных и функций, чтобы иметь рабочее представление о сроках и последствиях для ресурсов такой системы.

Есть множество отличных тестов, таких как этот Кевина Дж. Цю. Но он измеряет синтетические наборы данных и простые функции, что хорошо для необработанных операций или документов в секунду при определенной настройке, но реальный мир часто отличается от наших предположений.

Итак, начнем с самого начала.

Набор данных

В этой статье мы будем использовать подмножество публичных документов от Национального агентства по предотвращению коррупции Украины. А именно 290406 объявлений с довольно сложной структурой. Этот набор данных прошел некоторую предварительную обработку, такую ​​как нормализация числовых значений от строкового представления к числам с плавающей запятой. Обработка данных - полезная процедура в целом, поскольку она экономит вычислительные затраты при последующем применении функций. Лучше потратить некоторые ресурсы один раз, чем тратить их многократно.

Исходный размер в JSON перед импортом составляет ≈4,8 ГБ. CouchDB делит этот набор данных на 8 сегментов, что составляет ≈0,9 ГБ на диске (сжато с помощью Google Snappy). Сырой JSON (сжатый) доступен здесь. В будущем я надеюсь создать какой-то общедоступный узел, доступный для репликации чтения (возможно, Cloudant?)

В конце концов, мы хотим масштабироваться для миллионов документов и десятков ГБ, увеличиваясь каждый год с новыми документами.

Методология

Протестированные серверы запросов - это couchjs по умолчанию (под капотом довольно старая Mozilla SpiderMonkey 1.8.5), couch-chakra (экспериментальный сервер запросов JS, использующий MS ChakraCore), couchpy с CPython 2.7. 9 (к сожалению, couchpy не поддерживает Python 3 на сервере запросов) и PyPy 5.6.0 (Python 2), а также Erlang через собственный сервер запросов. Мы будем тестировать каждый сервер запросов с функциями, максимально похожими друг на друга. В дополнение к этому мы будем использовать базовую функцию Erlang, которая минимальна, насколько это возможно, но делает что-то полезное.

Показатели производительности будут измеряться с помощью сценария, основанного на работе Кевина, который запрашивает активные задачи CouchDB каждую секунду, записывая разницу во времени между последним обновлением и запуском задачи, а также количество изменений, обработанных задачей. Скорость - это количество изменений, деленное на разницу во времени. Мы выведем сумму по всем задачам выборки и метрики одной задачи. В конце процесса мы усредним все суммы средней скорости и выведем общее время, затраченное последней активной задачей (которая является самой длинной оставшейся задачей).

В то же время мы будем вручную запрашивать использование системных ресурсов, когда обработка приближается к финишной черте. Для этого воспользуемся программой pidstat из замечательного пакета sysstat со следующей командой:

sudo pidstat -hrudvlI -C "beam|couchjs|couch-chakra|python|pypy" 2 1 > /path/to/stat.log

Это будет собирать и сообщать о процессоре (с учетом SMP), памяти, вводе-выводе и потоках ОС для процессов, которые имеют «луч» (Erlang VM), «couchjs», «couch-chakra», «python» или «pypy». в своих соответствующих командах один раз с интервалом в 2 секунды.

Сам CouchDB будет запускаться из специально построенного контейнера докеров в режиме одного узла с настройками по умолчанию (кроме дополнительных серверов запросов). Перед каждым запуском теста база данных будет очищаться, и весь набор данных будет повторно импортирован. Это связано с тем, что CouchDB никогда ничего не удаляет, поскольку основан на структуре данных только с добавлением, что на самом деле довольно хорошо для реальных случаев использования (при регулярном обслуживании), но не так много для повторного тестирования. Представления будут повторно использовать столько данных, сколько они могут даже из удаленных объектов (на самом деле только отмеченных как удаленных). CouchDB можно заставить действительно отказаться от него путем сжатия и процедуры очистки представления, но простая очистка БД более надежна, и я ленив :)

Примечание: в этой статье не будет проверяться сокращение или какие-либо другие функции, которые поддерживает CouchDB.

Код для образов докеров, справочных функций и вспомогательных скриптов можно найти в репозитории проекта.

Испытательная установка

БД будет работать на моем ноутбуке с четырехъядерным процессором Intel i7-4710HQ (8 с HT) с частотой 2,50 ГГц, 16 ГБ оперативной памяти DDR3L, 1 ТБ Seagate Hybrid «SSHD» (да, не SSD, дополнительную информацию см. В результатах). Операционная система хоста - Fedora Linux 25 с ядром Linux 4.9.13. Все тесты будут выполняться из VT с полностью отключенной графической средой, чтобы минимизировать внешнее влияние на ресурсы.

Механизм хранения Docker - это «оверлей2», поддерживаемый встроенной в ядро ​​«оверлейной» ФС. Объем данных БД напрямую отображается на хост-систему FS, которая смонтирована с расширением «ext4» с relatime,barrier=0.

Хорошо, хватит скучной болтовни - перейдем к сути!

Исходный уровень

Чтобы иметь какой-то уровень, выше которого мы не сможем зайти слишком далеко с этой настройкой, я подготовил очень простую функцию карты в Erlang. Полный отказ от ответственности, прежде чем мы двинемся дальше: я не очень хорошо разбираюсь в Erlang, поэтому, пожалуйста, извините меня, если ваше представление о функциональном программировании серьезно задето из-за того, что будет дальше (и, пожалуйста, оставьте отзыв!).

Это отображает документ в простое представление, при условии, что значение step_0.declarationType равно «1».

Представление:

INFO:dragnet.addview:average = 1614.30c/s (all_tasks) 203.20c/s (one task)
INFO:dragnet.addview:total time = 180s

Таким образом, это было выполнено ровно за 3 минуты со средней скоростью 1614,30 документов (или изменений) в секунду для всех 8 задач (одна задача на сегмент) или 203,20 для одной из этих задач. Это практически лучшее, что мы можем сделать с этим набором данных.

Очень сложно измерить использование ресурсов собственными задачами представления Erlang, поскольку они выполняются внутри процесса CouchDB и неразличимы, поэтому я не предоставляю это.

JavaScript

Что ж, ECMAScript. Или что сейчас?

Начнем со встроенного сервера запросов couchjs. На самом деле он не такой встроенный, как Erlang, в том смысле, что на самом деле это отдельная программа, взаимодействующая с CouchDB через конвейер, а не с внутренними механизмами.

Это написано на ES5.1, что SpiderMonkey 1.8.5 прекрасно понимает. Обратите внимание, что это намного сложнее, чем базовая функция. Он по-прежнему отображает документы в гораздо меньшие представления, но в то же время выполняет некоторые итерации по структуре данных, выполняя некоторые сравнения и арифметические операции. В предыдущем воплощении он также включал синтаксический анализ числового значения из incomeDoc.sizeIncome, но, поскольку мы переместили это в помощник импорта, он больше не нужен.

Насколько это будет плохо?

INFO:dragnet.addview:average = 1316.46c/s (all_tasks) 164.20c/s (one task)
INFO:dragnet.addview:total time = 228s

Это несколько хуже, чем базовый уровень, но не в критическом смысле. Давайте посмотрим на использование ресурсов (которое мы теперь можем разделить).

Хотя статистика ЦП не очень полезна, поскольку она относительна, она показывает пропорции. Размер виртуальной памяти («VSZ») и незначительные ошибки страниц ясно показывают, что CouchDB удалось поместить весь набор данных в кеш, при этом оперативно используя только то, что ему нужно в данный момент. Никто не заблокирован на дисковом вводе-выводе, и база данных выполняет умеренные записи. Использование памяти процессами сервера запросов неплохо, но, думаю, могло бы быть лучше.

Ладно, это было не так уж и плохо. Перейдем к причудливой «диван-чакре» и его движку Microsoft ChakraCore, на котором работает их новый браузер Edge. Мы будем делать ES6 с этим, поскольку движок изначально поддерживает его.

В данном случае это не сильно отличается, просто небольшое удобство, но это может избавить вас от боли в долгосрочной перспективе.

В этом тестовом прогоне его производительность была чуть лучше, чем у couchjs, и это общая тенденция, которую я наблюдал с ним - стабильно лучше, но только с небольшим отрывом.

INFO:dragnet.addview:average = 1330.95c/s (all_tasks) 166.98c/s (one task)
INFO:dragnet.addview:total time = 226s

Использование ресурсов рисует несколько иную картину.

Менее реальное использование памяти couch-chakraprocesses, что неудивительно, поскольку Microsoft продает ChakraCore для IoT-сцены. Я не совсем уверен, что случилось с использованием виртуальной памяти (35 ГБ!), Вполне возможно, что это просто где-то ошибка, поскольку этот сервер запросов в настоящее время находится на очень ранней стадии разработки. Использование процессора аналогично.

Я бы сказал, что это хорошая перспектива, а встроенная поддержка ES6 + - хорошая вещь в моей книге. Более того, автор проекта Даниэль Мюнч (Daniel Münch) проделывает увлекательную работу по превращению его в собственный сервер наравне с Erlang.

Но этого достаточно ECMAScript для этой статьи, пересылка к…

Python

Проект couchpy в настоящее время поддерживает Python 3 для своего клиента, но не для сервера запросов. Я честно пробовал запустить его на Python 3, но он вылетал во многих отношениях, поэтому я не стал беспокоиться о продолжении, так что это Python 2.

Он концептуально выполняет то же самое, что и функции ES, максимально приближенным образом.

Мы сделаем это с двумя средами выполнения: CPython 2.7.9 и PyPy 5.6.0. Результаты первого CPython.

INFO:dragnet.addview:average = 1341.77c/s (all_tasks) 167.98c/s (one task)
INFO:dragnet.addview:total time = 222s

Еще на шаг ближе, но опять же, не существенно.

Вот это интересно. Примерно в два раза меньше памяти, чем у обоих ES-серверов, меньшее использование ЦП процессом сервера запросов, отсутствие ошибок страниц, более эффективный процесс CouchDB (возможно, тратит меньше времени на ожидание результатов). Он определенно делает что-то намного лучше. Не уверен, что это интерпретатор (который даже не JIT!), Сервер запросов или функция достигли удачного пути или, возможно, это комбинация факторов.

Но давайте посмотрим на PyPy с той же функцией. Наверняка с JIT все даже лучше, правда?

INFO:dragnet.addview:average = 1154.04c/s (all_tasks) 143.07c/s (one task)
INFO:dragnet.addview:total time = 251s

НЕТ.

Фактически ожидается более высокое использование памяти из-за того, что JIT и GC выполняют свою работу. Фактически, этот плохой (на самом деле худший) результат можно объяснить накладными расходами на JIT-компиляцию. В этом тесте важно просто как можно быстрее выполнить небольшой фрагмент кода, не позволяя JIT разогреться должным образом. PyPy отлично справляется с обработкой сложных чисел и запуском сложного объектно-ориентированного кода, а не с тестами. Вполне вероятно, что некоторые функции могут попадать по неправильным путям с CPython и работать намного лучше с PyPy.

А теперь о другом ...

Erlang

Хорошо, признаю, изначально я действительно делал это, просто чтобы выглядеть как один из тех крутых детей FP на блоке :) Хотя мне потребовалось несколько часов, чтобы на самом деле написать идентично работающую функцию, которая не дает сбоев, это была довольно забавное упражнение. Мне не приходится заниматься функциональным программированием в повседневной жизни, так что терпите меня.

Основная его часть - это функция Calc_income/2, которая вместе с lists:foldl/3 заменяет итерацию предыдущих случаев рекурсивной редукцией с оптимизацией хвоста. Код несколько «уродлив» из-за того, что CouchDB принимает только одну анонимную функцию, поэтому ее нельзя разделить на несколько подходящих функций, соответствующих шаблону.

Во всяком случае, он смешается?

INFO:dragnet.addview:average = 1507.90c/s (all_tasks) 190.04c/s (one task)
INFO:dragnet.addview:total time = 197s

О, да. Это намного ближе к нашему исходному уровню. Фактически, это всегда будет иметь место с собственными серверами запросов. Если только вам не удастся написать очень плохую функцию (что не так уж сложно, заметьте!)

Причина в том, что он никогда не выполняет конвейерный ввод-вывод, выполняясь внутри виртуальной машины Erlang в том же процессе, что и CouchDB, тем самым снижая накладные расходы до абсолютного минимума.

В то же время это ясно показывает, как усложнение функции влияет на скорость обработки по сравнению с базовой линией.

Я не предоставляю статистику использования ресурсов по тем же причинам, что и для базового уровня.

Вход, выход и их отсутствие

Пока что даже с Erlang результаты не кажутся слишком разбросанными, не на порядок или даже в два раза. Это заставило меня задуматься, может ли дисковый ввод-вывод мешает моему жесткому диску. Может ли SSD работать лучше? Внимательный читатель заметил бы, что статистика дисковых операций ввода-вывода не показывала слишком много операций записи (максимум 2 МБ / с) и не происходила блокировка операций ввода-вывода, но при этом необходимо учитывать задержку.

Несмотря на то, что у меня под рукой не было сопоставимой системы SSD, есть еще лучший способ исключить дисковый ввод-вывод - ramdisk. tmpfs - это особый вид файловой системы в Linux, которая сопоставляет расположение файловой системы с областью энергозависимой памяти, так что все операции ввода-вывода в этой области происходят в вашей оперативной памяти. Единственное требование - наличие достаточного количества ОЗУ для всего содержимого этого места, в противном случае начнется подкачка (или OOMing, если подкачки нет).

Поэтому я перезапустил свой док-контейнер с объемом данных, сопоставленным с каталогом внутри /tmp, который в Linux обычно монтируется как tmpfs. С 16 ГБ ОЗУ и около 1 ГБ данных это вполне возможно.

Имеет смысл сначала проверить это с помощью базовой функции Erlang. Если дисковый ввод-вывод является узким местом, он должен быть заметно быстрее. Если это не будет быстрее, никто другой не будет.

Момент истины…

INFO:dragnet.addview:average = 1622.27c/s (all_tasks) 203.39c/s (one task)
INFO:dragnet.addview:total time = 180s

Он практически такой же, как и на HDD. Чтобы убедиться, что у нас нет доступа к диску, на этот раз мы также измерим использование ресурсов.

Нет, нет ввода-вывода с 3 последовательными запросами с интервалом в 2 секунды, так что это выглядит правильно.

Это приводит нас к…

Выводы

Что, я считаю, к настоящему времени совершенно очевидно, но вот несколько быстрых диаграмм, чтобы подвести итог.

При достаточно большом и достаточно сложном наборе данных применение нетривиальных функций обычно зависит от ЦП. Твердотельные накопители не принесут особой пользы при записи и при размещении данных в памяти - при чтении разные серверы запросов могут быть лучше других только с точки зрения производительности.

Это, конечно, не вся история. К каждому тесту следует относиться с недоверием, поскольку в действительности он не может переставить все возможные варианты использования. Я хочу повторить, что это о сложном наборе данных и нетривиальных функциях. Другое дело, что даже небольшая (но измеримая) разница в производительности имеет значение для масштабирования. Разница в 10 секунд на 1 ГБ может принести 10 секунд минут на 10 ГБ. Или не. Эта экстраполяция также нуждается в тестировании (которое я планирую проводить постоянно, к счастью, с нашими инструментами это довольно просто).

Я действительно тестировал это на гораздо менее мощной системе (с SSD и все еще помещая данные в RAM), и разница между, скажем, родным Erlang и couchjs была примерно в 2 раза в пользу первого. Как в диапазоне от ≈400 с до ≈800 с, и около ≈600 с для CPython. К сожалению, я не записал результаты - я слишком ленив, чтобы повторить это еще раз на медленной машине, но это доказывает мою точку зрения.

Не доводите контрольные показатели до абсолютного, вместо этого сделайте выводы о тенденциях, проведите собственные тесты на своих собственных сценариях использования. Также ... не обрабатывайте гигабайты сложных данных на вашем тостере.

В заключение я хотел бы поделиться некоторыми мыслями о нашем варианте использования.

Было бы заманчиво просто сказать: «Давай воспользуемся CPython и покончим с этим» (потому что, сказав то же самое об Erlang, скорее всего, я оторвусь от общества на конец дней). Он действительно выглядит неплохо во всех отношениях, но я не уверен, что это действительно хороший выбор для будущего. Дело в том, что не гарантируется отличная работа со всеми видами функций, в то время как производительность ES более однородна. Внедрение ES получило более широкое распространение, и у автора «couch-chakra» есть несколько отличных идей по устранению накладных расходов на ввод / вывод канала. Так что пока я предпочитаю использовать «couch-chakra» в нашем проекте в качестве основного сервера запросов и ES6 + в качестве основного языка.

В дополнение к этому, производительность обработки полного набора данных для нас не так важна - это будет происходить не слишком часто, и мы можем реализовать какие-то разогревающие запросы и использовать параметр «устаревший» для реальных запросов, чтобы иметь почти обновленные просмотры, а также возможность их немедленного получения. Кроме того, CouchDB на самом деле довольно хорошо умеет повторно использовать как можно больше данных и избегать полного пересчета. Он также будет постепенно применять карту к входящим документам, поэтому простые обновления набора данных обычно не запускают полную обработку базы данных со всеми доступными представлениями (что, очевидно, может занять много времени).

Когда одного сервера перестает хватать, CouchDB 2.0+ делает кластеризацию довольно простой, просто размещая шарды на большем количестве узлов и балансируя запросы с помощью haproxy. Репликация мастер-мастер делает возможным изменение конфигурации кластера с минимальными усилиями, поэтому в основном это покрывается.

Следует рассмотреть дополнительную предварительную обработку, такую ​​как уменьшение идентификаторов (это может иметь огромное влияние на размер представления и время вставки), значения выбора кодировки и т. Д.

Я продолжу работу над этой статьей, когда планы действительно сбудутся, и это начнет работать с остальными службами. Но на этом пока все. Надеюсь, вам понравилось!