如何使用 OpenTelemetry 修复 OpenTelemetry 中的一个 bug
OpenTelemetry 可以帮助我们快速找到软件中的问题根源。最近我们遇到了一个问题,通过使用 OpenTelemetry 的一个功能,我们成功解决了另一个功能中的 bug。
在本博客文章中,我们想要与您分享这个有趣的经历。通过这个经历,您将了解到语言特定实现中的细微差异可能会产生有趣的影响,以及如何使用 Java 和 Python 的功能来帮助您调试上下文传播问题。
问题
描述该 bug
在博客文章 学习如何使用 OpenTelemetry 为 NGINX 加入监控 中,我们创建了一个小型示例应用程序,其中有一个基于 Node.js 的前端应用程序,调用了充当反向代理的 NGINX,用于后端的 Python 应用程序。
我们的目标是创建一个可重用的 docker-compose
,不仅展示人们如何使用 OpenTelemetry 为 NGINX 加入监控,还展示跨网络服务器的分布式跟踪的情况。
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 up
1 来启动该环境,然后使用 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 请求和响应头作为跟踪的属性。
通过通过环境变量 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 up
1 启动我们的示例应用程序,并使用 curl localhost:8080
向前端应用程序发送一些请求。
在 Jaeger 中,我们仍然看到跟踪是不连续的。然而,在查看其中一条跟踪时,我们可以看到从 NGINX 到后端收集到的请求头:
看到了吧!跟踪头 (baggage
, traceparent
, tracestate
) 作为多个报头字段发送:NGINX 模块多次添加了每个报头的值,因为多值报头由 RFC7230 支持,所以这并没有立即引起问题。
我们通过测试了从 NGINX 到下游服务的关联能力,并且不需要深入研究 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
is deprecated. For details, see Migrate to Compose V2. ↩︎ ↩︎