Как получить доступ к интерфейсу 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:
Итак, теперь осталось только назначить динамический порт нашему маршрутизатору JMX и использовать его в качестве порта RMI для нашего приложения 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
Итак, теперь, как только оба контейнера запущены и работают, используйте
Проверено и работает у нас. Надеюсь, это будет полезно и другим.