Skip to content
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

Memory usage when using Mailkit in Docker Container #1105

Closed
kvanderbok opened this issue Nov 24, 2020 · 42 comments
Closed

Memory usage when using Mailkit in Docker Container #1105

kvanderbok opened this issue Nov 24, 2020 · 42 comments
Labels
question A question about how to do something

Comments

@kvanderbok
Copy link

Hi jstedfast,

We are using your Mailkit library for sending emails from our dotnet core 3.1 web api. Actually we developed a netstandard 2.0 library that includes Mailkit as a dependency. This netstandard library is used by our webapi. We run our application in a docker container using mcr.microsoft.com/dotnet/aspnet:3.1 as base.

What we notice is that when using either SSL (SslOnConnect) or STartTls (StartTls), the container uses an excesive amount of memory making our container crash. The memory increases with each email sent, but seems to level at some point. We tested this in a kubernetes cluster and on a dev machine. On the cluster the memory goes up to about 2GB and levels of. On our localmachine the memory goes up to 1,6GB and then levels off. When setting the the SecureSocketOptions to None, we don't see a dramatic increase in memory is use around 200 MB.

What we are wondering is whether this memory usage is typical or whether this is as we think too much.

Regards, Kees

@jstedfast
Copy link
Owner

jstedfast commented Nov 24, 2020

That sounds excessive.

My first inclination is that you could use the following code to manually Dispose all of the streams used by a MimeMessage to see if that helps:

static void DisposeMimeMessage (MimeMessage message)
{
    foreach (var bodyPart in message.BodyParts) {
        if (bodyPart is MessagePart rfc822) {
            DisposeMimeMessage (rfc822.Message);
        } else {
            var part = (MimePart) bodyPart;

            part.Content.Stream.Dispose ();
        }
    }
}

@kvanderbok
Copy link
Author

kvanderbok commented Nov 24, 2020

Thanks, I'll test this

I don't see and implementation of DisposeMessagePart in your example and I can't find one in MailKit. Did you mean the following? So recursively calling the method DisposeMimeMessage?

`
static void DisposeMimeMessage (MimeMessage message)
{
foreach (var bodyPart in message.BodyParts) {
if (bodyPart is MessagePart rfc822) {
DisposeMimeMessage (rfc822.Message);
} else {
var part = (MimePart) bodyPart;

        part.Content.Stream.Dispose ();
    }
}

}
`

@jstedfast
Copy link
Owner

Sorry, yes, I meant DisposeMimeMessage(). I updated the code in my previous comment.

Thanks!

@jstedfast jstedfast added the question A question about how to do something label Nov 25, 2020
@kvanderbok
Copy link
Author

I have tried your suggestion, but it is not solving the problem. I did some further analysis and the issue seems to be related to the SSL or StartTls (encrypted connection). What I observe is that the memory increases every time I call
public override void Connect(string host, int port = 0, SecureSocketOptions options = SecureSocketOptions.Auto, CancellationToken cancellationToken = default);

using
SecureSocketOptions.SslOnConnect or SecureSocketOptions.StartTls The memory issue is not there when using SecureSocketOptions.None

The memory increase but does not decrease. So it seems that some memory is allocated on this call by Mailkit or maybe the OS that is not released for some reason.

@jstedfast jstedfast reopened this Dec 1, 2020
@jstedfast
Copy link
Owner

@kvanderbok

Let me make sure I understand your scenario correctly...

Are you re-using the same ImapClient to connect to multiple sources over the lifespan of the object?

In other words:

var client = new ImapClient ();

client.Connect ("one.host.com", 993, SecureSocketOptions.SslOnConnect);
// do some stuff...
client.Disconnect (true);

client.Connect ("two.host.com", 993, SecureSocketOptions.SslOnConnect);
// do some stuff...
client.Disconnect (true);

@kvanderbok
Copy link
Author

kvanderbok commented Dec 1, 2020

This is what I am basically doing, this is part of the code I do dispose the message.

