Load-test rig: k6 swarm script + in-cluster target drone
This commit is contained in:
38
manifests/loadtest/k6-script.yaml
Normal file
38
manifests/loadtest/k6-script.yaml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: loadtest
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: k6-script
|
||||||
|
namespace: loadtest
|
||||||
|
data:
|
||||||
|
test.js: |
|
||||||
|
// Generic load generator. Configured by env:
|
||||||
|
// TARGET_URL - full URL to hit
|
||||||
|
// VUS - virtual users per pod (default 20)
|
||||||
|
// DURATION - k6 duration string (default "60s")
|
||||||
|
import http from 'k6/http';
|
||||||
|
import { check, sleep } from 'k6';
|
||||||
|
|
||||||
|
const target = __ENV.TARGET_URL || 'http://example.com';
|
||||||
|
const vus = parseInt(__ENV.VUS || '20');
|
||||||
|
const duration = __ENV.DURATION || '60s';
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
stages: [
|
||||||
|
{ duration: '10s', target: vus },
|
||||||
|
{ duration: duration, target: vus },
|
||||||
|
{ duration: '5s', target: 0 },
|
||||||
|
],
|
||||||
|
thresholds: { http_req_failed: ['rate<0.01'] },
|
||||||
|
noConnectionReuse: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const res = http.get(target, { timeout: '10s' });
|
||||||
|
check(res, { 'status 2xx/3xx': (r) => r.status < 300 });
|
||||||
|
sleep(1 / vus * 10);
|
||||||
|
}
|
||||||
31
manifests/loadtest/target-drone.yaml
Normal file
31
manifests/loadtest/target-drone.yaml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: target-drone
|
||||||
|
namespace: loadtest
|
||||||
|
spec:
|
||||||
|
replicas: 3
|
||||||
|
selector:
|
||||||
|
matchLabels: {app: target-drone}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels: {app: target-drone}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: nginx
|
||||||
|
image: nginx:1.29-alpine
|
||||||
|
ports: [{containerPort: 80}]
|
||||||
|
resources:
|
||||||
|
requests: {cpu: 10m, memory: 16Mi}
|
||||||
|
limits: {memory: 64Mi}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: target-drone
|
||||||
|
namespace: loadtest
|
||||||
|
spec:
|
||||||
|
selector: {app: target-drone}
|
||||||
|
ports:
|
||||||
|
- port: 80
|
||||||
|
targetPort: 80
|
||||||
65
manifests/monitoring/grafana.yaml
Normal file
65
manifests/monitoring/grafana.yaml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: grafana-datasources
|
||||||
|
namespace: monitoring
|
||||||
|
data:
|
||||||
|
datasources.yaml: |
|
||||||
|
apiVersion: 1
|
||||||
|
datasources:
|
||||||
|
- name: Prometheus
|
||||||
|
type: prometheus
|
||||||
|
access: proxy
|
||||||
|
url: http://prometheus.monitoring.svc:9090
|
||||||
|
isDefault: true
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: grafana-admin
|
||||||
|
namespace: monitoring
|
||||||
|
stringData:
|
||||||
|
admin-user: adamcarr
|
||||||
|
admin-password: planck-lab-admin
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: grafana
|
||||||
|
namespace: monitoring
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels: {app: grafana}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels: {app: grafana}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: grafana
|
||||||
|
image: grafana/grafana:12.0.2
|
||||||
|
env:
|
||||||
|
- {name: GF_SECURITY_ADMIN_USER, valueFrom: {secretKeyRef: {name: grafana-admin, key: admin-user}}}
|
||||||
|
- {name: GF_SECURITY_ADMIN_PASSWORD, valueFrom: {secretKeyRef: {name: grafana-admin, key: admin-password}}}
|
||||||
|
- {name: GF_AUTH_ANONYMOUS_ENABLED, value: "true"}
|
||||||
|
ports:
|
||||||
|
- containerPort: 3000
|
||||||
|
resources:
|
||||||
|
requests: {cpu: 50m, memory: 128Mi}
|
||||||
|
limits: {memory: 512Mi}
|
||||||
|
volumeMounts:
|
||||||
|
- {name: datasources, mountPath: /etc/grafana/provisioning/datasources}
|
||||||
|
volumes:
|
||||||
|
- name: datasources
|
||||||
|
configMap: {name: grafana-datasources}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: grafana
|
||||||
|
namespace: monitoring
|
||||||
|
spec:
|
||||||
|
selector: {app: grafana}
|
||||||
|
ports:
|
||||||
|
- port: 3000
|
||||||
|
targetPort: 3000
|
||||||
41
manifests/monitoring/node-exporter.yaml
Normal file
41
manifests/monitoring/node-exporter.yaml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: DaemonSet
|
||||||
|
metadata:
|
||||||
|
name: node-exporter
|
||||||
|
namespace: monitoring
|
||||||
|
labels: {app: node-exporter}
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
matchLabels: {app: node-exporter}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels: {app: node-exporter}
|
||||||
|
spec:
|
||||||
|
hostNetwork: true
|
||||||
|
hostPID: true
|
||||||
|
tolerations:
|
||||||
|
- operator: Exists
|
||||||
|
containers:
|
||||||
|
- name: node-exporter
|
||||||
|
image: quay.io/prometheus/node-exporter:v1.9.1
|
||||||
|
args:
|
||||||
|
- --path.procfs=/host/proc
|
||||||
|
- --path.sysfs=/host/sys
|
||||||
|
- --collector.filesystem.mount-points-exclude=^/(dev|proc|sys|var/lib/docker/.+|var/lib/kubelet/.+)($|/)
|
||||||
|
ports:
|
||||||
|
- containerPort: 9100
|
||||||
|
hostPort: 9100
|
||||||
|
resources:
|
||||||
|
requests: {cpu: 20m, memory: 32Mi}
|
||||||
|
limits: {memory: 64Mi}
|
||||||
|
volumeMounts:
|
||||||
|
- {name: proc, mountPath: /host/proc, readOnly: true}
|
||||||
|
- {name: sys, mountPath: /host/sys, readOnly: true}
|
||||||
|
- {name: root, mountPath: /rootfs, readOnly: true}
|
||||||
|
volumes:
|
||||||
|
- name: proc
|
||||||
|
hostPath: {path: /proc}
|
||||||
|
- name: sys
|
||||||
|
hostPath: {path: /sys}
|
||||||
|
- name: root
|
||||||
|
hostPath: {path: /}
|
||||||
115
manifests/monitoring/prometheus.yaml
Normal file
115
manifests/monitoring/prometheus.yaml
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: monitoring
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: prometheus
|
||||||
|
namespace: monitoring
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRole
|
||||||
|
metadata:
|
||||||
|
name: prometheus
|
||||||
|
rules:
|
||||||
|
- apiGroups: [""]
|
||||||
|
resources: [nodes, nodes/proxy, services, endpoints, pods]
|
||||||
|
verbs: [get, list, watch]
|
||||||
|
- apiGroups: ["extensions", "networking.k8s.io"]
|
||||||
|
resources: [ingresses]
|
||||||
|
verbs: [get, list, watch]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: ClusterRoleBinding
|
||||||
|
metadata:
|
||||||
|
name: prometheus
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: ClusterRole
|
||||||
|
name: prometheus
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: prometheus
|
||||||
|
namespace: monitoring
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: prometheus-config
|
||||||
|
namespace: monitoring
|
||||||
|
data:
|
||||||
|
prometheus.yml: |
|
||||||
|
global:
|
||||||
|
scrape_interval: 15s
|
||||||
|
external_labels:
|
||||||
|
cluster: planck
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: prometheus
|
||||||
|
static_configs:
|
||||||
|
- targets: ["localhost:9090"]
|
||||||
|
- job_name: node-exporter
|
||||||
|
kubernetes_sd_configs:
|
||||||
|
- role: node
|
||||||
|
relabel_configs:
|
||||||
|
- source_labels: [__meta_kubernetes_node_address_InternalIP]
|
||||||
|
target_label: instance_ip
|
||||||
|
- action: labelmap
|
||||||
|
regex: __meta_kubernetes_node_label_(.+)
|
||||||
|
metric_relabel_configs:
|
||||||
|
- source_labels: [__name__]
|
||||||
|
regex: go_.*
|
||||||
|
action: drop
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: prometheus
|
||||||
|
namespace: monitoring
|
||||||
|
spec:
|
||||||
|
serviceName: prometheus
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels: {app: prometheus}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels: {app: prometheus}
|
||||||
|
spec:
|
||||||
|
serviceAccountName: prometheus
|
||||||
|
containers:
|
||||||
|
- name: prometheus
|
||||||
|
image: prom/prometheus:v3.6.0
|
||||||
|
args:
|
||||||
|
- --config.file=/etc/prometheus/prometheus.yml
|
||||||
|
- --storage.tsdb.retention.time=15d
|
||||||
|
- --storage.tsdb.path=/data
|
||||||
|
ports:
|
||||||
|
- containerPort: 9090
|
||||||
|
resources:
|
||||||
|
requests: {cpu: 150m, memory: 512Mi}
|
||||||
|
limits: {memory: 1Gi}
|
||||||
|
volumeMounts:
|
||||||
|
- {name: config, mountPath: /etc/prometheus}
|
||||||
|
- {name: data, mountPath: /data}
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap: {name: prometheus-config}
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata: {name: data}
|
||||||
|
spec:
|
||||||
|
accessModes: [ReadWriteOnce]
|
||||||
|
storageClassName: local-path
|
||||||
|
resources:
|
||||||
|
requests: {storage: 20Gi}
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: prometheus
|
||||||
|
namespace: monitoring
|
||||||
|
spec:
|
||||||
|
selector: {app: prometheus}
|
||||||
|
ports:
|
||||||
|
- port: 9090
|
||||||
|
targetPort: 9090
|
||||||
47
scripts/loadtest.sh
Executable file
47
scripts/loadtest.sh
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Spawn a k6 load swarm against a target.
|
||||||
|
# Usage: ./scripts/loadtest.sh <target-url> [pods] [vus-per-pod] [duration]
|
||||||
|
# Example: ./scripts/loadtest.sh https://staging.example.com 20 50 5m
|
||||||
|
# -> 20 pods x 50 virtual users = 1000 concurrent requests, 5 minutes
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
TARGET=${1:?usage: loadtest.sh <target-url> [pods] [vus-per-pod] [duration]}
|
||||||
|
PODS=${2:-20}
|
||||||
|
VUS=${3:-20}
|
||||||
|
DURATION=${4:-60s}
|
||||||
|
NAME="loadtest-$(date +%s)"
|
||||||
|
|
||||||
|
kubectl -n loadtest apply -f - <<EOF
|
||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: $NAME
|
||||||
|
spec:
|
||||||
|
parallelism: $PODS
|
||||||
|
completions: $PODS
|
||||||
|
ttlSecondsAfterFinished: 300
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
containers:
|
||||||
|
- name: k6
|
||||||
|
image: grafana/k6:1.4.0
|
||||||
|
command: ["k6", "run", "/scripts/test.js"]
|
||||||
|
env:
|
||||||
|
- {name: TARGET_URL, value: "$TARGET"}
|
||||||
|
- {name: VUS, value: "$VUS"}
|
||||||
|
- {name: DURATION, value: "$DURATION"}
|
||||||
|
resources:
|
||||||
|
requests: {cpu: 100m, memory: 128Mi}
|
||||||
|
limits: {memory: 256Mi}
|
||||||
|
volumeMounts:
|
||||||
|
- {name: scripts, mountPath: /scripts}
|
||||||
|
volumes:
|
||||||
|
- name: scripts
|
||||||
|
configMap: {name: k6-script}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Swarm '$NAME' launched: $PODS pods x $VUS vus for $DURATION against $TARGET"
|
||||||
|
echo "Watch: kubectl -n loadtest get pods -w"
|
||||||
|
echo "Logs: kubectl -n loadtest logs -f job/$NAME --all-pods=true --tail=5"
|
||||||
|
echo "Stop: kubectl -n loadtest delete job $NAME"
|
||||||
Reference in New Issue
Block a user