Перейти к содержимому

Сбои как узлы ​

Чтобы увидеть, как система справляется, когда сеть деградирует или зависимость отключается, поместите сбой в эксперимент. Реле или эмулятор открывается вместе с запуском, шаг переключает его в нужный момент, а отчёт о запуске подсчитывает, что произошло в каждой фазе. Конец запуска — успешный, неудачный или остановленный — закрывает их, так что после него ничего не остаётся ухудшенным.

УзелЧто делает
Сетевые помехиреле между тестируемой системой и её целью, которое ухудшает всё, что через него проходит, на всё время запуска
Сменить помехипереключает реле запуска на другой профиль начиная с этого шага
ЭмуляторAPI, устройство или брокер в исполнении Signal Lab на всё время запуска
Эмулятор: выкл./вкл.отключает эмулятор запуска или возвращает его

Все четыре есть в меню добавления, в группах Сбои и Эмуляция. Их поля описаны в справочнике узлов; само реле — на странице Помехи, эмуляторы — на странице Эмуляторы.

Сетевые помехи ​

Тестируемая система отправляет данные на реле вместо настоящей цели; реле пересылает их цели, несёт ответы обратно и ухудшает оба направления.

ПолеЧто
СлушатьIP:port, на который тестируемая система отправляет данные или подключается; порт не 0
ПересылатьIP:port настоящего адресата или host:port; имя хоста разрешается при старте запуска
ПротоколUDP — у каждой датаграммы своя судьба — или TCP — каждое соединение связывается с собственным соединением к цели
профильПресет или собственные значения

Что реле читает из профиля, зависит от протокола; остальные значения не учитываются:

ПротоколПомехи
UDPзадержка, джиттер, потери пакетов, пачки потерь, дублирование, искажение, перестановка, ограничение полосы, нет связи
TCPзадержка и джиттер (поток сохраняет порядок), ограничение полосы (отправитель замедляется, ничего не теряется), сброс соединений, полуоткрытые соединения, нет связи

Открывается до первого шага. Каждое реле эксперимента открывается при старте запуска, как сокеты ожиданий, поэтому его поля Слушать и Пересылать принимают только текст и параметры (node.params_only) — {{relay}} с параметром relay, но никогда не переменную. Порт, который не удаётся открыть, или имя адресата, которое не находится, останавливает запуск ещё до всякого трафика, на этом узле.

В потоке проходит сразу. Когда запуск доходит до узла, тот сразу проходит, а лента сообщает, что он ухудшает и как. Реле работает от начала запуска до конца, где бы ни стоял узел в графе.

Закрывается вместе с запуском. Как бы ни закончился запуск, реле закрывается; соединения TCP-реле закрываются вместе с ним. Если реле само перестало пересылать, оно хранит причину: шаги, которые им пользуются, не проходят с этой причиной, и отчёт это отражает.

Трафик узла через помехи

На узле OSC-сообщение или UDP-датаграмма кнопка Пропустить через помехи ставит перед ним узел сетевых помех: реле слушает свободный порт на 127.0.0.1, пересылает на цель узла с пресетом LAN, а узел теперь отправляет на реле.

Смена помех ​

Узел Сменить помехи указывает в поле Помехи одно из реле эксперимента и задаёт профиль, которым оно будет ухудшать трафик начиная с этого шага. Реле сохраняет свой порт и соединения; новые значения действуют со следующего пакета или куска данных. Лента показывает новый профиль.

Каждая смена завершает фазу. Отчёт о запуске хранит для каждого реле:

  • адреса прослушивания и цели, а также протокол, если это TCP;
  • общие счётчики: принято, переслано, отброшено, не пропущено ограничением полосы, продублировано, искажено, переставлено, байты — а для TCP ещё соединения, сброшенные и оставленные полуоткрытыми;
  • каждую фазу: имя профиля, когда она началась и закончилась (в миллисекундах от открытия реле), и те же счётчики только за эту фазу.

Пакет учитывается в той фазе, которая решила его судьбу, даже если его задержанная копия уходит уже после переключения. Реле хранит последние 1000 фаз; более старые учитываются в счётчиках, но не хранятся.

Узел Сменить помехи, который не указывает ни одного реле эксперимента, отклоняется (impair.relay_unknown).

Эмулятор ​

Узел Эмулятор играет зависимость на всё время запуска: HTTP API, OSC-, UDP- или TCP-устройство или MQTT-брокер. Это тот же эмулятор, который экран Эмуляторы запускает сам по себе: кнопка Изменить… открывает его правила, В библиотеку сохраняет копию в библиотеке, Из библиотеки берёт эмулятор оттуда.

  • Он открывается до первого шага и отвечает, пока запуск не закончится; порт, который не удаётся открыть, останавливает запуск ещё до всякого трафика. В потоке узел проходит сразу.
  • Его адрес — буквальный IP:port. Шаблоны, по которым он сопоставляет входящее, принимают только параметры; его ответы — шаблоны, которые читают то, что пришло ({{request.…}}), и параметры запуска. Секреты он читать не может.
  • Его случайный выбор — взвешенная смесь ответов, разброс задержки, генераторы в ответах — берётся из seed запуска.
  • HTTP-эмулятор — это ещё и то, что слушает узел Ждать HTTP-запрос на том же адресе: так проверяется, что прислала тестируемая система. Если эмулятора там нет, собственный слушатель запуска отвечает на каждый запрос 204.
  • OSC- или UDP-эмулятор делит свой порт с ожиданиями запуска на нём: и те и другие видят каждую датаграмму.
  • MQTT-эмулятор — это брокер, которым узлы Публикация MQTT и Ждать MQTT запуска могут пользоваться как любым другим.

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

Отключение и включение эмулятора ​

Узел Эмулятор: выкл./вкл. указывает в поле Эмулятор один из эмуляторов запуска; в поле Состояние — Отключён или Работает. Пока эмулятор отключён:

ЭмуляторЧто встречает клиент
HTTPто, что задано в поле Пока отключён: 503 Недоступен (503, тело {"error":"unavailable"}), Закрыть соединение или Без ответа — запрос удерживается, пока клиент не сдастся, но не дольше 120 с
TCP-устройство, MQTT-брокерсоединения рвутся, новые отклоняются
OSC-, UDP-устройствоничего не отвечает

То, что приходит во время отключения, учитывается как down и никогда — как запрос, который не взяло ни одно правило. Узел Ждать HTTP-запрос по-прежнему видит запросы. Эмулятор остаётся отключённым, пока шаг его не включит, что бы ни говорило его собственное расписание отключений, а конец запуска в любом случае его закрывает.

Узел Эмулятор: выкл./вкл., который не указывает ни одного эмулятора эксперимента, отклоняется (emulator.node_unknown).

Отключения по расписанию ​

Эмулятор может отключаться и сам: в его правилах флажок Время от времени отключается включает поля Работает, мс и Отключён, мс, каждое — 10–3 600 000 мс, а для HTTP — ещё Пока отключён. Эмулятор отвечает в течение первого времени, отключён в течение второго и так далее, считая с момента открытия, — в запуске это до первого шага. Во время отключения по расписанию 503 от HTTP-эмулятора несёт Retry-After с целым числом секунд до возвращения, не меньше 1; у 503 во время отключения шагом Эмулятор: выкл./вкл. этого заголовка нет — никто не знает, когда оно закончится.

Расписанию не нужен шаг; шагу не нужно расписание. Расписание — для зависимости, которая то пропадает, то появляется, шаг — для отключения в выбранной точке сценария.

Пример: отключение за медленным каналом ​

Клиент запрашивает у API заказ через реле. Пока он спрашивает, вторая ветка замедляет канал до 4G, отключает API на две секунды, возвращает его и снова делает канал чистым. Клиент должен спрашивать, пока не получит ответ.

text
start → orders → link → split
split ─ branch1 → settle → until_ok ─ done → answered → joined
                           until_ok ─ body → get → status → pause → until_ok
split ─ branch2 → slow → down → outage → up → clean → joined
joined → end

Имена — это id узлов в файле ниже.

  1. Добавьте параметр api = http://127.0.0.1:18091 — адрес реле, а не API.
  2. Добавьте узел Эмулятор: HTTP, 127.0.0.1:18090, маршрут GET /orders/:id, который отвечает 200 с {"order":"{{request.params.id}}"}.
  3. После него — узел Сетевые помехи: Слушать127.0.0.1:18091, Пересылать 127.0.0.1:18090, Протокол TCP, пресет LAN.
  4. После него — узел Параллельный запуск.
  5. На выходе Поток 1 — клиент: Пауза на 300 мс, затем Цикл — Итераций не больше 40, выйти раньше, если{{status}} равно 200. Его тело: HTTP-запросGET {{api}}/orders/42, узел Извлечь значение, который берёт Код статуса в status, пауза 250 мс и связь обратно к циклу. На выходе Готово — узел Лог / Отметка с текстом Orders API answers again: HTTP {{status}}.
  6. На выходе Поток 2 — сбои: Сменить помехи реле на 4G; Эмулятор: выкл./вкл., который переводит Orders API в состояние Отключён с 503 Недоступен; пауза 2000 мс; ещё один Эмулятор: выкл./вкл., который возвращает его в состояние Работает; ещё один Сменить помехи обратно на LAN.
  7. Соедините обе ветки с узлом Слияние потоков, а его — с узлом Финиш.
  8. Запустите эксперимент.

Лента показывает запросы клиента, на которые через медленный канал приходит 503, возвращение API, затем 200 и выход цикла через Готово. Отчёт насчитывает примерно пять запросов, заставших API отключённым, и один, на который ответил его маршрут, а также три фазы реле — LAN на мгновение, 4G на время отключения и снова LAN, — каждую со своим трафиком.

Эксперимент в виде файла

Сохраните его в файл .json и откройте кнопкой Открыть JSON… в меню Эксперименты.

json
{
  "version": 9,
  "name": "Outage behind a slow link",
  "params": [{ "name": "api", "value": "http://127.0.0.1:18091" }],
  "profiles": [],
  "profile": null,
  "seed": null,
  "nodes": [
    { "id": "start", "type": "start", "x": 40, "y": 270 },
    { "id": "orders", "type": "emulator", "x": 260, "y": 270,
      "emulator": { "name": "Orders API", "bind": "127.0.0.1:18090", "protocol": "http",
        "routes": [{ "method": "GET", "path": "/orders/:id", "when": [], "order": "sequence",
          "responses": [{ "status": 200, "headers": [], "body": "{\"order\":\"{{request.params.id}}\"}", "delay_ms": 0, "jitter_ms": 0, "fault": "none", "weight": 1 }] }],
        "fallback": null } },
    { "id": "link", "type": "impairment", "x": 490, "y": 270, "listen": "127.0.0.1:18091", "target": "127.0.0.1:18090", "protocol": "tcp",
      "profile": { "name": "lan", "latency_ms": 1, "jitter_ms": 1 } },
    { "id": "split", "type": "fork", "x": 720, "y": 270 },
    { "id": "settle", "type": "delay", "x": 950, "y": 140, "ms": 300 },
    { "id": "until_ok", "type": "loop", "x": 1180, "y": 140, "max": 40,
      "until": { "value": "{{status}}", "op": "eq", "expected": "200" } },
    { "id": "get", "type": "http", "x": 1410, "y": 20,
      "request": { "method": "GET", "url": "{{api}}/orders/42", "headers": [], "body": null, "timeout_ms": 3000 } },
    { "id": "status", "type": "extract", "x": 1640, "y": 20, "variable": "status", "from": "status", "expr": "" },
    { "id": "pause", "type": "delay", "x": 1870, "y": 20, "ms": 250 },
    { "id": "answered", "type": "log", "x": 1410, "y": 140, "message": "Orders API answers again: HTTP {{status}}" },
    { "id": "slow", "type": "impairment_change", "x": 950, "y": 400, "relay": "link",
      "profile": { "name": "4g", "latency_ms": 60, "jitter_ms": 25, "rate_kbps": 20000 } },
    { "id": "down", "type": "emulator_state", "x": 1180, "y": 400, "emulator": "orders", "down": true, "fault": "unavailable" },
    { "id": "outage", "type": "delay", "x": 1410, "y": 400, "ms": 2000 },
    { "id": "up", "type": "emulator_state", "x": 1640, "y": 400, "emulator": "orders", "down": false, "fault": "unavailable" },
    { "id": "clean", "type": "impairment_change", "x": 1870, "y": 400, "relay": "link",
      "profile": { "name": "lan", "latency_ms": 1, "jitter_ms": 1 } },
    { "id": "joined", "type": "join", "x": 2100, "y": 270 },
    { "id": "end", "type": "end", "x": 2330, "y": 270 }
  ],
  "edges": [
    { "from": "start", "to": "orders" },
    { "from": "orders", "to": "link" },
    { "from": "link", "to": "split" },
    { "from": "split", "to": "settle", "port": "branch1" },
    { "from": "split", "to": "slow", "port": "branch2" },
    { "from": "settle", "to": "until_ok" },
    { "from": "until_ok", "to": "get", "port": "body" },
    { "from": "get", "to": "status" },
    { "from": "status", "to": "pause" },
    { "from": "pause", "to": "until_ok" },
    { "from": "until_ok", "to": "answered", "port": "done" },
    { "from": "answered", "to": "joined" },
    { "from": "slow", "to": "down" },
    { "from": "down", "to": "outage" },
    { "from": "outage", "to": "up" },
    { "from": "up", "to": "clean" },
    { "from": "clean", "to": "joined" },
    { "from": "joined", "to": "end" }
  ]
}