message.From.Add(new MailboxAddress("From name", "from@email.test"));
message.To.Add(new MailboxAddress("To name", "to@email.test"));
message.Subject = "How you doin'?";

message.Body = new TextPart("plain")
{
    Text = @"Hey, hello this is a test"
};

using (var client = new SmtpClient())
{
    client.Connect("one.host.com", 465, SecureSocketOptions.SslOnConnect);

    client.Authenticate("username", "password");

    client.Send(message);

    client.Disconnect(true);
}

@jstedfast
Copy link
Owner

I'm going to need a memory profiler for this... I'm not seeing any obvious cause in the code 😦

@kvanderbok
Copy link
Author

kvanderbok commented Dec 2, 2020

I created a small project (console app) to reproduce the issue here https://github.com/kvanderbok/MailkitMemIssue When I run this I see that the memory grows until 300 MB then levels of. So it is not as dramatic as in my WebApi project.

@jstedfast
Copy link
Owner

I might be wrong, but doesn't that kinda sound like the GC is just allowing the heap to grow to a certain size?

If that's not the case, what I'll need to debug this is a tool that can show me what objects are still dangling after the SmtpClient has been disposed. As far as I can tell, the SmtpClient correctly disposes the SslStream, so I'm not sure what MailKit could be leaking.

@jstedfast
Copy link
Owner

I just confirmed by stepping thru everything with a debugger that the SslStream's Dispose() method gets called and that the underlying NetworkStream's Dispose() method also gets called, so that eliminates the theory that the SslStream isn't getting disposed properly.

@jstedfast
Copy link
Owner

@kvanderbok I don't suppose you've made any discoveries on this since December?

@FelixMarek
Copy link

FelixMarek commented Jul 12, 2021

