Как и большинство NoSQL-решений, C* подвержена одной крайне неприятной эпидемии: она является отличным инструментом для узкого класса задач, но позиционируется евангелистами как очередная серебряная пуля по хранению данных. В этой статье я расскажу о своём опыте внедрения C* в (сравнительно) нагруженный проект веб-аналитики. Она будет полезна всем, кто стоит перед выбором масштабируемого хранилища данных, и развенчает мифы и заблуждения об этом инструменте.



Для начала о позитивных моментах. Как я и сказал раньше, с основными своими задачами C* справляется на Ура. Она была создана для быстрой записи, масштабируемости и отказоустойчивости, и в этом она, наверное, лучшая: я ещё не встречал более простого управления кластером, и за всё это время она меня не подвела ни разу. Однако, самая большая проблема C* в том, что её область применения гораздо уже, чем может показаться из документации. И далее я по пунктам расскажу почему.

CQL — это не SQL


Честно говоря, я вообще не понимаю, зачем разработчики C* решили создать CQL. Он запутывает и дезориентирует, создаёт ложные впечатления о специфике работы C*. Вот вам несколько фактов:

  1. Главное заблуждение, в которое вводит CQL всех новичков, — это иллюзии о том, что вы сможете делать какие-то выборки. Это не так. С* — это key-value хранилище. Вы не сможете получить подмножество строк в таблице. Либо одну (по ключу), либо все. Для обхода этого ограничения в C* есть «wide rows» — возможность писать в строку любые колонки (независимость от схемы, до 2 млрд уникальных колонок в строке). Но и это спасает только при особом подходе к планированию модели данных.
  2. CQL вводит понятия partition key и cluster key внутри PRIMARY KEY. Ещё одно крупное заблуждение в принципе работы этой БД. На самом деле строка в C* определяется только через partition-часть. Все «записи», отличающиеся по cluster key, будут просто укладываться друг за другом внутри одной и той же строки.
  3. Нет никакого compound partition key. Проще всего понять поведение compound key — это представить, что значения полей ключа конкатенируются перед сохранением. И строку можно получить только по полному значению ключа (как в redis, к примеру).
  4. INSERT и UPDATE в C* — это одно и то же. Отныне и во веки веков.
  5. Коллекции в CQL — это лишь синтаксический сахар, и записи в них хранятся в отдельных колонках.
  6. Вторичные индексы — тоже лишь синтаксический сахар. C* создаёт новое семейство ключей (таблицу) для каждого вторичного индекса и дублирует туда записи.


Судя по всему, CQL был создан, чтобы популяризовать эту БД среди новичков, пытаясь скрыть самые важные фундаментальные понятия в работе этой БД.

Особенности проектирования данных


Главный принцип при проектировании данных в C* — «SELECT-driven model». Вы проектируете данные так, чтобы вы их смогли потом получить с помощью SELECT. В рамках key-value wide-row хранилища это подразумевает очень сильную денормализацию. Вы не просто денормализуете данные, как вы раньше привыкли это делать в реляционных БД, вы фактически создаёте отдельную таблицу под каждый запрос. И во многих проектах (там, где очень много данных по объёму) это даёт либо огромный overhead во время map/reduce и агрегации, либо огромный overhead по объёму хранимых данных.

И да, вам сразу стоит быть готовым к тому, что без распределённого агрегирования (Hadoop, Spark, Hive и т.д.) эта БД бесполезна. Разработчики обещают в следующей версии операторы для агрегации данных в CQL, но можете особо не надеяться на них. По архитектуре этой БД ясно, что они будут работать только внутри одной строки.

Каунтеры


Отвожу этому типу данных в этой БД свой собственный раздел, и вот почему: изначально, когда я начал внедрять C* в свой проект, я очень обрадовался: для веб-аналитики очень круто иметь атомарные каунтеры, они очень сильно упрощают систему. Но потом я понял простую истину: никогда, слышите? НИКОГДА не используйте каунтеры в C* (по крайней мере в версии до 2.1.* включительно). Дело в том, что этот тип данных противоречит всей идеологии этой БД, и из-за этого у него огромное количество проблем. Если вы спросите у любого специалиста по C* про каунтеры, он в ответ начнёт лишь злобно хихикать.

Короче:
  1. Вы не можете класть в строку с каунтерами никаких других значений, кроме каунтеров
  2. Вы не можете класть каунтеры в коллекции (по причине выше)
  3. Вы не можете сделать каунтер cluster и partition key (по причине выше), сортировать по ним и ставить на них вторичные индексы, само собой, тоже
  4. Вы не можете установить каунтеру Time To Live
  5. Каунтеры имеют проблемы с репликацией (редко, но метко)
  6. Если вы удалите каунтер, то вы ещё долго не сможете создать его заново


