Pull to refresh

Comments 50

А можно подробней про сравнение Druid vs ClickHouse именно в части выборки данных и запросов? (скорость выборки и агрегаций, пускай и в попугаях)

ещё вижу сложности для Druid в случае «Использование внешних данных для обработки запроса», скормить файл на данном этапе в него нельзя.

p.s. Tranquility не использовал, обычно realtime nodes из kafka вычитывали данные самостоятельно.
Как только мы приготовились запускать Druid в продакшн, мы успели накопить в нём данные всего лишь за месяц. Druid показал хорошую производительность на данных с глубиной в несколько дней. При запросах большей глубины были заметные тормоза.

Как только мы увидели ClickHouse в открытом доступе, мы удалили с серверов Druid и поставили ClickHouse. В ClickHouse данные у нас храняться больше 3,5 месяцев.

Прямого сравнения у нас не было, поэтому в статье мы не приводим бенчмарки.

Druid много чего не поддерживает по сравнению с ClickHouse. Мы хотели/мечтали о базе с «SQL-подобными запросами». Когда мы анализировали Druid, он не понравился нам запросами в «своем формате». Писать SQL-запросы приятнее и проще, чем огромные «массивы» в postAggregation, hyperUniqueCardinality и т. д.
тогда еще один вопрос.

С druid масштабирование понятно (докинули серверов в кластер, достаточно указать zookeeper-hadoop, там открылись сегменты новых-ребаланс старых)

С ClickHouse на этапе знакомства я не нашел как по быстрому можно расширить кластер.

Как вы собираетесь увеличивать емкость ClickHouse когда увидите, что нагрузка увеличилась x2 и нужно докинуть серверов?

В общем случае добавление новых узлов в кластер требует либо тяжелой операции перешардирования (с равномерным перераспределением данных), либо использования механизма весов, когда данные пишутся более активно на новые сервера с более высоким весом, и когда объём данных на новых и старых узлах становится примерно равный, веса выравниваются.


У каждого из подходов есть плюсы и минусы:


Перешардирование даёт лучшее распределение данных в кластере и при выполнении запросов узлы кластера будут нагружены примерно одинаково. Минус перешардирования — кроме того, что это очень тяжелая операция, возможно, потребует остановки кластера. Если СУБД, к примеру, является подсистемой, то остановка кластера может быть недопустима.


Использование весов даёт неравномерное распределение, зато позволяет быстро добавлять узлы без создания дополнительной нагрузки на кластер.


Учитывая, что в нашем случае CH находится под нагрузкой, более правильным видится вариант с весами.


Рекомендую:
Перешардирование
bug on groups

ещё вижу сложности для Druid в случае «Использование внешних данных для обработки запроса», скормить файл на данном этапе в него нельзя.

http://druid.io/docs/latest/querying/lookups.html не подходит?

нет, в примере в статье говориться «использовать ids из вот этого файла»
lookup в druid это «я хочу чтобы ты вместо id вернул мне строковое значение из этого словаря» и происходит на этапе уже возврата данных клиенту

Вы пробовали пожаловаться здесь: https://groups.google.com/forum/#!forum/druid-user? Возможно есть решение, про которое я не знаю. Или разработчики услышат частые запросы и добавят такую фичу.

нет, там даже большой массив на вхождение будет неплохо снижать производительность. поэтому обычно все укладывается в бакеты (1: 1-10, 2: 11-20 и тд)

отдельно стоит уточнить, что все ключи для dimension когда bitmap строят в string переводят, поэтому использовать float/double как измерение очень не советую (много место сожрет на хранение, а выборку по диапазону все равно не получите)

В общем для некоторых вещей в лоб он очень неплох, особенно с учетом гибкого масштабирования, но вот модель «как и зачем хранить» как и с любым nosql нужно продумывать досконально

Не строковые dimensions пилят.

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

>> Не строковые dimensions пилят.

это пилили уже пол года назад =) все еще пилят

>> Возможно, потому что никто не сделал эффективный способ, потому что никто не просил

