Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
В продолжение наших статей про Chaos Engineering расскажу про недавний опыт проверки на прочность приложения в кластере Kubernetes c помощью оператора Chaos Mesh.
В рамках подготовки к выходу в production возникла потребность протестировать следующие сценарии в staging-окружении:
отказ узлов, на которых работают микросервисы;
отказ инфраструктурных зависимостей (StatefulSet'ы баз данных, менеджеров очередей и т.д.);
сетевые проблемы.
Как вы, возможно, помните из этой статьи, Open Source-решение Chaos Mesh состоит из двух компонентов с названиями, которые говорят за себя: Chaos Operator и Chaos Dashboard. Начнем с оператора, а точнее — с той новой магии разрушения, что в него «завезли» со времен предыдущего обзора.
Что нового?
Вот новые эксперименты, которые теперь можно запускать в кластере Kubernetes:
JVM Application Faults. Фактически, это новый модуль byteman, с помощью которого можно полноценно тестировать приложения, работающие на виртуальной машине Java. Сценарии позволяют генерировать исключения, явно и излишне вызывать сборщик мусора, подменять возвращаемые методами значения и творить другие ужасы. Есть требование — ядро Linux v4.1 или новее.
Simulate AWS/GCP Faults. Всё просто и наверняка очевидно — Chaos Mesh получает доступ в AWS или GCP и может вызывать в этих сценариях остановку или рестарт конкретных EC2/GCP-инстансов, а также безжалостно отрывать у них диски!
HTTP faults. Позволяет имитировать разные ошибки в работе HTTP-сервера: обрывать соединения, внедрять задержки в работу, заменять или дополнять содержимое HTTP-запросов или ответов, а также подменять коды ответов. Можно указать конкретный путь, запросы по которому будут модифицироваться, или не мелочиться и пойти ва-банк с шаблоном
*
, модифицируя вообще все.
И, наконец, с помощью Chaos Mesh теперь можно тестировать не только ресурсы кластера Kubernetes, но и нагружать сами физические или виртуальные машины. Для этого, помимо установки самого оператора, на каждый целевой хост нужно доустановить сервер Chaosd. После этого получится проводить практически те же эксперименты, управляя ими через оператор в кластере Kubernetes.
Установка
Chaos Mesh можно установить Bash-скриптом, который предлагается в документации, но я настоятельно рекомендую использовать Helm-чарт.
Сперва создаем пространство имен:
kubectl create namespace chaos-testing
Затем устанавливаем оператор. Тут важно не ошибиться с исполняемой средой контейнеров, иначе оператор не сможет внедрять эксперименты.
Для containerd:
helm install chaos-mesh chaos-mesh/chaos-mesh -n=chaos-testing --set chaosDaemon.runtime=containerd --set chaosDaemon.socketPath=/run/containerd/containerd.sock
В результате получается следующий набор ресурсов в пространстве имен chaos-testing
:
$ kubectl -n chaos-testing get pods
NAME READY STATUS RESTARTS AGE
chaos-controller-manager-5f657fc99c-2k8v8 1/1 Running 0 7d7h
chaos-controller-manager-5f657fc99c-xr9sq 1/1 Running 0 7d7h
chaos-controller-manager-5f657fc99c-zjg7c 1/1 Running 0 7d7h
chaos-daemon-56n5r 1/1 Running 0 7d7h
chaos-daemon-9467d 1/1 Running 0 7d7h
chaos-daemon-hjcgb 1/1 Running 0 7d7h
chaos-daemon-hxnjt 1/1 Running 0 7d7h
chaos-dashboard-7c95b6b99b-7ckr7 1/1 Running 0 7d7h
Здесь сразу нужно обратить внимание на количество Pod’ов chaos-daemon
. Оператор требует наличия демона на всех узлах, имеющих отношение к тестируемому пространству имен. Например, это может быть группа StatefulSet-узлов со своими taint’ами и т.п. Лучше сразу об этом подумать и наделить DaemonSet-оператора соответствующим иммунитетом.
Все дороги не ведут в Chaos Dashboard
Вы можете получить доступ к Chaos Dashboard, перенаправив порт соответствующей службы:
kubectl port-forward -n chaos-testing svc/chaos-dashboard 2333:2333
Другой вариант – настроить Ingress, если по какой-либо причине вам необходимо получить доступ к панели мониторинга из внешней сети. Но, по очевидным причинам, делать это не слишком разумно.
Вы должны, по крайней мере, использовать базовый механизм аутентификации Ingress, чтобы защитить Dashboard от доступа извне.
Приветственный экран Dashboard выглядит так:
Сразу предлагается быстрый и понятный туториал, поэтому разбирать все элементы Chaos Dashboard в подробностях не имеет особого смысла.
Самое главное удобство Chaos Dashboard — это возможность накликать эксперимент вручную и получить сгенерированный YAML-манифест для последующего запуска. Хотя всё же рекомендуется предварительно проверять полученный результат. Chaos Dashboard далеко не идеален и не имеет защиты от новичка: в некоторых сценариях бывают ошибки. Например, при описании NetworkChaos можно выбрать «direction both» и не указать «target» — Dashboard смиренно сгенерирует манифест, но работать он не будет.
Запущенные эксперименты можно вживую наблюдать в одноименной вкладке Experiments и быстро отменять их, если что-то пойдет совсем не так, как планировалось (конечно, если chaos daemon присутствует на каждом задействованном узле).
Chaos Mesh позволяет определять композитные сценарии во вкладке Workflow, которые как конструктор собираются из любых Chaos Mesh-экспериментов. Workflow может быть одиночным (по сути, это обычный эксперимент), последовательным (serial) или параллельным.
Кроме того, можно создать workflow типа Task, в котором помимо выбранных экспериментов запускается дополнительный контейнер на базе указанного образа для выполнения необходимой команды. Ещё есть HTTP Request workflow – его название говорит само за себя.
В отдельной вкладке Schedules определяются повторяемые по cron-расписанию эксперименты.
Поначалу описывать вручную комплексные workflow-эксперименты может быть достаточно сложно и чревато ошибками, поэтому Dashboard отлично подходит для знакомства с оператором.
Но на этом, к сожалению, всё. Мне не понравилось отсутствие возможности отложенного запуска. Не по расписанию, а именно отложенного. То есть, настроив весь workflow, можно только нажать на Submit, отправив эксперименты в сразу в работу. Конечно, ничто не мешает собрать сгенерированные эксперименты в отдельный репозиторий и запускать их с помощью CI/CD, но, согласитесь, легковесное хранилище для манифестов оператору не помешало бы. Кроме того, почему бы вообще не складывать их в Pod’ы контроллера? Ведь намного удобнее сохранять эксперименты в том же инструменте, где они описываются и мониторятся, чтобы запустить позже.
Поваренная книга начинающего хаос-мага
Первой задачей стоит анализ случая падения рабочих узлов.
У подопытного dev-кластера на момент испытаний было 4 узла, без особых affinity и tolerations — StatefulSet’ы живут там, куда их определил планировщик. Поэтому было решено перезагрузить каждый узел по очереди и посмотреть, к чему это приведет — весело же!
Воспользуемся сценарием AWS Fault.
Для начала нужно создать секрет с AWS-ключами:
apiVersion: v1
kind: Secret
metadata:
name: сrucible
namespace: chaos-testing
type: Opaque
stringData:
aws_access_key_id: ZG9vbWd1eQo=
aws_secret_access_key: bmV2ZXIgc2FpZCBoZWxsbwo=
В Dashboard’е создаем новый workflow типа serial. Указываем количество дочерних экспериментов в поле Number:
Интересно, что у дочерних блоков эксперимента можно выбирать тип. То есть ничто не мешает сделать серию задач, каждая из которых будет выполнять несколько параллельных (или последовательных) экспериментов. В данном случае ограничимся типом Single и создадим подзадачу для перезапуска каждого worker-узла в кластере. Выбираем Kubernetes -> AWS Fault -> Restart EC2. В последней форме нужно указать имя заранее созданного Secret’а с ключами, регион, в котором расположены машины, и ID инстанса.
После описания всех подпроцессов жмем на Submit у Serial-задачи. Не бойтесь, это не приведет к непосредственному запуску экспериментов, но будет сгенерирован YAML-файл. У нас получился такой:
kind: Workflow
apiVersion: chaos-mesh.org/v1alpha1
metadata:
namespace: staging
name: serial-ec2-restarts
spec:
entry: entry
templates:
- name: entry
templateType: Serial
deadline: 20m
children:
- serial-ec2-restarts
- name: restart-i-<1st_node_id>
templateType: AWSChaos
deadline: 5m
awsChaos:
action: ec2-restart
secretName: crucible
awsRegion: eu-central-1
ec2Instance: i-<1st_node_id>
- name: restart-i-<2nd_node_id>
templateType: AWSChaos
deadline: 5m
awsChaos:
action: ec2-restart
secretName: crucible
awsRegion: eu-central-1
ec2Instance: i-<2nd_node_id>
- name: restart-i-<3d_node_id>
templateType: AWSChaos
deadline: 5m
awsChaos:
action: ec2-restart
secretName: crucible
awsRegion: eu-central-1
ec2Instance: i-<3d_node_id>
- name: restart-i-<4th_node_id>
templateType: AWSChaos
deadline: 5m
awsChaos:
action: ec2-restart
secretName: crucible
awsRegion: eu-central-1
ec2Instance: i-<4th_node_id>
- name: serial-ec2-restarts
templateType: Serial
deadline: 20m
children:
- restart-i-<1st_node_id>
- restart-i-<2nd_node_id>
- restart-i-<3d_node_id>
- restart-i-<4th_node_id>
Поле deadline
здесь служит исключительно для отслеживания дочерних экспериментов. Если вложенные тесты не закончатся за время, указанное в deadline
шаблона workflow, все эксперименты в этом workflow будут остановлены.
Ещё одна странность: это кажется очевидной необходимостью, но в операторе до сих пор нельзя добавить таймауты между экспериментами в workflow. deadline
тут не поможет, так как речь здесь немного о другом. У каких-то отдельных задач есть таймауты, например, у PodChaos
можно подкрутить gracePeriod
перед убийством контейнера, но на этом, кажется, всё.
Довольно о грустном! Давайте рассмотрим получившиеся workflow, имитирующие отказ RabbitMQ, PostgreSQL и сетевые проблемы.
Для начала немного помучаем «кролика»:
apiVersion: chaos-mesh.org/v1alpha1
kind: Workflow
metadata:
name: rmq-kill-{{- uuidv4 }}
namespace: staging
spec:
entry: entry
templates:
- name: entry
templateType: Serial
deadline: 30m
children:
- rabbit-disaster-flow
- name: crashloop-rabbit
templateType: PodChaos
deadline: 4m
podChaos:
selector:
namespaces:
- staging
labelSelectors:
statefulset.kubernetes.io/pod-name: rabbitmq-server-0
mode: all
action: pod-failure
- name: rabbit-kill
templateType: PodChaos
deadline: 20m
podChaos:
selector:
namespaces:
- staging
labelSelectors:
statefulset.kubernetes.io/pod-name: rabbitmq-server-0
mode: all
action: pod-kill
gracePeriod: 5
- name: parallel-stress-on-rabbit
templateType: Parallel
deadline: 6m
children:
- rabbitserver-stress
- rabbitserver-packets-corruption
- name: rabbitserver-stress
templateType: StressChaos
deadline: 6m
stressChaos:
selector:
namespaces:
- staging
labelSelectors:
statefulset.kubernetes.io/pod-name: rabbitmq-server-0
mode: all
stressors:
cpu:
workers: 2
load: 65
memory:
workers: 2
size: 512MB
- name: rabbitserver-packets-corruption
templateType: NetworkChaos
deadline: 6m
networkChaos:
selector:
namespaces:
- staging
labelSelectors:
statefulset.kubernetes.io/pod-name: rabbitmq-server-0
mode: all
action: corrupt
corrupt:
corrupt: '50'
correlation: '50'
- name: rabbit-disaster-flow
templateType: Serial
deadline: 30m
children:
- crashloop-rabbit
- rabbit-kill
- parallel-stress-on-rabbit
Здесь и далее Helm-функция uuidv4
используется для генерации уникальных имен. Если имя детерминировано, запустить workflow повторно без его предварительного удаления не удастся.
В этом последовательном workflow на первом этапе эмулируется crash loop у всех Pod’ов с лейблом rabbitmq-server-0
, далее они убиваются и, наконец, в параллельном режиме работают StressChaos и NetworkChaos. Да, довольно жестоко, но ведь задача в том, чтобы протестировать отказ, так?
Для разнообразия было решено ещё раз «положить» RabbitMQ, но чуточку иначе: вежливо пригласив OOM Killer:
apiVersion: chaos-mesh.org/v1alpha1
kind: Workflow
metadata:
name: rmq-stresss-{{- uuidv4 }}
namespace: staging
spec:
entry: entry
templates:
- name: entry
templateType: Serial
deadline: 10m
children:
- rabbit-disaster-flow
- name: burn-rabbit-burn
templateType: StressChaos
deadline: 5m
stressChaos:
selector:
namespaces:
- staging
labelSelectors:
statefulset.kubernetes.io/pod-name: rabbitmq-server-0
mode: all
stressors:
cpu:
workers: 5
load: 100
memory:
workers: 5
size: 3GB
- name: parallel-stress-on-rabbit
templateType: Parallel
deadline: 5m
children:
- rabbitserver-stress
- rabbitserver-packets-corruption
- name: rabbitserver-stress
templateType: StressChaos
deadline: 5m
stressChaos:
selector:
namespaces:
- staging
labelSelectors:
statefulset.kubernetes.io/pod-name: rabbitmq-server-0
mode: all
stressors:
cpu:
workers: 3
load: 70
memory:
workers: 2
size: 777MB
- name: rabbitserver-packets-corruption
templateType: NetworkChaos
deadline: 5m
networkChaos:
selector:
namespaces:
- staging
labelSelectors:
statefulset.kubernetes.io/pod-name: rabbitmq-server-0
mode: all
action: corrupt
corrupt:
corrupt: '50'
correlation: '50'
- name: rabbit-disaster-flow
templateType: Serial
deadline: 10m
children:
- burn-rabbit-burn
- parallel-stress-on-rabbit
Сценарий для PostgreSQL похож на первый сценарий для RabbitMQ, поэтому опустим его для экономии места.
Интереснее рассмотреть тест сети в определенном пространстве имен.
Воспользуемся следующим workflow:
apiVersion: chaos-mesh.org/v1alpha1
kind: Workflow
metadata:
name: namespace-wide-network-disaster
namespace: staging
spec:
entry: entry
templates:
- name: entry
templateType: Serial
deadline: 13m
children:
- network-nightmare
- name: delay-chaos
templateType: NetworkChaos
deadline: 10m
networkChaos:
selector:
namespaces:
- staging
mode: all
action: delay
delay:
latency: 60ms
jitter: 350ms
correlation: '50'
direction: both
- name: bad-packet-loss
templateType: NetworkChaos
deadline: 10m
networkChaos:
selector:
namespaces:
- staging
mode: all
action: loss
loss:
loss: '30'
correlation: '50'
direction: both
- name: duplicate-packets
templateType: NetworkChaos
deadline: 10m
networkChaos:
selector:
namespaces:
- staging
mode: all
action: duplicate
duplicate:
duplicate: '15'
correlation: '30'
direction: both
- name: corrupt-packets
templateType: NetworkChaos
deadline: 10m
networkChaos:
selector:
namespaces:
- staging
mode: all
action: corrupt
corrupt:
corrupt: '13'
correlation: '25'
direction: both
- name: network-nightmare
templateType: Parallel
deadline: 10m
children:
- delay-chaos
- bad-packet-loss
- duplicate-packets
- corrupt-packets
На протяжении десяти минут для Pod’ов в соответствующем пространстве имен эмулируются серьезные задержки, потери, дупликации и повреждения сетевых пакетов. Тест в очередной раз получился избыточно жестким, и все Pod’ы «посыпались» с проваленными пробами, поэтому параметры самих экспериментов было решено сделать более щадящими. Тут интересно другое. Видите ошибку? Вот и я не вижу, а она есть.
Dashboard позволяет указать только пространство имен, и документация оператора в этом ему соответствует. Единичные эксперименты работают нормально, но при комплексных издевательствах над сетью возникают проблемы. Как это работает? Ну, демоны идут и правят iptables в контейнерах — всё. Только вот в случае, когда конкретный селектор не указан, Chaos Mesh иногда забывает эти изменения за собой подчистить.
Решение простое: проставлять на все контейнеры в пространстве имен какой то лейбл перед запуском теста:
kubectl -n staging label pods --all chaos=target --overwrite
Есть ещё одна неприятность, с которой можно столкнутся. Связана она с пропуском узла, на котором chaos daemon должен присутствовать. Может случиться так, что workflow из за этого не сможет корректно удалиться, и в кластере зависнет много ресурсов Chaos Mesh. В Dashboard они будут бесконечно крутиться в состоянии удаления.
Вылечить это можно добавлением tolerations для DaemonSet’а, чтобы тот разъехался по нужным узлам. Кроме того, в ходе тестов может случиться так, что один или несколько Pod’ов намертво зависнут в crashloop. Если в workflow есть сценарий NetworkChaos, то контроллер оператора будет ждать, пока Pod поднимется, чтобы удостовериться в корректности iptables. Очевидно, Pod можно вытащить из crash loop (а заодно и починить iptables), просто удалив его вручную (kubectl delete pod
). Но если все Pod’ы работают, демоны на местах, а workflow и его дочерние ресурсы зависли и не удаляются... Придется немного по'bash'ировать:
#!/usr/bin/env bash
set +e
export res_list=$(kubectl api-resources | grep chaos-mesh | awk '{print $1}')
for i in $res_list;
do
kubectl -n staging get $i --no-headers=true | awk '{print $1}' | xargs
kubectl -n staging patch $i -p '{"metadata":{"finalizers":null}}' --type=merge
kubectl -n staging get $i --no-headers=true | awk '{print $1}' | xargs
kubectl -n staging delete $i --force=true
done
kubectl -n staging label pods --all chaos-
set -e
Скрипт пробегает по всем манифестам ресурсов Chaos Mesh, убирает finalizer и беспощадно удаляет манифесты.
Итог
В целом, несмотря на некоторые субъективные моменты, Chaos Mesh хорош и оправдал наши ожидания.
Дополнительная нагрузка на приложение в staging-окружении генерировалась нашим клиентом с помощью JMeter. В результате удалось протестировать всё, что хотелось протестировать, и удостовериться в правильности выбранного курса развития инфраструктуры проекта для production-среды.
Спасибо авторам Chaos Mesh, а всем остальным — удачи в экспериментах с хаосом!
P.S.
Эта же статья доступна и на английском языке в блоге Flant. Там же можно подписаться на технические материалы от наших инженеров, которыми легко делиться с ИТ-коллегами со всего мира.
Читайте также в нашем блоге:
«Обзор инструментов для chaos engineering в Kubernetes. Часть 1: kube-monkey, chaoskube, Chaos Mesh»;
«Обзор инструментов для chaos engineering в Kubernetes. Часть 2: Litmus Chaos, Chaos Toolkit, KubeInvaders и другие»;
«Chaos Engineering: искусство умышленного разрушения. Часть 1».