如何从外部访问docker中的JMX接口?

51

我正在尝试远程监控在docker中运行的JVM。配置如下:

  • 机器1:在Ubuntu机器上以docker方式运行JVM(例如,运行kafka);该机器的IP地址为10.0.1.201;在docker中运行的应用程序位于172.17.0.85。

  • 机器2:运行JMX监视器。

请注意,当我从机器2运行JMX监视器时,会出现以下错误(请注意:当我运行jconsole、jvisualvm、jmxtrans和node-jmx/npm:jmx时都会出现相同的错误):

每个JMX监视工具失败时的堆栈跟踪如下:

java.rmi.ConnectException: Connection refused to host: 172.17.0.85; nested exception is
    java.net.ConnectException: Operation timed out
    at sun.rmi.transport.tcp.TCPEndpoint.newSocket(TCPEndpoint.java:619)
    (followed by a large stack trace)

现在有趣的部分是,当我在运行docker的同一台机器上(来自上面的machine 1)运行相同的工具(jconsole、jvisualvm、jmxtrans和node-jmx/npm:jmx)时,JMX监视正常工作。

我认为这表明我的JMX端口是活动的并且正常工作,但是当我从远程执行JMX监视(来自机器2)时,看起来JMX工具无法识别内部docker IP(172.17.0.85)

下面是在机器1上与JMX监视工作相关的(我认为)网络配置元素(注意docker ip,172.17.42.1):

docker0   Link encap:Ethernet  HWaddr ...
      inet addr:172.17.42.1  Bcast:0.0.0.0  Mask:255.255.0.0
      inet6 addr:... Scope:Link
      UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
      RX packets:6787941 errors:0 dropped:0 overruns:0 frame:0
      TX packets:4875190 errors:0 dropped:0 overruns:0 carrier:0
      collisions:0 txqueuelen:0
      RX bytes:1907319636 (1.9 GB)  TX bytes:639691630 (639.6 MB)

wlan0     Link encap:Ethernet  HWaddr ... 
      inet addr:10.0.1.201  Bcast:10.0.1.255  Mask:255.255.255.0
      inet6 addr:... Scope:Link
      UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
      RX packets:4054252 errors:0 dropped:66 overruns:0 frame:0
      TX packets:2447230 errors:0 dropped:0 overruns:0 carrier:0
      collisions:0 txqueuelen:1000
      RX bytes:2421399498 (2.4 GB)  TX bytes:1672522315 (1.6 GB)

这是远程机器 (机器2) 上相关的网络配置元素,我从中获取JMX错误:

lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
    options=3<RXCSUM,TXCSUM>
    inet6 ::1 prefixlen 128 
    inet 127.0.0.1 netmask 0xff000000 
    inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 
    nd6 options=1<PERFORMNUD>

en1: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    ether .... 
    inet6 ....%en1 prefixlen 64 scopeid 0x5 
    inet 10.0.1.203 netmask 0xffffff00 broadcast 10.0.1.255
    nd6 options=1<PERFORMNUD>
    media: autoselect
    status: active

我创建了一个 GitHub 项目,其中包含一个从 Docker 容器中准备好的 JMX 实现。它包含一个带有适当 entrypoint.shDockerfile,以及一个用于轻松部署的 docker-compose.yml - cstroe
6个回答

67

为了完整起见,以下解决方案可行。应使用特定参数运行JVM,以启用远程Docker JMX监视,参数如下:

-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.port=<PORT>
-Dcom.sun.management.jmxremote.rmi.port=<PORT>
-Djava.rmi.server.hostname=<IP>

where:

<IP> is the IP address of the host that where you executed 'docker run'
<PORT> is the port that must be published from docker where the JVM's JMX port is configured (docker run --publish 7203:7203, for example where PORT is 7203). Both `port` and `rmi.port` can be the same. 

完成后,您应该能够在本地或远程计算机上执行JMX监视(jmxtrans、node-jmx、jconsole等)。

感谢@Chris-Heald使这成为一个非常快速和简单的修复!


