記錄並分享一下最近工作中遇到的 Too many open files 異常的解決過程。
問題背景
產品有個上傳壓縮包並匯入配置資訊到資料庫中的功能,主要流程如下:
- 使用者上傳壓縮包;
- 後端解壓存放在臨時目錄,並返回列表給使用者;
- 使用者選擇需要匯入哪些資訊;
- 後端按需插入資料庫中,完成後刪除臨時目錄。
這個功能上線兩三年了,一直沒出現問題,最近測試在功能迴歸時,匯入的時候出現Too many open files
異常。
但是透過lsof -p pid | wc -l
檢視當前程式開啟的控制程式碼數時,又是正常的。
Too many open files是Linux系統中常見的錯誤,字面意思就是說開啟了太多的檔案,超過了系統的限制。
這裡的檔案(file)更準確的意思是檔案控制程式碼,或者是檔案描述符。可以說,Linux系統裡的一切都是檔案,包括網路連線、埠等等。
lsof -p pid
命令可以檢視指定程式當前開啟的檔案資訊。wc -l
命令指按行統計。
問題分析
當時的第一反應是該系統的檔案控制程式碼數配置的太低了,因為在其他環境都是正常的。
透過ulimit -a
命令,可以檢視當前系統對各種資源的限制。
[uyong@linuxtest ~]# ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 31767
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 4096
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 31767
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
open files 那行就是表示開啟的檔案數限制,即一個程式最多可以同時開啟的檔案數。
也可以透過
ulimit -n
直接檢視最大開啟檔案數量。
當時查出來的配置是4096,檢視其他沒問題的環境上的配置,數量都是遠遠大於這個數。而且系統重新啟動後,沒做任何操作時,透過lsof -p pid | wc -l
檢視檔案佔用,只有100多個。在好的環境上執行匯入成功後,再次檢視,檔案佔用數不變。在有問題的環境上匯入失敗後,檢視檔案佔用數也是不變。
雖然當時的壓縮包檔案很大,但4096按說也夠的。有點奇怪,為了不影響測試進度,只能先臨時修改系統配置,增大檔案數限制,ulimit -n 65535
命令執行後,再次匯入沒問題了。
命令
ulimit -n 65536
只能臨時臨時調整檔案數量,系統重啟後配置就會失效。如果要永久生效,需要在配置檔案
/etc/security/limits.conf
裡增加如下兩行:* soft nofile 65536 * hard nofile 65535
問題到此就結束了嗎,NO😒,治標不治本,而且測試還順手提了個遺留缺陷🐛
問題解決
出現這個問題,不用懷疑,肯定就是開啟的檔案太多了,又沒有及時釋放,檔案越多,佔用的控制程式碼就越多。
擼了一遍整個流程的程式碼,有兩個介面:
一個介面用於上傳壓縮包,解壓後,返回資源列表,偽碼如下:
public List<Item> upload(MultipartFile zipFile) {
try {
// 解壓壓縮包到臨時目錄
unzip(zipFile, tmpDir);
// 蒐集所有的資源返回給前端
return collectItems(tmpDir);
} catch (Exception e) {
// 只要發生了異常,就把臨時目錄清理了
clear(tmpDir);
}
}
一個介面用於處理使用者選擇的資源並匯入到資料庫,偽碼如下:
public void importDb(List<Item> selected) {
try {
// 讀取檔案並插入資料庫
readFilesAndImportDb(tmpDir, selected);
} finally {
// 處理完成後,不管失敗與否,都把臨時目錄清理了
clear(tmpDir);
}
}
不管哪個介面,都在最後清理了資源,也就解釋了不管匯入成功還是失敗,檢視檔案佔用情況都是正常的。
逐個方法排查後,最終確定是unzip方法有問題,這裡貼一下解壓程式碼,看看你有沒有發現問題所在👀
import java.io.*;
import java.util.*;
import org.apache.commons.io.IOUtils;
/**
* @param in zip輸入流
* @param outDir 解壓後檔案存在目錄
*/
public void unzip(InputStream in, String outDir) throw IOException {
try (ZipInputStream zipIn = new ZipInputStream(new BufferedInputStream(in))) {
ZipEntry zipEntry;
while((zipEntry = zipIn.getNextEntry()) != null) {
File outItemFile = new File(outDir, zipEntry.getName);
if (zipEntry.isDirectory()) {
outItemFile.mkdirs();
} else {
outItemFile.getParentFile().mkdirs();
outItemFile.createNewFile();
IOUtils.copy(zipIn, new FileOutputStream(outItemFile));
}
}
}
}
上述這段程式碼就會導致,壓縮包裡的檔案越多,所佔用的檔案控制程式碼就越多。
最終,只要再加一行程式碼就解決問題了:
try (FileOutputStream fos = new FileOutputStream(outItemFile)) {
IOUtils.copy(zipIn, fos);
}
根本原因就是apache的IOUtils.copy方法並不會主動關閉輸出流。
總結
- 常見的工具方法,能用現成的就用現成的,輪子可以自己慢慢刨析,私下裡學習研究重造;
- 要了解所使用的第三方工具方法,它們會不會影響入參的狀態,比如這裡的copy方法,會不會主動關閉輸入輸出流。