-
Notifications
You must be signed in to change notification settings - Fork 38.3k
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
Improve WebFlux performance for header management [SPR-17250] #21783
Comments
Brian Clozel commented After the analysis done in the issue description, we've decided to optimize on both fronts:
The first optimization directly shows improvements for POST/PUT requests where the server reads the request The second optimization brings the most benefit for Spring WebFlux applications. All When looking at hot methods with a high overhead profiler (sorted by their "own time", adding the total amount of time spent in them overall), we get the following result: amongst the top 3 hottest methods (all We'll work on further optimizations in separate issues. |
As of gh-21783, Spring WebFlux uses a `TomcatHeadersAdapter` implementation to directly address the native headers used by the server. In the case of Tomcat, "Content-Length" and "Content-Type" headers are processed separately and should not be added to the native headers map. This commit improves the `HandlerAdapter` implementation for Tomcat and removes those headers, if previously set in the map. The adapter already has a section that handles the Tomcat-specific calls for such headers. Fixes gh-24361
As of gh-21783, Spring WebFlux uses a `TomcatHeadersAdapter` implementation to directly address the native headers used by the server. In the case of Tomcat, "Content-Length" and "Content-Type" headers are processed separately and should not be added to the native headers map. This commit improves the `HandlerAdapter` implementation for Tomcat and removes those headers, if previously set in the map. The adapter already has a section that handles the Tomcat-specific calls for such headers. Fixes gh-24387
Prior to this commit, gh-21783 introduced `ReadOnlyHttpHeaders` to avoid parsing media types multiple times during the lifetime of an HTTP exchange: such values are cached and the headers map is made read-only. This also added a new `HttpHeaders.writableHttpHeaders` method to unwrap the read-only variant when needed. It turns out this method sends the wrong signal to the community because: * the underlying map might be unmodifiable even if this is not an instance of ReadOnlyHttpHeaders * developers were assuming that modifying the collection that backs the read-only instance would work around the cached values for Content-Type and Accept headers This commit adds more documentation to highlight the desired behavior for cached values by the read-only variant, and deprecates the `writableHttpHeaders` method as `ReadOnlyHttpHeaders` is package private and we should not surface that concept anyway. Instead, this commit unwraps the read-only variant if needed when a new HttpHeaders instance is created. Closes gh-32116
Brian Clozel opened SPR-17250 and commented
In a recent reactor netty issue, small apps are being used to benchmark different frameworks. While working on that, Violeta Georgieva found a few interesting points we could take a look at, as opportunities for performance improvements and micro-benchmark cases.
Hotspots
The benchmark has underlined a few hostpots in the Spring Framework codebase:
org.springframework.util.LinkedCaseInsensitiveMap.convertKey(String)
(own time, 5th overall)org.springframework.util.MimeType.<init>(String, String, Map)
(own time, 10th overall)LinkedCaseInsensitiveMap
The
convertKey
method is used evenly byget
andput
operations on the map itself. This is callingString.toLowerCase
many times, which involves a complex logic (it depends on theLocale
) and creates newString
instances. In general, copying headers from the native request and looking them up in our data structure is not efficient.Request Content-Type parsing
HttpHeaders.getContentType
is the main offender forget
operations, called in several places:org.springframework.web.server.adapter.DefaultServerWebExchange.initFormData(ServerHttpRequest, ServerCodecConfigurer)
org.springframework.web.reactive.function.BodyExtractors.contentType(HttpMessage)
org.springframework.web.server.adapter.DefaultServerWebExchange.initMultipartData(ServerHttpRequest, ServerCodecConfigurer)
org.springframework.http.codec.DecoderHttpMessageReader.getContentType(HttpMessage)
In general, we're spending a lot of time on
org.springframework.util.MimeType.<init>(String, String, Map)
andorg.springframework.http.MediaType.parseMediaType(String)
when parsing the content type of the request.Possible solutions
We could use a more optimized data structure behind
HttpHeaders
. A data structure that usesequalsIgnoreCase
or even case insensitivehashcode
should be better. Many server implementations don't even useMap
implementations, given that requests often contain just a few headers and the cost of setting up aMap
outweighs the lookup performance gains.Benchmarks show that even a
new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER)
shows better performance.Other implementations tend to wrap the
String
instances to store their case insensitive hash internally and use that for comparisons.For Spring WebFlux, we could even wrap the native HTTP request headers and never copy those values in other data structures. This requires dedicated implementations for each server (Tomcat, Jetty, Reactor Netty and Undertow), but since most of those are using pooled resources for headers, we might improve a lot the GC pressure applied for each HTTP exchange handling.
Microbenchmark results
Here are microbenchmark results on
HttpHeaders
variants, no other Spring infrastructure piece involved.The "GET requests" case is reading a few headers from the request; the "POST requests" case is reading multiple times the "Content-Type" and "Content-Length" headers, as Spring does.
The baseline is just performing those read operations from an existing map. the other variants are doing what Spring is supposed to do, treating that map as the native request headers. In the regular
HttpHeaders
case, we're copying headers into our instance and performing the lookup operations. Other variants are trying the same with other map implementations.This shows that the copying/allocations are using resources and changing the underlying implementation only slightly improves performance. This means we should instead look into leveraging the native headers directly and avoid copying in the first place.
Affects: 5.1 RC2
Reference URL: reactor/reactor-netty#392
Issue Links:
Referenced from: commits 10d5de7, 58b3af9, f12c28e, ce7278a
0 votes, 7 watchers
The text was updated successfully, but these errors were encountered: