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