03 December 2006

JDK 5 內建了 jmx 的 client: jconsole。只消在 jvm 啟動時加上參數:

    java myapp -Dcom.sun.management.jmxremote 

就能使用 jconsole 在本地端連上該 jvm (server),觀測它的 memory 和 thread 等等的情報,非常方便。

但是!一切美好的東西一旦變成遠端處理,問題就層出不窮... 下面介紹兩種連線的方式。

(1) 假設在沒有 firewall 的情況下,我們仍然能夠使用 jvm 內建的 rmi 做遠端存取,只要參數再加上:

  -Dcom.sun.management.jmxremote 
  -Dcom.sun.management.jmxremote.port=1099 
  -Dcom.sun.management.jmxremote.ssl=false 
  -Dcom.sun.management.jmxremote.password.file=/mypath/jmxremote.password

上面的參數我們指定 jvm 啟動一 rmi registry server 在 1099 port,而連線時所需要的帳號密碼,則寫在 /mypath/jmxremote.password 裡。這個檔案的格式可以參考 JRE_HOME/lib/management/jmxremote.password.template,一般的設定是:

  controlRole  myAdminPassword
  monitorRole  myMonitorPassword

帳號 controlRole 可以讀寫 jmx 的操作,而帳號 monitorRole 則只能唯讀 jmx 的資料。另外 /mypath/jmxremote.password 檔案權限要設為只有自己才能讀取:

   chmod 600 /mypath/jmxremote.password

ok,完成上述步驟後即可使用 jconsole 遠端連至 jvm (server):

   jconsole myapp.example.com:1099

登入視窗輸入帳號密碼: controlRole/myAdminPassword 即可。

上面的做法使用 jvm 本身內建的 rmi server connector 和 rmi registry 而達到遠端連線。 如果你用 netstat 之類的指令去追蹤,可發現除了 1099 之外,還有一個亂數的 port 的連線。它就是 rmi server connector 另外建立的,用來做遠端物件的傳輸。可惜的是 jvm 內建的 connector 無法指定這個亂數的 port... 對有 firewall 的系統而言,就無法做特殊的設定了。

為解決這個亂數 port 的問題,只好放棄 jvm 內建的 rmi 服務,自己建一個。

(2) 由於使用自己建立的 rmi server,我們可以還原環境變數成:

-Dcom.sun.management.jmxremote 

接下來,我們利用 spring 來建立 rmi server:

<!-- create rmi registry on specific port -->
<bean class="org.springframework.remoting.rmi.RmiRegistryFactoryBean" >
    <property name="port" value="9000"/>
</bean>

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

    <property name="objectName" value="connector:name=rmi"/>
      <!-- the first url (rmi://...) is for rmi objects transfer
           the second url (/jndi/rmi:....) is for rmiregistry 
      -->
      <property name="serviceUrl" 
                   value="service:jmx:rmi://localhost:3000/jndi/rmi://localhost:9000/server" />

      <!-- make connector use it's own thread, default is false -->
      <property name="threaded" value="true" /> 

      <!-- make server as deamon, default is false -->
      <property name="daemon" value="true" />
      
      <property name="environmentMap">
          <map>
              <entry key="jmx.remote.x.password.file" value="/mypath/jmxremote.password">
              </entry>
          </map>
      </property>
</bean>

上面的設定檔,第一個 bean 我們註冊了一個 RMIRegistry 在 port 9000。 第二個bean "rmiConnectorServer" 使用 serviceUrl 指定 port:

service:jmx:rmi://localhost:3000/jndi/rmi://localhost:9000/server

前半段的 rmi://localhost:3000 指定 RMI 使用 port 3000進行遠端物件的傳輸, 後半端的 jndi/rmi://localhost:9000/server則表示將 RMIConnectorServer bind 到使用 9000 port 的 RMIRegistry。至於 environmentMap 內指定的是 password 檔,內容同上。

ok,設好之後,spring 啟動時便會啟動 rmi registry 和 connector,我們可以用下面的 jconsole 指令遠端連線:

jconsole service:jmx:rmi://myapp.example.com:3000/jndi/rmi://myapp.example.com:9000/server

輸入帳號密碼後即可登入。這一次就會使用 port 3000 和 9000 連線了,所以 firewall 可以對 3000/9000 port 做一些設定。

注意 !如果你的 jvm (server) 是在 linux 下使用,請先檢查 hostname -i 這個指令是否回傳 127.0.0.1。如果是,RMI 將無法連線,你可以使用下列的解法:

#設定 /etc/hosts, xxx.xxx.xxx.xxx 是真的 ip
xxx.xxx.xxx.xxx myhostname

或是指定 jvm 參數:

-Djava.rmi.server.hostname=myhostname

呼... 總算搞定了... linux 那個 hostname 問題搞了我一整個下午, 真是 @$#^%!$。至於如何使用 firewall 並經由 ssl tunnel ... 呃... 還有一堆參數和設定檔要做,光用想的就快暈了,下次有緣再試吧!

參考資料:

ps. 文中提及的 jvm 都是指 server 端的 vm,而不是 client vm (即jconsole)。