1. 問題描述
4月29日上午,測試同學通過壓測工具測試"網關->業務層->分析服務"鏈路,QPS 200。
測試開始不久後,CloudMonitor告警"分析服務"服務器磁盤佔用超過80%,經過排查,確定告警根原因是java.nio.file.Files lines方法使用不當引發的文件句柄泄露,臨時文件被刪除後磁盤空間未釋放導致。
2. 排查步驟
-
測試開始後10分鐘左右,CloudMonitor告警"分析服務"服務器磁盤佔用超過80%,登錄服務器刪除部分日誌後,磁盤佔用降低到70%,告警解除;
-
10分鐘後,CloudMonitor告警"分析服務"服務器磁盤佔用超過80%,登錄服務器查看日誌目錄,發現日誌目錄佔用磁盤空間不足1GB,判斷是其他目錄佔用了磁盤空間;
-
執行du -h -s * 檢查主要目錄後,發現所有目錄佔用空間遠小於df -h命令返回的磁盤總使用空間,判斷是文件句柄泄露導致文件雖被刪除但磁盤空間未釋放;
-
執行lsof | grep deleted 列出所有已打開且已刪除的文件,果然返回大量臨時文件;
-
重啓JAVA進程後,磁盤空間佔有率降至50%以下,問題原因確定爲JAVA代碼導致的文件句柄泄露。
3. 代碼檢查
通常的,文件句柄泄露是由於BufferedWriter BufferedReader 之類的文件讀寫操作類沒有關閉導致,因此重點檢查了相關代碼,但發現開發同學相關操作時均使用了try-with-resources優化關閉資源,並不會導致文件句柄泄露。
public static void writeFileByFullPath(String filename, List<String> lines) {
try (FileWriter fw = new FileWriter(filename, true)) {
try(BufferedWriter bw = new BufferedWriter(fw)) {
for (String line : lines) {
bw.write(line);
bw.newLine();
}
bw.flush();
}
}
}
逐行審查代碼後發現,如下代碼:
long total = java.nio.file.Files.lines(filePath).count();
java.nio.file.Files.lines是JDK8加入的方法,能夠幫助開發者更加簡單的處理文本文件,類似於Groory中的
def list = new File(filePath).collect { it }
Files.lines源代碼如下:
public static Stream<String> lines(Path path) throws IOException {
return lines(path, StandardCharsets.UTF_8);
}
public static Stream<String> lines(Path path, Charset cs) throws IOException {
BufferedReader br = Files.newBufferedReader(path, cs);
try {
// 添加asUncheckedRunnable到Stream的關閉回調
// asUncheckedRunnable中關閉br
return br.lines().onClose(asUncheckedRunnable(br));
} catch (Error|RuntimeException e) {
try {
br.close();
} catch (IOException ex) {
try {
e.addSuppressed(ex);
} catch (Throwable ignore) {}
}
throw e;
}
}
private static Runnable asUncheckedRunnable(Closeable c) {
return () -> {
try {
c.close();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
};
}
3. 問題修復
將問題代碼修改爲如下,併發布後,問題修復。
long total = 0L;
try (Stream<String> stream = java.nio.file.Files.lines(filePath)) {
total = stream.count();
}