Skip to content

Commit fc6d53f

Browse files
ezelkow1Evan Zelkowitz
andauthored
Backport #12998 - Parallel SSL cert load (#13043)
* Resurrecting #7877 Parallel ssl cert loading Added updates, with tests, logging, settings and a benchmark script which shows 2x improvement on load times for certs (on my macbook) Removing the -1, 0 will autoselect, default is now 1 for old behavior * Add license to benchmark --------- Co-authored-by: Evan Zelkowitz <e_zelkowitz@apple.com>
1 parent a2515d7 commit fc6d53f

File tree

9 files changed

+551
-33
lines changed

9 files changed

+551
-33
lines changed

doc/admin-guide/files/records.yaml.en.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4079,6 +4079,17 @@ SSL Termination
40794079
:file:`ssl_multicert.config` file successfully load. If false (``0``), SSL certificate
40804080
load failures will not prevent |TS| from starting.
40814081

4082+
.. ts:cv:: CONFIG proxy.config.ssl.server.multicert.concurrency INT 1
4083+
4084+
Controls parallelism when loading :file:`ssl_multicert.config`.
4085+
A value of ``0`` automatically selects one thread per CPU core.
4086+
A value of ``1`` (the default) means single-threaded loading.
4087+
Values greater than ``1`` use that many threads.
4088+
4089+
On initial startup (before any traffic is flowing), the loader will use
4090+
``max(hardware_concurrency, configured)`` threads since there is no
4091+
traffic to compete with for CPU.
4092+
40824093
.. ts:cv:: CONFIG proxy.config.ssl.server.cert.path STRING /config
40834094
40844095
The location of the SSL certificates and chains used for accepting

include/iocore/net/SSLMultiCertConfigLoader.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@
3131

3232
#include <string>
3333
#include <set>
34+
#include <mutex>
35+
#include <thread>
36+
#include <tuple>
3437
#include <vector>
3538

3639
struct SSLConfigParams;
@@ -51,7 +54,7 @@ class SSLMultiCertConfigLoader
5154
SSLMultiCertConfigLoader(const SSLConfigParams *p) : _params(p) {}
5255
virtual ~SSLMultiCertConfigLoader(){};
5356

54-
swoc::Errata load(SSLCertLookup *lookup);
57+
swoc::Errata load(SSLCertLookup *lookup, bool firstLoad = false);
5558

5659
virtual SSL_CTX *default_server_ssl_ctx();
5760

@@ -88,6 +91,13 @@ class SSLMultiCertConfigLoader
8891
virtual bool _store_ssl_ctx(SSLCertLookup *lookup, const shared_SSLMultiCertConfigParams &ssl_multi_cert_params);
8992
bool _prep_ssl_ctx(const shared_SSLMultiCertConfigParams &sslMultCertSettings, SSLMultiCertConfigLoader::CertLoadData &data,
9093
std::set<std::string> &common_names, std::unordered_map<int, std::set<std::string>> &unique_names);
94+
95+
using SSLConfigLines = std::vector<std::tuple<char *, unsigned>>;
96+
void _load_lines(SSLCertLookup *lookup, SSLConfigLines::const_iterator begin, SSLConfigLines::const_iterator end,
97+
swoc::Errata &errata);
98+
99+
std::mutex _loader_mutex;
100+
91101
virtual void _set_handshake_callbacks(SSL_CTX *ctx);
92102
virtual bool _setup_session_cache(SSL_CTX *ctx);
93103
virtual bool _setup_dialog(SSL_CTX *ctx, const SSLMultiCertConfigParams *sslMultCertSettings);

src/iocore/net/P_SSLConfig.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ struct SSLConfigParams : public ConfigInfo {
7272
char *cipherSuite;
7373
char *client_cipherSuite;
7474
int configExitOnLoadError;
75+
int configLoadConcurrency;
7576
int clientCertLevel;
7677
int verify_depth;
7778
int ssl_origin_session_cache;

src/iocore/net/SSLConfig.cc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
#include <array>
4949
#include <cstring>
5050
#include <cmath>
51+
#include <thread>
5152
#include <unordered_map>
5253

5354
int SSLConfig::config_index = 0;
@@ -216,6 +217,7 @@ SSLConfigParams::reset()
216217
ssl_session_cache_timeout = 0;
217218
ssl_session_cache_auto_clear = 1;
218219
configExitOnLoadError = 1;
220+
configLoadConcurrency = 1;
219221
clientCertExitOnLoadError = 0;
220222
}
221223

