OTel Collector 到 Jaeger 无数据?解决 connection refused 错误
2025-04-03 06:48:34
解决 OpenTelemetry Collector 无法导出数据到 Jaeger UI 的问题
咱们在搭建分布式追踪系统的时候,OpenTelemetry (OTel) Collector 配上 Jaeger 是个挺常见的组合。但有时候,一顿操作猛如虎,配置跑起来了,服务看起来也都正常,可就是 Jaeger UI 里啥追踪数据都看不到。这感觉就像快递到了小区门口,就是送不到你手上,挺急人的。
最近就遇到了这么个情况:用 Docker Compose 部署了一套包含应用(demo
)、PostgreSQL、Jaeger 和 OTel Collector 的环境。服务都启动成功了,但是 demo
应用产生的追踪数据就是到不了 Jaeger。更让人头疼的是,OTel Collector 日志里还报了个错:
warn [exporterhelper/retry_sender.go:155] Exporting failed. Will retry the request after interval. {"kind": "exporter", "data_type": "traces", "name": "otlp", "error": "failed to push trace data via OTLP exporter: rpc error: code = Unavailable desc = connection error: desc = \"transport: Error while dialing: dial tcp 172.18.0.4:4317: connect: connection refused\"", "interval": "5.181844873s"}
# 或者类似的 gRPC 错误
warn [[email protected]](/cdn-cgi/l/email-protection)/clientconn.go:1381 [core] [Channel #1 SubChannel #2]grpc: addrConn.createTransport failed to connect to {Addr: "otel-collector:4317", ServerName: "otel-collector:4317", }. Err: connection error: desc = "transport: Error while dialing: dial tcp 172.18.0.4:4317: connect: connection refused" {"grpc_log": true}
这个 connection refused
错误是关键线索,说明 OTel Collector 在尝试导出数据时,连接某个目标地址的 4317 端口失败了。看看这个目标地址 otel-collector:4317
,嗯?怎么是 Collector 自己?
咱们来看看这背后的原因,顺便把问题给解决了。
问题根源分析
仔细检查一下提供的 docker-compose.yml
和 otel-collector-config.yaml
文件,就能发现几个配置上的问题点。
1. OTel Collector 导出器 (Exporter) 配置错误:
这是导致日志中那个 connection refused
错误的最直接原因。看看 otel-collector-config.yaml
里的 exporters
部分:
exporters:
debug: {}
otlphttp:
endpoint: "http://jaeger:16686" # 问题点 1: 端口错误
otlp:
endpoint: "http://otel-collector:4317" # 问题点 2: 目标错误
otlphttp
导出器: 它被配置用来将数据通过 OTLP/HTTP 协议发送。目标地址是http://jaeger:16686
。但是,16686
端口通常是 Jaeger UI 和 API 查询的端口,并不是用来接收 OTLP 数据的。Jaegerall-in-one
镜像默认接收 OTLP/HTTP 数据是在4318
端口。otlp
导出器: 这个配置更离谱。它被配置用来将数据通过 OTLP/gRPC 协议发送(因为 OTLP gRPC 默认端口是 4317,虽然这里写了http://
,但 OTel Collector 的otlp
exporter 通常指 gRPC)。它的目标地址竟然是http://otel-collector:4317
,也就是 Collector 自己 !这就形成了一个奇怪的循环:Collector 收到数据,然后尝试把数据再发给自己监听的端口。这就直接导致了那个恼人的connection refused
错误,因为 Collector 启动时,它的 gRPC 服务端口可能还没完全准备好接收外部连接,或者这种自我连接本身就存在问题。这个 exporter 根本就不应该指向自己。
2. service.pipelines.traces
配置引用了错误的 Exporter:
再看看 service
部分:
service:
pipelines:
traces:
receivers: [otlp]
exporters: [debug, otlphttp, otlp] # 引用了上面两个有问题的 exporter
# ... metrics pipeline ...
追踪数据的管道 (traces
) 同时使用了 debug
、otlphttp
和 otlp
这三个导出器。因为 otlphttp
和 otlp
的配置都有问题,数据自然就没办法正确地发送到 Jaeger。debug
导出器会把数据打印到 Collector 的日志里,这倒是可以用来调试,确认数据是否到达了 Collector。
3. 应用程序 (demo service) 的 OTel 配置可能不明确:
看看 docker-compose.yml
中 demo
服务的环境变量:
environment:
# ...
- OTEL_TRACES_EXPORTER=jaeger # 问题点 3: 可能与 Collector 模式冲突
- OTEL_EXPORTER_JAEGER_ENDPOINT=http://jaeger:16686 # 问题点 4: 端口/协议不匹配
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 # 这个看起来是给 OTLP 用的
# ...
OTEL_TRACES_EXPORTER=jaeger
: 这个变量告诉应用的 OTel SDK 直接使用 Jaeger exporter,尝试将数据直接发给 Jaeger,而不是发给 OTel Collector。这与我们部署 OTel Collector 的初衷相悖。通常,用了 Collector 之后,应用应该配置成把数据发给 Collector。OTEL_EXPORTER_JAEGER_ENDPOINT=http://jaeger:16686
: 这个是配合OTEL_TRACES_EXPORTER=jaeger
使用的。但同样,16686
是 Jaeger UI 端口。如果直接用 Jaeger exporter,通常需要配置成 Jaeger 的收集器端口,比如 gRPC 的14250
或者 HTTP Thrift 的14268
。这个配置在这里也是不合适的。OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
: 这个变量指定了 OTLP 导出器的目标地址。如果OTEL_TRACES_EXPORTER
被设置为otlp
,那么应用就会把数据发送到otel-collector
的4317
端口(OTLP gRPC)。这个目标地址本身是正确的,但需要配合正确的OTEL_TRACES_EXPORTER
设置。
综合来看,核心问题在于 OTel Collector 的导出配置完全错误,导致它无法将接收到的数据转发给 Jaeger,甚至在尝试发给自己时出错。同时,应用程序的配置也需要调整,确保它将数据发送给 Collector 而不是直接发送给 Jaeger UI 端口。
解决方案
针对上面分析出来的问题,咱们一步步来修复配置。
方案一:修正 OTel Collector 配置 (otel-collector-config.yaml
)
这是最关键的一步,必须保证 Collector 能正确地把数据导出到 Jaeger。
原理:
修改 exporters
部分,让导出器指向 Jaeger 正确的 OTLP 接收端口。Jaeger all-in-one
镜像默认在 4317
端口监听 OTLP/gRPC,在 4318
端口监听 OTLP/HTTP。我们选择其中一种协议导出即可(推荐 OTLP)。同时,移除那个指向 Collector 自身的错误 exporter 配置。
操作步骤:
-
打开你的
otel-collector-config.yaml
文件。 -
修改
exporters
部分,指向 Jaeger 的 OTLP 端口。这里我们用 OTLP/gRPC (4317
) 作为例子。如果你更倾向于 OTLP/HTTP,可以用4318
。我们只需要一个指向 Jaeger 的 OTLP exporter 就够了,可以去掉原来的otlphttp
和otlp
,或者修改其中一个。这里我们修改otlp
指向 Jaeger,并移除otlphttp
。exporters: # debug exporter 用于在 collector 日志中查看数据,方便调试,建议保留 debug: verbosity: detailed # 可以设为 detailed 看到更详细的数据 # otlp exporter 用于将数据发送给 Jaeger # 注意:endpoint 不再是 http:// 开头,因为 OTLP gRPC 不通过 HTTP # 如果 Jaeger 服务名是 jaeger,端口是 4317 otlp: endpoint: "jaeger:4317" # OTLP gRPC 默认是安全的 (secure), 但在 Docker 内部网络,通常不需要 TLS # 需要明确禁用 TLS,否则可能连接失败 tls: insecure: true # (可选) 如果想用 OTLP/HTTP 协议,可以这样配置: # otlphttp: # endpoint: "http://jaeger:4318" # # OTLP HTTP 可以指定 headers, tls 等,根据需要配置
-
修改
service.pipelines.traces
部分,确保exporters
列表里包含我们修正后的、指向 Jaeger 的 exporter (比如otlp
),并移除原来错误的 exporter (如果之前选了移除)。service: pipelines: traces: receivers: [otlp] # 确保这里引用了指向 Jaeger 的 exporter,例如 'otlp' # 移除了之前错误的 otlphttp 和 指向自己的 otlp (如果修改了 otlp, 这里保持 otlp 即可) exporters: [debug, otlp] # 只保留 debug 和 指向 Jaeger 的 otlp exporter metrics: # Metrics 配置类似,如果也想发给某个地方,需正确配置 exporter receivers: [otlp] exporters: [debug] # 假设 metrics 暂时只输出到 debug 日志
修改后的 otel-collector-config.yaml
(仅含相关部分):
receivers:
otlp:
protocols:
http: # 监听 4318 端口
grpc: # 监听 4317 端口
exporters:
debug:
verbosity: detailed # 日志更详细方便排错
otlp: # 这个 exporter 将数据发送给 Jaeger
endpoint: "jaeger:4317" # Jaeger 的 OTLP gRPC 端口
tls:
insecure: true # Docker 内部网络,禁用 TLS
service:
pipelines:
traces:
receivers: [otlp]
# 使用 debug 和 指向 Jaeger 的 otlp exporter
exporters: [debug, otlp]
metrics: # 如果不需要 metrics 可以整个注释掉
receivers: [otlp]
exporters: [debug] # metrics 只输出到日志
安全建议:
- 这里的
tls.insecure: true
是因为在 Docker Compose 内部网络通信,通常认为是安全的,不需要加密。如果你的 Collector 和 Jaeger 部署在不同的网络环境或者公网上,强烈建议 配置 TLS 进行加密通信,需要设置证书等。
进阶技巧:
- 你可以使用环境变量替换 Collector 配置中的值,增加灵活性。比如,将
endpoint: "jaeger:4317"
改为endpoint: "${JAEGER_OTLP_GRPC_ENDPOINT}"
,然后在docker-compose.yml
的otel-collector
服务里定义这个环境变量。 - Jaeger 也支持其他协议的接收器,如
jaeger
(Thrift over UDP/HTTP) 和zipkin
。如果你需要兼容这些,可以在 Collector 中配置相应的 exporter (jaeger
,zipkin
) 指向 Jaeger 的对应端口 (例如jaeger-thrift
exporter 指向jaeger:14268
)。但 OTLP 是 OpenTelemetry 的原生协议,推荐优先使用。
方案二:修正应用程序配置 (docker-compose.yml
)
现在 Collector 配置好了,还需要确保 demo
应用程序把数据正确地发送给 Collector。
原理:
修改 demo
服务的环境变量,告诉应用的 OTel SDK 使用 OTLP exporter,并将数据发送到 OTel Collector 监听的 OTLP 端口(4317
for gRPC 或 4318
for HTTP)。
操作步骤:
- 打开
docker-compose.yml
文件。 - 找到
demo
服务下的environment
部分。 - 修改 OTel 相关环境变量:
- 将
OTEL_TRACES_EXPORTER
的值从jaeger
改为otlp
。 - 确保
OTEL_EXPORTER_OTLP_ENDPOINT
指向 Collector 的 OTLP 地址。通常 OTel SDK 会根据 endpoint 的 scheme (http://
或https://
) 以及是否包含端口来推断协议和默认端口。指向 Collector 的4317
端口(默认 OTLP gRPC)是常见的做法。原始配置http://otel-collector:4317
有点混淆(http scheme + gRPC port),但很多 SDK 也能处理。更标准的 OTLP gRPC endpoint 写法是不带 scheme 或者明确 gRPC 相关设置。为了清晰,我们保持原始写法,因为 Collector 确实在 4317 监听 gRPC。或者,如果想用 OTLP/HTTP,可以指向http://otel-collector:4318
。这里我们继续使用4317
(gRPC)。 - 可以移除或注释掉不再使用的
OTEL_EXPORTER_JAEGER_ENDPOINT
变量,避免混淆。
- 将
修改后的 docker-compose.yml
(仅 demo
服务相关部分):
version: '3.8'
services:
demo:
container_name: demo
build:
dockerfile: Dockerfile
environment:
- OTEL_SERVICE_NAME=demo
- OTEL_METRICS_EXPORTER=none # 保持不变,或配置为 otlp 发给 collector
- OTEL_TRACES_EXPORTER=otlp # <-- 改为 'otlp'
# - OTEL_EXPORTER_JAEGER_ENDPOINT=http://jaeger:16686 # <-- 注释掉或移除
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317 # <-- 确认指向 collector 的 OTLP gRPC 端口
# ... 其他环境变量保持不变 ...
- SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/postgres
# ...
ports:
- "8080:8080"
depends_on:
- postgres
- otel-collector # 确保 collector 先启动
# ... postgres, jaeger, otel-collector 服务定义保持不变 ...
postgres:
# ...
jaeger:
# ...
otel-collector:
# ... (确保使用了修正后的 config 文件)
进阶技巧:
- 协议选择: OTLP 支持 gRPC 和 HTTP/protobuf 两种传输方式。gRPC 通常性能更好,而 HTTP 更易于通过防火墙和代理。你可以根据需要,通过设置
OTEL_EXPORTER_OTLP_PROTOCOL=grpc
或http/protobuf
(环境变量名可能因 SDK 不同略有差异)以及对应的OTEL_EXPORTER_OTLP_ENDPOINT
(如http://otel-collector:4318
for HTTP) 来选择。 - Endpoint 细节:
OTEL_EXPORTER_OTLP_ENDPOINT
的具体格式和行为可能受到你使用的 OTel SDK (Java, Python, Go 等) 的影响。查阅对应 SDK 的文档可以获取最准确的配置方式。有些 SDK 可能需要区分 gRPC 和 HTTP 的 endpoint 变量名,例如OTEL_EXPORTER_OTLP_TRACES_ENDPOINT
。不过,OTEL_EXPORTER_OTLP_ENDPOINT
是比较通用的。 - Metrics: 如果你也想通过 Collector 收集应用的 metrics,需要将
OTEL_METRICS_EXPORTER
设置为otlp
,并确保 Collector 的metrics
管道配置了正确的 exporter。
方案三:简化流程 - 直接从应用发送到 Jaeger (可选)
如果你的场景比较简单,不需要 OTel Collector 提供的聚合、过滤、转换或者向多个后端导出等高级功能,也可以考虑让应用直接把 OTLP 数据发送给 Jaeger。
原理:
Jaeger all-in-one
本身就能接收 OTLP 数据。所以,可以绕过 Collector,直接在应用中配置 OTLP exporter 指向 Jaeger 的 OTLP 端口。
操作步骤:
-
修改
docker-compose.yml
文件中demo
服务的环境变量:- 设置
OTEL_TRACES_EXPORTER=otlp
。 - 设置
OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317
(gRPC) 或http://jaeger:4318
(HTTP)。这里用 gRPC 举例。 - 移除或注释掉
OTEL_EXPORTER_JAEGER_ENDPOINT
。
environment: - OTEL_SERVICE_NAME=demo - OTEL_METRICS_EXPORTER=none - OTEL_TRACES_EXPORTER=otlp # 使用 OTLP exporter - OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317 # 直接指向 Jaeger 的 OTLP gRPC 端口 # ... 其他环境变量 ...
- 设置
-
在
docker-compose.yml
文件中,你可以直接移除otel-collector
服务及其所有相关配置(包括depends_on
里的otel-collector
)。
注意事项:
- 这种方式更简单直接,适合快速原型或小型项目。
- 但你会失去 OTel Collector 带来的好处,比如:
- 解耦: 应用不需要知道最终的追踪后端是 Jaeger 还是其他系统。
- 可靠性: Collector 可以做缓冲和重试,减少数据丢失风险。
- 数据处理: Collector 可以对数据进行采样、过滤、添加属性等。
- 多后端导出: 可以同时把数据发往 Jaeger、Prometheus、日志系统等。
- Agent 模式: 可以部署 Collector Agent 在应用节点上,减少网络开销和应用配置复杂度。
因此,是否采用这种简化方案,需要根据你的具体需求权衡。对于大多数生产环境或复杂场景,使用 OTel Collector 仍然是推荐的做法。
验证修复
修改完配置后,需要重新构建并启动你的 Docker Compose 环境:
# 停掉并移除旧容器
docker-compose down
# (如果修改了 Dockerfile 或者应用代码) 重新构建镜像
# docker-compose build demo
# 重新启动所有服务
docker-compose up -d
服务启动后:
- 访问你的
demo
应用的某个接口,触发一些操作,产生追踪数据。 - 稍等片刻(数据需要经过应用 -> Collector -> Jaeger 的流程),然后打开 Jaeger UI (浏览器访问
http://localhost:16686
)。 - 在 Jaeger UI 的 "Search" 页面,选择你的服务 (
demo
),点击 "Find Traces"。如果配置正确,你应该能看到生成的追踪数据了。 - (如果还有问题)查看 OTel Collector 的日志:
docker logs otel-collector
。因为我们配置了debug
exporter,应该能看到 Collector 接收到数据并尝试导出的日志。注意看是否有新的错误信息。 - (如果还有问题)查看
demo
应用的日志:docker logs demo
。确认应用是否成功初始化 OTel SDK 并尝试导出数据。
通过以上步骤,通常就能定位并解决 OTel Collector 数据无法导出到 Jaeger 的问题。核心在于仔细检查 Collector 的 exporter 配置,确保目标地址、端口和协议都正确无误,同时也要保证应用端的 OTel 配置是指向 Collector 的(如果使用 Collector 的话)。