BYPASS SHELL BY ./RAZORGANZ
Server: nginx/1.20.1
System: Linux iZdzfnv9mwfppeZ 5.10.134-19.2.al8.x86_64 #1 SMP Wed Oct 29 22:47:09 CST 2025 x86_64
User: apache (48)
PHP: 8.2.30
Disabled: NONE
Upload Files
File: //usr/local/sysak/.sysak_components/tools/plugin_cache.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import json
import tempfile
from plugin_system import *
import mem_utils


@plugin(name="cache")
@plugin_requires("taskProc", "meminfo")
@plugin_pod_support()
class CachePlugin(PluginBase):
    """Cache 插件,用于扫描系统 Cache 的使用情况

    启用条件:
        - 参数带 -f

    输出:
    1. filecache: 文件 Cache 使用情况
        节点:
            [
                ["/tmp/test", {"cached": 1024, "size": 1024, "task": ["systemd(1)"]}],
                ...
            ]
        pod:
            {
                "container1": [
                    ["/tmp/test", {"cached": 1024, "size": 1024, "task": ["systemd(1)"]}],
                    ...
                ],
                "container2": [
                    ["/tmp/test", {"cached": 1024, "size": 1024, "task": ["systemd(1)"]}],
                    ...
                ]]
            }
    2. filecache_shm: 共享内存文件 Cache 使用情况
        节点:
            [
                ["/dev/shm/test", {"cached": 1024, "size": 1024, "task": ["systemd(1)"], "shmem_kind": "Shmem"}],
                ...
            ]
        Pod:
            {
                "container1": [
                    ["/dev/shm/test", {"cached": 1024, "size": 1024, "task": ["systemd(1)"], "shmem_kind": "Shmem"}],
                    ...
                ],
                "container2": [
                    ["/dev/shm/test", {"cached": 1024, "size": 1024, "task": ["systemd(1)"], "shmem_kind": "Shmem"}],
            }
    """

    def get_cache_file_pid(self, filePidMap, taskInfo, filename):
        res = []
        if filename not in filePidMap:
            return res
        for pid in filePidMap[filename]:
            if pid not in taskInfo:
                continue
            name = taskInfo[pid]["Name"]
            res.append("{}({})".format(name, pid))
        return res

    def get_file_cache_by_mincore(self, filePidMap, taskInfo):
        """通过 mincore 系统调用来计算文件的缓存大小。"""
        collect_info = PluginCollectInfo()
        topN = self.params.get("topN", 30)
        filecache_map, total_cache = {}, 0
        for file in filePidMap.keys():
            cache = mem_utils.cal_cache_size(file)
            total_cache += cache
            if cache > 0:
                filecache_map[file] = cache
        sorted_filecache = sorted(
            filecache_map.items(), key=lambda kv: (kv[1], kv[0]), reverse=True
        )
        filecache = []
        count = 0
        for k, v in sorted_filecache:
            filecache.append(
                {
                    "file": k,
                    "cached": int(v) * 4,
                    "size": int(v) * 4,
                    "task": self.get_cache_file_pid(filePidMap, taskInfo, k),
                },
            )
            count += 1
            if count >= topN:
                break
        filecache = {
            "filecache": filecache,
        }
        collect_info.store("host", filecache)
        return collect_info

    def get_file_cache_by_podmem(self, context, enable_pod=False):
        """通过 podmem 工具扫描系统的文件 Cache 使用情况"""

        def extract_real_file_path(file):
            """
            Pod 模式下,尝试还原容器的真实路径
            /ostree/deploy/LifseaOS/deploy/9401830260e19a6e0c84aad1f3ee493dc634ca208b669dff63159b41e366ce28.0/var/ostree/deploy/LifseaOS/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/29/fs/var/log/ntgh_data.log
                ↓
            /var/log/ntgh_data.log
            Args:
                item (_type_): _description_
            """
            overly_index = file.find(
                "/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/"
            )
            if overly_index != -1:
                fs_index = file.find("/fs/", overly_index)
                if fs_index != -1:
                    return file[fs_index + 3 :]
            return file

        def convert_podmem_file_cache_item(item, filePidMap, taskInfo):
            file = item.get("file", "")
            if enable_pod:
                file = extract_real_file_path(file)
            shmem = int(item.get("shmem", -1))
            shmem_kind = item.get("shmem_kind", "").strip()
            if shmem > 0:
                return {
                    "file": file,
                    "cached": item.get("cached", 0),
                    "size": item.get("size", 0),
                    "task": self.get_cache_file_pid(filePidMap, taskInfo, file),
                    "shmem_kind": shmem_kind,
                }
            else:
                return {
                    "file": file,
                    "cached": item.get("cached", 0),
                    "size": item.get("size", 0),
                    "task": self.get_cache_file_pid(filePidMap, taskInfo, file),
                }

        def get_one_file_cache(filecache_ins, filePidMap, taskInfo):
            filecache = []
            filecache_shm = []

            if "filecache_file" in filecache_ins and "shmem_file" in filecache_ins:
                # V2 版本,支持 cache 和 shmem 单独统计排序
                cache = filecache_ins["filecache_file"]
                for item in cache:
                    filecache.append(
                        convert_podmem_file_cache_item(item, filePidMap, taskInfo)
                    )
                shmem = filecache_ins["shmem_file"]
                for item in shmem:
                    filecache_shm.append(
                        convert_podmem_file_cache_item(item, filePidMap, taskInfo)
                    )
            elif "sort_file" in filecache_ins:
                # V1 版本,只支持 cache 和 shmem 放在一起排序
                system_file_cache_top = filecache_ins["sort_file"]
                for idx, item in enumerate(system_file_cache_top):
                    shmem = int(item.get("shmem", -1))
                    if idx < 30:
                        filecache.append(
                            convert_podmem_file_cache_item(item, filePidMap, taskInfo)
                        )
                    if shmem > 0:
                        filecache_shm.append(
                            convert_podmem_file_cache_item(item, filePidMap, taskInfo)
                        )

            return filecache, filecache_shm

        collect_info = PluginCollectInfo()
        podmem_path = os.getenv("PODMEM_PATH", None)
        if not podmem_path:
            script_dir = os.path.dirname(os.path.abspath(__file__))
            podmem_path = os.path.join(script_dir, "podmem")

        mem_total = 0
        with open(mem_utils.get_real_path("/proc/meminfo"), "r") as f:
            lines = f.readlines()
            for line in lines:
                items = line.strip().split()
                if len(items) < 3:
                    continue
                name = items[0].strip()[:-1]
                size = int(items[1].strip())
                if name == "MemTotal":
                    mem_total = size
                    break

        with tempfile.NamedTemporaryFile() as tmpfile:
            pod_name = ""
            scan_rate = "100"
            if mem_total > 32 * 1024 * 1024 * 1024:
                scan_rate = "500"
            if mem_total > 64 * 1024 * 1024 * 1024:
                scan_rate = "1000"

            # 使用 podmem 工具扫描 filecache 和 shmem
            if enable_pod:
                pod_name = context.get_collect_info("podDetect").load("pod_name")
                cmd = [
                    podmem_path,
                    "-r",
                    scan_rate,
                    "-t",
                    "30",
                    "-j",
                    tmpfile.name,
                    "-p",
                    pod_name,
                ]
            else:
                cmd = [
                    podmem_path,
                    "-r",
                    scan_rate,
                    "-t",
                    "30",
                    "-j",
                    tmpfile.name,
                    "-s",
                ]

            # run podmem, set timeout to 180s
            code, result, error = mem_utils.run_cmd(cmd, timeout=180)
            if code != 0:
                raise Exception("Failed to run podmem %s" % error)
            with open(tmpfile.name, "r") as f:
                data = json.loads(f.read())
                if not enable_pod:
                    # host mode
                    task_proc_collect_info = context.get_collect_info("taskProc")
                    taskInfo = task_proc_collect_info.load("taskInfo")
                    filePidMap = task_proc_collect_info.load("filePidMap")
                    system_filecache = data.get("data", {}).get("system", {})[0]
                    filecache_res, filecache_shm_res = get_one_file_cache(
                        system_filecache, filePidMap, taskInfo
                    )
                    collect_info.store(
                        "host",
                        {
                            "filecache": filecache_res,
                            "filecache_shm": filecache_shm_res,
                        },
                    )
                    return collect_info

                # pod mode
                pod_filecache = data.get("data", {}).get(pod_name, {})
                for cons_filecache in pod_filecache:
                    taskInfo = {}
                    filePidMap = {}
                    cname = cons_filecache.get("cname", "")
                    task_proc_collect_info = context.get_collect_info("taskProc")
                    if task_proc_collect_info.exists(cname):
                        taskInfo = task_proc_collect_info.load(cname).get(
                            "taskInfo", {}
                        )
                        filePidMap = task_proc_collect_info.load(cname).get(
                            "filePidMap", {}
                        )
                    con_filecache, cons_filecache_shm = get_one_file_cache(
                        cons_filecache, filePidMap, taskInfo
                    )

                    collect_info.store(
                        cname,
                        {
                            "filecache": con_filecache,
                            "filecache_shm": cons_filecache_shm,
                        },
                    )

            return collect_info

    def should_scan_cache(self, context, enable_pod):
        """判断是否需要扫描缓存,只有当 filecache + shmem 占用总使用内存的 30% 以上才扫描
        
        Args:
            context: 插件上下文
            enable_pod: 是否为 pod 模式
            
        Returns:
            bool: True 表示需要扫描,False 表示不需要扫描
        """
        CACHE_THRESHOLD = 0.30  # 30% 阈值
        
        try:
            meminfo_collect = context.get_collect_info("meminfo")
            if meminfo_collect is None:
                # 如果 meminfo 插件未运行,默认执行扫描
                return True
            
            if enable_pod:
                # Pod 模式:检查 pod 的内存信息
                pod_name = context.get_collect_info("podDetect").load("pod_name")
                if not meminfo_collect.exists(pod_name):
                    # 如果没有 pod 内存信息,默认执行扫描
                    if mem_utils.is_debug:
                        print("[Cache] Pod %s: no meminfo found, will scan cache" % pod_name)
                    return True
                    
                pod_meminfo = meminfo_collect.load(pod_name)
                cache = pod_meminfo.get("cache", 0)  # filecache
                shmem = pod_meminfo.get("shmem", 0)  # shmem
                usage = pod_meminfo.get("usage", 0)  # 总使用内存
                
                if usage <= 0:
                    # 使用内存为 0,不需要扫描
                    if mem_utils.is_debug:
                        print("[Cache] Pod %s: usage=0, skip cache scan" % pod_name)
                    return False
                    
                cache_ratio = float(cache + shmem) / usage
                if mem_utils.is_debug:
                    print(
                        "[Cache] Pod %s: cache=%s, shmem=%s, usage=%s, "
                        "ratio=%.2f%%, threshold=%.2f%%" % (
                            pod_name, cache, shmem, usage,
                            cache_ratio * 100, CACHE_THRESHOLD * 100
                        )
                    )
                
                return cache_ratio >= CACHE_THRESHOLD
            else:
                # Host 模式:检查主机内存信息
                cached = meminfo_collect.load("Cached")  # KB
                buffers = meminfo_collect.load("Buffers")  # KB
                shmem = meminfo_collect.load("Shmem")  # KB
                mem_total = meminfo_collect.load("MemTotal")  # KB
                mem_free = meminfo_collect.load("MemFree")  # KB
                
                # 计算总使用内存
                mem_used = mem_total - mem_free
                if mem_used <= 0:
                    # 使用内存为 0,不需要扫描
                    if mem_utils.is_debug:
                        print("[Cache] Host: mem_used=0, skip cache scan")
                    return False
                
                # filecache = Cached + Buffers, shmem = Shmem
                filecache = cached + buffers
                cache_ratio = float(filecache + shmem) / mem_used
                
                if mem_utils.is_debug:
                    print(
                        "[Cache] Host: filecache=%sKB, shmem=%sKB, mem_used=%sKB, "
                        "ratio=%.2f%%, threshold=%.2f%%" % (
                            filecache, shmem, mem_used,
                            cache_ratio * 100, CACHE_THRESHOLD * 100
                        )
                    )
                
                return cache_ratio >= CACHE_THRESHOLD
                
        except Exception as e:
            mem_utils.log_exception_in_debug_mode(e)
            # 如果判断失败,默认执行扫描
            if mem_utils.is_debug:
                print("[Cache] Error checking cache ratio, will scan cache by default")
            return True

    def collect(self, context):
        enable_pod = self.params.get("enable_pod", False)
        res = PluginCollectInfo()
        
        # 判断是否需要扫描缓存
        if not self.should_scan_cache(context, enable_pod):
            if mem_utils.is_debug:
                print("[Cache] Cache ratio is below 30% threshold, skipping podmem scan")
            return res
        
        try:
            res = self.get_file_cache_by_podmem(context=context, enable_pod=enable_pod)
        except Exception as e:
            mem_utils.log_exception_in_debug_mode(e)
            # if podmem failed and enable_pod is True, return empty result
            if enable_pod:
                return res
            task_proc_collect_info = context.get_collect_info("taskProc")
            taskInfo = task_proc_collect_info.load("taskInfo")
            filePidMap = task_proc_collect_info.load("filePidMap")
            res = self.get_file_cache_by_mincore(filePidMap, taskInfo)

        return res

    def analysis_one_table(self, filecache, filecache_shm, topN, cname=None):
        # print file cache
        title = u"文件 Cache Top %s" % topN
        if cname:
            title = u"容器 %s " % cname + title
        headers = ["文件名", "缓存大小", "关联任务"]
        data = []
        max_col_lengths = [80, 30, 30]
        res_str = ""
        for value in filecache:
            data.append(
                [
                    value["file"],
                    mem_utils.format_unit(value["cached"]),
                    ",".join(value["task"]),
                ]
            )
        res_str += mem_utils.print_table(
            headers, data, title, max_col_lengths, show=False
        )

        # print file cache shm
        title = u"共享内存文件 Cache Top %s" % topN
        if cname:
            title = u"容器 %s" % cname + title
        headers = ["文件名", "缓存大小", "共享内存类型", "关联任务"]
        max_col_lengths = [80, 30, 30, 30]
        data = []
        for value in filecache_shm:
            data.append(
                [
                    value["file"],
                    mem_utils.format_unit(value["cached"]),
                    value["shmem_kind"],
                    ",".join(value["task"]),
                ]
            )
        res_str += mem_utils.print_table(
            headers, data, title, max_col_lengths, show=False
        )

        return res_str

    def analysis_pod(self, cache_info, topN):
        analysis_info = PluginAnalysisInfo()
        analysis_info.title = "Cache"
        res_str = ""
        for cname, value in cache_info.items():
            filecache = value.get("filecache", [])
            filecache_shm = value.get("filecache_shm", [])
            analysis_info.result[cname] = {
                "filecache": filecache,
                "filecache_shm": filecache_shm,
            }
            res_str += self.analysis_one_table(filecache, filecache_shm, topN, cname)
        analysis_info.render_text = res_str
        return analysis_info

    def analysis_host(self, cache_info, topN):
        analysis_info = PluginAnalysisInfo()
        analysis_info.title = "Cache"
        filecache = cache_info.get("filecache", [])
        filecache_shm = cache_info.get("filecache_shm", [])
        res_str = self.analysis_one_table(filecache, filecache_shm, topN)
        analysis_info.result["filecache"] = filecache
        analysis_info.result["filecache_shm"] = filecache_shm
        analysis_info.render_text = res_str
        return analysis_info

    def analysis(self, context):
        topN = self.params.get("topN", 30)
        enable_pod = self.params.get("enable_pod", False)
        cache_collect_info = context.get_collect_info("cache")
        
        # 如果 collect 阶段跳过了扫描(缓存占比低于阈值),返回空的分析结果
        if enable_pod:
            if not cache_collect_info.data:
                # Pod 模式下没有数据,返回空结果
                analysis_info = PluginAnalysisInfo()
                analysis_info.title = "Cache"
                return analysis_info
            return self.analysis_pod(cache_collect_info.data, topN)
        else:
            host_data = cache_collect_info.load("host")
            if host_data is None:
                # Host 模式下没有数据,返回空结果
                analysis_info = PluginAnalysisInfo()
                analysis_info.title = "Cache"
                return analysis_info
            return self.analysis_host(host_data, topN)