@@ -523,6 +525,10 @@ SSLConfigParams::initialize()
523525

524526
configFilePath = ats_stringdup(RecConfigReadConfigPath("proxy.config.ssl.server.multicert.filename"));
525527
configExitOnLoadError = RecGetRecordInt("proxy.config.ssl.server.multicert.exit_on_load_fail").value_or(0);
528+
configLoadConcurrency = RecGetRecordInt("proxy.config.ssl.server.multicert.concurrency").value_or(0);
529+
if (configLoadConcurrency == 0) {
530+
configLoadConcurrency = std::thread::hardware_concurrency();
531+
}
526532

527533
{
528534
auto rec_str{RecGetRecordStringAlloc("proxy.config.ssl.server.private_key.path")};
@@ -778,7 +784,7 @@ SSLCertificateConfig::reconfigure()
778784
ink_hrtime_sleep(HRTIME_SECONDS(secs));
779785
}
780786

781-
auto errata = SSLMultiCertConfigLoader(params).load(lookup);
787+
auto errata = SSLMultiCertConfigLoader(params).load(lookup, configid == 0);
782788
if (!lookup->is_valid || (errata.has_severity() && errata.severity() >= ERRATA_ERROR)) {
783789
retStatus = false;
784790
}

src/iocore/net/SSLUtils.cc

Lines changed: 99 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@
4848
#endif
4949
#include "swoc/swoc_file.h"
5050
#include "swoc/Errata.h"
51+
52+
#include <thread>
53+
#include <tuple>
5154
#include <openssl/asn1.h>
5255
#include <openssl/bio.h>
5356
#include <openssl/bn.h>
@@ -1662,11 +1665,20 @@ SSLMultiCertConfigLoader::_store_ssl_ctx(SSLCertLookup *lookup, const shared_SSL
16621665
SSLMultiCertConfigLoader::CertLoadData data;
16631666

16641667
if (!this->_prep_ssl_ctx(sslMultCertSettings, data, common_names, unique_names)) {
1665-
lookup->is_valid = false;
1668+
{
1669+
std::lock_guard<std::mutex> lock(_loader_mutex);
1670+
lookup->is_valid = false;
1671+
}
16661672
return false;
16671673
}
16681674

16691675
std::vector<SSLLoadingContext> ctxs = this->init_server_ssl_ctx(data, sslMultCertSettings.get());
1676+
1677+
// Serialize all mutations to the shared SSLCertLookup.
1678+
// The expensive work above (_prep_ssl_ctx + init_server_ssl_ctx) runs
1679+
// without the lock, allowing parallel cert loading across threads.
1680+
std::lock_guard<std::mutex> lock(_loader_mutex);
1681+
16701682
for (const auto &loadingctx : ctxs) {
16711683
if (!sslMultCertSettings ||
16721684
!this->_store_single_ssl_ctx(lookup, sslMultCertSettings, shared_SSL_CTX{loadingctx.ctx, SSL_CTX_free}, loadingctx.ctx_type,
@@ -1916,17 +1928,49 @@ ssl_extract_certificate(const matcher_line *line_info, SSLMultiCertConfigParams
19161928
return true;
19171929
}
19181930

1919-
swoc::Errata
1920-
SSLMultiCertConfigLoader::load(SSLCertLookup *lookup)
1931+
void
1932+
SSLMultiCertConfigLoader::_load_lines(SSLCertLookup *lookup, SSLConfigLines::const_iterator begin,
1933+
SSLConfigLines::const_iterator end, swoc::Errata &errata)
19211934
{
1922-
const SSLConfigParams *params = this->_params;
1935+
const matcher_tags sslCertTags = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, false};
1936+
const SSLConfigParams *params = this->_params;
1937+
1938+
// Each thread needs its own ElevateAccess since POSIX capabilities
1939+
// are per-thread and don't propagate to spawned threads.
1940+
uint32_t elevate_setting = RecGetRecordInt("proxy.config.ssl.cert.load_elevated").value_or(0);
1941+
ElevateAccess elevate_access(elevate_setting ? ElevateAccess::FILE_PRIVILEGE : 0);
19231942

1924-
char *tok_state = nullptr;
1925-
char *line = nullptr;
1926-
unsigned line_num = 0;
1927-
matcher_line line_info;
1943+
for (auto it = begin; it != end; ++it) {
1944+
auto &[line, line_num] = *it;
19281945

1929-
const matcher_tags sslCertTags = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, false};
1946+
shared_SSLMultiCertConfigParams sslMultiCertSettings = std::make_shared<SSLMultiCertConfigParams>();
1947+
matcher_line line_info;
1948+
const char *errPtr;
1949+
1950+
errPtr = parseConfigLine(line, &line_info, &sslCertTags);
1951+
Dbg(dbg_ctl_ssl_load, "currently parsing %s at line %d from config file: %s", line, line_num, params->configFilePath);
1952+
if (errPtr != nullptr) {
1953+
Warning("%s: discarding %s entry at line %d: %s", __func__, params->configFilePath, line_num, errPtr);
1954+
} else {
1955+
if (ssl_extract_certificate(&line_info, sslMultiCertSettings.get())) {
1956+
if (sslMultiCertSettings->cert || sslMultiCertSettings->opt != SSLCertContextOption::OPT_TUNNEL) {
1957+
if (!this->_store_ssl_ctx(lookup, sslMultiCertSettings)) {
1958+
std::lock_guard<std::mutex> lock(_loader_mutex);
1959+
errata.note(ERRATA_ERROR, "Failed to load certificate on line {}", line_num);
1960+
}
1961+
} else {
1962+
std::lock_guard<std::mutex> lock(_loader_mutex);
1963+
errata.note(ERRATA_WARN, "No ssl_cert_name specified and no tunnel action set on line {}", line_num);
1964+
}
1965+
}
1966+
}
1967+
}
1968+
}
1969+
1970+
swoc::Errata
1971+
SSLMultiCertConfigLoader::load(SSLCertLookup *lookup, bool firstLoad)
1972+
{
1973+
const SSLConfigParams *params = this->_params;
19301974

19311975
Note("(%s) %s loading ...", this->_debug_tag(), ts::filename::SSL_MULTICERT);
19321976

@@ -1935,7 +1979,6 @@ SSLMultiCertConfigLoader::load(SSLCertLookup *lookup)
19351979
if (ec) {
19361980
switch (ec.value()) {
19371981
case ENOENT:
1938-
// missing config file is an acceptable runtime state
19391982
return swoc::Errata(ERRATA_WARN, "Cannot open SSL certificate configuration \"{}\" - {}", params->configFilePath, ec);
19401983
default:
19411984
return swoc::Errata(ERRATA_ERROR, "Failed to read SSL certificate configuration from \"{}\" - {}", params->configFilePath,
@@ -1949,8 +1992,13 @@ SSLMultiCertConfigLoader::load(SSLCertLookup *lookup)
19491992
elevate_setting = RecGetRecordInt("proxy.config.ssl.cert.load_elevated").value_or(0);
19501993
ElevateAccess elevate_access(elevate_setting ? ElevateAccess::FILE_PRIVILEGE : 0);
19511994

1995+
// Collect all non-empty, non-comment lines for processing
1996+
char *tok_state = nullptr;
1997+
char *line = nullptr;
1998+
unsigned line_num = 0;
1999+
SSLConfigLines config_lines;
2000+
19522001
line = tokLine(content.data(), &tok_state);
1953-
swoc::Errata errata(ERRATA_NOTE);
19542002
while (line != nullptr) {
19552003
line_num++;
19562004

@@ -1960,28 +2008,49 @@ SSLMultiCertConfigLoader::load(SSLCertLookup *lookup)
19602008
}
19612009

19622010
if (*line != '\0' && *line != '#') {
1963-
shared_SSLMultiCertConfigParams sslMultiCertSettings = std::make_shared<SSLMultiCertConfigParams>();
1964-
const char *errPtr;
2011+
config_lines.emplace_back(line, line_num);
2012+
}
2013+
line = tokLine(nullptr, &tok_state);
2014+
}
19652015

1966-
errPtr = parseConfigLine(line, &line_info, &sslCertTags);
1967-
Dbg(dbg_ctl_ssl_load, "currently parsing %s at line %d from config file: %s", line, line_num, params->configFilePath);
1968-
if (errPtr != nullptr) {
1969-
Warning("%s: discarding %s entry at line %d: %s", __func__, params->configFilePath, line_num, errPtr);
1970-
} else {
1971-
if (ssl_extract_certificate(&line_info, sslMultiCertSettings.get())) {
1972-
// There must be a certificate specified unless the tunnel action is set
1973-
if (sslMultiCertSettings->cert || sslMultiCertSettings->opt != SSLCertContextOption::OPT_TUNNEL) {
1974-
if (!this->_store_ssl_ctx(lookup, sslMultiCertSettings)) {
1975-
errata.note(ERRATA_ERROR, "Failed to load certificate on line {}", line_num);
1976-
}
1977-
} else {
1978-
errata.note(ERRATA_WARN, "No ssl_cert_name specified and no tunnel action set on line {}", line_num);
1979-
}
1980-
}
1981-
}
2016+
swoc::Errata errata(ERRATA_NOTE);
2017+
2018+
if (params->configLoadConcurrency > 1 && config_lines.size() > 1) {
2019+
// On first load (no traffic yet), allow more threads for faster startup
2020+
int num_threads = params->configLoadConcurrency;
2021+
if (firstLoad) {
2022+
num_threads = std::max(static_cast<int>(std::thread::hardware_concurrency()), num_threads);
19822023
}
19832024

1984-
line = tokLine(nullptr, &tok_state);
2025+
// Don't spawn more threads than lines
2026+
num_threads = std::min(num_threads, static_cast<int>(config_lines.size()));
2027+
2028+
std::size_t bucket_size = config_lines.size() / num_threads;
2029+
std::size_t remainder = config_lines.size() % num_threads;
2030+
SSLConfigLines::const_iterator current = config_lines.cbegin();
2031+
std::vector<std::thread> threads;
2032+
2033+
Note("(%s) loading %zu certs with %d threads", this->_debug_tag(), config_lines.size(), num_threads);
2034+
2035+
for (int t = 0; t < num_threads; ++t) {
2036+
// Distribute remainder lines across the first threads
2037+
std::size_t this_bucket = bucket_size + (static_cast<std::size_t>(t) < remainder ? 1 : 0);
2038+
SSLConfigLines::const_iterator end = current + this_bucket;
2039+
2040+
threads.emplace_back(&SSLMultiCertConfigLoader::_load_lines, this, lookup, current, end, std::ref(errata));
2041+
current = end;
2042+
}
2043+
2044+
for (auto &th : threads) {
2045+
th.join();
2046+
}
2047+
2048+
Note("(%s) loaded %zu certs in %d threads", this->_debug_tag(), config_lines.size(), num_threads);
2049+
} else {
2050+
// Single-threaded path (concurrency == 1 or only 1 line)
2051+
this->_load_lines(lookup, config_lines.cbegin(), config_lines.cend(), errata);
2052+
2053+
Note("(%s) loaded %zu certs (single-threaded)", this->_debug_tag(), config_lines.size());
19852054
}
19862055

19872056
// We *must* have a default context even if it can't possibly work. The default context is used to

src/records/RecordsConfig.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1174,7 +1174,9 @@ static constexpr RecordElement RecordsConfig[] =
11741174
{RECT_CONFIG, "proxy.config.ssl.server.multicert.filename", RECD_STRING, ts::filename::SSL_MULTICERT, RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
11751175
,
11761176
{RECT_CONFIG, "proxy.config.ssl.server.multicert.exit_on_load_fail", RECD_INT, "1", RECU_RESTART_TS, RR_NULL, RECC_INT, "[0-1]", RECA_NULL}
1177-
,
1177+
,
1178+
{RECT_CONFIG, "proxy.config.ssl.server.multicert.concurrency", RECD_INT, "1", RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
1179+
,
11781180
{RECT_CONFIG, "proxy.config.ssl.servername.filename", RECD_STRING, ts::filename::SNI, RECU_RESTART_TS, RR_NULL, RECC_NULL, nullptr, RECA_NULL}
11791181
,
11801182
{RECT_CONFIG, "proxy.config.ssl.server.ticket_key.filename", RECD_STRING, nullptr, RECU_DYNAMIC, RR_NULL, RECC_NULL, nullptr, RECA_NULL}

tests/gold_tests/tls/ssl_multicert_loader.test.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,52 @@
109109
ts2.Disk.traffic_out.Content = Testers.ExcludesExpression(
110110
'Traffic Server is fully initialized', 'process should fail when invalid certificate specified')
111111
ts2.Disk.diags_log.Content = Testers.IncludesExpression('EMERGENCY: failed to load SSL certificate file', 'check diags.log"')
112+
113+
##########################################################################
114+
# Ensure parallel cert loading works correctly with multiple certs
115+
116+
ts3 = Test.MakeATSProcess("ts3", enable_tls=True)
117+
server3 = Test.MakeOriginServer("server4")
118+
server3.addResponse("sessionlog.json", request_header, response_header)
119+
120+
ts3.Disk.records_config.update(
121+
{
122+
'proxy.config.ssl.server.cert.path': f'{ts3.Variables.SSLDir}',
123+
'proxy.config.ssl.server.private_key.path': f'{ts3.Variables.SSLDir}',
124+
'proxy.config.ssl.server.multicert.concurrency': 4,
125+
'proxy.config.diags.debug.enabled': 1,
126+
'proxy.config.diags.debug.tags': 'ssl_load',
127+
})
128+
129+
ts3.addDefaultSSLFiles()
130+
131+
ts3.Disk.remap_config.AddLine(f'map / http://127.0.0.1:{server3.Variables.Port}')
132+
133+
# Add enough cert lines to exercise multiple threads (need > 1 for parallel path,
134+
# and ideally >= concurrency to actually use all threads)
135+
ts3.Disk.ssl_multicert_config.AddLines(
136+
[
137+
'dest_ip=* ssl_cert_name=server.pem ssl_key_name=server.key',
138+
'ssl_cert_name=server.pem ssl_key_name=server.key',
139+
'ssl_cert_name=server.pem ssl_key_name=server.key',
140+
'ssl_cert_name=server.pem ssl_key_name=server.key',
141+
'ssl_cert_name=server.pem ssl_key_name=server.key',
142+
])
143+
144+
tr5 = Test.AddTestRun("Verify parallel cert loading works")
145+
tr5.Processes.Default.StartBefore(ts3)
146+
tr5.Processes.Default.StartBefore(server3)
147+
tr5.StillRunningAfter = ts3
148+
tr5.StillRunningAfter = server3
149+
tr5.MakeCurlCommand(
150+
f"-q -s -v -k --resolve '{sni_domain}:{ts3.Variables.ssl_port}:127.0.0.1' https://{sni_domain}:{ts3.Variables.ssl_port}",
151+
ts=ts3)
152+
tr5.Processes.Default.ReturnCode = 0
153+
tr5.Processes.Default.Streams.stdout = Testers.ExcludesExpression("Could Not Connect", "Check response")
154+
tr5.Processes.Default.Streams.stderr = Testers.IncludesExpression(f"CN={sni_domain}", "Check response")
155+
156+
# Verify the parallel loading code path was actually exercised.
157+
# With 5 identical cert lines, 4 will fail to insert as duplicates.
158+
# This confirms all lines were processed (regardless of thread count).
159+
ts3.Disk.diags_log.Content = Testers.ContainsExpression(
160+
'Failed to insert SSL_CTX for certificate', 'duplicate cert insertions confirm all lines were processed')

0 commit comments

Comments
 (0)