Про некоторые свои failure stories мы уже писали и раньше, но теперь мне выпала честь формально открыть специальный цикл из таких статей. Ведь аварии, их причины и последствия — это тоже часть нашей жизни, и исследовать эту «тёмную сторону» не менее интересно, чем всё остальное. Тем более, что они всё больше влияют даже на повседневный быт, так что из любой аварии можно и нужно извлекать уроки. Да и читатели не раз просили нас рассказывать о таком почаще — давайте попробуем! Первая история — о том, как плоха и к каким последствиям может привести недостаточная коммуникация. Мы, конечно, высоко ценим и поддерживаем культуру открытого, качественного и (при необходимости) максимально плотного взаимодействия. Однако и на старуху бывает проруха. Произошедшее здесь — отличная иллюстрация того, как проблема скорее организационного характера находит слабое место в технических особенностях и выливается в неожиданный сбой.  Перейдем к технической стороне… ClickHouse-кластер состоял из четырех серверов на больших HDD-дисках. В нем было 2 шарда, по 2 реплики в каждом. А также несколько таблиц (в рамках одной БД), включая две основные: одна — для хранения raw-данных, вторая — для обработанных. Кластер использовался для хранения аналитики различных данных для бизнеса, и с этими задачами справлялся отлично. Но иногда разработке требуется совершать значительно большие выборки со сложными агрегациями и получать быстрые результаты. Кластер не соответствовал этим требованиям: был слишком медленным, не справлялся с большой нагрузкой и не успевал вовремя обработать вновь поступающие данные. Тогда решили развернуть второй кластер ClickHouse, который стал бы «быстрым буфером»: с идентичным набором данных, но за сильно меньший период. Логику записи данных и работы с ними целиком взяла на себя разработка. С нашей стороны потребовалось только развертывание кластера. Решили его делать на двух серверах на быстрых (NVMe) дисках, два шарда по реплике в каждой. Избыточность и защищенность от потери данных не требовались, потому что те же данные были в основном кластере, а простой кластера (в случае потери реплик) не пугает: восстановление можно проводить в плановом режиме. Получив железо и приступив к настройке кластера, мы столкнулись с дилеммой, как быть с кластером ZooKeeper. Формально он был, конечно, не нужен, т.к. реплик в нем пока не ожидалось. Но ведь лучше сделать конфигурацию идентичную текущей, не так ли? Разработка не будет думать, как на каком кластере создавать таблицы, а у нас останется возможность быстро изменить топологию кластера: добавить шарды или сделать несколько реплик. Что же делать с ZooKeeper? В рамках имеющейся инфраструктуры ресурсов для дополнительного кластера ZK не было, а городить второй кластер на двух новых серверах прямо по соседству с ClickHouse, конечно, не хотелось (смешивать роли VM — антипаттерн). Тогда мы решили прикрутить новый кластер к уже существующему кластеру ZK — благо, это не должно вызывать проблем (и отнюдь не накладно в смысле производительности).  Так и сделали: запустили новую инсталляцию CH, использующую текущий ZK, и передали доступы разработке.  Вот схема, которая в итоге получилась: И ничто не предвещало беды… Прошло несколько дней. Был прекрасный солнечный выходной и моё дежурство. Ближе к вечеру поступает запрос по поводу кластеров ClickHouse: в одну из таблиц основного CH, куда складываются обработанные данные, перестала выполняться запись. Со смутным ощущением тревоги я начинаю изучать данные мониторинга Linux-систем, используемых в кластерах ClickHouse. На первый взгляд всё замечательно: ни повышенной нагрузки на репликах, ни других явных проблем в подсистемах серверов.  Однако, изучая уже логи и системные таблицы CH, замечаю нехорошие ошибки. Они выглядели примерно так: Заглядываю в replication_queue, а там — сотни строк с ошибкой про невозможность провести репликацию на сервер нового кластера. При этом в error-логе основного кластера есть следующие записи: И такое тоже есть: В этот момент начинаю догадываться, что же произошло. Чтобы подтвердить свои подозрения, смотрю на SHOW CREATE TABLE для проблемной таблицы на обоих кластерах. Да, это он — одинаковый ZKPATH! Теперь набор коротких фактов для полной картины: 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 «залипла». Тогда я попытался выполнить: … и получил ошибку node not empty, что довольно странно. Ведь эта команда должна удалять рекурсивно znode с «детьми». В документации ClickHouse есть информация про системные команды для DROP’а ZK-ключей, касающихся определенных реплик. На основном кластере не удалось их использовать, поскольку там версия ClickHouse была 19, а команды появились только с 20-й. Однако в новом кластере CH был нужной версии, чем я и воспользовался, выполнив: … и это сработало! Теперь был чистый (в смысле, чистый от лишних реплик) ZK-кластер, однако основной CH-кластер по-прежнему не работал. Почему? Потому что replication_queue все еще был забит мусорными записями. Оставалось разобраться с очередью репликации, после чего (по идее) всё должно запуститься. Как известно, сама по себе очередь репликации хранится также в ключах ZK-кластера. Вот здесь: Чтобы понимать, какие ключи можно и нужно удалять, я воспользовался таблицей replication_queue в самом CH. При выполнении SELECT там можно увидеть поле nodename, которое и является тем самым именем ключа по вышеуказанному пути. Далее — дело техники. Следующей командой собираются имена ключей, касающихся именно проблемных реплик: А после этого на ZK: Тут надо проявить внимательность и выполнять удаление ключей именно по тому пути шарда, для которого выполнялся SELECT. После того, как для каждой реплики каждого шарда были собраны «плохие» задачи в очереди репликации и удалены из ZK, всё почти мгновенно завелось и поехало — включая запись в основной кластер. В результате всех манипуляций никакие данные не были потеряны, все валидные задания на репликацию отработали как положено и работа кластера восстановилась в полном объеме. Какие уроки можно извлечь из данной ситуации? Предварительно и в процессе внедрения решения, к которому вы хотите прийти в итоге, обсуждайте его с коллегами. Убедитесь, что все явно понимают ключевые моменты и возможные нюансы. «Семь раз отмерь — один раз отрежь». При проектировании решения продумывайте сценарии, когда что-то может пойти не так (и почему). Стоит допускать любые варианты, включая самые, казалось бы, невероятные. Полезно быть дотошным и внимательным, хотя и помнить, что абсолютно всё учесть вряд ли получится. Когда вы готовите какую-либо инфраструктуру для разработки, давайте коллегам её полное и максимально детальное описание, чтобы не случилось казуса. Это ещё важнее в контексте наиболее «смежных» областей — как в нашем случае, когда настройка, производимая на стороне Dev (часть схемы таблицы), напрямую влияла на работу инфраструктуры, настраиваемой на стороне Ops. Такие области надо заранее выявлять и уделять им особое внимание. Старайтесь исключить любой hardcode, а также любые пересечения наименования, особенно когда это касается идентичных логических элементов инфраструктуры: пути в ZooKeeper, именование баз/таблиц в соседних кластерах БД, адреса внешних сервисов в конфигах… — всё должно поддаваться конфигурации. Конечно, в рамках разумного. А помимо организационных моментов хотелось бы (в очередной раз) отметить выдающуюся стойкость ClickHouse к различным вариантам поломок, а важнее — высокую надёжность хранения данных а этой БД. В случае с CH, соблюдая базовые рекомендации и принципы конфигурации кластера, как бы вы ни старались (если это не злой умысел, конечно), свои данные не потеряете. Читайте также в нашем блоге: «Практические истории из наших SRE-будней. Часть 2» (см. историю №2 про ClickHouse); «Одна история с оператором Redis в K8s и мини-обзор утилит для анализа данных этой БД»  Head of tech department