Documentation
¶
Overview ¶
Package go_loadgen provides utilities for load testing that is protocol-agnostic.
This library allows you to test any HTTP, gRPC, or other services based on a simple API: to test a service, you need to define a client, a data provider, and a collector. Go Loadgen then takes care of executing the workload and collecting the results. It also provides a simple workload-pattern generator that can generate a workload based on a configuration if you want to create a workload with many different phases which have different RPS. The pattern generator supports weighted time allocation, allowing you to control how much time each pattern takes up in your workload.
Motivation ¶
This library was created as an alternative to k6 for longer workloads where resource usage was too high, and as an alternative to ghz which stores all results in memory and writes them to disk at the end. This library provides full flexibility to implement your own collector.
Features ¶
- Protocol-agnostic
- Type-safe using go generics
- Support for constant and variable RPS
- Support for workload-pattern generation with weighted time allocation
Example ¶
Say you want to test a gRPC server's "/increment" endpoint with a variable RPS:
// First, define your data model: a request and a response.
type endpointRequest struct {
// We need to send a delta to the endpoint to change the counter
Delta int32 `json:"delta"`
}
type endpointResponse struct {
// We'd like to track the latency of the request
Latency time.Duration `json:"latency"`
// We'd like to track the counter value
Counter int32 `json:"counter"`
// We'd like to track any errors that occur
Error string `json:"error,omitempty"`
}
// Then, define your client and data provider. Your client will be used to send
// the request to the server, it must implement the Client interface.
type myClient struct{}
// CallEndpoint will be called by executors using data (endpointRequest) provided
// by our data provider.
func (c *myClient) CallEndpoint(ctx context.Context, req endpointRequest) endpointResponse {
// We can track the latency of the request
startTime := time.Now()
body, err := json.Marshal(req)
if err != nil {
return endpointResponse{Error: err.Error()}
}
// We can send an http request to a server.
resp, err := http.Post("http://localhost:8080/increment", "application/json", bytes.NewBuffer(body))
if err != nil {
return endpointResponse{Error: err.Error()}
}
defer resp.Body.Close()
var respBody endpointResponse
if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
return endpointResponse{Error: err.Error()}
}
respBody.Latency = time.Since(startTime)
return respBody
}
// Your data provider will provide the data to be sent to the server in each request.
// It must be thread safe.
type myDataProvider struct{}
func (d *myDataProvider) GetData() endpointRequest {
// We'll increment the counter by 1 each time
return endpointRequest{Delta: 1}
}
// Then, define your collector. For this we can use the CSVCollector. We can also
// provide a flush interval, which will be used to flush the collector to the disk
// every flushInterval.
collector, err := go_loadgen.NewCSVCollector[endpointResponse]("results.csv", 1*time.Second)
if err != nil {
log.Fatalf("Failed to create collector: %v", err)
}
defer collector.Close()
// With all of this, we can create a new EndpointWorkload.
ew, err := go_loadgen.NewEndpointWorkload(
"increment",
&go_loadgen.Config{
GenerateWorkload: false,
MaxDuration: 20 * time.Second,
Phases: []go_loadgen.TestPhase{
{
Name: "increment",
// constant RPS
Type: "constant",
StartTime: 0,
Duration: 10 * time.Second,
StartRPS: 1,
Step: 1,
},
{
Name: "increment",
// variable RPS. Increments by 10 every second
Type: "variable",
StartTime: 10 * time.Second,
Duration: 10 * time.Second,
StartRPS: 10,
EndRPS: 100,
Step: 10,
},
},
},
&myClient{},
&myDataProvider{},
collector,
)
if err != nil {
log.Fatalf("Failed to create endpoint workload: %v", err)
}
// The workload will run until the max duration is reached or the workload is stopped.
// All of the results will be collected and written to the CSV file.
ew.Run()
This simple library gives you flexibility to test any service and avoid rewriting the same executor code each time.
Index ¶
Constants ¶
const MIN_INTERVAL = 10 * time.Millisecond
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type CSVCollector ¶
type CSVCollector[R CSVSerializable] struct { // contains filtered or unexported fields }
CSVCollector can collect results and write them to a CSV file. It requires result types to implement CSVSerializable. It will write the headers on the first collect and then every flushInterval. Note that headers will be rewritten if a new collector is created.
func NewCSVCollector ¶
func NewCSVCollector[R CSVSerializable](filePath string, flushInterval time.Duration) (*CSVCollector[R], error)
NewCSVCollector creates a new CSV collector and starts a goroutine to flush the collector every flushInterval.
func (*CSVCollector[R]) Close ¶
func (c *CSVCollector[R]) Close()
Close flushes the CSV collector and closes the file.
func (*CSVCollector[R]) Collect ¶
func (c *CSVCollector[R]) Collect(result R)
Collect collects a result and writes it to the CSV file.
func (*CSVCollector[R]) RunFlush ¶
func (c *CSVCollector[R]) RunFlush(ctx context.Context)
RunFlush flushes the CSV collector every flushInterval.
type CSVSerializable ¶
type CSVSerializable interface {
// CSVHeaders returns the headers that should be used for a CSV file.
CSVHeaders() []string
// CSVRecord returns the record that should be used to store the struct as a row in a CSV file.
CSVRecord() []string
}
CSVSerializable is a struct that can be serialized to CSV
type Client ¶
type Client[C any, R any] interface { // CallEndpoint should send a request using the provided data to the endpoint and return a result. CallEndpoint(ctx context.Context, req C) R }
A Client is a generic interface that can be used to call an endpoint.
type Collector ¶
type Collector[R any] interface { // Collect should collect the result from a client. Collect(result R) Close() }
A Collector is a generic interface that can be used to collect results from a client. Users are free to implement their own mechanism for saving results to disk. Executors will call Collector.Collect() concurrently , and Collector.Close() will be called after all results are collected.
type ConstantExecutor ¶
A ConstantExecutor is a LoadExecutor that executes a workload of TestPhases with a constant RPS.
func NewConstantExecutor ¶
func NewConstantExecutor[C any, R any]( client Client[C, R], collector Collector[R], dataProvider DataProvider[C], ) *ConstantExecutor[C, R]
NewConstantExecutor creates a new ConstantExecutor.
func (*ConstantExecutor[C, R]) Execute ¶
func (e *ConstantExecutor[C, R]) Execute(ctx context.Context, phase TestPhase)
Execute executes a workload of TestPhases with a constant RPS.
func (*ConstantExecutor[C, R]) Stop ¶
func (e *ConstantExecutor[C, R]) Stop()
Stop stops the ConstantExecutor.
type DataProvider ¶
type DataProvider[C any] interface { // GetData should return a data object for a request. It must be thread safe. GetData() C }
A DataProvider is a generic interface that can be used to get data for a request. Users are free to implement their own mechanism for getting data. Executors will call DataProvider.GetData() concurrently.
type EndpointWorkload ¶
type EndpointWorkload[C any, R any] struct { Name string Config *Config Client Client[C, R] DataProvider DataProvider[C] Collector Collector[R] }
An EndpointWorkload can execute a workload of TestPhases on a provided client. Users must provide their own implementation of Client, DataProvider, and Collector, which all use the same input and output types.
func NewEndpointWorkload ¶
func NewEndpointWorkload[C any, R any](name string, config *Config, client Client[C, R], dataProvider DataProvider[C], collector Collector[R]) (*EndpointWorkload[C, R], error)
NewEndpointWorkload creates a new EndpointWorkload. If GenerateWorkload is true, the workload will be generated using the provided patterns. If GenerateWorkload is false, the workload will be executed using the TestPhases in the Config.
func (*EndpointWorkload[C, R]) Run ¶
func (e *EndpointWorkload[C, R]) Run()
Run executes the workload according to the TestPhases in the Config.
type LoadExecutor ¶
A LoadExecutor is a generic interface that can be used to execute a workload of TestPhases.
type PhaseParameters ¶
type PhasePattern ¶
type PhasePattern struct {
Name string `yaml:"name"`
PhaseCount IntRange `yaml:"phase_count"`
ConstantLikelihood float64 `yaml:"constant_likelihood"` // 0.0-1.0
RampingLikelihood float64 `yaml:"ramping_likelihood"` // 0.0-1.0
// What percentage of the total workload time this pattern should take up. 0.0-1.0
// Note: all patterns must sum to 1.0. If not provided, all patterns will be weighted equally.
Weight float64 `yaml:"weight"`
// Knobs to affect the generated workload phase
Parameters PhaseParameters `yaml:"parameters"`
// contains filtered or unexported fields
}
PhasePattern is a template for a workload phase. It is used to generate a workload of TestPhases.
type RampingExecutor ¶
A RampingExecutor is a LoadExecutor that executes a workload of TestPhases with variable RPS.
func NewRampingExecutor ¶
func NewRampingExecutor[C any, R any]( client Client[C, R], collector Collector[R], dataProvider DataProvider[C], ) *RampingExecutor[C, R]
NewRampingExecutor creates a new RampingExecutor.
func (*RampingExecutor[C, R]) Execute ¶
func (e *RampingExecutor[C, R]) Execute(ctx context.Context, phase TestPhase)
Execute executes a workload of TestPhases with a variable RPS.
func (*RampingExecutor[C, R]) Stop ¶
func (e *RampingExecutor[C, R]) Stop()
Stop stops the RampingExecutor.
type TestPhase ¶
type TestPhase struct {
Name string `yaml:"name"`
// "constant" | "variable"
Type string `yaml:"type"`
// Starting time of the phase, relative to workload start
StartTime time.Duration `yaml:"start_time"`
// Total duration of the phase
Duration time.Duration `yaml:"duration"`
// Starting RPS for the phase. If constant, this is also the constant RPS.
StartRPS int `yaml:"start_rps"`
// Maximum end RPS for the phase, which may not be reached if max duration is reached
// or step is too small.
EndRPS int `yaml:"end_rps,omitempty"`
// Step increment for the RPS. If variable, this is the increment per second.
Step int `yaml:"step,omitempty"`
}
type WorkloadPatternGenerator ¶
type WorkloadPatternGenerator struct {
// contains filtered or unexported fields
}
func NewWorkloadPatternGenerator ¶
func NewWorkloadPatternGenerator(seed int64, maxDuration time.Duration, patterns []*PhasePattern) *WorkloadPatternGenerator
func (*WorkloadPatternGenerator) GenerateWorkload ¶
func (g *WorkloadPatternGenerator) GenerateWorkload() ([]TestPhase, error)
Generates a workload for the given patterns.