Skip to content

cache NIOSSLContext (saves 27k allocs per conn) #362

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

Merged
merged 1 commit into from
May 13, 2021

Conversation

weissi
Copy link
Contributor

@weissi weissi commented May 8, 2021

Motivation:

At the moment, AHC assumes that creating a NIOSSLContext is both cheap
and doesn't block.

Neither of these two assumptions are true.

To create a NIOSSLContext, BoringSSL will have to read a lot of
certificates in the trust store (on disk) which require a lot of ASN1
parsing and much much more.

On my Ubuntu test machine, creating one NIOSSLContext is about 27,000
allocations!!! To make it worse, AHC allocates a fresh NIOSSLContext
for every single connection, whether HTTP or HTTPS. Yes, correct.

Modification:

  • Cache NIOSSLContexts per TLSConfiguration in a LRU cache
  • Don't get an NIOSSLContext for HTTP (plain text) connections

Result:

New connections should be much faster in general assuming that you're
not using a different TLSConfiguration for every connection.

@weissi weissi force-pushed the jw-cache-sslcontext branch 2 times, most recently from c6bfa23 to c946b23 Compare May 8, 2021 11:59
@weissi
Copy link
Contributor Author

weissi commented May 8, 2021

before

49,640 allocations for 100 plain-text HTTP requests over 1 connection

before

after

22,158 allocations for 100 plain-text HTTP requests over 1 connection

after

@weissi weissi force-pushed the jw-cache-sslcontext branch from c946b23 to 15b2e3b Compare May 11, 2021 13:43
@@ -148,7 +153,7 @@ final class ConnectionPool {
var host: String
var port: Int
var unixPath: String
var tlsConfiguration: BestEffortHashableTLSConfiguration?
private var tlsConfiguration: BestEffortHashableTLSConfiguration?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: make this private because nobody accesses it from the outside and if not private, then there are places where we have both a key.tlsConfiguration and a configuration.tlsConfiguration which aren't necessarily the same (eg. when doing https:// over a http:// proxy).

@weissi weissi force-pushed the jw-cache-sslcontext branch 3 times, most recently from 65ff26c to e6f5270 Compare May 11, 2021 14:28
@weissi weissi marked this pull request as ready for review May 11, 2021 14:29
@weissi weissi force-pushed the jw-cache-sslcontext branch 2 times, most recently from e184939 to ae9c9ac Compare May 11, 2021 14:44
@weissi weissi force-pushed the jw-cache-sslcontext branch from ae9c9ac to 539671d Compare May 11, 2021 18:49
Comment on lines +294 to +279
var isNIOTS: Bool {
#if canImport(Network)
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
return self.underlyingBootstrap is NIOTSConnectionBootstrap
} else {
return false
}
#else
return false
#endif
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason, why we don't use this pattern?

Suggested change
var isNIOTS: Bool {
#if canImport(Network)
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
return self.underlyingBootstrap is NIOTSConnectionBootstrap
} else {
return false
}
#else
return false
#endif
}
var isNIOTS: Bool {
#if canImport(Network)
if #available(OSX 10.14, iOS 12.0, tvOS 12.0, watchOS 6.0, *) {
return self.underlyingBootstrap is NIOTSConnectionBootstrap
}
#endif
return false
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reduced the amount of code we need here but happy to add another } else { return false }

port: key.port,
requiresTLS: requiresTLS,
configuration: configuration,
sslContextCache: sslContextCache,
Copy link
Member

@fabianfett fabianfett May 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we pass the SSLContextCache down here? We already have a sslContext (a Future) created before. IMHO it would make more sense to pass that down.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is an excellent question, I have no idea. I'll look into this, maybe we can just inject the SSLContext into that method.

Comment on lines +215 to +102
sslContext.map { sslContext -> (Channel, NIOSSLContext?) in
(channel, sslContext)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we now need the actual context. We're kicking off the connection creation and an SSLContext creation in parallel and this is the point where we need both.

Comment on lines 101 to 103
bootstrap = sslContextCache.sslContext(tlsConfiguration: configuration.tlsConfiguration ?? .forClient(),
eventLoop: eventLoop,
logger: logger)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is hit with in a test that doesn't require https: testUploadStreamingBackpressure

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great catch! Thanks you

@weissi weissi force-pushed the jw-cache-sslcontext branch from 539671d to e9c243b Compare May 12, 2021 11:45
@weissi weissi force-pushed the jw-cache-sslcontext branch 2 times, most recently from 2d23f5a to 064060e Compare May 12, 2021 13:18
@weissi weissi force-pushed the jw-cache-sslcontext branch 2 times, most recently from 5fa35bd to a7a30a8 Compare May 12, 2021 13:36
@weissi weissi requested review from Lukasa and fabianfett May 12, 2021 13:40
@Lukasa Lukasa added the 🔨 semver/patch No public API change. label May 12, 2021
Copy link
Collaborator

@Lukasa Lukasa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, this LGTM.

@weissi weissi force-pushed the jw-cache-sslcontext branch from a7a30a8 to 89336a0 Compare May 12, 2021 17:49
Copy link
Member

@fabianfett fabianfett left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. 🎉

@weissi weissi force-pushed the jw-cache-sslcontext branch from 89336a0 to b2092fc Compare May 13, 2021 11:04
Motivation:

At the moment, AHC assumes that creating a `NIOSSLContext` is both cheap
and doesn't block.

Neither of these two assumptions are true.

To create a `NIOSSLContext`, BoringSSL will have to read a lot of
certificates in the trust store (on disk) which require a lot of ASN1
parsing and much much more.

On my Ubuntu test machine, creating one `NIOSSLContext` is about 27,000
allocations!!! To make it worse, AHC allocates a fresh `NIOSSLContext`
for _every single connection_, whether HTTP or HTTPS. Yes, correct.

Modification:

- Cache NIOSSLContexts per TLSConfiguration in a LRU cache
- Don't get an NIOSSLContext for HTTP (plain text) connections

Result:

New connections should be _much_ faster in general assuming that you're
not using a different TLSConfiguration for every connection.
@weissi weissi force-pushed the jw-cache-sslcontext branch from b2092fc to 97b4896 Compare May 13, 2021 11:05
@weissi weissi merged commit e2d03ff into swift-server:main May 13, 2021
@weissi weissi deleted the jw-cache-sslcontext branch May 13, 2021 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🔨 semver/patch No public API change.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants