Русский
Русский
English
Статистика
Реклама

Аварии как опыт 2. Как развалить Elasticsearch при переносе внутри Kubernetes

В нашей внутренней production-инфраструктуре есть не слишком критичный участок, на котором периодически обкатываются различные технические решения, в том числе и различные версии Rook для stateful-приложений. На момент проведения описываемых работ эта часть инфраструктуры работала на основе Kubernetes-кластера версии 1.15, и возникла потребность в его обновлении.

За заказ persistent volumes в кластере отвечал Rook версии 0.9. Мало того, что этот оператор сам по себе был старой версии, его Helm-релиз содержал ресурсы с deprecated-версиями API, что препятствовало обновлению кластера. Решив не возиться с обновлением Rook вживую, мы стали полностью разбирать его.

Внимание! Это история провала: не повторяйте описанные ниже действия в production, не прочитав внимательно до конца.

Итак, вынос данных в хранилища StorageClassов, не управляемых Rookом, шел уже несколько часов успешно


Беспростойная миграция данных Elasticsearch

когда дело дошло до развернутого в Kubernetes кластера Elasticsearch из 3-х узлов:

~ $ kubectl -n kibana-production get po | grep elasticsearchelasticsearch-0                               1/1     Running     0         77d2helasticsearch-1                               1/1     Running     0         77d2helasticsearch-2                               1/1     Running     0         77d2h

Для него было принято решение осуществить переезд на новые PV без простоя. Конфиг в ConfigMap был проверен и сюрпризов не ожидалось. Хотя в алгоритме действий по миграции и присутствует пара опасных поворотов, чреватых аварией при выпадении узлов Kubernetes-кластера, эти узлы работают стабильно да и вообще: Я сто раз так делал, так что поехали!

1. Вносим изменения в StatefulSet в Helm-чарте для Elasticsearch (es-data-statefulset.yaml):

apiVersion: apps/v1kind: StatefulSetmetadata:  labels:    component: {{ template "fullname" . }}    role: data  name: {{ template "fullname" . }}spec:  serviceName: {{ template "fullname" . }}-data volumeClaimTemplates:  - metadata:      name: data      annotations:        volume.beta.kubernetes.io/storage-class: "high-speed"

В последней строчке (с определением storage class) было ранее указано значение rbd вместо нынешнего high-speed.

2. Удаляем существующий StatefulSet с cascade=false. Это опасный поворот, потому что наличие podов с ES больше не контролируется StatefulSetом и в случае внезапного отказа какого-либо K8s-узла, на котором запущен pod с ES, этот pod не возродится автоматически. Однако операция некаскадного удаления StatefulSet и его редеплоя с новыми параметрами занимает секунды, поэтому риски относительны (т.е. зависят от конкретного окружения, конечно).

Приступим:

 $ kubectl -n kibana-production delete sts elasticsearch --cascade=falsestatefulset.apps "elasticsearch" deleted

3. Деплоим заново наш Elasticsearch, а затем масштабируем StatefulSet до 6 реплик:

~ $ kubectl -n kibana-production scale sts elasticsearch --replicas=6statefulset.apps/elasticsearch scaled

и смотрим на результат:

~ $ kubectl -n kibana-production get po | grep elasticsearchelasticsearch-0                               1/1     Running     0         77d2helasticsearch-1                               1/1     Running     0         77d2helasticsearch-2                               1/1     Running     0         77d2helasticsearch-3                               1/1     Running     0         11melasticsearch-4                               1/1     Running     0         10melasticsearch-5                               1/1     Running     0         10m~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodes10.244.33.142  8 98 49 7.89 4.86 3.45 dim - elasticsearch-410.244.33.118 26 98 35 7.89 4.86 3.45 dim - elasticsearch-210.244.33.140  8 98 60 7.89 4.86 3.45 dim - elasticsearch-310.244.21.71   8 93 58 8.53 6.25 4.39 dim - elasticsearch-510.244.33.120 23 98 33 7.89 4.86 3.45 dim - elasticsearch-010.244.33.119  8 98 34 7.89 4.86 3.45 dim * elasticsearch-1

Картина с хранилищем данных:

~ $ kubectl -n kibana-production get get pvc | grep elasticsearchNAME                   STATUS        VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS    AGEdata-elasticsearch-0   Bound   pvc-a830fb81-...   12Gi       RWO            rbd             77ddata-elasticsearch-1   Bound   pvc-02de4333-...   12Gi       RWO            rbd             77ddata-elasticsearch-2   Bound   pvc-6ed66ff0-...   12Gi       RWO            rbd             77ddata-elasticsearch-3   Bound   pvc-74f3b9b8-...   12Gi       RWO            high-speed      12mdata-elasticsearch-4   Bound   pvc-16cfd735-...   12Gi       RWO            high-speed      12mdata-elasticsearch-5   Bound   pvc-0fb9dbd4-...   12Gi       RWO            high-speed      12m

