Python3 os.walk()函數導致buffer/cache占用過高問題處理


一、背景說明

os.walk()應該是當前python中遍歷目錄最推薦的函數,之前用python寫了一個用於收集系統用到的第三方組件的腳本,在測試時使用os.walk()遍歷了部分目錄,並通過了全網的測試。但在改成遍歷根目錄后,被業務反饋說腳本占用內存過高導致了內存告警。

在直觀感覺上,只遍歷目錄又不打開文件,應該只是相當於加載了一個目錄樹,不可能造成幾十G內存的上漲。但一方面內存上漲時間和腳本的時間是一致的,另一方面在殺除腳本后內存出現了下降。所以基本可以確定內存上漲確實和該腳本是有關系的。

經過反復的測試和觀察,總結出以下兩個現象:

  1. pythonos.walk和系統tree命令,只要文件一多,占用的buffer/cache就會明顯上漲。
  2. find命令,如果/proc目錄文件一多,占用的buffer/cache也會明顯上漲。

將該結論反饋給技術大佬,他分析之后給出這兩個現象的更根本原因:

  1. pythonos.walk和系統tree命令在遍歷目錄時除了加載目錄樹還會加載文件的stat信息,所以文件一多就會占用很多buffer/cache
  2. 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)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM