一、背景說明
os.walk()應該是當前python中遍歷目錄最推薦的函數,之前用python寫了一個用於收集系統用到的第三方組件的腳本,在測試時使用os.walk()遍歷了部分目錄,並通過了全網的測試。但在改成遍歷根目錄后,被業務反饋說腳本占用內存過高導致了內存告警。
在直觀感覺上,只遍歷目錄又不打開文件,應該只是相當於加載了一個目錄樹,不可能造成幾十G內存的上漲。但一方面內存上漲時間和腳本的時間是一致的,另一方面在殺除腳本后內存出現了下降。所以基本可以確定內存上漲確實和該腳本是有關系的。
經過反復的測試和觀察,總結出以下兩個現象:
- python的os.walk和系統tree命令,只要文件一多,占用的buffer/cache就會明顯上漲。
- find命令,如果/proc目錄文件一多,占用的buffer/cache也會明顯上漲。
將該結論反饋給技術大佬,他分析之后給出這兩個現象的更根本原因:
- python的os.walk和系統tree命令在遍歷目錄時除了加載目錄樹還會加載文件的stat信息,所以文件一多就會占用很多buffer/cache。
- find在遍歷其他目錄時只加載目錄樹不加域stat信息,所以不明顯占用buffer/cache;但在遍歷/proc時也會加載stat信息,所以/proc文件一多也會導致buffer/cache上漲。其實只是簡單根據文件名的find不加載stat信息,如果根據日期等條件去find還是要加載stat信息。
另外對於由於目錄問題導致的buffer/cache上漲,可使用以下命令進行清理:
sync; echo 2 > /proc/sys/vm/drop_caches
參考:https://www.tecmint.com/clear-ram-memory-cache-buffer-and-swap-space-on-linux/
二、問題處理
所以buffer/cache漲不漲的兩個因素已經很明顯了:文件數量和加不加載文件stat信息。
回到我們最初的目標收集所有第三方組件,這必然要求遍歷整個磁盤,所以文件數量是不可限制的,所以只能想辦法不加載文件的stat信息。不加載stat信息到現在看只好用普通的find命令,但這不是python原生的做法而且限制比較大。后來技術大佬看了find源碼,仿照寫了個不加stat信息的函數。
import os import pdb from ctypes import CDLL, c_char_p, c_int, c_long, c_ushort, c_byte, c_char, Structure, POINTER from ctypes.util import find_library class c_dir(Structure): """Opaque type for directory entries, corresponds to struct DIR""" pass c_dir_p = POINTER(c_dir) class c_dirent(Structure): """Directory entry""" # FIXME not sure these are the exactly correct types! _fields_ = ( ('d_ino', c_long), # inode number ('d_off', c_long), # offset to the next dirent ('d_reclen', c_ushort), # length of this record ('d_type', c_byte), # type of file; not supported by all file system types ('d_name', c_char * 4096) # filename ) c_dirent_p = POINTER(c_dirent) c_lib = CDLL(find_library("c")) opendir = c_lib.opendir opendir.argtypes = [c_char_p] opendir.restype = c_dir_p # FIXME Should probably use readdir_r here readdir = c_lib.readdir readdir.argtypes = [c_dir_p] readdir.restype = c_dirent_p closedir = c_lib.closedir closedir.argtypes = [c_dir_p] closedir.restype = c_int DT_FIFO = 1 DT_CHR = 2 DT_DIR = 4 DT_BLK = 6 DT_REG = 8 DT_LNK = 10 DT_SOCK = 12 DT_WHT = 14 def listdir(path): """ A generator to return the names of files in the directory passed in """ dir_p = opendir(path) try: while True: p = readdir(dir_p) if not p: break name = p.contents.d_name if name not in (".", ".."): yield name, p.contents.d_type finally: closedir(dir_p) def _traversal_path(name, parent, res_array, follow_link=False, ): if not os.path.exists(name): return cur = os.path.join(parent, name) if not os.path.isdir(name): res_array.append(cur) elif cur in dir_white_list: return else: for cn, ct in listdir(name): if ct & DT_DIR != DT_DIR: res_array.append(os.path.join(cur, cn)) elif not follow_link and ( ct & DT_LNK == DT_LNK ): res_array.append(os.path.join(cur, cn)) else: os.chdir(name) _traversal_path(cn, cur, res_array, follow_link) os.chdir("..") def traversal_path(path, follow_link=False): # pdb.set_trace() files = [] name = os.path.basename(path) parent = os.path.dirname(path) if name == "": name = parent parent = "." cur = os.curdir os.chdir(parent) _traversal_path(name, parent, files, follow_link) os.chdir(cur) return files # 白名單目錄 # 其實做了不加載stat信息處理,所以即便遍歷/proc預期上也不會導致buffer/cache上漲 # 但一般這些目錄都是系統目錄,尤其是/proc文件系統還比較復雜,所以我們直接略過省時省心 dir_white_list = ["/proc", "/sys", "/dev", "/boot"] if __name__ == '__main__': for f in traversal_path("/"): print(f)