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 }