Два шаблона в меню Эксперименты делают то же самое иначе: Фазы сбоев отправляет датаграммы эмулированному устройству через UDP-реле, которое переключается между чистой сетью, потерями, отсутствием связи и снова чистой сетью; Отказ зависимости отключает эмулированный API на две секунды, пока клиент продолжает спрашивать.

Порты ​

Сокеты одного запуска — ожидания, ответы, эмуляторы, реле — не могут делить порт одного протокола; UDP- и TCP-сокет могут использовать один и тот же номер. Для поля Слушать реле адрес на 0.0.0.0 конфликтует с любым адресом на том же порту.

СокетС чем не может делить порт
HTTP-, TCP- или MQTT-эмуляторс другим таким же (emulator.bind_taken)
OSC- или UDP-эмуляторс другим таким же (emulator.bind_taken)
TCP- или MQTT-эмуляторс узлом Ждать HTTP-запрос (emulator.bind_taken)
Слушать UDP-релес другим UDP-реле, OSC- или UDP-эмулятором, ожиданием или сокетом ответа (impair.bind_taken)
Слушать TCP-релес другим TCP-реле, HTTP-, TCP- или MQTT-эмулятором, узлом Ждать HTTP-запрос (impair.bind_taken)

Общие намеренно: HTTP-эмулятор и шаги Ждать HTTP-запрос на его адресе; OSC- или UDP-эмулятор и ожидания на его порту; ожидания на одном адресе между собой.

Реле не может пересылать само в себя ни напрямую, ни через другие реле: его трафик ходил бы по кругу через loopback (impair.loop). Два реле подряд перед устройством — нормально.

Повторение запуска со сбоями ​

Каждое решение реле — потерять ли пакет, продублировать, исказить или задержать, какой джиттер ему достанется — берётся из seed запуска, отдельно для каждого направления и пакет за пакетом. Случайный выбор эмулятора тоже берётся из него. Запустите снова с тем же seed и тем же трафиком — и те же пакеты встретят ту же судьбу: сбой, замеченный один раз, можно увидеть снова.

Чтобы сохранить seed, нажмите Закрепить рядом с ним на ленте или запустите эксперимент с ним через Запустить с…; см. seed. Чего seed удержать не может — это время: когда тестируемая система отправляет данные и, значит, в какую фазу попадает пакет.