-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
A large set of configuration keys provided by the built-in .NET IConfigurationProvider's negatively affect performance significantly #65885
Comments
Tagging subscribers to this area: @dotnet/area-extensions-configuration Issue DetailsDescriptionA large set of configuration keys provided by the built-in .NET IConfigurationProvider's negatively affect performance significantly. I've created a benchmark project here: https://github.com/NetMatch/dotnet-config-performance-issue Configuration
Regression?We encountered this issue in .NET Core 3.1, the benchmark project shows it still reproduces in .NET 6. Data
AnalysisWe have a large web application in which we have a significant amount of configuration, this adds up to about 20k configuration keys. We have a non-trivial amount of IOptionsSnapshot instances that are resolved per request via DI. We noticed that as configuration got added, performance and CPU usage went up across the board. Digging deeper we found that this is due to how the Every request the IOptionsSnapshots are constructed via the All of the default When you have a lot of configuration and a non-trivial amount of Options that get bound per request you end up with a serious amount of scanning over keys for no reason. And that gave us big performance issues. After identifying this issue, we added a new IMO the problem is twofold. The
|
The design of passing And because the final combined result is internally not sorted by That design could be improved by moving the sort operation into It could just be a basic Wonder if it shouldn't actually use an array instead of a list though. Given that classes consuming the result are not expected to actually go and add/remove elements here? And that an array iterator is cheaper than a list iterator? |
Related to #62510 |
Add a similar benchmark at dotnet/performance#2572 and hope the performance can be improved in net7.0 |
.NET 7 is done. |
@davidfowl you are right. If not planned, I would like to contribute to improve it in net8.0. |
@wangyuantao I think the first change would be to get this into our performance tests. cc @eerhardt |
So that PR #74736 was never completed? Our application is already performance bad because of this. Now we want to make it multi-tenant, makeing the problem even worse :-( |
@kwaazaar |
Reading this issue makes me super skeptical about introducing more We also have a fairly large monolith with centralized configuration coming from a custom db source and Azure AppConfiguration in addition to the other standard sources, and we have been ramping up usages of Seems like this missed .NET9 too... oh well 😞 I might have to start recommending developers to not use |
You could still use IOptionSnapshot in all code and use your own implementation for DI. That way your code still adheres to a standard approach and the impact is minimal in the future to swap back to the improved implementation in .NET 10 (or later :-() |
Oh I know it helps mitigating the issue. I helped write it to do just that. And indeed: _mitigates - It's not an end solution. |
I just spent 2 hours finding out why importing a big JSON file takes 14 minutes... We need multiple calls to We sadly need to keep using it, since it's not distinguishable if a section is not existing or if it was only empty. Only |
I was able to improve my loading from 14 minutes to 675 milliseconds. The main bottleneck is this: runtime/src/libraries/Microsoft.Extensions.Configuration/src/ConfigurationProvider.cs Lines 61 to 93 in dda0d4e
runtime/src/libraries/Microsoft.Extensions.Configuration/src/InternalConfigurationRootExtensions.cs Lines 21 to 41 in dda0d4e
It is basically adding one string for each entry that contains the substring. For our big config file, this were 53k of For testing purposes, I changed the In case someone also has problems and is interested in the workaround, let me know and I will extract it into a MCVE. The better solution would be to fix the original implementation and maybe take the hit for the sorting to not take place. An feasable alternative would be to just add new methods that return the Sadly any change to |
Description
A large set of configuration keys provided by the built-in .NET IConfigurationProvider's negatively affect performance significantly.
I've created a benchmark project here: https://github.com/NetMatch/dotnet-config-performance-issue
Configuration
Regression?
We encountered this issue in .NET Core 3.1, the benchmark project shows it still reproduces in .NET 6.
Data
Analysis
We have a large web application in which we have a significant amount of configuration, this adds up to about 20k configuration keys. We have a non-trivial amount of IOptionsSnapshot instances that are resolved per request via DI. We noticed that as configuration got added, performance and CPU usage went up across the board. Digging deeper we found that this is due to how the
Microsoft.Extensions.Configuration.ConfigurationProvider
andMicrosoft.Extensions.Configuration.ConfigurationBinder
work.Every request the IOptionsSnapshots are constructed via the
ConfigurationBinder
. TheConfigurationBinder
is passed anIConfiguration
which is actually aIConfigurationSection
instance that is scoped to a certain path in the configuration. Based on the public API, usingGetSection
, one expects that theIConfigurationSection
instance is optimized to access the child keys of a certain section. But this is not the case. Whenever theConfigurationBinder
accesses the configuration and callsGetChildren()
it ends up going to theConfigurationRoot
and calls into each registeredIConfigurationProvider
to fetch child keys for the path of theConfigurationSection
.All of the default
IConfigurationProvider
implementations inherit fromConfigurationProvider
which has aGetChildKeys
method where things get troublesome. This methods does a loop over ALL registered keys in the provider to find a prefix match for the given path. This means that for every call ofGetChildren()
on anIConfiguration
instance, it scans every possible key in all the providers. On top of that various code paths in the binder lead it to callGetChildren()
multiple times on the sameIConfiguration
. All of this combined adds up to a lot of scans.When you have a lot of configuration and a non-trivial amount of Options that get bound per request you end up with a serious amount of scanning over keys for no reason. And that gave us big performance issues. After identifying this issue, we added a new
IConfigurationSource
andIConfigurationProvider
that caches calls intoGetChildKeys
before going to the underlyingIConfigurationProvider
. The implementation in the linked repo is a simplified combined version of this which should not be used in production. However for the benchmark it shows its effects well enough, it's used when theUseCache
option is set to true. With this deployed in our application, our 95 percentile response times were 4 times lower and we are using 10 times(!) less CPU during load, at the cost of slightly higher memory usage.IMO the problem is twofold. The
ConfigurationBinder
is suboptimal by calling into theGetChildren()
method so often. But most important is the fact that the IConfiguration(Section) implementation seems flawed. ConfigurationSection's are not really optimized to access the keys in their section, it always goes to the root and scans all keys without any caching or optimizations.The text was updated successfully, but these errors were encountered: