Skip to content

DRIVERS-3620 Reduce OpenTelemetry tracing overhead, benchmark driver configurations - #3108

Draft
comandeo-mongo wants to merge 11 commits into
mongodb:masterfrom
comandeo-mongo:DRIVERS-3620
Draft

comandeo-mongo wants to merge 11 commits into
mongodb:masterfrom
comandeo-mongo:DRIVERS-3620

Conversation

@comandeo-mongo

@comandeo-mongo comandeo-mongo commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

DRIVERS-3620

Enabling OpenTelemetry cost up to 26% of throughput on small-document tasks, almost all of it in driver code that ran before the sampling decision was known. This PR removes that cost and adds benchmarking to catch this class of regression.

Tracing hot path:

  • Command and operation spans are created with only the cheap, sampler-visible attributes. Expensive attributes (query summary/text, lsid, cursor id, txn number) are set after creation, only when the span is recording.
  • Span work is skipped entirely for spans with invalid contexts (no trace identity, e.g. no SDK installed). Valid-but-unsampled spans keep the full path, so context propagation is unaffected. This is a state check on the created span, not the prohibited SDK detection: a custom API-only provider returning valid spans gets the full tracing path.
  • Invariant values are computed once and reused: per-connection attributes (frozen), operation names per operation class, the lsid UUID per session (bounded cache), transaction map keys per transaction. The command document and name are extracted once per command.

Benchmarking:

  • DriverBench can now run under named driver configurations (off, api-only, sdk-never, sdk-parent-1pct, sdk-always). rake driver_bench:compare runs them interleaved, one configuration per process, and records each configuration's overhead against the untraced baseline as its own metric.
  • New daily Evergreen task driver-bench-otel runs the comparison on a bounded set of high-signal tasks.
  • OTel spec tests now run only on the master waterfall, not on PRs.

Measured on Evergreen (overhead vs off, before -> after this PR):

Task api-only sdk-never sdk-always
Small doc insertOne -17.1% -> -4.4% -21.7% -> -8.3% -25.7% -> -14.9%
Find one by ID -8.7% -> -3.7% -13.0% -> -7.7% -19.8% -> -12.0%

Spec change documenting the guidelines and benchmark configurations: mongodb/specifications#1986

Testing: tracer unit specs, OTel unified spec tests against a local replica set (OTEL_SPEC_TESTS=1), a local allocation mini-bench (traced command allocations fell from ~+115/op to ~+10/op in the non-recording configurations), and two Evergreen comparison runs.

DriverBench only ever measured the default configuration, so no
automated benchmark executed the OpenTelemetry code path at all and a
large regression in it went unnoticed. Make the driver configuration an
axis of the benchmark instead of a property of the environment.

A result is now identified by the pair (task, configuration), so every
configuration becomes its own time series that can be watched for
regressions independently. Comparison runs the tasks under each
configuration and records the throughput given up relative to the
baseline as a metric of its own, rather than leaving it to be derived by
comparing two time series later: recording it directly makes it
watchable, and computing it within a run cancels host-to-host variation.

Configurations are measured one per subprocess, because OpenTelemetry
cannot be reconfigured once its SDK has been installed and the
"api-only" configuration requires that the SDK was never loaded. They
are run interleaved rather than one to completion and then the next, so
that a machine which slows down partway through a run does not put one
configuration's samples in the fast half and another's in the slow half
and report the difference as overhead.

The SDK configurations install no span processor. The cost to attribute
to the driver is that of creating and recording spans; a processor would
additionally charge the SDK's SpanData conversion, background thread and
queue to the driver's account, and add variance that hides small
driver-side changes. Sampled spans are still fully recorded without one.

Task filters are included because the full suite spends at least a
minute per micro-benchmark per configuration, which is too slow for an
optimize-and-remeasure loop. Composites are skipped when the task list
is filtered, since they average a fixed list of micro-benchmarks.
The comparison is too long to run on a laptop: it runs the tasks once per
configuration, several times over, and each micro-benchmark has a 60 second
floor. It also has to run undisturbed, since the quantity being measured is a
difference between configurations and a machine that sleeps or throttles
partway through reports that as overhead.

Runs a bounded set of micro-benchmarks rather than the whole suite so that
the task fits in a sensible wall clock: the four high-signal single- and
multi-doc tasks, which create one operation span and one command span per
operation and so show the per-span cost most clearly. Parallel tasks are
excluded as disk- and concurrency-bound, and BSON tasks are excluded by
Comparison itself since they never reach a server and create no spans. All
three of the task list, the configuration list and the repetition count are
expansions, so a patch can widen them without a config change.

The results are uploaded as a task artifact rather than sent to the
performance store. What is wanted here is a baseline to compare later runs
against by hand, not a trend series.

The task needs its own exec_timeout_secs; the global 5400 is not enough.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant