github.com/muhammadn/cortex@v1.9.1-0.20220510110439-46bb7000d03d/pkg/frontend/v1/frontend.go (about)

     1  package v1
     2  
     3  import (
     4  	"context"
     5  	"flag"
     6  	"fmt"
     7  	"net/http"
     8  	"time"
     9  
    10  	"github.com/go-kit/log"
    11  	"github.com/go-kit/log/level"
    12  	"github.com/grafana/dskit/services"
    13  	"github.com/opentracing/opentracing-go"
    14  	"github.com/pkg/errors"
    15  	"github.com/prometheus/client_golang/prometheus"
    16  	"github.com/prometheus/client_golang/prometheus/promauto"
    17  	"github.com/weaveworks/common/httpgrpc"
    18  
    19  	"github.com/cortexproject/cortex/pkg/frontend/v1/frontendv1pb"
    20  	"github.com/cortexproject/cortex/pkg/querier/stats"
    21  	"github.com/cortexproject/cortex/pkg/scheduler/queue"
    22  	"github.com/cortexproject/cortex/pkg/tenant"
    23  	"github.com/cortexproject/cortex/pkg/util"
    24  	"github.com/cortexproject/cortex/pkg/util/httpgrpcutil"
    25  	"github.com/cortexproject/cortex/pkg/util/validation"
    26  )
    27  
    28  var (
    29  	errTooManyRequest = httpgrpc.Errorf(http.StatusTooManyRequests, "too many outstanding requests")
    30  )
    31  
    32  // Config for a Frontend.
    33  type Config struct {
    34  	MaxOutstandingPerTenant int           `yaml:"max_outstanding_per_tenant"`
    35  	QuerierForgetDelay      time.Duration `yaml:"querier_forget_delay"`
    36  }
    37  
    38  // RegisterFlags adds the flags required to config this to the given FlagSet.
    39  func (cfg *Config) RegisterFlags(f *flag.FlagSet) {
    40  	f.IntVar(&cfg.MaxOutstandingPerTenant, "querier.max-outstanding-requests-per-tenant", 100, "Maximum number of outstanding requests per tenant per frontend; requests beyond this error with HTTP 429.")
    41  	f.DurationVar(&cfg.QuerierForgetDelay, "query-frontend.querier-forget-delay", 0, "If a querier disconnects without sending notification about graceful shutdown, the query-frontend will keep the querier in the tenant's shard until the forget delay has passed. This feature is useful to reduce the blast radius when shuffle-sharding is enabled.")
    42  }
    43  
    44  type Limits interface {
    45  	// Returns max queriers to use per tenant, or 0 if shuffle sharding is disabled.
    46  	MaxQueriersPerUser(user string) int
    47  }
    48  
    49  // Frontend queues HTTP requests, dispatches them to backends, and handles retries
    50  // for requests which failed.
    51  type Frontend struct {
    52  	services.Service
    53  
    54  	cfg    Config
    55  	log    log.Logger
    56  	limits Limits
    57  
    58  	requestQueue *queue.RequestQueue
    59  	activeUsers  *util.ActiveUsersCleanupService
    60  
    61  	// Subservices manager.
    62  	subservices        *services.Manager
    63  	subservicesWatcher *services.FailureWatcher
    64  
    65  	// Metrics.
    66  	queueLength       *prometheus.GaugeVec
    67  	discardedRequests *prometheus.CounterVec
    68  	numClients        prometheus.GaugeFunc
    69  	queueDuration     prometheus.Histogram
    70  }
    71  
    72  type request struct {
    73  	enqueueTime time.Time
    74  	queueSpan   opentracing.Span
    75  	originalCtx context.Context
    76  
    77  	request  *httpgrpc.HTTPRequest
    78  	err      chan error
    79  	response chan *httpgrpc.HTTPResponse
    80  }
    81  
    82  // New creates a new frontend. Frontend implements service, and must be started and stopped.
    83  func New(cfg Config, limits Limits, log log.Logger, registerer prometheus.Registerer) (*Frontend, error) {
    84  	f := &Frontend{
    85  		cfg:    cfg,
    86  		log:    log,
    87  		limits: limits,
    88  		queueLength: promauto.With(registerer).NewGaugeVec(prometheus.GaugeOpts{
    89  			Name: "cortex_query_frontend_queue_length",
    90  			Help: "Number of queries in the queue.",
    91  		}, []string{"user"}),
    92  		discardedRequests: promauto.With(registerer).NewCounterVec(prometheus.CounterOpts{
    93  			Name: "cortex_query_frontend_discarded_requests_total",
    94  			Help: "Total number of query requests discarded.",
    95  		}, []string{"user"}),
    96  		queueDuration: promauto.With(registerer).NewHistogram(prometheus.HistogramOpts{
    97  			Name:    "cortex_query_frontend_queue_duration_seconds",
    98  			Help:    "Time spend by requests queued.",
    99  			Buckets: prometheus.DefBuckets,
   100  		}),
   101  	}
   102  
   103  	f.requestQueue = queue.NewRequestQueue(cfg.MaxOutstandingPerTenant, cfg.QuerierForgetDelay, f.queueLength, f.discardedRequests)
   104  	f.activeUsers = util.NewActiveUsersCleanupWithDefaultValues(f.cleanupInactiveUserMetrics)
   105  
   106  	var err error
   107  	f.subservices, err = services.NewManager(f.requestQueue, f.activeUsers)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  
   112  	f.numClients = promauto.With(registerer).NewGaugeFunc(prometheus.GaugeOpts{
   113  		Name: "cortex_query_frontend_connected_clients",
   114  		Help: "Number of worker clients currently connected to the frontend.",
   115  	}, f.requestQueue.GetConnectedQuerierWorkersMetric)
   116  
   117  	f.Service = services.NewBasicService(f.starting, f.running, f.stopping)
   118  	return f, nil
   119  }
   120  
   121  func (f *Frontend) starting(ctx context.Context) error {
   122  	f.subservicesWatcher.WatchManager(f.subservices)
   123  
   124  	if err := services.StartManagerAndAwaitHealthy(ctx, f.subservices); err != nil {
   125  		return errors.Wrap(err, "unable to start frontend subservices")
   126  	}
   127  
   128  	return nil
   129  }
   130  
   131  func (f *Frontend) running(ctx context.Context) error {
   132  	for {
   133  		select {
   134  		case <-ctx.Done():
   135  			return nil
   136  		case err := <-f.subservicesWatcher.Chan():
   137  			return errors.Wrap(err, "frontend subservice failed")
   138  		}
   139  	}
   140  }
   141  
   142  func (f *Frontend) stopping(_ error) error {
   143  	// This will also stop the requests queue, which stop accepting new requests and errors out any pending requests.
   144  	return services.StopManagerAndAwaitStopped(context.Background(), f.subservices)
   145  }
   146  
   147  func (f *Frontend) cleanupInactiveUserMetrics(user string) {
   148  	f.queueLength.DeleteLabelValues(user)
   149  	f.discardedRequests.DeleteLabelValues(user)
   150  }
   151  
   152  // RoundTripGRPC round trips a proto (instead of a HTTP request).
   153  func (f *Frontend) RoundTripGRPC(ctx context.Context, req *httpgrpc.HTTPRequest) (*httpgrpc.HTTPResponse, error) {
   154  	// Propagate trace context in gRPC too - this will be ignored if using HTTP.
   155  	tracer, span := opentracing.GlobalTracer(), opentracing.SpanFromContext(ctx)
   156  	if tracer != nil && span != nil {
   157  		carrier := (*httpgrpcutil.HttpgrpcHeadersCarrier)(req)
   158  		err := tracer.Inject(span.Context(), opentracing.HTTPHeaders, carrier)
   159  		if err != nil {
   160  			return nil, err
   161  		}
   162  	}
   163  
   164  	request := request{
   165  		request:     req,
   166  		originalCtx: ctx,
   167  
   168  		// Buffer of 1 to ensure response can be written by the server side
   169  		// of the Process stream, even if this goroutine goes away due to
   170  		// client context cancellation.
   171  		err:      make(chan error, 1),
   172  		response: make(chan *httpgrpc.HTTPResponse, 1),
   173  	}
   174  
   175  	if err := f.queueRequest(ctx, &request); err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	select {
   180  	case <-ctx.Done():
   181  		return nil, ctx.Err()
   182  
   183  	case resp := <-request.response:
   184  		return resp, nil
   185  
   186  	case err := <-request.err:
   187  		return nil, err
   188  	}
   189  }
   190  
   191  // Process allows backends to pull requests from the frontend.
   192  func (f *Frontend) Process(server frontendv1pb.Frontend_ProcessServer) error {
   193  	querierID, err := getQuerierID(server)
   194  	if err != nil {
   195  		return err
   196  	}
   197  
   198  	f.requestQueue.RegisterQuerierConnection(querierID)
   199  	defer f.requestQueue.UnregisterQuerierConnection(querierID)
   200  
   201  	// If the downstream request(from querier -> frontend) is cancelled,
   202  	// we need to ping the condition variable to unblock getNextRequestForQuerier.
   203  	// Ideally we'd have ctx aware condition variables...
   204  	go func() {
   205  		<-server.Context().Done()
   206  		f.requestQueue.QuerierDisconnecting()
   207  	}()
   208  
   209  	lastUserIndex := queue.FirstUser()
   210  
   211  	for {
   212  		reqWrapper, idx, err := f.requestQueue.GetNextRequestForQuerier(server.Context(), lastUserIndex, querierID)
   213  		if err != nil {
   214  			return err
   215  		}
   216  		lastUserIndex = idx
   217  
   218  		req := reqWrapper.(*request)
   219  
   220  		f.queueDuration.Observe(time.Since(req.enqueueTime).Seconds())
   221  		req.queueSpan.Finish()
   222  
   223  		/*
   224  		  We want to dequeue the next unexpired request from the chosen tenant queue.
   225  		  The chance of choosing a particular tenant for dequeueing is (1/active_tenants).
   226  		  This is problematic under load, especially with other middleware enabled such as
   227  		  querier.split-by-interval, where one request may fan out into many.
   228  		  If expired requests aren't exhausted before checking another tenant, it would take
   229  		  n_active_tenants * n_expired_requests_at_front_of_queue requests being processed
   230  		  before an active request was handled for the tenant in question.
   231  		  If this tenant meanwhile continued to queue requests,
   232  		  it's possible that it's own queue would perpetually contain only expired requests.
   233  		*/
   234  		if req.originalCtx.Err() != nil {
   235  			lastUserIndex = lastUserIndex.ReuseLastUser()
   236  			continue
   237  		}
   238  
   239  		// Handle the stream sending & receiving on a goroutine so we can
   240  		// monitoring the contexts in a select and cancel things appropriately.
   241  		resps := make(chan *frontendv1pb.ClientToFrontend, 1)
   242  		errs := make(chan error, 1)
   243  		go func() {
   244  			err = server.Send(&frontendv1pb.FrontendToClient{
   245  				Type:         frontendv1pb.HTTP_REQUEST,
   246  				HttpRequest:  req.request,
   247  				StatsEnabled: stats.IsEnabled(req.originalCtx),
   248  			})
   249  			if err != nil {
   250  				errs <- err
   251  				return
   252  			}
   253  
   254  			resp, err := server.Recv()
   255  			if err != nil {
   256  				errs <- err
   257  				return
   258  			}
   259  
   260  			resps <- resp
   261  		}()
   262  
   263  		select {
   264  		// If the upstream request is cancelled, we need to cancel the
   265  		// downstream req.  Only way we can do that is to close the stream.
   266  		// The worker client is expecting this semantics.
   267  		case <-req.originalCtx.Done():
   268  			return req.originalCtx.Err()
   269  
   270  		// Is there was an error handling this request due to network IO,
   271  		// then error out this upstream request _and_ stream.
   272  		case err := <-errs:
   273  			req.err <- err
   274  			return err
   275  
   276  		// Happy path: merge the stats and propagate the response.
   277  		case resp := <-resps:
   278  			if stats.ShouldTrackHTTPGRPCResponse(resp.HttpResponse) {
   279  				stats := stats.FromContext(req.originalCtx)
   280  				stats.Merge(resp.Stats) // Safe if stats is nil.
   281  			}
   282  
   283  			req.response <- resp.HttpResponse
   284  		}
   285  	}
   286  }
   287  
   288  func (f *Frontend) NotifyClientShutdown(_ context.Context, req *frontendv1pb.NotifyClientShutdownRequest) (*frontendv1pb.NotifyClientShutdownResponse, error) {
   289  	level.Info(f.log).Log("msg", "received shutdown notification from querier", "querier", req.GetClientID())
   290  	f.requestQueue.NotifyQuerierShutdown(req.GetClientID())
   291  
   292  	return &frontendv1pb.NotifyClientShutdownResponse{}, nil
   293  }
   294  
   295  func getQuerierID(server frontendv1pb.Frontend_ProcessServer) (string, error) {
   296  	err := server.Send(&frontendv1pb.FrontendToClient{
   297  		Type: frontendv1pb.GET_ID,
   298  		// Old queriers don't support GET_ID, and will try to use the request.
   299  		// To avoid confusing them, include dummy request.
   300  		HttpRequest: &httpgrpc.HTTPRequest{
   301  			Method: "GET",
   302  			Url:    "/invalid_request_sent_by_frontend",
   303  		},
   304  	})
   305  
   306  	if err != nil {
   307  		return "", err
   308  	}
   309  
   310  	resp, err := server.Recv()
   311  
   312  	// Old queriers will return empty string, which is fine. All old queriers will be
   313  	// treated as single querier with lot of connections.
   314  	// (Note: if resp is nil, GetClientID() returns "")
   315  	return resp.GetClientID(), err
   316  }
   317  
   318  func (f *Frontend) queueRequest(ctx context.Context, req *request) error {
   319  	tenantIDs, err := tenant.TenantIDs(ctx)
   320  	if err != nil {
   321  		return err
   322  	}
   323  
   324  	now := time.Now()
   325  	req.enqueueTime = now
   326  	req.queueSpan, _ = opentracing.StartSpanFromContext(ctx, "queued")
   327  
   328  	// aggregate the max queriers limit in the case of a multi tenant query
   329  	maxQueriers := validation.SmallestPositiveNonZeroIntPerTenant(tenantIDs, f.limits.MaxQueriersPerUser)
   330  
   331  	joinedTenantID := tenant.JoinTenantIDs(tenantIDs)
   332  	f.activeUsers.UpdateUserTimestamp(joinedTenantID, now)
   333  
   334  	err = f.requestQueue.EnqueueRequest(joinedTenantID, req, maxQueriers, nil)
   335  	if err == queue.ErrTooManyRequests {
   336  		return errTooManyRequest
   337  	}
   338  	return err
   339  }
   340  
   341  // CheckReady determines if the query frontend is ready.  Function parameters/return
   342  // chosen to match the same method in the ingester
   343  func (f *Frontend) CheckReady(_ context.Context) error {
   344  	// if we have more than one querier connected we will consider ourselves ready
   345  	connectedClients := f.requestQueue.GetConnectedQuerierWorkersMetric()
   346  	if connectedClients > 0 {
   347  		return nil
   348  	}
   349  
   350  	msg := fmt.Sprintf("not ready: number of queriers connected to query-frontend is %d", int64(connectedClients))
   351  	level.Info(f.log).Log("msg", msg)
   352  	return errors.New(msg)
   353  }