Суть расхождения идеологии каунтеров и самой БД в том, что все операции в C* — идемпотентны (т.е. повторное применении операции не изменяет результата). Именно это даёт безотказную архитектуру при падении узлов, датацентров, проблемах со связью и т.п. Каунтеры же нарушают этот принцип, и внутри сделаны через «light transactions» — сначала считать значение, потом увеличить его. И это вызывает много проблем. В общем, если вам нужны счётчики, лучше воспользуйтесь Redis-прослойкой, а в С* уже складывайте окончательное значение.

На этом пока всё. Выбирайте инструменты с умом. Если что-то кажется слишком хорошим, чтобы быть правдой, то возможно, так оно и есть.

Комментарии (19)


  1. andrewnester
    22.05.2015 21:07
    +3

    Спасибо за статью, жаль ее не было, когда я так в ней нуждался :)

    Как раз недавно делал аналитику и БД была Cassandra
    Вкратце — задача была хранить логи действий пользователей в системе и потом на основе этих данных выводить статистику

    Так вот
    1) действительно столкнулся с проблемой каунтеров (но без них тяжеловато было бы вести подсчет)

    2) данные действительно очень денормализуются, вместо 1 коассической реляционной таблицы получилось 12 «кассандровских»
    Правда потом уменьшилось до 4, так как часть работы по аггрегации данных вынеслась в логику приложения

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


    1. grossws
      22.05.2015 23:05

      Ещё кассандра хороша, как движок для хорошего параллельного hash-merge join'а =)


    1. Jabher
      23.05.2015 01:02
      +2

      аэээээмм, elasticsearch?


  1. slayerhabr
    23.05.2015 02:11

    Подтверждаю, со многим столкнулся. И в моем случае postgresql показал лучшие результаты по скорости выборки. И вместо mysql+C* просто перешел на postgres.


    1. GearHead Автор
      23.05.2015 02:36
      +4

      Ну тут вы уж загнули. если задачу можно решить с помощью реляционных БД, то её нужно решить с помощью реляционных БД.
      Я вообще не вижу смысла в инструментах типа C*, если ваш набор данных может обработать одна машина.


      1. slayerhabr
        23.05.2015 02:55

        Дело в том, что задача — накопление данных скажем по датчикам, чтото типа timeseries (что как раз хорошо ложится в идею К). Можно решить реляционными БД? Конечно, но хотелось использовать чтото заточенное под накопление данных. После сравнительных тестов мускль, К (4 ноды в кластере) и простой постгрес. Победил постгрес по всем показателям.


        1. grossws
          23.05.2015 03:29

          Для накопления данных неплохи всякие решения типа rrd, influx, carbon/whisper, если data flow понятен на этапе проектирования


          1. slayerhabr
            23.05.2015 03:53

            Да, но rrd/carbon не подходил — выборки не только по time индексу, в К держал двойную-тройную копию данных под разные запросы. influxdb на тот момент был еще слишком молод.


          1. slayerhabr
            23.05.2015 03:58

            influxdb не подходит по тем же причинам (кастомные индексы насколько я знаю должны появится в 0.9).
            pg справился хорошо — одна большая таблица и несколько индексов.


        1. GearHead Автор
          23.05.2015 04:56
          +2

          так ведь о том и речь. C* не заточена под абстрактное «накопление данных», она заточена под задачи, в которых вам 100% не хватит одной машины (с PG, не с PG, не важно). просто если рано или поздно PG начнёт захлёбываться, и вам придётся выписывать кульбиты с шардингом и репликациями (и множеством сопутствующих проблем), C* позволит линейно и безотказно расширяться с помощью лишь одной команды в консоли.


          1. slayerhabr
            23.05.2015 14:00

            В ближайшие пару-тройку лет сможем расширятся вертиально. Память и диск сейчас достаточно дешевые. Потом будем думать.


  1. 2ANikulin
    23.05.2015 09:35

    Если речь заходит об аналитике на больших данных, map-reduce и всё такое, то надо смотреть в сторону HBase. Это хранилище спроектированно именно для этих целей. Кассандра больше подходит для географически распределенных систем, с повышенной доступностью.


  1. AlexeyShurygin
    23.05.2015 16:30

    Вопрос по п.5.
    Почему update и insert — это одно и то же? Afaik в C* это разные вещи.


    1. andrewnester
      23.05.2015 16:33

      Отличается только тем, что Insert не работает с каунтерами, в остальном — одно и тоже


    1. andrewnester
      23.05.2015 16:35

      И да — по факту и Insert, и Update это Upsert :)


      1. AlexeyShurygin
        23.05.2015 16:50

        Так верно, да.


  1. AlexeyShurygin
    23.05.2015 16:53

    По поводу для чего придумали CQL.
    Подход мне тоже не очень нравится — там внутри неочевидная магия делается, которую чтобы понять надо знать как физически хранятся данные.
    Однако программировать через альтернативный Thrift API — это застрелиться, достаточно тяжело, т.ч. SQL-like язык нужен и полезен.


    1. grossws
      23.05.2015 17:36

      Был ещё кроме голого thrift'а довольно удобный hector


  1. lukianenko
    24.05.2015 18:51

    прекрасная статья, большое спасибо!