Repository navigation
benchmarking: add observability benchmarking for the OTel collector - #749
Conversation
Krisztian F (krisztianfekete)
left a comment
There was a problem hiding this comment.
Added some comments! Do we want to make this repeatable locally (and in CI?) via a script?
Might do it in a follow-up and figure out the best way to trigger it. Running it against each CL might be expensive, can consider running it as a daily job..? Can connect with people who work on benchmarking and CI/CD to discuss about it. |
85fc01e to
0011ab3
Compare
0011ab3 to
b79b2ef
Compare
|
Hi Max, can you please review it? It doesn't generate any report yet, just set up things we need to benchmark observability. Max Smythe (@maxsmythe) |
Max Smythe (maxsmythe)
left a comment
There was a problem hiding this comment.
This looks awesome! Couple questions that may just need clarificatoin.
| `cycles/sec ≈ users / (1.0s + avg_wait_time)`. Change the number of users. Do | ||
| not change the wait time. | ||
|
|
||
| | # | Scenario | Duration | Setup | |
There was a problem hiding this comment.
Note that Python doesn't play well with gRPC when the number of users gets high enough, causing seconds worth of latency and throttling QPS. This is why the switch to boomer occurred.
Sleep currently uses Python worker. Might want to switch to glutton user to avoid this.
There was a problem hiding this comment.
Yeah, I can update it to glutton in a follow-up that adds the ladder to the config.
|
|
||
| ## 2. Open items | ||
|
|
||
| - Add the ladder to [automation/tests.yaml](automation/tests.yaml). Write the |
There was a problem hiding this comment.
The ladder seems like something that locust/runner.py should execute in some way.
The non-linear traffic pattern is something that is ideal to provide as a custom shape (locust/shapes)
Changing trace probability dynamically is an interesting wrinkle.
The Boomer worker does dynamically fetch config from master:
substrate/internal/benchmarking/boomer/dynconfig/dynconfig.go
Lines 89 to 109 in febeb7a
So it is possible. Reloads occur whenever worker receives a spawn event from locust, though that may not always be an intuitive trigger.
There was a problem hiding this comment.
Agree, and this is already the planned follow-up. This PR focus on getting manifest ready.
There was a problem hiding this comment.
Makes sense, should have said this was an informational comment rather than a blocker.
| traces/count: | ||
| receivers: [otlp] | ||
| processors: [transform/svc] | ||
| exporters: [count] | ||
| traces/forward: | ||
| receivers: [otlp] | ||
| exporters: [otlp/managed] | ||
| metrics/count: | ||
| receivers: [otlp] | ||
| processors: [transform/svc] | ||
| exporters: [count] | ||
| metrics/forward: | ||
| receivers: [otlp] | ||
| exporters: [otlp/managed] | ||
| metrics/counts: | ||
| receivers: [count] | ||
| processors: [deltatocumulative] | ||
| exporters: [prometheus] |
There was a problem hiding this comment.
Can we add memory_limiter, and configure GOMEMLIMIT?
There was a problem hiding this comment.
This is just a tee needed by GKE Managed OTel collector.
This tee collector is not needed for kind cluster and it should only process telemetry data and stream it to the GKE Managed collector, which means this is not a "real" collector. I am open to add memory_limiter and configure GOMEMLIMIT but just think it's not necessary.
There was a problem hiding this comment.
I these are still beneficial for any perf/loadtesting setups to ensure reliability, but we can add them as a follow-up.
There was a problem hiding this comment.
I will keep this comment open to allow me track it.
| # long run out of Cloud Monitoring, but it makes the resource values of | ||
| # the managed collector those of an idle collector. | ||
| otlp/managed: | ||
| endpoint: opentelemetry-collector.gke-managed-otel.svc.cluster.local:4317 |
There was a problem hiding this comment.
Same as in deploy.sh, can we make this work with local clusters as well?
There was a problem hiding this comment.
Maybe we should also make this use the centralized OTel config introduced in #746?
There was a problem hiding this comment.
We don't need the meter.yaml for kind cluster because the otel collector is customized. Instead, I think I can have a follow-up to update the manifest of the otel collector deployed to the kind cluster and integrate it with the benchmark.
I can do it in a follow-up.
ea2deda to
8b172ad
Compare
Part of agent-substrate#563. The suite measured how fast substrate is. Nothing measured how much telemetry it produces, or what that telemetry costs the OTel collector. This change adds the measurement, and builds it from the parts that the suite already has. The managed collector sends its self-metrics to Cloud Monitoring only, and gives no data for each source. Thus benchmarking/telemetry/meter.yaml adds a second collector that counts the spans and the datapoints by service.name. The meter is a tee: it counts the telemetry and then sends it on to the managed collector, thus one run gives the volume for each service and the cost of the load together. Contents: - telemetry/meter.yaml and telemetry/README.md: the instrument and the method. Install, measure, read, remove. - hack/install-ate.sh --otlp-endpoint: sends all control plane telemetry to the meter. Each component reads the endpoint from the ate-otel-config ConfigMap through envFrom, and ate-controller copies the value to the ateom worker pods, thus one patch is sufficient for all of them. The override runs after the bundle apply, because the bundle has its own copy of the ConfigMap and would replace an earlier patch with no error message. deploy_locust.sh forwards the flag. - workloads/deploy.sh --otlp-endpoint: the same for actor containers, which substrate configures separately. - monitoring.yaml: scrapes the meter and cAdvisor. - observability.md: the prerequisites and the scenario ladder. - docs/dev/best-practices/otel-collector.md: the volume model and the size guidance for Deployment or DaemonSet. This file already had a shorter form of the same model, which was incorrect after the ParentBased defaults from agent-substrate#711. Correction: the glutton ActorTemplate had no env. Substrate puts no OTLP configuration in an actor container, thus the exporter used the SDK default localhost:4317, no process listens on that address in the sandbox, and substrate discarded each actor span with no error message. The template now sets OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_RESOURCE_ATTRIBUTES. ActorTemplate.spec is immutable, thus the deploy function deletes the templates before it applies them. Findings: - With ParentBased, the caller makes the root decision. Locust always sends a traceparent, thus its --trace-probability is the root rate for benchmark traffic and the default in ateapi does not apply. - The metrics increase with the number of components, not with the load. The spans increase with the rate of the operations. - An idle worker pod sends zero datapoints. Lifecycle traffic starts the telemetry of ateom, not the number of actors. - atelet and ateom-gvisor send no spans of their own. They inherit the decision that ateapi makes. - Actor throughput becomes constant at the limit of the snapshot suspend and restore operations, not at a limit of the telemetry. At that load the collector used a small part of its memory limit but stayed at maxReplicas, thus throughput is the wrong axis for the size calculation. The number of pods in the cluster sets the limit. The guidance is qualitative. It gives the shape of each curve and tells the reader to measure the values for their own cluster. No result tables go in the repository, because a result becomes incorrect after a change to the sample rates or to the instruments. All prose is in Simplified Technical English (ASD-STE100).
8b172ad to
2661df9
Compare
Co-authored-by: Krisztian F <103492698+krisztianfekete@users.noreply.github.com>
The scenario ladder was a table in observability.md that an operator followed by hand. Each step was a separate run, thus each step also carried a deploy, a set of cold caches, and a new baseline for the reader to align. The sample-rate sweep needed a new run for each rate, because the rate reached the boomer worker one time, at its start. Add shapes/ladder_shape.py and put the ladder in tests.yaml. `--ladder users:duration[:trace_probability],...` makes the steps of a sweep one run. The shape holds each step for its duration and then goes to the next one, thus it owns the end of the run and runner.py forwards no `-t` for a ladder. A step that names a probability changes the sample rate as it starts: the value goes to the parsed options of the master, thus a Python user class reads it from the sampler of common.trace and a boomer worker reads it from /boomer-config at the spawn message that the step change makes. A headless master has no Flask app, thus it had no /boomer-config and runner.py gave boomer the values one time through --config-json. That is sufficient for a run that holds one set of values, and not for a ladder. Serve the same payload from a plain HTTP server on the loopback interface when --boomer-config-port is set, which runner.py sets for a ladder along with the same port as boomer's --master-web-port. The four tests run on glutton, and thus on boomer. maxsmythe called this out on agent-substrate#749: Python and gRPC hold each other at a high number of users, so a ladder on SleepUser would put the latency of the load generator in the measurement. The user sweep stops at 15 users, and not at the 30 of the earlier table, because the pool of the run is 10 workers; a step above that measures the queue of the scheduler. The idle floor sends no request on purpose, thus locust writes no measurement row for it and runner.py counted the run as a failure. Add --allow-empty-stats for that one case, rather than accepting an empty result for every test. Follow-up to agent-substrate#749.
The scenario ladder was a table in observability.md that an operator followed by hand. Each step was a separate run, thus each step also carried a deploy, a set of cold caches, and a new baseline for the reader to align. The sample-rate sweep needed a new run for each rate, because the rate reached the boomer worker one time, at its start. Add shapes/ladder_shape.py and put the ladder in tests.yaml. `--ladder users:duration[:trace_probability],...` makes the steps of a sweep one run. The shape holds each step for its duration and then goes to the next one, thus it owns the end of the run and runner.py forwards no `-t` for a ladder. A step that names a probability changes the sample rate as it starts: the value goes to the parsed options of the master, thus a Python user class reads it from the sampler of common.trace and a boomer worker reads it from /boomer-config at the spawn message that the step change makes. A headless master has no Flask app, thus it had no /boomer-config and runner.py gave boomer the values one time through --config-json. That is sufficient for a run that holds one set of values, and not for a ladder. Serve the same payload from a plain HTTP server on the loopback interface when --boomer-config-port is set, which runner.py sets for a ladder along with the same port as boomer's --master-web-port. The four tests run on glutton, and thus on boomer. maxsmythe called this out on agent-substrate#749: Python and gRPC hold each other at a high number of users, so a ladder on SleepUser would put the latency of the load generator in the measurement. The user sweep stops at 15 users, and not at the 30 of the earlier table, because the pool of the run is 10 workers; a step above that measures the queue of the scheduler. The idle floor sends no request on purpose, thus locust writes no measurement row for it and runner.py counted the run as a failure. Add --allow-empty-stats for that one case, rather than accepting an empty result for every test. Follow-up to agent-substrate#749.
Follow-up to #749. One of three; the other two are independent of this one. This closes the review thread that stayed open on that PR: krisztianfekete asked for `memory_limiter` and `GOMEMLIMIT` on the meter, and I kept it open to track.
The scenario ladder was a table in observability.md that an operator followed by hand. Each step was a separate run, thus each step also carried a deploy, a set of cold caches, and a new baseline for the reader to align. The sample-rate sweep needed a new run for each rate, because the rate reached the boomer worker one time, at its start. Add shapes/ladder_shape.py and put the ladder in tests.yaml. `--ladder users:duration[:trace_probability],...` makes the steps of a sweep one run. The shape holds each step for its duration and then goes to the next one, thus it owns the end of the run and runner.py forwards no `-t` for a ladder. A step that names a probability changes the sample rate as it starts: the value goes to the parsed options of the master, thus a Python user class reads it from the sampler of common.trace and a boomer worker reads it from /boomer-config at the spawn message that the step change makes. A headless master has no Flask app, thus it had no /boomer-config and runner.py gave boomer the values one time through --config-json. That is sufficient for a run that holds one set of values, and not for a ladder. Serve the same payload from a plain HTTP server on the loopback interface when --boomer-config-port is set, which runner.py sets for a ladder along with the same port as boomer's --master-web-port. The four tests run on glutton, and thus on boomer. maxsmythe called this out on agent-substrate#749: Python and gRPC hold each other at a high number of users, so a ladder on SleepUser would put the latency of the load generator in the measurement. The user sweep stops at 15 users, and not at the 30 of the earlier table, because the pool of the run is 10 workers; a step above that measures the queue of the scheduler. The idle floor sends no request on purpose, thus locust writes no measurement row for it and runner.py counted the run as a failure. Add --allow-empty-stats for that one case, rather than accepting an empty result for every test. Follow-up to agent-substrate#749.
Follow-up to #749, for comment of adding the ladder design in benchmarking for observability.
The volume measurement worked on GKE only. There the managed collector
cannot hold a connector, thus benchmarking/telemetry/meter.yaml puts a
second collector in front of it and counts the data as it goes through.
On kind the collector is ours, thus it can do the count itself and the
cluster needs no meter and no extra hop.
Add the count connector to the kind collector. It now gives the same two
counts as the meter, substrate_spans_total{service_name} and
substrate_datapoints_total{service_name}, on the Prometheus exporter port
that it already opens. The connector resolves each key from the attributes
of the item, then the scope, then the resource, thus service.name resolves
with no transform in front of it. Drop the transform from the meter also.
Give the collector its otelcol_* counters also. The default reader binds
to localhost, thus nothing outside the pod could read it. Bind it to each
interface and open port 8888. The counters are the positive control of a
measurement: without them a service that reports zero volume and data that
never arrived look the same.
One scrape job reads both ports. The prometheus.io/scrape annotation can
name one port only, and port 8889 re-exports each metric that the pipelines
push. The pods of ateapi, atelet, and atenet-router already carry that
annotation on 9090, thus an annotation on the collector would bring each of
their series in a second time and sum() would give double. The job keeps
the substrate_* and otelcol_* names only.
The actors need no change. benchmarking/workloads/deploy.sh reads the
endpoint from the ate-otel-config ConfigMap, and the kind overlay of that
ConfigMap already names this collector.
Follow-up to agent-substrate#749.
The volume measurement worked on GKE only. There the managed collector
cannot hold a connector, thus benchmarking/telemetry/meter.yaml puts a
second collector in front of it and counts the data as it goes through.
On kind the collector is ours, thus it can do the count itself and the
cluster needs no meter and no extra hop.
Add the count connector to the kind collector. It now gives the same two
counts as the meter, substrate_spans_total{service_name} and
substrate_datapoints_total{service_name}, on the Prometheus exporter port
that it already opens. The connector resolves each key from the attributes
of the item, then the scope, then the resource, thus service.name resolves
with no transform in front of it. Drop the transform from the meter also.
Give the collector its otelcol_* counters also. The default reader binds
to localhost, thus nothing outside the pod could read it. Bind it to each
interface and open port 8888. The counters are the positive control of a
measurement: without them a service that reports zero volume and data that
never arrived look the same.
One scrape job reads both ports. The prometheus.io/scrape annotation can
name one port only, and port 8889 re-exports each metric that the pipelines
push. The pods of ateapi, atelet, and atenet-router already carry that
annotation on 9090, thus an annotation on the collector would bring each of
their series in a second time and sum() would give double. The job keeps
the substrate_* and otelcol_* names only.
The actors need no change. benchmarking/workloads/deploy.sh reads the
endpoint from the ate-otel-config ConfigMap, and the kind overlay of that
ConfigMap already names this collector.
Follow-up to agent-substrate#749.
The volume measurement worked on GKE only. There the managed collector
cannot hold a connector, thus benchmarking/telemetry/meter.yaml puts a
second collector in front of it and counts the data as it goes through.
On kind the collector is ours, thus it can do the count itself and the
cluster needs no meter and no extra hop.
Add the count connector to the kind collector. It now gives the same two
counts as the meter, substrate_spans_total{service_name} and
substrate_datapoints_total{service_name}, on the Prometheus exporter port
that it already opens. The connector resolves each key from the attributes
of the item, then the scope, then the resource, thus service.name resolves
with no transform in front of it. Drop the transform from the meter also.
Give the collector its otelcol_* counters also. The default reader binds
to localhost, thus nothing outside the pod could read it. Bind it to each
interface and open port 8888. The counters are the positive control of a
measurement: without them a service that reports zero volume and data that
never arrived look the same.
One scrape job reads both ports. The prometheus.io/scrape annotation can
name one port only, and port 8889 re-exports each metric that the pipelines
push. The pods of ateapi, atelet, and atenet-router already carry that
annotation on 9090, thus an annotation on the collector would bring each of
their series in a second time and sum() would give double. The job keeps
the substrate_* and otelcol_* names only.
The actors need no change. benchmarking/workloads/deploy.sh reads the
endpoint from the ate-otel-config ConfigMap, and the kind overlay of that
ConfigMap already names this collector.
Follow-up to agent-substrate#749.
The volume measurement worked on GKE only. There the managed collector
cannot hold a connector, thus benchmarking/telemetry/meter.yaml puts a
second collector in front of it and counts the data as it goes through.
On kind the collector is ours, thus it can do the count itself and the
cluster needs no meter and no extra hop.
Add the count connector to the kind collector. It now gives the same two
counts as the meter, substrate_spans_total{service_name} and
substrate_datapoints_total{service_name}, on the Prometheus exporter port
that it already opens. The connector resolves each key from the attributes
of the item, then the scope, then the resource, thus service.name resolves
with no transform in front of it. Drop the transform from the meter also.
Give the collector its otelcol_* counters also. The default reader binds
to localhost, thus nothing outside the pod could read it. Bind it to each
interface and open port 8888. The counters are the positive control of a
measurement: without them a service that reports zero volume and data that
never arrived look the same.
One scrape job reads both ports. The prometheus.io/scrape annotation can
name one port only, and port 8889 re-exports each metric that the pipelines
push. The pods of ateapi, atelet, and atenet-router already carry that
annotation on 9090, thus an annotation on the collector would bring each of
their series in a second time and sum() would give double. The job keeps
the substrate_* and otelcol_* names only.
The actors need no change. benchmarking/workloads/deploy.sh reads the
endpoint from the ate-otel-config ConfigMap, and the kind overlay of that
ConfigMap already names this collector.
Follow-up to agent-substrate#749.
The volume measurement worked on GKE only. There the managed collector
cannot hold a connector, thus benchmarking/telemetry/meter.yaml puts a
second collector in front of it and counts the data as it goes through.
On kind the collector is ours, thus it can do the count itself and the
cluster needs no meter and no extra hop.
Add the count connector to the kind collector. It now gives the same two
counts as the meter, substrate_spans_total{service_name} and
substrate_datapoints_total{service_name}, on the Prometheus exporter port
that it already opens. The connector resolves each key from the attributes
of the item, then the scope, then the resource, thus service.name resolves
with no transform in front of it. Drop the transform from the meter also.
Give the collector its otelcol_* counters also. The default reader binds
to localhost, thus nothing outside the pod could read it. Bind it to each
interface and open port 8888. The counters are the positive control of a
measurement: without them a service that reports zero volume and data that
never arrived look the same.
One scrape job reads both ports. The prometheus.io/scrape annotation can
name one port only, and port 8889 re-exports each metric that the pipelines
push. The pods of ateapi, atelet, and atenet-router already carry that
annotation on 9090, thus an annotation on the collector would bring each of
their series in a second time and sum() would give double. The job keeps
the substrate_* and otelcol_* names only.
The actors need no change. benchmarking/workloads/deploy.sh reads the
endpoint from the ate-otel-config ConfigMap, and the kind overlay of that
ConfigMap already names this collector.
Follow-up to agent-substrate#749.
The volume measurement in current benchmarking repo worked on GKE only. There the managed collector cannot hold a connector, thus benchmarking/telemetry/meter.yaml puts a second collector in front of it and counts the data as it goes through. This PR adds the support to kind cluster to count the telemetry data. Follow-up to #749. > It's a good idea to open an issue first for discussion. - [x] Tests pass - [x] Appropriate changes to documentation are included in the PR
Part of #563
Substrate had no way to measure how much telemetry it sends. This PR adds one, and builds it from the parts the benchmark suite already has.
The managed collector sends its self-metrics to Cloud Monitoring only, and gives no data for each source. Thus you cannot see which component makes the volume. A second collector, the telemetry meter, counts the spans and the datapoints by
service.name.Contents
benchmarking/telemetry/meter.yaml— the meter. A tee: it counts the telemetry by service, then sends it on to the managed collector. Thus one run gives the volume for each service and the cost of the load together.benchmarking/telemetry/README.md— the method: install, measure, read, remove.hack/install-ate.sh --otlp-endpoint— sends all control plane telemetry to the meter. Each component reads the endpoint from theate-otel-configConfigMap throughenvFrom, andate-controllercopies the value to the ateom worker pods, thus one patch is sufficient for all of them.benchmarking/workloads/deploy.sh --otlp-endpoint— the same for actor containers, which substrate configures separately.benchmarking/monitoring.yaml— scrapes the meter and cAdvisor.benchmarking/observability.md— prerequisites and the scenario ladder.docs/dev/best-practices/otel-collector.md— the volume model and the size guidance, corrected for theParentBaseddefaults from Define a production-ready default tracing policy #711.Correction
The
gluttonActorTemplate had noenv. Substrate puts no OTLP configuration in an actor container, thus the exporter used the SDK defaultlocalhost:4317, no process listens on that address in the sandbox, and substrate discarded each actor span with no error message. The template now setsOTEL_EXPORTER_OTLP_ENDPOINTandOTEL_RESOURCE_ATTRIBUTES.ActorTemplate.specis immutable, thusdeploy.sh --deploydeletes the templates before it applies them.How I verified this
On a GKE cluster with managed OTel. The sequence:
Result
The idle floor, from one measurement on 10 workers:
ate-controllerateapiateletatenet-routerateom-gvisorglutton-actorThe
glutton-actorrow is the correction above. Before it, actor telemetry could not arrive at all.Positive control for the same window:
otelcol_receiver_accepted_metric_points10.5/s, and eachotelcol_receiver_refused_*counter at zero. Thus the zero values in the table are true zeros, and not data that did not arrive.Two values differ from the earlier manual measurement.
ate-controlleris now the largest source, andateom-gvisoris no longer zero. The new imagecache counters from #831 are a probable cause. This difference is the reason to keep results out of the repository.Traces
Trace volume follows the root sample decision, not the rate of each component.
ateletandateom-gvisorsent zero spans in each run, because they inherit the decision thatateapimakes and start no trace of their own. At idle each component is near zero: a trace needs an operation, but a metric pushes on a timer.This run gives no span volume for a usual load. The actors crashed on suspend, and the failed retries made an artificial rate of 13.7k spans/s. That rate is not a measurement of substrate. It does show that the meter accepted the spans with no refusals.
Follow-up
A separate PR adds the ladder to
automation/tests.yamland writes the results as artifacts of each run. The open items are at the end ofobservability.md.