В нашей внутренней production-инфраструктуре есть не слишком критичный участок, на котором периодически обкатываются различные технические решения, в том числе и различные версии Rook для stateful-приложений. На момент проведения описываемых работ эта часть инфраструктуры работала на основе Kubernetes-кластера версии 1.15, и возникла потребность в его обновлении. За заказ persistent volumes в кластере отвечал Rook версии 0.9. Мало того, что этот оператор сам по себе был старой версии, его Helm-релиз содержал ресурсы с deprecated-версиями API, что препятствовало обновлению кластера. Решив не возиться с обновлением Rook «вживую», мы стали полностью разбирать его. Внимание! Это история провала: не повторяйте описанные ниже действия в production, не прочитав внимательно до конца. Итак, вынос данных в хранилища StorageClass’ов, не управляемых Rook’ом, шел уже несколько часов успешно… … когда дело дошло до развернутого в Kubernetes кластера Elasticsearch из 3-х узлов: Для него было принято решение осуществить переезд на новые PV без простоя. Конфиг в ConfigMap был проверен и сюрпризов не ожидалось. Хотя в алгоритме действий по миграции и присутствует пара опасных поворотов, чреватых аварией при выпадении узлов Kubernetes-кластера, эти узлы работают стабильно… да и вообще: «Я сто раз так делал», — так что поехали! 1. Вносим изменения в StatefulSet в Helm-чарте для Elasticsearch (es-data-statefulset.yaml): В последней строчке (с определением storage class) было ранее указано значение rbd вместо нынешнего high-speed. 2. Удаляем существующий StatefulSet с cascade=false. Это опасный поворот, потому что наличие pod’ов с ES больше не контролируется StatefulSet’ом и в случае внезапного отказа какого-либо K8s-узла, на котором запущен pod с ES, этот pod не «возродится» автоматически. Однако операция некаскадного удаления StatefulSet и его редеплоя с новыми параметрами занимает секунды, поэтому риски относительны (т.е. зависят от конкретного окружения, конечно).  Приступим: 3. Деплоим заново наш Elasticsearch, а затем масштабируем StatefulSet до 6 реплик: … и смотрим на результат: Картина с хранилищем данных: Отлично! 4. Добавим бодрости переносу данных. Если в вас еще жив дух авантюризма и неудержимо влечет к приключениям (т.е. данные в окружении не столь критичны), можно ускорить процесс, оставив одну реплику индексов: … но мы, конечно, так делать не будем: Иначе утрата одного pod’а приведет к неконсистентности данных до его восстановления, а утрата хотя бы одного PV в случае ошибки приведет к потере данных. Увеличим лимиты перебалансировки: 5. Выгоним шарды с первых трех старых узлов ES: Вскоре получим первые 3 узла без данных: 6. Пришла пора по одной убить старые узлы Elasticsearch. Готовим вручную три PersistentVolumeClaim такого вида: Удаляем по очереди PVC и pod у реплик 0, 1 и 2, друг за другом. При этом создаем PVC вручную и убеждаемся, что экземпляр ES в новом pod’е, порожденном StatefulSet’ом, успешно «запрыгнул» в кластер ES: Наконец, добираемся до ES-узла №0: удаляем pod elasticsearch-0, после чего он успешно запускается с новым storageClass, заказывает себе PV. Результат: При этом в соседнем pod’е: Поздравляю: мы получили split-brain в production! И сейчас новые данные случайным образом сыпятся в два разных кластера ES! В предыдущем разделе, закончившемся несколько секунд назад, мы резко перешли от плановых работ к восстановительным. И в первую очередь остро стоит вопрос срочного предотвращения поступления данных в пустой «недокластер» ES, состоящий из одного узла. Может, скинуть label у pod’а elasticsearch-0, чтобы исключить его из балансировки на уровне Service? Но ведь, исключив pod, мы не сможем его «затолкать» обратно в кластер ES, потому что при формировании кластера обнаружение членов кластера происходит через тот же Service!  За это отвечает переменная окружения: … и ее использование в ConfigMap’е elasticsearch.yaml (см. документацию): В общем, по такому пути мы не пойдем. Вместо этого лучше срочно остановить workers, которые пишут данные в ES в реальном времени. Для этого отмасштабируем все три нужных deployment’а в 0. (К слову, хорошо, что приложение придерживается микросервисной архитектуры и не надо останавливать весь сервис целиком.) Итак, простой посреди дня, пожалуй, всё же лучше, чем нарастающая потеря данных. А теперь разберемся в причинах произошедшего и добьемся нужного нам результата. В чем же дело? Почему узел №0 не присоединился к кластеру? Еще раз проверяем конфигурационные файлы: с ними все в порядке. Проверяем внимательно еще раз Helm-чарты… вот же оно! Итак, проблемный es-data-statefulset.yaml: Зачем же так определены initial_master_nodes?! Здесь задано (см. документацию) жесткое ограничение, что при первичном старте кластера в выборах мастера участвует только 0-й узел. Так и произошло: pod elasticsearch-0 поднялся с пустым PV, начался процесс бутстрапа кластера, а мастер в pod’е elasticsearch-2 был благополучно проигнорирован.  Ок, добавим в ConfigMap «на живую»:  … и удалим эту переменную окружения из StatefulSet: StatefulSet начинает перекатывать все pod’ы по очереди согласно стратегии RollingUpdate, и делает это, разумеется, с конца, т.е. от 5-го pod’а к 0-му: Что произойдет, когда перекат дойдет до конца? Как отработает бутстрап кластера? Ведь перекат StatefulSet идет быстро… как пройдут выборы в таких условиях, если даже в документации заявляется, что «auto-bootstrapping is inherently unsafe»? Не получим ли мы кластер, забустрапленный из 0-го узла с «огрызком» индекса?Примерно из-за таких мыслей спокойно наблюдать за происходящим у меня ну никак не получалось. Забегая вперёд: нет, в заданных условиях всё будет хорошо. Однако 100% уверенности в тот момент не было. А представьте, что это production, где много данных, которые критичны для бизнеса (= это чревато дополнительной возней с бэкапами)… Поэтому, пока перекат не докатился до 0-го pod’а, сохраним и убьем сервис, отвечающий за discovery: … и «оторвем» PVC у 0-го pod’а: Теперь, когда перекат прошел, elasticsearch-0 в состоянии Pending из-за отсутствия PVC, а кластер полностью развален, т.к. узлы ES потеряли друг друга: На всякий случай исправим ConfigMap вот так: После этого создадим новый пустой PV для elasticsearch-0, создав PVC: И перезапустим узлы для применения изменений в ConfigMap: Можно возвращать на место сервис из недавно сохраненного нами YAML-манифеста: Посмотрим, что получилось: Ура! Выборы прошли нормально, кластер собрался полностью, индексы на месте. Осталось только: Снова вернуть в ConfigMap значения initial_master_nodes для elasticsearch-0..2; Еще раз перезапустить все pod’ы; Аналогично шагу, описанному в начале статьи, выгнать все шарды на узлы 0..2 и отмасштабировать кластер с 6 до 3-х узлов; Наконец, сделанные вручную изменения донести до репозитория. Какие уроки можно извлечь из данного случая? Работая с переносом данных в production, всегда следует иметь в виду, что что-то может пойти не так: будет допущена ошибка в конфигурации приложения или сервиса, произойдет внезапная авария в ЦОД, начнутся сетевые проблемы… да все что угодно! Соответственно, перед началом работ должны быть предприняты меры, которые позволят либо предотвратить аварию, либо максимально купировать ее последствия. Обязательно должен быть подготовлен «План Б». Использованный нами алгоритм действий был неустойчив к внезапным проблемам. Перед выполнением этих работ в более важном окружении было бы необходимо: Выполнить переезд в тестовом окружении с production-конфигурацией ES. Запланировать простой сервиса. Либо временно переключить нагрузку на другой кластер. (Предпочтительный путь зависит от требований к доступности.) В случае варианта с простоем следовало предварительно остановить workers, пишущие данные в Elasticsearch, снять затем свежую резервную копию, а после этого приступить к работам по переносу данных в новое хранилище. Читайте также в нашем блоге: «Аварии как опыт #1. Как сломать два кластера ClickHouse, не уточнив один нюанс»; «Как мы Elasticsearch в порядок приводили: разделение данных, очистка, бэкапы»; «elasticsearch-extractor — утилита для извлечения индексов из снапшотов Elasticsearch».  IT engineer