у druid в общем случае это обычный отсортированный словарь ключей, по ключу находят строку в bitmap и дальше бегут по ней находя нужные rowid (условно конечно). когда у нас имеется несколько значений в IN, то нельзя просто сканировать один ряд, нужно пачку сразу и результаты результаты клеить.

дальше учитываем что основной протокол общения это rest/json, соответсвенно и передать большой кусок сложно само по себе.

кстати уже ловил проблему, когда несколько historical возвращают большие ответы и 90% времени запроса проводим в broker занимаясь «распарсили ответ от historical — merge результатов — генерация json клиенту»

p.s. это не наезд, из opensource для некоторых задач, особенно в условиях когда нужна доступность и возможность гибко расширять кластер, druid подходит хорошо и в новом проекте собираюсь опять его использовать
у druid в общем случае это обычный отсортированный словарь ключей, по ключу находят строку в bitmap и дальше бегут по ней находя нужные rowid (условно конечно). когда у нас имеется несколько значений в IN, то нельзя просто сканировать один ряд, нужно пачку сразу и результаты результаты клеить.

Опять не понял, но сейчас в InFilter просто запрос в Set, скорее всего HashSet: https://github.com/druid-io/druid/blob/d981a2aa02aedfdf18e0bed5848e704bd9597563/processing/src/main/java/io/druid/segment/filter/InFilter.java#L110


дальше учитываем что основной протокол общения это rest/json, соответсвенно и передать большой кусок сложно само по себе.

Сейчас да, но запилить "особый" вариант In, который идет за массивом куда-нибудь в сеть или на диск и кеширует этот массив на query ноде ничего не мешает, было бы желание у разработчиков, а желание появляется если появляются клиенты.


кстати уже ловил проблему, когда несколько historical возвращают большие ответы и 90% времени запроса проводим в broker занимаясь «распарсили ответ от historical — merge результатов — генерация json клиенту»

А можно тут подробнее? Это какой тип запроса?

>> Опять не понял, но сейчас в InFilter просто запрос в Set, скорее всего HashSet

посыпаю голову пеплом, уже пофиксили (к версии 0.9.2) =) но это было всего 2 марта, если откроете фикс который он делал, то там хорошо написано: Currently, InDimFilter is translated to «or + selector filters». Value matcher can use hash set for faster filtering.

      List<DimFilter> fields = Lists.<DimFilter>newArrayList(new SelectorDimFilter(dimensionName, value));
       for (String val : values) {
         fields.add(new SelectorDimFilter(dimensionName, val));
       }
       dimFilter = new OrDimFilter(fields);


Or в свою очередь доходит до OrFilter

Обратите внимание на ValueMatcher: он последовательно применяет для каждого selector проверку пока кто-то не сработает. На больших объемах в IN это было достаточно плохо.

В принципе сейчас действительно уже проще, когда смотрел плотно эту часть, там с интерфейсами конкретно под эту задачу было сложнее. Сейчас же просто проверку вместо Set на что-то еще сделать не проблема, а если добавить какой bloom для больших наборов так вообще может оказаться очень хорошо.

>> А можно тут подробнее? Это какой тип запроса?

вот так сразу не вспомню точно условия, но насколько помню было «granularity»: «all» и порядочно сегментов.
Из платных баз данных мы тестировали HP Vertica и Greenplum

Эти СУБД из другого сегмента. Если clickHouse для real-time аналитики(грузим и анализируем одновременно) то эти для такого использования: «загрузили данные, закомитились, проанализировали когда захотели».

Кстати, из приведенных вами запросов вообще не увидел «риалтаймовости» — для исторического анализа real-time особо не нужен.
Что считать real-time наверно, для исторического анализа каждую секунду на больших объемах, это мне кажется real-time. Т.к. даже сами разработчики рекомендуют делать редкие, но большие запросы, а не единичные и частые.
Под realtime мы подразумевали, что данные достаточно быстро доступны для чтения, по сравнению со стеком Hadoop.

