Skip to content

Resident Set Size on Linux remains much higher than managed memory usage in .NET 5.0 #52592

Closed

Description

@emz00

After upgrading our application, running on Ubuntu 18.04, from .NET Core 3.1 to .NET 5.0 we're seeing a consistently higher Resident Set Size memory usage.

The application normally processes web requests at a steady rate, but once per day it updates its data, which causes much higher RAM usage for 1-30 minutes. After this, we see the managed memory (as reported by GC.GetTotalMemory) drop about the same way on .NET 3.1 and 5.0. However, the Resident Set Size for the process either remains unchanged or drops gradually over the next few hours, but is still way higher than managed memory - we're talking ~6 GB reported by GC.GetTotalMemory and ~33 GB Resident Set Size.

To further confuse things, we had 3 instances of the application running at the same time on the same machine receiving the same requests. One on .NET 3.1, another on .NET 5.0 with otherwise identical code and another on .NET 5.0 with some code differences. The RSS dropped for the .NET 3.1 version and the identical .NET 5.0 version, but not the newer .NET 5 version, implying something about our code changes may have caused the difference, but I can't see what that can be when the managed memory went down the same way for all of them.

https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-5/ mentions a number of GC changes in .NET 5, in particular #37894 which sounds relevant. Is there any documentation on how RSS is expected to behave in .NET 5? For example, when does the GC consider itself to be in a "low memory situation"? I was unable to find anything. Basically, what we're trying to figure out is whether this is a problem or expected behavior. If expected, the next question would then be how to tell how much RAM the server really has available to run more instances of the application.

