Falhas como nós
Para ver como um sistema se vira quando a rede se degrada ou uma dependência sai do ar, coloque a falha no experimento. Um retransmissor ou um emulador abre com a execução, uma etapa o aciona na hora certa, e o relatório da execução conta o que aconteceu em cada fase. O fim da execução — aprovada, com falha ou parada — os fecha, então nada fica degradado depois dela.
| Nó | O que faz |
|---|---|
| Degradação | um retransmissor entre o sistema em teste e o destino dele, degradando o que passa, durante toda a execução |
| Alterar degradação | passa um retransmissor da execução para outro perfil, desta etapa em diante |
| Emulador | uma API, um dispositivo ou um broker interpretado pelo Signal Lab, durante toda a execução |
| Derrubar/reativar emulador | tira do ar um emulador da execução, ou o traz de volta |
Os quatro estão no menu de adicionar, em Falhas e Emular. Os campos deles estão na referência dos nós; o retransmissor em si é descrito em Degradação, e os emuladores, em Emuladores.
Degradação
O sistema em teste envia para o retransmissor em vez do destino real; o retransmissor encaminha para o destino, traz as respostas de volta e degrada os dois sentidos.
| Campo | O quê |
|---|---|
| Escutar em | IP:port para o qual o sistema em teste envia ou ao qual se conecta, porta diferente de 0 |
| Encaminhar para | IP:port do destino real |
| Protocolo | UDP — cada datagrama tem a sua própria sorte — ou TCP — cada conexão é ligada a uma conexão própria até o destino |
| o perfil | uma Predefinição ou valores seus |
O que um retransmissor lê do perfil depende do protocolo; os demais valores ficam de fora:
| Protocolo | Degradações |
|---|---|
| UDP | latência, jitter, perda de pacotes, perda em rajada, duplicação, corrupção, reordenação, um limite de banda, sem rede |
| TCP | latência e jitter (um fluxo continua em ordem), um limite de banda (o remetente é freado, nada é descartado), conexões redefinidas, conexões deixadas semiabertas, sem rede |
Aberto antes da primeira etapa. Todo retransmissor do experimento abre quando a execução começa, como os sockets das esperas, então os campos Escutar em e Encaminhar para dele aceitam só texto e parâmetros (node.params_only) — {{relay}} com um parâmetro relay, nunca uma variável. Uma porta que não pode ser aberta interrompe a execução antes de qualquer tráfego, no nó.
De passagem no fluxo. Quando a execução chega ao nó, ele passa imediatamente, e a linha do tempo diz o que ele degrada e com quê. O retransmissor funciona do início ao fim da execução, seja qual for a posição do nó no grafo.
Fechado com a execução. Seja como for que a execução termine, o retransmissor fecha; as conexões de um retransmissor TCP fecham com ele. Um retransmissor que parou de retransmitir por conta própria guarda o motivo: as etapas que o usam falham com ele, e o relatório informa isso.
Passar pela degradação
Em um nó Mensagem OSC ou Datagrama UDP, o botão Passar pela degradação coloca uma Degradação na frente dele: o retransmissor escuta em uma porta livre de 127.0.0.1, encaminha para o destino do nó com a predefinição LAN, e o nó passa a enviar para o retransmissor.
Alterar degradação
O nó Alterar degradação indica um dos retransmissores do experimento em Degradação e dá o perfil com que ele degrada daquela etapa em diante. O retransmissor mantém a porta e as conexões; os novos valores valem a partir do próximo pacote ou bloco. A linha do tempo mostra o novo perfil.
Cada alteração encerra uma fase. O relatório da execução guarda, para cada retransmissor:
- os endereços de escuta e de destino, e o protocolo, quando é TCP;
- as contagens totais: recebidos, encaminhados, descartados, estrangulados, duplicados, corrompidos, reordenados, bytes — e, para TCP, as conexões, as redefinidas e as deixadas semiabertas;
- cada fase: o nome do perfil, quando ela começou e terminou, em milissegundos a partir do momento em que o retransmissor abriu, e as mesmas contagens só daquela fase.
Um pacote é contado na fase que decidiu a sorte dele, mesmo quando a cópia atrasada dele sai depois da troca. Um retransmissor guarda as suas últimas 1.000 fases; as mais antigas são contadas, não guardadas.
Um nó Alterar degradação que não indica nenhum retransmissor do experimento é recusado (impair.relay_unknown).
Emulador
O nó Emulador faz o papel de uma dependência durante toda a execução: uma API HTTP, um dispositivo OSC, UDP ou TCP, ou um broker MQTT. É o mesmo emulador que a tela Emuladores executa sozinha: o botão Editar… abre as regras dele, Para a biblioteca guarda uma cópia na biblioteca, Da biblioteca pega um de lá.
- Ele abre antes da primeira etapa e responde até o fim da execução; uma porta que não pode ser aberta interrompe a execução antes de qualquer tráfego. No fluxo, ele passa imediatamente.
- O endereço dele é um
IP:portliteral. Os padrões de correspondência dele aceitam só parâmetros; as respostas são modelos lidos com o que chegou ({{request.…}}) e os parâmetros da execução. Ele não pode ler segredos. - As escolhas aleatórias dele — uma mistura ponderada de respostas, o jitter dos atrasos, os geradores nas respostas — são sorteadas a partir da semente da execução.
- Um emulador HTTP é também o que um nó Aguardar requisição HTTP no mesmo endereço escuta: ele verifica o que o sistema em teste enviou. Sem um emulador ali, o próprio ouvinte da execução responde a toda requisição com
204. - Um emulador OSC ou UDP compartilha a porta dele com as esperas da execução nessa porta: os dois veem cada datagrama.
- Um emulador MQTT é um broker que os nós Publicação MQTT e Aguardar MQTT da execução podem usar como qualquer outro.
O relatório da execução guarda, para cada nó de emulador, o nome, o protocolo e o endereço, e as contagens: requisições ao todo, as que nenhuma regra aceitou, as que falharam, as que o encontraram fora do ar, as mensagens que um broker não conseguiu entregar a um cliente lento e as correspondências de cada regra.
Emulador fora do ar e de volta
O nó Derrubar/reativar emulador indica um dos emuladores da execução em Emulador; o Estado é Fora do ar ou No ar. Enquanto ele está fora do ar:
| Emulador | O que se encontra |
|---|---|
| HTTP | o que diz o campo Enquanto fora do ar: 503 Unavailable (503, corpo {"error":"unavailable"}), Fechar a conexão ou Sem resposta — a requisição fica segurada até o cliente desistir, no máximo 120 s |
| Dispositivo TCP, broker MQTT | as conexões caem e as novas são recusadas |
| Dispositivo OSC, UDP | nada é respondido |
O que chega enquanto ele está fora do ar conta como down, nunca como uma requisição que nenhuma regra aceitou. Um nó Aguardar requisição HTTP continua vendo as requisições. O emulador fica fora do ar até que uma etapa o traga de volta, diga o que disser o ciclo de quedas dele, e o fim da execução o fecha de qualquer forma.
Um nó Derrubar/reativar emulador que não indica nenhum emulador do experimento é recusado (emulator.node_unknown).
Quedas programadas
Um emulador também pode sair do ar sozinho: nas regras dele, a opção Cai de vez em quando define No ar por, ms e Fora do ar por, ms, cada um de 10–3.600.000 ms, e Enquanto fora do ar para HTTP. Ele responde durante o primeiro, fica fora do ar durante o segundo, e assim por diante, contando a partir de quando abriu — em uma execução, antes da primeira etapa. Fora do ar pelo ciclo, o 503 de um emulador HTTP leva Retry-After com os segundos inteiros que faltam para ele voltar, pelo menos 1; um 503 enquanto uma etapa Derrubar/reativar emulador o mantém fora do ar não leva nenhum, já que ninguém sabe quando isso termina.
Um ciclo não precisa de etapa; uma etapa não precisa de ciclo. Use o ciclo para uma dependência que oscila, e a etapa para uma queda em um ponto escolhido do fluxo.
Exemplo: uma queda atrás de um enlace lento
Um cliente pede um pedido a uma API por meio de um retransmissor. Enquanto ele pede, um segundo ramo deixa o enlace lento, em 4G, tira a API do ar por dois segundos, a traz de volta e deixa o enlace limpo de novo. O cliente precisa continuar pedindo até receber a resposta.
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 → endOs nomes são os ids dos nós no arquivo abaixo.
- Acrescente um parâmetro
api=http://127.0.0.1:18091— o retransmissor, não a API. - Acrescente um Emulador: HTTP,
127.0.0.1:18090, uma rotaGET /orders/:idque responde200com{"order":"{{request.params.id}}"}. - Depois dele, uma Degradação: Escutar em
127.0.0.1:18091, Encaminhar para127.0.0.1:18090, Protocolo TCP, predefinição LAN. - Depois dela, um nó Ramo paralelo.
- Em Ramo 1, o cliente: um Atraso de 300 ms e depois um Laço — Máximo de iterações 40, parar antes quando
{{status}}é igual a200. O corpo dele: uma Requisição HTTPGET {{api}}/orders/42, um nó Extrair valor que leva o Código de status parastatus, um atraso de 250 ms, ligado de volta ao Laço. Em Concluído, um Marcador de logOrders API answers again: HTTP {{status}}. - Em Ramo 2, as falhas: um nó Alterar degradação que passa o retransmissor para 4G; um nó Derrubar/reativar emulador que deixa a Orders API Fora do ar com 503 Unavailable; um atraso de 2.000 ms; outro Derrubar/reativar emulador que a deixa No ar de novo; outro Alterar degradação de volta para LAN.
- Ligue os dois ramos a um nó Unir ramos, e esse nó ao Fim.
- Execute.
A linha do tempo mostra as requisições do cliente respondidas com 503 pelo enlace lento, a API voltando e então 200 e o Laço saindo por Concluído. O relatório conta umas cinco requisições que encontraram a API fora do ar e uma respondida pela rota dela, e as três fases do retransmissor — LAN por um instante, 4G durante a queda, LAN de novo —, cada uma com o seu próprio tráfego.
O experimento como arquivo
Salve-o como arquivo .json e abra-o com Abrir JSON… em Experimentos.
{
"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" }
]
}Dois modelos em Experimentos fazem o mesmo de outras maneiras: Fases de falha envia datagramas a um dispositivo emulado por um retransmissor UDP que passa por limpo, com perdas, sem rede e limpo de novo; Queda de dependência tira uma API emulada do ar por dois segundos enquanto um cliente continua perguntando.
Portas
Os sockets de uma execução — esperas, respostas, emuladores, retransmissores — não podem compartilhar uma porta do mesmo protocolo; um socket UDP e um TCP podem usar o mesmo número. No campo Escutar em de um retransmissor, um endereço em 0.0.0.0 entra em conflito com qualquer endereço na mesma porta.
| Socket | Não pode compartilhar a porta com |
|---|---|
| um emulador HTTP, TCP ou MQTT | outro deles (emulator.bind_taken) |
| um emulador OSC ou UDP | outro deles (emulator.bind_taken) |
| um emulador TCP ou MQTT | um Aguardar requisição HTTP (emulator.bind_taken) |
| o Escutar em de um retransmissor UDP | outro retransmissor UDP, um emulador OSC ou UDP, uma espera ou o socket de uma resposta (impair.bind_taken) |
| o Escutar em de um retransmissor TCP | outro retransmissor TCP, um emulador HTTP, TCP ou MQTT, um Aguardar requisição HTTP (impair.bind_taken) |
Compartilhados de propósito: um emulador HTTP e as etapas Aguardar requisição HTTP no endereço dele; um emulador OSC ou UDP e as esperas na porta dele; esperas em um mesmo endereço entre si.
Um retransmissor não pode encaminhar para si mesmo, diretamente ou por outros retransmissores: o tráfego dele daria voltas no loopback (impair.loop). Dois retransmissores em sequência na frente de um dispositivo não são problema.
Repetir uma execução com falhas
Cada decisão que um retransmissor toma — se um pacote é perdido, duplicado, corrompido ou retido, quanto jitter ele recebe — é sorteada a partir da semente da execução, separadamente para cada sentido e pacote a pacote. As escolhas aleatórias de um emulador também saem dela. Execute de novo com a mesma semente e o mesmo tráfego, e os mesmos pacotes têm a mesma sorte: uma falha vista uma vez pode ser vista de novo.
Para manter a semente, pressione Fixar ao lado dela na linha do tempo, ou execute com ela em Executar com…; veja sementes. O que a semente não consegue fixar é o tempo: quando o sistema em teste envia e, portanto, em qual fase cai um pacote.