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

tzxcd3kk  于 2023-03-22  发布在  Docker
关注(0)|答案(6)|浏览(150)

我正在尝试远程监控在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的同一台机器(上面的机器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
y1aodyip

y1aodyip1#

为了完整起见,下面的解决方案是可行的。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使这成为一个真正快速和简单的解决方案!

62lalag4

62lalag42#

对于开发环境,您可以将java.rmi.server.hostname设置为catch-all IP address0.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
xzlaal3s

xzlaal3s3#

我发现尝试在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时,您需要在类路径中添加相同的jar,以便通过JMXMP进行连接:

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

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

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

(默认端口为9875)

omvjsjqw

omvjsjqw4#

在周围挖了相当多,我发现这个配置

-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

wlsrxk51

wlsrxk515#

为了补充一些额外的见解,我使用了一些Docker端口Map,之前的答案都没有直接适用于我。经过调查,我在这里找到了答案:How to connect with JMX from host to Docker container in Docker machine?以提供所需的见解。
这就是我所相信的:
我按照其他答案中的建议设置了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容器并暴露/Map从主机到容器的端口。假设我在Docker中Map端口host:1099-〉container:1098。
  • 我使用上面的JMX设置在docker中运行JVM。
  • Docker容器中的JMX代理现在监听给定的端口1098。
  • 我在主机(Docker外部)上使用URL localhost:1099启动JConsole。我使用1099,因为我使用了1099:1098的host:docker端口Map。
  • JConsole可以很好地连接到Docker中的JMX代理。
  • JConsole询问JMX从何处读取监视数据。
  • JMX代理使用配置的信息和地址进行响应:localhost:1098
  • JConsole现在尝试连接到给定地址localhost:1098
  • 这会失败,因为本地主机(Docker外部)上的端口1098没有侦听。端口1099Map到Docker:1098。JMX应该告诉JConsole从localhost:1099读取监控信息,而不是localhost:1098,因为1099是从主机Map到Docker容器内1098的端口。

作为修复,我将我的host:docker端口Map从1099:1098更改为1098:1098。现在,JMX仍然告诉JConsole连接到localhost:1098以获取监视信息。但现在它可以工作,因为外部端口与Docker中的JMX广告相同。
我希望这同样适用于SSH隧道和类似的场景,你必须匹配你配置的JMX广告和JConsole看到的主机上的地址空间。
也许可以使用jmxremote.portjmxremove.rmi.porthostname属性来使用不同的端口Map,但我有机会使用相同的端口,所以使用它们简化了它,这是可行的(对我来说)。

nnvyjq4y

nnvyjq4y6#

AWS ECS等动态云解决方案

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

随后,在容器化应用程序的情况下,这意味着JMX/RMI配置需要JVM应用程序的预定义/静态端口,并且该端口应该在容器外部Map到容器内部的等效端口。这是使其工作的唯一方法。
因此,现在我想回答的主要问题是如何连接到运行在云中的JVM应用程序,该应用程序位于私有网络后面,并且只有当该端口由云管理而不是由我们管理时才由动态端口公开。
解决方案是存在的!它需要一些基础设施的巧妙方法。让我们看一下图表。

  • 因此,基本上我们希望首先运行JMX路由器容器,作为服务的一部分。该容器的目的是将传入的流量重定向到我们将用于JMX/RMI连接的JVM端口。
  • 我们将用于JMX的端口是Map到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。您可以选择允许使用的任何端口。但是在您选择之后-这正是我们在启动JVM应用程序时用来查找ECSMap到它的动态端口的端口。
现在,剩下的唯一任务就是获取分配给JMX路由器的动态端口,并将其用作JVM应用程序的RMI端口。为此,在entrypoint.sh中,我们为JVM应用程序映像创建了以下内容:

#!/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应用程序。
希望能帮上忙。

相关问题