| #!/usr/bin/python3 |
| |
| import os, re, sys, gzip |
| from collections import defaultdict |
| from cpunoise_lib import (parse_cpulist, get_cmdline_param, read_file, |
| read_int, read_cpulist, safe_parse_cpulist, |
| get_nohz_full_cpus,to_str_range) |
| from log_output import Log |
| |
| # max number of items (IRQ vectors, workqueues, ...) listed in a single message |
| MAX_LISTED = 12 |
| |
| # deepest C-state exit latency (us) tolerated on a nohz_full CPU |
| CSTATE_LATENCY_WARN_US = 100 |
| |
| # vm.stat_interval (s) below which vmstat_update is a relevant noise source |
| STAT_INTERVAL_WARN_S = 10 |
| |
| # isolcpus= accepts a list of flags before the CPU list |
| ISOLCPUS_FLAGS = ("nohz", "domain", "managed_irq") |
| |
| WQ_ROOT = "/sys/devices/virtual/workqueue" |
| |
| def parse_cpumask(mask): |
| # "ffff" and "0000ffff,ffffffff" are both valid hex cpumasks |
| if mask is None: |
| return [] |
| |
| bits = mask.replace(",", "").strip() |
| if not bits: |
| return [] |
| |
| try: |
| value = int(bits, 16) |
| except ValueError: |
| return [] |
| |
| return [cpu for cpu in range(value.bit_length()) if value >> cpu & 1] |
| |
| def read_cpumask(path): |
| return parse_cpumask(read_file(path)) |
| |
| def expand_cpu_spec(spec): |
| # a kernel cmdline CPU spec also accepts the "all" keyword |
| if spec is None: |
| return None |
| |
| spec = spec.strip() |
| if spec == "all": |
| return get_online_cpus() |
| |
| return safe_parse_cpulist(spec) |
| |
| def has_cmdline_flag(flag): |
| cmdline = read_file("/proc/cmdline") |
| if cmdline is None: |
| return False |
| |
| return flag in cmdline.split() |
| |
| def get_online_cpus(): |
| return read_cpulist("/sys/devices/system/cpu/online") |
| |
| def intersect(a, b): |
| return sorted(set(a) & set(b)) |
| |
| def truncate(items): |
| items = list(items) |
| if len(items) <= MAX_LISTED: |
| return ", ".join(items) |
| |
| return "%s, ... +%d more" % (", ".join(items[:MAX_LISTED]), len(items) - MAX_LISTED) |
| |
| def get_running_procs(prefixes): |
| found = [] |
| |
| for entry in os.listdir("/proc"): |
| if not entry.isdigit(): |
| continue |
| |
| comm = read_file("/proc/%s/comm" % entry) |
| if comm and comm.startswith(prefixes): |
| found.append((entry, comm)) |
| |
| return found |
| |
| def get_task_affinity(pid): |
| status = read_file("/proc/%s/status" % pid) |
| if status is None: |
| return None |
| |
| s = re.search(r'^Cpus_allowed_list:\s*(\S+)', status, re.M) |
| if s is None: |
| return None |
| |
| return safe_parse_cpulist(s.group(1)) |
| |
| _kernel_config = None |
| |
| def get_kernel_config(): |
| global _kernel_config |
| |
| if _kernel_config is not None: |
| return _kernel_config |
| |
| _kernel_config = {} |
| data = None |
| |
| if os.path.exists("/proc/config.gz"): |
| try: |
| with gzip.open("/proc/config.gz", "rt") as f: |
| data = f.read() |
| except (OSError, EOFError): |
| data = None |
| |
| if data is None: |
| release = os.uname().release |
| for path in ["/boot/config-%s" % release, "/lib/modules/%s/config" % release]: |
| data = read_file(path) |
| if data is not None: |
| break |
| |
| if data is None: |
| return _kernel_config |
| |
| for line in data.splitlines(): |
| line = line.strip() |
| if not line or line.startswith("#") or "=" not in line: |
| continue |
| |
| key, value = line.split("=", 1) |
| _kernel_config[key] = value.strip('"') |
| |
| return _kernel_config |
| |
| REQUIRED_CONFIGS = [ |
| ("CONFIG_NO_HZ_FULL", "full dynticks support"), |
| ("CONFIG_HIGH_RES_TIMERS", "high resolution timers (needed by nohz_full)"), |
| ("CONFIG_RCU_NOCB_CPU", "RCU callback offloading"), |
| ("CONFIG_CPU_ISOLATION", "isolcpus / housekeeping CPU support"), |
| ] |
| |
| PREEMPT_MODELS = [ |
| "CONFIG_PREEMPT_RT", |
| "CONFIG_PREEMPT", |
| "CONFIG_PREEMPT_VOLUNTARY", |
| "CONFIG_PREEMPT_NONE", |
| ] |
| |
| def get_thread_siblings_list(): |
| thread_siblings_list = {} |
| seen_cpus = set() |
| |
| for entry in sorted(os.listdir("/sys/devices/system/cpu/")): |
| if not re.match(r'^cpu\d+$', entry): |
| continue |
| |
| cpu = int(entry[3:]) |
| if cpu in seen_cpus: |
| continue |
| |
| siblings = read_cpulist(f"/sys/devices/system/cpu/{entry}/topology/thread_siblings_list") |
| |
| seen_cpus.update(siblings) |
| thread_siblings_list[cpu] = siblings |
| |
| return thread_siblings_list |
| |
| def check_siblings_thread(cpulist): |
| Log.header("SIBLINGS THREAD <=> NOHZ_FULL") |
| |
| smt_path = "/sys/devices/system/cpu/smt/active" |
| smt = read_int(smt_path) |
| |
| if smt is None: |
| Log.print(Log.Status.FAIL, "SMT_CHECK", f"Can't find {smt_path}") |
| return |
| |
| Log.print(Log.Status.INFO, "SMT_NOISE", "Non-isolated cores can cause noise. If both are isolated, they can interfere.") |
| |
| siblings_list = [] |
| |
| if smt: |
| Log.print(Log.Status.WARN, "SMT_CHECK", "SMT is enabled. On nohz_full with isolated CPU it should be disabled.") |
| siblings_list = get_thread_siblings_list() |
| else: |
| Log.print(Log.Status.OK, "SMT_CHECK", "SMT is disabled.") |
| |
| # none of the siblings should be in cpulist |
| for hw_thread in siblings_list: |
| siblings_nohz_intersect = intersect(siblings_list[hw_thread], cpulist) |
| |
| if siblings_nohz_intersect: |
| Log.print(Log.Status.WARN, "SMT_ISOLATION", f"SMT threads should NOT be isolated: {siblings_list[hw_thread]} | found: {siblings_nohz_intersect}") |
| |
| def check_cpu_governor(cpulist): |
| Log.header("CPU FREQUENCY / IDLE") |
| |
| # CPU0 is a housekeeping CPU: its governor says nothing about the isolated |
| # ones, so report the nohz_full CPUs when we know them |
| cpus = sorted(cpulist) if cpulist else [0] |
| |
| governors = defaultdict(set) |
| drivers = set() |
| missing = [] |
| |
| for cpu in cpus: |
| governor = read_file(f"/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_governor") |
| if governor is None: |
| missing.append(cpu) |
| continue |
| |
| governors[governor].add(cpu) |
| |
| driver = read_file(f"/sys/devices/system/cpu/cpu{cpu}/cpufreq/scaling_driver") |
| if driver: |
| drivers.add(driver) |
| |
| if missing: |
| Log.print(Log.Status.WARN, "CPU_GOVERNOR", |
| f"No cpufreq sysfs for CPUs {to_str_range(missing)} (driver missing or BIOS controlled)") |
| |
| for governor, gov_cpus in governors.items(): |
| Log.print(Log.Status.INFO, "CPU_GOVERNOR", f"CPUs {to_str_range(gov_cpus)}: governor '{governor}'") |
| |
| if governor != "performance": |
| Log.print(Log.Status.WARN, "CPU_GOVERNOR_POLICY", |
| f"CPUs {to_str_range(gov_cpus)}: governor '{governor}' allows frequency " |
| "transitions, a jitter source. 'performance' is recommended.") |
| |
| if len(governors) > 1: |
| Log.print(Log.Status.WARN, "CPU_GOVERNOR_MIX", "nohz_full CPUs do not share the same governor") |
| |
| if drivers: |
| Log.print(Log.Status.INFO, "CPU_FREQ_DRIVER", f"scaling driver: {', '.join(sorted(drivers))}") |
| |
| check_turbo_boost() |
| check_cpu_idle(cpus) |
| |
| # no_turbo and boost use opposite polarity: no_turbo=1 means turbo is OFF, |
| # boost=1 means it's ON. Only one of the two exists, depending on the driver. |
| def check_turbo_boost(): |
| no_turbo = read_int("/sys/devices/system/cpu/intel_pstate/no_turbo") |
| if no_turbo is not None: |
| if no_turbo: |
| Log.print(Log.Status.OK, "CPU_TURBO", "intel_pstate/no_turbo=1: turbo disabled. Ok!") |
| else: |
| Log.print(Log.Status.WARN, "CPU_TURBO", |
| "intel_pstate/no_turbo=0: turbo boost is enabled, a source of " |
| "frequency-transition jitter.") |
| return |
| |
| boost = read_int("/sys/devices/system/cpu/cpufreq/boost") |
| if boost is None: |
| return |
| |
| if boost: |
| Log.print(Log.Status.WARN, "CPU_TURBO", |
| "cpufreq/boost=1: turbo boost is enabled, a source of frequency-transition jitter.") |
| else: |
| Log.print(Log.Status.OK, "CPU_TURBO", "cpufreq/boost=0: turbo disabled. Ok!") |
| |
| def get_cpu_idle_states(cpu): |
| path = f"/sys/devices/system/cpu/cpu{cpu}/cpuidle" |
| states = [] |
| |
| if not os.path.isdir(path): |
| return states |
| |
| for state in sorted(os.listdir(path)): |
| if not re.match(r'^state\d+$', state): |
| continue |
| |
| name = read_file(os.path.join(path, state, "name")) |
| latency = read_int(os.path.join(path, state, "latency")) |
| |
| if name is None or read_int(os.path.join(path, state, "disable")): |
| continue |
| |
| states.append((name, latency if latency is not None else 0)) |
| |
| return states |
| |
| def check_cpu_idle(cpus): |
| if has_cmdline_flag("idle=poll"): |
| Log.print(Log.Status.INFO, "CPU_IDLE", "idle=poll set: CPUs never enter an idle state") |
| |
| # group the CPUs sharing the same idle configuration to keep the output short |
| signatures = defaultdict(set) |
| |
| for cpu in cpus: |
| states = get_cpu_idle_states(cpu) |
| if states: |
| signatures[tuple(states)].add(cpu) |
| |
| for states, state_cpus in signatures.items(): |
| desc = ", ".join(f"{name}({latency}us)" for name, latency in states) |
| deepest = max(latency for _, latency in states) |
| too_deep = deepest > CSTATE_LATENCY_WARN_US |
| |
| status = Log.Status.WARN if too_deep else Log.Status.INFO |
| Log.print(status, "CPU_IDLE_STATES", f"CPUs {to_str_range(state_cpus)}: enabled C-states: {desc}") |
| |
| if too_deep: |
| Log.print(Log.Status.WARN, "CPU_IDLE_LATENCY", |
| f"Deepest enabled C-state costs {deepest}us to exit: it adds jitter on isolated CPUs") |
| |
| # current global PM QoS target: reading it registers no request |
| #try: |
| # with open("/dev/cpu_dma_latency", "rb") as f: |
| # latency = int.from_bytes(f.read(4), sys.byteorder) |
| # Log.print(Log.Status.INFO, "CPU_DMA_LATENCY", f"PM QoS cpu_dma_latency target: {latency}us") |
| #except (OSError, ValueError): |
| # pass |
| |
| def get_irq_name(vec): |
| path = "/proc/irq/%s" % vec |
| |
| try: |
| names = [e for e in os.listdir(path) if os.path.isdir(os.path.join(path, e))] |
| except OSError: |
| return None |
| |
| return names[0] if names else None |
| |
| def describe_vectors(vectors): |
| desc = [] |
| |
| for vec in sorted(vectors, key=lambda v: int(v) if v.isdigit() else -1): |
| name = get_irq_name(vec) |
| desc.append(f"{vec} ({name})" if name else vec) |
| |
| return truncate(desc) |
| |
| def get_irq_nohzfull_intersect(cpulist, attr="smp_affinity_list"): |
| smp_list = defaultdict(set) |
| |
| for vec in os.listdir("/proc/irq"): |
| path = "/proc/irq/%s" % vec |
| if not os.path.isdir(path) or vec == "0": |
| continue |
| |
| cpus = intersect(read_cpulist(os.path.join(path, attr)), cpulist) |
| if cpus: |
| smp_list[frozenset(cpus)].add(vec) |
| |
| return smp_list |
| |
| def check_irq_affinity(cpulist): |
| Log.header("IRQ AFFINITY ON NOHZ_FULL CPUs") |
| |
| smp_list = get_irq_nohzfull_intersect(cpulist, "smp_affinity_list") |
| |
| for cpus, vectors in smp_list.items(): |
| Log.print(Log.Status.FAIL, "IRQ_AFFINITY", |
| f"NOHZ CPUS {to_str_range(cpus)} not isolated from IRQs: {describe_vectors(vectors)}") |
| |
| # smp_affinity_list is only the *requested* mask: managed interrupts |
| # (per-queue NVMe/NIC vectors) ignore it, so they look clean there while |
| # still firing on an isolated CPU. effective_affinity_list is the real one. |
| requested = set().union(*smp_list.values()) |
| managed = {} |
| |
| for cpus, vectors in get_irq_nohzfull_intersect(cpulist, "effective_affinity_list").items(): |
| only_effective = vectors - requested |
| if only_effective: |
| managed[cpus] = only_effective |
| |
| for cpus, vectors in managed.items(): |
| Log.print(Log.Status.FAIL, "IRQ_EFFECTIVE", |
| f"NOHZ CPUS {to_str_range(cpus)} are the effective target of IRQs: {to_str_range(vectors)}") |
| |
| if managed: |
| Log.print(Log.Status.INFO, "IRQ_EFFECTIVE_HINT", |
| "These are likely managed IRQs: they ignore smp_affinity_list. " |
| "Use isolcpus=managed_irq to keep them off the isolated CPUs.") |
| |
| default_mask = read_cpumask("/proc/irq/default_smp_affinity") |
| default_affinity = intersect(default_mask, cpulist) |
| |
| # irqaffinity= is the boot-time request; default_smp_affinity is the |
| # runtime state it should have produced |
| irqaffinity = get_cmdline_param("irqaffinity") |
| if irqaffinity is not None: |
| requested = safe_parse_cpulist(irqaffinity) |
| Log.print(Log.Status.INFO, "IRQAFFINITY_CMDLINE", f"irqaffinity={to_str_range(requested)} in /proc/cmdline") |
| if requested is not None and default_mask and sorted(requested) != sorted(default_mask): |
| Log.print(Log.Status.WARN, "IRQAFFINITY_MISMATCH", |
| f"cmdline irqaffinity={to_str_range(requested)} but /proc/irq/default_smp_affinity=" |
| f"{to_str_range(default_mask)}") |
| |
| if default_affinity: |
| Log.print(Log.Status.WARN, "IRQ_DEFAULT_AFFINITY", |
| f"/proc/irq/default_smp_affinity includes nohz_full CPUs {to_str_range(default_affinity)}: " |
| "new IRQs will be allowed on them") |
| |
| if not smp_list and not managed and not default_affinity: |
| Log.print(Log.Status.OK, "IRQ_AFFINITY", "No IRQ is routed to the nohz_full CPUs.") |
| |
| IRQBALANCE_CONFIGS = [ |
| "/etc/sysconfig/irqbalance", |
| "/etc/default/irqbalance", |
| "/etc/conf.d/irqbalance", |
| ] |
| |
| def get_irqbalance_banned(): |
| for path in IRQBALANCE_CONFIGS: |
| data = read_file(path) |
| if data is None: |
| continue |
| |
| s = re.search(r'^\s*IRQBALANCE_BANNED_CPULIST\s*=\s*"?([^"\n#]+)"?', data, re.M) |
| if s: |
| return safe_parse_cpulist(s.group(1).strip()), path |
| |
| s = re.search(r'^\s*IRQBALANCE_BANNED_CPUS\s*=\s*"?([^"\n#]+)"?', data, re.M) |
| if s: |
| return parse_cpumask(s.group(1).strip()), path |
| |
| return None, None |
| |
| def check_irqbalance(cpulist): |
| Log.header("IRQBALANCE") |
| |
| procs = get_running_procs(("irqbalance",)) |
| if not procs: |
| Log.print(Log.Status.OK, "IRQBALANCE", "irqbalance is not running.") |
| return |
| |
| pids = ", ".join(pid for pid, _ in procs) |
| Log.print(Log.Status.INFO, "IRQBALANCE", f"irqbalance is running (pid: {pids})") |
| |
| banned, path = get_irqbalance_banned() |
| |
| if banned is None: |
| Log.print(Log.Status.WARN, "IRQBALANCE_BANNED", |
| "No IRQBALANCE_BANNED_CPULIST / IRQBALANCE_BANNED_CPUS found: irqbalance " |
| "can move IRQs back onto the isolated CPUs at runtime.") |
| Log.print(Log.Status.INFO, "IRQBALANCE_HINT", |
| "Recent irqbalance versions read /sys/devices/system/cpu/isolated and " |
| "nohz_full on their own, but banning the CPUs explicitly is safer.") |
| return |
| |
| missing = sorted(set(cpulist) - set(banned)) |
| if missing: |
| Log.print(Log.Status.FAIL, "IRQBALANCE_BANNED", |
| f"{path}: nohz_full CPUs {to_str_range(missing)} are NOT banned from irqbalance") |
| else: |
| Log.print(Log.Status.OK, "IRQBALANCE_BANNED", |
| f"{path}: all nohz_full CPUs are banned from irqbalance. Ok!") |
| |
| def check_rcu_nocbs(cpulist): |
| Log.header("RCU CALLBACK OFFLOADING") |
| |
| default_all = get_kernel_config().get("CONFIG_RCU_NOCB_CPU_DEFAULT_ALL") == "y" |
| nocbs = expand_cpu_spec(get_cmdline_param("rcu_nocbs")) |
| |
| if nocbs is not None: |
| Log.print(Log.Status.INFO, "RCU_NOCBS", f"rcu_nocbs={to_str_range(nocbs)}") |
| elif default_all: |
| nocbs = get_online_cpus() |
| Log.print(Log.Status.INFO, "RCU_NOCBS", |
| "No rcu_nocbs= in cmdline but CONFIG_RCU_NOCB_CPU_DEFAULT_ALL=y") |
| else: |
| nocbs = [] |
| Log.print(Log.Status.WARN, "RCU_NOCBS", |
| "No rcu_nocbs= in /proc/cmdline. nohz_full implies callback offloading, " |
| "but setting it explicitly is recommended.") |
| |
| if nocbs: |
| missing = sorted(set(cpulist) - set(nocbs)) |
| if missing: |
| Log.print(Log.Status.FAIL, "RCU_NOCBS_COVERAGE", |
| f"nohz_full CPUs {to_str_range(missing)} are NOT in rcu_nocbs: RCU callbacks " |
| "will run there and keep the tick alive.") |
| else: |
| Log.print(Log.Status.OK, "RCU_NOCBS_COVERAGE", "All nohz_full CPUs offload RCU callbacks. Ok!") |
| |
| if has_cmdline_flag("rcu_nocb_poll"): |
| Log.print(Log.Status.INFO, "RCU_NOCB_POLL", |
| "rcu_nocb_poll set: offload kthreads poll instead of being woken by the isolated CPUs") |
| |
| # offloading is pointless if the offload kthreads can run back on the |
| # isolated CPUs |
| threads = get_running_procs(("rcuo",)) |
| if not threads: |
| Log.print(Log.Status.WARN, "RCU_OFFLOAD_THREADS", |
| "No rcuo* kthread found: RCU callback offloading looks inactive.") |
| return |
| |
| offenders = defaultdict(set) |
| |
| for pid, comm in threads: |
| affinity = get_task_affinity(pid) |
| if affinity is None: |
| continue |
| |
| cpus = intersect(affinity, cpulist) |
| if cpus: |
| offenders[frozenset(cpus)].add(comm) |
| |
| for cpus, comms in offenders.items(): |
| Log.print(Log.Status.FAIL, "RCU_OFFLOAD_AFFINITY", |
| f"RCU offload kthreads can run on nohz_full CPUs {to_str_range(cpus)}: {truncate(sorted(comms))}") |
| |
| if not offenders: |
| Log.print(Log.Status.OK, "RCU_OFFLOAD_AFFINITY", |
| "All rcuo* kthreads are pinned to housekeeping CPUs. Ok!") |
| |
| def check_workqueue_affinity(cpulist): |
| Log.header("WORKQUEUE AFFINITY") |
| |
| isolated_path = os.path.join(WQ_ROOT, "cpumask_isolated") |
| if os.path.exists(isolated_path): |
| wq_isolated = read_cpumask(isolated_path) |
| missing = sorted(set(cpulist) - set(wq_isolated)) |
| |
| if missing: |
| Log.print(Log.Status.WARN, "WQ_CPUMASK_ISOLATED", |
| f"nohz_full CPUs {to_str_range(missing)} are not in cpumask_isolated. " |
| "Use isolcpus=domain or workqueue.unbound_cpus= to exclude them.") |
| else: |
| Log.print(Log.Status.OK, "WQ_CPUMASK_ISOLATED", |
| f"Workqueue isolated cpumask: {to_str_range(wq_isolated)}") |
| |
| def check_watchdog(cpulist): |
| Log.header("WATCHDOG") |
| |
| if has_cmdline_flag("nowatchdog"): |
| Log.print(Log.Status.INFO, "WATCHDOG_CMDLINE", "nowatchdog set in /proc/cmdline") |
| |
| watchdog = read_int("/proc/sys/kernel/watchdog") |
| |
| if watchdog is None: |
| Log.print(Log.Status.WARN, "WATCHDOG", "Can't read /proc/sys/kernel/watchdog") |
| elif watchdog: |
| Log.print(Log.Status.INFO, "WATCHDOG", "The soft-lockup watchdog is enabled.") |
| else: |
| Log.print(Log.Status.OK, "WATCHDOG", "The soft-lockup watchdog is disabled. Ok!") |
| |
| if read_int("/proc/sys/kernel/nmi_watchdog"): |
| Log.print(Log.Status.INFO, "NMI_WATCHDOG", "nmi_watchdog is enabled.") |
| |
| if not watchdog: |
| return |
| |
| # the watchdog arms a per-CPU hrtimer and wakes the watchdog/N kthread: a |
| # guaranteed periodic tick source on every CPU it covers. It defaults to |
| # the housekeeping CPUs, but the sysctl can be widened back to include |
| # the isolated ones. |
| cpumask = read_cpulist("/proc/sys/kernel/watchdog_cpumask") |
| cpus = intersect(cpumask, cpulist) |
| |
| if cpus: |
| Log.print(Log.Status.WARN, "WATCHDOG_CPUMASK", |
| f"watchdog_cpumask includes nohz_full CPUs {to_str_range(cpus)}: the " |
| "soft-lockup hrtimer will keep ticking there") |
| else: |
| Log.print(Log.Status.OK, "WATCHDOG_CPUMASK", "watchdog_cpumask excludes the nohz_full CPUs. Ok!") |
| |
| def check_noise_knobs(cpulist): |
| Log.header("KERNEL NOISE KNOBS") |
| |
| # timers queued on an isolated CPU should be pulled to a housekeeping one |
| timer_migration = read_int("/proc/sys/kernel/timer_migration") |
| if timer_migration == 1: |
| Log.print(Log.Status.OK, "TIMER_MIGRATION", "kernel.timer_migration=1. Ok!") |
| elif timer_migration == 0: |
| Log.print(Log.Status.WARN, "TIMER_MIGRATION", |
| "kernel.timer_migration=0: timers will not be moved off the isolated CPUs") |
| |
| check_mce(cpulist) |
| check_clocksource() |
| |
| # NUMA balancing drives IPIs and page faults on the isolated CPUs |
| numa_balancing = read_int("/proc/sys/kernel/numa_balancing") |
| if numa_balancing: |
| Log.print(Log.Status.WARN, "NUMA_BALANCING", |
| "kernel.numa_balancing=1: it injects page faults and IPIs on isolated CPUs") |
| elif numa_balancing == 0: |
| Log.print(Log.Status.OK, "NUMA_BALANCING", "kernel.numa_balancing=0. Ok!") |
| |
| check_thp() |
| |
| def check_mce(cpulist): |
| # the MCE poll timer is a classic x86 nohz_full noise source |
| offenders = defaultdict(set) |
| found = False |
| |
| for cpu in sorted(cpulist): |
| interval = read_int(f"/sys/devices/system/machinecheck/machinecheck{cpu}/check_interval") |
| if interval is None: |
| continue |
| |
| found = True |
| if interval: |
| offenders[interval].add(cpu) |
| |
| if not found: |
| return |
| |
| for interval, cpus in offenders.items(): |
| Log.print(Log.Status.WARN, "MCE_CHECK_INTERVAL", |
| f"CPUs {to_str_range(cpus)}: machine check poll timer every {interval}s. " |
| "Set check_interval to 0 on the isolated CPUs.") |
| |
| if not offenders: |
| Log.print(Log.Status.OK, "MCE_CHECK_INTERVAL", "MCE polling is disabled on the nohz_full CPUs. Ok!") |
| |
| def check_clocksource(): |
| current = read_file("/sys/devices/system/clocksource/clocksource0/current_clocksource") |
| if current is None: |
| return |
| |
| if current == "tsc": |
| Log.print(Log.Status.OK, "CLOCKSOURCE", "Current clocksource: tsc. Ok!") |
| else: |
| available = read_file("/sys/devices/system/clocksource/clocksource0/available_clocksource") |
| Log.print(Log.Status.WARN, "CLOCKSOURCE", |
| f"Current clocksource is '{current}', not 'tsc': reading it is slower and " |
| f"adds jitter. Available: {available}") |
| |
| # the TSC watchdog periodically compares the TSC against a slower clocksource |
| tsc = get_cmdline_param("tsc") |
| if tsc: |
| Log.print(Log.Status.INFO, "TSC_PARAM", f"tsc={tsc} in /proc/cmdline") |
| else: |
| Log.print(Log.Status.INFO, "TSC_WATCHDOG", |
| "No tsc= in cmdline: consider tsc=nowatchdog to avoid the periodic " |
| "TSC watchdog checks.") |
| |
| def check_thp(): |
| thp = read_file("/sys/kernel/mm/transparent_hugepage/enabled") |
| if thp is None: |
| return |
| |
| if "[always]" in thp: |
| Log.print(Log.Status.WARN, "TRANSPARENT_HUGEPAGE", |
| f"transparent_hugepage={thp}: khugepaged causes TLB shootdown IPIs on " |
| "isolated CPUs. 'madvise' or 'never' is recommended.") |
| else: |
| Log.print(Log.Status.OK, "TRANSPARENT_HUGEPAGE", f"transparent_hugepage={thp}. Ok!") |
| |
| # unlike 'enabled', 'always' here defrags synchronously in the fault path |
| # itself: a much sharper stall than khugepaged's asynchronous background work |
| defrag = read_file("/sys/kernel/mm/transparent_hugepage/defrag") |
| if defrag is None: |
| return |
| |
| if "[always]" in defrag: |
| Log.print(Log.Status.WARN, "THP_DEFRAG", |
| f"transparent_hugepage/defrag={defrag}: page faults can trigger " |
| "synchronous compaction. 'madvise' or 'never' is recommended.") |
| else: |
| Log.print(Log.Status.OK, "THP_DEFRAG", f"transparent_hugepage/defrag={defrag}. Ok!") |
| |
| def check_net_rps_xps(cpulist): |
| Log.header("NETWORK RPS / XPS") |
| |
| net_root = "/sys/class/net" |
| if not os.path.isdir(net_root): |
| return |
| |
| offenders = defaultdict(set) |
| |
| for iface in sorted(os.listdir(net_root)): |
| queues = os.path.join(net_root, iface, "queues") |
| if iface == "lo" or not os.path.isdir(queues): |
| continue |
| |
| for queue in sorted(os.listdir(queues)): |
| if queue.startswith("rx-"): |
| mask_file = "rps_cpus" |
| elif queue.startswith("tx-"): |
| mask_file = "xps_cpus" |
| else: |
| continue |
| |
| cpus = intersect(read_cpumask(os.path.join(queues, queue, mask_file)), cpulist) |
| if cpus: |
| offenders[(iface, mask_file, frozenset(cpus))].add(queue) |
| |
| for (iface, mask_file, cpus), queues in offenders.items(): |
| Log.print(Log.Status.WARN, "NET_%s" % mask_file.upper(), |
| f"{iface}: {mask_file} steers packets to nohz_full CPUs {to_str_range(cpus)} " |
| f"({len(queues)} queue(s))") |
| |
| if not offenders: |
| Log.print(Log.Status.OK, "NET_RPS_XPS", "No RX/TX queue steers packets to the nohz_full CPUs. Ok!") |
| |
| def err_nohz_isolated(nohz_full, res): |
| Log.print(Log.Status.FAIL, "ISOLATION_MISMATCH", f"Not all nohz_full CPUs are isolated. nohz_full={nohz_full} | Found: {res}") |
| |
| def get_cgroup_version(): |
| if os.path.exists("/sys/fs/cgroup/cgroup.controllers"): |
| return 2 |
| if os.path.isdir("/sys/fs/cgroup/cpuset"): |
| return 1 |
| |
| return 0 |
| |
| def split_isolcpus(value): |
| # isolcpus= accepts "domain,managed_irq,2-11": the flags matter, keep them |
| flags = [] |
| cpu_tokens = [] |
| |
| for token in value.split(","): |
| token = token.strip() |
| if not token: |
| continue |
| |
| if token in ISOLCPUS_FLAGS: |
| flags.append(token) |
| else: |
| cpu_tokens.append(token) |
| |
| cpus = expand_cpu_spec(",".join(cpu_tokens)) if cpu_tokens else [] |
| |
| return flags, cpus or [] |
| |
| # root is '/sys/fs/cgroup/folder' |
| def is_cfs_quota_valid(root): |
| quota = read_file(os.path.join(root, "cpu.max")) |
| |
| if quota is None: |
| return Log.Status.OK, "CFS Quota not set, this is valid on nohz_full." |
| |
| if quota.split(None, 1)[0].isnumeric(): |
| return Log.Status.FAIL, "Found CFS quota on nohz_full %s. This is not valid." % quota |
| |
| return Log.Status.OK, "CFS quota '%s'. This is considered valid on nohz_full." % quota |
| |
| # root is '/sys/fs/cgroup/folder' |
| # |
| # Return the CPUs a cgroup v2 isolated partition is about. Whether they really |
| # are isolated depends on the partition state and is decided by the caller. |
| def partition_cpus(root, partition_state): |
| requested = read_cpulist(os.path.join(root, "cpuset.cpus")) |
| |
| # an invalid partition is demoted to a member: cpuset.cpus.effective then |
| # reports the parent CPUs instead of the ones we asked to isolate, so the |
| # requested list is the only meaningful one left to report |
| if partition_state.startswith("isolated invalid"): |
| return requested |
| |
| # .effective is the mask the kernel really granted: it accounts for the |
| # parent cpuset and for the offline CPUs. Old kernels may not expose it. |
| return read_cpulist(os.path.join(root, "cpuset.cpus.effective")) or requested |
| |
| # Return the nohz_full CPUs that the cgroup v2 isolated partitions are isolating |
| def check_cpuset_v2(cpulist): |
| covered = set() |
| unrelated = 0 |
| |
| for root, dirs, files in os.walk("/sys/fs/cgroup"): |
| if "cpuset.cpus.partition" not in files: |
| continue |
| |
| partition_state = read_file(os.path.join(root, "cpuset.cpus.partition")) |
| |
| if partition_state is None or not partition_state.startswith("isolated"): |
| continue |
| |
| cgroup_cpus = partition_cpus(root, partition_state) |
| |
| # nohz_full can be split across several partitions: every partition is |
| # checked against the nohz_full CPUs it holds, not against the whole |
| # list. The union is verified by the caller. |
| mine = intersect(cgroup_cpus, cpulist) |
| if not mine: |
| unrelated += 1 |
| continue |
| |
| if partition_state.startswith("isolated invalid"): |
| Log.print(Log.Status.FAIL, "CGROUP_PARTITION", f"Path: {root} | State: {partition_state}") |
| Log.print(Log.Status.FAIL, "CPUSET_MATCH", |
| f"cpuset: {to_str_range(cgroup_cpus)} | nohz_full CPUs {to_str_range(mine)} are NOT " |
| "isolated: the partition did not take effect") |
| continue |
| |
| covered.update(mine) |
| |
| Log.print(Log.Status.OK, "CGROUP_PARTITION", f"Path: {root} | State: {partition_state}") |
| Log.print(Log.Status.OK, "CPUSET_MATCH", |
| f"cpuset: {to_str_range(cgroup_cpus)} | isolating nohz_full CPUs: {to_str_range(mine)}. Ok!") |
| |
| is_quota_valid, quota_str = is_cfs_quota_valid(root) |
| Log.print(is_quota_valid, "CFS_QUOTA", f"CFS Quota: {quota_str}") |
| |
| if unrelated: |
| Log.print(Log.Status.INFO, "CGROUP_PARTITION", |
| f"{unrelated} more isolated partition(s) hold no nohz_full CPU: not checked") |
| |
| return covered |
| |
| # Return the nohz_full CPUs that the cgroup v1 cpusets are isolating |
| def check_cpuset_v1(cpulist): |
| # on cgroup v1 the isolated partition is a cpuset with |
| # cpuset.sched_load_balance disabled |
| cpuset_root = "/sys/fs/cgroup/cpuset" |
| covered = set() |
| balanced = [] |
| |
| for root, dirs, files in os.walk(cpuset_root): |
| # the root cpuset always holds every CPU: it is not an isolation |
| if root == cpuset_root or "cpuset.cpus" not in files: |
| continue |
| |
| # effective_cpus is the mask the kernel really applies |
| cgroup_cpus = read_cpulist(os.path.join(root, "cpuset.effective_cpus")) or \ |
| read_cpulist(os.path.join(root, "cpuset.cpus")) |
| |
| # nohz_full can be split across several cpusets: every cpuset is checked |
| # against the nohz_full CPUs it holds, not against the whole list |
| mine = intersect(cgroup_cpus, cpulist) |
| if not mine: |
| continue |
| |
| load_balance = read_int(os.path.join(root, "cpuset.sched_load_balance")) |
| |
| if load_balance != 0: |
| # the ancestors of an isolated cpuset hold its CPUs too and are |
| # normally still load balanced: report them only if no other cpuset |
| # ends up isolating those CPUs |
| balanced.append((root, cgroup_cpus, mine, load_balance)) |
| continue |
| |
| covered.update(mine) |
| |
| Log.print(Log.Status.OK, "CPUSET_V1_PARTITION", |
| f"Path: {root} | cpus: {to_str_range(cgroup_cpus)} | isolating nohz_full CPUs: " |
| f"{to_str_range(mine)} | sched_load_balance=0. Ok!") |
| |
| # the CFS quota lives in the cpu controller hierarchy on v1 |
| relpath = os.path.relpath(root, cpuset_root) |
| quota = read_int(os.path.join("/sys/fs/cgroup/cpu", relpath, "cpu.cfs_quota_us")) |
| |
| if quota is None: |
| continue |
| |
| if quota == -1: |
| Log.print(Log.Status.OK, "CFS_QUOTA", "CFS Quota not set, this is valid on nohz_full.") |
| else: |
| Log.print(Log.Status.FAIL, "CFS_QUOTA", |
| f"Found CFS quota {quota} on nohz_full. This is not valid.") |
| |
| for root, cgroup_cpus, mine, load_balance in balanced: |
| left = sorted(set(mine) - covered) |
| if not left: |
| continue |
| |
| Log.print(Log.Status.FAIL, "CPUSET_V1_PARTITION", |
| f"Path: {root} | cpus: {to_str_range(cgroup_cpus)} | sched_load_balance={load_balance}: " |
| f"nohz_full CPUs {to_str_range(left)} are still part of a scheduling domain") |
| |
| return covered |
| |
| def check_isolcpus(cpulist, isolcpus): |
| Log.print(Log.Status.INFO, "ISOLCPUS_CMD", "isolcpus= found in cmdline") |
| |
| flags, isolcpus_list = split_isolcpus(isolcpus) |
| |
| # the flags keep managed IRQs and unbound work away from the isolated CPUs |
| if flags: |
| Log.print(Log.Status.INFO, "ISOLCPUS_FLAGS", f"isolcpus flags: {', '.join(flags)}") |
| else: |
| Log.print(Log.Status.INFO, "ISOLCPUS_FLAGS", |
| "No isolcpus flag set: 'domain' is implied. Consider adding " |
| "'managed_irq' to keep managed interrupts off the isolated CPUs.") |
| |
| if "managed_irq" not in flags: |
| Log.print(Log.Status.WARN, "ISOLCPUS_MANAGED_IRQ", |
| "isolcpus=managed_irq is not set: managed IRQs can still target " |
| "the isolated CPUs.") |
| |
| if get_cgroup_version() > 0: |
| return |
| |
| res = intersect(isolcpus_list, cpulist) |
| if cpulist != res: |
| err_nohz_isolated(cpulist, res) |
| else: |
| Log.print(Log.Status.OK, "ISOLCPUS_MATCH", "nohz_full is part of isolcpus. Ok!") |
| |
| # nohz_full= and isolcpus=nohz,<list> are equivalent ways to request full |
| # dynticks: either is authoritative, nohz_full= takes precedence if both are set |
| def get_requested_nohz_full(): |
| requested = get_cmdline_param("nohz_full") |
| if requested is not None: |
| return expand_cpu_spec(requested) |
| |
| isolcpus = get_cmdline_param("isolcpus") |
| if isolcpus is None: |
| return None |
| |
| flags, cpus = split_isolcpus(isolcpus) |
| return cpus if "nohz" in flags else None |
| |
| def check_isolcpus_cpuset(cpulist): |
| Log.header("ISOLCPUS / CPUSET AND NOHZ_FULL") |
| |
| # the applied mask can differ from the requested one: CPU0 is always kept |
| # as housekeeping and the mask is clamped to CONFIG_NR_CPUS |
| effective = get_nohz_full_cpus() |
| requested = get_requested_nohz_full() |
| |
| if not effective: |
| Log.print(Log.Status.FAIL, "NOHZ_FULL_MASK", |
| "/sys/devices/system/cpu/nohz_full is empty: full dynticks is NOT active") |
| else: |
| Log.print(Log.Status.INFO, "NOHZ_FULL_MASK", f"Effective nohz_full: {to_str_range(effective)}") |
| |
| if requested is None: |
| Log.print(Log.Status.WARN, "NOHZ_FULL_CMDLINE", |
| "No nohz_full= or isolcpus=nohz found in /proc/cmdline") |
| elif effective and sorted(requested) != sorted(effective): |
| dropped = sorted(set(requested) - set(effective)) |
| Log.print(Log.Status.FAIL, "NOHZ_FULL_MISMATCH", |
| f"cmdline nohz_full={to_str_range(requested)} but kernel applied " |
| f"{to_str_range(effective)} | dropped: {to_str_range(dropped)}") |
| |
| isolated = read_cpulist("/sys/devices/system/cpu/isolated") |
| Log.print(Log.Status.INFO, "ISOLATED_MASK", f"/sys/devices/system/cpu/isolated: {to_str_range(isolated)}") |
| |
| if not cpulist: |
| return |
| |
| if 0 in cpulist: |
| Log.print(Log.Status.FAIL, "CPU0_NOHZ_FULL", |
| "CPU0 is in the nohz_full list. The kernel always keeps CPU0 as housekeeping.") |
| |
| online = get_online_cpus() |
| if not online: |
| return |
| |
| not_online = sorted(set(cpulist) - set(online)) |
| if not_online: |
| Log.print(Log.Status.WARN, "NOHZ_FULL_OFFLINE", f"nohz_full CPUs not online: {to_str_range(not_online)}") |
| |
| |
| isolcpus = get_cmdline_param("isolcpus") |
| if isolcpus is not None: |
| check_isolcpus(cpulist, isolcpus) |
| |
| version = get_cgroup_version() |
| |
| if version == 2: |
| Log.print(Log.Status.INFO, "CGROUP_VERSION", |
| "cgroup v2 hierarchy detected: checking /sys/fs/cgroup") |
| covered = check_cpuset_v2(cpulist) |
| elif version == 1: |
| Log.print(Log.Status.INFO, "CGROUP_VERSION", |
| "cgroup v1 hierarchy detected: checking /sys/fs/cgroup/cpuset") |
| covered = check_cpuset_v1(cpulist) |
| else: |
| Log.print(Log.Status.WARN, "CGROUP_VERSION", "No cgroup hierarchy found under /sys/fs/cgroup") |
| covered = set() |
| |
| if isolcpus is None: |
| if not covered: |
| Log.print(Log.Status.FAIL, "CPUSET_MISSING", |
| f"nohz_full specified {to_str_range(cpulist)} but no isolated partitions (isolcpus | cpusets) found!") |
| elif set(cpulist) - covered: |
| err_nohz_isolated(cpulist, sorted(covered)) |
| else: |
| Log.print(Log.Status.OK, "CPUSET_MATCH", |
| f"every nohz_full CPU ({to_str_range(cpulist)}) is in an isolated partition. Ok!") |
| |
| def check_configs(cpulist): |
| if not cpulist: |
| Log.print(Log.Status.WARN, "NO_CPULIST", |
| "No nohz_full CPU list available: skipping the per-CPU checks.") |
| return |
| |
| # the isolation check must run regardless of SMT: it is the most important |
| # one and SMT is expected to be *disabled* on a tuned nohz_full machine |
| check_isolcpus_cpuset(cpulist) |
| check_siblings_thread(cpulist) |
| check_cpu_governor(cpulist) |
| check_irqbalance(cpulist) |
| check_irq_affinity(cpulist) |
| check_rcu_nocbs(cpulist) |
| check_workqueue_affinity(cpulist) |
| check_watchdog(cpulist) |
| check_noise_knobs(cpulist) |
| check_net_rps_xps(cpulist) |
| |
| print() |