Пробуем Chaos Mesh, или Гармония хаоса и есть порядок

В продолжение наших статей про Chaos Engineering расскажу про недавний опыт проверки на прочность приложения в кластере Kubernetes c помощью оператора Chaos Mesh.

2eb65319cdc2e39377eb7c6243b46c90.png

В рамках подготовки к выходу в 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 выглядит так:

3a24972b4aa39b16f73882e13bf422d3.png

Сразу предлагается быстрый и понятный туториал, поэтому разбирать все элементы 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:

d70f0e2bd97d3c88317766c4835ba9c1.png

Интересно, что у дочерних блоков эксперимента можно выбирать тип. То есть ничто не мешает сделать серию задач, каждая из которых будет выполнять несколько параллельных (или последовательных) экспериментов. В данном случае ограничимся типом Single и создадим подзадачу для перезапуска каждого worker-узла в кластере. Выбираем KubernetesAWS FaultRestart 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. Там же можно подписаться на технические материалы от наших инженеров, которыми легко делиться с ИТ-коллегами со всего мира.

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

© Habrahabr.ru