В видеороликах продемонстрировано, насколько быстро ClickHouse анализирует исторические данные. Демонстировать запросы данных за последние 5-10 минут тяжело, т. к. их выполнение занимает сотые доли секунды.

Как пример, для чего мы используем запросы, близкие к realtime, могу привести ранжирование статей. Допустим, 1 минутут назад статью просмотрело 100 человек, в промежутке между 2-й и 3-й минутами от текущего времени — 120 человек. Эти данные используются нами для ранжирования статей. У нас порядка 25 тысяч статей. По каждой из них нужно получать состояние по каждой минуте и на основе этой информации ранжировать статьи.
Без обид, но это не real-time аналитика, а просто быстрое выполнение запросов
А что ты считаешь real-time аналитикой? Например, почему кафка+вертика не может быть real-time?
Спасибо, было интересно.

один вопрос, а как выглядит ожидание данных из асинхронного запроса?

С нетерпением жду продолжениея.

Спасибо
А у меня такой вопрос.
Вот к примеру user_uuid проверяется как-то на валидность?

Если да, то делается это средствами ClickHouse или как-то на стороне?
И если это делается через ClickHouse достаточно быстр он для этого, чтоб делать проверку «в лоб»

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

Самим средствами CH — проверить при вставке валидность uuid думаю не правильно, это просто строка, без типизации.


Можно отдельно отправить запрос и узнать сколько «плохих» строк, которые не соответствуют длине, не содержат ‘-’ и т/д.


Лучший кейс — это проверка при сохранении «события» в файл, т/е на стороне «писателя» данных.


Если «писатель» получил пустой uuid или не валидный — то можно проставить новый рандомный uuid, пометив что он искусственный, через отдельную колонку в таблице типа is_true_uuis = [ 0 | 1 ].


Теоретически, можно сделать еще так, через новое поле check_uuis DEFAULT (length(uuid)=61 ? 1 : 0 ), но думаю это тоже плохой кейс.

Большое спасибо за статью и за PHP-библиотеку.

Вопрос касательно последней: почему таки не стали использовать один из готовых HTTP-клиентов? Я видел ремарку об этом в статье, но причины не описаны.

Объясню, почему я считаю, что лучше взять готовый:
— Guzzle или HTTPlug (или подобные) поддерживают promises для асинхронных запросов. Этот подход удобнее, чем Ваш, и, в частности, может быть интегрирован с event loop (тот же React). Ваш клиент я не могу интегрировать с event loop, мне придётся блокироваться для выполнения пачки запросов в любом случае.
— PSR-7 помог бы навешивать всякие middleware на HTTP клиента для библиотеки. К примеру, сейчас мне нужно лезть в кишки, чтобы навесить хитрое журналирование HTTP-запросов, а так бы я просто передал HTTP-клиента, обвешанного моим middleware, при создании клиента и имел бы успех.

Я надеялся избежать этого вопроса )


  • Желание изучить максимально предметную область — все “фичи” CH на самом низком уровне, чтобы реализовать нужный нам ф-ционал, асинхронная отправка с сжатием потока.
  • В нашем приложении под HHVM, использую только максимально легковесный код, в котором можно быть уверенным что все работает так как нужно.
  • Драйвер мы не используем под большой нагрузкой, т/к не представляю у нас такой кейс.

Есть альтернативный драйвер на Guzzle, но в нем не реализован функционал которые хотелось и выглядит он заброшенным.
HTTPlug показался неподходящим, т/к не нашел в нем реализации curl_multi_exec, (возможно плохо искал).
Реализация показалась тяжелой в Guzzle, хотя в нем отлично реализован GuzzleHttp\Handler\CurlMultiHandler


Примеры странного специфичного кода


Вас понял, всё логично. Но я таки посклоняю :)

HTTPlug действительно не в кассу, если хочется таких деталей — он больше для простых/средних запросов в публичных библиотеках. Смысл: это проект-обёртка, по сути, чтобы пользователю было удобно подсунуть туда свою текущий HTTP-клиент, который он использует в проекте (Guzzle5, Guzzle6, React HTTP,..). Это удобно для Facebook API SDK, к примеру, чтобы не навязывать в проект Guzzle, к примеру (но и в то же время не писать очередную обёртку над cURL).

