В предыдущей статье я рассказал, как и почему мы выбрали Вертику. В этой части я постараюсь рассказать об особенностях этой необычной базы данных, которой мы пользуемся уже более двух лет. Написание этой статьи заняло несколько больше времени, чем я планировал, в частности из-за того, что надо было рассказать с одной стороны достаточно технически подробно, с другой — доступно, и при этом не нарушить NDA. В результате я пошел по компромиссному пути: я попытаюсь описать, как Вертика устроена и работает в принципе, не касаясь деталей.
Часть 3. Vertica. Simply Fast
Simply Fast — этот вертиковский слоган возник не на пустом месте. Она, действительно, очень быстрая. Быстрая даже с “коробочными” настройками, что показали наши тесты во время выбора решения. В процессе миграции инфраструктуры мы хорошо изучили, как сделать Вертику еще быстрее и получать от нее максимальную производительность. Но обо всем по порядку.
Начну с того, что в Вертике нет нескольких очевидных вещей. Наверное, это может удивить, но в Вертике, по сравнению со многими популярными SQL-базами данных нет:
- хранимых процедур и недетерменированных функций
- строгих констрейнтов — то есть можно добавлять записи, которые нарушают констрейнты
- возможности удалять колонки
- индексов
- кэшей
Если наличие индексов компенсируется особым способом хранения данных, то остальные ограничения — это, скорее, вынужденная необходимость, плата за производительность. Некоторые вещи делать не совсем удобно, однако, серьезной проблемой они не являются. Отдельно скажу про отсутствие кэша. Вертика считает, что те объемы данных, которые используются для анализа (а это терабайты), в кэш все равно не уместить, поэтому и делать его не стоит. Любой запрос идет за данными на диск. Есть некоторая специальная область памяти, которая используется для промежуточного хранения данных при записи или изменении. Но как правило, все на диске. Это несколько противоречит устойчивому мнению, что в базе данных самое медленное место — это дисковая подсистема, и поэтому надо стремиться максимально поднимать данные в память. Тем не менее.
Я не буду подробно касаться высокой производительности загрузки, по сути она ограничена той скоростью, с которой диски успевают записывать, с поправкой на то, что данные закодированы с целью уменьшения объема. С точки зрения пользователей, гораздо интереснее производительность SQL-запросов. Вертика — специализированная аналитическая база данных, поэтому от нее не надо ждать высокой производительности для простых запросов по ключу. В полной мере она раскрывается на типичных для аналитики агрегирующих запросах с фильтрами.
Высокая производительность аналитических запросов обеспечивается несколькими способами организации физического хранения данных:
- Колонко-ориентированное хранение данных
- Оптимизированный под определенные типы запросов способ хранения колонок
- Возможность иметь много физических представлений таблицы, оптимизированных под разные задач
- Линейное масштабирование кластера, MPP — massive parallel processing, то есть один запрос может распараллеливаться на все узлы кластера и на все ядра процессора одновременно
Остановимся на этих способах и как они влияют на производительность поподробнее.
Колонко-ориентированное хранение
Обычно таблицы, используемые в аналитических задачах, имеют десятки и сотни колонок, характеризующих некоторые события (факты). Например, статистика продаж, статистика звонков или интернет-соединений, статистика показов и кликов, динамика цены рыночного актива, и т.д. Характеристики событий обычно называют измерениями (dimensions), а все вместе — многомерным кубом данных. Однако, человек никогда не анализирует все характеристики вместе, а рассматривает, во-первых, те или иные срезы данных, а во-вторых, проекции на некоторое относительно небольшое число измерений. (Специалисты по теории принятия решений давно установили, что человек не может анализировать одновременно больше семи параметров). Переводя на язык баз данных, срез — это предикат или условие where, а проекция на определенные измерения — это агрегирование с group by. Я немного упрощаю, есть еще joins, бывают аналитические функции и др., но это уже незначительные детали. В любом случае из всех колонок в запросе используется лишь небольшое число. Поэтому возможность читать с диска не все данные, а лишь их часть, существенно ускоряет запросы. Физически каждая колонка хранится в одном или нескольких относительно небольших файлах, которые никогда не меняются (только создаются или удаляются). Поэтому чтение колонки — очень быстрая операция.
Специфические способы кодирования (encoding) колонок
Кодирование колонок в Вертике используется для двух целей:
- уменьшение размера данных на диске, тем самым ускоряя чтение-запись
- размещение данных на диске таким образом, чтобы по ним удобно было делать фильтрацию, джойны, группировку, сортировку и прочие стандартные операции
Как вы помните, в Вертике нет индексов. Вместо этого, сами данные на диске “укладываются” так, чтобы их можно было быстро найти. Способ укладки называется проекцией (projection). Это намек на то, что в проекции могут быть не все колонки, но к этому мы вернемся позже. Быстрый поиск достигается при помощи сочетания порядка (order by) и RLE (run length encoding, то есть кодирование количества повторений значения подряд). Как работает RLE с порядком проще всего продемонстрировать на примере. Представьте, что есть таблица с полями event_date, amount и еще 100 полей. Дней в году 365. В каждом дне — 1 миллиард записей. Если типичный запрос — это посчитать деньги за период (день, неделя и т.п.), то используем RLE кодирование для event_date с сортировкой по event_date. Если отсортировать таблицу по event_date, то в колонке event_date были бы блоки их 1,000,000,000 одинаковых значений. RLE кодирование означает, что физически на диске для event_date колонки для каждого дня будет только дата и количество повторений (миллиард в нашем случае). В год — 365 таких записей. Поднять с диска и найти одну — дело долей секунд. Запись с event_date содержит нечто вроде указателя на блоки с остальными колонками. То есть чтобы выполнить “select sum(amount) from t where event_date=’2012-07-01’”
, достаточно прочитать колонку event_date, а потом блок или блоки amount, которые относятся к искомой дате. Это очень быстро. Понятно, что упорядоченных колонок с RLE может быть несколько. И также понятно, что такой способ кодирования лучше всего подходит для колонок, где не очень много уникальных значений. Что типично для измерений.
В целом это несколько напоминает индекс. Однако, работает несколько по-другому. Представьте, что у нас есть еще колонки account_type (два значения), и, скажем, department (пять значений). И по ним тоже часто приходится делать поиск. В случае с индексами пришлось бы делать отдельные, на каждый случай. А в Вертике, достаточно одной проекции, которая будет работать на поиск по всем трем колонкам в любой их комбинации. Например, со следующим порядком: order by account_type, department, event_date. Все колонки с RLE encoding. Козалось бы, что таким образом “ломается” поиск по event_date. С индексом по account_type, department, event_date это так бы так и было. Однако, в случае с Вертикой запрос по event_date будет выполняться лишь немногим медленнее, чем в предыдущем примере… Вместо одного блока event_date, теперь придется прочитать их десять (2 account_type * 5 department), в каждом найти “свой” event_date, и прочитать соответствующий блок amount. C учетом маленького размера блоков event_date из-за RLE, эта разница будет почти незаметна.
Надеюсь, что идея понятна. Вместо индекса некоторые колонки на диске лежат отсортированные и хитрым образом связанные. Обычно это иллюстрируют картинкой или таблицей, которой здесь, к сожалению, не будет.
Помимо поиска, RLE encoding используется также и для ускорения сортировки в group by, так как данные уже расположены таким образом, что иногда их сортировать не надо. Как и в случае поиска, ускорение (так называемый pipelined group by) происходит и в том случае, если group by поле не на первой позиции в проекции.
Вертика поддерживает много способов кодирования колонок, но остальные используются для более компактного размещения данных, что, конечно, тоже влияет на производительность, но не в первую очередь.
Кроме кодирования Вертика поддерживает и традиционное партиционирование данных, а, соответственно, partition elimination или partition pruning, что в некоторых случаях позволяет еще больше сократить объем читаемых с диска данных.
Возможность иметь много физических представлений таблицы, оптимизированных под разные задачи
После прочтения предыдущего раздела может закрасться разумное подозрение, что одним физическим представлением таблицы или проекцией обойтись можно не всегда. Так как даже самый изощренный RLE/order-by не поможет для всех случаев жизни. Поэтому Вертика поддерживает множественные проекции для одной таблицы. Как минимум одна проекция должна содержать все колонки (супер-проекция), а остальные могут содержать только часть. Это позволяет для разных типов запросов строить специфические небольшие проекции. Подход немного напоминает материализованные представления (materialized views). Но MVs — это примочка “сбоку”, а проекции — основной и единственный способ хранения данных. Вертиковским инженерам удалось сделать использование проекций совершенно незаметным для пользователей. Они сами поддерживаются в актуальном состоянии, а оптимайзер запросов безошибочно, насколько я могу судить, выбирает самую подходящую проекцию для каждого запроса.
Поняв основные принципы, довольно просто самостоятельно разрабатывать дизайн проекций под конкретные задачи. Но для начинающих или ленивых в комплекте с Вертикой есть специальная утилита — DB Designer, которая помогает сгенерировать оптимальный дизайн для набора таблиц и тестовых запросов.
Все изменения дизайна можно производить на работающей базе данных. Существующая супер-проекция используется для создания новых. Это, конечно, подсаживает производительность, но при небольших проекциях (по количеству колонок) не занимает много времени.
Линейное масштабирование. MPP
Вертика почти линейно масштабируется по количеству:
- дисков
- ядер
- серверов
Масштабирование по серверам обеспечивается равномерным “размазыванием” (сегментированием) проекций по серверам (узлам) кластера. Способ сегментации задается разработчиком — обычно это хэш функция от одной или нескольких колонок, нг может быть и явно заданное условие. При запросе каждый из серверов выполняет операции над своей частью, включая для этого все ядра, а затем результаты объединяются на сервере, инициировавшим запрос. Ничего сверхъестественного кроме того, что это реально работает. Не все таблицы имеет смысл сегментировать. Маленькие таблицы-измерения, которые почти всегда используются только в джойнах, обычно не сегментируются, и на каждом узле хранится полная копия. Для одной и той же таблицы можно одни проекции сегментировать, а другие нет. Все это позволяет очень гибко настраивать физический дизайн под конкретные задачи, добиваясь оптимальной производительности.
Кроме производительности, кластер позволяет сконфигурировать отказоустойчивость. В этом случае каждый блок данных хранится в двух или более экземплярах на разных узлах кластера. Уровень дублирования называется K-safety level. Со стандартным уровнем 1 кластер переживает потерю от одного (гарантированно) до floor(N/1) узлов в самом лучшем случае. Однако, мы обнаружили, что производительность кластера с одним или несколькими упавшими узлами для некоторых (не для всех) типов запросов может существенно (на порядки) снижаться. Вертиковцы подтвердили, что это баг и он будет исправлен (уже в Vertica 6), а нам удалось изменить запросы так, чтобы не попадать на эту проблему. К слову, железо ломается часто, и нам неоднократно приходилось жить на “хромом” кластере.
Вертика поддерживает прозрачное расширение кластера, причем процесс полностью управляем. После добавления новых нужно вручную или автоматически перебалансировать все проекции. На продакшн-системе это лучше делать вручную в не очень рабочие часы, так как перебалансировка сильно загружает диски. Начиная с 5й версии данные на дисках хранятся таким образом, чтобы их было удобно перебалансировать (elastic cluster). Достаточно передвинуть блоки, не занимаясь их перекодированием. Это гораздо быстрее и менее затратно по производительности.
Еще немного практики. Нам пришлось сделать несколько разных экземпляров базы данных на Вертике на разном железе под разные задачи (см. мою не самую удачную статью про резервирование и клонирование данных). Произошло это из-за того, что при всей масштабируемости, кластер относительно медленно выполняет короткие простые запросы. Вероятно, сказываются сетевые задержки и необходимость лишнего “прохода” для объединения результатов с разных узлов. Сила кластера раскрывается, когда для выполнения запроса требуется на самом деле много данных, сортировка большого объема и т.д.
Расширения Вертики
Вертика одна из первых из производителей RDBMS предложила интеграцию с Хадупом. Поэтому было естественно попробовать одну и ту же задачу посчитать на Вертике и на Хадупе. В некотором роде мы шли по следам статьи “A Comparison of Approaches to Large-Scale Data Analysis” . Для эксперимента была использована реальная задача подсчета уникальных посетителей по ряду измерений. Надо заметить, что вертиковский адаптер не очень быстрый, поэтому мы тестировали и через адаптер, и через сохранении данных сначала в HDFS.
Мы пробовали несколько реализаций для этой задачи. Тестовый сценарий — подсчет числа уникальных посетителей, с разбивкой по странам и еще некоторым измерениям. Объем тестовых данных — около 300 миллионов записей, в которых 60-70 миллионов уникальных. Мы намеренно не использовали кластера, так как и Вертика и Хадуп масштабируются практически линейно, и результаты одно-серверного эксперимента были бы достаточны. Понятно, что Хадуп на одном сервере в реальности использовать смысла большого нет. Места распределились следующим образом:
- Google Big Query — самый быстрый, запрос выполнялся пару минут. Пришлось отказаться, после того как гугловцы захотели за свой сервис очень много денег (получалось несколько сотен долларов на запрос). Здесь мы использовали md5 идентефикатор посетителя; в остальных случаях — 8-байтовый int. То есть GBQ мог быть еще быстрее. Отдельная проблема — загрузка данных.
- Vertica — отдельностоящий сервер (не кластер) на хорошем железе, который использовался только под эту задачу. Выполнение запроса примерно 15 минут. Задачу можно было довольно легко распараллелить, если бы была такая необходимость. Скорость нас устраивала.
- 1-thread Plain Java. 2 часа. Один проход и HashMap.
- Hadoop (не кластер). Запрос на Pig. В лоб порядка 8-10 часов. С хранением данных локально и загрузкой всех ядер — 2 часа (то есть сравнимо с однопотоковым джава приложением). Вероятно, расширение до кластера дало бы линейный или близкий к линейному прирост. Но разница с Вертикой все равно очень большая.
Резюмируя эксперимент, мы убедились, что интеграция работает, а Hadoop, как и ожидалось, достаточно медленный. Преимущество написания запросов непосредственно на Pig/Hive поэтому достаточно сомнительное, и полностью отсутствует, когда ту же задачу можно выразить в терминах SQL. Возможно, у нас еще появятся задачи, где требуется полноценный MapReduce.
Hadoop — это не единственный способ расширить функциональность Вертики. Вертика позволяет писать UDF на С++, в том числе свои агрегатные и аналитические функции, а в последней версии предлагает интеграцию с языком R. Мы и раньше пробовали интегрироваться с R (и даже в качестве интересного упражнения с J), решая на нем задачи, сложные с точки зрения SQL. Для этого использовался ODBC или наш собственный REST-сервис. Теперь эта интеграция еще более упрощена.
Заключение
В настоящее время Вертика у нас работает в продакшене уже более полутора лет. У нас есть и односерверные и кластерные системы. Вертика показала себя очень надежной базой данных, у нас ни разу не было потери данных или отказа по вине базы данных. Были ошибки в одной из первых версий, которые приводили к неправильным результатам некоторых запросов, но они быстро исправлялись. Были проблемы с производительностью, особенно, на «хромом» кластере, но и они были тоже решены. В настоящее время мы загружаем в наши вертики около миллиарда фактов в день (а на одном из серверов — 5 миллиардов), которые агрегируются в пару сотен миллионов записей в разных агрегатах. Вертика выполняет порядка 10тыс пользовательских запросов в сутки, и примерно в два раза больше запросов от рантайм-систем, мониторинга и прочих внутренних роботов. Большинство пользовательских запросов, обычно запрашивающих срезы статистики за дни или недели, выполняются 5-10 секунд, а рантаймовые (довольно простые) — 1.5-2 секунды. Это не OLTP-база данных, это аналитика, поэтому такие времена ответов вполне устраивают. Однако, сейчас мы мигрируем на более мощный кластер, который уменьшает время выполнения запросов в среднем в 3 раза.
Но время не стоит на месте, мы задумываемся о необходимости иметь что-то вроде multi-tier datawarehouse или in-memory OLAP. Но об этом в следующей статье.
P.S. 11июля я могу ответить на любые вопросы по Вертике на нашем митапе в Москве: habrahabr.ru/events/836/
Автор: alexzaitsev