Java 理論與實踐: 用JMX檢測應用程序
【IT168 技術文檔】
調試器和分析器可以提供對應用程序的行為的深入觀察,但在出現嚴重問題之前,這些工具通常用不上。將監視掛鉤(hook)構建到應用程序內,會使理解程序 的執行變得更容易而且不會破壞調試器。既然 Java 管理擴展(JMX)已經構建進了 Java SE 平臺,而且 jconsole 查看器提供了統一的監視 GUI,那么用 JMX 為應用程序提供一個窗口,要比以前更加容易而且更為有效。
有多少次您曾經注視著運行中的應用程序,問自己:“它到底在做什么?為什么用了這么長時間呢?” 在這些時刻,您可能會想如果自己在應用程序中構建了更多的監視功能就好了。例如,在服務器應用程序中,能夠查看排隊等候處理的任務的數量和類型、當前正在處理的任務、過去一分鐘或一小時內的吞吐量統計、平均任務處理時間等。這些統計值容易搜集,但是在需要數據的時候,如果沒有非侵入性的數據檢索機制,那么這些值就不太有用。
可以用許多方式導出操作性數據——可以把周期性統計快照寫入日志文件、創建 Swing GUI、使用內嵌的 HTTP 服務器在 Web 頁面上顯示統計值或者發布可以用來查詢應用程序的 Web 服務。但是在缺少監視和數據發布基礎設施的情況下,多數應用程序開發人員都做不到這些,因此造成對應用程序工作情況的了解要比預期的少很多。
JMX
在 Java 5.0 中,類庫和和 JVM 提供了一種全面的管理和監視基礎設施——JMX。JMX 是一種用來提供可以遠程訪問的管理接口的標準措施,也是一種向應用程序添加靈活且強大的管理接口的簡易方式。被稱作受管 bean(MBean)的 JMX 組件,是提供與實體的管理有關的訪問器和業務方法的 JavaBean。每個受管的實體(可能是整個應用程序或應用程序中的服務)實例化一個 MBean 并用可讀懂的名稱注冊它。支持 JMX 的應用程序依賴于 MBeanServer,它充當 MBean 的容器,提供遠程訪問、命名空間管理和安全服務。在客戶端,jconsole 工具可以充當統一的 JMX 客戶機。結合兩者,對 JMX 的平臺支持極大地降低了使應用程序支持外部管理接口所需的工作和努力。
除了提供 MBeanServer 實現,Java SE 5.0 還提供 JVM 以更方便地了解內存管 理、類裝入、活動線程、日志和平臺配置的狀態。多數平臺服務的監視和管理在默認情況下都是開啟的(性能影響最小),所以只需要連接應用程序與 JMX 客戶機即可。圖 1 給出了 jconsole JMX 客戶機(JDK 的一部分) ,它顯示了其中一個內存管理視圖——一段時間內的堆使用情況。Perform GC 按鈕則證明了 JMX 可以提供 除了查看操作統計值之外的初始化操作的功能。

圖 1. 用 jconsole 查看堆使用情況
傳輸和安全性
JMX 指定了在 MBeanServer 和 JMX 客戶之間通信所 使用的協議,協議可以在各種傳輸機制上運行。可以使用針對本地連接的內置傳輸,及通過 RMI、socket 或 SSL 的遠程傳輸(可以通過 JMX Connector API 創建新的傳輸)。認證是由傳輸執行的;本地傳輸允許用相同的用戶 ID 連接到運行在本地系統上的 JVM;遠程傳輸可以用口令或證書進行認證。本地傳輸在 Java 6 下默認就是啟用的。要在 Java 5.0 下啟用它,需要在 JVM 啟動時定義系統屬性 com.sun.management.jmxremote。“Monitoring and Management using JMX” 這份文檔描述了啟用和配置傳輸的配置步驟。
檢測 Web 服務器
檢測應用程序來使用 JMX 很容易。像其他許多遠程調用框架(RMI、EJB 和 JAX-RPC)一樣,JMX 也是基于接口的。要創建管理服務,需要創建指定管理方法的 MBean 接口。然后可以創建一個 MBean 來實現此接口、實例化它及把它注冊到 MBeanServer。
清單 1 顯示了網絡服 務(例如 Web 服務器)的 MBean 接口。它提供了檢索配置信息(例如端口號)和操作性信息(例如服務是否啟動)的 getter。它還包含查看和修改可配置參數(例如當前日志級別)的 getter 和 setter,還有調用管理操作(例如 start() 和 stop())的方法。
清單 1. 某個 Web 服務器的 MBean 接口
public interface WebServerMBean { public int getPort(); public String getLogLevel(); public void setLogLevel(String level); public boolean isStarted(); public void stop(); public void start(); } |
實現 MBean 類通常非常直接明了,因為 MBean 接口要反映現有實體或服務的屬性和管理操作。例如,MBean 中的 getLogLevel() 和 setLogLevel() 方法會直接轉給被 Web 服務器使用的 Logger 上的 getLevel() 和 setLevel() 方法。JMX 做了一些命名限制。例如,MBean 接口名稱必須以 MBean 結尾,FooMBean 接口的 MBean 類必須叫作 Foo。(可以用更高級的 JMX 特性——動態 MBean 來去除這個限制。)把 MBean 注冊到默認的 MBeanServer 也很容易,如清單 2 所示:
清單 2. 用內置的 JMX 實現注冊 MBean
public class WebServer implements WebServerMBean { ... } ... WebServer ws = new WebServer(...); MBeanServer server = ManagementFactory.getPlatformMBeanServer(); server.registerMBean(ws, new ObjectName("myapp:type=webserver,name=Port 8080")); |
傳遞給 registerMBean() 的 ObjectName 標識了受管實體。因為預見到指定應用程序可能包含許多受管實體,所以名稱包含域(清單 2 中的 “myapp”)和許多標識域中的受管資源的鍵-值對。“name” 和 “type” 這兩個鍵是常用的,在使用的時候,name 應當在域中所有的同類 MBean 中能夠唯一地標識受管實體。也可以指定其他鍵-值對,而且 JMX API 還包含進行對象名稱通配匹配的工具。
創建并注冊了 MBean 之后,立即就可以把 jconsole 指向應用程序(在命令行輸入 jconsole)并在 “MBeans” 視圖中查看它的管理屬性和操作。圖 2 顯示了 jconsole 中針對新 MBean 的 Attributes 標簽,圖 3 顯示了 Operations 標簽。使用反射,JMX 可以指出哪個屬性是只讀的(Started、Port),哪個屬性是可讀寫的(LogLevel),而且 jconsole 允許修改讀寫屬性。如果讀寫屬性的 setter 拋出異常(例如 IllegalArgumentException),JMX 就把異常報告給客戶機。

圖 2. jconsole 中 MBean 的 Attributes 標簽

圖 3. jconsole 中 MBean 的 Operations 標簽
數據類型
MBean 中的訪問器和操作能夠用任何其簽名形式的原語類型,以及 String、Date 和其他標準庫類。也可以使用這些允許的類型的數組和集合。MBean 方法也可以使用其他可以序列化的數據類型,但是這樣做會造成互操作性問題,因為類文件也必須對 JMX 客戶機可用。(如果使用 RMI 傳輸,可以使用 RMI 的自動類下載特性完成這項任務。)如果想在管理接口中使用結構化數據類型,還想避免與類可用性相關的互操作性問題,可以使用 JMX 的開放 MBean 特性來表達復合或表格數據。
檢測服務器應用程序
在創建管理接口時,某些參數和操作的特點很自然地就表明這些參數和數據應當被包含在內,例如配置參數、操作統計值、調試操作(例如修改日志級別或把應用 程序狀態導出到文件)、生命周期操作(啟動、停止)。檢測一個應用程序,讓它支持對這些屬性和操作的訪問,通常相當容易。但是,要從 JMX 獲得最大價值,就要在設計時考慮什么數據在運行時對用戶和操作員有用。
如果用 JMX 了解服務器應 用程序的工作情況,需要一種標識和跟蹤工作單元的機制。如果使用標準的 Runnable 和 Callable 接口描述任務,通過讓任務類自描述(例如實現toString() 方法),可以在任務生命周期內跟蹤它們,并提供 MBean 方法來返回等候中、處理中和完成的任務列表。
清單 3 中的 TrackingThreadPool 演示的是 ThreadPoolExecutor 的一個子類,它及時給出正在處理中的是哪些任務,以及已經完成的任務的時間統計值。它通過覆蓋 beforeExecute() 和 afterExecute() 掛鉤,并提供能檢索所搜集數據的 getter,實現這些任務。
清單 3. 搜集處理中的任務和平均的任務時間統計值的線程池類
public class TrackingThreadPool extends ThreadPoolExecutor {
private final Map<Runnable, Boolean> inProgress
= new ConcurrentHashMap<Runnable,Boolean>();
private final ThreadLocal<Long> startTime = new ThreadLocal<Long>();
private long totalTime;
private int totalTasks;
public TrackingThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
inProgress.put(r, Boolean.TRUE);
startTime.set(new Long(System.currentTimeMillis()));
}
protected void afterExecute(Runnable r, Throwable t) {
long time = System.currentTimeMillis() - startTime.get().longValue();
synchronized (this) {
totalTime += time;
++totalTasks;
}
inProgress.remove(r);
super.afterExecute(r, t);
}
public Set<Runnable> getInProgressTasks() {
return Collections.unmodifiableSet(inProgress.keySet());
}
public synchronized int getTotalTasks() {
return totalTasks;
}
public synchronized double getAverageTaskTime() {
return (totalTasks == 0) ? 0 : totalTime / totalTasks;
}
}
清單 4. TrackingThreadPool 的 MBean 接口
public interface ThreadPoolStatusMBean { public int getActiveThreads(); public int getActiveTasks(); public int getTotalTasks(); public int getQueuedTasks(); public double getAverageTaskTime(); public String[] getActiveTaskNames(); public String[] getQueuedTaskNames(); } |
如果任務的重量級足夠,那么甚至可以再進一步,在每個任務提交時都為它注冊一個 MBean (然后在任務完成時再取消注冊)。然后可以用管理接口查詢每個任務的當前狀態、運行了多長時間,或者請求取消任務。
清單 5 中的 ThreadPoolStatus 實現了 ThreadPoolStatusMBean 接口,它提供了每個訪問器的明顯實現。與 MBean 實現類中的典型情況一樣,每個操作實現起來都很細碎,所以把實現委托給了底層受管對象。在這個示例中,JMX 代碼完全獨立于受管實體的代碼。TrackingThreadPool 對于 JMX 一無所知;通過為相關的屬性提供管理方法和訪問器,它提供了自己的編程管理接口。 還可以選擇在實現類中直接實現管理功能(讓 TrackingThreadPool 實現 TrackingThreadPoolMBean 接口),或者單獨實現(如清單 4 和 5 所示)。
清單 5. TrackingThreadpool 的 MBean 實現
public class ThreadPoolStatus implements ThreadPoolStatusMBean {
private final TrackingThreadPool pool;
public ThreadPoolStatus(TrackingThreadPool pool) {
this.pool = pool;
}
public int getActiveThreads() {
return pool.getPoolSize();
}
public int getActiveTasks() {
return pool.getActiveCount();
}
public int getTotalTasks() {
return pool.getTotalTasks();
}
public int getQueuedTasks() {
return pool.getQueue().size();
}
public double getAverageTaskTime() {
return pool.getAverageTaskTime();
}
public String[] getActiveTaskNames() {
return toStringArray(pool.getInProgressTasks());
}
public String[] getQueuedTaskNames() {
return toStringArray(pool.getQueue());
}
private String[] toStringArray(Collection<Runnable> collection) {
ArrayList<String> list = new ArrayList<String>();
for (Runnable r : collection)
list.add(r.toString());
return list.toArray(new String[0]);
}
}
清單 6. Web 搜尋應用程序中使用的 FetchTask 類
public class FetchTask implements Runnable { private final String name; public FetchTask(String name) { this.name = name; } public String toString() { return "FetchTask: " + name; } public void run() { /* Fetch remote resource */ } } |
當此程序處理每個頁面時,可能還會對新任務進行排隊以獲取這個頁面上鏈接的頁面,所以在指定時間內,可能會既 有獲取任務又有尚未完成的索引任務。能夠正確地判斷正在處理哪個頁面,或者正在等候處理哪個頁面,不僅讓您可以理解應用程序的性能特征,還可以理解應用程 序所操作的數據的特征。
圖 4 顯示了正在處理 whitehouse.gov 站點的 Web 搜尋程序的快照。從圖中可以看到已經獲取并索引了主頁,程序現在的工作是獲取和索引直接從該主頁鏈接出的頁面。單擊 Refresh 按鈕,可以對應用程序的工作流程進行取樣,它可以提供許多關于應用程序工作情況的信息,卻不需引入大量日志或者在調試器中運行應用程序。

圖 4. Web 搜尋應用程序中的活動任務和排隊任務
結束語
結合平臺內的 JMX 支持和 jconsole JMX 客戶機可以提供一種向應用程序添加管理和監視功能的輕松方式。即使是沒有具體管理需求的應用程序,為它們構建這些功能也會讓您對程序的運行及其所處理的數 據的性質獲得深入了解,而且不需太多的工作和努力。如果應用程序導出管理接口,此接口讓您可以查看它操作的內容,那么您就會更加了解它的運行狀態——對它 是否按預期的方式工作也會更有信心——而不必求助于額外的工具(例如添加日志代碼或使用調試器或分析器)。