Я бы всё таки рекомендовал посмотреть на Guzzle и попробовал на нём запросы. Ошибки на HHVM связаны в основном с генераторами, а вы их и так не используете. С точки зрения интерфейса там всё довольно просто, как по мне, а производительность нужно измерять, с ней других вариантов нет :) Мне кажется, что Guzzle будет не сильно медленнее, учитывая всё остальные плюсы.

Странные примеры реализуются передачей CURLOPT_FILE & CURLOPT_INFILE в объект запроса через опции. Guzzle не имеет поддержки этих функций в своём интерфейсе, но всегда позволяет передать что-то cURL-у «напрямую».

PR не обещаю, т.к. не работаю пока с ClickHouse сам, а переделка глобальная :) Но в деталях помочь могу.

Ещё раз спасибо за проект!
Возможно глупый вопрос, но все же — почему настолько важно производить ранжирование статей каждую минуту? Неужели от этого так сильно зависит объем читающей аудитории и статьи настолько быстро устаревают? Извиняюсь, если вопрос реально глупый, просто в СМИ не работал, поэтому не в теме.

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


Поэтому сейчас мы ранжируем за 5 секунд весь набор активных новостей, плюс-минус 30 тысяч, и пользователь получает каждую минуту свежий набор новостей. Безусловно, это не значит, что обновляются вообще ВСЕ новости – речь о том, что мы стремимся показывать актуальный набор на конкретный момент времени.

Кто-нибудь из достопочтенных донов в курсе, есть ли какие-то дашборды для работы с CH? Очень хочется красивые графики. Может кто-то запилит плагин для grafana?;)

Готовых продуктов/дашбордов мы не знаем, частично поэтому мы реализовали свой GUI для редактора SQL. В котором планируется реализовать отрисовку графиков ( визуализацию данных) после создания запроса.


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


Для Graphite, в CH предусмотрен отдельный сторедж, который хранит и “сворачивает” данные в виде Graphite схемы данных. GraphiteRollupSorted не документирован, и опыта у нас работы с ним пока нет.

А как реализовывали, у них есть какое-то формальное описание грамматики?

Сообщество выпустило интеграцию с grafana: https://github.com/Vertamedia/clickhouse-grafana


О GraphiteRollupSorted лучше спросить в Telegram группе — там есть люди которые активно используют этот сторедж, мы не стали использовать его.

Спасибо, отличная статья. Вопрос по вот этой ремарке

Для нас не важна транзакционность и отсутствие UPDATE / DELETE. Мы давно привыкли обходить эти проблемы


К примеру, мы всю аналитику делаем в пределах пользовательских сессий (вешается кука, потом в пределах нее что-то делается). Сессия бывает авторизованной или нет, но иногда происходят события, которые меняют сессию (к примеру сессия авторизовалась, либо зарегистрировалась), т.е. происходит update

Потом данные периодически выгружаются в ElasticSearch к которому уже идут аналитические запросы вида
«какие из авторизованных юзеров с сессией > 5 минут посещали такую-то страницу»

Т.е. там используется изменение ранее вставленной сессии (к примеру user_id прописывается). Как вы подобные задачи решаете? я так понимаю — вы бы в этом случае делали отдельную таблицу на авторизацию и join на нее?

Есть много способов решения задачи. Основная идея в том, что лучше хранить события в одной большой таблице и не использовать JOIN по большим данным. Или дублировать данные при вставке в несколько таблиц, в терминологии статьи это значит "писать в несколько TSV-файлов".


Также есть вариант, когда вы пишете RAW событий в одну большую таблицу (назовем ее "поток"), к которой прикреплены MaterializedView. Эти MaterializedView будут раскладывать RAW-поток на нужные составляющие.


Возможно, лучше изменить понятие uuid, и ввести понятие session_id. Сессия меняется, а uuid нет, если это возможно.


