Skip to content

Commit e7a5ccc

Browse files
committed
add configurable upstream connection limits
Add new --upstream.max-idle-conns and --upstream.max-conns options to configure the HTTP transport connection pool settings. - max-idle-conns: max idle connections total (default: 100) - max-conns: max connections per upstream host, 0=unlimited (default: 0) This allows users to limit concurrent connections to backend servers, preventing connection exhaustion when upstreams have limited capacity. Related to #159
1 parent 3965075 commit e7a5ccc

4 files changed

Lines changed: 115 additions & 10 deletions

File tree

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,15 @@ Reproxy allows to define system level max req/sec value for the overall system a
333333

334334
User activity limited for both matched and unmatched routes. All unmatched routes considered as a "single destination group" and get a common limiter which is `rate*3`. It means if 10 (req/sec) defined with `--throttle.user=10` the end user will be able to perform up to 30 request pers second for either static assets or unmatched routes. For matched routes this limiter maintained per destination (route), i.e. request proxied to s1.example.com/api will allow 10 r/s and the request proxied to s2.example.com will allow another 10 r/s.
335335

336+
## Upstream connection limits
337+
338+
Reproxy allows configuring upstream connection pool settings to control how many connections are maintained to backend servers:
339+
340+
- `--upstream.max-idle-conns` - Maximum number of idle connections across all upstream hosts. Default: 100.
341+
- `--upstream.max-conns` - Maximum number of connections per upstream host (0 = unlimited). Default: 0.
342+
343+
Setting `--upstream.max-conns` limits concurrent connections to each backend, which is useful when upstream servers have limited capacity or to prevent connection exhaustion.
344+
336345
## Basic auth
337346

338347
Reproxy supports basic auth for all requests. This is useful for protecting endpoints during the development and testing, before allowing unrestricted access to them. This functionality is disabled by default and not granular enough to allow for per-route auth. I.e. enabled basic auth will affect all requests.
@@ -505,6 +514,10 @@ throttle:
505514
--throttle.system= throttle overall activity' (default: 0) [$THROTTLE_SYSTEM]
506515
--throttle.user= limit req/sec per user and per proxy destination (default: 0) [$THROTTLE_USER]
507516

517+
upstream:
518+
--upstream.max-idle-conns= max idle connections total (default: 100) [$UPSTREAM_MAX_IDLE_CONNS]
519+
--upstream.max-conns= max connections per upstream host (0=unlimited) (default: 0) [$UPSTREAM_MAX_CONNS]
520+
508521
plugin:
509522
--plugin.enabled enable plugin support [$PLUGIN_ENABLED]
510523
--plugin.listen= registration listen on host:port (default: 127.0.0.1:8081) [$PLUGIN_LISTEN]

app/main.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ var opts struct {
146146
User int `long:"user" env:"USER" default:"0" description:"limit req/sec per user and per proxy destination"`
147147
} `group:"throttle" namespace:"throttle" env-namespace:"THROTTLE"`
148148

149+
Upstream struct {
150+
MaxIdleConns int `long:"max-idle-conns" env:"MAX_IDLE_CONNS" default:"100" description:"max idle connections total"`
151+
MaxConnsPerHost int `long:"max-conns" env:"MAX_CONNS" default:"0" description:"max connections per upstream host (0=unlimited)"`
152+
} `group:"upstream" namespace:"upstream" env-namespace:"UPSTREAM"`
153+
149154
Plugin struct {
150155
Enabled bool `long:"enabled" env:"ENABLED" description:"enable plugin support"`
151156
Listen string `long:"listen" env:"LISTEN" default:"127.0.0.1:8081" description:"registration listen on host:port"`
@@ -291,15 +296,17 @@ func run() error {
291296
ExpectContinue: opts.Timeouts.ExpectContinue,
292297
ResponseHeader: opts.Timeouts.ResponseHeader,
293298
},
294-
Metrics: makeMetrics(ctx, svc),
295-
Reporter: errReporter,
296-
PluginConductor: makePluginConductor(ctx),
297-
ThrottleSystem: opts.Throttle.System * 3,
298-
ThrottleUser: opts.Throttle.User,
299-
BasicAuthEnabled: len(basicAuthAllowed) > 0,
300-
BasicAuthAllowed: basicAuthAllowed,
301-
KeepHost: opts.KeepHost,
302-
OnlyFrom: makeOnlyFromMiddleware(),
299+
Metrics: makeMetrics(ctx, svc),
300+
Reporter: errReporter,
301+
PluginConductor: makePluginConductor(ctx),
302+
ThrottleSystem: opts.Throttle.System * 3,
303+
ThrottleUser: opts.Throttle.User,
304+
BasicAuthEnabled: len(basicAuthAllowed) > 0,
305+
BasicAuthAllowed: basicAuthAllowed,
306+
KeepHost: opts.KeepHost,
307+
OnlyFrom: makeOnlyFromMiddleware(),
308+
UpstreamMaxIdleConns: opts.Upstream.MaxIdleConns,
309+
UpstreamMaxConnsPerHost: opts.Upstream.MaxConnsPerHost,
303310
}
304311

