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