fix: add api level throttler for LIST calls
This commit is contained in:
parent
dc1a46e5d2
commit
5151c429e4
|
@ -36,7 +36,7 @@ const (
|
||||||
|
|
||||||
// adminAPIHandlers provides HTTP handlers for MinIO admin API.
|
// adminAPIHandlers provides HTTP handlers for MinIO admin API.
|
||||||
type adminAPIHandlers struct {
|
type adminAPIHandlers struct {
|
||||||
mu sync.Mutex
|
mu *sync.Mutex
|
||||||
healSetsMap map[string]healInitSetParams
|
healSetsMap map[string]healInitSetParams
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +44,7 @@ type adminAPIHandlers struct {
|
||||||
func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool) {
|
func registerAdminRouter(router *mux.Router, enableConfigOps, enableIAMOps bool) {
|
||||||
|
|
||||||
adminAPI := adminAPIHandlers{
|
adminAPI := adminAPIHandlers{
|
||||||
|
mu: &sync.Mutex{},
|
||||||
healSetsMap: make(map[string]healInitSetParams),
|
healSetsMap: make(map[string]healInitSetParams),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,9 +19,14 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/minio/minio/cmd/config/api"
|
||||||
xhttp "github.com/minio/minio/cmd/http"
|
xhttp "github.com/minio/minio/cmd/http"
|
||||||
|
"github.com/minio/minio/pkg/env"
|
||||||
"github.com/minio/minio/pkg/wildcard"
|
"github.com/minio/minio/pkg/wildcard"
|
||||||
"github.com/rs/cors"
|
"github.com/rs/cors"
|
||||||
)
|
)
|
||||||
|
@ -48,6 +53,7 @@ func newCachedObjectLayerFn() CacheObjectLayer {
|
||||||
type objectAPIHandlers struct {
|
type objectAPIHandlers struct {
|
||||||
ObjectAPI func() ObjectLayer
|
ObjectAPI func() ObjectLayer
|
||||||
CacheAPI func() CacheObjectLayer
|
CacheAPI func() CacheObjectLayer
|
||||||
|
Throttler map[string]chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getHost tries its best to return the request host.
|
// getHost tries its best to return the request host.
|
||||||
|
@ -60,12 +66,39 @@ func getHost(r *http.Request) string {
|
||||||
return r.Host
|
return r.Host
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// api throttler constants
|
||||||
|
const (
|
||||||
|
listAPI = "LIST"
|
||||||
|
|
||||||
|
granularDeadline = 10 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseThrottler(throttle string) map[string]chan struct{} {
|
||||||
|
th := make(map[string]chan struct{})
|
||||||
|
for _, v := range strings.Split(throttle, ";") {
|
||||||
|
vs := strings.SplitN(v, "=", 2)
|
||||||
|
if len(vs) == 2 {
|
||||||
|
l, err := strconv.Atoi(vs[1])
|
||||||
|
if err == nil {
|
||||||
|
if l >= len(globalEndpoints.Hostnames()) {
|
||||||
|
l /= len(globalEndpoints.Hostnames())
|
||||||
|
} else {
|
||||||
|
l = 1
|
||||||
|
}
|
||||||
|
th[vs[0]] = make(chan struct{}, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return th
|
||||||
|
}
|
||||||
|
|
||||||
// registerAPIRouter - registers S3 compatible APIs.
|
// registerAPIRouter - registers S3 compatible APIs.
|
||||||
func registerAPIRouter(router *mux.Router) {
|
func registerAPIRouter(router *mux.Router) {
|
||||||
// Initialize API.
|
// Initialize API.
|
||||||
api := objectAPIHandlers{
|
api := objectAPIHandlers{
|
||||||
ObjectAPI: newObjectLayerFn,
|
ObjectAPI: newObjectLayerFn,
|
||||||
CacheAPI: newCachedObjectLayerFn,
|
CacheAPI: newCachedObjectLayerFn,
|
||||||
|
Throttler: parseThrottler(env.Get(api.EnvAPIRequestsGranularMax, "")),
|
||||||
}
|
}
|
||||||
|
|
||||||
// API Router
|
// API Router
|
||||||
|
|
|
@ -67,7 +67,7 @@ func waitForLowHTTPReq(maxIO int, maxWait time.Duration) {
|
||||||
// Bucket notification and http trace are not costly, it is okay to ignore them
|
// Bucket notification and http trace are not costly, it is okay to ignore them
|
||||||
// while counting the number of concurrent connections
|
// while counting the number of concurrent connections
|
||||||
maxIOFn := func() int {
|
maxIOFn := func() int {
|
||||||
return maxIO + int(globalHTTPListen.NumSubscribers()) + int(globalHTTPTrace.NumSubscribers())
|
return maxIO + globalHTTPListen.NumSubscribers() + globalHTTPTrace.NumSubscribers()
|
||||||
}
|
}
|
||||||
|
|
||||||
if httpServer := newHTTPServerFn(); httpServer != nil {
|
if httpServer := newHTTPServerFn(); httpServer != nil {
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
|
@ -82,9 +83,27 @@ func validateListObjectsArgs(marker, delimiter, encodingType string, maxKeys int
|
||||||
// of the versions of objects in a bucket.
|
// of the versions of objects in a bucket.
|
||||||
func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListObjectVersions")
|
ctx := newContext(r, w, "ListObjectVersions")
|
||||||
|
|
||||||
defer logger.AuditLog(w, r, "ListObjectVersions", mustGetClaimsFromToken(r))
|
defer logger.AuditLog(w, r, "ListObjectVersions", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
pool := api.Throttler[listAPI]
|
||||||
|
if pool != nil {
|
||||||
|
deadlineTimer := time.NewTimer(granularDeadline)
|
||||||
|
defer deadlineTimer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case pool <- struct{}{}:
|
||||||
|
defer func() { <-pool }()
|
||||||
|
case <-deadlineTimer.C:
|
||||||
|
// Send a http timeout message
|
||||||
|
writeErrorResponse(ctx, w,
|
||||||
|
errorCodes.ToAPIErr(ErrOperationMaxedOut),
|
||||||
|
r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
|
@ -119,6 +138,7 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
|
||||||
if forwardStr == "" {
|
if forwardStr == "" {
|
||||||
forwardStr = bucket
|
forwardStr = bucket
|
||||||
}
|
}
|
||||||
|
|
||||||
if proxyRequestByStringHash(ctx, w, r, forwardStr) {
|
if proxyRequestByStringHash(ctx, w, r, forwardStr) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -152,9 +172,27 @@ func (api objectAPIHandlers) ListObjectVersionsHandler(w http.ResponseWriter, r
|
||||||
// MinIO continues to support ListObjectsV1 and V2 for supporting legacy tools.
|
// MinIO continues to support ListObjectsV1 and V2 for supporting legacy tools.
|
||||||
func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListObjectsV2M")
|
ctx := newContext(r, w, "ListObjectsV2M")
|
||||||
|
|
||||||
defer logger.AuditLog(w, r, "ListObjectsV2M", mustGetClaimsFromToken(r))
|
defer logger.AuditLog(w, r, "ListObjectsV2M", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
pool := api.Throttler[listAPI]
|
||||||
|
if pool != nil {
|
||||||
|
deadlineTimer := time.NewTimer(granularDeadline)
|
||||||
|
defer deadlineTimer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case pool <- struct{}{}:
|
||||||
|
defer func() { <-pool }()
|
||||||
|
case <-deadlineTimer.C:
|
||||||
|
// Send a http timeout message
|
||||||
|
writeErrorResponse(ctx, w,
|
||||||
|
errorCodes.ToAPIErr(ErrOperationMaxedOut),
|
||||||
|
r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
|
@ -229,9 +267,27 @@ func (api objectAPIHandlers) ListObjectsV2MHandler(w http.ResponseWriter, r *htt
|
||||||
// MinIO continues to support ListObjectsV1 for supporting legacy tools.
|
// MinIO continues to support ListObjectsV1 for supporting legacy tools.
|
||||||
func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListObjectsV2")
|
ctx := newContext(r, w, "ListObjectsV2")
|
||||||
|
|
||||||
defer logger.AuditLog(w, r, "ListObjectsV2", mustGetClaimsFromToken(r))
|
defer logger.AuditLog(w, r, "ListObjectsV2", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
pool := api.Throttler[listAPI]
|
||||||
|
if pool != nil {
|
||||||
|
deadlineTimer := time.NewTimer(granularDeadline)
|
||||||
|
defer deadlineTimer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case pool <- struct{}{}:
|
||||||
|
defer func() { <-pool }()
|
||||||
|
case <-deadlineTimer.C:
|
||||||
|
// Send a http timeout message
|
||||||
|
writeErrorResponse(ctx, w,
|
||||||
|
errorCodes.ToAPIErr(ErrOperationMaxedOut),
|
||||||
|
r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
|
@ -358,9 +414,27 @@ func proxyRequestByStringHash(ctx context.Context, w http.ResponseWriter, r *htt
|
||||||
//
|
//
|
||||||
func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
|
func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := newContext(r, w, "ListObjectsV1")
|
ctx := newContext(r, w, "ListObjectsV1")
|
||||||
|
|
||||||
defer logger.AuditLog(w, r, "ListObjectsV1", mustGetClaimsFromToken(r))
|
defer logger.AuditLog(w, r, "ListObjectsV1", mustGetClaimsFromToken(r))
|
||||||
|
|
||||||
|
pool := api.Throttler[listAPI]
|
||||||
|
if pool != nil {
|
||||||
|
deadlineTimer := time.NewTimer(granularDeadline)
|
||||||
|
defer deadlineTimer.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case pool <- struct{}{}:
|
||||||
|
defer func() { <-pool }()
|
||||||
|
case <-deadlineTimer.C:
|
||||||
|
// Send a http timeout message
|
||||||
|
writeErrorResponse(ctx, w,
|
||||||
|
errorCodes.ToAPIErr(ErrOperationMaxedOut),
|
||||||
|
r.URL, guessIsBrowserReq(r))
|
||||||
|
return
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
bucket := vars["bucket"]
|
bucket := vars["bucket"]
|
||||||
|
|
||||||
|
@ -393,6 +467,7 @@ func (api objectAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http
|
||||||
if forwardStr == "" {
|
if forwardStr == "" {
|
||||||
forwardStr = bucket
|
forwardStr = bucket
|
||||||
}
|
}
|
||||||
|
|
||||||
if proxyRequestByStringHash(ctx, w, r, forwardStr) {
|
if proxyRequestByStringHash(ctx, w, r, forwardStr) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ const (
|
||||||
apiRemoteTransportDeadline = "remote_transport_deadline"
|
apiRemoteTransportDeadline = "remote_transport_deadline"
|
||||||
|
|
||||||
EnvAPIRequestsMax = "MINIO_API_REQUESTS_MAX"
|
EnvAPIRequestsMax = "MINIO_API_REQUESTS_MAX"
|
||||||
|
EnvAPIRequestsGranularMax = "MINIO_API_REQUESTS_GRANULAR_MAX"
|
||||||
EnvAPIRequestsDeadline = "MINIO_API_REQUESTS_DEADLINE"
|
EnvAPIRequestsDeadline = "MINIO_API_REQUESTS_DEADLINE"
|
||||||
EnvAPIClusterDeadline = "MINIO_API_CLUSTER_DEADLINE"
|
EnvAPIClusterDeadline = "MINIO_API_CLUSTER_DEADLINE"
|
||||||
EnvAPICorsAllowOrigin = "MINIO_API_CORS_ALLOW_ORIGIN"
|
EnvAPICorsAllowOrigin = "MINIO_API_CORS_ALLOW_ORIGIN"
|
||||||
|
|
|
@ -85,34 +85,34 @@ func (t *apiConfig) getClusterDeadline() time.Duration {
|
||||||
return t.clusterDeadline
|
return t.clusterDeadline
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *apiConfig) getRequestsPool() (chan struct{}, <-chan time.Time) {
|
func (t *apiConfig) getRequestsPool() (chan struct{}, time.Duration) {
|
||||||
t.mu.RLock()
|
t.mu.RLock()
|
||||||
defer t.mu.RUnlock()
|
defer t.mu.RUnlock()
|
||||||
|
|
||||||
if t.requestsPool == nil {
|
if t.requestsPool == nil {
|
||||||
return nil, nil
|
return nil, time.Duration(0)
|
||||||
}
|
|
||||||
if t.requestsDeadline <= 0 {
|
|
||||||
return t.requestsPool, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return t.requestsPool, time.NewTimer(t.requestsDeadline).C
|
return t.requestsPool, t.requestsDeadline
|
||||||
}
|
}
|
||||||
|
|
||||||
// maxClients throttles the S3 API calls
|
// maxClients throttles the S3 API calls
|
||||||
func maxClients(f http.HandlerFunc) http.HandlerFunc {
|
func maxClients(f http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
pool, deadlineTimer := globalAPIConfig.getRequestsPool()
|
pool, deadline := globalAPIConfig.getRequestsPool()
|
||||||
if pool == nil {
|
if pool == nil {
|
||||||
f.ServeHTTP(w, r)
|
f.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deadlineTimer := time.NewTimer(deadline)
|
||||||
|
defer deadlineTimer.Stop()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case pool <- struct{}{}:
|
case pool <- struct{}{}:
|
||||||
defer func() { <-pool }()
|
defer func() { <-pool }()
|
||||||
f.ServeHTTP(w, r)
|
f.ServeHTTP(w, r)
|
||||||
case <-deadlineTimer:
|
case <-deadlineTimer.C:
|
||||||
// Send a http timeout message
|
// Send a http timeout message
|
||||||
writeErrorResponse(r.Context(), w,
|
writeErrorResponse(r.Context(), w,
|
||||||
errorCodes.ToAPIErr(ErrOperationMaxedOut),
|
errorCodes.ToAPIErr(ErrOperationMaxedOut),
|
||||||
|
|
Loading…
Reference in a new issue