Если без uuid, то тогда можно сделать еще такие варианты:


  • Использовать две колонки uuid: например first_uuid и main_uuid. При изменении uuid вы пишете в разные поля, как изменилась сессия.
  • Использовать отдельное поле, массив сессий.
  • Использовать движок CollapsingMergeTree.
  • Ввести единый uuid и добавить признак типа enum|int — is_login.

Я бы посоветовал написать прототип по одному из вариантов или сразу несколько вариантов и начать делать запросы SELECT. Придумать несколько штук, которые самые популярные у вас. И исходя из запросов на чтение понять оптимальную структуру.


В статье мы привели самый простой пример, для упрощения. На самом деле мы используем практически все из перечисленных выше методов. И меняли структуру данных под CH.

Спасибо за подробный ответ
Да, мы как раз денормализуем в одну большую таблицу чтобы не использовать JOIN. Но как раз из-за этого там иногда возникает потребность в обновлении. Предложенные варианты решения — да, их все можно делать, потребность в них возникает как раз из-за отсутствия UPDATE/DELETE
Хотя конечно описанные плюсы перекрывают этот минус
Спасибо за статью! Если нет update'ов, то как лучше решать такой кейс:

Есть таблица с продажами, где у каждой продажи может быть статус pending/approved/rejected/failed — причем эти статусы могут менятся во времени несколько раз. У продаж есть уникальный uuid который никогда не меняется. В некоторых случаях продажи имеют место быть удаленными (коррекции).

Если по каким-то причинах реляционная СУБД не подходит (например, очень много данных) и требуется выполнять обновления записей, то при выполнении ряда условий:


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

рекомендуется использовать движок https://clickhouse.yandex/reference_ru.html#CollapsingMergeTree, работа которого основана на двух состояниях записи { old_state, new_state }, и иметь возможность быстрого получения состояний продаж, которые потенциально могут быть изменены, например, во внешнем key-value хранилище, где ключом будет идентификатор продажи, а значением — информация о продаже, включая статус.


Например, будет следующая структура таблицы: ( sign, ( sale_uuid ), manager_id, price, status ). Тогда работа с данными будет выглядеть следующим образом.


  1. Добавление продажи (запись в key-value хранилище отсутствует по заданному ключу: sale_uuid):

Key-value:


put( 'sale_uuid_0', { 867, 120.34, 'pending' } )

CH:


( 1, 'sale_uuid_0', 867, 120.34, 'pending' )

  1. Обновление продажи (запись в key-value хранилище присутствует по заданному ключу: sale_uuid):
    old_state = get( 'sale_uuid_0' )
    new_state = { 867, 120.34, 'approved' }

Key-value:


put( 'sale_uuid_0', { old_state.manager_id, old_state.price, 'approved' } )

CH:


(-1, 'sale_uuid_0', 867, 120.34, 'pending' )
( 1, 'sale_uuid_0', 867, 120.34, 'approved' )

На текущем этапе CH будет хранить 3 записи:


( 1, 'sale_uuid_0', 867, 120.34, 'pending' )
(-1, 'sale_uuid_0', 867, 120.34, 'pending' )
( 1, 'sale_uuid_0', 867, 120.34, 'approved' )

, которые потенциально могут быть схлопнуты по ключу в одну запись:


( 1, 'sale_uuid_0', 867, 120.34, 'approved' )

Аналогичным образом могут 'изменяться' любые поля для заданного ключа.


  1. Удаление продажи (запись в key-value хранилище присутствует по заданному ключу: sale_uuid):

old_state = get( 'sale_uuid_0' )
new_state — не задан


Key-value:


remove( 'sale_uuid_0' )


CH:


(-1, 'sale_uuid_0', old_state.manager_id, old_state.price, old_state.status )

На текущем этапе CH будет хранить 4 записи (если схлопывание движком еще не произошло):


( 1, 'sale_uuid_0', 867, 120.34, 'pending' )
(-1, 'sale_uuid_0', 867, 120.34, 'pending' )

