java.nio.file.Files lines方法使用不當引發的文件句柄泄露

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();
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章