#!/usr/bin/env bash
# xmhls-lowlat-check 1.0.0 — 低延迟调优体检（只读）
#
# 用法: sudo bash xmhls-lowlat-check-1.0.0.sh [--isolated CPU列表] [--nic 网卡]... [-h]
#   --isolated LIST  预期的隔离核（如 2-15 或 2-7,10-15），核对 isolcpus / nohz_full / rcu_nocbs 是否一致
#   --nic IFACE      需要检查中断合并的交易网卡（可重复；缺省检查全部物理网卡）
# 输出: 逐项 PASS / WARN / INFO 与建议值，末尾汇总。只读取状态，不做任何修改。
# 依据: https://xmhls.com/guide/#tuning（各项均链接内核文档或厂商调优指南）
#
# MIT License · Copyright (c) 2026 西门华莱硕投资咨询有限公司 · https://xmhls.com/guide/#script-lowlat
set -uo pipefail
VERSION=1.0.0
ISOLATED=""
NICS=()

usage() { sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//'; }
while [ $# -gt 0 ]; do
    case "$1" in
        --isolated) ISOLATED="${2:?--isolated 需要 CPU 列表}"; shift 2 ;;
        --nic) NICS+=("${2:?--nic 需要网卡名}"); shift 2 ;;
        -h|--help) usage; exit 0 ;;
        *) echo "未知参数: $1" >&2; usage >&2; exit 2 ;;
    esac
done

P=0; W=0; I=0
pass() { printf '  [PASS] %s\n' "$1"; P=$((P + 1)); }
warn() { printf '  [WARN] %s\n         建议: %s\n' "$1" "$2"; W=$((W + 1)); }
info() { printf '  [INFO] %s\n' "$1"; I=$((I + 1)); }
have() { command -v "$1" >/dev/null 2>&1; }
rd() { cat "$1" 2>/dev/null || echo NA; }
CMDLINE="$(cat /proc/cmdline)"
cmdline_val() { tr ' ' '\n' <<<"$CMDLINE" | sed -n "s/^$1=//p" | tail -n 1; }

# CPU 列表（如 "2-5,8"）展开为排序去重的数字序列，便于比较
expand_cpus() {
    local out=() part a b
    IFS=',' read -ra parts <<<"$1"
    for part in "${parts[@]}"; do
        part="${part##*[!0-9-]}"   # 去掉 managed_irq,domain 之类的前缀标志
        [ -z "$part" ] && continue
        if [[ "$part" == *-* ]]; then a="${part%-*}"; b="${part#*-}"; for ((c = a; c <= b; c++)); do out+=("$c"); done
        else out+=("$part"); fi
    done
    printf '%s\n' "${out[@]}" | sort -n | uniq | tr '\n' ' '
}

echo "xmhls-lowlat-check $VERSION · $(date -u '+%F %T UTC') · host=$(hostname) · kernel=$(uname -r)"
[ "$(id -u)" -eq 0 ] || echo "提示：非 root 运行，部分项（如网卡中断合并）可能无法读取"

echo; echo "== CPU 频率与节能 =="
govs="$(cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor 2>/dev/null | sort | uniq -c | tr -s ' ' | tr '\n' ';')"
if [ -z "$govs" ]; then info "无 cpufreq 接口（虚拟机或 BIOS 接管调频）"
elif [ "$(cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor | sort -u)" = performance ]; then pass "全部核心调速器为 performance"
else warn "调速器分布:${govs}" "设为 performance（tuned latency-performance / cpupower frequency-set -g performance）"; fi

if [ -f /sys/devices/system/cpu/intel_pstate/no_turbo ]; then
    [ "$(rd /sys/devices/system/cpu/intel_pstate/no_turbo)" = 0 ] && pass "Turbo 已开启（intel_pstate）" \
        || warn "Turbo 被关闭（intel_pstate/no_turbo=1）" "超频机通常保持 Turbo 开启，以 BIOS 超频配置为准"
elif [ -f /sys/devices/system/cpu/cpufreq/boost ]; then
    [ "$(rd /sys/devices/system/cpu/cpufreq/boost)" = 1 ] && pass "Boost 已开启" \
        || warn "Boost 被关闭（cpufreq/boost=0）" "超频机通常保持 Boost 开启，以 BIOS 超频配置为准"
else info "Turbo / Boost 状态不可读（由 BIOS / 驱动接管）"; fi

idle_drv="$(rd /sys/devices/system/cpu/cpuidle/current_driver)"
if grep -qw 'idle=poll' <<<"$CMDLINE"; then pass "启动参数 idle=poll（不进入 C-state）"
elif [[ "$(cmdline_val intel_idle.max_cstate)" == 0 && "$(cmdline_val processor.max_cstate)" =~ ^[01]$ ]]; then pass "启动参数限制 C-state（intel_idle.max_cstate=0 processor.max_cstate=$(cmdline_val processor.max_cstate)）"
elif [ "$idle_drv" = NA ] || [ "$idle_drv" = none ]; then info "无 cpuidle 驱动（C-state 由 BIOS 决定，请在 BIOS 中确认已关闭 C1E / C6 / 封装 C-state）"
else
    deep=""
    for st in /sys/devices/system/cpu/cpu0/cpuidle/state*; do
        [ -d "$st" ] || continue
        [ "$(rd "$st/disable")" = 0 ] && [ "$(rd "$st/latency")" -gt 2 ] 2>/dev/null && deep+="$(rd "$st/name")($(rd "$st/latency")µs) "
    done
    [ -z "$deep" ] && pass "cpu0 深度 C-state 均已禁用（驱动 $idle_drv）" \
        || warn "cpu0 启用了深度 C-state：$deep（驱动 $idle_drv）" "BIOS 关闭 C1E/C6/封装 C-state，或启动参数 intel_idle.max_cstate=0 processor.max_cstate=1 / idle=poll（AMD）"
fi

smt="$(rd /sys/devices/system/cpu/smt/active)"
case "$smt" in
    0) pass "SMT / 超线程已关闭" ;;
    1) warn "SMT / 超线程开启" "低延迟主机通常在 BIOS 关闭 SMT，避免兄弟线程争用核心资源" ;;
    *) info "SMT 状态不可读" ;;
esac

echo; echo "== 核隔离与内核噪声 =="
iso_cmd="$(cmdline_val isolcpus)"; nohz_cmd="$(cmdline_val nohz_full)"; rcu_cmd="$(cmdline_val rcu_nocbs)"
iso_sys="$(rd /sys/devices/system/cpu/isolated)"; nohz_sys="$(rd /sys/devices/system/cpu/nohz_full)"
if [ -n "$iso_cmd" ] || [ -n "$nohz_cmd" ]; then
    info "启动参数: isolcpus=${iso_cmd:-（未设）} nohz_full=${nohz_cmd:-（未设）} rcu_nocbs=${rcu_cmd:-（未设）}；生效: isolated=${iso_sys:-空} nohz_full=${nohz_sys:-空}"
    [ -n "$rcu_cmd" ] || warn "未设置 rcu_nocbs" "与 nohz_full 使用相同的核列表，把 RCU 回调移出隔离核"
else
    warn "未配置核隔离（isolcpus / nohz_full 均未设置）" "为策略线程预留隔离核：isolcpus=managed_irq,domain,<核> nohz_full=<核> rcu_nocbs=<核> irqaffinity=<管理核>"
fi
if [ -n "$ISOLATED" ]; then
    want="$(expand_cpus "$ISOLATED")"
    for pair in "isolated:$iso_sys" "nohz_full:$nohz_sys"; do
        name="${pair%%:*}"; val="${pair#*:}"
        if [ "$(expand_cpus "${val/NA/}")" = "$want" ]; then pass "$name 与预期一致（$ISOLATED）"
        else warn "$name 生效值 '${val}' ≠ 预期 '$ISOLATED'" "检查启动参数并重启生效"; fi
    done
fi

if systemctl is-active --quiet irqbalance 2>/dev/null; then
    banned="$(grep -hE '^(IRQBALANCE_BANNED_CPULIST|IRQBALANCE_BANNED_CPUS)=' /etc/default/irqbalance /etc/sysconfig/irqbalance 2>/dev/null | tail -n 1)"
    if [ -n "$banned" ]; then info "irqbalance 运行中，已配置排除：$banned"
    else warn "irqbalance 运行中且未排除隔离核" "停用 irqbalance，或在其配置中设置 IRQBALANCE_BANNED_CPULIST=<隔离核>"; fi
else pass "irqbalance 未运行（中断亲和性由人工 / tuned 管理）"; fi

thp="$(rd /sys/kernel/mm/transparent_hugepage/enabled)"
case "$thp" in
    *"[never]"*) pass "透明大页 never" ;;
    *"[madvise]"*) info "透明大页 madvise（按应用评估；network-latency 配置集设为 never）" ;;
    *"[always]"*) warn "透明大页 always" "设为 never 或 madvise，避免 khugepaged 合并带来的延迟毛刺" ;;
    *) info "透明大页状态不可读" ;;