7
这个方法有效,但是要连接 Docker 容器,我还需要添加以下内容:-Dcom.sun.management.jmxremote.port=1098并连接到该端口而不是 rmi.port - jaguililla
1
"-Djava.rmi.server.hostname=<IP>" 对我来说是重要的部分。谢谢! - m.kocikowski
3
如果我不想在Dockerfile中指定一个固定的IP地址,你有没有任何最佳实践方法来实现这一点?问题是,我正在使用一个镜像来启动应用程序,该镜像使用“CMD java -jar …”命令,而我不能依赖于主机的固定IP地址。 - DoNuT
3
非常重要的一点是,无论何时使用SSH端口转发,客户端(例如VisualVM)连接时必须使用传递给-Dcom.sun.management.jmxremote.rmi.port的“相同端口”。例如,如果您将-Dcom.sun.management.jmxremote.rmi.port=8888设置为docker run -p25000:8888 ...,然后通过端口转发SSH ssh -L 8888:localhost:25000 your-docker-host.com进行连接,那么您可以使用localhost:8888连接VisualVM。请注意,不管使用哪种端口转发,都必须使用传递给-Dcom.sun.management.jmxremote.rmi.port的端口号才能成功连接。 - Mihai Todor
@DoNuT 如果你使用 -Djava.rmi.server.hostname=0.0.0.0,你就不需要指定你的主机IP。 - undefined
显示剩余2条评论

18

对于开发环境,您可以将java.rmi.server.hostname设置为万能IP地址0.0.0.0

示例:

 -Djava.rmi.server.hostname=0.0.0.0 \
                -Dcom.sun.management.jmxremote \
                -Dcom.sun.management.jmxremote.port=${JMX_PORT} \
                -Dcom.sun.management.jmxremote.rmi.port=${JMX_PORT} \
                -Dcom.sun.management.jmxremote.local.only=false \
                -Dcom.sun.management.jmxremote.authenticate=false \
                -Dcom.sun.management.jmxremote.ssl=false

1
不支持在 Docker for Windows + kind + kubectl port-forward 环境下运行。 - Robin Green
@RobinGreen 应该可以工作。你设置了什么作为 JMX_PORT - OneCricketeer

9

我发现通过RMI设置JMX很痛苦,尤其是由于需要在启动时指定-Djava.rmi.server.hostname=<IP>。我们正在Kubernetes中运行docker镜像,其中一切都是动态的。

最终我使用了JMXMP而不是RMI,因为这只需要一个TCP端口打开,而且不需要指定主机名。

我的当前项目使用Spring,可以通过添加以下内容进行配置:

<bean id="serverConnector"
    class="org.springframework.jmx.support.ConnectorServerFactoryBean"/>

如果你在Spring之外工作,你需要设置自己的JMXConncetorServer以使其正常工作。

除了这个依赖项(因为JMXMP是可选扩展而不是JDK的一部分):

<dependency>
    <groupId>org.glassfish.main.external</groupId>
    <artifactId>jmxremote_optional-repackaged</artifactId>
    <version>4.1.1</version>
</dependency>

当您启动JVisualVM以通过JMXMP进行连接时,您需要将相同的jar文件添加到类路径中:

jvisualvm -cp "$JAVA_HOME/lib/tools.jar:<your_path>/jmxremote_optional-repackaged-4.1.1.jar"

然后使用以下连接字符串进行连接:

service:jmx:jmxmp://<url:port>

(默认端口为9875)

8

经过一番搜索,我找到了这个配置

-Dcom.sun.management.jmxremote.ssl=false 
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.port=1098
-Dcom.sun.management.jmxremote.rmi.port=1098
-Djava.rmi.server.hostname=localhost
-Dcom.sun.management.jmxremote.local.only=false

与上述其他内容不同的是,java.rmi.server.hostname被设置为localhost而不是0.0.0.0

1
这对我来说是正确的解决方案。如果您使用SSH端口转发,请在本地机器上也使用相同的1098端口。连接字符串为service:jmx:rmi:/// jndi / rmi:// localhost:1098 / jmxrmi - jausen brett

2
为了提供更多的见解,我使用了一些Docker端口映射,之前的答案都不能直接为我所用。经过调查,我在这里找到了答案:如何在Docker机器中从主机连接到Docker容器中的JMX? 提供了必要的见解。
这是我认为发生的事情:
我按照其他答案中建议的设置了JMX:
-Dcom.sun.management.jmxremote.ssl=false 
-Dcom.sun.management.jmxremote.authenticate=false 
-Dcom.sun.management.jmxremote.port=1098
-Dcom.sun.management.jmxremote.rmi.port=1098
-Djava.rmi.server.hostname=localhost
-Dcom.sun.management.jmxremote.local.only=false

程序流程:
  • 我运行Docker容器并将端口从主机映射到容器中。比如我将主机的1099端口映射到Docker容器的1098端口。
  • 我在Docker容器内运行JVM,并使用上述JMX设置。
  • Docker容器内的JMX代理现在监听给定的1098端口。
  • 我在主机上(Docker外部)使用URL localhost:1099启动JConsole。我使用1099,因为我使用了主机:Docker端口映射的1099:1098。
  • JConsole成功连接到Docker内部的JMX代理。
  • JConsole询问JMX从哪里读取监控数据。
  • JMX代理以配置的信息和地址回应:localhost:1098
  • JConsole现在试图连接到给定的地址localhost:1098
  • 这会失败,因为本地主机上的1098端口(Docker外部)没有在监听。端口1099被映射到Docker:1098。JMX应该告诉JConsole从localhost:1099读取监控信息,因为1099是从主机映射到Docker容器内的1098端口。

作为解决方法,我将我的host:docker端口映射从1099:1098更改为1098:1098。现在,JMX仍然告诉JConsole连接到localhost:1098以获取监控信息。但现在它可以工作了,因为外部端口与Docker内部JMX所宣传的端口相同。

我认为对于SSH隧道和类似的场景也适用。您必须匹配您配置JMX进行广告的内容,并且JConsole看到的是您运行它的主机上的地址空间。

也许可以通过玩弄jmxremote.portjmxremove.rmi.porthostname属性来使用不同的端口映射使其工作。但我有机会使用相同的端口,因此使用它们简化了操作,这对我有效。


0

云服务解决方案,例如 AWS ECS

主要挑战在于 JMX/RMI 协议要求服务器(您的 JVM 应用程序)和客户端(例如 VisualVM)之间的主机和端口相对应。换句话说,如果这些参数中的任何一个不匹配,那么就无法建立连接。

因此,在容器化应用程序的情况下,这意味着 JMX/RMI 配置需要为 JVM 应用程序预定义/静态端口,并且该端口应在容器外部映射到容器内部的等效端口。这是使其正常工作的唯一方法。

现在,我想回答的主要问题是如何连接到在私有网络后运行并仅由云管理的动态端口公开的云中的 JVM 应用程序。

解决方案已经存在!并且需要一些基础设施巧妙的方法。让我们来看看图表。

enter image description here

  • 基本上,我们想要先运行我们的JMX路由器容器作为我们服务的一部分。该容器的目的是将传入的流量重定向到我们将用于JMX/RMI连接的JVM端口。
  • 我们将用于JMX的端口将是映射到JMX路由器容器静态入站端口的动态端口。
  • 一旦我们获得了动态端口(路由器容器已启动)- 我们将使用它来启动我们的JVM应用程序。

为了构建我们的JMX路由器,我们将使用HAproxy。为了构建镜像,我们需要Dockerfile

FROM haproxy:latest


USER root

RUN apt update && apt -y install curl jq

COPY ./haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]

entrypoint.sh 文件中:

#!/bin/bash
set -x

port=$(curl -s ${ECS_CONTAINER_METADATA_URI_V4}/task | jq '.Containers | .[] | select(.Name=="haproxy-jmx") | .Ports | .[] | select(.ContainerPort==9090) | select(.HostIp=="0.0.0.0") | .HostPort')

while [ -z "$port" ]; do
    echo "Empty response, waiting 1 second and trying again..."
    sleep 1

    port=$(curl -s ${ECS_CONTAINER_METADATA_URI_V4}/task | jq '.Containers | .[] | select(.Name=="haproxy-jmx") | .Ports | .[] | select(.ContainerPort==9090) | select(.HostIp=="0.0.0.0") | .HostPort')
