效能監控之 JMX 監控 Docker 容器中的 Java 應用

語言: CN / TW / HK

「這是我參與11月更文挑戰的第20天,活動詳情檢視:2021最後一次更文挑戰


一、前言

今天在配置 docker 和 JMX 監控的時候,看到有一個細節和非容器環境中的 JMX 配置不太一樣。所以在這裡寫一下,以備其他人查閱。

二、遇到的問題

1、問題現象

一般情況下,我們配置 JMX 只要寫上下面這些引數就可以了。

以下是無密碼監控時的 JMX 配置引數(有密碼監控的配置和常規監控無異)

bash -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9998 -Djava.rmi.server.hostname=<serverip> -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false

但是在 docker 容器中這樣配置的時候,會出現這個錯誤。 在這裡插入圖片描述

2、問題分析

這裡就要說明一下邏輯了。為什麼會這樣呢?

先看 docker 環境的網路結構。

容器使用預設的網路模型,就是 bridge 模式。在這種模式下是 docker run 時做的 DNAT 規則,實現資料轉發的能力。所以我們看到的網路資訊是以下這樣的:

docker 中的網絡卡資訊:

bash [root@f627e4cb0dbc /]# ifconfig eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 172.18.0.3 netmask 255.255.0.0 broadcast 0.0.0.0 inet6 fe80::42:acff:fe12:3 prefixlen 64 scopeid 0x20<link> ether 02:42:ac:12:00:03 txqueuelen 0 (Ethernet) RX packets 366 bytes 350743 (342.5 KiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 358 bytes 32370 (31.6 KiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 docker 中的路由資訊:

bash [root@a2a7679f8642 /]# netstat -r Kernel IP routing table Destination Gateway Genmask Flags MSS Window irtt Iface default gateway 0.0.0.0 UG 0 0 0 eth0 172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0 [root@a2a7679f8642 /]#

宿主機上的對應網絡卡資訊:

