-
-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Prometheus metrics are redundant and slow #4644
Comments
I've looked through the other issues regarding Prometheus metrics and if host-based metrics might some day be implemented, they shouldn't affect performance that much. As for adding support for customizable metrics - seems like they would be an even bigger problem performance-wise |
Thanks for filing this @renbou - I'll try to find some time this coming week to dig into it. My gut feeling is that some of the issue is due to the way middleware is handled and nested by Caddy, so it might be a bit more tricky to fully fix. On the surface this is alarming:
However percentages can sometimes be misleading, so it would be good to get some actual numbers... I haven't looked at the profile yet (and I won't this weekend - recovering from jet lag). Caddy's handling of static responses in particular is very fast, and so it's not actually that surprising that the instrumentation surrounding it would account for more CPU time. As with all performance tuning, the most useful question isn't "how fast can we make it go?" but instead "how fast does it need to be?" In SRE terms that might translate to "we have a latency SLO of 100ms 99% of the time, but we can't reach that reliably if the metrics instrumentation takes 80ms" |
I might've solved how this and the other Prometheus issues/requests could be implemented/solved. As discussed previously in #4016, a new
The problem which surfaced previously is that we'd metrics would supposedly have to be lost on reloads. To solve this, we can globally store the currently existing When provisioning a server, we'll need to setup a new
When removing the old To solve another set of issues including performance, needing information from the matchers (for example the host-matcher), we can simply leverage the already present replacer. Since, as mentioned in the beginning, labels should be configurable with placeholders, we could simply add the necessary values to the request replacer during any time of the request. For example, a host matcher could set the value for "http.server.host", the handler instrumentation (which is what currently affects performance by running the same metric instrumentation code for each route) could set the value for "http.server.handler", etc. All of the keys accumulated during the request could then be used during metric collection, which would happen one time and be one of the first handlers in the middleware chain (thus giving it access to ALL of the gathered placeholders), by simply getting them from the replacer. The way of gathering metrics I've described here will also leave us with actually meaningful metrics about handlers (since the http.server.handler key will be set only once - after the handler which actually gave a response returns) and everything else, since they are gathered once per request instead of multiple times like they are now (which is why currently metrics are pretty useless). |
Worth noting that
I'm thinking we might want to support somekind of setup where you could say when to evaluate the labels, i.e. if you want to add a label at the start of the request vs at the end (after all the How would your described changes behave with existing metrics collected? My understanding was that the histograms etc may need to be reset on config reload or something if their config changed? I'm not sure I totally follow. @hairyhenderson should definitely chime in here. |
Currently As for when the metrics are collected what I meant was that the metrics handler would now be initialized only once as the first middleware in the chain, meaning that it'd start collecting metrics just before the request is handled and finish collecting them after, as well as use the replacer afterwards since it's filled with all of the needed placeholders at that point. Existing metrics are reset only if their labels change, pretty much. If the labels change, then their isn't a proper way to "migrate" metrics with the old label space to the new one, because they act like totally different metrics. For example, when adding a host label, it wouldn't make sense to "continue using the old metric" and just append a new label to it - the results wouldn't make sense (also this is what can't be done using the Actually, since currently Caddy's metrics to which we'd be adding the possibilities of label modification (pretty much request-specific histogram metrics) all depend on the same set of labels, we'd only need to keep a version of the labels which were used to provision the current server and the current labels as a global value, instead of having to upkeep multiple lists (Collectors, Observers...) |
I'm still not sure I totally follow, but that's probably just my fault for not having looked into it enough to wrap my head around it. But it sounds like more of a plan than we've had before, so that's exciting! |
Hmm, not all of them do - take http_port and https_port for example. Metrics can also be made global in the Caddyfile because it'd make more sense, I think. This shouldn't be that hard to implement, but I'd like to contribute a few optimization-related changes since they should overall clean up the request-handling code a bit. But basically the way we won't be losing metrics is by using a new |
Those two options are on the
Well, it depends whether it's config per server or config on the |
Do you have an idea how #4140 could fit in maybe? |
You're right, those options are on the As for the reverse proxy upstreams - I really do not see the point in adding latency and status code metrics for upstreams, since that is already available through the present metrics. A health status metric can surely be added though, and per-path configuration should be possible with what I've described here: we could add a "metrics" structure to the routes ( Considering the fact that a lot of metrics might be added over the time, we can also make the server-wide metrics config look like this:
which would allow enabling/disabling various metrics globally. Though we'd definitely have to think this design through a bit more, lol, since this doesn't look very nice. |
We do though; take a look again at how the In case it wasn't clear, I was talking about whether
Hmm, interesting. Yeah, we'll probably need a proof-of-concept implementation to see how that feels and whether it actually works 👍 Thanks for looking into this! |
Ah, okay. I wonder why some server options, such as atomatic https, are added as global ones in the Caddyfile then... |
Because |
@renbou is on to something. I was just sent a goroutine dump from a customer, and noticed that the metrics middleware is wrapping the response for every middleware that is chained in:
The offending code is routes.go, line 260: func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler) Middleware {
// wrap the middleware with metrics instrumentation
metricsHandler := newMetricsInstrumentedHandler(caddy.GetModuleName(mh), mh) This will result in excessive allocations and a relatively serious performance penalty. @hairyhenderson I know you're busy, but do you think we can escalate this? We will need to fix this or remove this before the 2.5 release to ensure stability and reliability. (It's my bad for not catching this in code review, but it was a large change, and I didn't realize the extent at which it was creating new response recorders.) |
@mholt I've described a way to fix this as well as add various requested features related to the metrics in the discussion above, and began thinking of the way to actually implement this. I was planning to contribute a few other optimization-related PR's first, but if 2.5 is coming up soon (I haven't really seen a deadline), I can start working on this instead (probably closer to the weekend though) |
I am a little behind on things. Sorry I haven't gotten around to reading your latest posts in detail yet! I suspect that moving the We don't have a firm deadline for 2.5, but soon-ish would be nice. :) Was kind of my hope to get it out at the beginning of April. |
The proposed solution should instrument the handler chain with the metrics middleware only once, globally, and the handler-specific information is going to be preserved by wrapping the handlers with a middleware which simply sets info about the handler (i.e. once the first time a handler returns its wrapper would set values in the request context signifying the name of the handler) |
This still needs to be fixed soon, but I don't think it will be done by the 2.5.1 release; bumping back to 2.5.2. |
@renbou Is this still something you'd be interested in working on? @hairyhenderson Any chance we could get your help on this one? Otherwise the fix may be less than optimal for Prometheus/Grafana users. |
Here's a simple comparison of serving a static file with and then without metrics: (config is literally just Without metrics as they are currently implemented, Caddy can be about 12-15% faster. (For this test, I simply disabled the wrapping of each handler with the metrics handler.) |
* caddyhttp: Make metrics opt-in Related to #4644 * Make configurable in Caddyfile
FWIW we serve metrics out of a different port than we use for proxying/file server, so we hope that that usecase can still be supported with however this is implemented in the future. IE, if you have to use the |
It definitely will, we have no plans to change that 👍 The |
Any updates on that or maybe some ETA? |
We haven't heard from @renbou in quite some time. I feel like the last we heard of them roughly coincides with the war in Ukraine (their profile mentions Saint-Petersburg, Russia) around March/April 2022. 🤔 |
@renbou any chance you'll continue the work on this? |
Hi, guys! I'm really sorry about my awful response time and the "abandoned" state I've left this issue with. Turns out that working a full-time job and studying for a bachelor's doesn't leave you with much free time (lol). I'd be really glad to take this issue as well as some of the other optimizations I've implemented in a different PR and refactor/complete them. I think we need to discuss, however, what is actually expected to be done in regards to the metrics. The last time I've tried working on this, it wasn't really clear how we want metrics to work and look in the config. Also, I've tried to bite off more than I could chew in a single sprint of changes (implement reloadable metrics, configurable metrics, etc), so I think we need to decide what should be prioritized right now. The main issue is the performance, right? I'll check out the changes which have occurred in the codebase and see what can be optimized. Then we can probably move on to more feature-related metrics tasks which need doing |
Hi @renbou!!! Great to hear from you! Hope your work and school is going well. I definitely know how that goes. What are you studying? Would you like an invite to our developers Slack? That might make some back-and-forth discussion easier 👍 Edit: You're already in our Slack 😅 Want to pop in and say hi? We can discuss things there if you prefer, or just here. Either way. |
Hello! |
Is this completely abandoned? |
@asychev The core Caddy team doesn't have any experience with metrics, so we're not equipped to make any progress. We need help. |
@francislavoie Sad to hear, but clear. Based on conversation above it is not about the metrics really, but middlewares handling architecture, isn't it? |
Per-request are disabled by default now as a mitigation for the majority of users, who don't need metrics. But yes, the way we're doing per-request metrics currently is probably inefficient, but we need help to resolve it. |
Hey all, Prometheus maintainer here. I'm not sure where this has gone wrong, but a typical metric instrumentation with Prometheus client_golang should have only nanoseconds and almost no allocs or impact to the request path. This used to be perfectly fine in Caddy, so I don't know where things have gone wrong recently. I'm not sure what "per-request" metrics means, all requests should be passed to |
@SuperQ any help would be greatly appreciated. For context, Caddy's HTTP handling is a middleware chain. When metrics were implemented, it was done by wrapping every handler in the routes with a function that adds metrics instrumentation. You can see that here (inside caddy/modules/caddyhttp/routes.go Line 153 in 976469c
Since v2.6.0 #5042 we made this wrapping opt-in so that it only happens if the users need metrics (cause not everyone actually scrapes metrics from Caddy, so why waste time doing that if not necessary?) There's a lot of missing functionality in the metrics for Caddy. We have demand for them to be more configurable, include the hostname somehow to separate metrics for different domains, etc. But I and the rest of the core Caddy team don't use metrics ourselves and don't have any experience with this stuff, so we're not equipped to implement improvements. The problems with metrics in general, as is my understanding:
See some of the other open issues for more detail I guess. There's some particularly relevant discussion in #4016. I've had a lot of informal conversations with @hairyhenderson about this on Slack (he implemented the initial version of metrics for us years ago but he no longer has time to maintain it) over the years, but we've never managed to figure out a path forwards. |
Metrics are stored in a Registry. There is a default registry that is used which is a process global store if you do not explicitly create your own Registry. Persistence is not something Caddy has to worry too much about. This is handled by Prometheus automatically. When Caddy is reloaded, you can choose a couple of options.
Resetting a counter/histogram to zero at reload shouldn't be necessary.
Cardinality is not a problem, it's simply a thing you have to capacity plan for. A typical Prometheus server can handle tens of millions of metrics. Per-domain metrics may be important depending on the deployment. The linked nginx-vts-exporter defaults to per-virtual-server labels. I suspect many users want to have at least per-server (domain and aliases) and maybe per-domain metrics by default. However, having some metrics or labels be configurable is a reasonable feature request. Creating your own metrics registry var with You may want to hop on the Prometheus dev community. The CNCF slack |
Some users may want to reload every few seconds, actually. Some use the config API and push config changes very rapidly. Caddy is able to handle that pretty well in many cases. Caddy is able to support dynamic hostnames (e.g. On-Demand TLS, wildcards) so a I have no plans to work on this myself. I'm just a volunteer to the project, and I don't use metrics myself. I have no time to take this on. What we're looking for is someone with metrics experience to help us maintain this. I'm happy to facilitate in any way I can, but I can't reasonably spend time implementing any kind of solution. |
If you want to be that dynamic, the The Delete allows you to remove stale dynamic records without resetting the whole registry. This leave all of the existing entries un-touched. Basically if the API call/reload removes a record from serving, it can trigger a delete of the metrics. Remember that metrics are a registry, based on the "metric family". The metric family is the top-level name like |
What I'm saying is that in the On-Demand case, we have no way to know if something is stale. There's no reference to the domain in the config, because TLS-SNI can contain anything, and we may issue a cert for that domain right away. We don't know if a domain will no longer be used, we just have to rely on letting certs expire normally if they stop being used long term. How can we delete something we have no reference to? I don't mean dynamic config, I mean dynamic at runtime. |
I get it now, that makes a bit more sense. If you don't remove the key from your database, then there's nothing to delete. But these label values should continue to exist in metrics anyway, as they are still valid in the runtime. So there is no problem there. I was mostly thinking about the |
The server label doesn't really matter. It's not that meaningful. It is auto-generated sequentially from Caddyfile config (e.g.
My concern with that is still about memory usage scaling though. We have people who run Caddy instances with hundreds of thousands of domains. They're definitely constrained on memory because Caddy needs to keep a certain amount of certificates decoded in cache (up to a limit, with eviction). Wouldn't it also make metrics HTTP responses huge? That's the trouble with cardinality. So it would definitely need to be opt-in, or have somekind of mechanism for limiting which domains are tracked to keep it in check. Not simple. |
It might be nice to be able to name servers rather than just auto-generate. :) Prometheus metrics scale mostly linearly with the amount of memory. It's a simple Go map datastructure. I have a simple test binary for generating random cardinality and testing metrics.
Running this test only results in about 27MiB of heap use. The Prometheus client_golang is reasonably well optimized. I agree, opt-in for domain name would be a good idea. I don't think we would want to get fancy include/exclude filter limiting, that's feels like premature optimization. Users who care about per-domain metrics can opt-in. Everyone else gets the default per-server. |
You can: https://caddyserver.com/docs/caddyfile/options#name |
While digging into Caddy's source code I've noticed that every (!) route handler is wrapped in a
metricsInstrumentedHandler
which updates Prometheus metrics during request execution. While it is a great feature and should definitely be enabled by default in Caddy, it currently uses up way to much CPU time and the metrics provided are quite redundant.Since Caddy tries out every route handler in order until it gets an answer to the request, metric instrumentation is called for each handler, even if it didn't actually partake (which is also quite difficult to define in this context) in resolving the request, so handler-specific metrics are constantly updated with unrelated data and as a result pretty much all of the handler-specific metrics are meaningless, making them only usable to track server-wide stats.
As an example, here are the metrics for a simple Caddy server with 2 hosts, 1 of which only has a
reverse_proxy
handler, and the other has 2respond
handlers, 1reverse_proxy
handler and 1file_server
handler. The metrics were taken after running an http load-testing tool on the endpoints.prometheus-metrics.txt
As seen in the example, all of the handler-specific metrics are pretty much the same for all handlers, even though in reality only the
respond
handler was requested.The handler metrics provide even less use if the web server hosts multiple domains, since requests from all domains get mixed up in the metrics.
However, questionable metrics wouldn't be much of an issue if they were as fast as providing server-wide metrics, but they, of course, aren't, since they are getting updated multiple times until the request is finally answered.
I've ran
pprof
while putting load usingh2load
to request one of the simplerespond
handlers, and it turned out that 73% of the time spent duringcaddyhttp.Server.primaryHandlerChain.ServeHTTP
was in themetricsInstrumentedHandler
(only 30% of the time was spend by the actualcaddyhttp.StaticResponse.ServeHTTP
). Here's the profile:profile.zip
I really think that metrics such as these should be server-wide where they make more sense and are quicker. https://github.com/nginxinc/nginx-prometheus-exporter could be seen as an example of similar Prometheus metrics commonly used for nginx.
The text was updated successfully, but these errors were encountered: