返回

OTel Collector 到 Jaeger 无数据?解决 connection refused 错误

java

解决 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.ymlotel-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 数据的。Jaeger all-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) 同时使用了 debugotlphttpotlp 这三个导出器。因为 otlphttpotlp 的配置都有问题,数据自然就没办法正确地发送到 Jaeger。debug 导出器会把数据打印到 Collector 的日志里,这倒是可以用来调试,确认数据是否到达了 Collector。

3. 应用程序 (demo service) 的 OTel 配置可能不明确:

看看 docker-compose.ymldemo 服务的环境变量:

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-collector4317 端口(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 配置。

操作步骤:

  1. 打开你的 otel-collector-config.yaml 文件。

  2. 修改 exporters 部分,指向 Jaeger 的 OTLP 端口。这里我们用 OTLP/gRPC (4317) 作为例子。如果你更倾向于 OTLP/HTTP,可以用 4318。我们只需要一个指向 Jaeger 的 OTLP exporter 就够了,可以去掉原来的 otlphttpotlp,或者修改其中一个。这里我们修改 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 等,根据需要配置
    
  3. 修改 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.ymlotel-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)。

操作步骤:

  1. 打开 docker-compose.yml 文件。
  2. 找到 demo 服务下的 environment 部分。
  3. 修改 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=grpchttp/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 端口。

操作步骤:

  1. 修改 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 端口
      # ... 其他环境变量 ...
    
  2. 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

服务启动后:

  1. 访问你的 demo 应用的某个接口,触发一些操作,产生追踪数据。
  2. 稍等片刻(数据需要经过应用 -> Collector -> Jaeger 的流程),然后打开 Jaeger UI (浏览器访问 http://localhost:16686)。
  3. 在 Jaeger UI 的 "Search" 页面,选择你的服务 (demo),点击 "Find Traces"。如果配置正确,你应该能看到生成的追踪数据了。
  4. (如果还有问题)查看 OTel Collector 的日志:docker logs otel-collector。因为我们配置了 debug exporter,应该能看到 Collector 接收到数据并尝试导出的日志。注意看是否有新的错误信息。
  5. (如果还有问题)查看 demo 应用的日志:docker logs demo。确认应用是否成功初始化 OTel SDK 并尝试导出数据。

通过以上步骤,通常就能定位并解决 OTel Collector 数据无法导出到 Jaeger 的问题。核心在于仔细检查 Collector 的 exporter 配置,确保目标地址、端口和协议都正确无误,同时也要保证应用端的 OTel 配置是指向 Collector 的(如果使用 Collector 的话)。