Activity

  1. ghost added
    untriagedNew issue has not been triaged by the area owner
    on May 11, 2021
  2. added
    questionAnswer questions and provide assistance, not an issue with source code or documentation.
    and removed
    untriagedNew issue has not been triaged by the area owner
    on May 11, 2021
  3. added this to the 6.0.0 milestone on May 11, 2021
  4. mangod9 commented on May 11, 2021

    @mangod9
    Member

    Adding @PeterSolMS as fyi. RSS is something handled by the OS and dont believe .net can influence how resident vs. swapped would be managed. Assume you dont see any other performance issues with the app (like throughput differences due to frequent GCs and such)?

  5. emz00 commented on May 11, 2021

    @emz00
    Author

    It's not using any swap, so RSS is the "total" RAM usage, as far as I understand. No, we're not seeing any major performance differences (.NET 5.0 is just slightly faster).

  6. Maoni0 commented on May 12, 2021

    @Maoni0
    Member

    are you using Server GC? in .NET 5 we moved the decommit at the end of ephemeral segment out of the GC pause so you would see resident pages gradually dropping, however it wouldn't be as long as hours. it should come down much more quickly than that unless you need the memory committed again. if you could collect a trace for these processes that'd be helpful.

    high memory load is explained here.

    @PeterSolMS it'd be helpful to have some instrumentation around the decommit since now we don't know how much we will be decommitting. perhaps at the beginning of the GC we could give some info how much we've decommitted so far?

  7. emz00 commented on May 12, 2021

    @emz00
    Author

    We are using server GC. Thanks for the link, that's the kind of thing I was looking for! I will read it in more detail later, but I saw that "low memory situation" means 90-97% memory used. That was not the case on this machine (it has 512 GB RAM and no more than 75% of that was ever used), so sounds like maybe the GC didn't think it needed to bother to return RAM to the OS?

    Here is the managed memory usage from last night:
    image
    The yellow and black lines are both .NET 3.1 versions and the blue one is .NET 5 with code changes to our application. (Edit: the black one was supposed to be .NET 5.0 with otherwise identical code to the .NET 3.1 version, but it's not. I'll get back to you next week with data for both app versions on both runtime versions.)

    Now, here is the sum of (VmRSS+VmSwap) over the same period:
    image
    Here the red and yellow lines are .NET 3.1 and the black is .NET 5.0 with code changes. (Confusing that the colors are not the same, I know, but I can't control that, sorry!)

    I'm not sure if I can get a trace - that would have have a high CPU or RAM overhead or use a lot of disk, right? Can it run with the tracing on for something like 12 hours?

  8. mangod9 commented on May 12, 2021

    @mangod9
    Member

    can you provide some details / pseudocode of your changes? Perhaps you could try reverting some which feel allocation/caching related and see if that has any effect? Thx.

  9. emz00 commented on May 17, 2021

    @emz00
    Author

    I ran the test again over the last 4 days, this time with each version of our code (labelled v44 and v45 here) on both .NET 3.1 and 5.0. There is also an earlier version (v43 here), which is on .NET 3.1 only.

    Again, the managed memory (GC.GetTotalMemory) is very similar between them:

    managed2

    VmRSS+VmSwap is markedly different between .NET 3.1 and 5.0 for otherwise identical code:

    private-bytes2

    ... but it seems to have gotten worse with each release of our code (v43->v44->v45) even on .NET 3.1. The spike occurs around 02:30 when we run a nightly scheduled task to reload some data and does not fully go down until 10:00-22:00, depending on that day - but it does eventually go down, which is something.

    No, I cannot provide pseudocode, as it's a large and complex application. We also made a lot of changes between releases and trying to bisect which change caused it to get worse would be very time-consuming.

    So my main question is: is there anything we can do to get .NET to return RAM to the OS sooner?

    I will try to get a trace. Is there any way I can provide it privately to the .NET runtime developers? I don't know what it contains, so would rather not upload it here.

  10. emz00 commented on May 18, 2021

    @emz00
    Author

    I found that our v43 version was doing a GC.Collect() after the big nightly data reload, while the v44 and v45 code versions were not. I enabled the GC.Collect() again and got this:

    private-bytes3-v45

    So the RAM usage goes down much sooner than without the GC.Collect(), but it still takes much longer in .NET 5.0. (We disabled the GC.Collect() because of a problem where if 5-10 processes did this around the same time they all got "stuck" inside the GC.Collect() call, which was pretty bad! So I'm reluctant to re-enable that in production.)

    In our v44 version I additionally made it reload some of the data every 15 minutes instead of just once per day, trying to repro the problem quicker, and got this:

    private-bytes3-v44

    So not really the same, unfortunately - .NET 5.0 does use noticeably more RAM, but it doesn't take hours and hours to release it (probably because we then use more RAM again very soon).

  11. emz00 commented on Jun 1, 2021

    @emz00
    Author

    We've had to downgrade our application to .NET Core 3.1 for now because of this issue, but will have to upgrade from 3.1 eventually, so we'd still like to solve this. I have traces now for .NET 3.1 and 5.0 versions over the same time period with the same requests sent to them. Is there a way I can send those traces to you without uploading them here @Maoni0 ?

  12. mangod9 commented on Jun 2, 2021

    @mangod9
    Member

    Hey @loop-evgeny, please send a link to the traces to my email (should be included in my profile). Also adding @brianrob since this is a perf degradation investigation when compared to 3.1. Thanks!

  13. 13 remaining items

  14. emz00 commented on Aug 19, 2021

    @emz00
    Author

    Kept running it and the results are not so great last night:

    W-2021-08-19

    Also ran the same on another version of our code with different data and the results are also not great there:

    X 2021-08-19

  15. mangod9 commented on Aug 19, 2021

    @mangod9
    Member

    Hi @loop-evgeny, is it possible to collect gc-only traces on both 5 and 6, so we could analyze this further?

  16. emz00 commented on Aug 19, 2021

    @emz00
    Author

    Hi @mangod9 you mean exactly the same traces, as before, right? dotnet-trace collect --profile gc-collect --duration 12:00:00 -p ... -o ... ?

    Should I do that with the daily build of the 6.0 SDK or the stable 5.0 SDK or the SDK version matching the .NET runtime for each process?

  17. mangod9 commented on Aug 19, 2021

    @mangod9
    Member

    you mean exactly the same traces, as before, right

    Yeah correct, ideally the sdk version matching the runtime to ensure there are not issues with collection.

  18. emz00 commented on Aug 19, 2021

    @emz00
    Author

    Alright, I've set that up, will email you the traces tomorrow if all goes well!

  19. emz00 commented on Aug 20, 2021

    @emz00
    Author

    Emailed you the traces @mangod9. Here are the corresponding memory usage profiles, for midnight to 10:00 today (the same time the traces cover).

    W-2021-08-20

    X 2021-08-20

  20. added a commit that references this issue on Aug 27, 2021
  21. PeterSolMS commented on Aug 27, 2021

    @PeterSolMS
    Contributor

    Hi @loop-evgeny, thanks for the traces!

    I found that there is an additional issue that is triggered by the combination of a large amount of physical memory on the machine and high memory usage - this is resolved by the change mentioned above.

    Other than that, one additional intentional change compared to 3.1 is that we don't release free space in gen 2 to the OS unless memory usage is very high. This was motivated by a scenario where we observed that we would frequently release space and then get it back very soon afterwards, causing unnecessary page faults. If memory gets tight, we will release the memory or trigger a compacting gen 2 GC that will also cause the free space to be given back to the OS. This change is relevant to your scenario because after the spike in memory usage around 2:30, you end up with a lot of free space in gen 2 (around 3 GB).

  22. emz00 commented on Sep 6, 2021

    @emz00
    Author

    Hi @PeterSolMS , I just got back from a vacation. Thanks for the further fixes! How do I test these? Apply the nightly binaries on top of preview7 as before?

  23. PeterSolMS commented on Sep 9, 2021

    @PeterSolMS
    Contributor

    Hi @loop-evgeny, I think that should work, if not, let me know.

  24. emz00 commented on Sep 13, 2021

    @emz00
    Author

    Ran the same test with .NET 6.0.0-rc.2.21458 and it looks much better now! Still not as good as .NET 3.1, but I think we'll be able to upgrade now. Thanks very much for the fixes!

    W2021-09-13
    X2021-09-13

  25. mangod9 commented on Sep 13, 2021

    @mangod9
    Member

    Thanks for validating @loop-evgeny. Will close this issue for now, please reopen if you see any other issues.

  26. ghost locked as resolved and limited conversation to collaborators on Nov 3, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area-GC-coreclrquestionAnswer questions and provide assistance, not an issue with source code or documentation.

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions