mirror of
https://github.com/VictoriaMetrics/VictoriaMetrics.git
synced 2026-07-31 13:02:44 +03:00
Compare commits
1 Commits
dependabot
...
vmauth-bac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5d994b436 |
@@ -73,22 +73,15 @@ type UserInfo struct {
|
||||
Username string `yaml:"username,omitempty"`
|
||||
Password string `yaml:"password,omitempty"`
|
||||
|
||||
URLPrefix *URLPrefix `yaml:"url_prefix,omitempty"`
|
||||
DiscoverBackendIPs *bool `yaml:"discover_backend_ips,omitempty"`
|
||||
URLMaps []URLMap `yaml:"url_map,omitempty"`
|
||||
DumpRequestOnErrors bool `yaml:"dump_request_on_errors,omitempty"`
|
||||
HeadersConf HeadersConf `yaml:",inline"`
|
||||
MaxConcurrentRequests int `yaml:"max_concurrent_requests,omitempty"`
|
||||
DefaultURL *URLPrefix `yaml:"default_url,omitempty"`
|
||||
RetryStatusCodes []int `yaml:"retry_status_codes,omitempty"`
|
||||
LoadBalancingPolicy string `yaml:"load_balancing_policy,omitempty"`
|
||||
MergeQueryArgs []string `yaml:"merge_query_args,omitempty"`
|
||||
DropSrcPathPrefixParts *int `yaml:"drop_src_path_prefix_parts,omitempty"`
|
||||
TLSCAFile string `yaml:"tls_ca_file,omitempty"`
|
||||
TLSCertFile string `yaml:"tls_cert_file,omitempty"`
|
||||
TLSKeyFile string `yaml:"tls_key_file,omitempty"`
|
||||
TLSServerName string `yaml:"tls_server_name,omitempty"`
|
||||
TLSInsecureSkipVerify *bool `yaml:"tls_insecure_skip_verify,omitempty"`
|
||||
URLPrefix *URLPrefix `yaml:"url_prefix,omitempty"`
|
||||
URLMaps []URLMap `yaml:"url_map,omitempty"`
|
||||
DumpRequestOnErrors bool `yaml:"dump_request_on_errors,omitempty"`
|
||||
HeadersConf HeadersConf `yaml:",inline"`
|
||||
BackendSettings BackendSettings `yaml:",inline"`
|
||||
DefaultURL *URLPrefix `yaml:"default_url,omitempty"`
|
||||
RetryStatusCodes []int `yaml:"retry_status_codes,omitempty"`
|
||||
MergeQueryArgs []string `yaml:"merge_query_args,omitempty"`
|
||||
DropSrcPathPrefixParts *int `yaml:"drop_src_path_prefix_parts,omitempty"`
|
||||
|
||||
MetricLabels map[string]string `yaml:"metric_labels,omitempty"`
|
||||
|
||||
@@ -154,6 +147,19 @@ type HeadersConf struct {
|
||||
hasAnyPlaceHolders bool
|
||||
}
|
||||
|
||||
// BackendSettings holds settings shared between UserInfo and BackendGroupConfig for controlling
|
||||
// how vmauth selects and connects to backends.
|
||||
type BackendSettings struct {
|
||||
LoadBalancingPolicy string `yaml:"load_balancing_policy,omitempty"`
|
||||
DiscoverBackendIPs *bool `yaml:"discover_backend_ips,omitempty"`
|
||||
MaxConcurrentRequests int `yaml:"max_concurrent_requests,omitempty"`
|
||||
TLSCAFile string `yaml:"tls_ca_file,omitempty"`
|
||||
TLSCertFile string `yaml:"tls_cert_file,omitempty"`
|
||||
TLSKeyFile string `yaml:"tls_key_file,omitempty"`
|
||||
TLSServerName string `yaml:"tls_server_name,omitempty"`
|
||||
TLSInsecureSkipVerify *bool `yaml:"tls_insecure_skip_verify,omitempty"`
|
||||
}
|
||||
|
||||
func (ui *UserInfo) beginConcurrencyLimit(ctx context.Context) error {
|
||||
select {
|
||||
case ui.concurrencyLimitCh <- struct{}{}:
|
||||
@@ -185,7 +191,7 @@ func (ui *UserInfo) endConcurrencyLimit() {
|
||||
}
|
||||
|
||||
func (ui *UserInfo) getMaxConcurrentRequests() int {
|
||||
mcr := ui.MaxConcurrentRequests
|
||||
mcr := ui.BackendSettings.MaxConcurrentRequests
|
||||
if mcr <= 0 {
|
||||
mcr = *maxConcurrentPerUserRequests
|
||||
}
|
||||
@@ -340,20 +346,30 @@ type URLPrefix struct {
|
||||
// how many request path prefix parts to drop before routing the request to backendURL
|
||||
dropSrcPathPrefixParts int
|
||||
|
||||
// busOriginal contains the original list of backends specified in yaml config.
|
||||
busOriginal []*url.URL
|
||||
// busOriginal contains the original list of backend groups specified in yaml config.
|
||||
busOriginal []*backendGroupSpec
|
||||
|
||||
// n is an atomic counter, which is used for balancing load among available backends.
|
||||
n atomic.Uint32
|
||||
|
||||
// backendGroupCounters holds one atomic counter per busOriginal entry, used for balancing load.
|
||||
backendGroupCounters []atomic.Uint32
|
||||
|
||||
// the list of backend urls
|
||||
//
|
||||
// the list can be dynamically updated if `discover_backend_ips` option is set.
|
||||
bus atomic.Pointer[backendURLs]
|
||||
|
||||
// if this option is set, then backend ips for busOriginal are periodically re-discovered and put to bus.
|
||||
// if this option is set by default, then backend ips for busOriginal are periodically re-discovered and put to bus.
|
||||
//
|
||||
// individual busOriginal entries may override this via their own discover_backend_ips setting.
|
||||
discoverBackendIPs bool
|
||||
|
||||
// hasAnyBackendDiscovery is true if discovery is effectively enabled for at least one busOriginal entry.
|
||||
// It is computed once in sanitizeAndInitialize, so discoverBackendAddrsIfNeeded can cheaply skip
|
||||
// the whole discovery machinery on the common no-discovery path.
|
||||
hasAnyBackendDiscovery bool
|
||||
|
||||
// The next deadline for DNS-based discovery of backend IPs
|
||||
nextDiscoveryDeadline atomic.Uint64
|
||||
|
||||
@@ -361,21 +377,208 @@ type URLPrefix struct {
|
||||
vOriginal any
|
||||
}
|
||||
|
||||
func (up *URLPrefix) setLoadBalancingPolicy(loadBalancingPolicy string) error {
|
||||
switch loadBalancingPolicy {
|
||||
case "", // empty string is equivalent to least_loaded
|
||||
"least_loaded",
|
||||
"first_available":
|
||||
up.loadBalancingPolicy = loadBalancingPolicy
|
||||
return nil
|
||||
// backendGroupSpec represents a single parsed `url_prefix` list item.
|
||||
//
|
||||
// It is built either from a plain url string (no overrides, inherits everything from the
|
||||
// enclosing scope) or from a BackendGroupConfig mapping.
|
||||
type backendGroupSpec struct {
|
||||
// name identifies this group in metrics. Falls back to its ordinal position in url_prefix when empty.
|
||||
name string
|
||||
|
||||
urls []*url.URL
|
||||
|
||||
// per-group overrides; zero values mean "inherit from the enclosing url_prefix / user / url_map scope".
|
||||
loadBalancingPolicy string
|
||||
discoverBackendIPs *bool
|
||||
maxConcurrentRequests int
|
||||
tlsCAFile string
|
||||
tlsCertFile string
|
||||
tlsKeyFile string
|
||||
tlsServerName string
|
||||
tlsInsecureSkipVerify *bool
|
||||
}
|
||||
|
||||
func (spec *backendGroupSpec) hasTLSOverride() bool {
|
||||
return spec.tlsCAFile != "" || spec.tlsCertFile != "" || spec.tlsKeyFile != "" ||
|
||||
spec.tlsServerName != "" || spec.tlsInsecureSkipVerify != nil
|
||||
}
|
||||
|
||||
// BackendGroupConfig represents a `url_prefix` list item specified as a YAML mapping instead of a plain string.
|
||||
//
|
||||
// It lets a group of backend urls override load_balancing_policy, discover_backend_ips,
|
||||
// max_concurrent_requests and tls_* settings, which are otherwise inherited from the enclosing
|
||||
// `user` / `url_map` scope. See https://docs.victoriametrics.com/victoriametrics/vmauth/#load-balancing
|
||||
type BackendGroupConfig struct {
|
||||
// Name optionally identifies this backend group in metrics. Falls back to its ordinal
|
||||
// position in url_prefix when not set.
|
||||
Name string `yaml:"name,omitempty"`
|
||||
URLPrefix stringOrSlice `yaml:"url_prefix"`
|
||||
BackendSettings BackendSettings `yaml:",inline"`
|
||||
}
|
||||
|
||||
// stringOrSlice unmarshals a YAML value that is either a single string or a list of strings.
|
||||
type stringOrSlice []string
|
||||
|
||||
func (s *stringOrSlice) UnmarshalYAML(f func(any) error) error {
|
||||
var v any
|
||||
if err := f(&v); err != nil {
|
||||
return err
|
||||
}
|
||||
urls, err := parseStringOrSliceValue(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot unmarshal `url_prefix`: %w", err)
|
||||
}
|
||||
*s = urls
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseStringOrSliceValue(v any) ([]string, error) {
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
return []string{x}, nil
|
||||
case []any:
|
||||
if len(x) == 0 {
|
||||
return nil, fmt.Errorf("must contain at least a single url")
|
||||
}
|
||||
us := make([]string, len(x))
|
||||
for i, xx := range x {
|
||||
s, ok := xx.(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("must contain array of strings; got %T", xx)
|
||||
}
|
||||
us[i] = s
|
||||
}
|
||||
return us, nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected load_balancing_policy: %q; want least_loaded or first_available", loadBalancingPolicy)
|
||||
return nil, fmt.Errorf("unexpected type: %T; want string or []string", v)
|
||||
}
|
||||
}
|
||||
|
||||
func validateLoadBalancingPolicyValue(policy string) error {
|
||||
switch policy {
|
||||
case "", // empty string is equivalent to least_loaded
|
||||
"least_loaded",
|
||||
"first_available":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unexpected load_balancing_policy: %q; want least_loaded or first_available", policy)
|
||||
}
|
||||
}
|
||||
|
||||
func (up *URLPrefix) setLoadBalancingPolicy(loadBalancingPolicy string) error {
|
||||
if err := validateLoadBalancingPolicyValue(loadBalancingPolicy); err != nil {
|
||||
return err
|
||||
}
|
||||
up.loadBalancingPolicy = loadBalancingPolicy
|
||||
return nil
|
||||
}
|
||||
|
||||
// backendUserSettings holds the resolved user-level settings needed for building backendURLGroups.
|
||||
//
|
||||
// It lets URLPrefix.sanitizeAndInitialize apply per-group overrides (load_balancing_policy, tls_*,
|
||||
// max_concurrent_requests) on top of the settings inherited from the enclosing user.
|
||||
type backendUserSettings struct {
|
||||
tlsCAFile string
|
||||
tlsCertFile string
|
||||
tlsKeyFile string
|
||||
tlsServerName string
|
||||
tlsInsecureSkipVerify *bool
|
||||
|
||||
ms *metrics.Set
|
||||
metricLabels string
|
||||
}
|
||||
|
||||
func backendGroupMetricLabels(userLabels, groupID string) string {
|
||||
label := fmt.Sprintf(`backend_group=%q`, groupID)
|
||||
if userLabels == "" {
|
||||
return "{" + label + "}"
|
||||
}
|
||||
return userLabels[:len(userLabels)-1] + "," + label + "}"
|
||||
}
|
||||
|
||||
type backendURLs struct {
|
||||
bhc backendHealthCheck
|
||||
bhc backendHealthCheck
|
||||
n *atomic.Uint32
|
||||
groups []*backendURLGroup
|
||||
}
|
||||
|
||||
// backendURLGroup holds the backend urls discovered for a single busOriginal entry.
|
||||
type backendURLGroup struct {
|
||||
// n is an atomic counter, which is used for balancing load among bus.
|
||||
//
|
||||
// It points into URLPrefix.backendGroupCounters, so it survives across the
|
||||
// group being recreated by backend IP rediscovery.
|
||||
n *atomic.Uint32
|
||||
|
||||
bus []*backendURL
|
||||
|
||||
loadBalancingPolicy string
|
||||
|
||||
// rt is a per-group HTTP RoundTripper. nil means inherit the enclosing user's RoundTripper.
|
||||
rt http.RoundTripper
|
||||
|
||||
// concurrencyLimitCh is a per-group concurrency limiter. nil means no group-level limit is configured.
|
||||
concurrencyLimitCh chan struct{}
|
||||
concurrencyLimitReached *metrics.Counter
|
||||
}
|
||||
|
||||
func (g *backendURLGroup) isBroken() bool {
|
||||
for _, bu := range g.bus {
|
||||
if !bu.isBroken() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *backendURLGroup) isAtConcurrencyLimit() bool {
|
||||
return g.concurrencyLimitCh != nil && len(g.concurrencyLimitCh) >= cap(g.concurrencyLimitCh)
|
||||
}
|
||||
|
||||
func (g *backendURLGroup) isUnavailable() bool {
|
||||
return g.isBroken() || g.isAtConcurrencyLimit()
|
||||
}
|
||||
|
||||
func (g *backendURLGroup) minConcurrentRequests() int32 {
|
||||
minReqs := int32(math.MaxInt32)
|
||||
for _, bu := range g.bus {
|
||||
if bu.isBroken() {
|
||||
continue
|
||||
}
|
||||
if n := bu.concurrentRequests.Load(); n < minReqs {
|
||||
minReqs = n
|
||||
}
|
||||
}
|
||||
return minReqs
|
||||
}
|
||||
|
||||
func (g *backendURLGroup) getBackendURL() *backendURL {
|
||||
if g.loadBalancingPolicy == "first_available" {
|
||||
return g.getFirstAvailable()
|
||||
}
|
||||
return g.getLeastLoaded()
|
||||
}
|
||||
|
||||
func (g *backendURLGroup) beginConcurrencyLimit() bool {
|
||||
if g.concurrencyLimitCh == nil {
|
||||
return true
|
||||
}
|
||||
select {
|
||||
case g.concurrencyLimitCh <- struct{}{}:
|
||||
return true
|
||||
default:
|
||||
if g.concurrencyLimitReached != nil {
|
||||
g.concurrencyLimitReached.Inc()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (g *backendURLGroup) endConcurrencyLimit() {
|
||||
if g.concurrencyLimitCh == nil {
|
||||
return
|
||||
}
|
||||
<-g.concurrencyLimitCh
|
||||
}
|
||||
|
||||
type backendHealthCheck struct {
|
||||
@@ -404,22 +607,34 @@ func (bhc *backendHealthCheck) stop() {
|
||||
bhc.wg.Wait()
|
||||
}
|
||||
|
||||
func newBackendURLs() *backendURLs {
|
||||
func newBackendURLs(n *atomic.Uint32) *backendURLs {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &backendURLs{
|
||||
bhc: backendHealthCheck{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
},
|
||||
n: n,
|
||||
}
|
||||
}
|
||||
|
||||
func (bus *backendURLs) add(u *url.URL) {
|
||||
bus.bus = append(bus.bus, &backendURL{
|
||||
url: u,
|
||||
bhc: &bus.bhc,
|
||||
hasPlaceHolders: hasAnyPlaceholders(u),
|
||||
})
|
||||
// addGroup appends a new backendURLGroup to bus, containing a backendURL for every url in urls,
|
||||
// and returns the newly created group so the caller can apply per-group settings to it.
|
||||
func (bus *backendURLs) addGroup(urls []*url.URL, n *atomic.Uint32) *backendURLGroup {
|
||||
g := &backendURLGroup{
|
||||
n: n,
|
||||
bus: make([]*backendURL, len(urls)),
|
||||
}
|
||||
for i, u := range urls {
|
||||
g.bus[i] = &backendURL{
|
||||
url: u,
|
||||
bhc: &bus.bhc,
|
||||
hasPlaceHolders: hasAnyPlaceholders(u),
|
||||
group: g,
|
||||
}
|
||||
}
|
||||
bus.groups = append(bus.groups, g)
|
||||
return g
|
||||
}
|
||||
|
||||
func (bus *backendURLs) stopHealthChecks() {
|
||||
@@ -436,6 +651,8 @@ type backendURL struct {
|
||||
url *url.URL
|
||||
|
||||
hasPlaceHolders bool
|
||||
|
||||
group *backendURLGroup
|
||||
}
|
||||
|
||||
func (bu *backendURL) isBroken() bool {
|
||||
@@ -495,7 +712,11 @@ func (bu *backendURL) put() {
|
||||
|
||||
func (up *URLPrefix) getBackendsCount() int {
|
||||
bus := up.bus.Load()
|
||||
return len(bus.bus)
|
||||
n := 0
|
||||
for _, g := range bus.groups {
|
||||
n += len(g.bus)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// getBackendURL returns the backendURL depending on the load balance policy.
|
||||
@@ -507,19 +728,34 @@ func (up *URLPrefix) getBackendURL() *backendURL {
|
||||
up.discoverBackendAddrsIfNeeded()
|
||||
|
||||
bus := up.bus.Load()
|
||||
if len(bus.bus) == 0 {
|
||||
if len(bus.groups) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var g *backendURLGroup
|
||||
if up.loadBalancingPolicy == "first_available" {
|
||||
return getFirstAvailableBackendURL(bus.bus)
|
||||
g = bus.getFirstAvailable()
|
||||
} else {
|
||||
g = bus.getLeastLoaded()
|
||||
}
|
||||
return getLeastLoadedBackendURL(bus.bus, &up.n)
|
||||
if len(g.bus) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return g.getBackendURL()
|
||||
}
|
||||
|
||||
// effectiveDiscoverBackendIPs returns whether backend IP discovery is enabled for spec,
|
||||
// taking into account spec's own override of up.discoverBackendIPs.
|
||||
func (up *URLPrefix) effectiveDiscoverBackendIPs(spec *backendGroupSpec) bool {
|
||||
if spec.discoverBackendIPs != nil {
|
||||
return *spec.discoverBackendIPs
|
||||
}
|
||||
return up.discoverBackendIPs
|
||||
}
|
||||
|
||||
func (up *URLPrefix) discoverBackendAddrsIfNeeded() {
|
||||
if !up.discoverBackendIPs {
|
||||
// The discovery is disabled.
|
||||
if !up.hasAnyBackendDiscovery {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -540,66 +776,89 @@ func (up *URLPrefix) discoverBackendAddrsIfNeeded() {
|
||||
return
|
||||
}
|
||||
|
||||
// Discover ips for all the backendURLs
|
||||
// Discover ips for all the backendURLs which need it.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(intervalSec))
|
||||
hostToAddrs := make(map[string][]string)
|
||||
for _, bu := range up.busOriginal {
|
||||
host := bu.Hostname()
|
||||
port := bu.Port()
|
||||
if hostToAddrs[host] != nil {
|
||||
// ips for the given host have been already discovered
|
||||
for _, spec := range up.busOriginal {
|
||||
if !up.effectiveDiscoverBackendIPs(spec) {
|
||||
continue
|
||||
}
|
||||
for _, bu := range spec.urls {
|
||||
host := bu.Hostname()
|
||||
port := bu.Port()
|
||||
if hostToAddrs[host] != nil {
|
||||
// ips for the given host have been already discovered
|
||||
continue
|
||||
}
|
||||
|
||||
var resolvedAddrs []string
|
||||
if strings.HasPrefix(host, "srv+") {
|
||||
// The host has the format 'srv+realhost'. Strip 'srv+' prefix before performing the lookup.
|
||||
srvHost := strings.TrimPrefix(host, "srv+")
|
||||
_, addrs, err := netutil.Resolver.LookupSRV(ctx, "", "", srvHost)
|
||||
if err != nil {
|
||||
logger.Warnf("cannot discover backend SRV records for %s: %s; use it literally", bu, err)
|
||||
resolvedAddrs = []string{host}
|
||||
} else {
|
||||
resolvedAddrs = make([]string, len(addrs))
|
||||
for i, addr := range addrs {
|
||||
hostPort := port
|
||||
if hostPort == "" && addr.Port > 0 {
|
||||
hostPort = strconv.FormatUint(uint64(addr.Port), 10)
|
||||
var resolvedAddrs []string
|
||||
if strings.HasPrefix(host, "srv+") {
|
||||
// The host has the format 'srv+realhost'. Strip 'srv+' prefix before performing the lookup.
|
||||
srvHost := strings.TrimPrefix(host, "srv+")
|
||||
_, addrs, err := netutil.Resolver.LookupSRV(ctx, "", "", srvHost)
|
||||
if err != nil {
|
||||
logger.Warnf("cannot discover backend SRV records for %s: %s; use it literally", bu, err)
|
||||
resolvedAddrs = []string{host}
|
||||
} else {
|
||||
resolvedAddrs = make([]string, len(addrs))
|
||||
for i, addr := range addrs {
|
||||
hostPort := port
|
||||
if hostPort == "" && addr.Port > 0 {
|
||||
hostPort = strconv.FormatUint(uint64(addr.Port), 10)
|
||||
}
|
||||
resolvedAddrs[i] = net.JoinHostPort(addr.Target, hostPort)
|
||||
}
|
||||
resolvedAddrs[i] = net.JoinHostPort(addr.Target, hostPort)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addrs, err := netutil.Resolver.LookupIPAddr(ctx, host)
|
||||
if err != nil {
|
||||
logger.Warnf("cannot discover backend IPs for %s: %s; use it literally", bu, err)
|
||||
resolvedAddrs = []string{host}
|
||||
} else {
|
||||
resolvedAddrs = make([]string, len(addrs))
|
||||
for i, addr := range addrs {
|
||||
resolvedAddrs[i] = net.JoinHostPort(addr.String(), port)
|
||||
addrs, err := netutil.Resolver.LookupIPAddr(ctx, host)
|
||||
if err != nil {
|
||||
logger.Warnf("cannot discover backend IPs for %s: %s; use it literally", bu, err)
|
||||
resolvedAddrs = []string{host}
|
||||
} else {
|
||||
resolvedAddrs = make([]string, len(addrs))
|
||||
for i, addr := range addrs {
|
||||
resolvedAddrs[i] = net.JoinHostPort(addr.String(), port)
|
||||
}
|
||||
}
|
||||
}
|
||||
// sort resolvedAddrs, so they could be compared below in areEqualBackendURLGroups()
|
||||
sort.Strings(resolvedAddrs)
|
||||
hostToAddrs[host] = resolvedAddrs
|
||||
}
|
||||
// sort resolvedAddrs, so they could be compared below in areEqualBackendURLs()
|
||||
sort.Strings(resolvedAddrs)
|
||||
hostToAddrs[host] = resolvedAddrs
|
||||
}
|
||||
cancel()
|
||||
|
||||
// generate new backendURLs for the resolved IPs
|
||||
busNew := newBackendURLs()
|
||||
for _, bu := range up.busOriginal {
|
||||
host := bu.Hostname()
|
||||
for _, addr := range hostToAddrs[host] {
|
||||
buCopy := *bu
|
||||
buCopy.Host = addr
|
||||
busNew.add(&buCopy)
|
||||
// generate new backendURLs for the resolved IPs, one group per busOriginal entry
|
||||
oldGroups := up.bus.Load().groups
|
||||
busNew := newBackendURLs(&up.n)
|
||||
for i, spec := range up.busOriginal {
|
||||
var urls []*url.URL
|
||||
if up.effectiveDiscoverBackendIPs(spec) {
|
||||
for _, bu := range spec.urls {
|
||||
host := bu.Hostname()
|
||||
for _, addr := range hostToAddrs[host] {
|
||||
buCopy := *bu
|
||||
buCopy.Host = addr
|
||||
urls = append(urls, &buCopy)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
urls = spec.urls
|
||||
}
|
||||
g := busNew.addGroup(urls, &up.backendGroupCounters[i])
|
||||
if i < len(oldGroups) {
|
||||
// Per-group settings are static for the lifetime of this URLPrefix - carry them over
|
||||
// instead of recomputing, so a per-group RoundTripper / concurrency limiter isn't
|
||||
// rebuilt (and in-flight concurrency accounting isn't lost) on every rediscovery.
|
||||
g.loadBalancingPolicy = oldGroups[i].loadBalancingPolicy
|
||||
g.rt = oldGroups[i].rt
|
||||
g.concurrencyLimitCh = oldGroups[i].concurrencyLimitCh
|
||||
g.concurrencyLimitReached = oldGroups[i].concurrencyLimitReached
|
||||
}
|
||||
}
|
||||
|
||||
bus := up.bus.Load()
|
||||
if areEqualBackendURLs(bus.bus, busNew.bus) {
|
||||
if areEqualBackendURLGroups(bus.groups, busNew.groups) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -608,6 +867,18 @@ func (up *URLPrefix) discoverBackendAddrsIfNeeded() {
|
||||
bus.stopHealthChecks()
|
||||
}
|
||||
|
||||
func areEqualBackendURLGroups(a, b []*backendURLGroup) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, g := range a {
|
||||
if !areEqualBackendURLs(g.bus, b[i].bus) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func areEqualBackendURLs(a, b []*backendURL) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
@@ -621,11 +892,12 @@ func areEqualBackendURLs(a, b []*backendURL) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// getFirstAvailableBackendURL returns the first available backendURL, which isn't broken.
|
||||
// If all backendURLs are broken, then returns the first backendURL.
|
||||
// getFirstAvailable returns the first available backendURL in g, which isn't broken.
|
||||
// If all backendURLs in g are broken, then returns the first one.
|
||||
//
|
||||
// backendURL.put() must be called on the returned backendURL after the request is complete.
|
||||
func getFirstAvailableBackendURL(bus []*backendURL) *backendURL {
|
||||
func (g *backendURLGroup) getFirstAvailable() *backendURL {
|
||||
bus := g.bus
|
||||
bu := bus[0]
|
||||
if !bu.isBroken() {
|
||||
// Fast path - send the request to the first url.
|
||||
@@ -648,11 +920,10 @@ func getFirstAvailableBackendURL(bus []*backendURL) *backendURL {
|
||||
return bu
|
||||
}
|
||||
|
||||
// getLeastLoadedBackendURL returns a non-broken backendURL with the lowest number of concurrent requests.
|
||||
// If all backendURLs are broken, then returns the first backendURL.
|
||||
//
|
||||
// backendURL.put() must be called on the returned backendURL after the request is complete.
|
||||
func getLeastLoadedBackendURL(bus []*backendURL, atomicCounter *atomic.Uint32) *backendURL {
|
||||
func (g *backendURLGroup) getLeastLoaded() *backendURL {
|
||||
bus := g.bus
|
||||
atomicCounter := g.n
|
||||
|
||||
firstBu := bus[0]
|
||||
if len(bus) == 1 {
|
||||
firstBu.get()
|
||||
@@ -704,6 +975,62 @@ func getLeastLoadedBackendURL(bus []*backendURL, atomicCounter *atomic.Uint32) *
|
||||
return buMin
|
||||
}
|
||||
|
||||
// getFirstAvailable returns the first backendURLGroup in bus, which has at least a single non-broken backend url.
|
||||
// If all groups are fully broken, then returns the first one.
|
||||
func (bus *backendURLs) getFirstAvailable() *backendURLGroup {
|
||||
groups := bus.groups
|
||||
g := groups[0]
|
||||
if !g.isUnavailable() {
|
||||
// Fast path - use the first group.
|
||||
return g
|
||||
}
|
||||
|
||||
// Slow path - the first group is temporarily unavailable. Fall back to the remaining groups.
|
||||
for i := 1; i < len(groups); i++ {
|
||||
if !groups[i].isUnavailable() {
|
||||
return groups[i]
|
||||
}
|
||||
}
|
||||
|
||||
// All groups are unavailable, then return the first one, it could help increase the success rate of the requests.
|
||||
return g
|
||||
}
|
||||
|
||||
// getLeastLoaded returns a non-broken backendURLGroup in bus with the lowest number of concurrent requests
|
||||
// among its non-broken backend urls. If all groups are broken, then returns the first one.
|
||||
func (bus *backendURLs) getLeastLoaded() *backendURLGroup {
|
||||
groups := bus.groups
|
||||
atomicCounter := bus.n
|
||||
firstGroup := groups[0]
|
||||
if len(groups) == 1 {
|
||||
return firstGroup
|
||||
}
|
||||
|
||||
n := atomicCounter.Add(1) - 1
|
||||
gMinIdx := n % uint32(len(groups))
|
||||
minRequests := groups[gMinIdx].minConcurrentRequests()
|
||||
for i := uint32(1); i < uint32(len(groups)); i++ {
|
||||
idx := (n + i) % uint32(len(groups))
|
||||
g := groups[idx]
|
||||
if g.isUnavailable() {
|
||||
continue
|
||||
}
|
||||
|
||||
reqs := g.minConcurrentRequests()
|
||||
if reqs < minRequests || groups[gMinIdx].isUnavailable() {
|
||||
gMinIdx = idx
|
||||
minRequests = reqs
|
||||
}
|
||||
}
|
||||
gMin := groups[gMinIdx]
|
||||
if gMin.isUnavailable() {
|
||||
// If all groups are unavailable, then returns the first group.
|
||||
return firstGroup
|
||||
}
|
||||
atomicCounter.CompareAndSwap(n+1, gMinIdx+1)
|
||||
return gMin
|
||||
}
|
||||
|
||||
// UnmarshalYAML unmarshals up from yaml.
|
||||
func (up *URLPrefix) UnmarshalYAML(f func(any) error) error {
|
||||
var v any
|
||||
@@ -712,39 +1039,79 @@ func (up *URLPrefix) UnmarshalYAML(f func(any) error) error {
|
||||
}
|
||||
up.vOriginal = v
|
||||
|
||||
var urls []string
|
||||
var items []any
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
urls = []string{x}
|
||||
items = []any{x}
|
||||
case []any:
|
||||
if len(x) == 0 {
|
||||
return fmt.Errorf("`url_prefix` must contain at least a single url")
|
||||
}
|
||||
us := make([]string, len(x))
|
||||
for i, xx := range x {
|
||||
s, ok := xx.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("`url_prefix` must contain array of strings; got %T", xx)
|
||||
}
|
||||
us[i] = s
|
||||
}
|
||||
urls = us
|
||||
items = x
|
||||
default:
|
||||
return fmt.Errorf("unexpected type for `url_prefix`: %T; want string or []string", v)
|
||||
return fmt.Errorf("unexpected type for `url_prefix`: %T; want string, []string or a list containing backend group mappings", v)
|
||||
}
|
||||
|
||||
bus := make([]*url.URL, len(urls))
|
||||
for i, u := range urls {
|
||||
pu, err := url.Parse(u)
|
||||
specs := make([]*backendGroupSpec, len(items))
|
||||
for i, item := range items {
|
||||
spec, err := parseBackendGroupSpecItem(item)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot unmarshal %q into url: %w", u, err)
|
||||
return fmt.Errorf("cannot unmarshal `url_prefix` item #%d: %w", i+1, err)
|
||||
}
|
||||
bus[i] = pu
|
||||
specs[i] = spec
|
||||
}
|
||||
up.busOriginal = bus
|
||||
up.busOriginal = specs
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseBackendGroupSpecItem(item any) (*backendGroupSpec, error) {
|
||||
switch x := item.(type) {
|
||||
case string:
|
||||
pu, err := url.Parse(x)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot unmarshal %q into url: %w", x, err)
|
||||
}
|
||||
return &backendGroupSpec{urls: []*url.URL{pu}}, nil
|
||||
case map[interface{}]interface{}:
|
||||
data, err := yaml.Marshal(x)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot re-marshal backend group mapping: %w", err)
|
||||
}
|
||||
var bgc BackendGroupConfig
|
||||
if err := yaml.UnmarshalStrict(data, &bgc); err != nil {
|
||||
return nil, fmt.Errorf("cannot unmarshal backend group mapping: %w", err)
|
||||
}
|
||||
if len(bgc.URLPrefix) == 0 {
|
||||
return nil, fmt.Errorf("missing `url_prefix` in backend group mapping")
|
||||
}
|
||||
if err := validateLoadBalancingPolicyValue(bgc.BackendSettings.LoadBalancingPolicy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
urls := make([]*url.URL, len(bgc.URLPrefix))
|
||||
for i, u := range bgc.URLPrefix {
|
||||
pu, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot unmarshal %q into url: %w", u, err)
|
||||
}
|
||||
urls[i] = pu
|
||||
}
|
||||
return &backendGroupSpec{
|
||||
name: bgc.Name,
|
||||
urls: urls,
|
||||
loadBalancingPolicy: bgc.BackendSettings.LoadBalancingPolicy,
|
||||
discoverBackendIPs: bgc.BackendSettings.DiscoverBackendIPs,
|
||||
maxConcurrentRequests: bgc.BackendSettings.MaxConcurrentRequests,
|
||||
tlsCAFile: bgc.BackendSettings.TLSCAFile,
|
||||
tlsCertFile: bgc.BackendSettings.TLSCertFile,
|
||||
tlsKeyFile: bgc.BackendSettings.TLSKeyFile,
|
||||
tlsServerName: bgc.BackendSettings.TLSServerName,
|
||||
tlsInsecureSkipVerify: bgc.BackendSettings.TLSInsecureSkipVerify,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected type for `url_prefix` item: %T; want a string or a mapping with url_prefix/load_balancing_policy/discover_backend_ips/etc", item)
|
||||
}
|
||||
}
|
||||
|
||||
// MarshalYAML marshals up to yaml.
|
||||
func (up *URLPrefix) MarshalYAML() (any, error) {
|
||||
return up.vOriginal, nil
|
||||
@@ -997,7 +1364,7 @@ func parseAuthConfig(data []byte) (*AuthConfig, error) {
|
||||
}
|
||||
|
||||
if ui.hasAnyURLs() {
|
||||
if err := ui.initURLs(); err != nil {
|
||||
if err := ui.initURLs(ac.ms); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -1020,7 +1387,7 @@ func parseAuthConfig(data []byte) (*AuthConfig, error) {
|
||||
return float64(len(ui.concurrencyLimitCh))
|
||||
})
|
||||
|
||||
rt, err := newRoundTripper(ui.TLSCAFile, ui.TLSCertFile, ui.TLSKeyFile, ui.TLSServerName, ui.TLSInsecureSkipVerify)
|
||||
rt, err := newRoundTripper(ui.BackendSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot initialize HTTP RoundTripper: %w", err)
|
||||
}
|
||||
@@ -1059,7 +1426,7 @@ func parseAuthConfigUsers(ac *AuthConfig) (map[string]*UserInfo, error) {
|
||||
if err := parseJWTPlaceholdersForUserInfo(ui, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ui.initURLs(); err != nil {
|
||||
if err := ui.initURLs(ac.ms); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1082,7 +1449,7 @@ func parseAuthConfigUsers(ac *AuthConfig) (map[string]*UserInfo, error) {
|
||||
return float64(len(ui.concurrencyLimitCh))
|
||||
})
|
||||
|
||||
rt, err := newRoundTripper(ui.TLSCAFile, ui.TLSCertFile, ui.TLSKeyFile, ui.TLSServerName, ui.TLSInsecureSkipVerify)
|
||||
rt, err := newRoundTripper(ui.BackendSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot initialize HTTP RoundTripper: %w", err)
|
||||
}
|
||||
@@ -1118,7 +1485,7 @@ func (ui *UserInfo) getMetricLabels() (string, error) {
|
||||
return labelsStr, nil
|
||||
}
|
||||
|
||||
func (ui *UserInfo) initURLs() error {
|
||||
func (ui *UserInfo) initURLs(ms *metrics.Set) error {
|
||||
retryStatusCodes := defaultRetryStatusCodes.Values()
|
||||
loadBalancingPolicy := *defaultLoadBalancingPolicy
|
||||
mergeQueryArgs := *defaultMergeQueryArgs
|
||||
@@ -1127,8 +1494,8 @@ func (ui *UserInfo) initURLs() error {
|
||||
if ui.RetryStatusCodes != nil {
|
||||
retryStatusCodes = ui.RetryStatusCodes
|
||||
}
|
||||
if ui.LoadBalancingPolicy != "" {
|
||||
loadBalancingPolicy = ui.LoadBalancingPolicy
|
||||
if ui.BackendSettings.LoadBalancingPolicy != "" {
|
||||
loadBalancingPolicy = ui.BackendSettings.LoadBalancingPolicy
|
||||
}
|
||||
if len(ui.MergeQueryArgs) != 0 {
|
||||
mergeQueryArgs = ui.MergeQueryArgs
|
||||
@@ -1136,15 +1503,26 @@ func (ui *UserInfo) initURLs() error {
|
||||
if ui.DropSrcPathPrefixParts != nil {
|
||||
dropSrcPathPrefixParts = *ui.DropSrcPathPrefixParts
|
||||
}
|
||||
if ui.DiscoverBackendIPs != nil {
|
||||
discoverBackendIPs = *ui.DiscoverBackendIPs
|
||||
if ui.BackendSettings.DiscoverBackendIPs != nil {
|
||||
discoverBackendIPs = *ui.BackendSettings.DiscoverBackendIPs
|
||||
}
|
||||
|
||||
metricLabels, err := ui.getMetricLabels()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
userSettings := backendUserSettings{
|
||||
tlsCAFile: ui.BackendSettings.TLSCAFile,
|
||||
tlsCertFile: ui.BackendSettings.TLSCertFile,
|
||||
tlsKeyFile: ui.BackendSettings.TLSKeyFile,
|
||||
tlsServerName: ui.BackendSettings.TLSServerName,
|
||||
tlsInsecureSkipVerify: ui.BackendSettings.TLSInsecureSkipVerify,
|
||||
ms: ms,
|
||||
metricLabels: metricLabels,
|
||||
}
|
||||
|
||||
up := ui.URLPrefix
|
||||
if up != nil {
|
||||
if err := up.sanitizeAndInitialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
up.retryStatusCodes = retryStatusCodes
|
||||
up.dropSrcPathPrefixParts = dropSrcPathPrefixParts
|
||||
up.discoverBackendIPs = discoverBackendIPs
|
||||
@@ -1152,9 +1530,12 @@ func (ui *UserInfo) initURLs() error {
|
||||
return err
|
||||
}
|
||||
up.mergeQueryArgs = mergeQueryArgs
|
||||
if err := up.sanitizeAndInitialize(userSettings); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if ui.DefaultURL != nil {
|
||||
if err := ui.DefaultURL.sanitizeAndInitialize(); err != nil {
|
||||
if err := ui.DefaultURL.sanitizeAndInitialize(userSettings); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1166,9 +1547,6 @@ func (ui *UserInfo) initURLs() error {
|
||||
if e.URLPrefix == nil {
|
||||
return fmt.Errorf("missing `url_prefix` in `url_map`")
|
||||
}
|
||||
if err := e.URLPrefix.sanitizeAndInitialize(); err != nil {
|
||||
return err
|
||||
}
|
||||
rscs := retryStatusCodes
|
||||
lbp := loadBalancingPolicy
|
||||
mqa := mergeQueryArgs
|
||||
@@ -1196,6 +1574,9 @@ func (ui *UserInfo) initURLs() error {
|
||||
e.URLPrefix.mergeQueryArgs = mqa
|
||||
e.URLPrefix.dropSrcPathPrefixParts = dsp
|
||||
e.URLPrefix.discoverBackendIPs = dbd
|
||||
if err := e.URLPrefix.sanitizeAndInitialize(userSettings); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(ui.URLMaps) == 0 && ui.URLPrefix == nil {
|
||||
return fmt.Errorf("missing `url_prefix` or `url_map`")
|
||||
@@ -1298,19 +1679,89 @@ func getAuthTokensFromRequest(r *http.Request) []string {
|
||||
return ats
|
||||
}
|
||||
|
||||
func (up *URLPrefix) sanitizeAndInitialize() error {
|
||||
for i, bu := range up.busOriginal {
|
||||
puNew, err := sanitizeURLPrefix(bu)
|
||||
if err != nil {
|
||||
return err
|
||||
// sanitizeAndInitialize validates up.busOriginal and (re)initializes up.bus from it,
|
||||
// applying per-group overrides on top of the settings inherited from userSettings.
|
||||
func (up *URLPrefix) sanitizeAndInitialize(userSettings backendUserSettings) error {
|
||||
for _, spec := range up.busOriginal {
|
||||
for i, bu := range spec.urls {
|
||||
puNew, err := sanitizeURLPrefix(bu)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
spec.urls[i] = puNew
|
||||
}
|
||||
up.busOriginal[i] = puNew
|
||||
}
|
||||
|
||||
// Initialize up.bus
|
||||
bus := newBackendURLs()
|
||||
for _, bu := range up.busOriginal {
|
||||
bus.add(bu)
|
||||
up.backendGroupCounters = make([]atomic.Uint32, len(up.busOriginal))
|
||||
|
||||
up.hasAnyBackendDiscovery = false
|
||||
for _, spec := range up.busOriginal {
|
||||
if up.effectiveDiscoverBackendIPs(spec) {
|
||||
up.hasAnyBackendDiscovery = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize up.bus with a single group per busOriginal entry.
|
||||
bus := newBackendURLs(&up.n)
|
||||
for i, spec := range up.busOriginal {
|
||||
g := bus.addGroup(spec.urls, &up.backendGroupCounters[i])
|
||||
|
||||
lbp := spec.loadBalancingPolicy
|
||||
if lbp == "" {
|
||||
lbp = up.loadBalancingPolicy
|
||||
}
|
||||
g.loadBalancingPolicy = lbp
|
||||
|
||||
if spec.hasTLSOverride() {
|
||||
bs := BackendSettings{
|
||||
TLSCAFile: spec.tlsCAFile,
|
||||
TLSCertFile: spec.tlsCertFile,
|
||||
TLSKeyFile: spec.tlsKeyFile,
|
||||
TLSServerName: spec.tlsServerName,
|
||||
TLSInsecureSkipVerify: spec.tlsInsecureSkipVerify,
|
||||
}
|
||||
if bs.TLSCAFile == "" {
|
||||
bs.TLSCAFile = userSettings.tlsCAFile
|
||||
}
|
||||
if bs.TLSCertFile == "" {
|
||||
bs.TLSCertFile = userSettings.tlsCertFile
|
||||
}
|
||||
if bs.TLSKeyFile == "" {
|
||||
bs.TLSKeyFile = userSettings.tlsKeyFile
|
||||
}
|
||||
if bs.TLSServerName == "" {
|
||||
bs.TLSServerName = userSettings.tlsServerName
|
||||
}
|
||||
if bs.TLSInsecureSkipVerify == nil {
|
||||
bs.TLSInsecureSkipVerify = userSettings.tlsInsecureSkipVerify
|
||||
}
|
||||
|
||||
rt, err := newRoundTripper(bs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot initialize HTTP RoundTripper for a backend group at `url_prefix` item #%d: %w", i+1, err)
|
||||
}
|
||||
g.rt = rt
|
||||
}
|
||||
|
||||
if spec.maxConcurrentRequests > 0 {
|
||||
g.concurrencyLimitCh = make(chan struct{}, spec.maxConcurrentRequests)
|
||||
if userSettings.ms != nil {
|
||||
groupID := spec.name
|
||||
if groupID == "" {
|
||||
groupID = strconv.Itoa(i)
|
||||
}
|
||||
groupLabels := backendGroupMetricLabels(userSettings.metricLabels, groupID)
|
||||
g.concurrencyLimitReached = userSettings.ms.GetOrCreateCounter(`vmauth_backend_group_concurrent_requests_limit_reached_total` + groupLabels)
|
||||
ch := g.concurrencyLimitCh
|
||||
_ = userSettings.ms.GetOrCreateGauge(`vmauth_backend_group_concurrent_requests_capacity`+groupLabels, func() float64 {
|
||||
return float64(cap(ch))
|
||||
})
|
||||
_ = userSettings.ms.GetOrCreateGauge(`vmauth_backend_group_concurrent_requests_current`+groupLabels, func() float64 {
|
||||
return float64(len(ch))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
up.bus.Store(bus)
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -365,11 +367,13 @@ users:
|
||||
tls_insecure_skip_verify: true
|
||||
`, map[string]*UserInfo{
|
||||
getHTTPAuthBasicToken("foo", "bar"): {
|
||||
Username: "foo",
|
||||
Password: "bar",
|
||||
URLPrefix: mustParseURL("http://aaa:343/bbb"),
|
||||
MaxConcurrentRequests: 5,
|
||||
TLSInsecureSkipVerify: &insecureSkipVerifyTrue,
|
||||
Username: "foo",
|
||||
Password: "bar",
|
||||
URLPrefix: mustParseURL("http://aaa:343/bbb"),
|
||||
BackendSettings: BackendSettings{
|
||||
MaxConcurrentRequests: 5,
|
||||
TLSInsecureSkipVerify: &insecureSkipVerifyTrue,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
@@ -386,14 +390,16 @@ users:
|
||||
tls_key_file: "foo/foo"
|
||||
`, map[string]*UserInfo{
|
||||
getHTTPAuthToken("foo"): {
|
||||
AuthToken: "foo",
|
||||
URLPrefix: mustParseURL("https://aaa:343/bbb"),
|
||||
MaxConcurrentRequests: 5,
|
||||
TLSInsecureSkipVerify: &insecureSkipVerifyTrue,
|
||||
TLSServerName: "foo.bar",
|
||||
TLSCAFile: "foo/bar",
|
||||
TLSCertFile: "foo/baz",
|
||||
TLSKeyFile: "foo/foo",
|
||||
AuthToken: "foo",
|
||||
URLPrefix: mustParseURL("https://aaa:343/bbb"),
|
||||
BackendSettings: BackendSettings{
|
||||
MaxConcurrentRequests: 5,
|
||||
TLSInsecureSkipVerify: &insecureSkipVerifyTrue,
|
||||
TLSServerName: "foo.bar",
|
||||
TLSCAFile: "foo/bar",
|
||||
TLSCertFile: "foo/baz",
|
||||
TLSKeyFile: "foo/foo",
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
@@ -421,12 +427,14 @@ users:
|
||||
"http://node1:343/bbb",
|
||||
"http://srv+node2:343/bbb",
|
||||
}),
|
||||
TLSInsecureSkipVerify: &insecureSkipVerifyFalse,
|
||||
BackendSettings: BackendSettings{
|
||||
TLSInsecureSkipVerify: &insecureSkipVerifyFalse,
|
||||
LoadBalancingPolicy: "first_available",
|
||||
DiscoverBackendIPs: &discoverBackendIPsTrue,
|
||||
},
|
||||
RetryStatusCodes: []int{500, 501},
|
||||
LoadBalancingPolicy: "first_available",
|
||||
MergeQueryArgs: []string{"foo", "bar"},
|
||||
DropSrcPathPrefixParts: new(1),
|
||||
DiscoverBackendIPs: &discoverBackendIPsTrue,
|
||||
},
|
||||
}, nil)
|
||||
|
||||
@@ -736,11 +744,11 @@ unauthorized_user:
|
||||
}
|
||||
|
||||
ui := m[getHTTPAuthBasicToken("foo", "bar")]
|
||||
if !isSetBool(ui.TLSInsecureSkipVerify, true) {
|
||||
if !isSetBool(ui.BackendSettings.TLSInsecureSkipVerify, true) {
|
||||
t.Fatalf("unexpected TLSInsecureSkipVerify value for user foo")
|
||||
}
|
||||
|
||||
if !isSetBool(ac.UnauthorizedUser.TLSInsecureSkipVerify, false) {
|
||||
if !isSetBool(ac.UnauthorizedUser.BackendSettings.TLSInsecureSkipVerify, false) {
|
||||
t.Fatalf("unexpected TLSInsecureSkipVerify value for unauthorized_user")
|
||||
}
|
||||
}
|
||||
@@ -841,7 +849,7 @@ func TestGetLeastLoadedBackendURL(t *testing.T) {
|
||||
up.loadBalancingPolicy = "least_loaded"
|
||||
|
||||
pbus := up.bus.Load()
|
||||
bus := pbus.bus
|
||||
bus := pbus.groups[0].bus
|
||||
|
||||
fn := func(ns ...int) {
|
||||
t.Helper()
|
||||
@@ -913,7 +921,7 @@ func TestBrokenBackend(t *testing.T) {
|
||||
})
|
||||
up.loadBalancingPolicy = "least_loaded"
|
||||
pbus := up.bus.Load()
|
||||
bus := pbus.bus
|
||||
bus := pbus.groups[0].bus
|
||||
|
||||
// explicitly mark one of the backends as broken
|
||||
bus[1].setBroken()
|
||||
@@ -932,11 +940,12 @@ func TestDiscoverBackendIPsWithIPV6(t *testing.T) {
|
||||
t.Helper()
|
||||
up := mustParseURL(actualUrl)
|
||||
up.discoverBackendIPs = true
|
||||
up.hasAnyBackendDiscovery = true
|
||||
up.loadBalancingPolicy = "least_loaded"
|
||||
|
||||
up.discoverBackendAddrsIfNeeded()
|
||||
pbus := up.bus.Load()
|
||||
bus := pbus.bus
|
||||
bus := pbus.groups[0].bus
|
||||
|
||||
if len(bus) != 1 {
|
||||
t.Fatalf("expected url list to be of size 1; got %d instead", len(bus))
|
||||
@@ -996,6 +1005,114 @@ func TestDiscoverBackendIPsWithIPV6(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestAreEqualBackendURLGroups(t *testing.T) {
|
||||
newGroup := func(hosts ...string) *backendURLGroup {
|
||||
g := &backendURLGroup{}
|
||||
for _, h := range hosts {
|
||||
g.bus = append(g.bus, &backendURL{url: &url.URL{Host: h}})
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
f := func(a, b []*backendURLGroup, expected bool) {
|
||||
t.Helper()
|
||||
if got := areEqualBackendURLGroups(a, b); got != expected {
|
||||
t.Fatalf("unexpected result; got %v; want %v", got, expected)
|
||||
}
|
||||
}
|
||||
|
||||
// identical grouping
|
||||
f(
|
||||
[]*backendURLGroup{newGroup("10.0.0.1", "10.0.0.2"), newGroup("10.0.0.3")},
|
||||
[]*backendURLGroup{newGroup("10.0.0.1", "10.0.0.2"), newGroup("10.0.0.3")},
|
||||
true,
|
||||
)
|
||||
|
||||
// different number of groups
|
||||
f(
|
||||
[]*backendURLGroup{newGroup("10.0.0.1")},
|
||||
[]*backendURLGroup{newGroup("10.0.0.1"), newGroup("10.0.0.2")},
|
||||
false,
|
||||
)
|
||||
|
||||
// the flattened address sequence is unchanged, but an address moved across the group boundary
|
||||
f(
|
||||
[]*backendURLGroup{newGroup("10.0.0.1", "10.0.0.2"), newGroup("10.0.0.3")},
|
||||
[]*backendURLGroup{newGroup("10.0.0.1"), newGroup("10.0.0.2", "10.0.0.3")},
|
||||
false,
|
||||
)
|
||||
|
||||
// genuinely different content
|
||||
f(
|
||||
[]*backendURLGroup{newGroup("10.0.0.1")},
|
||||
[]*backendURLGroup{newGroup("10.0.0.9")},
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
func TestDiscoverBackendAddrsIfNeededDetectsGroupBoundaryShift(t *testing.T) {
|
||||
customResolver := &fakeResolver{
|
||||
Resolver: &net.Resolver{},
|
||||
lookupIPAddrResults: map[string][]net.IPAddr{
|
||||
"zonea": {
|
||||
{IP: net.ParseIP("10.0.0.1")},
|
||||
{IP: net.ParseIP("10.0.0.2")},
|
||||
},
|
||||
"zoneb": {
|
||||
{IP: net.ParseIP("10.0.0.3")},
|
||||
},
|
||||
},
|
||||
}
|
||||
origResolver := netutil.Resolver
|
||||
netutil.Resolver = customResolver
|
||||
defer func() {
|
||||
netutil.Resolver = origResolver
|
||||
}()
|
||||
|
||||
up := &URLPrefix{}
|
||||
up.busOriginal = []*backendGroupSpec{
|
||||
{urls: []*url.URL{{Scheme: "http", Host: "zonea"}}},
|
||||
{urls: []*url.URL{{Scheme: "http", Host: "zoneb"}}},
|
||||
}
|
||||
up.backendGroupCounters = make([]atomic.Uint32, 2)
|
||||
bus0 := newBackendURLs(&up.n)
|
||||
bus0.addGroup(up.busOriginal[0].urls, &up.backendGroupCounters[0])
|
||||
bus0.addGroup(up.busOriginal[1].urls, &up.backendGroupCounters[1])
|
||||
up.bus.Store(bus0)
|
||||
up.discoverBackendIPs = true
|
||||
up.hasAnyBackendDiscovery = true
|
||||
|
||||
up.discoverBackendAddrsIfNeeded()
|
||||
bus := up.bus.Load()
|
||||
if len(bus.groups) != 2 || len(bus.groups[0].bus) != 2 || len(bus.groups[1].bus) != 1 {
|
||||
t.Fatalf("unexpected initial grouping: zonea=%d zoneb=%d", len(bus.groups[0].bus), len(bus.groups[1].bus))
|
||||
}
|
||||
|
||||
// Simulate 10.0.0.2 moving from zonea to zoneb. The flattened address sequence
|
||||
// stays [10.0.0.1, 10.0.0.2, 10.0.0.3], but the group boundaries shift, so the
|
||||
// rediscovery must still be picked up.
|
||||
customResolver.lookupIPAddrResults["zonea"] = []net.IPAddr{
|
||||
{IP: net.ParseIP("10.0.0.1")},
|
||||
}
|
||||
customResolver.lookupIPAddrResults["zoneb"] = []net.IPAddr{
|
||||
{IP: net.ParseIP("10.0.0.2")},
|
||||
{IP: net.ParseIP("10.0.0.3")},
|
||||
}
|
||||
up.nextDiscoveryDeadline.Store(0)
|
||||
up.discoverBackendAddrsIfNeeded()
|
||||
|
||||
bus = up.bus.Load()
|
||||
if len(bus.groups) != 2 {
|
||||
t.Fatalf("unexpected groups count after rediscovery: %d", len(bus.groups))
|
||||
}
|
||||
if len(bus.groups[0].bus) != 1 || bus.groups[0].bus[0].url.Host != "10.0.0.1:" {
|
||||
t.Fatalf("unexpected zonea group after rediscovery: %v", bus.groups[0].bus)
|
||||
}
|
||||
if len(bus.groups[1].bus) != 2 {
|
||||
t.Fatalf("unexpected zoneb group size after rediscovery; got %d; want 2", len(bus.groups[1].bus))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogRequest(t *testing.T) {
|
||||
ui := &UserInfo{AccessLog: &AccessLog{}}
|
||||
|
||||
@@ -1041,7 +1158,8 @@ func TestGetFirstAvailableBackend(t *testing.T) {
|
||||
}
|
||||
bus[i].broken.Store(broken[i])
|
||||
}
|
||||
bu := getFirstAvailableBackendURL(bus)
|
||||
g := &backendURLGroup{bus: bus}
|
||||
bu := g.getFirstAvailable()
|
||||
if bu == nil {
|
||||
t.Fatalf("unexpected nil backend")
|
||||
}
|
||||
@@ -1058,6 +1176,355 @@ func TestGetFirstAvailableBackend(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func newTestGroup(broken ...bool) *backendURLGroup {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
bhc := &backendHealthCheck{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
g := &backendURLGroup{
|
||||
bus: make([]*backendURL, len(broken)),
|
||||
}
|
||||
for i, b := range broken {
|
||||
g.bus[i] = &backendURL{
|
||||
url: &url.URL{Host: fmt.Sprintf("target-%d", i)},
|
||||
bhc: bhc,
|
||||
}
|
||||
g.bus[i].broken.Store(b)
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
func TestGetFirstAvailableGroup(t *testing.T) {
|
||||
// the first group is available
|
||||
bus := &backendURLs{groups: []*backendURLGroup{newTestGroup(false), newTestGroup(false)}}
|
||||
if g := bus.getFirstAvailable(); g != bus.groups[0] {
|
||||
t.Fatalf("expecting the first group to be returned")
|
||||
}
|
||||
|
||||
// the first group is fully broken - falls back to the second one
|
||||
bus = &backendURLs{groups: []*backendURLGroup{newTestGroup(true), newTestGroup(false)}}
|
||||
if g := bus.getFirstAvailable(); g != bus.groups[1] {
|
||||
t.Fatalf("expecting the second group to be returned")
|
||||
}
|
||||
|
||||
// the first group has a non-broken target - it is still considered available
|
||||
bus = &backendURLs{groups: []*backendURLGroup{newTestGroup(true, false), newTestGroup(false)}}
|
||||
if g := bus.getFirstAvailable(); g != bus.groups[0] {
|
||||
t.Fatalf("expecting the first group to be returned, since it has a non-broken target")
|
||||
}
|
||||
|
||||
// all groups are fully broken - the first one is returned
|
||||
bus = &backendURLs{groups: []*backendURLGroup{newTestGroup(true), newTestGroup(true)}}
|
||||
if g := bus.getFirstAvailable(); g != bus.groups[0] {
|
||||
t.Fatalf("expecting the first group to be returned when all groups are broken")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLeastLoadedGroup(t *testing.T) {
|
||||
g0 := newTestGroup(false)
|
||||
g1 := newTestGroup(false)
|
||||
var n atomic.Uint32
|
||||
bus := &backendURLs{groups: []*backendURLGroup{g0, g1}, n: &n}
|
||||
|
||||
// both groups are idle - the first one is picked
|
||||
if g := bus.getLeastLoaded(); g != g0 {
|
||||
t.Fatalf("expecting g0 to be returned when all groups are idle")
|
||||
}
|
||||
|
||||
// g0 becomes more loaded than g1 - g1 must be picked
|
||||
g0.bus[0].concurrentRequests.Add(5)
|
||||
if g := bus.getLeastLoaded(); g != g1 {
|
||||
t.Fatalf("expecting g1 to be returned when it is less loaded than g0")
|
||||
}
|
||||
|
||||
// g1 is broken - g0 must be picked despite being more loaded
|
||||
g1.bus[0].setBroken()
|
||||
if g := bus.getLeastLoaded(); g != g0 {
|
||||
t.Fatalf("expecting g0 to be returned when g1 is broken")
|
||||
}
|
||||
|
||||
// all groups are broken - the first one is returned
|
||||
g0.bus[0].setBroken()
|
||||
if g := bus.getLeastLoaded(); g != g0 {
|
||||
t.Fatalf("expecting g0 to be returned when all groups are broken")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBackendURLTwoTier(t *testing.T) {
|
||||
// Simulates two backends (busOriginal entries), each expanded into two discovered targets.
|
||||
up := &URLPrefix{}
|
||||
up.backendGroupCounters = make([]atomic.Uint32, 2)
|
||||
bus := newBackendURLs(&up.n)
|
||||
g0 := bus.addGroup([]*url.URL{{Host: "g0-a"}, {Host: "g0-b"}}, &up.backendGroupCounters[0])
|
||||
g0.loadBalancingPolicy = "first_available"
|
||||
g1 := bus.addGroup([]*url.URL{{Host: "g1-a"}, {Host: "g1-b"}}, &up.backendGroupCounters[1])
|
||||
g1.loadBalancingPolicy = "first_available"
|
||||
|
||||
up.bus.Store(bus)
|
||||
up.loadBalancingPolicy = "first_available"
|
||||
|
||||
if n := up.getBackendsCount(); n != 4 {
|
||||
t.Fatalf("unexpected backends count; got %d; want 4", n)
|
||||
}
|
||||
|
||||
// Both policies are first_available, so vmauth must always pick group0's first target.
|
||||
for range 5 {
|
||||
bu := up.getBackendURL()
|
||||
if bu.url.Host != "g0-a" {
|
||||
t.Fatalf("unexpected target; got %q; want g0-a", bu.url.Host)
|
||||
}
|
||||
bu.put()
|
||||
}
|
||||
|
||||
// Mark group0's first target broken - the backend-level policy falls back to g0-b,
|
||||
// while the top-level group selection still prefers group0, since it has a non-broken target.
|
||||
bus.groups[0].bus[0].setBroken()
|
||||
bu := up.getBackendURL()
|
||||
if bu.url.Host != "g0-b" {
|
||||
t.Fatalf("unexpected target; got %q; want g0-b", bu.url.Host)
|
||||
}
|
||||
bu.put()
|
||||
|
||||
// Mark group0 fully broken - the top-level selection falls back to group1.
|
||||
bus.groups[0].bus[1].setBroken()
|
||||
bu = up.getBackendURL()
|
||||
if bu.url.Host != "g1-a" {
|
||||
t.Fatalf("unexpected target; got %q; want g1-a", bu.url.Host)
|
||||
}
|
||||
bu.put()
|
||||
}
|
||||
|
||||
func TestURLPrefixUnmarshalYAMLBackendGroup(t *testing.T) {
|
||||
var up URLPrefix
|
||||
data := []byte(`
|
||||
- http://plain-backend
|
||||
- url_prefix:
|
||||
- http://group-a
|
||||
- http://group-b
|
||||
load_balancing_policy: least_loaded
|
||||
discover_backend_ips: true
|
||||
max_concurrent_requests: 7
|
||||
tls_insecure_skip_verify: true
|
||||
`)
|
||||
if err := yaml.Unmarshal(data, &up); err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
if len(up.busOriginal) != 2 {
|
||||
t.Fatalf("unexpected number of busOriginal entries; got %d; want 2", len(up.busOriginal))
|
||||
}
|
||||
|
||||
spec0 := up.busOriginal[0]
|
||||
if len(spec0.urls) != 1 || spec0.urls[0].String() != "http://plain-backend" {
|
||||
t.Fatalf("unexpected spec0 urls: %v", spec0.urls)
|
||||
}
|
||||
if spec0.loadBalancingPolicy != "" || spec0.maxConcurrentRequests != 0 || spec0.hasTLSOverride() {
|
||||
t.Fatalf("unexpected overrides on a plain string spec: %+v", spec0)
|
||||
}
|
||||
|
||||
spec1 := up.busOriginal[1]
|
||||
if len(spec1.urls) != 2 || spec1.urls[0].String() != "http://group-a" || spec1.urls[1].String() != "http://group-b" {
|
||||
t.Fatalf("unexpected spec1 urls: %v", spec1.urls)
|
||||
}
|
||||
if spec1.loadBalancingPolicy != "least_loaded" {
|
||||
t.Fatalf("unexpected spec1.loadBalancingPolicy; got %q; want %q", spec1.loadBalancingPolicy, "least_loaded")
|
||||
}
|
||||
if spec1.discoverBackendIPs == nil || !*spec1.discoverBackendIPs {
|
||||
t.Fatalf("expecting spec1.discoverBackendIPs=true")
|
||||
}
|
||||
if spec1.maxConcurrentRequests != 7 {
|
||||
t.Fatalf("unexpected spec1.maxConcurrentRequests; got %d; want 7", spec1.maxConcurrentRequests)
|
||||
}
|
||||
if spec1.tlsInsecureSkipVerify == nil || !*spec1.tlsInsecureSkipVerify {
|
||||
t.Fatalf("expecting spec1.tlsInsecureSkipVerify=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestURLPrefixUnmarshalYAMLBackendGroupInvalidPolicy(t *testing.T) {
|
||||
var up URLPrefix
|
||||
data := []byte(`
|
||||
- url_prefix: http://a
|
||||
load_balancing_policy: bogus
|
||||
`)
|
||||
if err := yaml.Unmarshal(data, &up); err == nil {
|
||||
t.Fatalf("expecting non-nil error for invalid load_balancing_policy in a backend group")
|
||||
}
|
||||
}
|
||||
|
||||
func TestURLPrefixUnmarshalYAMLBackendGroupMissingURLPrefix(t *testing.T) {
|
||||
var up URLPrefix
|
||||
data := []byte(`
|
||||
- discover_backend_ips: true
|
||||
`)
|
||||
if err := yaml.Unmarshal(data, &up); err == nil {
|
||||
t.Fatalf("expecting non-nil error for missing url_prefix in a backend group mapping")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeAndInitializeBackendGroupOverrides(t *testing.T) {
|
||||
data := []byte(`
|
||||
users:
|
||||
- username: foo
|
||||
password: bar
|
||||
load_balancing_policy: first_available
|
||||
url_prefix:
|
||||
- http://primary-a
|
||||
- url_prefix:
|
||||
- http://standby-a
|
||||
- http://standby-b
|
||||
load_balancing_policy: least_loaded
|
||||
discover_backend_ips: true
|
||||
max_concurrent_requests: 5
|
||||
tls_insecure_skip_verify: true
|
||||
`)
|
||||
ac, err := parseAuthConfig(data)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
m, err := parseAuthConfigUsers(ac)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
var ui *UserInfo
|
||||
for _, u := range m {
|
||||
ui = u
|
||||
}
|
||||
if ui == nil {
|
||||
t.Fatalf("expecting a single parsed user")
|
||||
}
|
||||
|
||||
up := ui.URLPrefix
|
||||
bus := up.bus.Load()
|
||||
if len(bus.groups) != 2 {
|
||||
t.Fatalf("unexpected number of groups; got %d; want 2", len(bus.groups))
|
||||
}
|
||||
|
||||
g0 := bus.groups[0]
|
||||
if g0.loadBalancingPolicy != "first_available" {
|
||||
t.Fatalf("unexpected g0 loadBalancingPolicy; got %q; want %q (inherited)", g0.loadBalancingPolicy, "first_available")
|
||||
}
|
||||
if g0.rt != nil {
|
||||
t.Fatalf("expecting g0.rt to be nil (no per-group tls override)")
|
||||
}
|
||||
if g0.concurrencyLimitCh != nil {
|
||||
t.Fatalf("expecting g0.concurrencyLimitCh to be nil (no per-group max_concurrent_requests)")
|
||||
}
|
||||
|
||||
g1 := bus.groups[1]
|
||||
if g1.loadBalancingPolicy != "least_loaded" {
|
||||
t.Fatalf("unexpected g1 loadBalancingPolicy; got %q; want %q (overridden)", g1.loadBalancingPolicy, "least_loaded")
|
||||
}
|
||||
if g1.rt == nil {
|
||||
t.Fatalf("expecting g1.rt to be non-nil due to tls_insecure_skip_verify override")
|
||||
}
|
||||
if cap(g1.concurrencyLimitCh) != 5 {
|
||||
t.Fatalf("unexpected g1 concurrency limit capacity; got %d; want 5", cap(g1.concurrencyLimitCh))
|
||||
}
|
||||
if len(g1.bus) != 2 {
|
||||
t.Fatalf("unexpected g1 backend count; got %d; want 2", len(g1.bus))
|
||||
}
|
||||
if !up.hasAnyBackendDiscovery {
|
||||
t.Fatalf("expecting hasAnyBackendDiscovery=true, since g1 overrides discover_backend_ips=true")
|
||||
}
|
||||
|
||||
// g1 has no explicit `name`, so its metrics must fall back to its ordinal position (index 1).
|
||||
wantCounter := ac.ms.GetOrCreateCounter(`vmauth_backend_group_concurrent_requests_limit_reached_total{username="foo",backend_group="1"}`)
|
||||
if wantCounter != g1.concurrencyLimitReached {
|
||||
t.Fatalf("g1's concurrency limit metric wasn't registered with the expected ordinal backend_group label")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeAndInitializeBackendGroupName(t *testing.T) {
|
||||
data := []byte(`
|
||||
users:
|
||||
- username: foo
|
||||
password: bar
|
||||
url_prefix:
|
||||
- url_prefix: http://primary-a
|
||||
name: primary
|
||||
max_concurrent_requests: 3
|
||||
`)
|
||||
ac, err := parseAuthConfig(data)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
m, err := parseAuthConfigUsers(ac)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %s", err)
|
||||
}
|
||||
var ui *UserInfo
|
||||
for _, u := range m {
|
||||
ui = u
|
||||
}
|
||||
if ui == nil {
|
||||
t.Fatalf("expecting a single parsed user")
|
||||
}
|
||||
|
||||
g0 := ui.URLPrefix.bus.Load().groups[0]
|
||||
wantCounter := ac.ms.GetOrCreateCounter(`vmauth_backend_group_concurrent_requests_limit_reached_total{username="foo",backend_group="primary"}`)
|
||||
if wantCounter != g0.concurrencyLimitReached {
|
||||
t.Fatalf("g0's concurrency limit metric wasn't registered with the explicit backend_group=\"primary\" label")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackendURLGroupConcurrencyLimit(t *testing.T) {
|
||||
g := &backendURLGroup{
|
||||
concurrencyLimitCh: make(chan struct{}, 1),
|
||||
}
|
||||
if !g.beginConcurrencyLimit() {
|
||||
t.Fatalf("expecting first beginConcurrencyLimit to succeed")
|
||||
}
|
||||
if g.beginConcurrencyLimit() {
|
||||
t.Fatalf("expecting second beginConcurrencyLimit to fail since the limit is 1")
|
||||
}
|
||||
if !g.isAtConcurrencyLimit() {
|
||||
t.Fatalf("expecting isAtConcurrencyLimit to be true")
|
||||
}
|
||||
g.endConcurrencyLimit()
|
||||
if g.isAtConcurrencyLimit() {
|
||||
t.Fatalf("expecting isAtConcurrencyLimit to be false after endConcurrencyLimit")
|
||||
}
|
||||
if !g.beginConcurrencyLimit() {
|
||||
t.Fatalf("expecting beginConcurrencyLimit to succeed again after endConcurrencyLimit")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackendURLGroupConcurrencyLimitDisabled(t *testing.T) {
|
||||
g := &backendURLGroup{}
|
||||
for range 10 {
|
||||
if !g.beginConcurrencyLimit() {
|
||||
t.Fatalf("expecting beginConcurrencyLimit to always succeed when no limit is configured")
|
||||
}
|
||||
}
|
||||
// must not panic/block when no limit is configured
|
||||
g.endConcurrencyLimit()
|
||||
}
|
||||
|
||||
func TestGetFirstAvailableSkipsConcurrencyLimitedGroup(t *testing.T) {
|
||||
g0 := newTestGroup(false)
|
||||
g0.concurrencyLimitCh = make(chan struct{}, 1)
|
||||
g0.concurrencyLimitCh <- struct{}{} // saturate g0
|
||||
g1 := newTestGroup(false)
|
||||
bus := &backendURLs{groups: []*backendURLGroup{g0, g1}}
|
||||
|
||||
if g := bus.getFirstAvailable(); g != g1 {
|
||||
t.Fatalf("expecting g1 to be returned since g0 is at its concurrency limit")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLeastLoadedSkipsConcurrencyLimitedGroup(t *testing.T) {
|
||||
g0 := newTestGroup(false)
|
||||
g0.concurrencyLimitCh = make(chan struct{}, 1)
|
||||
g0.concurrencyLimitCh <- struct{}{} // saturate g0
|
||||
g1 := newTestGroup(false)
|
||||
var n atomic.Uint32
|
||||
bus := &backendURLs{groups: []*backendURLGroup{g0, g1}, n: &n}
|
||||
|
||||
if g := bus.getLeastLoaded(); g != g1 {
|
||||
t.Fatalf("expecting g1 to be returned since g0 is at its concurrency limit")
|
||||
}
|
||||
}
|
||||
|
||||
func getRegexs(paths []string) []*Regex {
|
||||
var sps []*Regex
|
||||
for _, path := range paths {
|
||||
@@ -1092,14 +1559,12 @@ func mustParseURL(u string) *URLPrefix {
|
||||
}
|
||||
|
||||
func mustParseURLs(us []string) *URLPrefix {
|
||||
bus := newBackendURLs()
|
||||
urls := make([]*url.URL, len(us))
|
||||
for i, u := range us {
|
||||
pu, err := url.Parse(u)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("BUG: cannot parse %q: %w", u, err))
|
||||
}
|
||||
bus.add(pu)
|
||||
urls[i] = pu
|
||||
}
|
||||
up := &URLPrefix{}
|
||||
@@ -1108,8 +1573,11 @@ func mustParseURLs(us []string) *URLPrefix {
|
||||
} else {
|
||||
up.vOriginal = us
|
||||
}
|
||||
up.busOriginal = []*backendGroupSpec{{urls: urls}}
|
||||
up.backendGroupCounters = make([]atomic.Uint32, 1)
|
||||
bus := newBackendURLs(&up.n)
|
||||
bus.addGroup(urls, &up.backendGroupCounters[0])
|
||||
up.bus.Store(bus)
|
||||
up.busOriginal = urls
|
||||
return up
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ func parseJWTUsers(ac *AuthConfig, oidcDP *oidcDiscovererPool) ([]*UserInfo, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := ui.initURLs(); err != nil {
|
||||
if err := ui.initURLs(ac.ms); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ func parseJWTUsers(ac *AuthConfig, oidcDP *oidcDiscovererPool) ([]*UserInfo, err
|
||||
return float64(len(ui.concurrencyLimitCh))
|
||||
})
|
||||
|
||||
rt, err := newRoundTripper(ui.TLSCAFile, ui.TLSCertFile, ui.TLSKeyFile, ui.TLSServerName, ui.TLSInsecureSkipVerify)
|
||||
rt, err := newRoundTripper(ui.BackendSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot initialize HTTP RoundTripper: %w", err)
|
||||
}
|
||||
@@ -422,30 +422,32 @@ func parseJWTPlaceholdersForUserInfo(ui *UserInfo, isAllowed bool) error {
|
||||
}
|
||||
|
||||
func validateJWTPlaceholdersForURL(up *URLPrefix, isAllowed bool) error {
|
||||
for _, bu := range up.busOriginal {
|
||||
ok := strings.Contains(bu.Path, placeholderPrefix)
|
||||
if ok && !isAllowed {
|
||||
return fmt.Errorf("placeholder: %q is only allowed at JWT token context", bu.Path)
|
||||
}
|
||||
if ok {
|
||||
p := bu.Path
|
||||
for _, ph := range allPlaceholders {
|
||||
p = strings.ReplaceAll(p, ph, ``)
|
||||
for _, spec := range up.busOriginal {
|
||||
for _, bu := range spec.urls {
|
||||
ok := strings.Contains(bu.Path, placeholderPrefix)
|
||||
if ok && !isAllowed {
|
||||
return fmt.Errorf("placeholder: %q is only allowed at JWT token context", bu.Path)
|
||||
}
|
||||
if strings.Contains(p, placeholderPrefix) {
|
||||
return fmt.Errorf("invalid placeholder found in URL request path: %q, supported values are: %s", bu.Path, strings.Join(allPlaceholders, ", "))
|
||||
}
|
||||
}
|
||||
for param, values := range bu.Query() {
|
||||
for _, value := range values {
|
||||
ok := strings.Contains(value, placeholderPrefix)
|
||||
if ok && !isAllowed {
|
||||
return fmt.Errorf("query param: %q with placeholder: %q is only allowed at JWT token context", param, value)
|
||||
if ok {
|
||||
p := bu.Path
|
||||
for _, ph := range allPlaceholders {
|
||||
p = strings.ReplaceAll(p, ph, ``)
|
||||
}
|
||||
if ok {
|
||||
// possible placeholder
|
||||
if !slices.Contains(allPlaceholders, value) {
|
||||
return fmt.Errorf("query param: %q has unsupported placeholder string: %q, supported values are: %s", param, value, strings.Join(allPlaceholders, ", "))
|
||||
if strings.Contains(p, placeholderPrefix) {
|
||||
return fmt.Errorf("invalid placeholder found in URL request path: %q, supported values are: %s", bu.Path, strings.Join(allPlaceholders, ", "))
|
||||
}
|
||||
}
|
||||
for param, values := range bu.Query() {
|
||||
for _, value := range values {
|
||||
ok := strings.Contains(value, placeholderPrefix)
|
||||
if ok && !isAllowed {
|
||||
return fmt.Errorf("query param: %q with placeholder: %q is only allowed at JWT token context", param, value)
|
||||
}
|
||||
if ok {
|
||||
// possible placeholder
|
||||
if !slices.Contains(allPlaceholders, value) {
|
||||
return fmt.Errorf("query param: %q has unsupported placeholder string: %q, supported values are: %s", param, value, strings.Join(allPlaceholders, ", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,6 +431,11 @@ func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo, tkn *j
|
||||
if bu == nil {
|
||||
break
|
||||
}
|
||||
if !bu.group.beginConcurrencyLimit() {
|
||||
// The backend group hit its own max_concurrent_requests limit - skip it and try another backend.
|
||||
bu.put()
|
||||
continue
|
||||
}
|
||||
targetURL := bu.url
|
||||
if tkn != nil {
|
||||
vmac := tkn.VMAccess()
|
||||
@@ -459,6 +464,7 @@ func processRequest(w http.ResponseWriter, r *http.Request, ui *UserInfo, tkn *j
|
||||
goto again
|
||||
}
|
||||
|
||||
bu.group.endConcurrencyLimit()
|
||||
bu.put()
|
||||
if ok {
|
||||
return
|
||||
@@ -493,7 +499,11 @@ func tryProcessingRequest(w http.ResponseWriter, r *http.Request, targetURL *url
|
||||
bb, bbOK := req.Body.(*bufferedBody)
|
||||
canRetry := !bbOK || bb.canRetry()
|
||||
|
||||
res, err := ui.rt.RoundTrip(req)
|
||||
rt := bu.group.rt
|
||||
if rt == nil {
|
||||
rt = ui.rt
|
||||
}
|
||||
res, err := rt.RoundTrip(req)
|
||||
if err == nil {
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
}
|
||||
@@ -711,25 +721,25 @@ var (
|
||||
bufferRequestBodyDuration = metrics.NewSummary(`vmauth_buffer_request_body_duration_seconds`)
|
||||
)
|
||||
|
||||
func newRoundTripper(caFileOpt, certFileOpt, keyFileOpt, serverNameOpt string, insecureSkipVerifyP *bool) (http.RoundTripper, error) {
|
||||
func newRoundTripper(bs BackendSettings) (http.RoundTripper, error) {
|
||||
caFile := *backendTLSCAFile
|
||||
if caFileOpt != "" {
|
||||
caFile = caFileOpt
|
||||
if bs.TLSCAFile != "" {
|
||||
caFile = bs.TLSCAFile
|
||||
}
|
||||
certFile := *backendTLSCertFile
|
||||
if certFileOpt != "" {
|
||||
certFile = certFileOpt
|
||||
if bs.TLSCertFile != "" {
|
||||
certFile = bs.TLSCertFile
|
||||
}
|
||||
keyFile := *backendTLSKeyFile
|
||||
if keyFileOpt != "" {
|
||||
keyFile = keyFileOpt
|
||||
if bs.TLSKeyFile != "" {
|
||||
keyFile = bs.TLSKeyFile
|
||||
}
|
||||
serverName := *backendTLSServerName
|
||||
if serverNameOpt != "" {
|
||||
serverName = serverNameOpt
|
||||
if bs.TLSServerName != "" {
|
||||
serverName = bs.TLSServerName
|
||||
}
|
||||
insecureSkipVerify := *backendTLSInsecureSkipVerify
|
||||
if p := insecureSkipVerifyP; p != nil {
|
||||
if p := bs.TLSInsecureSkipVerify; p != nil {
|
||||
insecureSkipVerify = *p
|
||||
}
|
||||
opts := &promauth.Options{
|
||||
|
||||
@@ -590,6 +590,65 @@ X-Forwarded-For: 12.34.56.78, 42.2.3.84`
|
||||
f(cfgStr, requestURL, backendHandler, responseExpected)
|
||||
}
|
||||
|
||||
func TestRequestHandlerSkipsBackendGroupAtConcurrencyLimit(t *testing.T) {
|
||||
tsA := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "backend=A")
|
||||
}))
|
||||
defer tsA.Close()
|
||||
tsB := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "backend=B")
|
||||
}))
|
||||
defer tsB.Close()
|
||||
|
||||
cfgStr := fmt.Sprintf(`
|
||||
unauthorized_user:
|
||||
load_balancing_policy: first_available
|
||||
url_prefix:
|
||||
- url_prefix: %s/foo
|
||||
max_concurrent_requests: 1
|
||||
- %s/foo
|
||||
`, tsA.URL, tsB.URL)
|
||||
|
||||
cfgOrigP := authConfigData.Load()
|
||||
if _, err := reloadAuthConfigData([]byte(cfgStr)); err != nil {
|
||||
t.Fatalf("cannot load config data: %s", err)
|
||||
}
|
||||
defer func() {
|
||||
cfgOrig := []byte("unauthorized_user:\n url_prefix: http://foo/bar")
|
||||
if cfgOrigP != nil {
|
||||
cfgOrig = *cfgOrigP
|
||||
}
|
||||
if _, err := reloadAuthConfigData(cfgOrig); err != nil {
|
||||
t.Fatalf("cannot load the original config: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
ac := authConfig.Load()
|
||||
up := ac.UnauthorizedUser.URLPrefix
|
||||
g0 := up.bus.Load().groups[0]
|
||||
if cap(g0.concurrencyLimitCh) != 1 {
|
||||
t.Fatalf("unexpected group0 concurrency limit; got %d; want 1", cap(g0.concurrencyLimitCh))
|
||||
}
|
||||
// Saturate group0's concurrency limit, simulating an in-flight request to backend A.
|
||||
g0.concurrencyLimitCh <- struct{}{}
|
||||
defer func() { <-g0.concurrencyLimitCh }()
|
||||
|
||||
r, err := http.NewRequest(http.MethodGet, "http://some-host.com/abc", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot initialize http request: %s", err)
|
||||
}
|
||||
r.RequestURI = r.URL.RequestURI()
|
||||
w := &fakeResponseWriter{}
|
||||
if !requestHandlerWithInternalRoutes(w, r) {
|
||||
t.Fatalf("unexpected false is returned from requestHandler")
|
||||
}
|
||||
|
||||
resp := w.getResponse()
|
||||
if !strings.Contains(resp, "backend=B") {
|
||||
t.Fatalf("expecting the request to be routed to backend B, since backend A's group is at its concurrency limit; got response:\n%s", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJWTRequestHandler(t *testing.T) {
|
||||
// Generate RSA key pair for testing
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
|
||||
@@ -87,7 +87,7 @@ func TestCreateTargetURLSuccess(t *testing.T) {
|
||||
expectedRetryStatusCodes []int, expectedLoadBalancingPolicy string, expectedDropSrcPathPrefixParts int) {
|
||||
t.Helper()
|
||||
|
||||
if err := ui.initURLs(); err != nil {
|
||||
if err := ui.initURLs(nil); err != nil {
|
||||
t.Fatalf("cannot initialize urls inside UserInfo: %s", err)
|
||||
}
|
||||
u, err := url.Parse(requestURI)
|
||||
@@ -172,8 +172,10 @@ func TestCreateTargetURLSuccess(t *testing.T) {
|
||||
mustNewHeader("'x: y'"),
|
||||
},
|
||||
},
|
||||
RetryStatusCodes: []int{503, 501},
|
||||
LoadBalancingPolicy: "first_available",
|
||||
RetryStatusCodes: []int{503, 501},
|
||||
BackendSettings: BackendSettings{
|
||||
LoadBalancingPolicy: "first_available",
|
||||
},
|
||||
DropSrcPathPrefixParts: new(2),
|
||||
}, "/a/b/c", "http://foo.bar/c", `bb: aaa`, `x: y`, []int{503, 501}, "first_available", 2)
|
||||
f(&UserInfo{
|
||||
@@ -402,10 +404,12 @@ func TestUserInfoGetBackendURL_SRV(t *testing.T) {
|
||||
URLPrefix: mustParseURL("http://vminsert:8480"),
|
||||
},
|
||||
},
|
||||
DiscoverBackendIPs: &allowed,
|
||||
URLPrefix: mustParseURL("http://non-exist-dns-addr"),
|
||||
BackendSettings: BackendSettings{
|
||||
DiscoverBackendIPs: &allowed,
|
||||
},
|
||||
URLPrefix: mustParseURL("http://non-exist-dns-addr"),
|
||||
}
|
||||
if err := ui.initURLs(); err != nil {
|
||||
if err := ui.initURLs(nil); err != nil {
|
||||
t.Fatalf("cannot initialize urls inside UserInfo: %s", err)
|
||||
}
|
||||
|
||||
@@ -455,10 +459,12 @@ func TestUserInfoGetBackendURL_SRVZeroBackends(t *testing.T) {
|
||||
URLPrefix: mustParseURL("http://srv+vmselect"),
|
||||
},
|
||||
},
|
||||
DiscoverBackendIPs: &allowed,
|
||||
URLPrefix: mustParseURL("http://non-exist-dns-addr"),
|
||||
BackendSettings: BackendSettings{
|
||||
DiscoverBackendIPs: &allowed,
|
||||
},
|
||||
URLPrefix: mustParseURL("http://non-exist-dns-addr"),
|
||||
}
|
||||
if err := ui.initURLs(); err != nil {
|
||||
if err := ui.initURLs(nil); err != nil {
|
||||
t.Fatalf("cannot initialize urls inside UserInfo: %s", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ See also [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-rel
|
||||
|
||||
## tip
|
||||
|
||||
* FEATURE: [vmauth](https://docs.victoriametrics.com/victoriametrics/vmauth/): allow specifying individual `url_prefix` list items as a mapping instead of a plain url string, so a group of backend urls can override `load_balancing_policy`, `discover_backend_ips`, `max_concurrent_requests` and `tls_*` options, instead of inheriting them from the enclosing `user` / `url_map` scope. This makes it possible, for example, to prefer a primary backend group over a standby one via the outer `load_balancing_policy`, while independently load-balancing across the targets discovered for each group. Such a group can also be given an optional `name`, used to identify it in [metrics](https://docs.victoriametrics.com/victoriametrics/vmauth/#concurrency-limiting) (falls back to its ordinal position in `url_prefix` when not set). See [these docs](https://docs.victoriametrics.com/victoriametrics/vmauth/#load-balancing) for more details.
|
||||
|
||||
## [v1.148.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.148.0)
|
||||
|
||||
Released at 2026-07-20
|
||||
|
||||
@@ -946,6 +946,38 @@ Each `url_prefix` in the [-auth.config](#auth-config) can be specified in the fo
|
||||
load_balancing_policy: first_available
|
||||
```
|
||||
|
||||
The `load_balancing_policy` option (and the `-loadBalancingPolicy` command-line flag) selects among the backends listed under `url_prefix`.
|
||||
Each `url_prefix` list item can also be specified as a mapping instead of a plain url string, letting a group of backend urls override
|
||||
`load_balancing_policy`, `discover_backend_ips`, `max_concurrent_requests` and `tls_*` options for that particular group, instead of inheriting
|
||||
them from the enclosing `user` / `url_map` scope. This is useful when a single backend is expanded into multiple discovered targets via
|
||||
[`discover_backend_ips`](#discovering-backend-ips): the outer `load_balancing_policy` still selects among the configured backends,
|
||||
while the group's own `load_balancing_policy` selects among the targets discovered for that particular backend.
|
||||
For example, the following config uses `first_available` to pick between the two availability zones, while spreading load evenly across the instances discovered in the active zone:
|
||||
|
||||
```yaml
|
||||
unauthorized_user:
|
||||
load_balancing_policy: first_available
|
||||
url_prefix:
|
||||
- name: az1
|
||||
url_prefix: http://vmselect-az1:8481/
|
||||
discover_backend_ips: true
|
||||
load_balancing_policy: least_loaded
|
||||
- name: az2
|
||||
url_prefix: http://vmselect-az2:8481/
|
||||
discover_backend_ips: true
|
||||
load_balancing_policy: least_loaded
|
||||
```
|
||||
|
||||
A backend group mapping supports the following options, all of which are optional and fall back to the value inherited from the
|
||||
enclosing `user` / `url_map` scope when omitted:
|
||||
|
||||
* `url_prefix` - a single url or a list of urls for this group. This is the only required field.
|
||||
* `name` - optionally identifies this group in its [metrics](#monitoring) (see `max_concurrent_requests` below). When not set, the group's ordinal position in `url_prefix` (`0`, `1`, ...) is used instead.
|
||||
* `load_balancing_policy` - overrides the policy used for selecting among this group's own targets (its static urls, or the ones discovered via `discover_backend_ips`).
|
||||
* `discover_backend_ips` - overrides whether backend IPs are discovered for this group. See [discovering backend IPs](#discovering-backend-ips).
|
||||
* `max_concurrent_requests` - limits the number of concurrent requests proxied to this group specifically, on top of the `user`-level and global concurrency limits. Requests that hit this limit are sent to another backend instead of being queued or rejected - see [concurrency limiting](#concurrency-limiting).
|
||||
* `tls_ca_file`, `tls_cert_file`, `tls_key_file`, `tls_server_name`, `tls_insecure_skip_verify` - override the TLS settings used when connecting to this group's backends. See [backend TLS setup](#backend-tls-setup).
|
||||
|
||||
The load balancing feature can be used in the following cases:
|
||||
|
||||
* Balancing the load among multiple `vmselect` and/or `vminsert` nodes in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/).
|
||||
@@ -1021,7 +1053,11 @@ There are the following solutions for this issue:
|
||||
|
||||
This functionality is useful for balancing load across backend instances running on different TCP ports, since DNS SRV records include TCP ports.
|
||||
|
||||
The `discover_backend_ips` option can be specified at `user` and `url_map` level in the [`-auth.config`](#auth-config). It can also be enabled globally via the `-discoverBackendIPs` command-line flag.
|
||||
The `discover_backend_ips` option can be specified at `user`, `url_map`, and individual backend group level in the [`-auth.config`](#auth-config) (see [load balancing docs](#load-balancing)
|
||||
for the backend group mapping syntax). It can also be enabled globally via the `-discoverBackendIPs` command-line flag.
|
||||
|
||||
By default, `vmauth` uses the same load balancing policy for selecting among the discovered targets of a backend as the one used for selecting among the configured backends themselves.
|
||||
This can be changed via the `load_balancing_policy` option inside a backend group mapping. See [these docs](#load-balancing) for more details.
|
||||
|
||||
See also [load balancing docs](#load-balancing).
|
||||
|
||||
@@ -1146,6 +1182,16 @@ The following [metrics](https://docs.victoriametrics.com/victoriametrics/vmauth/
|
||||
* `vmauth_unauthorized_user_concurrent_requests_limit_reached_total` - the number of requests rejected with `429 Too Many Requests` error
|
||||
because the concurrency limit has been reached for unauthorized users (if the `unauthorized_user` section is used).
|
||||
|
||||
It is also possible to limit the number of concurrent requests proxied to an individual backend group via the `max_concurrent_requests` option
|
||||
inside a [backend group mapping](#load-balancing). Unlike the per-user and global limits above, hitting a backend group's own limit doesn't queue
|
||||
the request or return `429 Too Many Requests` - `vmauth` immediately tries another backend instead, since the goal is to protect a single backend
|
||||
from overload while still using the rest of the configured backends. The `backend_group` label below is the group's `name` if set, or its ordinal
|
||||
position in `url_prefix` (`0`, `1`, ...) otherwise. The following metrics are exposed for backend groups which set `max_concurrent_requests`:
|
||||
|
||||
* `vmauth_backend_group_concurrent_requests_capacity{backend_group="..."}` - the configured `max_concurrent_requests` limit for the given backend group.
|
||||
* `vmauth_backend_group_concurrent_requests_current{backend_group="..."}` - the current number of concurrent requests proxied to the given backend group.
|
||||
* `vmauth_backend_group_concurrent_requests_limit_reached_total{backend_group="..."}` - the number of times the given backend group's limit was reached and the request was retried at another backend.
|
||||
|
||||
See also [request body buffering](https://docs.victoriametrics.com/victoriametrics/vmauth/#request-body-buffering).
|
||||
|
||||
## Request body buffering
|
||||
@@ -1213,6 +1259,9 @@ By default, `vmauth` uses system settings when performing requests to HTTPS back
|
||||
The `-backend.tlsCAFile`, `-backend.tlsCertFile`, `-backend.tlsKeyFile`, `tls_ca_file`, `tls_cert_file`, and `tls_key_file` may point either to a local file or to a `http` / `https` URL.
|
||||
The file is checked for modifications every second and is automatically re-read when it is updated.
|
||||
|
||||
Any of the `tls_*` options above can also be set on an individual backend group inside `url_prefix` (see the [backend group mapping](#load-balancing) syntax),
|
||||
overriding the per-user settings for that group only. This is useful when different backends behind a single user require different TLS credentials.
|
||||
|
||||
## IP filters
|
||||
|
||||
The [Enterprise version](https://docs.victoriametrics.com/victoriametrics/enterprise/) of `vmauth` can be configured to allow/deny incoming requests via global and per-user IP filters.
|
||||
|
||||
Reference in New Issue
Block a user