305312
err = px.Run(ctx)

app/proxy/proxy.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ type Http struct { // nolint golint
5858

5959
KeepHost bool
6060

61+
UpstreamMaxIdleConns int
62+
UpstreamMaxConnsPerHost int
63+
6164
dnsResolvers []string // used to mock DNS resolvers for testing
6265
}
6366

@@ -247,7 +250,8 @@ func (h *Http) proxyHandler() http.HandlerFunc {
247250
KeepAlive: h.Timeouts.KeepAlive,
248251
}).DialContext,
249252
ForceAttemptHTTP2: true,
250-
MaxIdleConns: 100,
253+
MaxIdleConns: h.UpstreamMaxIdleConns,
254+
MaxConnsPerHost: h.UpstreamMaxConnsPerHost,
251255
IdleConnTimeout: h.Timeouts.IdleConn,
252256
TLSHandshakeTimeout: h.Timeouts.TLSHandshake,
253257
ExpectContinueTimeout: h.Timeouts.ExpectContinue,

app/proxy/proxy_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,3 +1029,84 @@ func TestHttp_discoveredServers(t *testing.T) {
10291029
res := h.discoveredServers(context.Background(), time.Millisecond)
10301030
assert.Equal(t, []string{"s1", "s2", "s3"}, res)
10311031
}
1032+
1033+
func TestHttp_UpstreamConfig(t *testing.T) {
1034+
port := rand.Intn(10000) + 40000
1035+
1036+
ds := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1037+
fmt.Fprintf(w, "response %s", r.URL.String())
1038+
}))
1039+
defer ds.Close()
1040+
1041+
svc := discovery.NewService([]discovery.Provider{
1042+
&provider.Static{Rules: []string{
1043+
"localhost,^/api/(.*)," + ds.URL + "/test/$1,",
1044+
}},
1045+
}, time.Millisecond*10)
1046+
1047+
go func() {
1048+
_ = svc.Run(context.Background())
1049+
}()
1050+
time.Sleep(50 * time.Millisecond)
1051+
1052+
t.Run("with default upstream values", func(t *testing.T) {
1053+
h := Http{
1054+
Timeouts: Timeouts{ResponseHeader: 200 * time.Millisecond},
1055+
Address: fmt.Sprintf("127.0.0.1:%d", port),
1056+
AccessLog: io.Discard,
1057+
Matcher: svc,
1058+
Metrics: mgmt.NewMetrics(),
1059+
Reporter: &ErrorReporter{Nice: true},
1060+
UpstreamMaxIdleConns: 100, // default value
1061+
UpstreamMaxConnsPerHost: 0, // unlimited, default
1062+
}
1063+
1064+
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
1065+
defer cancel()
1066+
1067+
go func() {
1068+
_ = h.Run(ctx)
1069+
}()
1070+
time.Sleep(10 * time.Millisecond)
1071+
1072+
resp, err := http.Get("http://localhost:" + strconv.Itoa(port) + "/api/something")
1073+
require.NoError(t, err)
1074+
defer resp.Body.Close()
1075+
assert.Equal(t, http.StatusOK, resp.StatusCode)
1076+
1077+
body, err := io.ReadAll(resp.Body)
1078+
require.NoError(t, err)
1079+
assert.Equal(t, "response /test/something", string(body))
1080+
})
1081+
1082+
t.Run("with custom upstream values", func(t *testing.T) {
1083+
port2 := rand.Intn(10000) + 40000
1084+
h := Http{
1085+
Timeouts: Timeouts{ResponseHeader: 200 * time.Millisecond},
1086+
Address: fmt.Sprintf("127.0.0.1:%d", port2),
1087+
AccessLog: io.Discard,
1088+
Matcher: svc,
1089+
Metrics: mgmt.NewMetrics(),
1090+
Reporter: &ErrorReporter{Nice: true},
1091+
UpstreamMaxIdleConns: 50,
1092+
UpstreamMaxConnsPerHost: 10,
1093+
}
1094+
1095+
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
1096+
defer cancel()
1097+
1098+
go func() {
1099+
_ = h.Run(ctx)
1100+
}()
1101+
time.Sleep(10 * time.Millisecond)
1102+
1103+
resp, err := http.Get("http://localhost:" + strconv.Itoa(port2) + "/api/something")
1104+
require.NoError(t, err)
1105+
defer resp.Body.Close()
1106+
assert.Equal(t, http.StatusOK, resp.StatusCode)
1107+
1108+
body, err := io.ReadAll(resp.Body)
1109+
require.NoError(t, err)
1110+
assert.Equal(t, "response /test/something", string(body))
1111+
})
1112+
}

0 commit comments

Comments
 (0)