perf-skills 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +8 -0
- package/README.md +227 -0
- package/package.json +42 -0
- package/perf-skills/.claude-plugin/plugin.json +5 -0
- package/perf-skills/SKILL.md +169 -0
- package/perf-skills/references/tools/gatling.md +286 -0
- package/perf-skills/references/tools/jmeter.md +251 -0
- package/perf-skills/references/tools/k6.md +269 -0
- package/perf-skills/references/tools/loadrunner.md +116 -0
- package/perf-skills/references/tools/locust.md +133 -0
- package/perf-skills/references/tools/neoload.md +61 -0
- package/perf-skills/references/tools/octoperf.md +50 -0
- package/perf-skills/references/topics/database-testing.md +214 -0
- package/perf-skills/references/topics/modern-architectures.md +314 -0
- package/perf-skills/references/topics/observability.md +201 -0
- package/perf-skills/references/topics/production-testing.md +214 -0
- package/perf-skills/references/topics/protocol-testing.md +274 -0
- package/perf-skills/references/topics/results-analysis.md +244 -0
- package/perf-skills/references/topics/script-generation.md +221 -0
- package/perf-skills/references/topics/test-data.md +196 -0
- package/perf-skills/references/topics/test-execution.md +281 -0
- package/perf-skills/references/topics/workload-design.md +207 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Observability in Performance Testing
|
|
2
|
+
|
|
3
|
+
Observability is what bridges "the test shows slow responses" to "the database is saturating its connection pool at 300 VUs." Without correlated server-side metrics, performance test analysis is guesswork.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## The Three Pillars
|
|
8
|
+
|
|
9
|
+
| Pillar | What It Tells You | Tools |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| **Metrics** | Numeric measurements over time (CPU, RPS, latency histograms) | Prometheus, InfluxDB, Datadog, CloudWatch |
|
|
12
|
+
| **Logs** | Event records with context (error details, request IDs) | Elasticsearch, Loki, Splunk, CloudWatch Logs |
|
|
13
|
+
| **Traces** | End-to-end request path across services | Jaeger, Tempo, Datadog APM, Dynatrace |
|
|
14
|
+
|
|
15
|
+
A complete performance investigation uses all three, correlated by time and request ID.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Minimal Observability Stack for Performance Testing
|
|
20
|
+
|
|
21
|
+
### Open Source (self-hosted)
|
|
22
|
+
```
|
|
23
|
+
Load Tool (k6/JMeter)
|
|
24
|
+
│ pushes metrics
|
|
25
|
+
▼
|
|
26
|
+
InfluxDB ←──────────────────────────────────
|
|
27
|
+
│ │
|
|
28
|
+
▼ Prometheus
|
|
29
|
+
Grafana Dashboard │
|
|
30
|
+
│ Node Exporter (host metrics)
|
|
31
|
+
▼ cAdvisor (container metrics)
|
|
32
|
+
Alerting (Slack/PagerDuty) JMX Exporter (JVM metrics)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Cloud-native
|
|
36
|
+
```
|
|
37
|
+
Load Tool → Grafana Cloud (k6 native) → Grafana Dashboard
|
|
38
|
+
App → Datadog / Dynatrace / New Relic → correlated in single pane
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Correlating Load Test with APM
|
|
44
|
+
|
|
45
|
+
The most powerful workflow: overlay load metrics with APM metrics on the same timeline.
|
|
46
|
+
|
|
47
|
+
**Step 1:** Add a test identifier to all load-tool requests:
|
|
48
|
+
```javascript
|
|
49
|
+
// k6: tag all requests
|
|
50
|
+
export const options = {
|
|
51
|
+
tags: { testRun: __ENV.BUILD_ID || 'manual', env: 'staging' }
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
```bash
|
|
55
|
+
# JMeter: set custom header on all requests
|
|
56
|
+
# In HTTP Header Manager (at Test Plan level):
|
|
57
|
+
X-Load-Test-Run: ${__P(build.id,manual)}
|
|
58
|
+
X-Load-Test-Phase: ${__P(phase,load)}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Step 2:** In your APM, filter traces and metrics by the test tag — isolate test traffic from organic traffic.
|
|
62
|
+
|
|
63
|
+
**Step 3:** Align timelines — zoom in on the window when p95 degraded and look at:
|
|
64
|
+
- Service CPU and memory during that window
|
|
65
|
+
- DB query duration spike
|
|
66
|
+
- Thread pool queue depth
|
|
67
|
+
- Cache hit rate drop
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Prometheus + Grafana Setup
|
|
72
|
+
|
|
73
|
+
### k6 → Prometheus (remote write)
|
|
74
|
+
```bash
|
|
75
|
+
k6 run --out experimental-prometheus-rw \
|
|
76
|
+
-e K6_PROMETHEUS_RW_SERVER_URL=http://prometheus:9090/api/v1/write \
|
|
77
|
+
-e K6_PROMETHEUS_RW_TREND_STATS='p(50),p(95),p(99)' \
|
|
78
|
+
script.js
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### JMeter → InfluxDB
|
|
82
|
+
1. Install **Backend Listener** config element.
|
|
83
|
+
2. Set implementation: `InfluxdbBackendListenerClient`
|
|
84
|
+
3. Config: `influxdbUrl=http://influx:8086`, `measurement=jmeter`, `token=<token>`
|
|
85
|
+
|
|
86
|
+
### Useful Grafana Dashboards
|
|
87
|
+
| Dashboard | Grafana ID | For |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| k6 Load Testing Results | 2587 | k6 + InfluxDB |
|
|
90
|
+
| JMeter Load Test | 1152 | JMeter + InfluxDB |
|
|
91
|
+
| Node Exporter Full | 1860 | Host system metrics |
|
|
92
|
+
| JVM Overview | 4701 | Java app JVM metrics |
|
|
93
|
+
| Kubernetes Cluster | 7249 | K8s pod metrics |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## JVM Metrics (Java Applications)
|
|
98
|
+
|
|
99
|
+
For Java services under test, always collect JVM metrics alongside load tool metrics.
|
|
100
|
+
|
|
101
|
+
### Expose via JMX Exporter (Prometheus)
|
|
102
|
+
```yaml
|
|
103
|
+
# jmx_config.yaml
|
|
104
|
+
rules:
|
|
105
|
+
- pattern: java.lang<type=Memory><HeapMemoryUsage>used
|
|
106
|
+
name: jvm_heap_used_bytes
|
|
107
|
+
- pattern: java.lang<type=GarbageCollector, name=(.+)><CollectionTime>
|
|
108
|
+
name: jvm_gc_collection_seconds_total
|
|
109
|
+
labels:
|
|
110
|
+
gc: $1
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Key JVM Metrics to Monitor
|
|
114
|
+
| Metric | Normal | Warning |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| Heap used % | < 70% | > 85% = GC pressure |
|
|
117
|
+
| GC pause duration (p99) | < 200ms | > 500ms = impacting response time |
|
|
118
|
+
| GC frequency | Occasional | Continuous = memory leak |
|
|
119
|
+
| Thread pool queue size | 0–10 | Growing = throughput ceiling reached |
|
|
120
|
+
| JDBC pool active connections | < 80% | > 90% = connection starvation |
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Log Correlation
|
|
125
|
+
|
|
126
|
+
During a performance test, log volume explodes. Use structured logging and filtering:
|
|
127
|
+
|
|
128
|
+
### Mark test traffic in logs
|
|
129
|
+
```java
|
|
130
|
+
// Spring Boot: use MDC to add test context
|
|
131
|
+
MDC.put("testRun", request.getHeader("X-Load-Test-Run"));
|
|
132
|
+
// Log4j/Logback will include this in every log line for this thread
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Useful log queries during test (Elasticsearch/Kibana)
|
|
136
|
+
```
|
|
137
|
+
# Error spike investigation
|
|
138
|
+
testRun: "build-42" AND level: ERROR
|
|
139
|
+
| group by logger, message
|
|
140
|
+
| order by count desc
|
|
141
|
+
|
|
142
|
+
# Slow transactions
|
|
143
|
+
testRun: "build-42" AND duration_ms: >500
|
|
144
|
+
| group by endpoint
|
|
145
|
+
| percentile(duration_ms, 95)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Distributed Tracing
|
|
151
|
+
|
|
152
|
+
Traces show the end-to-end path of a single request across microservices — invaluable for pinpointing *which service* in a chain is slow.
|
|
153
|
+
|
|
154
|
+
### Setup for load testing
|
|
155
|
+
- Confirm tracing is active in the target environment.
|
|
156
|
+
- After the test, sample traces from the slowest requests (p99 window).
|
|
157
|
+
- Use the trace waterfall to find which service/DB call accounts for the most latency.
|
|
158
|
+
|
|
159
|
+
### Adding trace context to load tool requests
|
|
160
|
+
```javascript
|
|
161
|
+
// k6: pass trace context (Jaeger B3 format)
|
|
162
|
+
const traceId = randomString(32, '0123456789abcdef');
|
|
163
|
+
http.get(url, {
|
|
164
|
+
headers: {
|
|
165
|
+
'X-B3-TraceId': traceId,
|
|
166
|
+
'X-B3-SpanId': randomString(16, '0123456789abcdef'),
|
|
167
|
+
'X-B3-Sampled': '1',
|
|
168
|
+
'X-Load-Test-Run': __ENV.BUILD_ID
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Synthetic Monitoring (Post-Test)
|
|
176
|
+
|
|
177
|
+
After a load test validates a build, set up **synthetic monitors** to continuously test a minimal user journey in production:
|
|
178
|
+
|
|
179
|
+
| Tool | Description |
|
|
180
|
+
|---|---|
|
|
181
|
+
| Grafana Synthetic Monitoring | k6 scripts run on schedule from cloud probes |
|
|
182
|
+
| Datadog Synthetics | Browser + API tests from global locations |
|
|
183
|
+
| AWS CloudWatch Synthetics (Canaries) | Node.js scripts on schedule |
|
|
184
|
+
| Checkly | API + Browser checks with k6 integration |
|
|
185
|
+
|
|
186
|
+
Synthetic monitors catch regressions that slip through staging without requiring another full load test.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Performance Testing Observability Checklist
|
|
191
|
+
|
|
192
|
+
- [ ] APM tool active and capturing traces in test environment
|
|
193
|
+
- [ ] Server metrics (CPU, memory, disk I/O) dashboards ready before test start
|
|
194
|
+
- [ ] JVM metrics exposed (for Java services)
|
|
195
|
+
- [ ] DB slow query logging enabled and queryable
|
|
196
|
+
- [ ] Load tool pushing real-time metrics to dashboard (InfluxDB, Grafana Cloud, etc.)
|
|
197
|
+
- [ ] Test run tagged with build ID / run ID for filtering
|
|
198
|
+
- [ ] Alert thresholds set (so team is notified if something breaks during test)
|
|
199
|
+
- [ ] Log aggregation active and searchable
|
|
200
|
+
- [ ] Baseline metrics screenshot taken before test starts (for comparison)
|
|
201
|
+
- [ ] Metrics retention configured to keep results for trend analysis
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Production & Staging Performance Testing
|
|
2
|
+
|
|
3
|
+
Running tests in production is not inherently dangerous — it's a discipline. Many organizations run production performance testing routinely. The key is controlled blast radius, observability, and rollback readiness.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Staging Environment Testing
|
|
8
|
+
|
|
9
|
+
### What Makes Staging Valid for Performance Testing
|
|
10
|
+
|
|
11
|
+
Staging is only a reliable proxy for production if:
|
|
12
|
+
|
|
13
|
+
| Factor | Requirement |
|
|
14
|
+
|---|---|
|
|
15
|
+
| **Hardware** | Same instance type, CPU, memory as production |
|
|
16
|
+
| **Architecture** | Same number of replicas, same DB tier, same cache config |
|
|
17
|
+
| **Data volume** | DB has similar row counts to production (schema-level) |
|
|
18
|
+
| **Network** | Similar topology (no shortcutting the load balancer) |
|
|
19
|
+
| **Dependencies** | Real downstream services (not mocks for performance tests) |
|
|
20
|
+
|
|
21
|
+
Using mocks for load tests is only appropriate for component-level isolation tests — full user journey tests must hit real (or realistic stub) dependencies.
|
|
22
|
+
|
|
23
|
+
### Staging Limitations
|
|
24
|
+
- Cache hit rates will differ if data set is too small.
|
|
25
|
+
- Third-party integrations may behave differently (rate limits are usually lower in test).
|
|
26
|
+
- Infrastructure auto-scaling may not be configured identically.
|
|
27
|
+
- Cold-start performance distorts early-stage results — pre-warm before measuring.
|
|
28
|
+
|
|
29
|
+
### Pre-Warm Strategy
|
|
30
|
+
```bash
|
|
31
|
+
# Run a smoke test (1 VU) for 5 minutes before starting the load test
|
|
32
|
+
# This warms:
|
|
33
|
+
# - JVM JIT compilation
|
|
34
|
+
# - Connection pools
|
|
35
|
+
# - Application-level caches
|
|
36
|
+
# - CDN edge caches (if applicable)
|
|
37
|
+
|
|
38
|
+
k6 run --vus 1 --duration 5m --tag phase=warmup warmup.js
|
|
39
|
+
k6 run --vus 500 --duration 20m --tag phase=load main.js
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Production Testing Strategies
|
|
45
|
+
|
|
46
|
+
### Strategy 1: Canary Deployment + Load Test
|
|
47
|
+
|
|
48
|
+
Route a small % of production traffic to the new release and observe:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Production Traffic
|
|
52
|
+
│
|
|
53
|
+
├─ 95% → Stable instances (v1.0)
|
|
54
|
+
└─ 5% → Canary instances (v1.1) ← Monitor closely
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Gradually increase canary traffic percentage while monitoring:
|
|
58
|
+
- Error rate (should not increase)
|
|
59
|
+
- p95 response time (should not increase)
|
|
60
|
+
- Business metrics (conversion rate, etc.)
|
|
61
|
+
|
|
62
|
+
**Tools:** AWS ALB weighted target groups, Kubernetes Argo Rollouts, Istio traffic splitting, NGINX split_clients.
|
|
63
|
+
|
|
64
|
+
### Strategy 2: Shadow Testing (Traffic Mirroring)
|
|
65
|
+
|
|
66
|
+
Mirror production traffic to a shadow cluster — it receives all production requests but its responses are discarded:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
Real User Request
|
|
70
|
+
│
|
|
71
|
+
├─ Live → Production (v1) → Response to user
|
|
72
|
+
└─ Mirror → Shadow (v2) → Response discarded (no user impact)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Shadow cluster processes real production workload — perfect for validating performance of a new version without any user impact.
|
|
76
|
+
|
|
77
|
+
**Tools:** AWS ALB request mirroring, Istio `mirror`, NGINX `mirror` directive.
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
# Istio traffic mirroring
|
|
81
|
+
apiVersion: networking.istio.io/v1alpha3
|
|
82
|
+
kind: VirtualService
|
|
83
|
+
spec:
|
|
84
|
+
http:
|
|
85
|
+
- route:
|
|
86
|
+
- destination:
|
|
87
|
+
host: my-service-v1
|
|
88
|
+
mirror:
|
|
89
|
+
host: my-service-v2
|
|
90
|
+
mirrorPercentage:
|
|
91
|
+
value: 100.0
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Strategy 3: Synthetic Load Injection in Production
|
|
95
|
+
|
|
96
|
+
Inject artificial load at low volumes (typically 5–10% of peak) during off-peak hours:
|
|
97
|
+
|
|
98
|
+
- Use a dedicated load generator with production-class credentials.
|
|
99
|
+
- Tag synthetic requests to exclude from business metrics dashboards.
|
|
100
|
+
- Use a synthetic user pool (not real users).
|
|
101
|
+
- Monitor production SLAs during injection.
|
|
102
|
+
|
|
103
|
+
**When to use:** Validate autoscaling behavior, warm new nodes before peak, run regression checks post-deployment.
|
|
104
|
+
|
|
105
|
+
### Strategy 4: Chaos Engineering (Resilience Testing)
|
|
106
|
+
|
|
107
|
+
Intentionally inject failures to test how the system degrades:
|
|
108
|
+
|
|
109
|
+
| Experiment | Tool | Tests |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| Kill a pod/instance | Chaos Monkey, LitmusChaos | Failover speed, error rate |
|
|
112
|
+
| Throttle CPU/memory | Stress-ng, LitmusChaos | Degradation under resource pressure |
|
|
113
|
+
| Introduce network latency | tc netem, Chaos Mesh | Timeout handling, retry logic |
|
|
114
|
+
| Kill the database | LitmusChaos | Circuit breaker, fallback |
|
|
115
|
+
| Saturate disk I/O | fio, LitmusChaos | Log rotation, disk-full handling |
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# Inject 100ms network latency + 10% packet loss to a pod
|
|
119
|
+
kubectl exec -it chaos-pod -- \
|
|
120
|
+
tc qdisc add dev eth0 root netem delay 100ms loss 10%
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Always combine chaos with a baseline load** — run at 50% of expected peak so the system has load to respond to when faults occur.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Pre-Production Performance Gates
|
|
128
|
+
|
|
129
|
+
### Gate Criteria (must pass before production deploy)
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
1. Smoke test (1 VU): Zero errors, correct responses
|
|
133
|
+
2. Load test (target VUs): p95 < SLA, error rate < 1%
|
|
134
|
+
3. Stress test (1.5× target): Graceful degradation, no data corruption
|
|
135
|
+
4. Regression comparison: p95 within 10% of previous passing run
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Exemptions and Escalation
|
|
139
|
+
- If a test fails, categorize: **known regression** (documented, fix tracked) vs **unexpected regression** (block deploy).
|
|
140
|
+
- SLA breach of < 5% may be acceptable with product owner sign-off.
|
|
141
|
+
- Emergency hotfixes may skip stress test but must pass smoke + load.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Safety Controls for Production Testing
|
|
146
|
+
|
|
147
|
+
### Mandatory Safety Controls
|
|
148
|
+
|
|
149
|
+
| Control | Implementation |
|
|
150
|
+
|---|---|
|
|
151
|
+
| **Blast radius limit** | Never inject more than 10–20% of prod capacity without approval |
|
|
152
|
+
| **Kill switch** | A single command / button to stop all injectors immediately |
|
|
153
|
+
| **Auto-abort on error spike** | Test stops automatically if error rate exceeds threshold |
|
|
154
|
+
| **Rollback plan** | Documented and tested rollback procedure ready |
|
|
155
|
+
| **Communication** | Ops/SRE on standby; incident channel open |
|
|
156
|
+
| **Synthetic user tagging** | All test requests tagged to exclude from real user metrics |
|
|
157
|
+
| **No real user data** | Synthetic test data only — never process real PII in load tests |
|
|
158
|
+
|
|
159
|
+
### Auto-abort configuration
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
// k6: auto-abort on error spike
|
|
163
|
+
export const options = {
|
|
164
|
+
thresholds: {
|
|
165
|
+
http_req_failed: [{
|
|
166
|
+
threshold: 'rate<0.05', // Abort if error rate > 5%
|
|
167
|
+
abortOnFail: true,
|
|
168
|
+
delayAbortEval: '30s', // Wait 30s before aborting (avoid false positives)
|
|
169
|
+
}],
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# JMeter: stop test on error rate threshold via Backend Listener + custom alerting
|
|
176
|
+
# Or use Taurus wrapper:
|
|
177
|
+
bzt test.jmx \
|
|
178
|
+
-o modules.passfail.checks[0].subject=fail \
|
|
179
|
+
-o modules.passfail.checks[0].threshold=5% \
|
|
180
|
+
-o modules.passfail.checks[0].condition=over \
|
|
181
|
+
-o modules.passfail.checks[0].timeframe=60s \
|
|
182
|
+
-o modules.passfail.checks[0].stop=true
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Environments Progression
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
Developer Laptop (smoke, 1 VU)
|
|
191
|
+
↓
|
|
192
|
+
CI Environment (smoke + light load, 10–25 VUs, fast feedback)
|
|
193
|
+
↓
|
|
194
|
+
Staging / Performance Environment (full load + stress tests)
|
|
195
|
+
↓
|
|
196
|
+
Pre-Production / Mirror (shadow tests, canary validation)
|
|
197
|
+
↓
|
|
198
|
+
Production (synthetic monitoring, canary, controlled injection)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Each environment adds fidelity; the production stage assumes all previous stages passed.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Incident Response During a Performance Test
|
|
206
|
+
|
|
207
|
+
If something breaks during a test:
|
|
208
|
+
|
|
209
|
+
1. **Immediately stop the test** (kill switch — stop all injectors).
|
|
210
|
+
2. **Capture current state** — snapshot metrics, take thread dumps (Java), capture logs.
|
|
211
|
+
3. **Assess impact** — are real users affected? Is the failure isolated to test traffic?
|
|
212
|
+
4. **Roll back if needed** — restore previous version, restart services.
|
|
213
|
+
5. **Preserve evidence** — don't restart services without capturing logs and heap dumps.
|
|
214
|
+
6. **Post-mortem** — document what broke, at what load, what the root cause was.
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# Protocol-Specific Performance Testing
|
|
2
|
+
|
|
3
|
+
Covers gRPC, GraphQL, WebSocket/SSE, and message queue (Kafka, RabbitMQ, SQS) load testing — protocols with unique challenges beyond standard HTTP/REST.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## gRPC Performance Testing
|
|
8
|
+
|
|
9
|
+
gRPC uses HTTP/2, Protocol Buffers, and persistent connections — fundamentally different from REST at the wire level.
|
|
10
|
+
|
|
11
|
+
### Key Challenges
|
|
12
|
+
|
|
13
|
+
| Challenge | Why It Matters |
|
|
14
|
+
|---|---|
|
|
15
|
+
| **Protobuf compilation** | Tests require compiled `.proto` definitions; can't just send raw JSON |
|
|
16
|
+
| **Connection multiplexing** | HTTP/2 multiplexes streams over a single connection — fewer connections needed but different bottleneck profile |
|
|
17
|
+
| **Streaming** | Unary, server-streaming, client-streaming, and bidirectional-streaming each need different test strategies |
|
|
18
|
+
| **Metadata vs headers** | gRPC metadata is the equivalent of HTTP headers; auth tokens go here |
|
|
19
|
+
| **Error codes** | gRPC uses its own status codes (OK, UNAVAILABLE, DEADLINE_EXCEEDED) — not HTTP status codes |
|
|
20
|
+
|
|
21
|
+
### Tool Support
|
|
22
|
+
|
|
23
|
+
| Tool | gRPC Support | Notes |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| **k6** | `k6/grpc` built-in module | Supports unary and streaming; load `.proto` or use reflection |
|
|
26
|
+
| **Gatling** | `gatling-grpc` plugin | Scala/Java DSL; supports unary and streaming |
|
|
27
|
+
| **JMeter** | `gRPC Sampler` plugin | GUI-based; requires proto descriptor file |
|
|
28
|
+
| **ghz** | Dedicated gRPC benchmarking tool | CLI-only; excellent for quick benchmarks |
|
|
29
|
+
|
|
30
|
+
### k6 gRPC Example
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
import grpc from 'k6/grpc';
|
|
34
|
+
import { check, sleep } from 'k6';
|
|
35
|
+
|
|
36
|
+
const client = new grpc.Client();
|
|
37
|
+
client.load(['definitions'], 'hello.proto');
|
|
38
|
+
|
|
39
|
+
export default function () {
|
|
40
|
+
client.connect('grpc-server:50051', { plaintext: true });
|
|
41
|
+
|
|
42
|
+
const response = client.invoke('hello.HelloService/SayHello', {
|
|
43
|
+
greeting: 'perf-test',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
check(response, {
|
|
47
|
+
'status is OK': (r) => r && r.status === grpc.StatusOK,
|
|
48
|
+
'has message': (r) => r && r.message.reply !== '',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
client.close();
|
|
52
|
+
sleep(1);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### gRPC Testing Considerations
|
|
57
|
+
|
|
58
|
+
- **Connection reuse**: Keep connections open across iterations; don't connect/close per request.
|
|
59
|
+
- **Streaming throughput**: For server-streaming RPCs, measure messages-per-second, not just request latency.
|
|
60
|
+
- **Deadline propagation**: Set gRPC deadlines in tests to match production timeouts.
|
|
61
|
+
- **Load balancer awareness**: gRPC over HTTP/2 with persistent connections can cause uneven load across backends — test with client-side load balancing or L7 proxy.
|
|
62
|
+
- **Protobuf payload size**: Binary encoding is smaller than JSON — adjust throughput expectations accordingly.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## GraphQL Performance Testing
|
|
67
|
+
|
|
68
|
+
GraphQL introduces query complexity as a variable — the same endpoint can serve trivially cheap or devastatingly expensive requests.
|
|
69
|
+
|
|
70
|
+
### Key Challenges
|
|
71
|
+
|
|
72
|
+
| Challenge | Why It Matters |
|
|
73
|
+
|---|---|
|
|
74
|
+
| **Variable query cost** | A single endpoint can serve queries with wildly different server costs |
|
|
75
|
+
| **N+1 resolver problem** | Nested queries can trigger cascading DB calls; load tests expose this at scale |
|
|
76
|
+
| **Query depth / complexity** | Deep nesting can cause exponential resolver execution |
|
|
77
|
+
| **Batched queries** | Clients may send multiple operations in one request |
|
|
78
|
+
| **Persisted queries** | Production may use query allowlists; ad-hoc queries get rejected |
|
|
79
|
+
|
|
80
|
+
### Testing Strategy
|
|
81
|
+
|
|
82
|
+
1. **Catalog production queries**: Extract real queries from logs or APM traces — don't invent synthetic queries.
|
|
83
|
+
2. **Categorize by cost**: Light (single field), medium (nested 2 levels), heavy (deep joins, lists).
|
|
84
|
+
3. **Build a realistic query mix**: Weight tests by actual production query distribution.
|
|
85
|
+
4. **Test with and without caching**: GraphQL caching (DataLoader, CDN) dramatically changes performance profiles.
|
|
86
|
+
|
|
87
|
+
### k6 GraphQL Example
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
import http from 'k6/http';
|
|
91
|
+
import { check, sleep } from 'k6';
|
|
92
|
+
|
|
93
|
+
const GRAPHQL_ENDPOINT = `${__ENV.BASE_URL}/graphql`;
|
|
94
|
+
|
|
95
|
+
const QUERIES = {
|
|
96
|
+
lightQuery: `query { user(id: "${__VU}") { name email } }`,
|
|
97
|
+
heavyQuery: `query { users(first: 100) { edges { node { name orders(last: 10) { totalAmount items { name } } } } } }`,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default function () {
|
|
101
|
+
// 80% light queries, 20% heavy queries
|
|
102
|
+
const query = Math.random() < 0.8 ? QUERIES.lightQuery : QUERIES.heavyQuery;
|
|
103
|
+
|
|
104
|
+
const res = http.post(GRAPHQL_ENDPOINT, JSON.stringify({ query }), {
|
|
105
|
+
headers: {
|
|
106
|
+
'Content-Type': 'application/json',
|
|
107
|
+
'Authorization': `Bearer ${__ENV.TOKEN}`,
|
|
108
|
+
},
|
|
109
|
+
tags: { name: 'POST /graphql' },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
check(res, {
|
|
113
|
+
'status 200': (r) => r.status === 200,
|
|
114
|
+
'no errors': (r) => !r.json('errors'),
|
|
115
|
+
'has data': (r) => r.json('data') !== null,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
sleep(1);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### GraphQL-Specific Metrics to Track
|
|
123
|
+
|
|
124
|
+
- **Query complexity score** (if server exposes it)
|
|
125
|
+
- **Resolver execution time** (via APM / tracing)
|
|
126
|
+
- **DataLoader batch efficiency** (cache hit rate)
|
|
127
|
+
- **Error rate by query type** (complexity-related timeouts vs auth errors)
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## WebSocket / SSE Performance Testing
|
|
132
|
+
|
|
133
|
+
Persistent connections have fundamentally different scaling characteristics from request/response HTTP.
|
|
134
|
+
|
|
135
|
+
### Key Challenges
|
|
136
|
+
|
|
137
|
+
| Challenge | Why It Matters |
|
|
138
|
+
|---|---|
|
|
139
|
+
| **Connection count** | Each VU holds an open connection — tests memory/fd limits, not just CPU |
|
|
140
|
+
| **Message throughput** | Measure messages/sec independently from connection count |
|
|
141
|
+
| **Backpressure** | What happens when the server can't send fast enough? |
|
|
142
|
+
| **Reconnection behavior** | Dropped connections should auto-reconnect; test the reconnect storm |
|
|
143
|
+
| **Fan-out cost** | Broadcasting to N connections has O(N) server cost |
|
|
144
|
+
|
|
145
|
+
### Tool Support
|
|
146
|
+
|
|
147
|
+
| Tool | WebSocket | SSE |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| **k6** | `k6/ws` module | Not built-in; use HTTP streaming |
|
|
150
|
+
| **Gatling** | Full WebSocket DSL | Not built-in |
|
|
151
|
+
| **JMeter** | WebSocket Sampler plugin | Not built-in |
|
|
152
|
+
| **Artillery** | Built-in `ws` engine | Built-in SSE |
|
|
153
|
+
|
|
154
|
+
### k6 WebSocket Example
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
import ws from 'k6/ws';
|
|
158
|
+
import { check, sleep } from 'k6';
|
|
159
|
+
|
|
160
|
+
export default function () {
|
|
161
|
+
const url = `${__ENV.WS_URL}/ws/chat`;
|
|
162
|
+
const params = { headers: { 'Authorization': `Bearer ${__ENV.TOKEN}` } };
|
|
163
|
+
|
|
164
|
+
const res = ws.connect(url, params, function (socket) {
|
|
165
|
+
socket.on('open', () => {
|
|
166
|
+
socket.send(JSON.stringify({ type: 'subscribe', channel: 'updates' }));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
socket.on('message', (msg) => {
|
|
170
|
+
const data = JSON.parse(msg);
|
|
171
|
+
check(data, {
|
|
172
|
+
'has type': (d) => d.type !== undefined,
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
socket.on('error', (e) => {
|
|
177
|
+
console.error('WS error:', e.error());
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Hold connection open for 30 seconds, sending periodic pings
|
|
181
|
+
socket.setInterval(() => {
|
|
182
|
+
socket.send(JSON.stringify({ type: 'ping' }));
|
|
183
|
+
}, 5000);
|
|
184
|
+
|
|
185
|
+
socket.setTimeout(() => {
|
|
186
|
+
socket.close();
|
|
187
|
+
}, 30000);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
check(res, { 'WS status 101': (r) => r && r.status === 101 });
|
|
191
|
+
sleep(1);
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### WebSocket Testing Strategy
|
|
196
|
+
|
|
197
|
+
1. **Connection ramp**: Gradually open connections — don't blast 10k connections at once.
|
|
198
|
+
2. **Separate connection test from message test**: First, find max stable connections; then test message throughput at a stable connection count.
|
|
199
|
+
3. **Measure server-side**: File descriptor count, memory per connection, event loop lag.
|
|
200
|
+
4. **Test reconnection storms**: Kill server, observe client reconnection behavior and server recovery.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Message Queue / Event Streaming Testing
|
|
205
|
+
|
|
206
|
+
Testing Kafka, RabbitMQ, SQS, and similar systems requires measuring producer throughput, consumer lag, and end-to-end latency.
|
|
207
|
+
|
|
208
|
+
### Key Challenges
|
|
209
|
+
|
|
210
|
+
| Challenge | Why It Matters |
|
|
211
|
+
|---|---|
|
|
212
|
+
| **Producer throughput** | Messages/sec the system can ingest |
|
|
213
|
+
| **Consumer lag** | How far behind consumers fall under load |
|
|
214
|
+
| **End-to-end latency** | Time from produce to consume — the real user-facing metric |
|
|
215
|
+
| **Partition scaling** | Kafka throughput scales with partitions; test with realistic partition counts |
|
|
216
|
+
| **Message ordering** | Under load, verify ordering guarantees still hold |
|
|
217
|
+
| **Dead letter queues** | Verify failed messages route correctly under pressure |
|
|
218
|
+
|
|
219
|
+
### Tool Support
|
|
220
|
+
|
|
221
|
+
| Tool | Kafka | RabbitMQ | SQS |
|
|
222
|
+
|---|---|---|---|
|
|
223
|
+
| **k6 (xk6-kafka)** | Yes | No (use HTTP management API) | No |
|
|
224
|
+
| **JMeter** | Kafka plugin / JMS Sampler | JMS Sampler | AWS SDK Sampler |
|
|
225
|
+
| **kafka-producer-perf-test** | Built-in Kafka tool | N/A | N/A |
|
|
226
|
+
| **rabbitmq-perf-test** | N/A | Official RabbitMQ tool | N/A |
|
|
227
|
+
|
|
228
|
+
### k6 Kafka Example (xk6-kafka)
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
import { Writer, Reader, Connection } from 'k6/x/kafka';
|
|
232
|
+
|
|
233
|
+
const writer = new Writer({ brokers: ['kafka:9092'], topic: 'perf-test' });
|
|
234
|
+
const reader = new Reader({ brokers: ['kafka:9092'], topic: 'perf-test', groupID: 'perf-group' });
|
|
235
|
+
|
|
236
|
+
export default function () {
|
|
237
|
+
// Produce
|
|
238
|
+
writer.produce({
|
|
239
|
+
messages: [{
|
|
240
|
+
key: `key-${__VU}-${__ITER}`,
|
|
241
|
+
value: JSON.stringify({ orderId: `${__VU}-${__ITER}`, timestamp: Date.now() }),
|
|
242
|
+
}],
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Consume (in a separate scenario ideally)
|
|
246
|
+
const messages = reader.consume({ limit: 1 });
|
|
247
|
+
// Validate message content
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function teardown() {
|
|
251
|
+
writer.close();
|
|
252
|
+
reader.close();
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Message Queue Testing Strategy
|
|
257
|
+
|
|
258
|
+
1. **Test producers and consumers separately first**, then together.
|
|
259
|
+
2. **Measure consumer lag over time** — a growing lag under steady load indicates a bottleneck.
|
|
260
|
+
3. **Test with realistic message sizes** — a 100-byte message vs a 1MB payload have very different throughput ceilings.
|
|
261
|
+
4. **Verify idempotency** — under load with retries, duplicate messages should not corrupt state.
|
|
262
|
+
5. **Test partition rebalancing** — add/remove consumers during a test to verify rebalance behavior.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Protocol Testing Checklist
|
|
267
|
+
|
|
268
|
+
- [ ] Proto definitions / schemas compiled and available to test tool
|
|
269
|
+
- [ ] Connection reuse strategy defined (persistent vs per-request)
|
|
270
|
+
- [ ] Streaming scenarios identified and scripted separately from unary/request-response
|
|
271
|
+
- [ ] Message throughput targets defined (messages/sec, not just RPS)
|
|
272
|
+
- [ ] Backpressure / flow control behavior validated
|
|
273
|
+
- [ ] Error codes and retry behavior tested (gRPC status codes, AMQP nacks, etc.)
|
|
274
|
+
- [ ] End-to-end latency measured (not just request latency)
|