done

echo "Received port: $port"

sed -i "s/\$ECS_HOST_PORT/$port/" /usr/local/etc/haproxy/haproxy.cfg

haproxy -f /usr/local/etc/haproxy/haproxy.cfg

使用 haproxy.cfg 文件:

defaults
    mode tcp

frontend service-jmx
    bind :9090
    default_backend service-jmx

backend service-jmx
    server jmx app:$ECS_HOST_PORT

当我们的JMX路由器镜像准备好(发布到我们的注册表)后,我们可以将其用作任务定义中的一个容器定义,例如:

{
      "name": "haproxy-jmx",
      "image": "{IMAGE_SOURCE_FROM_YOUR_REGISTRY}",
      "logConfiguration": {
        "logDriver": "json-file",
        "secretOptions": null,
        "options": {
          "max-size": "50m",
          "max-file": "1"
        }
      },
      "portMappings": [
        {
          "hostPort": 0,
          "protocol": "tcp",
          "containerPort": 9090
        }
      ],
      "cpu": 0,
      "memoryReservation": 32,

      "links": [
        "${name}:app"
      ]
    }

在这里,我们定义了 JMX 静态端口为 9090。你可以选择任何允许使用的端口。但是,在你选择之后,我们将使用该端口来查找 ECS 在启动我们的 JVM 应用程序时映射到它的动态端口。

因此,现在,唯一剩下的就是获取分配给我们的 JMX 路由器的动态端口,并将其用作我们的 JVM 应用程序的 RMI 端口。为此,在我们的 JVM 应用程序镜像的 entrypoint.sh 中,我们有以下内容:

#!/usr/bin/env sh

# We set here our initial JVM settings
JAVA_OPTS="-Dserver.port=8080 \
           -Djava.net.preferIPv4Stack=true"

#If we want to enable JMX for the app we will pass JMX_ENABLE env as true
if [ "${JMX_ENABLE}" = "true" ]; then
  
  #we get EC2 instance IP to use as server host
  HOST_SERVER_IP=$(curl -s http://169.254.169.254/latest/meta-data/local-ipv4)

  # Get a dynamic ECS host port by agreed JMX static port
  JMX_PORT=$(curl -s ${ECS_CONTAINER_METADATA_URI_V4}/task | jq '.Containers | .[] | select(.Name=="haproxy-jmx") | .Ports | .[] | select(.ContainerPort==9090) | select(.HostIp=="0.0.0.0") | .HostPort')
  
  #it might take sometime to get the router container started, let's wait a bit if needed
  while [ -z "$JMX_PORT" ]; do
    echo "Empty response, waiting 1 second and trying again..."
    sleep 1

    JMX_PORT=$(curl -s ${ECS_CONTAINER_METADATA_URI_V4}/task | jq '.Containers | .[] | select(.Name=="haproxy-jmx") | .Ports | .[] | select(.ContainerPort==9090) | select(.HostIp=="0.0.0.0") | .HostPort')
  done

  echo "Received port: $JMX_PORT"
  
  #JMX/RMI configuration you've already seen 
  JMX_OPTS="-Dcom.sun.management.jmxremote=true \
            -Dcom.sun.management.jmxremote.local.only=false \
            -Dcom.sun.management.jmxremote.authenticate=false \
            -Dcom.sun.management.jmxremote.ssl=false \
            -Djava.rmi.server.hostname=$HOST_SERVER_IP \
            -Dcom.sun.management.jmxremote.port=$JMX_PORT \
            -Dcom.sun.management.jmxremote.rmi.port=$JMX_PORT \
            -Dspring.jmx.enabled=true"

  JAVA_OPTS="$JAVA_OPTS $JMX_OPTS"
else
  echo "JMX disabled"
fi

#launching our app from working dir
java ${JAVA_OPTS} -jar /opt/workdir/*.jar

现在,只要两个容器都已启动并运行 - 使用HOST_SERVER_IPJMX_PORT连接到ECS集群内的JVM应用程序。

我们已经测试并成功运行。希望对其他人也有所帮助。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接