mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: use separate HTTP clients in scale test load generators (#21288)
While scale testing, I noticed that our load generators send basically all requests to a single Coderd instance. e.g.  This is because our scale test commands create all `Runner`s using the same codersdk Client, which means they share an underlying HTTP client. With HTTP/2 a single TCP session can multiplex many different HTTP requests (including websockets). So, it creates a single TCP connection to a single coderd, and then sends all the requests down the one TCP connections. This PR modifies the `exp scaletest` load generator commands to create an independent HTTP client per `Runner`. This means that each runner will create its own TCP connection. This should help spread the load and make a more realistic test, because in a real deployment, scaled out load will be coming over different TCP connections.
This commit is contained in:
+38
-44
@@ -48,6 +48,8 @@ import (
|
||||
|
||||
const scaletestTracerName = "coder_scaletest"
|
||||
|
||||
var BypassHeader = map[string][]string{codersdk.BypassRatelimitHeader: {"true"}}
|
||||
|
||||
func (r *RootCmd) scaletestCmd() *serpent.Command {
|
||||
cmd := &serpent.Command{
|
||||
Use: "scaletest",
|
||||
@@ -691,15 +693,6 @@ func (r *RootCmd) scaletestCreateWorkspaces() *serpent.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
client.HTTPClient = &http.Client{
|
||||
Transport: &codersdk.HeaderTransport{
|
||||
Transport: http.DefaultTransport,
|
||||
Header: map[string][]string{
|
||||
codersdk.BypassRatelimitHeader: {"true"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if count <= 0 {
|
||||
return xerrors.Errorf("--count is required and must be greater than 0")
|
||||
}
|
||||
@@ -811,7 +804,13 @@ func (r *RootCmd) scaletestCreateWorkspaces() *serpent.Command {
|
||||
return xerrors.Errorf("validate config: %w", err)
|
||||
}
|
||||
|
||||
var runner harness.Runnable = createworkspaces.NewRunner(client, config)
|
||||
// use an independent client for each Runner, so they don't reuse TCP connections. This can lead to
|
||||
// requests being unbalanced among Coder instances.
|
||||
runnerClient, err := loadtestutil.DupClientCopyingHeaders(client, BypassHeader)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create runner client: %w", err)
|
||||
}
|
||||
var runner harness.Runnable = createworkspaces.NewRunner(runnerClient, config)
|
||||
if tracingEnabled {
|
||||
runner = &runnableTraceWrapper{
|
||||
tracer: tracer,
|
||||
@@ -1019,15 +1018,6 @@ func (r *RootCmd) scaletestWorkspaceUpdates() *serpent.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
client.HTTPClient = &http.Client{
|
||||
Transport: &codersdk.HeaderTransport{
|
||||
Transport: http.DefaultTransport,
|
||||
Header: map[string][]string{
|
||||
codersdk.BypassRatelimitHeader: {"true"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if workspaceCount <= 0 {
|
||||
return xerrors.Errorf("--workspace-count must be greater than 0")
|
||||
}
|
||||
@@ -1166,7 +1156,14 @@ func (r *RootCmd) scaletestWorkspaceUpdates() *serpent.Command {
|
||||
for i, config := range configs {
|
||||
name := fmt.Sprintf("workspaceupdates-%dw", config.WorkspaceCount)
|
||||
id := strconv.Itoa(i)
|
||||
var runner harness.Runnable = workspaceupdates.NewRunner(client, config)
|
||||
|
||||
// use an independent client for each Runner, so they don't reuse TCP connections. This can lead to
|
||||
// requests being unbalanced among Coder instances.
|
||||
runnerClient, err := loadtestutil.DupClientCopyingHeaders(client, BypassHeader)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create runner client: %w", err)
|
||||
}
|
||||
var runner harness.Runnable = workspaceupdates.NewRunner(runnerClient, config)
|
||||
if tracingEnabled {
|
||||
runner = &runnableTraceWrapper{
|
||||
tracer: tracer,
|
||||
@@ -1323,16 +1320,6 @@ func (r *RootCmd) scaletestWorkspaceTraffic() *serpent.Command {
|
||||
prometheusSrvClose := ServeHandler(ctx, logger, promhttp.HandlerFor(reg, promhttp.HandlerOpts{}), prometheusFlags.Address, "prometheus")
|
||||
defer prometheusSrvClose()
|
||||
|
||||
// Bypass rate limiting
|
||||
client.HTTPClient = &http.Client{
|
||||
Transport: &codersdk.HeaderTransport{
|
||||
Transport: http.DefaultTransport,
|
||||
Header: map[string][]string{
|
||||
codersdk.BypassRatelimitHeader: {"true"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
workspaces, err := targetFlags.getTargetedWorkspaces(ctx, client, me.OrganizationIDs, inv.Stdout)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1429,7 +1416,13 @@ func (r *RootCmd) scaletestWorkspaceTraffic() *serpent.Command {
|
||||
if err := config.Validate(); err != nil {
|
||||
return xerrors.Errorf("validate config: %w", err)
|
||||
}
|
||||
var runner harness.Runnable = workspacetraffic.NewRunner(client, config)
|
||||
// use an independent client for each Runner, so they don't reuse TCP connections. This can lead to
|
||||
// requests being unbalanced among Coder instances.
|
||||
runnerClient, err := loadtestutil.DupClientCopyingHeaders(client, BypassHeader)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create runner client: %w", err)
|
||||
}
|
||||
var runner harness.Runnable = workspacetraffic.NewRunner(runnerClient, config)
|
||||
if tracingEnabled {
|
||||
runner = &runnableTraceWrapper{
|
||||
tracer: tracer,
|
||||
@@ -1617,9 +1610,13 @@ func (r *RootCmd) scaletestDashboard() *serpent.Command {
|
||||
return xerrors.Errorf("create token for user: %w", err)
|
||||
}
|
||||
|
||||
userClient := codersdk.New(client.URL,
|
||||
codersdk.WithSessionToken(userTokResp.Key),
|
||||
)
|
||||
// use an independent client for each Runner, so they don't reuse TCP connections. This can lead to
|
||||
// requests being unbalanced among Coder instances.
|
||||
userClient, err := loadtestutil.DupClientCopyingHeaders(client, BypassHeader)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create runner client: %w", err)
|
||||
}
|
||||
codersdk.WithSessionToken(userTokResp.Key)(userClient)
|
||||
|
||||
config := dashboard.Config{
|
||||
Interval: interval,
|
||||
@@ -1766,15 +1763,6 @@ func (r *RootCmd) scaletestAutostart() *serpent.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
client.HTTPClient = &http.Client{
|
||||
Transport: &codersdk.HeaderTransport{
|
||||
Transport: http.DefaultTransport,
|
||||
Header: map[string][]string{
|
||||
codersdk.BypassRatelimitHeader: {"true"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if workspaceCount <= 0 {
|
||||
return xerrors.Errorf("--workspace-count must be greater than zero")
|
||||
}
|
||||
@@ -1840,7 +1828,13 @@ func (r *RootCmd) scaletestAutostart() *serpent.Command {
|
||||
if err := config.Validate(); err != nil {
|
||||
return xerrors.Errorf("validate config: %w", err)
|
||||
}
|
||||
var runner harness.Runnable = autostart.NewRunner(client, config)
|
||||
// use an independent client for each Runner, so they don't reuse TCP connections. This can lead to
|
||||
// requests being unbalanced among Coder instances.
|
||||
runnerClient, err := loadtestutil.DupClientCopyingHeaders(client, BypassHeader)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create runner client: %w", err)
|
||||
}
|
||||
var runner harness.Runnable = autostart.NewRunner(runnerClient, config)
|
||||
if tracingEnabled {
|
||||
runner = &runnableTraceWrapper{
|
||||
tracer: tracer,
|
||||
|
||||
@@ -4,18 +4,18 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/scaletest/loadtestutil"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/sloghuman"
|
||||
"github.com/coder/serpent"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/scaletest/dynamicparameters"
|
||||
"github.com/coder/coder/v2/scaletest/harness"
|
||||
)
|
||||
@@ -72,15 +72,6 @@ func (r *RootCmd) scaletestDynamicParameters() *serpent.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
client.HTTPClient = &http.Client{
|
||||
Transport: &codersdk.HeaderTransport{
|
||||
Transport: http.DefaultTransport,
|
||||
Header: map[string][]string{
|
||||
codersdk.BypassRatelimitHeader: {"true"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
reg := prometheus.NewRegistry()
|
||||
metrics := dynamicparameters.NewMetrics(reg, "concurrent_evaluations")
|
||||
|
||||
@@ -122,7 +113,13 @@ func (r *RootCmd) scaletestDynamicParameters() *serpent.Command {
|
||||
Metrics: metrics,
|
||||
MetricLabelValues: []string{fmt.Sprintf("%d", part.ConcurrentEvaluations)},
|
||||
}
|
||||
var runner harness.Runnable = dynamicparameters.NewRunner(client, cfg)
|
||||
// use an independent client for each Runner, so they don't reuse TCP connections. This can lead to
|
||||
// requests being unbalanced among Coder instances.
|
||||
runnerClient, err := loadtestutil.DupClientCopyingHeaders(client, BypassHeader)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create runner client: %w", err)
|
||||
}
|
||||
var runner harness.Runnable = dynamicparameters.NewRunner(runnerClient, cfg)
|
||||
if tracingEnabled {
|
||||
runner = &runnableTraceWrapper{
|
||||
tracer: tracer,
|
||||
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/scaletest/loadtestutil"
|
||||
|
||||
"cdr.dev/slog"
|
||||
|
||||
notificationsLib "github.com/coder/coder/v2/coderd/notifications"
|
||||
@@ -66,15 +68,6 @@ func (r *RootCmd) scaletestNotifications() *serpent.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
client.HTTPClient = &http.Client{
|
||||
Transport: &codersdk.HeaderTransport{
|
||||
Transport: http.DefaultTransport,
|
||||
Header: map[string][]string{
|
||||
codersdk.BypassRatelimitHeader: {"true"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if userCount <= 0 {
|
||||
return xerrors.Errorf("--user-count must be greater than 0")
|
||||
}
|
||||
@@ -206,7 +199,13 @@ func (r *RootCmd) scaletestNotifications() *serpent.Command {
|
||||
for i, config := range configs {
|
||||
id := strconv.Itoa(i)
|
||||
name := fmt.Sprintf("notifications-%s", id)
|
||||
var runner harness.Runnable = notifications.NewRunner(client, config)
|
||||
// use an independent client for each Runner, so they don't reuse TCP connections. This can lead to
|
||||
// requests being unbalanced among Coder instances.
|
||||
runnerClient, err := loadtestutil.DupClientCopyingHeaders(client, BypassHeader)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create runner client: %w", err)
|
||||
}
|
||||
var runner harness.Runnable = notifications.NewRunner(runnerClient, config)
|
||||
if tracingEnabled {
|
||||
runner = &runnableTraceWrapper{
|
||||
tracer: tracer,
|
||||
|
||||
@@ -4,7 +4,6 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"sync"
|
||||
@@ -14,6 +13,8 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/scaletest/loadtestutil"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/scaletest/harness"
|
||||
"github.com/coder/coder/v2/scaletest/prebuilds"
|
||||
@@ -56,15 +57,6 @@ func (r *RootCmd) scaletestPrebuilds() *serpent.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
client.HTTPClient = &http.Client{
|
||||
Transport: &codersdk.HeaderTransport{
|
||||
Transport: http.DefaultTransport,
|
||||
Header: map[string][]string{
|
||||
codersdk.BypassRatelimitHeader: {"true"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if numTemplates <= 0 {
|
||||
return xerrors.Errorf("--num-templates must be greater than 0")
|
||||
}
|
||||
@@ -140,7 +132,13 @@ func (r *RootCmd) scaletestPrebuilds() *serpent.Command {
|
||||
return xerrors.Errorf("validate config: %w", err)
|
||||
}
|
||||
|
||||
var runner harness.Runnable = prebuilds.NewRunner(client, cfg)
|
||||
// use an independent client for each Runner, so they don't reuse TCP connections. This can lead to
|
||||
// requests being unbalanced among Coder instances.
|
||||
runnerClient, err := loadtestutil.DupClientCopyingHeaders(client, BypassHeader)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create runner client: %w", err)
|
||||
}
|
||||
var runner harness.Runnable = prebuilds.NewRunner(runnerClient, cfg)
|
||||
if tracingEnabled {
|
||||
runner = &runnableTraceWrapper{
|
||||
tracer: tracer,
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/scaletest/loadtestutil"
|
||||
|
||||
"cdr.dev/slog"
|
||||
"cdr.dev/slog/sloggers/sloghuman"
|
||||
"github.com/coder/serpent"
|
||||
@@ -143,7 +145,13 @@ After all runners connect, it waits for the baseline duration before triggering
|
||||
return xerrors.Errorf("validate config for runner %d: %w", i, err)
|
||||
}
|
||||
|
||||
var runner harness.Runnable = taskstatus.NewRunner(client, cfg)
|
||||
// use an independent client for each Runner, so they don't reuse TCP connections. This can lead to
|
||||
// requests being unbalanced among Coder instances.
|
||||
runnerClient, err := loadtestutil.DupClientCopyingHeaders(client, BypassHeader)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("create runner client: %w", err)
|
||||
}
|
||||
var runner harness.Runnable = taskstatus.NewRunner(runnerClient, cfg)
|
||||
if tracingEnabled {
|
||||
runner = &runnableTraceWrapper{
|
||||
tracer: tracer,
|
||||
|
||||
Reference in New Issue
Block a user