( 1, 'sale_uuid_0', 867, 120.34, 'approved' )
(-1, 'sale_uuid_0', 867, 120.34, 'approved' )

Либо CH будет хранить 2 записи:


( 1, 'sale_uuid_0', 867, 120.34, 'approved' )
(-1, 'sale_uuid_0', 867, 120.34, 'approved' )

Во всех случаях (рано или поздно) записи будут схлопнуты и запись о продаже будет 'удалена'.

Спасибо большое, читаю документацию — буду на днях пробовать. Тем более есть еще похожий двиг ReplaceMergeTree который может лучше подойти.

Пока не попробовал, немного смущают эти копии записей… получается они при агрегациях будут сильно искажать данные?! В гугл группе clickhouse еще советуют решать подобную проблему архитектурно (так чтобы апдейты делать добавлением новых записей), но что-то пока ума не приложу как версионирование может тут помочь, чтобы опять же — не учитывать в агрегациях записей-дублей.
Блин, супер! Спасибо :))) Есть ли есть планы перевести на английский — буду всячески благодарен, есть куча коллег, кто будет ну очень заинтересован! :)

Кто то добрый взялся и перевел -> спасибо!

Данные хранятся на 6 серверах SX131 от Hetzner с 3 шардами по 2 реплики.

Вроде же это конфиги без SSD, или SSD все же есть?
Насколько в них есть необходимость, можно ли обойтись обычными крутящимися дисками?

CH позиционируется для очень больших хранилищ данных, для работы на HDD. Пока мы храним несколько десятков ТБ. В этом его сверх "крутость CH" что на блинах такая скорость работы )

Ну что ж, посмотрим какой она будет в случае SSD :)
А как писали парсер языка запросов Clickhouse для GUI?
Вижу что там используется ACE, в каком формате ему подсовывали грамматику?

Взял за основу pgsql + mssql "шаблоны" в AceJs и собрал под CH
Последняя Dev версия если интересно тут

Пробую сейчас ваш wrapper, но не выходит. Подскажите?
[ClickHouseDB\Exception\DatabaseException]
Access denied to database linear for user a05fa82a-02cd-4c50-b9e3
IN:SHOW TABLES FORMAT JSON (291)
/home/bitrix/www/local/php_interface/vendor/smi2/phpclickhouse/src/Statement.php:171
#0: ClickHouseDB\Statement->error()
/home/bitrix/www/local/php_interface/vendor/smi2/phpclickhouse/src/Statement.php:211
#1: ClickHouseDB\Statement->check()
/home/bitrix/www/local/php_interface/vendor/smi2/phpclickhouse/src/Statement.php:228
#2: ClickHouseDB\Statement->init()
/home/bitrix/www/local/php_interface/vendor/smi2/phpclickhouse/src/Statement.php:475
#3: ClickHouseDB\Statement->rowsAsTree(string)
/home/bitrix/www/local/php_interface/vendor/smi2/phpclickhouse/src/Client.php:485
#4: ClickHouseDB\Client->showTables()
/home/bitrix/www/local/partners/index.php:21

<?
require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");
require_once ( $_SERVER['DOCUMENT_ROOT'] . '/local/php_interface/vendor/autoload.php');
$APPLICATION->SetTitle("Система управления и мониторинга");
?>

<?
$config = [
	'host' => '185.X.X.X',
    'port' => '8123',
    'username' => 'a05fa82a-02cd-4c50-b9e3',
    'password' => 'ZZ{&g['
];
$db = new ClickHouseDB\Client($config);
$db->database('linear');
$db->setTimeout(1.5);      // 1500 ms
$db->setTimeout(10);       // 10 seconds
$db->setConnectTimeOut(5); // 5 seconds
print_r($db->showTables());
?>
<?require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");?>
Это я прочитал. На другой стороне — доступ «открыт» (со слов админа). Поэтому я и спросил, т.к. в т.ч. вылетели другие ошибки.
Sign up to leave a comment.