Аварии как опыт #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, всё почти мгновенно завелось и поехало — включая запись в основной кластер.
В результате всех манипуляций никакие данные не были потеряны, все валидные задания на репликацию отработали как положено и работа кластера восстановилась в полном объеме.
Выводы
Какие уроки можно извлечь из данной ситуации?
Предварительно и в процессе внедрения решения, к которому вы хотите прийти в итоге, обсуждайте его с коллегами. Убедитесь, что все явно понимают ключевые моменты и возможные нюансы.
«Семь раз отмерь — один раз отрежь». При проектировании решения продумывайте сценарии, когда что-то может пойти не так (и почему). Стоит допускать любые варианты, включая самые, казалось бы, невероятные. Полезно быть дотошным и внимательным, хотя и помнить, что абсолютно всё учесть вряд ли получится.
Когда вы готовите какую-либо инфраструктуру для разработки, давайте коллегам её полное и максимально детальное описание, чтобы не случилось казуса. Это ещё важнее в контексте наиболее «смежных» областей — как в нашем случае, когда настройка, производимая на стороне Dev (часть схемы таблицы), напрямую влияла на работу инфраструктуры, настраиваемой на стороне Ops. Такие области надо заранее выявлять и уделять им особое внимание.
Старайтесь исключить любой hardcode, а также любые пересечения наименования, особенно когда это касается идентичных логических элементов инфраструктуры: пути в ZooKeeper, именование баз/таблиц в соседних кластерах БД, адреса внешних сервисов в конфигах… — всё должно поддаваться конфигурации. Конечно, в рамках разумного.
А помимо организационных моментов хотелось бы (в очередной раз) отметить выдающуюся стойкость ClickHouse к различным вариантам поломок, а важнее — высокую надёжность хранения данных, а этой БД. В случае с CH, соблюдая базовые рекомендации и принципы конфигурации кластера, как бы вы ни старались (если это не злой умысел, конечно), свои данные не потеряете.
P.S.
Читайте также в нашем блоге: