生產環境 Tomcat 偶發假死(埠口無響應但進程存在)或崩潰退出。本文提供一個 **純 Java 守護進程**,結合 **HTTP 健康檢查**與 **進程掃描**,實現「檢測到異常 → 優雅停止 → 強制殺死 → 重新啟動」的完整自愈閉環,附完整代碼、部署建議與系統級替代方案對比。
定時任務 (每 30 秒)
↓
HTTP GET /health (timeout 8s)
├─ 成功 (200) → 正常,下一輪
└─ 失敗/超時
↓
掃描進程列表 (ps -aux) 確認 Tomcat 進程是否存在
├─ 存在 → 僵屍/假死 → 執行 shutdown.sh → 等待 → 執行 startup.sh
└─ 不存在 → 已崩潰 → 直接執行 startup.sh
↓
記錄日誌、告警通知
import java.io.*;
import java.net.*;
import java.util.Date;
public class TomcatWatchdog {
private static final String HEALTH_URL = "https://localhost:8080/health"; // 需在 Tomcat 部署健康檢查端點
private static final String TOMCAT_HOME = "/opt/tomcat"; // 實際安裝路徑
private static final String SHUTDOWN_CMD = TOMCAT_HOME + "/bin/shutdown.sh";
private static final String STARTUP_CMD = TOMCAT_HOME + "/bin/startup.sh";
private static final String PROCESS_GREP = "org.apache.catalina.startup.Bootstrap"; // 進程特徵
private static final int CHECK_INTERVAL_MS = 30_000;
public static void main(String[] args) {
System.out.println("[" + new Date() + "] Tomcat Watchdog 啟動");
while (true) {
try {
checkAndHeal();
} catch (Exception e) {
System.err.println("[" + new Date() + "] 守護異常: " + e.getMessage());
e.printStackTrace();
}
sleep(CHECK_INTERVAL_MS);
}
}
private static void checkAndHeal() throws IOException, InterruptedException {
boolean httpOk = httpHealthCheck();
boolean processAlive = isTomcatProcessAlive();
if (httpOk) {
System.out.println("[" + new Date() + "] HTTP 健康檢查通過");
return;
}
System.out.println("[" + new Date() + "] HTTP 健康檢查失敗, 進程存活: " + processAlive);
if (processAlive) {
System.out.println("[" + new Date() + "] 偵測到假死/僵屍進程, 執行優雅關閉...");
executeScript(SHUTDOWN_CMD, "Stopping");
sleep(10_000); // 等待優雅關閉
if (isTomcatProcessAlive()) {
System.out.println("[" + new Date() + "] 優雅關閉失敗, 強制殺死進程...");
killProcess(PROCESS_GREP);
sleep(3_000);
}
} else {
System.out.println("[" + new Date() + "] 進程已不存在, 直接啟動");
}
System.out.println("[" + new Date() + "] 執行啟動腳本...");
executeScript(STARTUP_CMD, "Starting");
sleep(15_000); // 等待啟動完成
System.out.println("[" + new Date() + "] 重啟流程結束");
}
private static boolean httpHealthCheck() {
try {
URL url = new URL(HEALTH_URL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setReadTimeout(8000);
conn.setRequestMethod("GET");
return conn.getResponseCode() == 200;
} catch (Exception e) {
return false;
}
}
private static boolean isTomcatProcessAlive() {
try {
Process ps = Runtime.getRuntime().exec(new String[]{"sh", "-c", "ps -aux | grep " + PROCESS_GREP + " | grep -v grep"});
try (BufferedReader br = new BufferedReader(new InputStreamReader(ps.getInputStream()))) {
return br.readLine() != null;
}
} catch (Exception e) {
return false;
}
}
private static void executeScript(String script, String successKeyword) {
try {
Process p = Runtime.getRuntime().exec(new String[]{"sh", "-c", script});
try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
if (line.contains(successKeyword)) {
System.out.println("[" + new Date() + "] 腳本輸出: " + line.trim());
return;
}
}
}
p.waitFor();
} catch (Exception e) {
System.err.println("[" + new Date() + "] 執行腳本失敗: " + e.getMessage());
}
}
private static void killProcess(String grepPattern) {
try {
String cmd = "ps -aux | grep " + grepPattern + " | grep -v grep | awk '{print $2}' | xargs kill -9";
Runtime.getRuntime().exec(new String[]{"sh", "-c", cmd});
} catch (Exception e) {
System.err.println("[" + new Date() + "] 強制殺死失敗: " + e.getMessage());
}
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}
}
/health(Spring Boot Actuator、自定義 Servlet 均可),回傳 200 且體驗極快(不查 DB、不呼叫外部服務)。tomcat) 運行。| 方案 | 優點 | 缺點 | 推薦場景 |
|---|---|---|---|
Systemd Restart=on-failure | 原生、零代碼、管理整個服務單元 | 無法檢測「進程在但埠口無響應」假死 | 簡單崩潰重啟 |
| Supervisord | 跨平台、Web UI、進程組管理 | 同 Systemd,無 HTTP 層檢查 | 需管理多個異構進程 |
| K8s Liveness/Readiness Probe | 原生 HTTP/TCP/Exec 探針、自動重建 Pod、滾動更新 | 需容器化環境 | 雲原生/K8s 部署 |
| Zabbix/Prometheus + Alertmanager | 完整監控體系、圖表、多維告警 | 架構複雜、非輕量級 | 企業級統一監控平台 |
| 本文 Java Watchdog | 純 Java、無外部依賴、邏輯可完全自定義、適合傳統 VM 部署 | 需自行維護代碼、單點故障 | 傳統機房/VM、無 K8s、需 HTTP 層自愈 |