Как получить доступ к интерфейсу JMX в Docker извне?

Я пытаюсь удаленно контролировать работу JVM в докере. Конфигурация выглядит так:

  • машина 1: запускает JVM (в моем случае - kafka) в докере на машине с Ubuntu; IP этой машины 10.0.1.201; приложение, запущенное в Docker, находится на 172.17.0.85.

  • машина 2: работает мониторинг JMX

Обратите внимание, что когда я запускаю мониторинг JMX с компьютера 2, он завершается с ошибкой версии следующей ошибки (примечание: такая же ошибка возникает при запуске 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)

Теперь интересно то, что когда я запускаю те же инструменты (jconsole, jvisualvm, jmxtrans и node-jmx / npm: jmx) на той же машине, на которой работает докер (машина 1 сверху), мониторинг JMX работает правильно.

Я думаю, это говорит о том, что мой порт JMX активен и работает должным образом, но когда я выполняю мониторинг JMX удаленно (с компьютера 2), похоже, что инструмент JMX не распознает внутренний IP-адрес док-станции (172.17.0.85)

Ниже приведены соответствующие (я думаю) элементы конфигурации сети на компьютере 1, где работает мониторинг JMX (обратите внимание на 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

7 ответов

Решение

Для полноты, следующее решение сработало. JVM должна быть запущена с определенными параметрами, установленными для включения мониторинга JMX удаленного докера:

-Dcom.sun.management.jmxremote
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false
-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)

Как только это будет сделано, вы сможете выполнять мониторинг JMX (jmxtrans, node-jmx, jconsole и т. Д.) С локальной или удаленной машины.

Спасибо Chris Heald за то, что сделали это действительно быстрым и простым исправлением!

Для среды разработки вы можете установить 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

Я обнаружил, что пытаться настроить JMX поверх RMI - это боль, особенно из-за -Djava.rmi.server.hostname=<IP> который вы должны указать при запуске. Мы запускаем наши докеры в Kubernetes, где все динамично.

В итоге я использовал 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>

И вам нужно добавить тот же самый jar ваш путь к классу при запуске JVisualVM для подключения через JMXMP:

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

Затем подключитесь с помощью следующей строки подключения:

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

(Порт по умолчанию 9875)

После долгих поисков я нашел эту конфигурацию

-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

Я создал проект GitHub, который содержит готовую реализацию JMX из контейнера Docker.

Он содержит Dockerfile с надлежащим entrypoint.shи docker-compose.yml для легкого развертывания.

Чтобы добавить некоторые дополнительные сведения, я использовал несколько сопоставлений портов Docker, и ни один из предыдущих ответов не работал у меня напрямую. После расследования я нашел здесь ответ: как подключиться с помощью JMX от хоста к контейнеру Docker на машине Docker? чтобы предоставить необходимую информацию.

Я верю, что происходит следующее:

Я установил 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 и открываю / отображаю порт от хоста к контейнеру. Скажем, я отображаю порт host:1099->container:1098 в Docker.
  • Я запускаю JVM внутри докера с указанными выше настройками JMX.
  • Агент JMX внутри контейнера Docker теперь прослушивает данный порт 1098.
  • Я запускаю JConsole на хосте (вне Docker) с URL-адресом localhost:1099. Я использую 1099, так как использовалhost:docker отображение портов 1099: 1098.
  • JConsole отлично подключается к агенту JMX внутри Docker.
  • JConsole спрашивает JMX, где читать данные мониторинга.
  • Агент JMX отвечает с настроенной информацией и адресом: localhost:1098
  • JConsole теперь пытается подключиться к заданному адресу localhost:1098
  • Это не удается, поскольку порт 1098 на локальном хосте (вне Docker) не прослушивается. Порт 1099 был сопоставлен сDocker:1098. Вместо тогоlocalhost:1098, JMX должен указать JConsole прочитать информацию мониторинга из localhost:1099, поскольку 1099 был портом, отображаемым с хоста на 1098 внутри контейнера Docker.

В качестве исправления я изменил свой host:docker отображение портов из 1099:1098 к 1098:1098. Теперь JMX по-прежнему сообщает JConsole подключиться кlocalhost:1098для мониторинга информации. Но теперь это работает, поскольку внешний порт такой же, как объявленный JMX внутри Docker.

Я ожидаю, что то же самое применимо и к туннелям SSH и аналогичным сценариям. Вы должны сопоставить то, что вы настраиваете JMX для рекламы, и то, что JConsole видит как адресное пространство на хосте, на котором вы его запускаете.

Может, можно немного поиграть с jmxremote.port, jmxremove.rmi.port, а также hostnameатрибуты, чтобы эта работа работала с использованием различных сопоставлений портов. Но у меня была возможность использовать те же порты, поэтому их использование упростило, и это работает (для меня).

Решение для облака, например AWS ECS

Основная проблема заключается в том, что протокол JMX/RMI требует, чтобы и хост, и порт соответствовали соответствующим образом между сервером (вашим приложением JVM) и клиентом (например, VisualVM), который подключается к серверу. Другими словами, если какой-либо из этих параметров не будет совпадать – установить соединение будет невозможно.

Впоследствии, в случае контейнерного приложения, это означает, что конфигурация JMX/RMI требует предопределенного/статического порта для приложения JVM, и этот порт должен быть сопоставлен вне контейнера с эквивалентным портом внутри контейнера. Это единственный способ заставить его работать.

Итак, теперь главный вопрос, на который я хочу ответить, заключается в том, как подключиться к вашему JVM-приложению, которое работает внутри вашего облака, за частной сетью и доступно через динамический порт только тогда, когда этот порт управляется облаком, а не нами.

Решение существует! И это потребует некоторого хитрого подхода к инфраструктуре. Давайте посмотрим на схему.

  • Итак, по сути, мы хотим сначала запустить контейнер маршрутизатора JMX как часть нашего сервиса. Целью этого контейнера является перенаправление входящего трафика на порт нашей JVM, который мы будем использовать для соединения JMX/RMI.
  • Порт, который мы будем использовать для JMX, будет динамическим портом, сопоставленным со статическим входящим портом контейнера маршрутизатора JMX.
  • Как только мы получили динамический порт (запустился контейнер маршрутизатора) — мы будем использовать его для запуска нашего JVM-приложения.

Для создания нашего маршрутизатора JMX мы будем использовать HAproxy. Для построения образа нам понадобится:

      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"]

где :

      #!/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

с:

      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:. Вы можете выбрать любой порт, который разрешено использовать. Но после того, как вы выберете, именно этот порт мы будем использовать для поиска динамического порта, сопоставленного ему ECS при запуске нашего приложения JVM.

Итак, теперь осталось только назначить динамический порт нашему маршрутизатору JMX и использовать его в качестве порта RMI для нашего приложения JVM. Для этого вдля нашего образа приложения 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

Итак, теперь, как только оба контейнера запущены и работают, используйтеидля подключения к вашему JVM-приложению внутри кластера ECS.

Проверено и работает у нас. Надеюсь, это будет полезно и другим.

Другие вопросы по тегам