Отлично!

4. Добавим бодрости переносу данных.

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

~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -H "Content-Type: application/json" -X PUT -sk https://localhost:9200/my-index-pattern-*/_settings -d '{"number_of_replicas": 0}'{"acknowledged":true}

но мы, конечно, так делать не будем:

~ $ ^C~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -H "Content-Type: application/json" -X PUT -sk https://localhost:9200/my-index-pattern-*/_settings -d '{"number_of_replicas": 2}'{"acknowledged":true}

Иначе утрата одного podа приведет к неконсистентности данных до его восстановления, а утрата хотя бы одного PV в случае ошибки приведет к потере данных.

Увеличим лимиты перебалансировки:

[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -XPUT -H 'Content-Type: application/json' -sk https://localhost:9200/_cluster/settings?pretty -d '{>   "transient" :{>     "cluster.routing.allocation.cluster_concurrent_rebalance" : 20,>     "cluster.routing.allocation.node_concurrent_recoveries" : 20,>     "cluster.routing.allocation.node_concurrent_incoming_recoveries" : 10,>     "cluster.routing.allocation.node_concurrent_outgoing_recoveries" : 10,>     "indices.recovery.max_bytes_per_sec" : "200mb">   }> }'{  "acknowledged" : true,  "persistent" : { },  "transient" : {    "cluster" : {      "routing" : {        "allocation" : {          "node_concurrent_incoming_recoveries" : "10",          "cluster_concurrent_rebalance" : "20",          "node_concurrent_recoveries" : "20",          "node_concurrent_outgoing_recoveries" : "10"        }      }    },    "indices" : {      "recovery" : {        "max_bytes_per_sec" : "200mb"      }    }  }}

5. Выгоним шарды с первых трех старых узлов ES:

[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -XPUT -H 'Content-Type: application/json' -sk https://localhost:9200/_cluster/settings?pretty -d '{>   "transient" :{>       "cluster.routing.allocation.exclude._ip" : "10.244.33.120,10.244.33.119,10.244.33.118">    }> }'{  "acknowledged" : true,  "persistent" : { },  "transient" : {    "cluster" : {      "routing" : {        "allocation" : {          "exclude" : {            "_ip" : "10.244.33.120,10.244.33.119,10.244.33.118"          }        }      }    }  }}

Вскоре получим первые 3 узла без данных:

[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/shards | grep 'elasticsearch-[0..2]' | wc -l0

6. Пришла пора по одной убить старые узлы Elasticsearch.

Готовим вручную три PersistentVolumeClaim такого вида:

~ $ cat pvc2.yaml---apiVersion: v1kind: PersistentVolumeClaimmetadata:  name: data-elasticsearch-2spec:  accessModes: [ "ReadWriteOnce" ]  resources:    requests:      storage: 12Gi  storageClassName: "high-speed"

Удаляем по очереди PVC и pod у реплик 0, 1 и 2, друг за другом. При этом создаем PVC вручную и убеждаемся, что экземпляр ES в новом podе, порожденном StatefulSetом, успешно запрыгнул в кластер ES:

~ $ kubectl -n kibana-production delete pvc data-elasticsearch-2 persistentvolumeclaim "data-elasticsearch-2" deleted^C~ $ kubectl -n kibana-production delete po elasticsearch-2pod "elasticsearch-2" deleted~ $ kubectl -n kibana-production apply -f pvc2.yamlpersistentvolumeclaim/data-elasticsearch-2 created~ $ kubectl -n kibana-production get po | grep elasticsearchelasticsearch-0                               1/1     Running     0         77d3helasticsearch-1                               1/1     Running     0         77d3helasticsearch-2                               1/1     Running     0         67selasticsearch-3                               1/1     Running     0         42melasticsearch-4                               1/1     Running     0         41melasticsearch-5                               1/1     Running     0         41m~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodes10.244.21.71  21 97 38 3.61 4.11 3.47 dim - elasticsearch-510.244.33.120 17 98 99 8.11 9.26 9.52 dim - elasticsearch-010.244.33.140 20 97 38 3.61 4.11 3.47 dim - elasticsearch-310.244.33.119 12 97 38 3.61 4.11 3.47 dim * elasticsearch-110.244.34.142 20 97 38 3.61 4.11 3.47 dim - elasticsearch-410.244.33.89  17 97 38 3.61 4.11 3.47 dim - elasticsearch-2

Наконец, добираемся до ES-узла 0: удаляем pod elasticsearch-0, после чего он успешно запускается с новым storageClass, заказывает себе PV. Результат:

~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodes10.244.33.151 17 98 99 8.11 9.26 9.52 dim * elasticsearch-0

При этом в соседнем podе:

~ $ kubectl -n kibana-production exec -ti elasticsearch-1 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodes10.244.21.71  16 97 27 2.59 2.76 2.57 dim - elasticsearch-510.244.33.140 20 97 38 2.59 2.76 2.57 dim - elasticsearch-310.244.33.35  12 97 38 2.59 2.76 2.57 dim - elasticsearch-110.244.34.142 20 97 38 2.59 2.76 2.57 dim - elasticsearch-410.244.33.89  17 97 98 7.20 7.53 7.51 dim * elasticsearch-2

Поздравляю: мы получили split-brain в production! И сейчас новые данные случайным образом сыпятся в два разных кластера ES!

Простой и потеря данных

В предыдущем разделе, закончившемся несколько секунд назад, мы резко перешли от плановых работ к восстановительным. И в первую очередь остро стоит вопрос срочного предотвращения поступления данных в пустой недокластер ES, состоящий из одного узла.

Может, скинуть label у podа elasticsearch-0, чтобы исключить его из балансировки на уровне Service? Но ведь, исключив pod, мы не сможем его затолкать обратно в кластер ES, потому что при формировании кластера обнаружение членов кластера происходит через тот же Service!

За это отвечает переменная окружения:

        env:        - name: DISCOVERY_SERVICE          value: elasticsearch

и ее использование в ConfigMapе elasticsearch.yaml (см. документацию):

discovery:      zen:        ping.unicast.hosts: ${DISCOVERY_SERVICE}

В общем, по такому пути мы не пойдем. Вместо этого лучше срочно остановить workers, которые пишут данные в ES в реальном времени. Для этого отмасштабируем все три нужных deploymentа в 0. (К слову, хорошо, что приложение придерживается микросервисной архитектуры и не надо останавливать весь сервис целиком.)

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

Причина аварии и восстановление

В чем же дело? Почему узел 0 не присоединился к кластеру? Еще раз проверяем конфигурационные файлы: с ними все в порядке.

Проверяем внимательно еще раз Helm-чарты вот же оно! Итак, проблемный es-data-statefulset.yaml:

apiVersion: apps/v1kind: StatefulSetmetadata:  labels:    component: {{ template "fullname" . }}    role: data  name: {{ template "fullname" . }}     containers:      - name: elasticsearch        env:        {{- range $key, $value :=  .Values.data.env }}        - name: {{ $key }}          value: {{ $value | quote }}        {{- end }}        - name: cluster.initial_master_nodes     # !!!!!!          value: "{{ template "fullname" . }}-0" # !!!!!!        - name: CLUSTER_NAME          value: myesdb        - name: NODE_NAME          valueFrom:            fieldRef:              fieldPath: metadata.name        - name: DISCOVERY_SERVICE          value: elasticsearch        - name: KUBERNETES_NAMESPACE          valueFrom:            fieldRef:              fieldPath: metadata.namespace        - name: ES_JAVA_OPTS          value: "-Xms{{ .Values.data.heapMemory }} -Xmx{{ .Values.data.heapMemory }} -Xlog:disable -Xlog:all=warning:stderr:utctime,level,tags -Xlog:gc=debug:stderr:utctime -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.host=127.0.0.1 -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.port=9099 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"...

Зачем же так определены initial_master_nodes?! Здесь задано (см. документацию) жесткое ограничение, что при первичном старте кластера в выборах мастера участвует только 0-й узел. Так и произошло: pod elasticsearch-0 поднялся с пустым PV, начался процесс бутстрапа кластера, а мастер в podе elasticsearch-2 был благополучно проигнорирован.

Ок, добавим в ConfigMap на живую:

~ $ kubectl -n kibana-production edit cm elasticsearchapiVersion: v1data:  elasticsearch.yml: |-    cluster:      name: ${CLUSTER_NAME}      initial_master_nodes:        - elasticsearch-0        - elasticsearch-1        - elasticsearch-2...

и удалим эту переменную окружения из StatefulSet:

~ $ kubectl -n kibana-production edit sts elasticsearch...      - env:        - name: cluster.initial_master_nodes          value: "elasticsearch-0"...

StatefulSet начинает перекатывать все podы по очереди согласно стратегии RollingUpdate, и делает это, разумеется, с конца, т.е. от 5-го podа к 0-му:

~ $ kubectl -n kibana-production get poNAME              READY   STATUS        RESTARTS   AGEelasticsearch-0   1/1     Running       0          11melasticsearch-1   1/1     Running       0          13melasticsearch-2   1/1     Running       0          15melasticsearch-3   1/1     Running       0          67melasticsearch-4   1/1     Running       0          67melasticsearch-5   0/1     Terminating   0          67m

Что произойдет, когда перекат дойдет до конца? Как отработает бутстрап кластера? Ведь перекат StatefulSet идет быстро как пройдут выборы в таких условиях, если даже в документации заявляется, что auto-bootstrapping is inherently unsafe? Не получим ли мы кластер, забустрапленный из 0-го узла с огрызком индекса?Примерно из-за таких мыслей спокойно наблюдать за происходящим у меня ну никак не получалось.

Забегая вперёд: нет, в заданных условиях всё будет хорошо. Однако 100% уверенности в тот момент не было. А ведь это production, данных много, они критичны для бизнеса, а это чревато дополнительной возней с бэкапами.

Поэтому, пока перекат не докатился до 0-го podа, сохраним и убьем сервис, отвечающий за discovery:

~ $ kubectl -n kibana-production get svc elasticsearch -o yaml > elasticsearch.yaml~ $ kubectl -n kibana-production delete svc elasticsearch service "elasticsearch" deleted

и оторвем PVC у 0-го podа:

~ $ kubectl -n kibana-production delete pvc data-elasticsearch-0 persistentvolumeclaim "data-elasticsearch-0" deleted^C

Теперь, когда перекат прошел, elasticsearch-0 в состоянии Pending из-за отсутствия PVC, а кластер полностью развален, т.к. узлы ES потеряли друг друга:

~ $ kubectl -n kibana-production exec -ti elasticsearch-1 bash[root@elasticsearch-1 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodesOpen Distro Security not initialized.

На всякий случай исправим ConfigMap вот так:

~ $ kubectl -n kibana-production edit cm elasticsearchapiVersion: v1data:  elasticsearch.yml: |-    cluster:      name: ${CLUSTER_NAME}      initial_master_nodes:        - elasticsearch-3        - elasticsearch-4        - elasticsearch-5...

После этого создадим новый пустой PV для elasticsearch-0, создав PVC:

 $ kubectl -n kibana-production apply -f pvc0.yamlpersistentvolumeclaim/data-elasticsearch-0 created

И перезапустим узлы для применения изменений в ConfigMap:

~ $ kubectl -n kibana-production delete po elasticsearch-0 elasticsearch-1 elasticsearch-2 elasticsearch-3 elasticsearch-4 elasticsearch-5pod "elasticsearch-0" deletedpod "elasticsearch-1" deletedpod "elasticsearch-2" deletedpod "elasticsearch-3" deletedpod "elasticsearch-4" deletedpod "elasticsearch-5" deleted

Можно возвращать на место сервис из недавно сохраненного нами YAML-манифеста:

~ $ kubectl -n kibana-production apply -f elasticsearch.yaml service/elasticsearch created

Посмотрим, что получилось:

~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodes10.244.98.100  11 98 32 4.95 3.32 2.87 dim - elasticsearch-010.244.101.157 12 97 26 3.15 3.00 2.10 dim - elasticsearch-310.244.107.179 10 97 38 1.66 2.46 2.52 dim * elasticsearch-110.244.107.180  6 97 38 1.66 2.46 2.52 dim - elasticsearch-210.244.100.94   9 92 36 2.23 2.03 1.94 dim - elasticsearch-510.244.97.25    8 98 42 4.46 4.92 3.79 dim - elasticsearch-4[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/indices | grep -v green | wc -l0

Ура! Выборы прошли нормально, кластер собрался полностью, индексы на месте.

Осталось только:

  1. Снова вернуть в ConfigMap значения initial_master_nodes для elasticsearch-0..2;

  2. Еще раз перезапустить все podы;

  3. Аналогично шагу, описанному в начале статьи, выгнать все шарды на узлы 0..2 и отмасштабировать кластер с 6 до 3-х узлов;

  4. Наконец, сделанные вручную изменения донести до репозитория.

Заключение

Какие уроки можно извлечь из данного случая?

Работая с переносом данных в production, всегда следует иметь в виду, что что-то может пойти не так: будет допущена ошибка в конфигурации приложения или сервиса, произойдет внезапная авария в ЦОД, начнутся сетевые проблемы да все что угодно! Соответственно, перед началом работ должны быть предприняты меры, которые позволят либо предотвратить аварию, либо максимально купировать ее последствия. Обязательно должен быть подготовлен План Б.

Использованный нами алгоритм действий был неустойчив к внезапным проблемам. Перед выполнением этих работ в более важном окружении было бы необходимо:

  1. Выполнить переезд в тестовом окружении с production-конфигурацией ES.

  2. Запланировать простой сервиса. Либо временно переключить нагрузку на другой кластер. (Предпочтительный путь зависит от требований к доступности.) В случае варианта с простоем следовало предварительно остановить workers, пишущие данные в Elasticsearch, снять затем свежую резервную копию, а после этого приступить к работам по переносу данных в новое хранилище.

P.S.

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

Источник: habr.com
К списку статей
Опубликовано: 28.01.2021 10:04:30
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании флант

Системное администрирование

Devops

Kubernetes

Elasticsearch

Failure stories

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru