|
| 1 | +--- |
| 2 | +title: "Setting up continuous profiling for open-telemetry collector" |
| 3 | +date: 2024-07-30 |
| 4 | +tags: |
| 5 | + - programming |
| 6 | + - golang |
| 7 | + - monitoring |
| 8 | +--- |
| 9 | + |
| 10 | +One of the biggest problem we have found at work is that the speed of opentelemetry (otel) moves way faster than other parts of the infrastructure. |
| 11 | +Namely, the [opentelemtry collector](https://github.com/open-telemetry/opentelemetry-collector) (and the [contrib](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main)) usually has a release every 2 weeks. |
| 12 | +As the opentelemetry collector sits in either the data gathering or transport layer, it is essentially a requirement to keep up with the latest release due to bug fixes and/or performance improvements. |
| 13 | +For bug fixes, it is often easy to test in a non-production environment to verify. |
| 14 | +Performance improvements?? Especially ones on paper which may or may not hold for your particular workload, well, one solution is to do continuous profiling and track the changes in real time. |
| 15 | +This can be done by enabling the [pprof extension](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/extension/pprofextension) in the collector and [pyroscope](https://pyroscope.io/) which pulls the profiles and stores in a backend. |
| 16 | + |
| 17 | +To demonstrate, We make use of the `v1beta1` of the OpenTelemetryCollector CRD which is only available after helm version `0.58.0`. |
| 18 | +Refer to the official [opentelemetry operator helm chart](https://github.com/open-telemetry/opentelemetry-helm-charts/tree/main/charts/opentelemetry-operator) for more details. |
| 19 | + |
| 20 | +We install the base minimum for the operator |
| 21 | + |
| 22 | +```shell |
| 23 | +# assuming we are already in kubernetes and is operating without the namespace otel |
| 24 | +helm install opentelemetry-operator open-telemetry/opentelemetry-operator \ |
| 25 | + --set manager.collectorImage.repository=otel/opentelemetry-collector-k8s \ |
| 26 | + --set admissionWebhooks.certManager.enabled=false \ |
| 27 | + --set admissionWebhooks.autoGenerateCert.enabled=true |
| 28 | +``` |
| 29 | + |
| 30 | +before applying the CRO. Note that we have to specify the port explicitly because the operator is not able to recognize the port and mutate the svc/pod even if defined in the config. |
| 31 | + |
| 32 | +```yaml |
| 33 | +apiVersion: opentelemetry.io/v1beta1 |
| 34 | +kind: OpenTelemetryCollector |
| 35 | +metadata: |
| 36 | + name: pprof |
| 37 | + namespace: otel |
| 38 | +spec: |
| 39 | + config: |
| 40 | + receivers: |
| 41 | + otlp: |
| 42 | + protocols: |
| 43 | + grpc: {} |
| 44 | + http: {} |
| 45 | + exporters: |
| 46 | + debug: {} |
| 47 | + extensions: |
| 48 | + pprof: {} |
| 49 | + service: |
| 50 | + extensions: [pprof] |
| 51 | + pipelines: |
| 52 | + traces: |
| 53 | + receivers: [otlp] |
| 54 | + processors: [] |
| 55 | + exporters: [debug] |
| 56 | + ports: |
| 57 | + - name: pprof |
| 58 | + port: 1777 |
| 59 | +``` |
| 60 | +
|
| 61 | +Now we have a collector running, all we got to do is setup pyroscope to be in ["pull mode"](https://grafana.com/docs/pyroscope/latest/configure-client/grafana-agent/go_pull/). |
| 62 | +The snippets of the pyroscope setup here forms the basis of our standard production upgrade where we begin continuous profiling 30 minutes before a change and ends 30 minutes after the change (be it a success or rollback). |
| 63 | +The general flow is pretty self-explanatory for anyone who is familiar with how Prometheus scrape works, as this is essentially the same but for profiles instead of metrics. |
| 64 | +
|
| 65 | +```river |
| 66 | +// Pod discovery, can also do service discovery. |
| 67 | +discovery.kubernetes "pods" { |
| 68 | + role = "pod" |
| 69 | + namespaces { |
| 70 | + names = ["otel"] |
| 71 | + } |
| 72 | +} |
| 73 | +// Filter and select the correct pods, and do "relabeling" to insert correct metadata into the profiles. |
| 74 | +discovery.relabel "otel" { |
| 75 | + targets = discovery.kubernetes.pods.targets |
| 76 | + rule { |
| 77 | + source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_component"] |
| 78 | + regex = "opentelemetry-collector" |
| 79 | + action = "keep" |
| 80 | + } |
| 81 | + // Since the collector will have many ports open to receive traffic, we should filter on pods that |
| 82 | + // has pprof enabled, which by default is port `1777` in the pprof extension. |
| 83 | + rule { |
| 84 | + source_labels = ["__meta_kubernetes_pod_container_port_number"] |
| 85 | + regex = "1777" |
| 86 | + action = "keep" |
| 87 | + } |
| 88 | + rule { |
| 89 | + target_label = "__port__" |
| 90 | + action = "replace" |
| 91 | + replacement = "1777" |
| 92 | + } |
| 93 | + rule { |
| 94 | + source_labels = ["__address__"] |
| 95 | + target_label = "__address__" |
| 96 | + regex = "(.+):(\\d+)" |
| 97 | + action = "replace" |
| 98 | + replacement = "${1}" |
| 99 | + } |
| 100 | + rule { |
| 101 | + source_labels = ["__address__", "__port__"] |
| 102 | + target_label = "__address__" |
| 103 | + separator = "@" |
| 104 | + regex = "(.+)@(\\d+)" |
| 105 | + replacement = "$1:$2" |
| 106 | + action = "replace" |
| 107 | + } |
| 108 | + // Create standard labels so that is is easier to understand, these are prometheus conventions. |
| 109 | + rule { |
| 110 | + action = "replace" |
| 111 | + source_labels = ["__meta_kubernetes_namespace"] |
| 112 | + target_label = "namespace" |
| 113 | + } |
| 114 | + rule { |
| 115 | + action = "replace" |
| 116 | + source_labels = ["__meta_kubernetes_pod_name"] |
| 117 | + target_label = "pod" |
| 118 | + } |
| 119 | + rule { |
| 120 | + action = "replace" |
| 121 | + source_labels = ["__meta_kubernetes_node_name"] |
| 122 | + target_label = "node" |
| 123 | + } |
| 124 | + rule { |
| 125 | + action = "replace" |
| 126 | + source_labels = ["__meta_kubernetes_pod_container_name"] |
| 127 | + target_label = "container" |
| 128 | + } |
| 129 | + // Both service_name and service_version are opentelemetry conventions. |
| 130 | + rule { |
| 131 | + action = "replace" |
| 132 | + source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_version"] |
| 133 | + target_label = "service_version" |
| 134 | + } |
| 135 | + rule { |
| 136 | + source_labels = ["__meta_kubernetes_namespace", "__meta_kubernetes_pod_label_app_kubernetes_io_name"] |
| 137 | + target_label = "service_name" |
| 138 | + separator = "@" |
| 139 | + regex = "(.*)@(.*)" |
| 140 | + replacement = "${1}/${2}" |
| 141 | + action = "replace" |
| 142 | + } |
| 143 | + // Always good to have some sort of unique identifier to track changes through time. This is the |
| 144 | + // sha of the config which the operator computes for us. |
| 145 | + rule { |
| 146 | + action = "replace" |
| 147 | + source_labels = ["__meta_kubernetes_pod_annotation_opentelemetry_operator_config_sha256"] |
| 148 | + target_label = "config_sha" |
| 149 | + } |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +then we define the backend which we want to ship to |
| 154 | + |
| 155 | +```river |
| 156 | +pyroscope.write "backend" { |
| 157 | + endpoint { |
| 158 | + url = env("PYROSCOPE_URL") |
| 159 | + basic_auth { |
| 160 | + username = env("PYROSCOPE_USERNAME") |
| 161 | + password = env("PYROSCOPE_PASSWORD") |
| 162 | + } |
| 163 | + } |
| 164 | +} |
| 165 | +``` |
| 166 | + |
| 167 | +and finally we define the pipeline which chains all the stages `discovery` -> `relabel` -> `scrape` -> `export`. |
| 168 | + |
| 169 | +```river |
| 170 | +pyroscope.scrape "otel_settings" { |
| 171 | + targets = [discovery.relabel.otel] |
| 172 | + forward_to = [pyroscope.write.backend.receiver] |
| 173 | + profiling_config { |
| 174 | + profile.goroutine { |
| 175 | + enabled = true |
| 176 | + path = "/debug/pprof/goroutine" |
| 177 | + delta = false |
| 178 | + } |
| 179 | + profile.process_cpu { |
| 180 | + enabled = true |
| 181 | + path = "/debug/pprof/profile" |
| 182 | + delta = true |
| 183 | + } |
| 184 | + profile.godeltaprof_memory { |
| 185 | + enabled = false |
| 186 | + path = "/debug/pprof/delta_heap" |
| 187 | + } |
| 188 | + profile.memory { |
| 189 | + enabled = true |
| 190 | + path = "/debug/pprof/heap" |
| 191 | + delta = false |
| 192 | + } |
| 193 | + profile.godeltaprof_mutex { |
| 194 | + enabled = false |
| 195 | + path = "/debug/pprof/delta_mutex" |
| 196 | + } |
| 197 | + profile.mutex { |
| 198 | + enabled = false |
| 199 | + path = "/debug/pprof/mutex" |
| 200 | + delta = false |
| 201 | + } |
| 202 | + profile.godeltaprof_block { |
| 203 | + enabled = false |
| 204 | + path = "/debug/pprof/delta_block" |
| 205 | + } |
| 206 | + profile.block { |
| 207 | + enabled = false |
| 208 | + path = "/debug/pprof/block" |
| 209 | + delta = false |
| 210 | + } |
| 211 | + } |
| 212 | +} |
| 213 | +``` |
| 214 | + |
| 215 | +Applying all of the above during and upgrade to `v0.101.0` of the opentelemetry collector contrib where the loadbalancing exporter gain a massive improvements, straight diff: |
| 216 | + |
| 217 | + |
| 218 | + |
| 219 | +and when focused on the `mergeTrace` function where the upgrade took place |
| 220 | + |
| 221 | + |
| 222 | + |
| 223 | +allowed us to confirm that the performance enhancement in the CHANGELOG matches (and exceeds) our expectation. |
| 224 | +Furthermore, by understanding how the collector behaves, we were able to fine tune the resources and various settings to improve the stability of our system. |
0 commit comments