We must have observed the same memory leak in our case.
We have used two systems with different SMTP servers. Regardless of the system, the problem occurred only with the one SMTP server.
The problematic SMTP server uses STartTls with port 587 (The server that worked used the same).
One solution for us was to wrap the lib and use the standard .Net solution (https://docs.microsoft.com/en-us/dotnet/api/system.net.mail.smtpclient?view=netcore-3.1). With the standard .Net lib no Memory Leak is observable.
This further suggests that the problem lies somewhere in the MailKit lib.

I have send with "swaks" with every SMTP Server Mails. It could be that this output can help:
SMTP Server that work:

=== Trying XXX:587...
=== Connected to XXX.
<-  220 XXX ESMTP Postfix (Debian/GNU)
 -> EHLO email-test
<-  250-XXX
<-  250-PIPELINING
<-  250-SIZE 52428800
<-  250-ETRN
<-  250-STARTTLS
<-  250-ENHANCEDSTATUSCODES
<-  250-8BITMIME
<-  250-DSN
<-  250 CHUNKING
 -> STARTTLS
<-  220 2.0.0 Ready to start TLS
=== TLS started with cipher TLSv1.2:XXX
=== TLS no local certificate set
=== TLS peer DN="/CN=*.XXX"
 ~> EHLO email-test
<~  250-XXX
<~  250-PIPELINING
<~  250-SIZE 52428800
<~  250-ETRN
<~  250-AUTH DIGEST-MD5 CRAM-MD5 PLAIN LOGIN
<~  250-ENHANCEDSTATUSCODES
<~  250-8BITMIME
<~  250-DSN
<~  250 CHUNKING
 ~> AUTH CRAM-MD5
<~  334 XXX
<~  235 2.7.0 Authentication successful
 ~> MAIL FROM:<XXX>
<~  250 2.1.0 Ok
 ~> RCPT TO:<XXX>
<~  250 2.1.5 Ok
 ~> DATA
<~  354 End data with <CR><LF>.<CR><LF>
 ~> Date: Mon, 12 Jul 2021 08:14:35 +0000
 ~> To: XXX
 ~> From: XXX
 ~> Subject: test Mon, 12 Jul 2021 08:14:35 +0000
 ~> Message-Id: <20210712081435.000448@email-test>
 ~> X-Mailer: swaks v20190914.0 jetmore.org/john/code/swaks/
 ~>
 ~> This is a test mailing
 ~>
 ~>
 ~> .
<~  250 2.0.0 Ok: queued as 5C44EA0621
 ~> QUIT
<~  221 2.0.0 Bye
=== Connection closed with remote host.`

The SMPT Server that not work:

root@email-test:/# swaks --to XXX --server XXX --auth-user XXX --auth-password XXX --from XXX -tls --port 587
=== Trying XXX:587...
=== Connected to XXX.
<-  220 XXXESMTP
 -> EHLO email-test
<-  250-XXX
<-  250-8BITMIME
<-  250-SIZE 20971520
<-  250 STARTTLS
 -> STARTTLS
<-  220 Go ahead with TLS
=== TLS started with cipher TLSv1.2:XXX
=== TLS no local certificate set
=== TLS peer DN="/C=DE/L=XXX/O=XXX AG/CN=*XXX"
 ~> EHLO email-test
<~  250-XXX
<~  250-8BITMIME
<~  250-SIZE 20971520
<~  250-AUTH PLAIN LOGIN
<~  250 AUTH=PLAIN LOGIN
 ~> AUTH LOGIN
<~  334 XXX
<~  334 XXX
<~  235 #2.0.0 OK Authenticated
 ~> MAIL FROM:<XXX>
<~  250 sender <XXX> ok
 ~> RCPT TO:<XXX>
<~  250 recipient <XXX> ok
 ~> DATA
<~  354 go ahead
 ~> Date: Mon, 12 Jul 2021 08:09:32 +0000
 ~> To: XXX
 ~> From: XXX
 ~> Subject: test Mon, 12 Jul 2021 08:09:32 +0000
 ~> Message-Id: <20210712080932.000447@email-test>
 ~> X-Mailer: swaks v20190914.0 jetmore.org/john/code/swaks/
 ~>
 ~> This is a test mailing
 ~>
 ~>
 ~> .
<~  250 ok:  Message 1778326 accepted
 ~> QUIT
<~  221 XXX
=== Connection closed with remote host.

@jstedfast
Copy link
Owner

Just so I make sure I understand correctly, the server that produced the first log did not have a leak but the second server did have a leak?

Or did both of them leak?

@FelixMarek
Copy link

Yes you understand correct.
The first server that produced the first log did not have a leak.
The second server has the leak (Same MailKit code).

@jstedfast
Copy link
Owner

jstedfast commented Jul 14, 2021

@FelixMarek Thanks.

I see that the second server supports the PIPELINING extension that the first server does not.

What if you modify your code to disable that?

client.Connect (...);
client.Authenticate (...);

// This will disable PIPELINING
client.Capabilities &= ~SmtpCapabilities.Pipelining;

client.Send (message);

Does this stop the leak?

@mzakrzewski
Copy link

mzakrzewski commented Jul 14, 2021

Maybe I'm reading it wrong, but isn't that reversed: the first smtp server supports pipelining?

@jstedfast
Copy link
Owner

D'oh, yes, you are right.

@mwalczyk81
Copy link

I'm also experiencing something similar to what I'm seeing in this issue. I've tried the suggestions I saw, but so far no luck. Definitely willing to help test if able.

@jstedfast
Copy link
Owner

Do you guys provide your own ServerCertificateValidationCallback on the SmtpClient?

@mwalczyk81
Copy link

I do not. We let it validate on it's own. We also let it default for the SecureSocketOptions

@jstedfast
Copy link
Owner

if there was a leak, it sounds like it'd have to be related to upgrading the connection to an SSL connection but that code is so simple it's basically just wrapping the NetworkStream in an SslStream and calling sslStream.AuthenticateAsClient() so I can't see how there could possibly be a leak.

@mwalczyk81
Copy link

I'm trying to get some heap dumps today to see if I can see anything. I'll let you know if I find anything useful.

@jstedfast
Copy link
Owner

The only potential leak I can find is https://github.com/jstedfast/MailKit/blob/master/MailKit/MailService.cs#L533 which would suggest you'd have to get an exception, but you guys don't appear to be getting an SslHandshakeException so I don't think that's it.

(this potentially leaks only because .NET >= 4.6 introduced a Dispose() method for X509Certificates)

@mwalczyk81
Copy link

I'm struggling to recreate this on my machine, and don't have access to the SMTP server or containers where I can recreate the issue. I'm still trying to work with the teams that do to see if I can be of any help with this, but it's much slower going than I had hoped for.

@jstedfast
Copy link
Owner

Oops, let me reopen this since you guys are clearly still having issues.

@jstedfast jstedfast reopened this Jul 24, 2021
@jstedfast
Copy link
Owner

I ended up modifying the app a bit to comment out MimeMessage creation (just to rule that out) and making the code just Connect & Disconnect w/o any SSL or anything and here's what I got:

root@e0b997520cda:/app# ./MailKitConsoleApp
Run 1 of 10
Called Connect Workingset: 40.8 MB Increase: 3.2 MB
NetworkStream disposing...
Called Disconnect Workingset: 41.2 MB Increase: 384.0 KB

Run 2 of 10
Called Connect Workingset: 41.2 MB Increase: 0.0 B
NetworkStream disposing...
Called Disconnect Workingset: 41.2 MB Increase: 0.0 B

Run 3 of 10
Called Connect Workingset: 41.5 MB Increase: 264.0 KB
NetworkStream disposing...
Called Disconnect Workingset: 41.5 MB Increase: 0.0 B

Run 4 of 10
Called Connect Workingset: 41.8 MB Increase: 312.0 KB
NetworkStream disposing...
Called Disconnect Workingset: 41.8 MB Increase: 0.0 B

Run 5 of 10
Called Connect Workingset: 42.0 MB Increase: 264.0 KB
NetworkStream disposing...
Called Disconnect Workingset: 42.0 MB Increase: 0.0 B

Run 6 of 10
Called Connect Workingset: 42.0 MB Increase: 0.0 B
NetworkStream disposing...
Called Disconnect Workingset: 42.0 MB Increase: 0.0 B

Run 7 of 10
Called Connect Workingset: 42.3 MB Increase: 0.0 B
NetworkStream disposing...
Called Disconnect Workingset: 42.3 MB Increase: 0.0 B

Run 8 of 10
Called Connect Workingset: 42.3 MB Increase: 0.0 B
NetworkStream disposing...
Called Disconnect Workingset: 42.6 MB Increase: 264.0 KB

Run 9 of 10
Called Connect Workingset: 42.8 MB Increase: 248.0 KB
NetworkStream disposing...
Called Disconnect Workingset: 42.8 MB Increase: 0.0 B

Run 10 of 10
Called Connect Workingset: 42.8 MB Increase: 0.0 B
NetworkStream disposing...
Called Disconnect Workingset: 43.1 MB Increase: 0.0 B

Emails sent

The amount of memory leaked each loop iteration makes me think this isn't in MailKit or I would expect it to be consistent. It's also a HUGE amount (248K, 264K, 312K, 384K, ...) - I don't think the SmtpClient allocates that much memory in total, nevermind could it leak that much per loop... not with just a Connect/Disconnect cycle.

Based on this, I added:

GC.Collect ();
GC.WaitForPendingFinalizers ();
GC.Collect ();

to a few locations - at the end of each loop and before taking a snapshot of the current process memory.

This change made it so that nearly every loop had a 0B increase in memory. Out of 10 loops, on the first loop had a memory increase (makes sense) and 1 other loop iteration (why? don't know.). This was pretty consistent, though, as I ran it several times.

Then I changed the settings to use STARTTLS and I got this:

root@d1a5d3327fce:/app# ./MailKitConsoleApp
Run 1 of 10
Called Connect Workingset: 55.1 MB Increase: 17.0 MB
NetworkStream disposing...
Called Disconnect Workingset: 55.1 MB Increase: 0.0 B

Run 2 of 10
Called Connect Workingset: 55.1 MB Increase: 0.0 B
NetworkStream disposing...
Called Disconnect Workingset: 55.1 MB Increase: 0.0 B

Run 3 of 10
Called Connect Workingset: 55.1 MB Increase: 0.0 B
NetworkStream disposing...
Called Disconnect Workingset: 55.3 MB Increase: 260.0 KB

Run 4 of 10
Called Connect Workingset: 55.3 MB Increase: 0.0 B
NetworkStream disposing...
Called Disconnect Workingset: 55.3 MB Increase: 0.0 B

Run 5 of 10
Called Connect Workingset: 56.6 MB Increase: 1.3 MB
NetworkStream disposing...
Called Disconnect Workingset: 56.6 MB Increase: 0.0 B

Run 6 of 10
Called Connect Workingset: 56.6 MB Increase: 0.0 B
NetworkStream disposing...
Called Disconnect Workingset: 56.6 MB Increase: 0.0 B

Run 7 of 10
Called Connect Workingset: 56.6 MB Increase: 0.0 B
NetworkStream disposing...
Called Disconnect Workingset: 56.6 MB Increase: 0.0 B

Run 8 of 10
Called Connect Workingset: 56.6 MB Increase: 0.0 B
NetworkStream disposing...
Called Disconnect Workingset: 56.6 MB Increase: 0.0 B

Run 9 of 10
Called Connect Workingset: 56.6 MB Increase: 0.0 B
NetworkStream disposing...
Called Disconnect Workingset: 56.6 MB Increase: 0.0 B

Run 10 of 10
Called Connect Workingset: 57.3 MB Increase: 644.0 KB
NetworkStream disposing...
Called Disconnect Workingset: 57.3 MB Increase: 0.0 B

Emails sent

Keep in mind that if the NetworkStream is getting disposed, that also proves that the container SslStream is also getting disposed. So where is that 1.3MB memory increase in loop 5 coming from? Or the 644KB increase in loop 10? Or the 260KB in loop 3?

If there is a leak, it seems to pretty clearly be a leak in .NET core libraries like SslStream.

@jstedfast
Copy link
Owner

Ended up using the memory profiler that comes as part of Visual Studio Enterprise and the only MailKit objects "leaked" (iow still alive after 100 iterations of the loop creating/connecting/disconnecting/disposing the SmtpClient) were 37 instances of TaskCompletionSource<bool> as created here: https://github.com/jstedfast/MailKit/blob/master/MailKit/Net/SocketUtils.cs#L61

Since TaskCompletionSource does not seem to be disposable, not sure how I can be "leaking" that so I suspect it's just that the GC didn't get around to it.

Also "leaked" are:

  • 1 instance of an SmtpClient (out of the 100 created)
  • 1 instance of a NullProtocolLogger
  • 1 instance of an SmtpAuthenticationSecretDetector
  • 1 instance of a List (created by the SmtpClient)
  • 1 MimeKit.FormatOptions
  • 1 HashSet<MimeKit.HeaderId>

Conclusion: unless the 37 instances of the TaskCOmpletionSource are actually a leak, there is no leak in MailKit.

@mwalczyk81
Copy link

Is this something we need the .net team to investigate? They must be doing something different because using the legacy SMTP component (against their wishes) doesn't seem to have the same issue.

@jstedfast
Copy link
Owner

Yes, this will need to be investigated by the .NET team but I suspect it's not a leak at all, but rather just the runtime allowing the GC to grow since it has so much RAM available to it.

@jstedfast
Copy link
Owner

memory-profiler-results
memory-profiler-results-kit

@jstedfast
Copy link
Owner

jstedfast commented Jul 31, 2021

I'm closing this because I can't find a leak in the test case I've been using which effectively boils down to this (netcoreapp3.1):

using System;

using MailKit.Security;
using MailKit.Net.Smtp;

namespace MailKitMemoryLeak
{
    class Program
    {
        static void Main (string[] args)
        {
            for (int i = 0; i < 100; i++) {
                using (var client = new SmtpClient ()) {
                    client.Connect ("smtp.gmail.com", 587, SecureSocketOptions.Auto);
                    client.Disconnect (true);
                }
            }

            Console.ReadKey ();
        }
    }
}

I've been putting a breakpoint before and after the for-loop so that I can take memory snapshots and then compare the heaps.

There was a theory that the leak only happens with some servers, so maybe... but I would need someone to provide me with one of these SMTP servers so that I can try this test case against one of those. That said, MailKit wouldn't treat those different servers differently - it'd still run all of the exact same code-paths, so I don't see how there would be a leak unless the SslStream (or something that the SslStream uses) is leaking(?).

@jstedfast
Copy link
Owner

What you guys are seeing is exactly the same as this: https://stackoverflow.com/questions/18739892/finding-memory-leaks-in-c-sharp

@axof
Copy link

axof commented Aug 10, 2021

Hello,

I had a similar problem. When using Mailkit in the docker container, the memory of the container would increase by about 150-300 Mbs for each email sent. When using SecureSocketOptions.None, no such memory error appeared and everything was okay.

Before the Connect, I've added _smtpClient.CheckCertificateRevocation = false; and it now seems to work even with SecureSocketOptions.StartTls.

I find it weird that the emails were still sending without any exception before.

The problem is that LetsEncrypt SSL certificates do not include a CRL location which means that certificate revocation checks will fail.

To bypass this, you need to set client.CheckCertificateRevocation = false; before connecting.

I think this was the issue in my case.

@jstedfast
Copy link
Owner

@axof

Where did you find the LetsEncrypt CRL location quote from?

Also, if setting client.CheckCertificateRevocation = false; eliminates the memory leak, then that suggests that this memory leak is inside SslStream and that it is likely that it is downloading CRLs and caching them in memory.

It's totally plausible that these CRLs are significant in size and that would certainly explain the memory usage increasing.

@jstedfast
Copy link
Owner

Oh, that quote is from my StackOverflow answer for an SslHandshakeException question here: https://stackoverflow.com/questions/61894037/mailkit-gets-an-sslhandshakeexception-with-letsencrypt-ssl-certificates

@axof
Copy link

axof commented Aug 10, 2021

Yes, sorry I lost the tab and only had the quote left.

So, it would mean that there is no problem checking if the certificate is revoked, but the problem is that after doing it, the memory is not freed and after a couple checks, there will be a huge memory leak (in Gbs), thus crashing the container.

Also, many users on the application simultaneously sending mails would crash it even if the memory was not leaked if you use .Connect on each mail sent.

@jstedfast
Copy link
Owner

Someone should probably submit a bug report about this to the .NET Core team https://github.com/dotnet/core/issues with specific runtime versions, OS, etc.

I think everyone is seeing this on Linux, right?

@mwalczyk81
Copy link

I was on Linux yes.

@axof
Copy link

axof commented Aug 10, 2021

Linux too, yes.

@kvanderbok
Copy link
Author

Someone should probably submit a bug report about this to the .NET Core team https://github.com/dotnet/core/issues with specific runtime versions, OS, etc.

I think everyone is seeing this on Linux, right?

We also had the issue on Linux .

@jspraul
Copy link

jspraul commented Jul 29, 2022

Just as heads up, the dotnet/runtime issue was just closed:

[...] I see essentially no variation in the total virtual memory size of the process [...]
Since this is consistent with @rzikm's findings, I don't see any other action for us to take.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question A question about how to do something
Projects
None yet
Development

No branches or pull requests

7 participants