Inject HTTP faults into Pod
This example shows how PodDisruptor can be used for testing the effect of faults injected in the HTTP requests served by a pod.
You will find the complete source code at the end of this document. Next sections examine the code in detail.
The example uses httpbin, a simple request/response application that offers endpoints for testing different HTTP requests.
The test requires httpbin
to be deployed in a cluster in the namespace httpbin
and exposed with an external IP. the IP address is expected in the environment variable SVC_IP
.
You will find the Kubernetes manifests and the instructions of how to deploy it to a cluster in the test setup section at the end of this document. You can learn more about how to get the external IP address in the expose your application section.
Initialization
The initialization code imports the external dependencies required by the test. The PodDisruptor
class imported from the xk6-disruptor
extension provides functions for injecting faults in pods. The k6/http module provides functions for executing HTTP requests.
import { PodDisruptor } from 'k6/x/disruptor';
import http from 'k6/http';
Test Load
The test load is generated by the default
function, which executes a request to the httpbin
pod using the IP obtained from the environment variable SVC_IP
. The test makes requests to the endpoint delay/0.1
which will return after 0.1
seconds (100ms
).
export default function (data) {
http.get(`http://${data.SVC_IP}/delay/0.1`);
}
Note
The test uses thedelay
endpoint which return after the requested delay. It requests a0.1s
(100ms
) delay to ensure the baseline scenario (see scenarios below) has meaningful statistics for the request duration. If we were simply calling a locally deployed http server (for examplenginx
), the response time would exhibit a large variation between a few microseconds to a few milliseconds. Having100ms
as baseline response time has proved to offer more consistent results.
Fault injection
The disrupt
function creates a PodDisruptor
using a selector that matches pods in the namespace httpbin
with the label app: httpbin
.
The http faults are injected by calling the PodDisruptor.injectHTTPFaults method using a fault definition that introduces a delay of 50ms
on each request and an error code 500
in 10%
of the requests.
export function disrupt(data) {
if (__ENV.SKIP_FAULTS == '1') {
return;
}
const selector = {
namespace: namespace,
select: {
labels: {
app: 'httpbin',
},
},
};
const podDisruptor = new PodDisruptor(selector);
// delay traffic from one random replica of the deployment
const fault = {
averageDelay: '50ms',
errorCode: 500,
errorRate: 0.1,
};
podDisruptor.injectHTTPFaults(fault, '30s');
}
Notice the following code snippet in the injectFaults
function above:
export function disrupt(data) {
if (__ENV.SKIP_FAULTS == '1') {
return;
}
//...
}
This code makes the function return without injecting faults if the SKIP_FAULTS
environment variable is passed to the execution of the test with a value of “1”. We will use this option to obtain a baseline execution without faults.
Scenarios
This test defines two scenarios to be executed. The load
scenario applies the test load to the httpbin
application for 30s
invoking the default
function. The disrupt
scenario invokes the disrupt
function to inject a fault in the HTTP requests of the target application.
export const options = {
scenarios: {
load: {
executor: 'constant-arrival-rate',
rate: 100,
preAllocatedVUs: 10,
maxVUs: 100,
exec: 'default',
startTime: '0s',
duration: '30s',
},
disrupt: {
executor: 'shared-iterations',
iterations: 1,
vus: 1,
exec: 'disrupt',
startTime: '0s',
},
},
};
Note
Notice that thedisrupt
scenario uses ashared-iterations
executor with one iteration and oneVU
. This setting ensures thedisrupt
function is executed only once. Executing this function multiples times concurrently may have unpredictable results.
Executions
Note
The commands in this section assume thexk6-disruptor
binary is available in your current directory. This location can change depending on the installation process and the platform. Refer to the installation section for details on how to install it in your environment.
Baseline execution
We will first execute the test without introducing faults to have an baseline using the following command:
xk6-disruptor run --env SKIP_FAULTS=1 --env SVC_IP=$SVC_IP disrupt-pod.js
xk6-disruptor run --env SKIP_FAULTS=1 --env "SVC_IP=$Env:SVC_IP" disrupt-pod.js
Notice the argument --env SKIP_FAULT=1
, which makes the disrupt
function return without injecting any fault as explained in the fault injection section. Also notice the --env SVC_IP
argument, which passes the external IP used to access the httpbin
application.
You should get an output similar to the one shown below (click Expand
button to see all output).
Fault injection
We repeat the execution injecting the faults. Notice we have removed the --env SKIP_FAULTS=1
argument.
xk6-disruptor run --env SVC_IP=$SVC_IP disrupt-pod.js
xk6-disruptor run --env "SVC_IP=$Env:SVC_IP" disrupt-pod.js
Comparison
Let’s take a closer look at the results for the requests on each scenario. We can observe that he base
scenario has an average of 103ms
and an error rate of 0%
while the faults
scenario has a median around 151.9ms
and an error rate of nearly 10%
, matching the definition of the faults defined in the disruptor.
Execution | Avg. Response | Failed requests |
---|---|---|
Baseline | 103.22ms | 0.00% |
Fault injection | 151.9 ms | 9.65% |
Note
Notice we have used the average response time reported asexpected_response:true
because this metric only consider successful requests whilehttp_req_duration
considers all requests, including those returning a fault.
Source Code
import { PodDisruptor } from 'k6/x/disruptor';
import http from 'k6/http';
export default function (data) {
http.get(`http://${__ENV.SVC_IP}/delay/0.1`);
}
export function disrupt(data) {
if (__ENV.SKIP_FAULTS == '1') {
return;
}
const selector = {
namespace: 'httpbin',
select: {
labels: {
app: 'httpbin',
},
},
};
const podDisruptor = new PodDisruptor(selector);
// delay traffic from one random replica of the deployment
const fault = {
averageDelay: '50ms',
errorCode: 500,
errorRate: 0.1,
};
podDisruptor.injectHTTPFaults(fault, '30s');
}
export const options = {
scenarios: {
load: {
executor: 'constant-arrival-rate',
rate: 100,
preAllocatedVUs: 10,
maxVUs: 100,
exec: 'default',
startTime: '0s',
duration: '30s',
},
disrupt: {
executor: 'shared-iterations',
iterations: 1,
vus: 1,
exec: 'disrupt',
startTime: '0s',
},
},
};
Test setup
The tests requires the deployment of the httpbin
application. The application must also be accessible using an external IP available in the SVC_IP
environment variable.
The manifests below define the resources required for deploying the application and exposing it as a LoadBalancer service.
You can deploy the application using the following commands:
# Create Namespace
kubectl apply -f namespace.yaml
namespace/httpbin created
# Deploy Pod
kubectl apply -f pod.yaml
pod/httpbin created
# Expose Pod as a Service
kubectl apply -f service.yaml
service/httpbin created
You can retrieve the resources using the following command:
kubectl -n httpbin get all
NAME READY STATUS RESTARTS AGE
pod/httpbin 1/1 Running 0 1m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/httpbin LoadBalancer 10.96.169.78 172.18.255.200 80:31224/TCP 1m
You must set the environment variable SVC_IP
with the external IP address and port used to access the httpbin
service from the test script.
You can learn more about how to get the external IP address in the expose your application section.
Manifests
apiVersion: 'v1'
kind: Namespace
metadata:
name: httpbin
kind: 'Pod'
apiVersion: 'v1'
metadata:
name: httpbin
namespace: httpbin
labels:
app: httpbin
spec:
containers:
- name: httpbin
image: kennethreitz/httpbin
ports:
- name: http
containerPort: 80
apiVersion: 'v1'
kind: 'Service'
metadata:
name: httpbin
namespace: httpbin
spec:
selector:
app: httpbin
type: 'LoadBalancer'
ports:
- port: 80
targetPort: 80