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

425e0afc46a9b375cc1347b87414fc7f.png

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

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

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

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

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

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

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

Реализация

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

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

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

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

3d5cbfc1dca1f994d264ac72d25ab3bf

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

Авария

Прошло несколько дней. Был прекрасный солнечный выходной и моё дежурство. Ближе к вечеру поступает запрос по поводу кластеров 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.

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

© Habrahabr.ru