runtime: Slow SSL Authentication on Ubuntu 16.04 / 18.04

We investigate errors during SSL authentication on ubuntu using kestrel. We initiate many concurrent connections and observer that we hang on AuthenticateAsClientAsync and AuthenticateAsServerAsync. While trying to make a minimal repo, we decided to do a simple sever-client app that simulate our usage.

Running the same app on windows resulted in: Plain ~20ms SSL ~200ms

while on ubuntu 18.04: Plain ~1s SSL ~40s

I also tried on a VM running 16.04 Plain ~1s SSL ~5s

In all of the test the dotnet version is 2.1.403

using System;
using System.Net;
using System.Diagnostics;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    public static class Program
    {
        public static void Main(string[] args)
        {
#if USE_SSL
        System.Console.WriteLine("SSL");
#else
            Console.WriteLine("Plain");
#endif
            var globalCertificate = new X509Certificate2("path/to/cert.pfx");
            var listener = new TcpListener(new IPAddress(new byte[] { 127, 0, 0, 1 }), 56789);
            listener.Start();
            for (var i = 0; i < 1; i++)
                Listen(listener, globalCertificate);
            var overall = Stopwatch.StartNew();
            int done = 0;
            int failed = 0;
            var clients = 200;
            var tasks = new Task[clients];
            for (int i = 0; i < clients; i++)
            {
                var j = i;
                tasks[i] = Task.Run(async () =>
                {
                    var sp = Stopwatch.StartNew();
                    try
                    {
                        using (var tcpClient = new TcpClient())
                        {
                            await tcpClient.ConnectAsync(new IPAddress(new byte[] { 127, 0, 0, 1 }), 56789);
#if USE_SSL
                            using (var wrapped = new SslStream(tcpClient.GetStream(), false, ((sender, certificate, chain, errors) => true)))
                            {
                                await wrapped.AuthenticateAsClientAsync("localhost", new X509CertificateCollection(new[] {globalCertificate}),
                                    SslProtocols.Tls12, false);
#else
                            using (var wrapped = tcpClient.GetStream())
                            {
#endif
                                byte[] buffer = { 1 };
                                await wrapped.WriteAsync(buffer, 0, 1);
                                var read = await wrapped.ReadAsync(buffer, 0, 1);

                                if (read != 1)
                                {
                                    Console.WriteLine("why?");
                                }

                                Interlocked.Increment(ref done);
                            }
                        }
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine(Interlocked.Increment(ref failed) + " FAILED " + sp.Elapsed);
                        Console.WriteLine(e);
                    }
                });
            }

            Task.WaitAll(tasks);

            Console.WriteLine("Done " + done + " failed " + failed + " in " + overall.Elapsed);
        }

        private static void Listen(TcpListener listener, X509Certificate globalCertificate)
        {
            Task.Run(async () =>
            {

                try
                {
                    var tcpClient = await listener.AcceptTcpClientAsync();
                    Listen(listener, globalCertificate);

#if USE_SSL
                    using (var wrapped = new SslStream(tcpClient.GetStream(), false, ((sender, certificate, chain, errors) => true)))
                    {
                        await wrapped.AuthenticateAsServerAsync(globalCertificate);
#else
                    using (var wrapped = tcpClient.GetStream())
                    {
#endif

                        var buffer = new byte[1];

                        var read = await wrapped.ReadAsync(buffer, 0, 1);

                        await wrapped.WriteAsync(buffer, 0, 1);
                    }
                }
                catch (Exception e)
                {
                    Console.WriteLine(e);
                }
            });
        }
    }
}

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 1
  • Comments: 23 (14 by maintainers)

Most upvoted comments

one more benchmark -> crank form asp.net RPS: 1,018 ->19,928

crank compare 6.0.json 7.0.json

application 6.0 7.0
CPU Usage (%) 70 78 +11.43%
Cores usage (%) 845 930 +10.06%
Working Set (MB) 182 286 +57.14%
Private Memory (MB) 679 862 +26.95%
Build Time (ms) 3,208 1,388 -56.73%
Start Time (ms) 149 232 +55.70%
Published Size (KB) 112,861 113,797 +0.83%
.NET Core SDK Version 6.0.400 7.0.100-rc.1.22423.18
load 6.0 7.0
CPU Usage (%) 100 99 -1.00%
Cores usage (%) 1,201 1,187 -1.17%
Working Set (MB) 109 49 -55.05%
Private Memory (MB) 460 357 -22.39%
Start Time (ms) 0 0
First Request (ms) 199 212 +6.53%
Requests/sec 1,018 19,928 +1,857.25%
Requests 15,365 300,772 +1,857.51%
Mean latency (ms) 2.81 0.56 -80.05%
Max latency (ms) 37.77 31.87 -15.62%
Bad responses 0 0
Socket errors 0 0
Read throughput (MB/s) 0.15 2.87 +1,857.43%
Latency 50th (ms) 0.73 0.30 -59.28%
Latency 75th (ms) 4.24 0.51 -88.07%
Latency 90th (ms) 9.92 0.77 -92.26%
Latency 99th (ms) 17.78 8.00 -55.01%

This should be fixed in 7.0 running benchmark from https://github.com/dotnet/performance

Ubuntu 20.04
|                           Method |        Job |  Runtime | Toolchain | protocol |         Mean |        Error |       StdDev |       Median |           Min |          Max | Ratio | RatioSD | Allocated | Alloc Ratio |
|--------------------------------- |----------- |--------- |---------- |--------- |-------------:|-------------:|-------------:|-------------:|--------------:|-------------:|------:|--------:|----------:|------------:|
| DefaultHandshakeContextIPv4Async | Job-TGUDPL | .NET 6.0 |    net6.0 |        ? | 7,140.94 us |  72.833 us |  68.128 us | 7,122.85 us | 7,049.24 us | 7,281.16 us |  1.00 |    0.00 |   19856 B |        1.00 |
| DefaultHandshakeContextIPv4Async | Job-GVAJIU | .NET 7.0 |    net7.0 |        ? | 2,226.72 us |  44.359 us |  39.323 us | 2,227.98 us | 2,141.81 us | 2,297.37 us |  0.31 |    0.01 |    8835 B |        0.44 |

Windows Server 2022
|                           Method |        Job |  Runtime | Toolchain | protocol |         Mean |        Error |       StdDev |       Median |           Min |          Max | Ratio | RatioSD | Allocated | Alloc Ratio |
|--------------------------------- |----------- |--------- |---------- |--------- |-------------:|-------------:|-------------:|-------------:|--------------:|-------------:|------:|--------:|----------:|------------:|
| DefaultHandshakeContextIPv4Async | Job-FEYPRU | .NET 6.0 |    net6.0 |        ? |  2,409.34 us |  16.292 us |  15.239 us |  2,407.36 us |  2,388.792 us |  2,438.48 us |  1.00 |    0.00 |   13915 B |        1.00 |
| DefaultHandshakeContextIPv4Async | Job-VJNBCK | .NET 7.0 |    net7.0 |        ? |  2,467.38 us |  71.931 us |  79.952 us |  2,421.02 us |  2,391.125 us |  2,650.48 us |  1.03 |    0.04 |    4811 B |        0.35 |

For TLS resume to work, the server side has to use SslStreamCertificateContext. (and no CipherSuitePolicy)

Re methodology here: I’m always a little skeptical of tests where we spawn a lot (200, here) of tasks at the same time. The problem is that this introduces the added variable of task/thread pool scheduling, which has differences between Windows and Linux.

A better approach, IMO, is to spawn a fixed number of threads and have them loop over many iterations, then measure the overall throughput.

I do think it’s likely we have a problem here, but I’d like to get numbers that better reflect the actual problem without introducing additional variables.

Windows results


BenchmarkDotNet=v0.11.1, OS=Windows 10.0.17134.345 (1803/April2018Update/Redstone4)
Intel Core i7-8700K CPU 3.70GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
Frequency=3609377 Hz, Resolution=277.0561 ns, Timer=TSC
.NET Core SDK=2.1.403
  [Host]     : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
  Job-WNQDRD : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT

MinIterationTime=1.0000 s  InvocationCount=1  UnrollFactor=1  

Method Clients UseSsl Mean Error StdDev Median
Logic 1 False 206.3 us 16.28 us 46.70 us 192.8 us
Logic 1 True 5,808.3 us 115.09 us 262.12 us 5,762.3 us
Logic 200 False 11,377.5 us 366.87 us 1,034.77 us 11,499.8 us
Logic 200 True 145,494.8 us 2,020.88 us 1,791.46 us 145,074.2 us

Ubuntu 16.04 VM


BenchmarkDotNet=v0.11.1, OS=ubuntu 16.04
Intel Core i7-8700K CPU 3.70GHz (Coffee Lake), 1 CPU, 4 logical and 4 physical cores
.NET Core SDK=2.1.403
  [Host]     : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
  Job-TGPQPU : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT

MinIterationTime=1.0000 s  InvocationCount=1  UnrollFactor=1  

Method Clients UseSsl Mean Error StdDev
Logic 1 False 320.4 us 42.36 us 116.7 us
Logic 1 True 100,485.5 us 2,289.40 us 6,750.4 us
Logic 200 False 1,022,986.8 us 7,503.23 us 7,018.5 us
Logic 200 True 4,839,151.4 us 40,209.63 us 37,612.1 us

Real machine Ubuntu 18.04


BenchmarkDotNet=v0.11.1, OS=ubuntu 18.04
Intel Core i7-2600 CPU 3.40GHz (Sandy Bridge), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=2.1.403
  [Host]     : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT
  Job-SYOZYF : .NET Core 2.1.5 (CoreCLR 4.6.26919.02, CoreFX 4.6.26919.02), 64bit RyuJIT

MinIterationTime=1.0000 s  InvocationCount=1  UnrollFactor=1

Method Clients UseSsl Mean Error StdDev Median
Logic 1 False 211.8 us 17.77 us 51.56 us 200.8 us
Logic 1 True 667,490.7 us 1,924.58 us 1,706.09 us 667,549.8 us
Logic 200 False 932,487.9 us 98,382.48 us 282,277.78 us 1,017,976.7 us
Logic 200 True 35,283,175.3 us 61,730.03 us 54,722.06 us 35,281,549.3 us

This is first time I use BenchmarkDotNet so hope I got it correct 😃

using System;
using System.Net;
using System.IO;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace ConsoleApp1
{
    public static class Program
    {
        [MinIterationTime(1000)]
        public class SslTest
        {
            public static X509Certificate2 GlobalCertificate = new X509Certificate2(@"cert.pfx");

            [Params(1, 200)]
            public  int Clients;

            [Params(false,true)]
            public bool UseSsl;

            public  int Port;

            public TcpListener Listener;

            [IterationSetup]
            public void Setup()
            {
                Listener = new TcpListener(IPAddress.Loopback, 0);
                Listener.Start();
                Port = ((IPEndPoint)Listener.LocalEndpoint).Port;

                for (var i = 0; i < 1; i++)
                    Listen(Listener, UseSsl);
            }
            [IterationCleanup]
            public void CleanUp()
            {
                Listener.Stop();
            }

            [Benchmark]
            public async Task Logic()
            {
                var tasks = new Task[Clients];
                for (int i = 0; i < Clients; i++)
                {
                    tasks[i] = Task.Run(async () =>
                    {
                        await SingleConnection(UseSsl);

                    });
                }

                await Task.WhenAll(tasks);
            }

            public async Task<Stream> WrapServer(TcpClient tcpClient, bool ssl)
            {
                var stream = tcpClient.GetStream();
                if (ssl == false)
                    return stream;

                var wrapped = new SslStream(tcpClient.GetStream(), false,
                    ((sender, certificate, chain, errors) => true));

                await wrapped.AuthenticateAsServerAsync(GlobalCertificate);
                return wrapped;
            }

            public async Task<Stream> WrapClient(TcpClient tcpClient, bool ssl)
            {
                var stream = tcpClient.GetStream();
                if (ssl == false)
                    return stream;

                var wrapped = new SslStream(stream, false, ((sender, certificate, chain, errors) => true));

                await wrapped.AuthenticateAsClientAsync("localhost",
                    new X509CertificateCollection(new X509Certificate[] {GlobalCertificate}),
                    SslProtocols.Tls12, false);
                return wrapped;
            }

            public async Task SingleConnection(bool ssl)
            {
                using (var tcpClient = new TcpClient())
                {
                    tcpClient.LingerState = new LingerOption(false, 0);
                    await tcpClient.ConnectAsync(IPAddress.Loopback, Port);
                    using (var wrapped = await WrapClient(tcpClient, ssl))
                    {
                        byte[] buffer = { 1 };
                        await wrapped.WriteAsync(buffer, 0, 1);
                        var read = await wrapped.ReadAsync(buffer, 0, 1);

                        if (read != 1)
                        {
                            Console.WriteLine("why?");
                        }
                    }
                }
            }

            private void Listen(TcpListener listener, bool ssl)
            {
                Task.Run(async () =>
                {
                    var tcpClient = await listener.AcceptTcpClientAsync();
                    Listen(listener, ssl);
                    tcpClient.LingerState = new LingerOption(false, 0);

                    using (var wrapped = await WrapServer(tcpClient, ssl))
                    {

                        var buffer = new byte[1];

                        var read = await wrapped.ReadAsync(buffer, 0, 1);

                        await wrapped.WriteAsync(buffer, 0, 1);
                    }
                });
            }
        }

        public static void Main(string[] args)
        {
            var summary = BenchmarkRunner.Run<SslTest>();
        }
    }
}