我们如何使用 OpenTelemetry 来修复 OpenTelemetry 中的一个 Bug
博客文章在发布后不会更新。这篇文章已经发布一年多了,其内容可能已过时,部分链接可能无效。在依赖任何信息之前,请务必核实。
OpenTelemetry 可以帮助我们快速找到软件问题的根本原因。我们最近遇到一个问题,通过使用 OpenTelemetry 的一个功能来识别另一个功能中的 bug 的根本原因,从而解决了这个问题。
在这篇博文中,我们想与您分享这次有趣的经历。通过它,您将了解到特定语言的实现中微小的差异可能带来有趣的后果,并且您拥有 Java 和 Python 的一个功能,可以帮助您调试上下文传播问题。
问题
描述 bug
为了撰写关于 如何使用 OpenTelemetry instrument NGINX 的博文,我们创建了一个小型示例应用程序,它有一个 Node.js 前端应用程序,该应用程序调用一个 NGINX,NGINX 作为 Python 后端应用程序的反向代理。
我们的目标是创建一个可重用的 docker-compose,它不仅向人们展示如何使用 OpenTelemetry instrument NGINX,还展示跨 Web 服务器的分布式跟踪是什么样的。
虽然 Jaeger 向我们展示了跟踪从前端应用程序流到 NGINX,但 NGINX 和 Python 应用程序之间的连接不可见:我们有两个断开的跟踪。
这出乎意料,因为在之前与 Java 应用程序作为后端的测试中,我们能够看到从 NGINX 到该下游应用程序的跟踪。
复现步骤
遵循有关如何 将 NGINX 放在两个服务之间 的说明。将基于 Java 的应用程序替换为 Python 应用程序,例如,将以下三个文件放入 backend 文件夹中
app.py:import time import redis from flask import Flask app = Flask(__name__) cache = redis.Redis(host='redis', port=6379) def get_hit_count(): retries = 5 while True: try: return cache.incr('hits') except redis.exceptions.ConnectionError as exc: if retries == 0: raise exc retries -= 1 time.sleep(0.5) @app.route('/') def hello(): count = get_hit_count() return 'Hello World! I have been seen {} times.\n'.format(count)Dockerfile:FROM python:3.10-alpine WORKDIR /code ENV FLASK_APP=app.py ENV FLASK_RUN_HOST=0.0.0.0 RUN apk add --no-cache gcc musl-dev linux-headers COPY requirements.txt requirements.txt RUN pip install -r requirements.txt RUN opentelemetry-bootstrap -a install EXPOSE 5000 COPY . . CMD ["opentelemetry-instrument", "--traces_exporter", "otlp_proto_http", "--metrics_exporter", "console", "flask", "run"]requirements.txt:flask redis opentelemetry-distro opentelemetry-exporter-otlp-proto-http
使用以下内容更新 docker-compose.yml
version: '2'
services:
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- '16686:16686'
collector:
image: otel/opentelemetry-collector:latest
command: ['--config=/etc/otel-collector-config.yaml']
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
nginx:
image: nginx-otel
volumes:
- ./opentelemetry_module.conf:/etc/nginx/conf.d/opentelemetry_module.conf
- ./default.conf:/etc/nginx/conf.d/default.conf
backend:
build: ./backend
image: backend-with-otel
environment:
- OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318/v1/traces
- OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
- OTEL_SERVICE_NAME=python-app
redis:
image: 'redis:alpine'
frontend:
build: ./frontend
image: frontend-with-otel
ports:
- '8000:8000'
environment:
- OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318/
- OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
- OTEL_SERVICE_NAME=frontend
运行 docker compose up1 启动该环境,并通过 curl localhost:8000 向前端发送一些请求。
你期望看到什么?
在 localhost:16686 的 Jaeger UI 中,您应该会看到跟踪从 frontend 经过 NGINX 流向 python-app。
你看到了什么?
在 localhost:16686 的 Jaeger UI 中,您会看到两个跟踪,一个从 frontend 流向 NGINX,另一个仅用于 python-app。
解决方案
线索
由于该设置在后端使用 Java 应用程序时可以正常工作,因此我们知道问题是由 Python 应用程序或 NGINX 仪表盘与 Python 应用程序的组合引起的。
我们可以很快排除 Python 应用程序本身有问题:尝试一个简单的 Node.js 应用程序作为后端,我们得到了相同的结果:两个跟踪,一个从前端到 NGINX,另一个仅用于 Node.js 应用程序。
因此,我们知道存在传播问题:跟踪上下文未成功从 NGINX 传输到 Python 和 Node.js 应用程序。
分析
知道问题不会出现在 Java 中,并且很可能是传播中断,我们就知道该怎么做了:我们需要查看跟踪头。
幸运的是,Java 和 Python 的仪表盘有一个功能,可以让我们轻松地捕获 HTTP 请求和响应头 作为 span 属性。
通过环境变量 OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST 和 OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE 提供逗号分隔的 HTTP 头名称列表,我们可以定义要捕获的 HTTP 头。在我们的例子中,我们发送了所有潜在的传播头。
OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=tracestate,traceparent,baggage,X-B3-TraceId
在我们的 docker-compose 示例中,我们只需将其添加到后端服务的定义中即可。
backend:
build: ./backend
image: backend-with-otel
environment:
- OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318/v1/traces
- OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
- OTEL_SERVICE_NAME=python-app
- OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=tracestate,traceparent,baggage,X-B3-TraceId
再次运行 docker compose up1 来启动我们的示例应用程序,并使用 curl localhost:8080 向前端应用程序发送一些请求。
在 Jaeger 中,我们仍然看到跟踪是断开的。但是,当我们查看其中一个跟踪时,可以看到从 NGINX 到后端的已收集请求头。

就在这里!跟踪头 (baggage, traceparent, tracestate) 以多个头字段的形式发送:NGINX 模块一遍又一遍地添加了每个头的值,并且由于多值头受到 RFC7230 的支持,这并没有立即导致问题。
我们测试了从 NGINX 到下游服务的关联能力,该服务使用的是 Java 应用程序。而且,在不深入研究 OTel Java SDK 源代码的情况下,Java 似乎能够灵活地接受具有多个值的 traceparent,尽管此类格式根据 W3C Trace Context 规范是无效的。因此,从 NGINX 到 Java 服务的传播是有效的,而相比之下,Python(以及其他语言)不提供这种灵活性,从 NGINX 到下游服务的传播会静默失败。
请注意,我们并不是说其他语言应该像 Java 那样灵活地读取 traceparent 或反之亦然:bug 存在于 NGINX 模块中,我们需要修复它。
修复
为了解决我们的问题,我们 为 NGINX 的模块添加了一些检查,以确保跟踪头只设置一次。
此修复包含在 otel-webserver-module 的 v1.0.1 版本 中。这意味着您可以更新 Dockerfile 以安装 NGINX 模块,如下所示。
FROM nginx:1.18
ADD https://github.com/open-telemetry/opentelemetry-cpp-contrib/releases/download/webserver%2Fv1.0.1/opentelemetry-webserver-sdk-x64-linux.tgz /opt
RUN cd /opt ; tar xvfz opentelemetry-webserver-sdk-x64-linux.tgz
RUN cd /opt/opentelemetry-webserver-sdk; ./install.sh
ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/opentelemetry-webserver-sdk/sdk_lib/lib
RUN echo "load_module /opt/opentelemetry-webserver-sdk/WebServerModule/Nginx/ngx_http_opentelemetry_module.so;\n$(cat /etc/nginx/nginx.conf)" > /etc/nginx/nginx.conf
COPY default.conf /etc/nginx/conf.d
COPY opentelemetry_module.conf /etc/nginx/conf.d
docker-compose已弃用。有关详细信息,请参阅 迁移到 Compose V2。 ↩︎ ↩︎