common/crypto: cache EVP_MD_fetch() result for non-FIPS MD5 - #71062
Open
JBakamovic wants to merge 1 commit into
Open
common/crypto: cache EVP_MD_fetch() result for non-FIPS MD5#71062JBakamovic wants to merge 1 commit into
JBakamovic wants to merge 1 commit into
Conversation
On OpenSSL 3, OpenSSLDigest::SetFlags(EVP_MD_CTX_FLAG_NON_FIPS_ALLOW)
calls EVP_MD_fetch(NULL, "MD5", "fips=no") every time it runs. rgw
constructs an MD5 digest and calls SetFlags() once per object write to
compute the etag (RGWPutObj::execute and friends), so under small
object workloads this performs a provider-store fetch per request.
EVP_MD_fetch() takes the provider store lock and evaluates the property
query, and OpenSSL documents the intended usage as fetch once, use many
times.
Fetch the algorithm once per process instead and hand each digest
instance its own reference via EVP_MD_up_ref(), keeping the
EVP_MD_free() in the destructor balanced. EVP_MD objects are
reference-counted and safe to share across threads.
Note that using the legacy static EVP_md5() instead would not avoid the
cost: on OpenSSL 3 initializing a digest context from a legacy static
EVP_MD performs an implicit fetch on every EVP_DigestInit_ex(), which
benchmarks the same as the explicit per-instance fetch (see "implicit"
below). Caching the fetched EVP_MD is the only variant that avoids the
per-request provider-store traffic. The other ceph_crypto digests
(SHA1/SHA256/SHA512, used per-request by e.g. rgw's SigV4 code via the
implicit-fetch path) could benefit from the same treatment as a
follow-up.
Microbenchmark (source in the PR description): each iteration performs
one complete etag-style digest, exactly as rgw does per PUT: allocate
an EVP_MD_CTX, obtain the MD5 EVP_MD, EVP_DigestInit_ex(), hash a 4KB
payload, EVP_DigestFinal_ex(), free the context. The three modes
differ only in how the EVP_MD is obtained:
- "fetch": EVP_MD_fetch(NULL, "MD5", "fips=no") per iteration,
EVP_MD_free() afterwards -- the current behavior, where every digest
instance performs its own provider-store fetch;
- "cached": EVP_MD_fetch() once into a function-local static, then
EVP_MD_up_ref() per iteration -- the behavior with this change;
- "implicit": the legacy static EVP_md5() -- no explicit fetch, but on
OpenSSL 3 EVP_DigestInit_ex() performs an implicit fetch internally
on every init.
N threads run the loop concurrently and share no state beyond what
libcrypto itself shares. Numbers are wall-clock ns per digest
operation (lower is better), 200K iterations per thread, libcrypto
3.2.4, shown as fetch -> cached:
1 thread 8 threads 32 threads
Ryzen 9 9955HX 16C/32T 3819 -> 3724 567 -> 500 311 -> 197
Xeon Gold 6152 22C/44T 6506 -> 6344 1131 -> 990 686 -> 583
Reading the numbers: at 1 thread the variants differ by only ~2.5%,
i.e. the uncontended fetch is cheap. The gap widens with concurrency:
+13%/+14% ops/s at 8 threads, +58%/+18% ops/s at 32 threads. That
pattern is the provider store lock serializing concurrent fetches: the
cost is contention rather than per-call latency, so it grows with
exactly the parameter (concurrent requests per daemon) that loaded
radosgw deployments maximize. The "implicit" mode benchmarks the same
as "fetch" on both machines (Ryzen 32T: 314 vs 311 ns/op), confirming
that switching to the legacy EVP_md5() static would not avoid the
contention; caching the fetched EVP_MD is what removes it.
End-to-end behavior is unchanged: s3-tests boto3 functional smoke
passes against a vstart cluster with this change (every PUT's etag is
an MD5 that clients verify). Profiling radosgw under a small vstart
PUT workload shows no measurable throughput difference, as expected:
such a setup is IO-bound and rgw CPU is not the bottleneck there. The
change targets CPU-saturated, many-threaded radosgw deployments.
Signed-off-by: Jusufadis Bakamovic <jusufadis.bakamovic@clyso.com>
Author
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
On OpenSSL 3, OpenSSLDigest::SetFlags(EVP_MD_CTX_FLAG_NON_FIPS_ALLOW) calls EVP_MD_fetch(NULL, "MD5", "fips=no") every time it runs. rgw constructs an MD5 digest and calls SetFlags() once per object write to compute the etag (RGWPutObj::execute and friends), so under small object workloads this performs a provider-store fetch per request. EVP_MD_fetch() takes the provider store lock and evaluates the property query, and OpenSSL documents the intended usage as fetch once, use many times.
Fetch the algorithm once per process instead and hand each digest instance its own reference via EVP_MD_up_ref(), keeping the EVP_MD_free() in the destructor balanced. EVP_MD objects are reference-counted and safe to share across threads.
Note that using the legacy static EVP_md5() instead would not avoid the cost: on OpenSSL 3 initializing a digest context from a legacy static EVP_MD performs an implicit fetch on every EVP_DigestInit_ex(), which benchmarks the same as the explicit per-instance fetch (see "implicit" below). Caching the fetched EVP_MD is the only variant that avoids the per-request provider-store traffic. The other ceph_crypto digests (SHA1/SHA256/SHA512, used per-request by e.g. rgw's SigV4 code via the implicit-fetch path) could benefit from the same treatment as a follow-up.
Microbenchmark (source in the PR description): each iteration performs one complete etag-style digest, exactly as rgw does per PUT: allocate an EVP_MD_CTX, obtain the MD5 EVP_MD, EVP_DigestInit_ex(), hash a 4KB payload, EVP_DigestFinal_ex(), free the context. The three modes differ only in how the EVP_MD is obtained:
N threads run the loop concurrently and share no state beyond what libcrypto itself shares. Numbers are wall-clock ns per digest operation (lower is better), 200K iterations per thread, libcrypto 3.2.4, shown as fetch -> cached:
Reading the numbers: at 1 thread the variants differ by only ~2.5%, i.e. the uncontended fetch is cheap. The gap widens with concurrency: +13%/+14% ops/s at 8 threads, +58%/+18% ops/s at 32 threads. That pattern is the provider store lock serializing concurrent fetches: the cost is contention rather than per-call latency, so it grows with exactly the parameter (concurrent requests per daemon) that loaded radosgw deployments maximize. The "implicit" mode benchmarks the same as "fetch" on both machines (Ryzen 32T: 314 vs 311 ns/op), confirming that switching to the legacy EVP_md5() static would not avoid the contention; caching the fetched EVP_MD is what removes it.
End-to-end behavior is unchanged: s3-tests boto3 functional smoke passes against a vstart cluster with this change (every PUT's etag is an MD5 that clients verify). Profiling radosgw under a small vstart PUT workload shows no measurable throughput difference, as expected: such a setup is IO-bound and rgw CPU is not the bottleneck there. The change targets CPU-saturated, many-threaded radosgw deployments.
Signed-off-by: Jusufadis Bakamovic jusufadis.bakamovic@clyso.com
Contribution Guidelines
To sign and title your commits, please refer to Submitting Patches to Ceph.
If you are submitting a fix for a stable branch (e.g. "quincy"), please refer to Submitting Patches to Ceph - Backports for the proper workflow.
When filling out the below checklist, you may click boxes directly in the GitHub web UI. When entering or editing the entire PR message in the GitHub web UI editor, you may also select a checklist item by adding an
xbetween the brackets:[x]. Spaces and capitalization matter when checking off items this way.Checklist
Show available Jenkins commands
jenkins test classic perfJenkins Job | Jenkins Job Definitionjenkins test crimson perfJenkins Job | Jenkins Job Definitionjenkins test signedJenkins Job | Jenkins Job Definitionjenkins test make checkJenkins Job | Jenkins Job Definitionjenkins test make check arm64Jenkins Job | Jenkins Job Definitionjenkins test submodulesJenkins Job | Jenkins Job Definitionjenkins test dashboardJenkins Job | Jenkins Job Definitionjenkins test dashboard cephadmJenkins Job | Jenkins Job Definitionjenkins test apiJenkins Job | Jenkins Job Definitionjenkins test docsReadTheDocs | Github Workflow Definitionjenkins test ceph-volume allJenkins Jobs | Jenkins Jobs Definitionjenkins test windowsJenkins Job | Jenkins Job Definitionjenkins test rook e2eJenkins Job | Jenkins Job DefinitionYou must only issue one Jenkins command per-comment. Jenkins does not understand
comments with more than one command.