company_banner

Аварии как опыт #1. Как сломать два кластера ClickHouse, не уточнив один нюанс

    Про некоторые свои failure stories мы уже писали и раньше, но теперь мне выпала честь формально открыть специальный цикл из таких статей. Ведь аварии, их причины и последствия — это тоже часть нашей жизни, и исследовать эту «тёмную сторону» не менее интересно, чем всё остальное. Тем более, что они всё больше влияют даже на повседневный быт, так что из любой аварии можно и нужно извлекать уроки. Да и читатели не раз просили нас рассказывать о таком почаще — давайте попробуем!

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

    Перейдем к технической стороне…


    Изначальная задача

    ClickHouse-кластер состоял из четырех серверов на больших HDD-дисках. В нем было 2 шарда, по 2 реплики в каждом. А также несколько таблиц (в рамках одной БД), включая две основные: одна — для хранения raw-данных, вторая — для обработанных.

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

    Тогда решили развернуть второй кластер ClickHouse, который стал бы «быстрым буфером»: с идентичным набором данных, но за сильно меньший период. Логику записи данных и работы с ними целиком взяла на себя разработка. С нашей стороны потребовалось только развертывание кластера. Решили его делать на двух серверах на быстрых (NVMe) дисках, два шарда по реплике в каждой. Избыточность и защищенность от потери данных не требовались, потому что те же данные были в основном кластере, а простой кластера (в случае потери реплик) не пугает: восстановление можно проводить в плановом режиме.

    Реализация

    Получив железо и приступив к настройке кластера, мы столкнулись с дилеммой, как быть с кластером ZooKeeper. Формально он был, конечно, не нужен, т.к. реплик в нем пока не ожидалось. Но ведь лучше сделать конфигурацию идентичную текущей, не так ли? Разработка не будет думать, как на каком кластере создавать таблицы, а у нас останется возможность быстро изменить топологию кластера: добавить шарды или сделать несколько реплик.

    Что же делать с ZooKeeper? В рамках имеющейся инфраструктуры ресурсов для дополнительного кластера ZK не было, а городить второй кластер на двух новых серверах прямо по соседству с ClickHouse, конечно, не хотелось (смешивать роли VM — антипаттерн). Тогда мы решили прикрутить новый кластер к уже существующему кластеру ZK — благо, это не должно вызывать проблем (и отнюдь не накладно в смысле производительности). 

    Так и сделали: запустили новую инсталляцию CH, использующую текущий ZK, и передали доступы разработке. 

    Вот схема, которая в итоге получилась:

    И ничто не предвещало беды…

    Авария

    Прошло несколько дней. Был прекрасный солнечный выходной и моё дежурство. Ближе к вечеру поступает запрос по поводу кластеров ClickHouse: в одну из таблиц основного CH, куда складываются обработанные данные, перестала выполняться запись.

    Со смутным ощущением тревоги я начинаю изучать данные мониторинга Linux-систем, используемых в кластерах ClickHouse. На первый взгляд всё замечательно: ни повышенной нагрузки на репликах, ни других явных проблем в подсистемах серверов. 

    Однако, изучая уже логи и системные таблицы CH, замечаю нехорошие ошибки. Они выглядели примерно так:

    2020.10.24 18:50:28.105250 [ 75 ] {} <Error> enriched_distributed.Distributed.DirectoryMonitor: Code: 252, e.displayText() = DB::Exception: Received from 192.168.1.4:9000. DB::Exception: Too many parts (300). Merges are processing significantly slower than inserts.. Stack trace:

    Заглядываю в replication_queue, а там — сотни строк с ошибкой про невозможность провести репликацию на сервер нового кластера. При этом в error-логе основного кластера есть следующие записи:

    /var/log/clickhouse-server/clickhouse-server.err.log.0.gz:2020.10.24 16:18:33.233639 [ 16 ] {} <Error> DB.TABLENAME: DB::StorageReplicatedMergeTree::queueTask()::<lambda(DB::StorageReplicatedMergeTree::LogEntryPtr&)>: Poco::Exception. Code: 1000, e.code() = 0, e.displayText() = DNS error: Temporary DNS error while resolving: clickhouse2-0 (version 19.15.2.2 (official build)

    И такое тоже есть:

    2020.10.24 18:38:51.192075 [ 53 ] {} <Error> search_analyzer_distributed.Distributed.DirectoryMonitor: Code: 210, e.displayText() = DB::NetException: 
    Connection refused (192.168.1.3:9000), Stack trace:
    2020.10.24 18:38:57.871637 [ 58 ] {} <Error> raw_distributed.Distributed.DirectoryMonitor: Code: 210, e.displayText() = DB::NetException: Connection refused (192.168.1.3:9000), Stack trace:

    В этот момент начинаю догадываться, что же произошло. Чтобы подтвердить свои подозрения, смотрю на SHOW CREATE TABLE для проблемной таблицы на обоих кластерах. Да, это он — одинаковый ZKPATH!

    ENGINE = ReplicatedMergeTree('/clickhouse/tables/DBNAME/{shard}/TABLENAME', '{replica}')

    Теперь набор коротких фактов для полной картины:

    • ZK-кластер используется один и тот же;

    • номера шардов — идентичные в обоих CH-кластерах;

    • имена реплицирующихся таблиц — тоже идентичные;

    • ZKPATH указан одинаковый, без разделения на разные кластеры.

    Следовательно, по мнению ClickHouse, это были реплики одной и той же таблицы. А раз так, они должны реплицировать друг друга.

    Все верно: мы забыли сообщить разработке, что используем один и тот же ZK для обоих кластеров, а они не предусмотрели указание корректных ZKPATH для таблиц в новом кластере.

    Дополнительную «смуту» на начальных этапах диагностики также внес фактор отложенной поломки. Сам кластер мы «отдали» еще в середине недели, однако разработчики создали таблицы и начали в них писать только через несколько дней. Поэтому и проблема проявилась позже, чем можно было ожидать.

    Решение

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

    После того, как я осознал происходящее, начал думать о том, что нужно:

    • аккуратно отделить эти кластеры/реплики друг от друга;

    • не потерять данные, которые не смогли реплицироваться (застряли в replication_queue);

    • не «размотать» окончательно основной кластер.

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

    Первый очевидный шаг — DROP для проблемной таблицы на каждой реплике нового кластера, что (согласно документации) должно привести к двум вещам:

    • удалению самой таблицы с данными на той реплике, где выполняется DROP (я это специально перепроверил, т.к. опасался, что зацепит основной кластер);

    • удалению собственно метаданных из кластера ZK, касающихся именно этой реплики, что мне и было нужно.

    У нас два шарда, в каждом по одной реплике, с названиями clickhouse2-0 и clickhouse2-1 (далее будут фигурировать как сервер 2-0 и сервер 2-1 соответственно).

    Выполнение DROP TABLE на сервере 2-0 сработало как положено, а вот на 2-1 что-то пошло не так: часть дерева ключей с данными об этой реплике в ZK «залипла».

    Тогда я попытался выполнить:

    rmr /clickhouse/tables/DBNAME/2/TABLENAME/replicas/clickhouse2-1

    … и получил ошибку node not empty, что довольно странно. Ведь эта команда должна удалять рекурсивно znode с «детьми».

    В документации ClickHouse есть информация про системные команды для DROP’а ZK-ключей, касающихся определенных реплик. На основном кластере не удалось их использовать, поскольку там версия ClickHouse была 19, а команды появились только с 20-й. Однако в новом кластере CH был нужной версии, чем я и воспользовался, выполнив:

    SYSTEM DROP REPLICA 'replica_name' FROM ZKPATH '/path/to/table/in/zk';

    … и это сработало!

    Теперь был чистый (в смысле, чистый от лишних реплик) ZK-кластер, однако основной CH-кластер по-прежнему не работал. Почему? Потому что replication_queue все еще был забит мусорными записями. Оставалось разобраться с очередью репликации, после чего (по идее) всё должно запуститься.

    Как известно, сама по себе очередь репликации хранится также в ключах ZK-кластера. Вот здесь:

    /clickhouse/tables/DBNAME/1/TABLENAME/replicas/clickhouse-0/queue/

    Чтобы понимать, какие ключи можно и нужно удалять, я воспользовался таблицей replication_queue в самом CH. При выполнении SELECT там можно увидеть поле nodename, которое и является тем самым именем ключа по вышеуказанному пути.

    Далее — дело техники. Следующей командой собираются имена ключей, касающихся именно проблемных реплик:

    clickhouse-client -h 127.0.0.1 --pass PASS -q "select node_name from system.replication_queue where source_replica='clickhouse2-1'" > bad_queueid

    А после этого на ZK:

    for id in $(cat bad_queueid); do /usr/share/zookeeper/bin/zkCli.sh rmr /clickhouse/tables/DBNAME/2/TABLENAME/replicas/clickhouse-1/queue/$id; sleep 2; done

    Тут надо проявить внимательность и выполнять удаление ключей именно по тому пути шарда, для которого выполнялся SELECT.

    После того, как для каждой реплики каждого шарда были собраны «плохие» задачи в очереди репликации и удалены из ZK, всё почти мгновенно завелось и поехало — включая запись в основной кластер.

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

    Выводы

    Какие уроки можно извлечь из данной ситуации?

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

    2. «Семь раз отмерь — один раз отрежь». При проектировании решения продумывайте сценарии, когда что-то может пойти не так (и почему). Стоит допускать любые варианты, включая самые, казалось бы, невероятные. Полезно быть дотошным и внимательным, хотя и помнить, что абсолютно всё учесть вряд ли получится.

    3. Когда вы готовите какую-либо инфраструктуру для разработки, давайте коллегам её полное и максимально детальное описание, чтобы не случилось казуса. Это ещё важнее в контексте наиболее «смежных» областей — как в нашем случае, когда настройка, производимая на стороне Dev (часть схемы таблицы), напрямую влияла на работу инфраструктуры, настраиваемой на стороне Ops. Такие области надо заранее выявлять и уделять им особое внимание.

    4. Старайтесь исключить любой hardcode, а также любые пересечения наименования, особенно когда это касается идентичных логических элементов инфраструктуры: пути в ZooKeeper, именование баз/таблиц в соседних кластерах БД, адреса внешних сервисов в конфигах… — всё должно поддаваться конфигурации. Конечно, в рамках разумного.

    А помимо организационных моментов хотелось бы (в очередной раз) отметить выдающуюся стойкость ClickHouse к различным вариантам поломок, а важнее — высокую надёжность хранения данных а этой БД. В случае с CH, соблюдая базовые рекомендации и принципы конфигурации кластера, как бы вы ни старались (если это не злой умысел, конечно), свои данные не потеряете.

    P.S.

    Читайте также в нашем блоге:

    Флант
    DevOps-as-a-Service, Kubernetes, обслуживание 24×7

    Комментарии 5

      +6

      Вы не первые, кто наступил в историю с общим ЗК на несколько кластеров КХ. И причём, чтобы КХ шли под одними ключами ) Спасибо за информацию, что так делать не надо ))) запомним ещё раз ))))


      выдающуюся стойкость ClickHouse к различным вариантам поломок

      Я бы не переоценивал это качество ) Коллеги умудрились таки получить фарш из данных )))

        +3

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


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


        Этот вариант не подходит по двум причинам:


        • добавление новой реплики при временной недоступности старой — полностью легальный сценарий;
        • это не защитит от ошибки, когда реплики всё-таки доступны по сети, но новая реплика добавляется по ошибке.

        Второй вариант — сделать новую форму запросов CREATE REPLICA, и сделать так, что CREATE TABLE будет только соглашаться создавать новую таблицу, а не новую реплику существующей таблицы. Но это поломает обратную совместимость. Или сделать форму запроса CREATE TABLE… FIRST REPLICA, которая будет проверять то же самое. Вроде уже лучше. Но непонятно, насколько внесение изменений в язык запросов соотносится с тем, будут ли это широко использовать.


        Третий вариант — это выводить логи в клиент при создании таблицы. В логах уже написано — первая ли это реплика или новая реплика, которая будет клонировать существующую. И эти логи можно получить, выставив send_logs_level. Остаётся только включить вывод каких-то вариантов логов на клиент по-умолчанию и рассчитывать на то, что их прочитают.


        Четвёртый вариант — при создании новой реплики, возвращать результат CREATE TABLE только после того, как новая реплика синхронизировалась. Этот вариант тоже отменяется, так как синхронизация часто бывает долгой (дни). Делать для этой опции отдельную настройку тоже нецелесообразно, так как её мало кто будет использовать.

          +2

          Спасибо за статью. Для обучения, одна история неудачи стоит десяти историй "успеха"!


          «Семь раз отмерь — один раз отрежь». При проектировании решения продумывайте сценарии, когда что-то может пойти не так (и почему). Стоит допускать любые варианты, включая самые, казалось бы, невероятные.

          По-английски это называется "Failure Mode and Effects Analysis", например, вот очень хороший пост на тему.


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

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

          Обычно такие выводы остаются исключительно на бумаге (в секции "будущие шаги" пост-мортема, прямо как у вас), прямо как новогодние обещания. Полезно быть реалистом на этот счет :)

            +1
            Для обучения, одна история неудачи стоит десяти историй «успеха»!

            Это верно! :)

            По-английски это называется «Failure Mode and Effects Analysis», например, вот очень хороший пост на тему.

            Спасибо, изучим!

            Обычно такие выводы остаются исключительно на бумаге (в секции «будущие шаги» пост-мортема, прямо как у вас), прямо как новогодние обещания. Полезно быть реалистом на этот счет :)

            Ну, тут два момента, первый — мы стараемся все-таки избегать вот этих вот бумажных формализмов и если и пишем про «будущие шаги», то стараемся их делать выполнимыми и реальными и второе — лично я реалист, но с верой в лучшее, как минимум стремление к выполнению этого шага уже даёт существенный позитивный рывок ;)
            +2

            Чтобы использовать один зк на нескольких кластерах безопасно, можно задать разные параметры root в конфигурации zookeeper https://clickhouse.tech/docs/en/operations/server-configuration-parameters/settings/#server-settings_zookeeper


            Тогда кластеры даже не будут иметь возможности залезть в одну и ту же "поддиректорию"

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

            Самое читаемое