bash docker0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 inet 172.18.0.1 netmask 255.255.0.0 broadcast 0.0.0.0 ether 02:42:44:5a:12:8f txqueuelen 0 (Ethernet) RX packets 6691477 bytes 498130 RX errors 0 dropped 0 overruns 0 frame 0 TX packets 6751310 bytes 3508684363 (3.2 GiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

宿主機上的路由資訊:

bash [root@7dgroup ~]# netstat -r Kernel IP routing table Destination Gateway Genmask Flags MSS Window irtt Iface default gateway 0.0.0.0 UG 0 0 0 eth0 link-local 0.0.0.0 255.255.0.0 U 0 0 0 eth0 172.17.208.0 0.0.0.0 255.255.240.0 U 0 0 0 eth0 172.18.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0 192.168.16.0 0.0.0.0 255.255.240.0 U 0 0 0 br-676bae33ff92 所以宿主機和容器是可以直接通訊的,即便埠沒有映射出來。如下所示:

bash [root@7dgroup ~]# telnet 172.18.0.3 8080 Trying 172.18.0.3... Connected to 172.18.0.3. Escape character is '^]'.

另外,因為是橋接的,宿主機上還有類似 veth0b5a080 的虛擬網絡卡裝置資訊,如:

bash eth0b5a080: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500 ether 42:c3:45:be:88:1a txqueuelen 0 (Ethernet) RX packets 2715512 bytes 2462280742 (2.2 GiB) RX errors 0 dropped 0 overruns 0 frame 0 TX packets 2380143 bytes 2437360499 (2.2 GiB) TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0

這就是虛擬網絡卡對 veth pair,docker 容器裡一個,宿主機一個。 在這種模式下,有幾個容器,主機上就會有幾個 veth 開頭的虛擬網絡卡裝置。

但是如果不是宿主機訪問的話,肯定是不通的。如下圖所示: 在這裡插入圖片描述 當我們用監控機 訪問的時候,會是這樣的結果: bash Zees-Air-2:~ Zee$ telnet <serverip> 8080 Trying <serverip>... telnet: connect to address <serverip>: Connection refused telnet: Unable to connect to remote host Zees-Air-2:~ Zee$ 因為 8080 是容器開的埠,並不是宿主機開的埠,其他機器是訪問不了的。 這就是為什麼要把埠映射出來給遠端訪問的原因,對映之後的埠,就會有 NAT 規則來保證資料包可達。

檢視下 NAT 規則,就知道。如下:

```bash [root@7dgroup ~]# iptables -t nat -vnL Chain PREROUTING (policy ACCEPT 171 packets, 9832 bytes) pkts bytes target prot opt in out source destination 553K 33M DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT 171 packets, 9832 bytes) pkts bytes target prot opt in out source destination

Chain OUTPUT (policy ACCEPT 2586 packets, 156K bytes) pkts bytes target prot opt in out source destination 205K 12M DOCKER all -- * * 0.0.0.0/0 !60.205.104.0/22 ADDRTYPE match dst-type LOCAL 0 0 DOCKER all -- * * 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT 2602 packets, 157K bytes) pkts bytes target prot opt in out source destination 265K 16M MASQUERADE all -- * !docker0 172.18.0.0/16 0.0.0.0/0 0 0 MASQUERADE all -- * !br-676bae33ff92 192.168.16.0/20 0.0.0.0/0 0 0 MASQUERADE tcp -- * * 192.168.0.4 192.168.0.4 tcp dpt:7001 0 0 MASQUERADE tcp -- * * 192.168.0.4 192.168.0.4 tcp dpt:4001 0 0 MASQUERADE tcp -- * * 192.168.0.5 192.168.0.5 tcp dpt:2375 0 0 MASQUERADE tcp -- * * 192.168.0.8 192.168.0.8 tcp dpt:8080 0 0 MASQUERADE tcp -- * * 172.18.0.4 172.18.0.4 tcp dpt:3306 0 0 MASQUERADE tcp -- * * 172.18.0.5 172.18.0.5 tcp dpt:6379 0 0 MASQUERADE tcp -- * * 172.18.0.2 172.18.0.2 tcp dpt:80 0 0 MASQUERADE tcp -- * * 172.18.0.6 172.18.0.6 tcp dpt:9997 0 0 MASQUERADE tcp -- * * 172.18.0.6 172.18.0.6 tcp dpt:9996 0 0 MASQUERADE tcp -- * * 172.18.0.6 172.18.0.6 tcp dpt:8080 0 0 MASQUERADE tcp -- * * 172.18.0.3 172.18.0.3 tcp dpt:9995 0 0 MASQUERADE tcp -- * * 172.18.0.3 172.18.0.3 tcp dpt:8080

Chain DOCKER (3 references) pkts bytes target prot opt in out source destination 159K 9544K RETURN all -- docker0 * 0.0.0.0/0 0.0.0.0/0 0 0 RETURN all -- br-676bae33ff92 * 0.0.0.0/0 0.0.0.0/0 1 40 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:3307 to:172.18.0.4:3306 28 1486 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:6379 to:172.18.0.5:6379 228 137K DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:91 to:172.18.0.2:80 3 192 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9997 to:172.18.0.6:9997 0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9996 to:172.18.0.6:9996 0 0 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9002 to:172.18.0.6:8080 12 768 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9995 to:172.18.0.3:9995 4 256 DNAT tcp -- !docker0 * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9004 to:172.18.0.3:8080

[root@7dgroup ~]# ```

我們看到了宿主機的 91 埠的資料會傳給 172.18.0.2 的 80 埠。宿主機的 3307 埠會傳給 172.18.0.4 的3306 埠。

囉囉嗦嗦說到這裡,那和 JMX 有啥關係。苦就苦在,JMX 是這樣的: 在這裡插入圖片描述 在註冊時使用的是引數 jmxremote.port,然後返回一個新的埠 jmxremote.rmi.port

在呼叫服務時使用是引數 jmxremote.rmi.port。 前面提到了,因為 docker 在 bridge 模式下埠是要用 -p 顯式指定的,不然沒 NAT 規則,資料包不可達。所以在這種情況下,只能把 jmxremote.rmi.port 也暴露出去。所以必須顯式指定。因為不指定的話,這個埠會隨機開。隨機開的埠又沒 NAT 規則,所以是不通的了。

三、解決方案

所以,這種以上情況只能指定 jmxremote.rmi.port 為固定值,並暴露出去。 配置如下:

bash -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9995 -Djava.rmi.server.hostname=<serverip> -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.rmi.port=9995 像上面的設定就是兩個都是 9995,這樣是允許的,這種情況下注冊和呼叫的埠就合併了。

再啟動 docker 容器的時候,就需要這樣了。

bash docker run -d -p 9003:8080 -p 9995:9995 --name 7dgroup-tomcat5 -e CATALINA_OPTS="-Dcom.sun.management.jmxremote \ -Dcom.sun.management.jmxremote.port=9995 \ -Djava.rmi.server.hostname=<serverip> \ -Dcom.sun.management.jmxremote.ssl=false \ -Dcom.sun.management.jmxremote.authenticate=false \ -Dcom.sun.management.jmxremote.rmi.port=9995" c375edce8dfd 然後就可以連線上 JMX 的工具了。 在這裡插入圖片描述 在這裡插入圖片描述 在這裡插入圖片描述 在有防火牆和其他的裝置的網路環境中,也有可能出同樣的問題。明白了JMX 的註冊呼叫邏輯之後,就可以解決各種類似的問題了。

網路鏈路是做效能分析的人必須想明白的技術點,所以前面說了那麼多內容。

四、總結

這裡對於 JMX 工具的選擇囉嗦兩句。有人喜歡花哨的,有人喜歡簡單的,有人喜歡黑視窗的。我覺得工具選擇的時候,要看適用情況,在效能分析的時候,一定要選擇合適的工具,而不是選擇體現技術高超的工具。

最後留個作業:

  • 如果 docker run 中如果指定 -p 19995:9995,也就是換個埠暴露出去,其他配置都不變。JMX 工具還能連得上嗎?

  • 如果 jmxremote.rmi.portjmxremote.port 不合並,並且同時把兩個埠都暴露出去,其他配置都不變。JMX 工具還能連得上嗎?

有興趣的可以自己嘗試下哦。