esac

nodes="$(ls -d /sys/devices/system/node/node[0-9]* 2>/dev/null | wc -l)"
nb="$(rd /proc/sys/kernel/numa_balancing)"
if [ "$nodes" -le 1 ]; then info "单 NUMA 节点（numa_balancing=$nb，影响很小）"
elif [ "$nb" = 0 ]; then pass "NUMA 自动均衡已关闭（$nodes 个节点）"
else warn "NUMA 自动均衡开启（$nodes 个节点）" "sysctl kernel.numa_balancing=0，并用 numactl 显式绑定内存与 CPU"; fi

tm="$(rd /proc/sys/kernel/timer_migration)"
[ "$tm" = 0 ] && pass "定时器迁移已关闭" || warn "kernel.timer_migration=$tm" "设为 0（tuned network-latency 默认如此）"

nmi="$(rd /proc/sys/kernel/nmi_watchdog)"
[ "$nmi" = 0 ] && pass "NMI watchdog 已关闭" || info "NMI watchdog=$nmi（周期性中断；隔离核场景常设 nmi_watchdog=0，但会失去硬死锁检测）"

sw="$(rd /proc/sys/vm/swappiness)"
[ "$sw" != NA ] && [ "$sw" -le 10 ] 2>/dev/null && pass "vm.swappiness=$sw" || info "vm.swappiness=$sw（交易主机通常 ≤ 10，或不配置 swap）"

echo; echo "== 时钟 =="
cs="$(rd /sys/devices/system/clocksource/clocksource0/current_clocksource)"
[ "$cs" = tsc ] && pass "时钟源 tsc" || warn "时钟源为 $cs" "确认 CPU 支持 constant_tsc / nonstop_tsc，并考虑启动参数 tsc=reliable"
for f in constant_tsc nonstop_tsc; do grep -qw "$f" /proc/cpuinfo && pass "CPU 支持 $f" || warn "CPU 不支持 $f" "TSC 可能不稳定，不宜作为时钟源"; done
if pgrep -x ptp4l >/dev/null; then pass "ptp4l 运行中（PTP）$(pgrep -x phc2sys >/dev/null && echo '，phc2sys 运行中' || echo '，但未见 phc2sys')"
elif pgrep -x chronyd >/dev/null; then
    off="$(chronyc tracking 2>/dev/null | awk -F': ' '/System time/{print $2}')"
    pass "chronyd 运行中（系统时间偏差：${off:-NA}）"
elif systemctl is-active --quiet systemd-timesyncd 2>/dev/null; then info "仅 systemd-timesyncd（SNTP，精度有限）；交易时间戳建议用 chrony 硬件时间戳或 PTP"
else warn "未发现时钟同步服务" "部署 chrony 或 linuxptp（ptp4l + phc2sys）"; fi

echo; echo "== 网卡中断合并 =="
if [ ${#NICS[@]} -eq 0 ]; then
    for dev in /sys/class/net/*; do [ -e "$dev/device" ] && NICS+=("$(basename "$dev")"); done
fi
if ! have ethtool; then info "未安装 ethtool，跳过网卡检查"
elif [ ${#NICS[@]} -eq 0 ]; then info "未发现物理网卡"
else
    for n in "${NICS[@]}"; do
        c="$(ethtool -c "$n" 2>/dev/null)" || { info "$n：不支持读取中断合并参数"; continue; }
        rxu="$(awk '/^rx-usecs:/{print $2}' <<<"$c")"; arx="$(awk '/^Adaptive RX:/{print $3}' <<<"$c")"
        if [ "${rxu:-NA}" = 0 ] && [ "${arx:-off}" != on ]; then pass "$n：rx-usecs=0，自适应合并关闭"
        else warn "$n：rx-usecs=${rxu:-NA} Adaptive RX=${arx:-NA}" "低延迟网卡：ethtool -C $n adaptive-rx off rx-usecs 0（内核旁路网卡以厂商建议为准）"; fi
    done
fi

echo; echo "== 内核 =="
if uname -v | grep -q PREEMPT_RT; then info "实时内核（PREEMPT_RT）"; else info "通用内核 $(uname -r)（是否需要实时内核取决于策略对尾延迟的要求）"; fi

printf '\n汇总：PASS=%d WARN=%d INFO=%d\n' "$P" "$W" "$I"
echo "说明：WARN 为常见低延迟建议，是否采纳取决于业务与安全要求；改动前后请用 cyclictest / 业务指标对比验证。"
