當前位置: 妍妍網 > 碼農

生產大檔下載導致 OOM,順藤摸瓜拿下

2024-03-26碼農

點選「 IT碼徒 」, 關註,置頂 公眾號

每日技術幹貨,第一時間送達!

上周遇到了生產環境 OOM 的問題,找了一番之後基本定位了是大檔下載導致的問題,於是在網上搜羅了一番文章,下面分享一篇優質的解決方案,整個排查思路非常清晰,小白可以直接對照著來排查。

1

事故發生

上周五下午營運人員反饋,筆者所負責的後台系統從 14 點以後就卡卡的,雖然頁面能夠正常載入,但是一直處於數據載入中,數據也送出不了,懷疑筆者的系統有BUG,當聽到營運人員的反饋我的第一反應是這不可能啊,這麽簡單的一個後台系統,還能出事故?

2

處理流程

  1. 摘除其中一台伺服器用於保留現場,其他伺服器先重新開機,保證系統可用。

  2. 下載GC日誌,系統dump檔用於分析

3

GC log分析

系統啟動參數,JVM記憶體分配: -Xmx4096m -Xms4096m -Xmn2560m

觀察日誌可知系統每隔 40S 發生一次 Full GC,耗時 200 毫秒, 回收以後系統老年代占用也不大,才 15M,但是新生代回收完還有 2 個 G。

有點不可思議,竟然不是老年代塞滿了數據,而是新生代塞滿了數據。

初步推測是新生代數據要晉升到老年代,結果放不進去而引起的 Full GC。

4

使用 MAT 對 Dump 檔進行分析

透過總圖可以看出來目前系統記憶體占用超過 2 個 G:

點選 Histogram 進行進一步分析,看出系統中占用最多的是byte[]

點選List Objects進入income參照統計界面

層層點開,發現byte[]被 ResponseEntity 物件所參照,且數量不小

5

翻閱程式碼

1)在系統中找到唯一ResponseEntity有關的程式碼

2)這程式碼看似沒什麽問題啊,這不是很正常的檔下載麽???我去看看使用者下載了啥,跑到目錄檔檢視一下下。

我的天,使用者下載的是一份2.4G的大檔,程式碼中FileUtils.readFileToByteArray(file) 的方式是把整個檔讀取到記憶體再輸出流裏寫入,此時記憶體不夠分配,又塞不進老年代,只能是 Full GC 了。

3)成功破案了,使用者下載了一份大檔,檔先載入到記憶體才往外寫,抹淚。。。。

6

解決方案

  • 使用FileSystemResource

  • @GetMapping("/down")
    public ResponseEntity download(@RequestParam("uri") String uri) throws IOException {
    File file = new File(uri);
    if (!file.isFile()) {
    thrownew ServiceException("檔不存在");
    }
    String filename = FilenameUtils.getName(uri);
    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
    HttpStatus status = HttpStatus.OK;
    returnnew ResponseEntity<>(new FileSystemResource(file), headers, status);
    }

  • 使用緩存流,邊讀邊寫

  • @GetMapping("/down")
    publicvoid download(@RequestParam("uri") String uri, HttpServletResponse response) throws IOException {
     File file = new File(uri);
    if (!file.isFile()) {
    thrownew ServiceException("檔不存在");
    }
    String filename = FilenameUtils.getName(uri);
    response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8"));
    try (FileInputStream fileInputStream = new FileInputStream(file);
    BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
    BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(response.getOutputStream())) {
    FileCopyUtils.copy(bufferedInputStream, bufferedOutputStream);
    } finally {
    // 使用的是try-with-resources
    }
    }


  • 檔儲存到 oss 或者是七牛雲

  • 將檔儲存到 oss 或者是七牛雲,繞過伺服器下載

    END

    PS:防止找不到本篇文章,可以收藏點贊,方便翻閱尋找哦。

    往期推薦