From dd9f6f3424f6d79dafbb1612cb7b3448d5d62b64 Mon Sep 17 00:00:00 2001 From: Jiangtian Feng Date: Fri, 3 Jul 2026 00:02:02 +0800 Subject: [PATCH 1/4] feat(ktuner): add deterministic kernel-tuning engine Add ktuner as a new component for automated kernel parameter tuning. The agent diagnoses system configuration against 207 rules, outputs JSON recommendations, and can safely apply/rollback changes. Subcommands: check, tune, fix, why, rollback. All stdout is JSON. Registered as a cosh skill for automatic agent discovery. --- .github/commitlint.config.json | 1 + .github/workflows/ci.yaml | 50 + src/ktuner/.gitignore | 1 + src/ktuner/Cargo.lock | 353 + src/ktuner/Cargo.toml | 22 + src/ktuner/README.md | 89 + src/ktuner/src/bench/mod.rs | 335 + src/ktuner/src/bin/ktuner.rs | 282 + src/ktuner/src/category.rs | 275 + src/ktuner/src/detect/mod.rs | 646 ++ src/ktuner/src/lib.rs | 16 + src/ktuner/src/profile/mod.rs | 226 + src/ktuner/src/rules/mod.rs | 7767 ++++++++++++++++++++ src/ktuner/src/services.rs | 92 + src/ktuner/src/tuner/mod.rs | 920 +++ src/os-skills/system-admin/ktuner/SKILL.md | 108 + 16 files changed, 11183 insertions(+) create mode 100644 src/ktuner/.gitignore create mode 100644 src/ktuner/Cargo.lock create mode 100644 src/ktuner/Cargo.toml create mode 100644 src/ktuner/README.md create mode 100644 src/ktuner/src/bench/mod.rs create mode 100644 src/ktuner/src/bin/ktuner.rs create mode 100644 src/ktuner/src/category.rs create mode 100644 src/ktuner/src/detect/mod.rs create mode 100644 src/ktuner/src/lib.rs create mode 100644 src/ktuner/src/profile/mod.rs create mode 100644 src/ktuner/src/rules/mod.rs create mode 100644 src/ktuner/src/services.rs create mode 100644 src/ktuner/src/tuner/mod.rs create mode 100644 src/os-skills/system-admin/ktuner/SKILL.md diff --git a/.github/commitlint.config.json b/.github/commitlint.config.json index 855bc73f5..6f8621db4 100644 --- a/.github/commitlint.config.json +++ b/.github/commitlint.config.json @@ -17,6 +17,7 @@ "memory", "anolisa", "skillfs", + "ktuner", "deps", "ci", "docs", diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 60439a122..c4e88b048 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -42,6 +42,11 @@ on: required: false type: boolean default: false + run_ktuner: + description: 'Force run ktuner tests (ignore change detection)' + required: false + type: boolean + default: false run_skillfs: description: 'Force run SkillFS tests (ignore change detection)' required: false @@ -73,6 +78,7 @@ jobs: anolisa: ${{ steps.changes.outputs.anolisa }} skillfs: ${{ steps.changes.outputs.skillfs }} cosh_ng: ${{ steps.changes.outputs.cosh_ng }} + ktuner: ${{ steps.changes.outputs.ktuner }} steps: - uses: actions/checkout@v4 with: @@ -102,6 +108,7 @@ jobs: ANOLISA=false SKILLFS=false COSH_NG=false + KTUNER=false # Path-based detection if echo "$CHANGED" | grep -q "^src/copilot-shell/"; then @@ -131,6 +138,9 @@ jobs: if echo "$CHANGED" | grep -q "^src/cosh-ng/"; then COSH_NG=true fi + if echo "$CHANGED" | grep -q "^src/ktuner/"; then + KTUNER=true + fi # Manual override via workflow_dispatch if [[ "${{ inputs.run_copilot_shell }}" == "true" ]]; then @@ -160,6 +170,9 @@ jobs: if [[ "${{ inputs.run_cosh_ng }}" == "true" ]]; then COSH_NG=true fi + if [[ "${{ inputs.run_ktuner }}" == "true" ]]; then + KTUNER=true + fi echo "copilot_shell=$COPILOT_SHELL" >> $GITHUB_OUTPUT echo "agent_sec_core=$AGENT_SEC" >> $GITHUB_OUTPUT @@ -170,6 +183,7 @@ jobs: echo "anolisa=$ANOLISA" >> $GITHUB_OUTPUT echo "skillfs=$SKILLFS" >> $GITHUB_OUTPUT echo "cosh_ng=$COSH_NG" >> $GITHUB_OUTPUT + echo "ktuner=$KTUNER" >> $GITHUB_OUTPUT echo "### Change Detection Results" >> $GITHUB_STEP_SUMMARY echo "| Component | Changed |" >> $GITHUB_STEP_SUMMARY @@ -183,6 +197,7 @@ jobs: echo "| anolisa | $ANOLISA |" >> $GITHUB_STEP_SUMMARY echo "| SkillFS | $SKILLFS |" >> $GITHUB_STEP_SUMMARY echo "| cosh-ng | $COSH_NG |" >> $GITHUB_STEP_SUMMARY + echo "| ktuner | $KTUNER |" >> $GITHUB_STEP_SUMMARY # ========================================================================= # Step 2: Build & Lint copilot-shell @@ -960,3 +975,38 @@ jobs: run: | cd src/cosh-ng cargo test --workspace + + # ========================================================================= + # Step 12: Test ktuner (deterministic kernel-tuning engine) + # ========================================================================= + test-ktuner: + name: Test ktuner + needs: detect-changes + if: needs.detect-changes.outputs.ktuner == 'true' + runs-on: [self-hosted, ubuntu] + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: '1.89.0' + components: 'rustfmt, clippy' + + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src/ktuner + + - name: Check formatting + run: | + cd src/ktuner + cargo fmt --all --check + + - name: Lint + run: | + cd src/ktuner + cargo clippy --all-targets -- -D warnings + + - name: Run tests + run: | + cd src/ktuner + cargo test --lib diff --git a/src/ktuner/.gitignore b/src/ktuner/.gitignore new file mode 100644 index 000000000..b83d22266 --- /dev/null +++ b/src/ktuner/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/src/ktuner/Cargo.lock b/src/ktuner/Cargo.lock new file mode 100644 index 000000000..06799c539 --- /dev/null +++ b/src/ktuner/Cargo.lock @@ -0,0 +1,353 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "ktuner" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "colored", + "libc", + "serde", + "serde_json", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/src/ktuner/Cargo.toml b/src/ktuner/Cargo.toml new file mode 100644 index 000000000..b2dd7d354 --- /dev/null +++ b/src/ktuner/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "ktuner" +version = "0.1.0" +edition = "2021" +description = "Deterministic kernel-tuning engine for ANOLISA agents" +license = "MIT" + +[lib] +name = "ktuner_engine" +path = "src/lib.rs" + +[[bin]] +name = "ktuner" +path = "src/bin/ktuner.rs" + +[dependencies] +anyhow = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +libc = "0.2" +colored = "2" +clap = { version = "4", features = ["derive"] } diff --git a/src/ktuner/README.md b/src/ktuner/README.md new file mode 100644 index 000000000..dc5887bce --- /dev/null +++ b/src/ktuner/README.md @@ -0,0 +1,89 @@ +# ktuner — deterministic kernel-tuning engine + +Agent-facing kernel parameter tuning engine for ANOLISA. Evaluates 207 rules against the running system and outputs structured JSON recommendations. Designed to be called by cosh/agent via `ktuner [options]`. + +## Usage + +```bash +# Diagnose — output score + recommendations +ktuner check +ktuner check --category net +ktuner check --conservative # high-confidence only + +# Apply recommendations (requires root) +sudo ktuner tune --dry-run # preview, no changes +sudo ktuner tune # apply all +sudo ktuner tune --conservative + +# Fix a single parameter (requires root) +sudo ktuner fix # e.g. sudo ktuner fix vm.swappiness + +# Explain why a parameter should change +ktuner why # e.g. ktuner why net.core.somaxconn + +# Undo all changes (requires root) +sudo ktuner rollback +``` + +## JSON output + +All output goes to **stdout as JSON**. Errors go to **stderr as JSON**. No ANSI colors, no progress bars, no human-formatted text on stdout. + +### Exit codes + +| Code | Meaning | +|------|---------| +| 0 | Success (check: system already optimal; tune/fix/rollback: applied OK) | +| 1 | check: has recommendations (not an error, system can be improved) | +| 2 | Error (details in stderr JSON) | + +### check output + +```json +{ + "score": 30, + "predicted_score": 100, + "total_checked": 196, + "recommendations": [ + { + "param": "net.ipv4.tcp_rfc1337", + "current": "0", + "recommended": "1", + "reason": "防止 TIME_WAIT 状态下的 RST 攻击", + "confidence": "high", + "category": "security", + "subcategory": "network", + "writable": true + } + ], + "counts": { "performance": 34, "security": 6, "high_confidence": 5, "writable": 40 }, + "system": { "kernel": "6.6.102+", "cpu_cores": 2, "memory_gb": 8, "numa_nodes": 1 }, + "environment": "物理机/虚拟机", + "workload": "mixed", + "services": ["Nginx", "PostgreSQL"] +} +``` + +### tune output + +```json +{ "applied": 5, "score_before": 30, "score_after": 35 } +``` + +### rollback output + +```json +{ "restored": 5, "failed": 0, "skipped": 0, "status": "Full" } +``` + +### error output (stderr) + +```json +{ "error": "tune requires root (sudo ktuner tune)" } +``` + +## Security + +- **Code-execution deny-list**: `kernel.core_pattern`, `kernel.modprobe`, `kernel.hotplug`, `kernel.poweroff_cmd`, `kernel.modules_disabled`, `kernel.kexec_load_disabled`, `kernel.usermodehelper.*`, `fs.binfmt_misc.*` are unconditionally blocked from any write path (tune/fix/rollback). Matching is done on the resolved filesystem path, not the parameter spelling, so slash/dot/traversal variants are all caught. +- **Rollback safety**: Partial failures preserve the rollback ledger; originals are never lost. +- **No autonomous root**: ktuner checks `euid == 0` and errors out if not root. cosh's sandbox-guard + permission prompt ensure the human approves before any `sudo ktuner tune` executes. diff --git a/src/ktuner/src/bench/mod.rs b/src/ktuner/src/bench/mod.rs new file mode 100644 index 000000000..104433bf1 --- /dev/null +++ b/src/ktuner/src/bench/mod.rs @@ -0,0 +1,335 @@ +use anyhow::Result; +use std::time::Instant; + +#[derive(Debug, Clone)] +pub struct BenchResult { + pub name: String, + pub value: f64, + pub unit: String, +} + +impl BenchResult { + pub fn summary(&self) -> String { + format!("{:.2} {}", self.value, self.unit) + } +} + +/// True if `dir` lives on tmpfs/ramfs (i.e. RAM, not a real disk). +fn is_tmpfs(dir: &str) -> bool { + let Ok(c) = std::ffi::CString::new(dir) else { + return false; + }; + unsafe { + let mut buf: libc::statfs = std::mem::zeroed(); + if libc::statfs(c.as_ptr(), &mut buf) == 0 { + let t = buf.f_type & 0xffff_ffff; + return t == 0x0102_1994 /* TMPFS_MAGIC */ || t == 0x8584_58f6u32 as i64 /* RAMFS_MAGIC */; + } + } + false +} + +/// Pick a directory for IO benchmarks that is backed by a real disk. /tmp is +/// frequently tmpfs (RAM), which would make the IO benchmark measure memory +/// speed and silently invalidate tune's before/after disk comparison. Prefer +/// the working directory, then /var/tmp, and only fall back to /tmp. +fn bench_io_dir() -> String { + let mut candidates: Vec = Vec::new(); + if let Ok(cwd) = std::env::current_dir() { + candidates.push(cwd.to_string_lossy().to_string()); + } + candidates.push("/var/tmp".to_string()); + candidates.push("/tmp".to_string()); + for dir in &candidates { + if std::path::Path::new(dir).is_dir() && !is_tmpfs(dir) { + return dir.clone(); + } + } + "/tmp".to_string() +} + +pub fn run_all_verbose() -> Result> { + run_all_inner(true) +} + +type BenchFn = fn() -> Result; + +fn run_all_inner(verbose: bool) -> Result> { + use std::io::Write; + + let benches: &[(&str, BenchFn)] = &[ + ("syscall", bench_syscall_overhead), + ("上下文切换", bench_context_switch), + ("内存带宽", bench_mem_bandwidth), + ("内存延迟", bench_mem_latency), + ("IO 延迟", bench_io_latency), + ("IO 吞吐", bench_io_throughput), + ("网络延迟", bench_net_latency), + ]; + + let is_tty = unsafe { libc::isatty(libc::STDOUT_FILENO) } != 0; + + let mut results = Vec::with_capacity(benches.len()); + for (i, (label, bench_fn)) in benches.iter().enumerate() { + if verbose && is_tty { + print!( + "\r ⋯ [{}/{}] 测量 {}...{}", + i + 1, + benches.len(), + label, + " ".repeat(10) + ); + std::io::stdout().flush().ok(); + } + results.push(bench_fn()?); + } + if verbose && is_tty { + print!("\r{}\r", " ".repeat(60)); + std::io::stdout().flush().ok(); + } + + Ok(results) +} + +fn bench_syscall_overhead() -> Result { + let iterations = 1_000_000u64; + let start = Instant::now(); + + for _ in 0..iterations { + unsafe { libc::getpid() }; + } + + let elapsed = start.elapsed(); + let ns_per_call = elapsed.as_nanos() as f64 / iterations as f64; + + Ok(BenchResult { + name: "syscall overhead".to_string(), + value: ns_per_call, + unit: "ns/call".to_string(), + }) +} + +fn bench_context_switch() -> Result { + use std::io::{Read, Write}; + use std::os::unix::net::UnixStream; + + let iterations = 100_000u64; + let (mut sock1, mut sock2) = UnixStream::pair()?; + let buf = [0u8; 1]; + + let child = std::thread::spawn(move || { + let mut rbuf = [0u8; 1]; + for _ in 0..iterations { + sock2.read_exact(&mut rbuf).ok(); + sock2.write_all(&buf).ok(); + } + }); + + let start = Instant::now(); + let mut rbuf = [0u8; 1]; + for _ in 0..iterations { + sock1.write_all(&buf)?; + sock1.read_exact(&mut rbuf)?; + } + let elapsed = start.elapsed(); + + child.join().unwrap(); + + let us_per_switch = elapsed.as_micros() as f64 / iterations as f64; + + Ok(BenchResult { + name: "context switch".to_string(), + value: us_per_switch, + unit: "μs/switch".to_string(), + }) +} + +fn bench_mem_bandwidth() -> Result { + let size = 64 * 1024 * 1024; // 64MB + let mut buf: Vec = vec![0u8; size]; + let iterations = 10; + + // Warm up + for b in buf.iter_mut() { + *b = 1; + } + + let start = Instant::now(); + for _ in 0..iterations { + let mut sum: u64 = 0; + for chunk in buf.chunks(64) { + sum = sum.wrapping_add(chunk[0] as u64); + } + std::hint::black_box(sum); + } + let elapsed = start.elapsed(); + + let total_bytes = size as f64 * iterations as f64; + let gb_per_sec = total_bytes / elapsed.as_secs_f64() / 1_000_000_000.0; + + Ok(BenchResult { + name: "memory bandwidth".to_string(), + value: gb_per_sec, + unit: "GB/s".to_string(), + }) +} + +fn bench_mem_latency() -> Result { + let size = 4 * 1024 * 1024; // 4M entries = 32MB on 64-bit + let mut chain: Vec = (0..size).collect(); + + // Shuffle to create random pointer chain + let mut seed = 42u64; + for i in (1..size).rev() { + seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1); + let j = (seed as usize) % (i + 1); + chain.swap(i, j); + } + + // Build linked list via index chain + let mut indices = vec![0usize; size]; + let mut pos = 0; + for &next in &chain { + indices[pos] = next; + pos = next; + } + + // Chase pointers + let iterations = 2_000_000; + let start = Instant::now(); + let mut idx = 0usize; + for _ in 0..iterations { + idx = indices[idx]; + } + std::hint::black_box(idx); + let elapsed = start.elapsed(); + + let ns_per_access = elapsed.as_nanos() as f64 / iterations as f64; + + Ok(BenchResult { + name: "memory latency".to_string(), + value: ns_per_access, + unit: "ns/access".to_string(), + }) +} + +fn bench_io_latency() -> Result { + use std::fs::OpenOptions; + use std::io::Write; + use std::os::unix::fs::OpenOptionsExt; + + let path = format!("{}/.ktuner_io_bench", bench_io_dir()); + let path = path.as_str(); + let iterations = 1000u64; + + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .custom_flags(libc::O_SYNC) + .open(path)?; + + let data = [0u8; 4096]; + + // Warm up + file.write_all(&data)?; + + let start = Instant::now(); + for _ in 0..iterations { + file.write_all(&data)?; + } + let elapsed = start.elapsed(); + + std::fs::remove_file(path).ok(); + + let us_per_write = elapsed.as_micros() as f64 / iterations as f64; + + Ok(BenchResult { + name: "IO latency (sync)".to_string(), + value: us_per_write, + unit: "μs/write".to_string(), + }) +} + +fn bench_io_throughput() -> Result { + use std::fs::OpenOptions; + use std::io::Write; + + let path = format!("{}/.ktuner_io_tput_bench", bench_io_dir()); + let path = path.as_str(); + let block_size = 1024 * 1024; // 1MB blocks + let total_size = 128 * 1024 * 1024; // 128MB total + let blocks = total_size / block_size; + + let data = vec![0u8; block_size]; + + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path)?; + + let start = Instant::now(); + for _ in 0..blocks { + file.write_all(&data)?; + } + file.sync_all()?; + let elapsed = start.elapsed(); + + std::fs::remove_file(path).ok(); + + let mb_per_sec = total_size as f64 / elapsed.as_secs_f64() / (1024.0 * 1024.0); + + Ok(BenchResult { + name: "IO throughput (seq)".to_string(), + value: mb_per_sec, + unit: "MB/s".to_string(), + }) +} + +fn bench_net_latency() -> Result { + use std::io::{Read, Write}; + use std::net::{TcpListener, TcpStream}; + + let listener = TcpListener::bind("127.0.0.1:0")?; + let addr = listener.local_addr()?; + let iterations = 50_000u64; + + let child = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut buf = [0u8; 1]; + for _ in 0..iterations { + stream.read_exact(&mut buf).ok(); + stream.write_all(&buf).ok(); + } + }); + + let mut stream = TcpStream::connect(addr)?; + stream.set_nodelay(true)?; + let buf = [0u8; 1]; + let mut rbuf = [0u8; 1]; + + // Warm up + for _ in 0..100 { + stream.write_all(&buf)?; + stream.read_exact(&mut rbuf)?; + } + + let measure_iterations = iterations - 100; + let start = Instant::now(); + for _ in 0..measure_iterations { + stream.write_all(&buf)?; + stream.read_exact(&mut rbuf)?; + } + let elapsed = start.elapsed(); + + child.join().unwrap(); + + let us_per_rtt = elapsed.as_micros() as f64 / measure_iterations as f64; + + Ok(BenchResult { + name: "net latency (TCP)".to_string(), + value: us_per_rtt, + unit: "μs/RTT".to_string(), + }) +} diff --git a/src/ktuner/src/bin/ktuner.rs b/src/ktuner/src/bin/ktuner.rs new file mode 100644 index 000000000..d21656c3a --- /dev/null +++ b/src/ktuner/src/bin/ktuner.rs @@ -0,0 +1,282 @@ +use anyhow::Result; +use clap::{Parser, Subcommand}; +use ktuner_engine::{ + self as engine, category, detect, evaluate, rules, services, tuner, Recommendation, +}; +use serde_json::json; + +#[derive(Parser)] +#[command(name = "ktuner", version, about = "Deterministic kernel-tuning engine")] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Diagnose system and output tuning recommendations + Check { + #[arg(long)] + category: Option, + #[arg(long)] + conservative: bool, + }, + /// Apply tuning recommendations + Tune { + #[arg(long)] + dry_run: bool, + #[arg(long)] + conservative: bool, + #[arg(long)] + category: Option, + }, + /// Fix a single parameter + Fix { param: String }, + /// Explain why a parameter should be changed + Why { param: String }, + /// Roll back all applied changes + Rollback, +} + +fn main() { + let cli = Cli::parse(); + let result = match cli.command { + Commands::Check { + category: cat, + conservative, + } => cmd_check(cat, conservative), + Commands::Tune { + dry_run, + conservative, + category: cat, + } => cmd_tune(dry_run, conservative, cat), + Commands::Fix { param } => cmd_fix(¶m), + Commands::Why { param } => cmd_why(¶m), + Commands::Rollback => cmd_rollback(), + }; + match result { + Ok(code) => std::process::exit(code), + Err(e) => { + let out = json!({ "error": format!("{e:#}") }); + eprintln!("{}", serde_json::to_string_pretty(&out).unwrap()); + std::process::exit(2); + } + } +} + +fn cmd_check(cat: Option, conservative: bool) -> Result { + if let Some(ref c) = cat { + category::validate_category(c)?; + } + let info = detect::gather_system_info()?; + let eval = evaluate(&info)?; + let workload = engine::classify(&info); + let runtime_env = detect::detect_runtime_env(); + let detected_services = services::detect_services(&info); + + let mut recs = eval.recommendations.clone(); + if let Some(ref c) = cat { + recs = category::filter_by_category(recs, c); + } + if conservative { + recs.retain(|r| r.confidence == rules::Confidence::High); + } + + let score = eval.score(); + let counts = category::RecCounts::from_recs(&recs); + let total_weight: usize = recs + .iter() + .map(|r| match r.confidence { + rules::Confidence::High => 3, + rules::Confidence::Medium => 2, + }) + .sum(); + let predicted_score = (score + total_weight).min(100); + + let recs_json: Vec = recs + .iter() + .map(|r| { + json!({ + "param": r.param, + "current": r.current_value, + "recommended": r.recommended_value, + "reason": r.reason, + "confidence": format!("{:?}", r.confidence).to_lowercase(), + "category": format!("{:?}", r.category).to_lowercase(), + "subcategory": category::param_subcategory(&r.param), + "writable": r.writable, + }) + }) + .collect(); + + let output = json!({ + "score": score, + "predicted_score": predicted_score, + "total_checked": eval.total_checked, + "recommendations": recs_json, + "counts": { + "performance": counts.perf, + "security": counts.sec, + "high_confidence": counts.high, + "writable": counts.writable, + }, + "system": { + "kernel": info.kernel_version, + "cpu_cores": info.cpu_cores, + "memory_gb": info.memory_total_gb, + "numa_nodes": info.numa_nodes, + }, + "environment": format!("{runtime_env}"), + "workload": format!("{workload}"), + "services": detected_services, + }); + println!("{}", serde_json::to_string_pretty(&output)?); + + let code = if recs.is_empty() { 0 } else { 1 }; + Ok(code) +} + +fn cmd_tune(dry_run: bool, conservative: bool, cat: Option) -> Result { + if !dry_run { + let is_root = unsafe { libc::geteuid() } == 0; + if !is_root { + anyhow::bail!("tune requires root (sudo ktuner tune)"); + } + } + + if let Some(ref c) = cat { + category::validate_category(c)?; + } + let (_info, eval) = gather()?; + let score_before = eval.score(); + let mut recs = eval.recommendations; + if let Some(ref c) = cat { + recs = category::filter_by_category(recs, c); + } + if conservative { + recs.retain(|r| r.confidence == rules::Confidence::High); + } + recs.retain(|r| r.writable && !category::is_runtime_dangerous(&r.param)); + + if recs.is_empty() { + let output = json!({ "status": "optimal", "applied": 0 }); + println!("{}", serde_json::to_string_pretty(&output)?); + return Ok(0); + } + + if dry_run { + let recs_json: Vec = recs.iter().map(rec_json).collect(); + let output = json!({ "dry_run": true, "would_apply": recs_json }); + println!("{}", serde_json::to_string_pretty(&output)?); + return Ok(0); + } + + let applied = tuner::apply_quiet(&recs)?; + let (_, eval_after) = gather()?; + let score_after = eval_after.score(); + + let output = json!({ + "applied": applied, + "score_before": score_before, + "score_after": score_after, + }); + println!("{}", serde_json::to_string_pretty(&output)?); + Ok(0) +} + +fn cmd_fix(param: &str) -> Result { + let is_root = unsafe { libc::geteuid() } == 0; + if !is_root { + anyhow::bail!("fix requires root (sudo ktuner fix {param})"); + } + let (_, eval) = gather()?; + let rec = eval + .recommendations + .iter() + .find(|r| r.param == param) + .ok_or_else(|| anyhow::anyhow!("parameter not found or already optimal: {param}"))?; + if !rec.writable { + anyhow::bail!("parameter {param} is read-only in this environment"); + } + if category::is_runtime_dangerous(&rec.param) { + anyhow::bail!( + "parameter {param} is dangerous to write at runtime, persist to /etc/sysctl.d instead" + ); + } + tuner::apply_one(rec)?; + let (_, eval_after) = gather()?; + let output = json!({ + "fixed": param, + "previous": rec.current_value, + "applied": rec.recommended_value, + "score_after": eval_after.score(), + "remaining": eval_after.recommendations.len(), + }); + println!("{}", serde_json::to_string_pretty(&output)?); + Ok(0) +} + +fn cmd_why(param: &str) -> Result { + let (_, eval) = gather()?; + let normalized = param.replace('/', ".").to_lowercase(); + if let Some(rec) = eval + .recommendations + .iter() + .find(|r| r.param == param || r.param == normalized) + { + let output = json!({ + "param": rec.param, + "current": rec.current_value, + "recommended": rec.recommended_value, + "reason": rec.reason, + "confidence": format!("{:?}", rec.confidence).to_lowercase(), + "category": format!("{:?}", rec.category).to_lowercase(), + "subcategory": category::param_subcategory(&rec.param), + "writable": rec.writable, + }); + println!("{}", serde_json::to_string_pretty(&output)?); + return Ok(1); + } + let path = tuner::param_to_path(&normalized); + if std::path::Path::new(&path).exists() { + let val = std::fs::read_to_string(&path).unwrap_or_default(); + let output = json!({ "param": normalized, "current": val.trim(), "status": "optimal" }); + println!("{}", serde_json::to_string_pretty(&output)?); + return Ok(0); + } + anyhow::bail!("parameter not found: {param}") +} + +fn cmd_rollback() -> Result { + let is_root = unsafe { libc::geteuid() } == 0; + if !is_root { + anyhow::bail!("rollback requires root (sudo ktuner rollback)"); + } + let outcome = tuner::rollback_quiet()?; + let status = tuner::classify_rollback(&outcome); + let output = json!({ + "restored": outcome.restored, + "failed": outcome.failed, + "skipped": outcome.skipped, + "status": format!("{status:?}"), + }); + println!("{}", serde_json::to_string_pretty(&output)?); + Ok(0) +} + +fn gather() -> Result<(detect::SystemInfo, rules::EvalResult)> { + let info = detect::gather_system_info()?; + let eval = evaluate(&info)?; + Ok((info, eval)) +} + +fn rec_json(r: &Recommendation) -> serde_json::Value { + json!({ + "param": r.param, + "current": r.current_value, + "recommended": r.recommended_value, + "reason": r.reason, + "confidence": format!("{:?}", r.confidence).to_lowercase(), + "category": format!("{:?}", r.category).to_lowercase(), + }) +} diff --git a/src/ktuner/src/category.rs b/src/ktuner/src/category.rs new file mode 100644 index 000000000..23da8bea1 --- /dev/null +++ b/src/ktuner/src/category.rs @@ -0,0 +1,275 @@ +use crate::rules::{Category, Confidence, Recommendation}; + +pub fn is_runtime_dangerous(param: &str) -> bool { + param == "vm.nr_hugepages" +} + +pub fn param_subcategory(param: &str) -> &'static str { + if param.starts_with("net.") || param.contains("conntrack") { + "network" + } else if param.starts_with("vm.") { + "memory" + } else if param.starts_with("block/") + || param.starts_with("transparent_hugepage/") + || param.starts_with("fs.inotify.") + { + "io" + } else if param.starts_with("kernel.sched_") + || param.starts_with("kernel.pid_") + || param.starts_with("kernel.threads") + || param.starts_with("kernel.numa_") + || param.starts_with("kernel.perf_") + || param.starts_with("kernel.nmi_") + || param.starts_with("kernel.watchdog") + || param.starts_with("kernel.hung_task") + || param.starts_with("kernel.softlockup") + || param.starts_with("kernel.hardlockup") + { + "cpu" + } else if param.starts_with("kernel.") { + match param { + "kernel.dmesg_restrict" + | "kernel.kptr_restrict" + | "kernel.yama.ptrace_scope" + | "kernel.randomize_va_space" + | "kernel.sysrq" + | "kernel.modules_disabled" + | "kernel.kexec_load_disabled" + | "kernel.unprivileged_bpf_disabled" => "security", + _ => "cpu", + } + } else if param.starts_with("fs.protected_") || param.starts_with("fs.suid_") { + "security" + } else if param.starts_with("fs.") { + "io" + } else { + "other" + } +} + +pub fn validate_category(cat: &str) -> anyhow::Result<()> { + let cat_lower = cat.to_lowercase(); + if !matches!( + cat_lower.as_str(), + "network" + | "net" + | "内存" + | "memory" + | "mem" + | "io" + | "disk" + | "磁盘" + | "cpu" + | "调度" + | "security" + | "sec" + | "安全" + ) { + anyhow::bail!("未知分类: {cat}(支持: net, mem, io, cpu, security)"); + } + Ok(()) +} + +pub fn filter_by_category(mut recs: Vec, cat: &str) -> Vec { + let cat_lower = cat.to_lowercase(); + recs.retain(|r| match cat_lower.as_str() { + "network" | "net" | "网络" => { + r.category != Category::Security + && (r.param.starts_with("net.") || r.param.contains("conntrack")) + } + "memory" | "mem" | "内存" => { + r.category != Category::Security + && (r.param.starts_with("vm.") + || (r.param.starts_with("fs.") + && !r.param.starts_with("fs.inotify.") + && !r.param.starts_with("fs.aio-")) + || r.param == "kernel.shmmax") + } + "io" | "disk" | "磁盘" => { + r.param.starts_with("block/") + || r.param.starts_with("transparent_hugepage/") + || r.param.starts_with("fs.inotify.") + || r.param.starts_with("fs.aio-") + } + "cpu" | "调度" => { + r.param.starts_with("kernel.sched") + || r.param.contains("pid_max") + || r.param.contains("numa") + || r.param == "kernel.threads-max" + } + "security" | "sec" | "安全" => r.category == Category::Security, + _ => true, + }); + recs +} + +pub struct RecCounts { + pub perf: usize, + pub sec: usize, + pub high: usize, + pub writable: usize, + pub net: usize, + pub mem: usize, + pub io: usize, + pub cpu: usize, +} + +impl RecCounts { + pub fn from_recs(recs: &[Recommendation]) -> Self { + let perf = recs + .iter() + .filter(|r| r.category == Category::Performance) + .count(); + let sec = recs + .iter() + .filter(|r| r.category == Category::Security) + .count(); + let high = recs + .iter() + .filter(|r| r.confidence == Confidence::High) + .count(); + let writable = recs.iter().filter(|r| r.writable).count(); + let net = recs + .iter() + .filter(|r| { + r.category != Category::Security && param_subcategory(&r.param) == "network" + }) + .count(); + let mem = recs + .iter() + .filter(|r| r.category != Category::Security && param_subcategory(&r.param) == "memory") + .count(); + let io = recs + .iter() + .filter(|r| r.category != Category::Security && param_subcategory(&r.param) == "io") + .count(); + let cpu = recs + .iter() + .filter(|r| r.category != Category::Security && param_subcategory(&r.param) == "cpu") + .count(); + Self { + perf, + sec, + high, + writable, + net, + mem, + io, + cpu, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_runtime_dangerous() { + assert!(is_runtime_dangerous("vm.nr_hugepages")); + assert!(!is_runtime_dangerous("vm.swappiness")); + assert!(!is_runtime_dangerous("net.core.somaxconn")); + } + + #[test] + fn test_param_subcategory() { + assert_eq!(param_subcategory("net.ipv4.tcp_fastopen"), "network"); + assert_eq!( + param_subcategory("net.netfilter.nf_conntrack_max"), + "network" + ); + assert_eq!(param_subcategory("vm.swappiness"), "memory"); + assert_eq!(param_subcategory("block/sda/scheduler"), "io"); + assert_eq!(param_subcategory("transparent_hugepage/enabled"), "io"); + assert_eq!(param_subcategory("fs.inotify.max_user_watches"), "io"); + assert_eq!(param_subcategory("kernel.sched_migration_cost_ns"), "cpu"); + assert_eq!(param_subcategory("kernel.pid_max"), "cpu"); + assert_eq!(param_subcategory("kernel.dmesg_restrict"), "security"); + assert_eq!(param_subcategory("kernel.randomize_va_space"), "security"); + assert_eq!(param_subcategory("fs.protected_hardlinks"), "security"); + assert_eq!(param_subcategory("fs.suid_dumpable"), "security"); + assert_eq!(param_subcategory("fs.file-max"), "io"); + assert_eq!(param_subcategory("unknown.param"), "other"); + } + + #[test] + fn test_validate_category_valid() { + for cat in [ + "net", "network", "mem", "memory", "io", "disk", "cpu", "security", "sec", + ] { + assert!(validate_category(cat).is_ok(), "{cat} should be valid"); + } + } + + #[test] + fn test_validate_category_invalid() { + assert!(validate_category("garbage").is_err()); + assert!(validate_category("").is_err()); + } + + #[test] + fn test_filter_by_category_net() { + let recs = vec![ + Recommendation { + param: "net.core.somaxconn".into(), + current_value: "128".into(), + recommended_value: "4096".into(), + reason: "".into(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }, + Recommendation { + param: "vm.swappiness".into(), + current_value: "60".into(), + recommended_value: "10".into(), + reason: "".into(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }, + Recommendation { + param: "kernel.dmesg_restrict".into(), + current_value: "0".into(), + recommended_value: "1".into(), + reason: "".into(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }, + ]; + let filtered = filter_by_category(recs, "net"); + assert_eq!(filtered.len(), 1); + assert_eq!(filtered[0].param, "net.core.somaxconn"); + } + + #[test] + fn test_rec_counts() { + let recs = vec![ + Recommendation { + param: "net.core.somaxconn".into(), + current_value: "128".into(), + recommended_value: "4096".into(), + reason: "".into(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }, + Recommendation { + param: "kernel.dmesg_restrict".into(), + current_value: "0".into(), + recommended_value: "1".into(), + reason: "".into(), + confidence: Confidence::Medium, + category: Category::Security, + writable: false, + }, + ]; + let c = RecCounts::from_recs(&recs); + assert_eq!(c.perf, 1); + assert_eq!(c.sec, 1); + assert_eq!(c.high, 1); + assert_eq!(c.writable, 1); + assert_eq!(c.net, 1); + } +} diff --git a/src/ktuner/src/detect/mod.rs b/src/ktuner/src/detect/mod.rs new file mode 100644 index 000000000..d76407eee --- /dev/null +++ b/src/ktuner/src/detect/mod.rs @@ -0,0 +1,646 @@ +use anyhow::{Context, Result}; +use std::fs; +use std::path::Path; + +#[derive(Debug, Clone)] +pub struct SystemInfo { + pub kernel_version: String, + pub os_distro: String, + pub cpu_model: String, + pub cpu_cores: usize, + pub numa_nodes: usize, + pub memory_total_gb: u64, + pub disks: Vec, + pub network: Vec, + pub sysctl: SysctlValues, + pub processes: Vec, +} + +#[derive(Debug, Clone)] +pub struct DiskInfo { + pub name: String, + pub disk_type: DiskType, + pub scheduler: String, + pub available_schedulers: Vec, + pub nr_requests: u64, + pub read_ahead_kb: u64, + pub rq_affinity: u64, +} + +#[derive(Debug, Clone, PartialEq)] +#[allow(clippy::upper_case_acronyms)] +pub enum DiskType { + NVMe, + SSD, + HDD, + Unknown, +} + +#[derive(Debug, Clone)] +pub struct NetInfo { + pub name: String, + pub speed_mbps: u64, +} + +#[derive(Debug, Clone)] +pub struct SysctlValues { + pub swappiness: u64, + pub dirty_ratio: u64, + pub dirty_background_ratio: u64, + pub somaxconn: u64, + pub tcp_fastopen: u64, + pub thp_enabled: String, +} + +#[derive(Debug, Clone)] +pub struct ProcessInfo { + pub name: String, +} + +impl std::fmt::Display for DiskType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DiskType::NVMe => write!(f, "NVMe"), + DiskType::SSD => write!(f, "SSD"), + DiskType::HDD => write!(f, "HDD"), + DiskType::Unknown => write!(f, "Unknown"), + } + } +} + +impl std::fmt::Display for DiskInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}({})", self.name, self.disk_type) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum RuntimeEnv { + BareHost, + Container, +} + +impl std::fmt::Display for RuntimeEnv { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + RuntimeEnv::BareHost => write!(f, "物理机/虚拟机"), + RuntimeEnv::Container => write!(f, "容器"), + } + } +} + +pub fn detect_runtime_env() -> RuntimeEnv { + if Path::new("/.dockerenv").exists() || Path::new("/run/.containerenv").exists() { + return RuntimeEnv::Container; + } + if let Ok(cgroup) = fs::read_to_string("/proc/1/cgroup") { + if cgroup.contains("docker") + || cgroup.contains("kubepods") + || cgroup.contains("containerd") + || cgroup.contains("lxc") + { + return RuntimeEnv::Container; + } + } + // PID 1 comm fallback: only treat an UNKNOWN init as a container. Bare hosts + // run a variety of init systems besides systemd/sysvinit (runit, s6, + // OpenRC, ...), so whitelist those to avoid misclassifying them as + // containers (which would wrongly mark params read-only and steer the user + // to the host-export workflow). + if let Ok(sched) = fs::read_to_string("/proc/1/sched") { + const KNOWN_INIT: &[&str] = &[ + "systemd", + "init", + "runit", + "s6-svscan", + "s6-linux-init", + "openrc-init", + "upstart", + "busybox", + "procd", + "dumb-init", + ]; + let comm = sched.split_whitespace().next().unwrap_or(""); + if !KNOWN_INIT.iter().any(|i| comm.starts_with(i)) { + return RuntimeEnv::Container; + } + } + RuntimeEnv::BareHost +} + +pub fn is_param_writable(path: &str) -> bool { + // Check write permission WITHOUT actually writing. The previous approach + // (reading the file and writing its content back) had two flaws: + // 1. It mutated /proc/sys during read-only `check`/`status` runs. + // 2. For /sys scheduler & THP the read-back includes the full option + // list (e.g. "none [mq-deadline] kyber"), which is not a valid value, + // so the write always failed and these params were wrongly flagged + // read-only — tune/fixall then never touched them. + // libc::access(W_OK) reflects file mode and read-only mounts (containers) + // without side effects. + if !Path::new(path).exists() { + return false; + } + let c_path = match std::ffi::CString::new(path) { + Ok(p) => p, + Err(_) => return false, + }; + unsafe { libc::access(c_path.as_ptr(), libc::W_OK) == 0 } +} + +pub fn gather_system_info() -> Result { + let (cpu_model, cpu_cores) = read_cpu_info()?; + Ok(SystemInfo { + kernel_version: read_kernel_version()?, + os_distro: read_os_distro(), + cpu_model, + cpu_cores, + numa_nodes: read_numa_nodes(), + memory_total_gb: read_memory_total_gb()?, + disks: read_disk_info()?, + network: read_network_info()?, + sysctl: read_sysctl_values()?, + processes: read_processes()?, + }) +} + +fn read_file_trimmed(path: &str) -> Result { + fs::read_to_string(path) + .with_context(|| format!("failed to read {path}")) + .map(|s| s.trim().to_string()) +} + +fn read_kernel_version() -> Result { + read_file_trimmed("/proc/sys/kernel/osrelease") +} + +fn read_os_distro() -> String { + if let Ok(content) = fs::read_to_string("/etc/os-release") { + let mut pretty_name = None; + let mut name = None; + let mut version = None; + for line in content.lines() { + if let Some(val) = line.strip_prefix("PRETTY_NAME=") { + pretty_name = Some(val.trim_matches('"').to_string()); + } else if let Some(val) = line.strip_prefix("NAME=") { + name = Some(val.trim_matches('"').to_string()); + } else if let Some(val) = line.strip_prefix("VERSION=") { + version = Some(val.trim_matches('"').to_string()); + } + } + if let Some(pn) = pretty_name { + return pn; + } + if let (Some(n), Some(v)) = (name, version) { + return format!("{n} {v}"); + } + } + "Unknown".to_string() +} + +fn read_cpu_info() -> Result<(String, usize)> { + let cpuinfo = fs::read_to_string("/proc/cpuinfo").context("failed to read /proc/cpuinfo")?; + let mut model = "Unknown CPU".to_string(); + let mut cores = 0usize; + for line in cpuinfo.lines() { + if line.starts_with("processor") { + cores += 1; + } else if model == "Unknown CPU" && line.starts_with("model name") { + if let Some(val) = line.split(':').nth(1) { + model = val.trim().to_string(); + } + } + } + Ok((model, cores.max(1))) +} + +fn read_numa_nodes() -> usize { + let path = "/sys/devices/system/node"; + if let Ok(entries) = fs::read_dir(path) { + entries + .filter_map(|e| e.ok()) + .filter(|e| { + e.file_name() + .to_str() + .map(|n| n.starts_with("node")) + .unwrap_or(false) + }) + .count() + } else { + 1 + } +} + +fn read_memory_total_gb() -> Result { + let meminfo = fs::read_to_string("/proc/meminfo").context("failed to read /proc/meminfo")?; + let mut host_kb: u64 = 0; + for line in meminfo.lines() { + if line.starts_with("MemTotal:") { + let parts: Vec<&str> = line.split_whitespace().collect(); + if let Some(kb_str) = parts.get(1) { + host_kb = kb_str.parse().unwrap_or(0); + break; + } + } + } + + let cgroup_kb = read_cgroup_memory_limit_kb(); + let effective_kb = if cgroup_kb > 0 && cgroup_kb < host_kb { + cgroup_kb + } else { + host_kb + }; + + // Floor to GB (unchanged), but never report 0 when the machine has any RAM: + // a sub-1GB host floored to 0 GB would make every memory-scaled rule + // misbehave. + let gb = effective_kb / 1024 / 1024; + Ok(if gb == 0 && effective_kb > 0 { 1 } else { gb }) +} + +fn read_cgroup_memory_limit_kb() -> u64 { + // cgroup v2 + if let Ok(s) = fs::read_to_string("/sys/fs/cgroup/memory.max") { + let s = s.trim(); + if s != "max" { + if let Ok(bytes) = s.parse::() { + return bytes / 1024; + } + } + return 0; + } + // cgroup v1 + if let Ok(s) = fs::read_to_string("/sys/fs/cgroup/memory/memory.limit_in_bytes") { + if let Ok(bytes) = s.trim().parse::() { + if bytes < 1u64 << 62 { + return bytes / 1024; + } + } + } + 0 +} + +fn read_disk_info() -> Result> { + let mut disks = Vec::new(); + let block_dir = "/sys/block"; + + if let Ok(entries) = fs::read_dir(block_dir) { + for entry in entries.filter_map(|e| e.ok()) { + let name = entry.file_name().to_string_lossy().to_string(); + + if name.starts_with("loop") + || name.starts_with("ram") + || name.starts_with("dm-") + || name.starts_with("sr") + || name.starts_with("zram") + || name.starts_with("nbd") + || name.starts_with("md") + { + continue; + } + + let disk_type = detect_disk_type(&name); + let scheduler = read_current_scheduler(&name); + let available_schedulers = read_available_schedulers(&name); + let nr_requests = read_nr_requests(&name); + let read_ahead_kb = read_read_ahead_kb(&name); + let rq_affinity = read_rq_affinity(&name); + + disks.push(DiskInfo { + name, + disk_type, + scheduler, + available_schedulers, + nr_requests, + read_ahead_kb, + rq_affinity, + }); + } + } + + Ok(disks) +} + +fn detect_disk_type(name: &str) -> DiskType { + if name.starts_with("nvme") { + return DiskType::NVMe; + } + + // virtio-blk (vd*) and Xen (xvd*) cloud disks frequently report + // rotational=1 even when backed by SSD/network storage, so the bare + // rotational flag would mislabel them HDD and apply spinning-disk tuning. + // Treat them as SSD-like (the SSD optimizations are safe and beneficial, + // and a real spinning disk presented as vd* in modern clouds is vanishingly + // rare). + let is_virtual = name.starts_with("vd") || name.starts_with("xvd"); + + let rotational_path = format!("/sys/block/{name}/queue/rotational"); + if let Ok(val) = fs::read_to_string(&rotational_path) { + match val.trim() { + "0" => DiskType::SSD, + "1" => { + if is_virtual { + DiskType::SSD + } else { + DiskType::HDD + } + } + _ => DiskType::Unknown, + } + } else if is_virtual { + DiskType::SSD + } else { + DiskType::Unknown + } +} + +fn read_current_scheduler(name: &str) -> String { + let path = format!("/sys/block/{name}/queue/scheduler"); + if let Ok(content) = fs::read_to_string(&path) { + // Current scheduler is enclosed in brackets: "none [mq-deadline] bfq" + for part in content.split_whitespace() { + if part.starts_with('[') && part.ends_with(']') { + return part[1..part.len() - 1].to_string(); + } + } + content.trim().to_string() + } else { + "unknown".to_string() + } +} + +fn read_available_schedulers(name: &str) -> Vec { + let path = format!("/sys/block/{name}/queue/scheduler"); + if let Ok(content) = fs::read_to_string(&path) { + content + .split_whitespace() + .map(|s| s.trim_matches(|c| c == '[' || c == ']').to_string()) + .collect() + } else { + Vec::new() + } +} + +fn read_nr_requests(name: &str) -> u64 { + let path = format!("/sys/block/{name}/queue/nr_requests"); + fs::read_to_string(&path) + .ok() + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(0) +} + +fn read_read_ahead_kb(name: &str) -> u64 { + let path = format!("/sys/block/{name}/queue/read_ahead_kb"); + fs::read_to_string(&path) + .ok() + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(0) +} + +fn read_rq_affinity(name: &str) -> u64 { + let path = format!("/sys/block/{name}/queue/rq_affinity"); + fs::read_to_string(&path) + .ok() + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(0) +} + +fn read_network_info() -> Result> { + let mut nets = Vec::new(); + let net_dir = "/sys/class/net"; + + if let Ok(entries) = fs::read_dir(net_dir) { + for entry in entries.filter_map(|e| e.ok()) { + let name = entry.file_name().to_string_lossy().to_string(); + if name == "lo" + || name.starts_with("veth") + || name.starts_with("br-") + || name.starts_with("virbr") + || name == "docker0" + || name == "bonding_masters" + { + continue; + } + + let speed_path = format!("/sys/class/net/{name}/speed"); + let speed_mbps = fs::read_to_string(&speed_path) + .ok() + .and_then(|s| s.trim().parse::().ok()) + .map(|s| if s > 0 { s as u64 } else { 0 }) + .unwrap_or(0); + + nets.push(NetInfo { + name: name.clone(), + speed_mbps, + }); + } + } + + Ok(nets) +} + +fn read_sysctl_values() -> Result { + Ok(SysctlValues { + swappiness: read_sysctl_u64("/proc/sys/vm/swappiness"), + dirty_ratio: read_sysctl_u64("/proc/sys/vm/dirty_ratio"), + dirty_background_ratio: read_sysctl_u64("/proc/sys/vm/dirty_background_ratio"), + somaxconn: read_sysctl_u64("/proc/sys/net/core/somaxconn"), + tcp_fastopen: read_sysctl_u64("/proc/sys/net/ipv4/tcp_fastopen"), + thp_enabled: read_thp_enabled(), + }) +} + +pub(crate) fn read_sysctl_u64(path: &str) -> u64 { + fs::read_to_string(path) + .ok() + .and_then(|s| s.trim().parse().ok()) + .unwrap_or(0) +} + +fn read_thp_enabled() -> String { + let path = "/sys/kernel/mm/transparent_hugepage/enabled"; + if let Ok(content) = fs::read_to_string(path) { + for part in content.split_whitespace() { + if part.starts_with('[') && part.ends_with(']') { + return part[1..part.len() - 1].to_string(); + } + } + content.trim().to_string() + } else { + "unknown".to_string() + } +} + +fn read_processes() -> Result> { + let mut procs = Vec::new(); + let proc_dir = "/proc"; + + if let Ok(entries) = fs::read_dir(proc_dir) { + for entry in entries.filter_map(|e| e.ok()) { + let fname = entry.file_name().to_string_lossy().to_string(); + if fname.parse::().is_ok() { + let comm_path = format!("/proc/{fname}/comm"); + if let Ok(comm) = fs::read_to_string(&comm_path) { + let comm = comm.trim().to_string(); + // /proc//comm is truncated to 15 chars and for a JVM is + // just "java" (likewise "python"/"node"/"beam.smp"), so the + // actual service is invisible. Recover it from cmdline. + if is_generic_runtime(&comm) { + if let Some(svc) = detect_runtime_service(&fname) { + procs.push(ProcessInfo { name: svc }); + } + } + procs.push(ProcessInfo { name: comm }); + } + } + } + } + + Ok(procs) +} + +fn is_generic_runtime(comm: &str) -> bool { + matches!( + comm, + "java" | "python" | "python3" | "node" | "nodejs" | "ruby" | "beam.smp" | "erlang" + ) +} + +/// Map a JVM/interpreter process to the concrete service it runs by scanning +/// its cmdline (main class / jar / script). Returns a canonical service name +/// that matches the has_process() checks used by rules and classification. +fn detect_runtime_service(pid: &str) -> Option { + let cmdline = fs::read_to_string(format!("/proc/{pid}/cmdline")).ok()?; + // cmdline args are NUL-separated. + let cmd = cmdline.replace('\0', " ").to_lowercase(); + // Order matters: more specific markers first. + const MARKERS: &[(&str, &str)] = &[ + ("org.elasticsearch", "elasticsearch"), + ("elasticsearch", "elasticsearch"), + ("org.opensearch", "opensearch"), + ("opensearch", "opensearch"), + ("kafka.kafka", "kafka"), + ("kafka", "kafka"), + ("org.apache.zookeeper", "zookeeper"), + ("zookeeper", "zookeeper"), + ("org.apache.flink", "flink"), + ("flink", "flink"), + ("org.apache.spark", "spark"), + ("spark", "spark"), + ("org.apache.cassandra", "cassandra"), + ("cassandra", "cassandra"), + ("org.apache.hadoop", "hadoop"), + ("hadoop", "hadoop"), + ("hbase", "hbase"), + ("solr", "solr"), + ("logstash", "logstash"), + ("pulsar", "pulsar"), + ("catalina", "tomcat"), + ("tomcat", "tomcat"), + ("jenkins", "jenkins"), + ]; + for (marker, svc) in MARKERS { + if cmd.contains(marker) { + return Some(svc.to_string()); + } + } + None +} + +impl SystemInfo { + pub fn has_process(&self, pattern: &str) -> bool { + self.processes.iter().any(|p| p.name.contains(pattern)) + } + + /// Exact process-name match. Use this for short names that are substrings of + /// unrelated processes (e.g. "node" vs the ubiquitous "node_exporter"). + pub fn has_process_exact(&self, name: &str) -> bool { + self.processes.iter().any(|p| p.name == name) + } + + pub fn max_net_speed(&self) -> u64 { + self.network.iter().map(|n| n.speed_mbps).max().unwrap_or(0) + } + + pub fn param_exists(&self, path: &str) -> bool { + Path::new(path).exists() + } + + pub fn has_listen_sockets(&self) -> bool { + has_tcp_listen_sockets() + } + + pub fn has_conntrack(&self) -> bool { + std::path::Path::new("/proc/sys/net/netfilter/nf_conntrack_max").exists() + || std::path::Path::new("/proc/sys/net/nf_conntrack_max").exists() + } +} + +fn has_tcp_listen_sockets() -> bool { + for path in &["/proc/net/tcp", "/proc/net/tcp6"] { + if let Ok(content) = fs::read_to_string(path) { + for line in content.lines().skip(1) { + let fields: Vec<&str> = line.split_whitespace().collect(); + if let Some(state) = fields.get(3) { + if *state == "0A" { + return true; + } + } + } + } + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_gather_system_info() { + let info = gather_system_info().unwrap(); + assert!(!info.kernel_version.is_empty()); + assert!(info.cpu_cores > 0); + assert!(info.memory_total_gb > 0); + } + + #[test] + fn test_detect_disk_type() { + assert_eq!(detect_disk_type("nvme0n1"), DiskType::NVMe); + assert_eq!(detect_disk_type("nvme1n1"), DiskType::NVMe); + } + + #[test] + fn test_has_process() { + let info = SystemInfo { + kernel_version: String::new(), + os_distro: String::new(), + cpu_model: String::new(), + cpu_cores: 1, + numa_nodes: 1, + memory_total_gb: 1, + disks: vec![], + network: vec![], + sysctl: SysctlValues { + swappiness: 60, + dirty_ratio: 20, + dirty_background_ratio: 10, + somaxconn: 128, + tcp_fastopen: 0, + thp_enabled: "always".to_string(), + }, + processes: vec![ + ProcessInfo { + name: "postgres".to_string(), + }, + ProcessInfo { + name: "nginx".to_string(), + }, + ], + }; + + assert!(info.has_process("postgres")); + assert!(info.has_process("nginx")); + assert!(!info.has_process("redis")); + } +} diff --git a/src/ktuner/src/lib.rs b/src/ktuner/src/lib.rs new file mode 100644 index 000000000..d10492d7e --- /dev/null +++ b/src/ktuner/src/lib.rs @@ -0,0 +1,16 @@ +pub mod bench; +pub mod category; +pub mod detect; +pub mod profile; +pub mod rules; +pub mod services; +pub mod tuner; + +pub use detect::{gather_system_info, RuntimeEnv, SystemInfo}; +pub use profile::{classify, WorkloadType}; +pub use rules::{evaluate, Category, Confidence, EvalResult, Recommendation}; +pub use tuner::{ + apply, apply_one, apply_quiet, auto_rollback_on_degradation, classify_rollback, + is_forbidden_param, param_to_path, rollback, rollback_preview, rollback_quiet, RollbackOutcome, + RollbackStatus, +}; diff --git a/src/ktuner/src/profile/mod.rs b/src/ktuner/src/profile/mod.rs new file mode 100644 index 000000000..a2e31cd44 --- /dev/null +++ b/src/ktuner/src/profile/mod.rs @@ -0,0 +1,226 @@ +use crate::detect::SystemInfo; + +#[derive(Debug, Clone, PartialEq)] +pub enum WorkloadType { + MemoryIntensive, + IoThroughput, + IoLatency, + NetworkIntensive, + Mixed, +} + +impl std::fmt::Display for WorkloadType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WorkloadType::MemoryIntensive => write!(f, "memory-intensive"), + WorkloadType::IoThroughput => write!(f, "io-throughput"), + WorkloadType::IoLatency => write!(f, "io-latency"), + WorkloadType::NetworkIntensive => write!(f, "network-intensive"), + WorkloadType::Mixed => write!(f, "mixed"), + } + } +} + +impl WorkloadType { + pub fn description(&self) -> &str { + match self { + WorkloadType::MemoryIntensive => "内存密集型(数据库、缓存、大数据)", + WorkloadType::IoThroughput => "高吞吐 IO(批量处理、ETL、日志)", + WorkloadType::IoLatency => "低延迟 IO(OLTP 数据库、KV 存储)", + WorkloadType::NetworkIntensive => "网络密集型(Web 服务、RPC、代理)", + WorkloadType::Mixed => "混合型负载", + } + } +} + +pub fn classify(info: &SystemInfo) -> WorkloadType { + // Process-based heuristics + let has_db = info.has_process("postgres") + || info.has_process("mysqld") + || info.has_process("mongod") + || info.has_process("clickhouse"); + let has_cache = info.has_process("redis-server") + || info.has_process("memcached") + || info.has_process("etcd"); + let has_web = info.has_process("nginx") + || info.has_process("httpd") + || info.has_process("envoy") + || info.has_process("haproxy") + || info.has_process("caddy"); + let has_java = info.has_process("java"); + let has_search = info.has_process("elasticsearch") || info.has_process("opensearch"); + let has_streaming = + info.has_process("kafka") || info.has_process("flink") || info.has_process("spark"); + + if has_db { + return WorkloadType::IoLatency; + } + + if has_cache { + return WorkloadType::MemoryIntensive; + } + + if has_search { + return WorkloadType::MemoryIntensive; + } + + if has_web && !has_db { + return WorkloadType::NetworkIntensive; + } + + if has_streaming { + return WorkloadType::IoThroughput; + } + + if has_java { + return WorkloadType::Mixed; + } + + WorkloadType::Mixed +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::detect::*; + + fn make_info(processes: Vec<&str>) -> SystemInfo { + SystemInfo { + kernel_version: String::new(), + os_distro: String::new(), + cpu_model: String::new(), + cpu_cores: 8, + numa_nodes: 1, + memory_total_gb: 64, + disks: vec![], + network: vec![], + sysctl: SysctlValues { + swappiness: 60, + dirty_ratio: 20, + dirty_background_ratio: 10, + somaxconn: 128, + tcp_fastopen: 1, + thp_enabled: "always".to_string(), + }, + processes: processes + .iter() + .map(|name| ProcessInfo { + name: name.to_string(), + }) + .collect(), + } + } + + #[test] + fn test_classify_database() { + assert_eq!( + classify(&make_info(vec!["postgres"])), + WorkloadType::IoLatency + ); + assert_eq!( + classify(&make_info(vec!["mysqld"])), + WorkloadType::IoLatency + ); + assert_eq!( + classify(&make_info(vec!["mongod"])), + WorkloadType::IoLatency + ); + } + + #[test] + fn test_classify_cache() { + assert_eq!( + classify(&make_info(vec!["redis-server"])), + WorkloadType::MemoryIntensive + ); + assert_eq!( + classify(&make_info(vec!["memcached"])), + WorkloadType::MemoryIntensive + ); + } + + #[test] + fn test_classify_web() { + assert_eq!( + classify(&make_info(vec!["nginx"])), + WorkloadType::NetworkIntensive + ); + assert_eq!( + classify(&make_info(vec!["envoy"])), + WorkloadType::NetworkIntensive + ); + } + + #[test] + fn test_classify_clickhouse() { + assert_eq!( + classify(&make_info(vec!["clickhouse"])), + WorkloadType::IoLatency + ); + } + + #[test] + fn test_classify_elasticsearch() { + assert_eq!( + classify(&make_info(vec!["elasticsearch"])), + WorkloadType::MemoryIntensive + ); + } + + #[test] + fn test_classify_etcd() { + assert_eq!( + classify(&make_info(vec!["etcd"])), + WorkloadType::MemoryIntensive + ); + } + + #[test] + fn test_classify_caddy() { + assert_eq!( + classify(&make_info(vec!["caddy"])), + WorkloadType::NetworkIntensive + ); + } + + #[test] + fn test_classify_compiler_ignored() { + // Compilers are transient — should not affect classification + assert_eq!(classify(&make_info(vec!["cc1"])), WorkloadType::Mixed); + assert_eq!(classify(&make_info(vec!["rustc"])), WorkloadType::Mixed); + } + + #[test] + fn test_classify_streaming() { + assert_eq!( + classify(&make_info(vec!["kafka"])), + WorkloadType::IoThroughput + ); + assert_eq!( + classify(&make_info(vec!["flink"])), + WorkloadType::IoThroughput + ); + } + + #[test] + fn test_classify_empty() { + assert_eq!(classify(&make_info(vec![])), WorkloadType::Mixed); + } + + #[test] + fn test_classify_priority_db_over_web() { + assert_eq!( + classify(&make_info(vec!["nginx", "postgres"])), + WorkloadType::IoLatency + ); + } + + #[test] + fn test_classify_priority_db_with_compiler() { + // Compiler is transient, db takes priority + assert_eq!( + classify(&make_info(vec!["cc1", "postgres", "nginx"])), + WorkloadType::IoLatency + ); + } +} diff --git a/src/ktuner/src/rules/mod.rs b/src/ktuner/src/rules/mod.rs new file mode 100644 index 000000000..a50609125 --- /dev/null +++ b/src/ktuner/src/rules/mod.rs @@ -0,0 +1,7767 @@ +use crate::detect::{read_sysctl_u64, DiskType, SystemInfo}; +use crate::profile::WorkloadType; +use anyhow::Result; + +#[derive(Debug, Clone, PartialEq, Default)] +pub enum Confidence { + #[default] + High, // Hardware-deterministic or zero-tradeoff, guaranteed correct + Medium, // Workload-deterministic, high probability +} + +#[derive(Debug, Clone, PartialEq, Default)] +pub enum Category { + #[default] + Performance, + Security, +} + +impl std::fmt::Display for Category { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Category::Performance => write!(f, "性能"), + Category::Security => write!(f, "安全"), + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct Recommendation { + pub param: String, + pub current_value: String, + pub recommended_value: String, + pub reason: String, + pub confidence: Confidence, + pub category: Category, + pub writable: bool, +} + +pub struct EvalResult { + pub recommendations: Vec, + pub total_checked: usize, +} + +impl EvalResult { + pub fn score(&self) -> usize { + if self.recommendations.is_empty() { + return 100; + } + let penalty: usize = self + .recommendations + .iter() + .map(|r| match r.confidence { + Confidence::High => 3, + Confidence::Medium => 2, + }) + .sum(); + 100usize.saturating_sub(penalty).max(30) + } +} + +pub fn evaluate(info: &SystemInfo) -> Result { + let workload = crate::profile::classify(info); + evaluate_with_workload(info, &workload) +} + +pub fn evaluate_with_workload(info: &SystemInfo, workload: &WorkloadType) -> Result { + let mut recs = Vec::new(); + let mut checked: usize = 0; + + // Performance rules + checked += eval_io_scheduler(info, &mut recs); + checked += eval_swappiness(info, workload, &mut recs); + checked += eval_thp(info, &mut recs); + checked += eval_dirty_ratio(info, workload, &mut recs); + checked += eval_somaxconn(info, workload, &mut recs); + checked += eval_tcp_fastopen(info, &mut recs); + checked += eval_min_free_kbytes(info, &mut recs); + + // Network performance rules + checked += eval_netdev_max_backlog(info, &mut recs); + checked += eval_tcp_max_syn_backlog(info, &mut recs); + checked += eval_rmem_max(info, &mut recs); + checked += eval_wmem_max(info, &mut recs); + checked += eval_tcp_rmem(info, &mut recs); + checked += eval_tcp_wmem(info, &mut recs); + checked += eval_tcp_slow_start_after_idle(info, &mut recs); + checked += eval_ip_local_port_range(info, &mut recs); + checked += eval_default_qdisc(info, &mut recs); + checked += eval_tcp_tw_reuse(info, &mut recs); + checked += eval_tcp_fin_timeout(info, &mut recs); + checked += eval_tcp_keepalive_time(info, &mut recs); + checked += eval_tcp_keepalive_intvl(info, &mut recs); + checked += eval_tcp_keepalive_probes(info, &mut recs); + checked += eval_tcp_max_tw_buckets(info, &mut recs); + checked += eval_tcp_mtu_probing(info, &mut recs); + checked += eval_tcp_no_metrics_save(info, &mut recs); + checked += eval_tcp_congestion_control(info, &mut recs); + checked += eval_nf_conntrack_max(info, &mut recs); + checked += eval_tcp_timestamps(info, &mut recs); + checked += eval_tcp_window_scaling(info, &mut recs); + checked += eval_tcp_ecn(info, &mut recs); + checked += eval_tcp_sack(info, &mut recs); + checked += eval_netdev_budget(info, &mut recs); + checked += eval_busy_poll(info, &mut recs); + checked += eval_busy_read(info, &mut recs); + checked += eval_tcp_retries2(info, &mut recs); + checked += eval_tcp_syn_retries(info, &mut recs); + checked += eval_tcp_synack_retries(info, &mut recs); + checked += eval_optmem_max(info, &mut recs); + checked += eval_neigh_gc_thresh3(info, &mut recs); + checked += eval_arp_announce(info, &mut recs); + checked += eval_arp_ignore(info, &mut recs); + + // VM/Memory rules + checked += eval_max_map_count(info, &mut recs); + checked += eval_zone_reclaim_mode(info, &mut recs); + checked += eval_vfs_cache_pressure(info, &mut recs); + checked += eval_watermark_scale_factor(info, &mut recs); + checked += eval_file_max(info, &mut recs); + checked += eval_nr_open(info, &mut recs); + checked += eval_inotify_max_user_watches(info, &mut recs); + checked += eval_aio_max_nr(info, &mut recs); + checked += eval_oom_kill_allocating_task(info, &mut recs); + checked += eval_overcommit_memory(info, &mut recs); + checked += eval_dirty_background_ratio(info, workload, &mut recs); + checked += eval_dirty_expire_centisecs(info, &mut recs); + checked += eval_dirty_writeback_centisecs(info, &mut recs); + checked += eval_shmmax(info, &mut recs); + + // IO/Disk rules + checked += eval_read_ahead_kb(info, &mut recs); + checked += eval_nr_requests(info, &mut recs); + checked += eval_rq_affinity(info, &mut recs); + + // CPU/Scheduler rules + checked += eval_numa_balancing(info, &mut recs); + checked += eval_sched_autogroup(info, &mut recs); + checked += eval_pid_max(info, &mut recs); + checked += eval_sched_migration_cost(info, &mut recs); + checked += eval_sched_min_granularity(info, &mut recs); + checked += eval_nmi_watchdog(info, &mut recs); + checked += eval_stat_interval(info, &mut recs); + checked += eval_hung_task_timeout(info, &mut recs); + + checked += eval_netdev_budget_usecs(info, &mut recs); + checked += eval_dirty_bytes(info, &mut recs); + checked += eval_sched_child_runs_first(info, &mut recs); + checked += eval_page_cluster(info, &mut recs); + checked += eval_rmem_default(info, &mut recs); + checked += eval_wmem_default(info, &mut recs); + checked += eval_sched_nr_migrate(info, &mut recs); + checked += eval_tcp_notsent_lowat(info, &mut recs); + checked += eval_unix_max_dgram_qlen(info, &mut recs); + checked += eval_rps_sock_flow_entries(info, &mut recs); + checked += eval_tcp_dsack(info, &mut recs); + checked += eval_ip_no_pmtu_disc(info, &mut recs); + checked += eval_sched_wakeup_granularity(info, &mut recs); + checked += eval_extfrag_threshold(info, &mut recs); + checked += eval_tcp_orphan_retries(info, &mut recs); + checked += eval_tcp_early_retrans(info, &mut recs); + checked += eval_tcp_tw_recycle(info, &mut recs); + checked += eval_arp_filter(info, &mut recs); + checked += eval_sched_cfs_bandwidth_slice(info, &mut recs); + + // Security rules (zero performance cost) + checked += eval_aslr(info, &mut recs); + checked += eval_dmesg_restrict(info, &mut recs); + checked += eval_kptr_restrict(info, &mut recs); + checked += eval_protected_links(info, &mut recs); + checked += eval_accept_redirects(info, &mut recs); + checked += eval_sysrq(info, &mut recs); + checked += eval_tcp_syncookies(info, &mut recs); + checked += eval_send_redirects(info, &mut recs); + checked += eval_perf_event_paranoid(info, &mut recs); + checked += eval_rp_filter(info, &mut recs); + checked += eval_panic(info, &mut recs); + checked += eval_panic_on_oom(info, &mut recs); + checked += eval_ip_forward(info, &mut recs); + checked += eval_unprivileged_bpf(info, &mut recs); + checked += eval_core_uses_pid(info, &mut recs); + checked += eval_yama_ptrace_scope(info, &mut recs); + checked += eval_log_martians(info, &mut recs); + checked += eval_icmp_echo_ignore_broadcasts(info, &mut recs); + checked += eval_accept_source_route(info, &mut recs); + checked += eval_tcp_rfc1337(info, &mut recs); + checked += eval_secure_redirects(info, &mut recs); + checked += eval_mmap_min_addr(info, &mut recs); + checked += eval_default_accept_redirects(info, &mut recs); + checked += eval_default_accept_source_route(info, &mut recs); + checked += eval_sched_latency_ns(info, &mut recs); + checked += eval_tcp_challenge_ack_limit(info, &mut recs); + checked += eval_rp_filter_all(info, &mut recs); + checked += eval_tcp_max_orphans(info, &mut recs); + checked += eval_threads_max(info, &mut recs); + checked += eval_nr_hugepages(info, &mut recs); + checked += eval_suid_dumpable(info, &mut recs); + checked += eval_icmp_ignore_bogus(info, &mut recs); + checked += eval_default_log_martians(info, &mut recs); + checked += eval_laptop_mode(info, &mut recs); + checked += eval_tcp_adv_win_scale(info, &mut recs); + checked += eval_sched_tunable_scaling(info, &mut recs); + checked += eval_panic_on_oops(info, &mut recs); + checked += eval_oom_dump_tasks(info, &mut recs); + checked += eval_tcp_moderate_rcvbuf(info, &mut recs); + checked += eval_flow_limit_table_len(info, &mut recs); + checked += eval_tcp_l3mdev_accept(info, &mut recs); + checked += eval_panic_on_warn(info, &mut recs); + checked += eval_dirty_background_bytes(info, &mut recs); + checked += eval_hardlockup_panic(info, &mut recs); + checked += eval_sched_rt_runtime(info, &mut recs); + checked += eval_tcp_thin_linear_timeouts(info, &mut recs); + checked += eval_arp_notify(info, &mut recs); + checked += eval_default_arp_announce(info, &mut recs); + checked += eval_default_arp_ignore(info, &mut recs); + checked += eval_default_send_redirects(info, &mut recs); + checked += eval_neigh_gc_thresh1(info, &mut recs); + checked += eval_neigh_gc_thresh2(info, &mut recs); + checked += eval_tcp_retries1(info, &mut recs); + checked += eval_tcp_limit_output_bytes(info, &mut recs); + checked += eval_dev_weight(info, &mut recs); + checked += eval_printk(info, &mut recs); + checked += eval_watchdog_thresh(info, &mut recs); + checked += eval_admin_reserve_kbytes(info, &mut recs); + checked += eval_msgmax(info, &mut recs); + checked += eval_msgmnb(info, &mut recs); + checked += eval_protected_fifos(info, &mut recs); + checked += eval_user_reserve_kbytes(info, &mut recs); + checked += eval_shmmni(info, &mut recs); + checked += eval_sem(info, &mut recs); + checked += eval_gc_stale_time(info, &mut recs); + checked += eval_shm_rmid_forced(info, &mut recs); + checked += eval_tcp_fack(info, &mut recs); + checked += eval_tcp_reordering(info, &mut recs); + checked += eval_sched_energy_aware(info, &mut recs); + checked += eval_percpu_pagelist_high_fraction(info, &mut recs); + checked += eval_accept_ra(info, &mut recs); + checked += eval_compact_memory(info, &mut recs); + checked += eval_min_slab_ratio(info, &mut recs); + checked += eval_tcp_autocorking(info, &mut recs); + checked += eval_tcp_workaround_signed_windows(info, &mut recs); + checked += eval_randomize_va_space_full(info, &mut recs); + checked += eval_max_user_instances(info, &mut recs); + checked += eval_keys_maxkeys(info, &mut recs); + checked += eval_tcp_available_ulp(info, &mut recs); + checked += eval_numa_stat(info, &mut recs); + checked += eval_tcp_base_mss(info, &mut recs); + checked += eval_tcp_min_tso_segs(info, &mut recs); + checked += eval_neigh_default_gc_interval(info, &mut recs); + checked += eval_neigh_default_gc_stale_time(info, &mut recs); + checked += eval_tcp_fastopen_blackhole_timeout(info, &mut recs); + checked += eval_max_queued_signals(info, &mut recs); + checked += eval_keys_maxbytes(info, &mut recs); + checked += eval_pipe_max_size(info, &mut recs); + checked += eval_shmall(info, &mut recs); + checked += eval_tcp_app_win(info, &mut recs); + checked += eval_ip_default_ttl(info, &mut recs); + checked += eval_tcp_frto(info, &mut recs); + checked += eval_icmp_ratelimit(info, &mut recs); + checked += eval_igmp_max_memberships(info, &mut recs); + checked += eval_tcp_recovery(info, &mut recs); + checked += eval_tcp_comp_sack_delay(info, &mut recs); + checked += eval_skb_frag_coalesce(info, &mut recs); + checked += eval_neigh_proxy_delay(info, &mut recs); + checked += eval_tcp_pacing_ca_ratio(info, &mut recs); + checked += eval_tcp_pacing_ss_ratio(info, &mut recs); + checked += eval_tcp_comp_sack_nr(info, &mut recs); + checked += eval_tcp_thin_dupack(info, &mut recs); + checked += eval_tcp_invalid_ratelimit(info, &mut recs); + checked += eval_tcp_init_cwnd(info, &mut recs); + checked += eval_tcp_tso_win_divisor(info, &mut recs); + checked += eval_sched_schedstats(info, &mut recs); + checked += eval_inotify_max_queued_events(info, &mut recs); + checked += eval_tcp_max_reordering(info, &mut recs); + checked += eval_tcp_retrans_collapse(info, &mut recs); + checked += eval_protected_regular(info, &mut recs); + checked += eval_bpf_jit_enable(info, &mut recs); + checked += eval_bpf_jit_harden(info, &mut recs); + checked += eval_tcp_available_congestion(info, &mut recs); + checked += eval_somaxconn_large(info, &mut recs); + checked += eval_promote_secondaries(info, &mut recs); + checked += eval_unres_qlen_bytes(info, &mut recs); + checked += eval_ip_nonlocal_bind(info, &mut recs); + checked += eval_conntrack_tcp_timeout_established(info, &mut recs); + checked += eval_softlockup_all_cpu_backtrace(info, &mut recs); + checked += eval_compact_unevictable(info, &mut recs); + checked += eval_perf_cpu_time_max_percent(info, &mut recs); + checked += eval_hung_task_warnings(info, &mut recs); + checked += eval_overcommit_ratio(info, &mut recs); + + // A few params are evaluated by more than one rule. Collapse duplicates so + // each param appears once — otherwise it is shown twice and its score + // penalty is double-counted. + let mut recs = dedupe_recommendations(recs); + + // Check writability for each recommendation + for rec in &mut recs { + let path = crate::tuner::param_to_path(&rec.param); + rec.writable = crate::detect::is_param_writable(&path); + } + + recs.sort_by(|a, b| { + let ca = match a.confidence { + Confidence::High => 0u8, + Confidence::Medium => 1, + }; + let cb = match b.confidence { + Confidence::High => 0u8, + Confidence::Medium => 1, + }; + ca.cmp(&cb).then_with(|| a.param.cmp(&b.param)) + }); + + Ok(EvalResult { + recommendations: recs, + total_checked: checked, + }) +} + +/// Collapse recommendations that target the same param to a single entry, +/// keeping the higher-confidence one (on a tie, the first seen, preserving +/// order). Prevents duplicate display lines and double-counted score penalties. +fn dedupe_recommendations(recs: Vec) -> Vec { + let mut seen: std::collections::HashMap = std::collections::HashMap::new(); + let mut deduped: Vec = Vec::with_capacity(recs.len()); + for rec in recs.into_iter() { + if let Some(&idx) = seen.get(&rec.param) { + if rec.confidence == Confidence::High && deduped[idx].confidence == Confidence::Medium { + deduped[idx] = rec; + } + } else { + seen.insert(rec.param.clone(), deduped.len()); + deduped.push(rec); + } + } + deduped +} + +// ─── Performance Rules ──────────────────────────────────────────────────────── + +fn eval_io_scheduler(info: &SystemInfo, recs: &mut Vec) -> usize { + let mut count = 0; + for disk in &info.disks { + match disk.disk_type { + DiskType::NVMe => { + count += 1; + let target = if disk.available_schedulers.contains(&"none".to_string()) { + "none" + } else if disk.available_schedulers.contains(&"noop".to_string()) { + "noop" + } else { + continue; + }; + + if disk.scheduler != target { + recs.push(Recommendation { + param: format!("block/{}/scheduler", disk.name), + current_value: disk.scheduler.clone(), + recommended_value: target.to_string(), + reason: "NVMe 磁盘无需软件 IO 调度,直通硬件队列延迟最低".to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + } + DiskType::SSD => { + count += 1; + if disk.scheduler == "cfq" { + let target = if disk.available_schedulers.contains(&"none".to_string()) { + "none" + } else if disk.available_schedulers.contains(&"noop".to_string()) { + "noop" + } else if disk.available_schedulers.contains(&"deadline".to_string()) { + "deadline" + } else { + continue; + }; + + recs.push(Recommendation { + param: format!("block/{}/scheduler", disk.name), + current_value: disk.scheduler.clone(), + recommended_value: target.to_string(), + reason: "SSD 随机读写性能强,cfq 的排队排序开销是不必要的".to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + } + _ => {} + } + } + count +} + +fn eval_swappiness( + info: &SystemInfo, + workload: &WorkloadType, + recs: &mut Vec, +) -> usize { + let is_db = info.has_process("postgres") + || info.has_process("mysqld") + || info.has_process("mongod") + || info.has_process("clickhouse") + || info.has_process("redis-server"); + + let (target, reason) = if is_db + || *workload == WorkloadType::IoLatency + || *workload == WorkloadType::MemoryIntensive + { + ( + 1, + "数据库/缓存场景,swap 会导致严重的延迟抖动,建议几乎禁用", + ) + } else if info.memory_total_gb >= 64 { + ( + 10, + "大内存机器(≥64GB)通常不需要积极 swap,降低可减少不必要的页面换出", + ) + } else { + return 1; + }; + + if info.sysctl.swappiness > target as u64 { + recs.push(Recommendation { + param: "vm.swappiness".to_string(), + current_value: info.sysctl.swappiness.to_string(), + recommended_value: target.to_string(), + reason: reason.to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_thp(info: &SystemInfo, recs: &mut Vec) -> usize { + let is_latency_sensitive = info.has_process("redis-server") + || info.has_process("memcached") + || info.has_process("postgres") + || info.has_process("mysqld") + || info.has_process("clickhouse"); + + if is_latency_sensitive && info.sysctl.thp_enabled == "always" { + recs.push(Recommendation { + param: "transparent_hugepage/enabled".to_string(), + current_value: "always".to_string(), + recommended_value: "madvise".to_string(), + reason: "检测到延迟敏感进程,THP 的合并/分裂操作会造成不可预测的延迟抖动".to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_dirty_ratio( + info: &SystemInfo, + workload: &WorkloadType, + recs: &mut Vec, +) -> usize { + let is_db = info.has_process("postgres") + || info.has_process("mysqld") + || info.has_process("mongod") + || info.has_process("clickhouse"); + let is_latency_sensitive = is_db || *workload == WorkloadType::IoLatency; + + // dirty_ratio and dirty_bytes are mutually exclusive in the kernel (setting + // one zeroes the other). Big-memory machines are handled by the bytes-based + // rules (eval_dirty_bytes / eval_dirty_background_bytes, which require + // >=64GB); cap the percentage-based advice to <64GB so no host ever gets + // both a *_ratio and a *_bytes recommendation for the same dimension. + if !is_latency_sensitive || info.memory_total_gb >= 64 { + return 2; + } + + let (target_ratio, target_bg) = (5, 3); + + if info.sysctl.dirty_ratio > target_ratio { + recs.push(Recommendation { + param: "vm.dirty_ratio".to_string(), + current_value: info.sysctl.dirty_ratio.to_string(), + recommended_value: target_ratio.to_string(), + reason: "延迟敏感负载需要更低的 dirty_ratio,避免脏页积压导致的写入延迟尖刺" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + + if info.sysctl.dirty_background_ratio > target_bg { + recs.push(Recommendation { + param: "vm.dirty_background_ratio".to_string(), + current_value: info.sysctl.dirty_background_ratio.to_string(), + recommended_value: target_bg.to_string(), + reason: "更早触发后台刷脏,平滑写入压力".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 2 +} + +fn eval_somaxconn( + info: &SystemInfo, + workload: &WorkloadType, + recs: &mut Vec, +) -> usize { + if !info.has_listen_sockets() { + return 1; + } + + let threshold = if *workload == WorkloadType::NetworkIntensive { + 8192 + } else { + 4096 + }; + + if info.sysctl.somaxconn < threshold as u64 { + recs.push(Recommendation { + param: "net.core.somaxconn".to_string(), + current_value: info.sysctl.somaxconn.to_string(), + recommended_value: "65535".to_string(), + reason: "检测到监听端口,增大 listen backlog 避免高并发时连接被拒绝".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_fastopen(info: &SystemInfo, recs: &mut Vec) -> usize { + if !info.param_exists("/proc/sys/net/ipv4/tcp_fastopen") { + return 1; + } + + if info.has_listen_sockets() && info.sysctl.tcp_fastopen < 3 { + recs.push(Recommendation { + param: "net.ipv4.tcp_fastopen".to_string(), + current_value: info.sysctl.tcp_fastopen.to_string(), + recommended_value: "3".to_string(), + reason: "启用 TCP Fast Open(客户端+服务端)减少连接建立延迟".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_min_free_kbytes(info: &SystemInfo, recs: &mut Vec) -> usize { + if !info.param_exists("/proc/sys/vm/min_free_kbytes") { + return 1; + } + + let current = read_sysctl_u64("/proc/sys/vm/min_free_kbytes"); + let mem_kb = info.memory_total_gb * 1024 * 1024; + let recommended = (mem_kb / 1000).min(2 * 1024 * 1024); + + if current < recommended / 2 { + recs.push(Recommendation { + param: "vm.min_free_kbytes".to_string(), + current_value: current.to_string(), + recommended_value: recommended.to_string(), + reason: format!( + "空闲页面水位偏低({}GB 内存),突发分配时易触发直接回收造成延迟,适当调高更平稳", + info.memory_total_gb + ), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +// ─── Security Rules (zero performance cost) ─────────────────────────────────── + +fn eval_aslr(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/randomize_va_space"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current < 2 { + recs.push(Recommendation { + param: "kernel.randomize_va_space".to_string(), + current_value: current.to_string(), + recommended_value: "2".to_string(), + reason: "ASLR 未完全启用,攻击者可预测内存地址布局实施代码注入".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_dmesg_restrict(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/dmesg_restrict"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "kernel.dmesg_restrict".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "普通用户可读取内核日志,可能泄露敏感信息(内存地址、硬件细节)".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_kptr_restrict(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/kptr_restrict"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "kernel.kptr_restrict".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "内核指针地址对普通用户可见,降低了内核漏洞利用的难度".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_protected_links(info: &SystemInfo, recs: &mut Vec) -> usize { + let hardlinks_path = "/proc/sys/fs/protected_hardlinks"; + let symlinks_path = "/proc/sys/fs/protected_symlinks"; + + if info.param_exists(hardlinks_path) { + let current = read_sysctl_u64(hardlinks_path); + if current == 0 { + recs.push(Recommendation { + param: "fs.protected_hardlinks".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "未启用硬链接保护,非特权用户可能利用硬链接进行提权攻击".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + } + + if info.param_exists(symlinks_path) { + let current = read_sysctl_u64(symlinks_path); + if current == 0 { + recs.push(Recommendation { + param: "fs.protected_symlinks".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "未启用符号链接保护,存在 TOCTOU 竞态条件攻击风险".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + } + 2 +} + +fn eval_accept_redirects(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/all/accept_redirects"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current == 1 { + recs.push(Recommendation { + param: "net.ipv4.conf.all.accept_redirects".to_string(), + current_value: "1".to_string(), + recommended_value: "0".to_string(), + reason: "接受 ICMP 重定向可被用于中间人攻击,服务器通常不需要此功能".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_sysrq(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/sysrq"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + // sysrq=1 means all functions enabled; high values also enable all + if current != 0 && current != 176 { + // 176 = safe subset (sync + remount-ro + reboot) + recs.push(Recommendation { + param: "kernel.sysrq".to_string(), + current_value: current.to_string(), + recommended_value: "176".to_string(), + reason: "SysRq 功能过于开放,限制为安全子集(同步+只读重挂载+重启)防止滥用" + .to_string(), + confidence: Confidence::Medium, + category: Category::Security, + writable: true, + }); + } + 1 +} + +// ─── Network Performance Rules ─────────────────────────────────────────────── + +fn eval_netdev_max_backlog(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/core/netdev_max_backlog"; + if !info.param_exists(path) { + return 1; + } + if info.max_net_speed() < 10000 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 10000 { + recs.push(Recommendation { + param: "net.core.netdev_max_backlog".to_string(), + current_value: current.to_string(), + recommended_value: "65536".to_string(), + reason: "万兆网卡场景下增大网卡收包队列深度,避免高流量时软中断处理不及导致丢包" + .to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_max_syn_backlog(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_max_syn_backlog"; + if !info.param_exists(path) { + return 1; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current < 8192 { + recs.push(Recommendation { + param: "net.ipv4.tcp_max_syn_backlog".to_string(), + current_value: current.to_string(), + recommended_value: "65536".to_string(), + reason: "增大半连接队列,避免突发连接请求时 SYN 被丢弃".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_rmem_max(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/core/rmem_max"; + if !info.param_exists(path) { + return 1; + } + if info.max_net_speed() < 10000 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 16777216 { + recs.push(Recommendation { + param: "net.core.rmem_max".to_string(), + current_value: current.to_string(), + recommended_value: "16777216".to_string(), + reason: "rmem_max 是 TCP 接收缓冲区的硬上限,不调大它 tcp_rmem 的 max 值不会生效" + .to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_wmem_max(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/core/wmem_max"; + if !info.param_exists(path) { + return 1; + } + if info.max_net_speed() < 10000 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 16777216 { + recs.push(Recommendation { + param: "net.core.wmem_max".to_string(), + current_value: current.to_string(), + recommended_value: "16777216".to_string(), + reason: "wmem_max 是 TCP 发送缓冲区的硬上限,不调大它 tcp_wmem 的 max 值不会生效" + .to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_rmem(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_rmem"; + if !info.param_exists(path) { + return 1; + } + if info.max_net_speed() < 10000 { + return 1; + } + let content = read_sysctl_string(path); + let max_val = content + .split_whitespace() + .nth(2) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + if max_val < 16777216 { + recs.push(Recommendation { + param: "net.ipv4.tcp_rmem".to_string(), + current_value: content, + recommended_value: "4096 131072 16777216".to_string(), + reason: "万兆网卡场景下增大 TCP 接收缓冲区上限,充分利用带宽-延迟积".to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_wmem(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_wmem"; + if !info.param_exists(path) { + return 1; + } + if info.max_net_speed() < 10000 { + return 1; + } + let content = read_sysctl_string(path); + let max_val = content + .split_whitespace() + .nth(2) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + if max_val < 16777216 { + recs.push(Recommendation { + param: "net.ipv4.tcp_wmem".to_string(), + current_value: content, + recommended_value: "4096 65536 16777216".to_string(), + reason: "万兆网卡场景下增大 TCP 发送缓冲区上限,避免大流量传输时发送端瓶颈".to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_slow_start_after_idle(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_slow_start_after_idle"; + if !info.param_exists(path) { + return 1; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 1 { + recs.push(Recommendation { + param: "net.ipv4.tcp_slow_start_after_idle".to_string(), + current_value: "1".to_string(), + recommended_value: "0".to_string(), + reason: "长连接空闲后重新慢启动会造成突发延迟,禁用后保持已探测的拥塞窗口".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_ip_local_port_range(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/ip_local_port_range"; + if !info.param_exists(path) { + return 1; + } + if !info.has_listen_sockets() { + return 1; + } + let content = read_sysctl_string(path); + let parts: Vec = content + .split_whitespace() + .filter_map(|s| s.parse().ok()) + .collect(); + if parts.len() == 2 && parts[1] - parts[0] < 30000 { + recs.push(Recommendation { + param: "net.ipv4.ip_local_port_range".to_string(), + current_value: content, + recommended_value: "1024 65535".to_string(), + reason: "可用临时端口范围过小,高并发短连接场景下可能耗尽端口导致连接失败".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_default_qdisc(info: &SystemInfo, recs: &mut Vec) -> usize { + let cc_path = "/proc/sys/net/ipv4/tcp_congestion_control"; + let qdisc_path = "/proc/sys/net/core/default_qdisc"; + if !info.param_exists(cc_path) || !info.param_exists(qdisc_path) { + return 1; + } + let cc = read_sysctl_string(cc_path); + if cc != "bbr" { + return 1; + } + let qdisc = read_sysctl_string(qdisc_path); + if qdisc == "fq" || qdisc == "fq_codel" { + return 1; + } + + let fq_available = is_qdisc_module_available("sch_fq"); + let fq_codel_available = is_qdisc_module_available("sch_fq_codel"); + let (target, reason) = if fq_available { + ( + "fq", + "BBR 拥塞控制依赖 fq 队列实现精确 pacing,当前 qdisc 会降低 BBR 效果", + ) + } else if fq_codel_available { + ( + "fq_codel", + "BBR 推荐 fq 但当前内核不支持,退而使用 fq_codel(支持部分 pacing)", + ) + } else { + return 1; + }; + recs.push(Recommendation { + param: "net.core.default_qdisc".to_string(), + current_value: qdisc, + recommended_value: target.to_string(), + reason: reason.to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + 1 +} + +fn is_qdisc_module_available(module: &str) -> bool { + // Non-destructive checks only. A previous version probed availability by + // writing the candidate qdisc to net.core.default_qdisc and reading it + // back, which mutated live kernel state as a side effect of read-only + // commands (check/status/list/why/tune --dry-run) — the same bug class + // fixed for is_param_writable in e9425244. Trade-off: qdiscs built + // statically into the kernel (no loadable module, no .ko file) are not + // detected here and won't be recommended, same as before that qdisc + // ever ships as non-modular on the kernels ktuner targets. + if std::path::Path::new(&format!("/sys/module/{module}")).exists() { + return true; + } + if crate::detect::detect_runtime_env() == crate::detect::RuntimeEnv::Container { + return false; + } + let uname = std::fs::read_to_string("/proc/sys/kernel/osrelease").unwrap_or_default(); + let ko_path = format!( + "/lib/modules/{}/kernel/net/sched/{}.ko", + uname.trim(), + module + ); + std::path::Path::new(&ko_path).exists() +} + +fn eval_tcp_tw_reuse(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_tw_reuse"; + if !info.param_exists(path) { + return 1; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_tw_reuse".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: + "允许复用 TIME_WAIT 状态的 socket 建立新的出站连接,减少高并发短连接场景的端口耗尽" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_fin_timeout(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_fin_timeout"; + if !info.param_exists(path) { + return 1; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current > 30 { + recs.push(Recommendation { + param: "net.ipv4.tcp_fin_timeout".to_string(), + current_value: current.to_string(), + recommended_value: "15".to_string(), + reason: "缩短 FIN_WAIT_2 超时时间,加速断开连接的资源回收".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_keepalive_time(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_keepalive_time"; + if !info.param_exists(path) { + return 1; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current > 1800 { + recs.push(Recommendation { + param: "net.ipv4.tcp_keepalive_time".to_string(), + current_value: current.to_string(), + recommended_value: "600".to_string(), + reason: "默认 7200 秒太长,缩短 keepalive 间隔可更早检测失效连接释放资源".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_congestion_control(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_congestion_control"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_string(path); + if current != "bbr" { + let avail_path = "/proc/sys/net/ipv4/tcp_available_congestion_control"; + if info.param_exists(avail_path) { + let available = read_sysctl_string(avail_path); + if available.contains("bbr") { + recs.push(Recommendation { + param: "net.ipv4.tcp_congestion_control".to_string(), + current_value: current, + recommended_value: "bbr".to_string(), + reason: + "BBR 拥塞控制在云网络环境下比 cubic 表现更好,尤其是高延迟和有丢包的链路" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + } + } + 1 +} + +fn eval_nf_conntrack_max(info: &SystemInfo, recs: &mut Vec) -> usize { + if !info.has_conntrack() { + return 1; + } + let path = if info.param_exists("/proc/sys/net/netfilter/nf_conntrack_max") { + "/proc/sys/net/netfilter/nf_conntrack_max" + } else { + "/proc/sys/net/nf_conntrack_max" + }; + let current = read_sysctl_u64(path); + let recommended = if info.memory_total_gb >= 128 { + 2097152 + } else if info.memory_total_gb >= 32 { + 1048576 + } else { + 262144 + }; + if current < recommended { + recs.push(Recommendation { + param: "net.netfilter.nf_conntrack_max".to_string(), + current_value: current.to_string(), + recommended_value: recommended.to_string(), + reason: format!( + "conntrack 表满会导致新连接被丢弃,{}GB 内存建议增大到 {}", + info.memory_total_gb, recommended + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_max_tw_buckets(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_max_tw_buckets"; + if !info.param_exists(path) { + return 1; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current < 200000 { + recs.push(Recommendation { + param: "net.ipv4.tcp_max_tw_buckets".to_string(), + current_value: current.to_string(), + recommended_value: "200000".to_string(), + reason: + "TIME_WAIT bucket 上限过低,高并发短连接场景可能导致 socket 被强制回收引发连接异常" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_keepalive_intvl(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_keepalive_intvl"; + if !info.param_exists(path) { + return 1; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current > 30 { + recs.push(Recommendation { + param: "net.ipv4.tcp_keepalive_intvl".to_string(), + current_value: current.to_string(), + recommended_value: "15".to_string(), + reason: "缩短 keepalive 探测间隔,配合 keepalive_time 更快检测失效连接".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_keepalive_probes(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_keepalive_probes"; + if !info.param_exists(path) { + return 1; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current > 5 { + recs.push(Recommendation { + param: "net.ipv4.tcp_keepalive_probes".to_string(), + current_value: current.to_string(), + recommended_value: "5".to_string(), + reason: "减少 keepalive 探测次数,默认 9 次太多,5 次足以确认连接失效".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_no_metrics_save(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_no_metrics_save"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_no_metrics_save".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: + "TCP 连接关闭后缓存的路由指标可能过时,新连接继承错误的拥塞窗口大小导致性能异常" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_mtu_probing(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_mtu_probing"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_mtu_probing".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "启用 MTU 探测避免 PMTU 黑洞问题,某些网络路径会丢弃大包导致连接卡住" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +// ─── VM/Memory Rules ───────────────────────────────────────────────────────── + +fn eval_max_map_count(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/max_map_count"; + if !info.param_exists(path) { + return 1; + } + let needs_high = info.has_process("java") + || info.has_process("elasticsearch") + || info.has_process_exact("node"); // exact: avoid matching node_exporter + if !needs_high { + return 1; + } + let current = read_sysctl_u64(path); + if current < 262144 { + recs.push(Recommendation { + param: "vm.max_map_count".to_string(), + current_value: current.to_string(), + recommended_value: "262144".to_string(), + reason: "JVM/Node 进程需要大量内存映射,max_map_count 过低会导致 mmap 失败".to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_zone_reclaim_mode(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/zone_reclaim_mode"; + if !info.param_exists(path) { + return 1; + } + if info.numa_nodes <= 1 { + return 1; + } + let current = read_sysctl_u64(path); + if current != 0 { + recs.push(Recommendation { + param: "vm.zone_reclaim_mode".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: "多 NUMA 节点下 zone_reclaim 会导致频繁本地回收而非跨节点分配,增大延迟抖动" + .to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_vfs_cache_pressure(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/vfs_cache_pressure"; + if !info.param_exists(path) { + return 1; + } + let is_db_or_cache = info.has_process("postgres") + || info.has_process("mysqld") + || info.has_process("clickhouse") + || info.has_process("redis-server") + || info.has_process("memcached") + || info.has_process("etcd") + || info.has_process("elasticsearch"); + if !is_db_or_cache { + return 1; + } + let current = read_sysctl_u64(path); + if current > 60 { + recs.push(Recommendation { + param: "vm.vfs_cache_pressure".to_string(), + current_value: current.to_string(), + recommended_value: "50".to_string(), + reason: "数据库/缓存场景降低 VFS 缓存回收压力,保留更多 dentry/inode 缓存减少查找开销" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_watermark_scale_factor(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/watermark_scale_factor"; + if !info.param_exists(path) { + return 1; + } + if info.memory_total_gb < 32 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 200 { + recs.push(Recommendation { + param: "vm.watermark_scale_factor".to_string(), + current_value: current.to_string(), + recommended_value: "200".to_string(), + reason: "大内存机器增大水位线间距,让 kswapd 更早唤醒减少直接回收触发的概率" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_file_max(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/fs/file-max"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current < 1000000 { + recs.push(Recommendation { + param: "fs.file-max".to_string(), + current_value: current.to_string(), + recommended_value: "2000000".to_string(), + reason: "系统级文件描述符上限过低,高并发网络服务或大量文件操作可能触及限制" + .to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_overcommit_memory(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/overcommit_memory"; + if !info.param_exists(path) { + return 1; + } + let needs_overcommit = info.has_process("redis-server"); + if !needs_overcommit { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "vm.overcommit_memory".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "Redis 使用 fork 进行 RDB/AOF 持久化,overcommit_memory=0 可能导致 fork 失败" + .to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +// ─── IO/Disk Rules ─────────────────────────────────────────────────────────── + +fn eval_read_ahead_kb(info: &SystemInfo, recs: &mut Vec) -> usize { + let is_streaming = info.has_process("kafka") + || info.has_process("flink") + || info.has_process("spark") + || info.has_process("hadoop"); + + let is_db = info.has_process("postgres") + || info.has_process("mysqld") + || info.has_process("mongod") + || info.has_process("clickhouse"); + + for disk in &info.disks { + match disk.disk_type { + DiskType::HDD if is_streaming && disk.read_ahead_kb < 2048 => { + recs.push(Recommendation { + param: format!("block/{}/read_ahead_kb", disk.name), + current_value: disk.read_ahead_kb.to_string(), + recommended_value: "2048".to_string(), + reason: "机械硬盘顺序读取场景,增大预读窗口提升吞吐(减少磁头寻道次数)" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + DiskType::NVMe | DiskType::SSD if is_db && disk.read_ahead_kb > 128 => { + recs.push(Recommendation { + param: format!("block/{}/read_ahead_kb", disk.name), + current_value: disk.read_ahead_kb.to_string(), + recommended_value: "128".to_string(), + reason: + "数据库随机 IO 为主,过大的预读会浪费内存和 IO 带宽,NVMe/SSD 延迟已经很低" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + _ => {} + } + } + 1 +} + +fn eval_nr_requests(info: &SystemInfo, recs: &mut Vec) -> usize { + for disk in &info.disks { + if disk.disk_type == DiskType::NVMe && disk.nr_requests < 256 { + recs.push(Recommendation { + param: format!("block/{}/nr_requests", disk.name), + current_value: disk.nr_requests.to_string(), + recommended_value: "1024".to_string(), + reason: "NVMe 硬件队列深度大,增大软件请求队列避免高并发 IO 时提前拥塞".to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + } + 1 +} + +fn eval_rq_affinity(info: &SystemInfo, recs: &mut Vec) -> usize { + if info.numa_nodes <= 1 { + return 1; + } + for disk in &info.disks { + if disk.disk_type == DiskType::NVMe && disk.rq_affinity != 2 { + recs.push(Recommendation { + param: format!("block/{}/rq_affinity", disk.name), + current_value: disk.rq_affinity.to_string(), + recommended_value: "2".to_string(), + reason: "多 NUMA 节点下强制 IO 完成中断在提交 CPU 上处理,减少跨节点内存访问" + .to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + } + 1 +} + +// ─── CPU/Scheduler Rules ───────────────────────────────────────────────────── + +fn eval_numa_balancing(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/numa_balancing"; + if !info.param_exists(path) { + return 1; + } + if info.numa_nodes <= 1 { + return 1; + } + let is_db = info.has_process("postgres") + || info.has_process("mysqld") + || info.has_process("mongod") + || info.has_process("clickhouse"); + if !is_db { + return 1; + } + let current = read_sysctl_u64(path); + if current == 1 { + recs.push(Recommendation { + param: "kernel.numa_balancing".to_string(), + current_value: "1".to_string(), + recommended_value: "0".to_string(), + reason: "数据库进程自行管理内存亲和性,内核 NUMA balancing 的页面迁移会引发延迟抖动" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_sched_autogroup(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/sched_autogroup_enabled"; + if !info.param_exists(path) { + return 1; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 1 { + recs.push(Recommendation { + param: "kernel.sched_autogroup_enabled".to_string(), + current_value: "1".to_string(), + recommended_value: "0".to_string(), + reason: "服务器环境下 autogroup 按 TTY 分组调度不适用,关闭后避免不合理的 CPU 带宽分配" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_pid_max(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/pid_max"; + if !info.param_exists(path) { + return 1; + } + if info.cpu_cores <= 32 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 4194304 { + recs.push(Recommendation { + param: "kernel.pid_max".to_string(), + current_value: current.to_string(), + recommended_value: "4194304".to_string(), + reason: format!( + "{}核 CPU 并发进程/线程量大,默认 pid_max 可能不够用", + info.cpu_cores + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_sched_migration_cost(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/sched_migration_cost_ns"; + if !info.param_exists(path) { + return 1; + } + if info.cpu_cores <= 16 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 5000000 { + recs.push(Recommendation { + param: "kernel.sched_migration_cost_ns".to_string(), + current_value: current.to_string(), + recommended_value: "5000000".to_string(), + reason: format!( + "{}核机器线程迁移开销大,增大 migration_cost 让调度器倾向于保持线程在同一 CPU 上运行", + info.cpu_cores + ), + confidence: Confidence::Medium, + category: Category::Performance, writable: true, + }); + } + 1 +} + +// ─── Additional Security Rules ─────────────────────────────────────────────── + +fn eval_tcp_syncookies(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_syncookies"; + if !info.param_exists(path) { + return 1; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_syncookies".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "未启用 SYN Cookie 防护,遭受 SYN Flood 时半连接队列会迅速溢出导致拒绝服务" + .to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_send_redirects(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/all/send_redirects"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current == 1 { + recs.push(Recommendation { + param: "net.ipv4.conf.all.send_redirects".to_string(), + current_value: "1".to_string(), + recommended_value: "0".to_string(), + reason: "服务器不应发送 ICMP 重定向,避免被利用进行网络拓扑探测或路由劫持".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_perf_event_paranoid(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/perf_event_paranoid"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current < 2 { + recs.push(Recommendation { + param: "kernel.perf_event_paranoid".to_string(), + current_value: current.to_string(), + recommended_value: "2".to_string(), + reason: "perf_event 权限过于宽松,非特权用户可采集性能计数器信息辅助侧信道攻击" + .to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_rp_filter(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/default/rp_filter"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.conf.default.rp_filter".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "未启用反向路径过滤,攻击者可伪造源 IP 进行欺骗(注意:非对称/多路径路由、部分 VPN 场景需保持 0)".to_string(), + confidence: Confidence::Medium, + category: Category::Security, writable: true, + }); + } + 1 +} + +fn eval_panic(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/panic"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "kernel.panic".to_string(), + current_value: "0".to_string(), + recommended_value: "10".to_string(), + reason: "内核 panic 后不自动重启,服务器会一直挂起直到人工干预。设为 10 秒后自动重启" + .to_string(), + confidence: Confidence::Medium, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_panic_on_oom(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/panic_on_oom"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current != 0 { + recs.push(Recommendation { + param: "vm.panic_on_oom".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: "OOM 时应让 OOM killer 杀进程而非触发 kernel panic,保持系统可用性".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_dirty_expire_centisecs(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/dirty_expire_centisecs"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current > 1500 { + recs.push(Recommendation { + param: "vm.dirty_expire_centisecs".to_string(), + current_value: current.to_string(), + recommended_value: "1500".to_string(), + reason: "缩短脏页过期时间,避免长时间积压导致突发刷盘造成 IO 延迟尖刺".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_dirty_writeback_centisecs(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/dirty_writeback_centisecs"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current > 500 { + recs.push(Recommendation { + param: "vm.dirty_writeback_centisecs".to_string(), + current_value: current.to_string(), + recommended_value: "500".to_string(), + reason: "缩短回写线程唤醒间隔,更及时地将脏页刷到磁盘".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +/// Whether the host appears to legitimately need IP forwarding (router, +/// hypervisor, container host, VPN/NAT gateway). Disabling forwarding on such a +/// host silently breaks guest/pod/tunnel traffic, so we must be conservative +/// and only suggest turning it off when we see no sign forwarding is in use. +fn host_needs_ip_forward(info: &SystemInfo) -> bool { + const PROCS: &[&str] = &[ + // container runtimes + "docker", + "dockerd", + "kubelet", + "containerd", + "podman", + "conmon", + "crio", + "k3s", + "lxc", + "lxd", + // virtualization + "libvirtd", + "qemu", + "virtqemud", + "vhost", + // VPN / tunneling + "openvpn", + "wireguard", + "charon", + "strongswan", + "tincd", + "tailscaled", + // routing daemons + "bird", + "bird2", + "zebra", + "bgpd", + "ospfd", + "frr", + "quagga", + ]; + if PROCS.iter().any(|p| info.has_process(p)) { + return true; + } + // Bridge / tunnel / virtual interfaces are a strong signal of VM, container + // or VPN networking. (info.network filters these out, so scan directly.) + if let Ok(entries) = std::fs::read_dir("/sys/class/net") { + for e in entries.filter_map(|e| e.ok()) { + let n = e.file_name().to_string_lossy().to_string(); + if n.starts_with("virbr") + || n.starts_with("docker") + || n.starts_with("br-") + || n.starts_with("cni") + || n.starts_with("flannel") + || n.starts_with("cali") + || n.starts_with("tun") + || n.starts_with("tap") + || n.starts_with("wg") + || n.starts_with("vxlan") + || n == "br0" + || n == "br1" + { + return true; + } + } + } + false +} + +/// Whether the host uses link bonding. The ARP-tuning rules key off "2+ NICs", +/// but bond members are multiple NICs forming ONE logical link where settings +/// like arp_filter/arp_ignore can break the bond, so they must skip bonded hosts. +fn has_bond() -> bool { + if std::path::Path::new("/proc/net/bonding").is_dir() { + return true; + } + if let Ok(entries) = std::fs::read_dir("/sys/class/net") { + for e in entries.filter_map(|e| e.ok()) { + if e.file_name().to_string_lossy().starts_with("bond") { + return true; + } + } + } + false +} + +fn eval_ip_forward(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/ip_forward"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current == 1 && !host_needs_ip_forward(info) { + recs.push(Recommendation { + param: "net.ipv4.ip_forward".to_string(), + current_value: "1".to_string(), + recommended_value: "0".to_string(), + reason: "未检测到容器/虚拟化/VPN/路由用途,关闭 IP 转发可防止被用作中间人或跳板(若本机需转发请忽略)".to_string(), + confidence: Confidence::Medium, + category: Category::Security, writable: true, + }); + } + 1 +} + +fn eval_unprivileged_bpf(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/unprivileged_bpf_disabled"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "kernel.unprivileged_bpf_disabled".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "非特权用户可加载 BPF 程序存在提权风险,应禁止".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_core_uses_pid(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/core_uses_pid"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "kernel.core_uses_pid".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "core dump 文件名应包含 PID,避免多进程崩溃时相互覆盖导致调试信息丢失" + .to_string(), + confidence: Confidence::Medium, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_yama_ptrace_scope(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/yama/ptrace_scope"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "kernel.yama.ptrace_scope".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "限制 ptrace 仅允许父进程调试子进程,防止任意进程注入攻击".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_log_martians(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/all/log_martians"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.conf.all.log_martians".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "启用火星包日志记录,帮助检测 IP 地址欺骗和路由异常".to_string(), + confidence: Confidence::Medium, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_tcp_max_orphans(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_max_orphans"; + if !info.param_exists(path) { + return 1; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + let recommended = if info.memory_total_gb >= 128 { + 262144 + } else if info.memory_total_gb >= 32 { + 131072 + } else { + 65536 + }; + if current < recommended { + recs.push(Recommendation { + param: "net.ipv4.tcp_max_orphans".to_string(), + current_value: current.to_string(), + recommended_value: recommended.to_string(), + reason: format!( + "孤儿 TCP 连接上限过低({}GB 内存建议 {}),超限后连接被直接 RST 导致服务中断", + info.memory_total_gb, recommended + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_threads_max(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/threads-max"; + if !info.param_exists(path) { + return 1; + } + if info.cpu_cores <= 32 { + return 1; + } + let current = read_sysctl_u64(path); + let recommended = (info.cpu_cores as u64 * 8192).min(4194304); + if current < recommended / 2 { + recs.push(Recommendation { + param: "kernel.threads-max".to_string(), + current_value: current.to_string(), + recommended_value: recommended.to_string(), + reason: format!( + "{}核机器最大线程数偏低,大量线程创建时可能触及限制导致 fork/clone 失败", + info.cpu_cores + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_nr_hugepages(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/nr_hugepages"; + if !info.param_exists(path) { + return 1; + } + let needs_hugepages = info.has_process("postgres") || info.has_process("mysqld"); + if !needs_hugepages { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 && info.memory_total_gb >= 16 { + let recommended = info.memory_total_gb * 1024 / 4 / 2; + recs.push(Recommendation { + param: "vm.nr_hugepages".to_string(), + current_value: "0".to_string(), + recommended_value: recommended.to_string(), + reason: + "数据库未启用 HugePages,启用后可减少 TLB miss 和页表开销,提升内存密集型查询性能" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_shmmax(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/shmmax"; + if !info.param_exists(path) { + return 1; + } + let needs_shm = info.has_process("postgres") || info.has_process("mysqld"); + if !needs_shm { + return 1; + } + let current = read_sysctl_u64(path); + let mem_bytes = info.memory_total_gb * 1024 * 1024 * 1024; + let recommended = mem_bytes / 2; + if current < recommended { + recs.push(Recommendation { + param: "kernel.shmmax".to_string(), + current_value: current.to_string(), + recommended_value: recommended.to_string(), + reason: "数据库使用共享内存进行缓冲池管理,shmmax 过低会限制可用的共享内存段大小" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_timestamps(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_timestamps"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_timestamps".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "TCP 时间戳用于精确 RTT 计算和 PAWS 防序号回绕,关闭会影响性能和可靠性" + .to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_window_scaling(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_window_scaling"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_window_scaling".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "关闭窗口缩放会限制 TCP 窗口最大 64KB,无法利用高带宽网络".to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_ecn(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_ecn"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 && info.has_listen_sockets() { + recs.push(Recommendation { + param: "net.ipv4.tcp_ecn".to_string(), + current_value: "0".to_string(), + recommended_value: "2".to_string(), + reason: "ECN(显式拥塞通知)可在不丢包的情况下感知拥塞,设 2 表示仅在对端请求时启用" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_sack(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_sack"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_sack".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "选择性确认(SACK)允许接收方告知发送方哪些段已收到,大幅减少不必要的重传" + .to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_netdev_budget(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/core/netdev_budget"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if info.max_net_speed() >= 10000 && current < 600 { + recs.push(Recommendation { + param: "net.core.netdev_budget".to_string(), + current_value: current.to_string(), + recommended_value: "600".to_string(), + reason: "万兆网络每次 NAPI 轮询处理的最大包数不够,增大可降低软中断频率提升吞吐" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_busy_poll(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/core/busy_poll"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + let is_latency_sensitive = info.has_process("redis-server") + || info.has_process("memcached") + || info.has_process("nginx") + || info.has_process("clickhouse"); + if is_latency_sensitive && current == 0 { + recs.push(Recommendation { + param: "net.core.busy_poll".to_string(), + current_value: "0".to_string(), + recommended_value: "50".to_string(), + reason: "延迟敏感服务开启 busy polling 可让 CPU 主动轮询网卡,减少中断延迟(微秒级)" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_retries2(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_retries2"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current > 8 { + recs.push(Recommendation { + param: "net.ipv4.tcp_retries2".to_string(), + current_value: current.to_string(), + recommended_value: "8".to_string(), + reason: format!( + "TCP 重传 {} 次才放弃(约 {}分钟),缩短到 8 次可更快检测断连释放资源", + current, + if current >= 15 { "13-30" } else { "6-13" } + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +// NOTE: tcp_abort_on_overflow rule removed. The kernel docs explicitly advise +// against enabling it ("in general it harms the clients"): the default 0 +// (drop the SYN-ACK so the client retransmits) rides out transient accept-queue +// bursts, whereas 1 turns every burst into a hard RST that fails the client. + +fn eval_tcp_syn_retries(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_syn_retries"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current > 3 { + recs.push(Recommendation { + param: "net.ipv4.tcp_syn_retries".to_string(), + current_value: current.to_string(), + recommended_value: "3".to_string(), + reason: format!( + "SYN 重试 {current} 次才放弃(超时约 30 秒),减少到 3 次(约 15 秒)可加速不可达主机的连接失败检测" + ), + confidence: Confidence::Medium, + category: Category::Performance, writable: true, + }); + } + 1 +} + +fn eval_tcp_synack_retries(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_synack_retries"; + if !info.param_exists(path) { + return 1; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current > 3 { + recs.push(Recommendation { + param: "net.ipv4.tcp_synack_retries".to_string(), + current_value: current.to_string(), + recommended_value: "3".to_string(), + reason: format!( + "SYNACK 重试 {current} 次太多,减少到 3 次可更快释放半开连接资源,降低 SYN flood 影响" + ), + confidence: Confidence::Medium, + category: Category::Performance, writable: true, + }); + } + 1 +} + +fn eval_optmem_max(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/core/optmem_max"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current < 81920 { + recs.push(Recommendation { + param: "net.core.optmem_max".to_string(), + current_value: current.to_string(), + recommended_value: "81920".to_string(), + reason: "套接字辅助缓冲区默认值偏小,增大可支持更多控制消息和套接字选项".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_oom_kill_allocating_task(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/oom_kill_allocating_task"; + if !std::path::Path::new(path).exists() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "vm.oom_kill_allocating_task".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "OOM 时优先杀死触发分配的进程而非遍历进程列表选择目标,响应更快更可预测" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_inotify_max_user_watches(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/fs/inotify/max_user_watches"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current < 524288 { + recs.push(Recommendation { + param: "fs.inotify.max_user_watches".to_string(), + current_value: current.to_string(), + recommended_value: "524288".to_string(), + reason: format!( + "inotify watch 上限 {current} 偏低,文件监控/IDE/构建工具可能报 'no space left on device' 错误" + ), + confidence: Confidence::High, + category: Category::Performance, writable: true, + }); + } + 1 +} + +fn eval_aio_max_nr(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/fs/aio-max-nr"; + if !info.param_exists(path) { + return 1; + } + let current = read_sysctl_u64(path); + if current < 1048576 { + recs.push(Recommendation { + param: "fs.aio-max-nr".to_string(), + current_value: current.to_string(), + recommended_value: "1048576".to_string(), + reason: "异步 IO 请求上限偏低,数据库和高并发 IO 场景可能触及限制导致 IO 提交失败" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_dirty_background_ratio( + info: &SystemInfo, + workload: &WorkloadType, + recs: &mut Vec, +) -> usize { + let path = "/proc/sys/vm/dirty_background_ratio"; + if !info.param_exists(path) { + return 1; + } + // >=64GB hosts use dirty_background_bytes instead (mutually exclusive with + // the ratio form in the kernel); avoid recommending both. + if info.memory_total_gb >= 64 { + return 1; + } + let current = info.sysctl.dirty_background_ratio; + let target = match workload { + WorkloadType::IoLatency => 3, + _ => 5, + }; + if current > target as u64 { + let has_db = info.has_process("postgres") + || info.has_process("mysqld") + || info.has_process("mongod") + || info.has_process("clickhouse"); + if has_db || matches!(workload, WorkloadType::IoLatency) || current > 10 { + recs.push(Recommendation { + param: "vm.dirty_background_ratio".to_string(), + current_value: current.to_string(), + recommended_value: target.to_string(), + reason: format!( + "后台回写触发阈值偏高({current}%),脏页堆积后突发刷盘会造成 IO 延迟尖刺" + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + } + 1 +} + +fn eval_sched_min_granularity(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/sched_min_granularity_ns"; + if !info.param_exists(path) { + return 0; + } + let current = read_sysctl_u64(path); + if info.cpu_cores > 32 && current < 10_000_000 { + recs.push(Recommendation { + param: "kernel.sched_min_granularity_ns".to_string(), + current_value: current.to_string(), + recommended_value: "10000000".to_string(), + reason: format!( + "{}核机器上调度器切换过频,增大最小调度粒度可减少上下文切换开销", + info.cpu_cores + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_icmp_echo_ignore_broadcasts(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.icmp_echo_ignore_broadcasts".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "未忽略广播 ICMP 请求,存在 Smurf 放大攻击风险".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_accept_source_route(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/all/accept_source_route"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current != 0 { + recs.push(Recommendation { + param: "net.ipv4.conf.all.accept_source_route".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: "允许源路由可被攻击者用于绕过网络安全策略和进行路由欺骗".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_tcp_rfc1337(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_rfc1337"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_rfc1337".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "TIME_WAIT 状态的连接可被伪造的 RST 报文异常终止,启用此保护可防止此类攻击" + .to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_secure_redirects(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/all/secure_redirects"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current != 0 { + recs.push(Recommendation { + param: "net.ipv4.conf.all.secure_redirects".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: "即使来自默认网关的 ICMP 重定向也不应接受,服务器无需动态修改路由表" + .to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_mmap_min_addr(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/mmap_min_addr"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 65536 { + recs.push(Recommendation { + param: "vm.mmap_min_addr".to_string(), + current_value: current.to_string(), + recommended_value: "65536".to_string(), + reason: + "最小 mmap 地址过低,用户态程序可映射低地址空间,增加 NULL 指针解引用漏洞利用风险" + .to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_default_accept_redirects(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/default/accept_redirects"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current != 0 { + recs.push(Recommendation { + param: "net.ipv4.conf.default.accept_redirects".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: "新创建网络接口默认接受 ICMP 重定向,攻击者可借此劫持流量".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_default_accept_source_route(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/default/accept_source_route"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current != 0 { + recs.push(Recommendation { + param: "net.ipv4.conf.default.accept_source_route".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: "新创建网络接口默认允许源路由,攻击者可绕过网络安全策略".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_sched_latency_ns(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/sched_latency_ns"; + if !info.param_exists(path) { + return 0; + } + if info.cpu_cores <= 32 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 12000000 { + recs.push(Recommendation { + param: "kernel.sched_latency_ns".to_string(), + current_value: current.to_string(), + recommended_value: "24000000".to_string(), + reason: format!( + "大核数 ({} 核) 服务器增大 CFS 调度周期可减少上下文切换开销", + info.cpu_cores + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_challenge_ack_limit(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_challenge_ack_limit"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current <= 100 { + recs.push(Recommendation { + param: "net.ipv4.tcp_challenge_ack_limit".to_string(), + current_value: current.to_string(), + recommended_value: "999999999".to_string(), + reason: + "默认值 100 存在 CVE-2016-5696 边信道攻击风险,攻击者可推断 TCP 连接状态并注入数据" + .to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_rp_filter_all(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/all/rp_filter"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.conf.all.rp_filter".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "未在所有接口启用反向路径过滤,攻击者可伪造源 IP 欺骗(注意:非对称/多路径路由场景需保持 0)".to_string(), + confidence: Confidence::Medium, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_busy_read(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/core/busy_read"; + if !info.param_exists(path) { + return 0; + } + let max_speed = info.network.iter().map(|n| n.speed_mbps).max().unwrap_or(0); + if max_speed < 10000 { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.core.busy_read".to_string(), + current_value: "0".to_string(), + recommended_value: "50".to_string(), + reason: "万兆网络启用忙轮询读可降低网络延迟(用少量 CPU 换取更低的收包延迟)" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_nmi_watchdog(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/nmi_watchdog"; + if !info.param_exists(path) { + return 0; + } + let current = read_sysctl_u64(path); + if current == 1 { + recs.push(Recommendation { + param: "kernel.nmi_watchdog".to_string(), + current_value: "1".to_string(), + recommended_value: "0".to_string(), + reason: "NMI watchdog 每核消耗一个 PMU 计数器和定期中断,服务器禁用可节省 CPU 资源" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_stat_interval(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/stat_interval"; + if !info.param_exists(path) { + return 0; + } + if info.cpu_cores < 32 { + return 1; + } + let current = read_sysctl_u64(path); + if current <= 1 { + recs.push(Recommendation { + param: "vm.stat_interval".to_string(), + current_value: current.to_string(), + recommended_value: "5".to_string(), + reason: "大核数机器上 vmstat 每秒更新开销大,增大间隔可减少 CPU 缓存行争用".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_hung_task_timeout(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/hung_task_timeout_secs"; + if !info.param_exists(path) { + return 0; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "kernel.hung_task_timeout_secs".to_string(), + current_value: "0".to_string(), + recommended_value: "120".to_string(), + reason: "hung_task 检测已禁用,无法发现卡死的内核任务(可能是 IO 阻塞或死锁)" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_netdev_budget_usecs(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/core/netdev_budget_usecs"; + if !info.param_exists(path) { + return 0; + } + let max_speed = info.network.iter().map(|n| n.speed_mbps).max().unwrap_or(0); + if max_speed < 10000 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 4000 { + recs.push(Recommendation { + param: "net.core.netdev_budget_usecs".to_string(), + current_value: current.to_string(), + recommended_value: "8000".to_string(), + reason: "万兆网络下 NAPI 轮询时间预算不足,可能导致频繁退出轮询增加中断开销" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_dirty_bytes(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/dirty_bytes"; + if !info.param_exists(path) { + return 0; + } + if info.memory_total_gb < 64 { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + let recommended = 256 * 1024 * 1024; // 256MB + recs.push(Recommendation { + param: "vm.dirty_bytes".to_string(), + current_value: "0".to_string(), + recommended_value: recommended.to_string(), + reason: format!("大内存服务器 ({} GB) 使用 dirty_ratio 百分比会导致脏页过多、IO 突刺,改用固定字节限制更平稳", info.memory_total_gb), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_sched_child_runs_first(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/sched_child_runs_first"; + if !info.param_exists(path) { + return 0; + } + let has_server = info.has_process("nginx") + || info.has_process("httpd") + || info.has_process("postgres") + || info.has_process("mysqld"); + if !has_server { + return 1; + } + let current = read_sysctl_u64(path); + if current != 0 { + recs.push(Recommendation { + param: "kernel.sched_child_runs_first".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: "服务器场景下 fork 后父进程先运行更优,避免 COW 页面不必要的复制".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_page_cluster(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/page-cluster"; + if !info.param_exists(path) { + return 0; + } + let has_ssd = info + .disks + .iter() + .any(|d| matches!(d.disk_type, DiskType::NVMe | DiskType::SSD)); + if !has_ssd { + return 1; + } + let current = read_sysctl_u64(path); + if current > 0 { + recs.push(Recommendation { + param: "vm.page-cluster".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: "SSD 上关闭 swap 预读可避免不必要的 IO,SSD 的随机读取延迟极低无需预读优化" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_rmem_default(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/core/rmem_default"; + if !info.param_exists(path) { + return 0; + } + let max_speed = info.network.iter().map(|n| n.speed_mbps).max().unwrap_or(0); + if max_speed < 10000 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 262144 { + recs.push(Recommendation { + param: "net.core.rmem_default".to_string(), + current_value: current.to_string(), + recommended_value: "262144".to_string(), + reason: "万兆网络默认 socket 接收缓冲区过小,新建连接可能需要动态扩展缓冲区增加延迟" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_wmem_default(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/core/wmem_default"; + if !info.param_exists(path) { + return 0; + } + let max_speed = info.network.iter().map(|n| n.speed_mbps).max().unwrap_or(0); + if max_speed < 10000 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 262144 { + recs.push(Recommendation { + param: "net.core.wmem_default".to_string(), + current_value: current.to_string(), + recommended_value: "262144".to_string(), + reason: "万兆网络默认 socket 发送缓冲区过小,新建连接初始发送性能受限".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_sched_nr_migrate(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/sched_nr_migrate"; + if !info.param_exists(path) { + return 0; + } + if info.cpu_cores <= 32 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 128 { + recs.push(Recommendation { + param: "kernel.sched_nr_migrate".to_string(), + current_value: current.to_string(), + recommended_value: "128".to_string(), + reason: format!( + "{}核 CPU 每次负载均衡仅迁移 {} 个任务,增大可加速核间负载均衡收敛", + info.cpu_cores, current + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_notsent_lowat(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_notsent_lowat"; + if !info.param_exists(path) { + return 0; + } + let current = read_sysctl_u64(path); + if current > 131072 { + recs.push(Recommendation { + param: "net.ipv4.tcp_notsent_lowat".to_string(), + current_value: current.to_string(), + recommended_value: "131072".to_string(), + reason: "默认值过大导致每个 TCP 连接可能缓存大量未发送数据浪费内存,设为 128KB 可降低内存占用并减少延迟".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_dsack(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_dsack"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_dsack".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "D-SACK 帮助发送方精确识别虚假重传,关闭会导致不必要的重传和带宽浪费" + .to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_unix_max_dgram_qlen(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/unix/max_dgram_qlen"; + if !info.param_exists(path) { + return 0; + } + let current = read_sysctl_u64(path); + if current < 1024 { + recs.push(Recommendation { + param: "net.unix.max_dgram_qlen".to_string(), + current_value: current.to_string(), + recommended_value: "1024".to_string(), + reason: "Unix socket 数据报队列默认 512 太小,systemd/journald 等高负载下可能丢失消息" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_rps_sock_flow_entries(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/core/rps_sock_flow_entries"; + if !info.param_exists(path) { + return 0; + } + let max_speed = info.network.iter().map(|n| n.speed_mbps).max().unwrap_or(0); + if max_speed < 10000 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 32768 { + recs.push(Recommendation { + param: "net.core.rps_sock_flow_entries".to_string(), + current_value: current.to_string(), + recommended_value: "32768".to_string(), + reason: "万兆网络启用 RFS 流分发表可将网络处理分散到多核,减少 CPU 热点提升吞吐" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_neigh_gc_thresh3(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/neigh/default/gc_thresh3"; + if !info.param_exists(path) { + return 0; + } + let current = read_sysctl_u64(path); + if current < 4096 { + recs.push(Recommendation { + param: "net.ipv4.neigh.default.gc_thresh3".to_string(), + current_value: current.to_string(), + recommended_value: "8192".to_string(), + reason: "ARP 表上限过低,大规模网络下可能触发 Neighbour table overflow 导致网络中断" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_neigh_gc_thresh1(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/neigh/default/gc_thresh1"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 2048 { + recs.push(Recommendation { + param: "net.ipv4.neigh.default.gc_thresh1".to_string(), + current_value: current.to_string(), + recommended_value: "2048".to_string(), + reason: "ARP 表 GC 起始阈值过低,频繁触发垃圾回收影响网络性能".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_neigh_gc_thresh2(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/neigh/default/gc_thresh2"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 4096 { + recs.push(Recommendation { + param: "net.ipv4.neigh.default.gc_thresh2".to_string(), + current_value: current.to_string(), + recommended_value: "4096".to_string(), + reason: "ARP 表软上限过低,超过后条目存活时间缩短为 5 秒,高连接数场景会频繁 ARP 解析" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_retries1(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_retries1"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current > 3 { + recs.push(Recommendation { + param: "net.ipv4.tcp_retries1".to_string(), + current_value: current.to_string(), + recommended_value: "3".to_string(), + reason: "TCP 重传次数过多才通知网络层,延迟路由切换和 PMTU 发现".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_limit_output_bytes(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_limit_output_bytes"; + if !info.param_exists(path) { + return 0; + } + let current = read_sysctl_u64(path); + let max_speed = info.network.iter().map(|n| n.speed_mbps).max().unwrap_or(0); + if max_speed >= 10000 && current < 524288 { + recs.push(Recommendation { + param: "net.ipv4.tcp_limit_output_bytes".to_string(), + current_value: current.to_string(), + recommended_value: "1048576".to_string(), + reason: "万兆网络下 TCP 输出限制过低,限制了单连接吞吐量".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_dev_weight(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/core/dev_weight"; + if !info.param_exists(path) { + return 0; + } + let max_speed = info.network.iter().map(|n| n.speed_mbps).max().unwrap_or(0); + if max_speed < 10000 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 128 { + recs.push(Recommendation { + param: "net.core.dev_weight".to_string(), + current_value: current.to_string(), + recommended_value: "128".to_string(), + reason: "万兆网络下 NAPI 每次轮询 TX 处理包数过少,增大可提升发送吞吐量".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_printk(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/printk"; + if !std::path::Path::new(path).exists() { + return 0; + } + let content = std::fs::read_to_string(path).unwrap_or_default(); + let level: u64 = content + .split_whitespace() + .next() + .and_then(|s| s.parse().ok()) + .unwrap_or(4); + if level > 4 { + recs.push(Recommendation { + param: "kernel.printk".to_string(), + current_value: level.to_string(), + recommended_value: "4".to_string(), + reason: "内核控制台日志级别过高,大量非关键消息输出到控制台影响性能".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_watchdog_thresh(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/watchdog_thresh"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.cpu_cores < 32 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 20 && current > 0 { + recs.push(Recommendation { + param: "kernel.watchdog_thresh".to_string(), + current_value: current.to_string(), + recommended_value: "30".to_string(), + reason: "大核数系统负载高时 watchdog 阈值过低容易触发误报软死锁告警".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_admin_reserve_kbytes(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/admin_reserve_kbytes"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.memory_total_gb < 64 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 131072 { + recs.push(Recommendation { + param: "vm.admin_reserve_kbytes".to_string(), + current_value: current.to_string(), + recommended_value: "131072".to_string(), + reason: "大内存机器管理员保留内存过少,OOM 时可能无法登录排查问题".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_nr_open(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/fs/nr_open"; + if !info.param_exists(path) { + return 0; + } + let current = read_sysctl_u64(path); + if current < 1048576 { + recs.push(Recommendation { + param: "fs.nr_open".to_string(), + current_value: current.to_string(), + recommended_value: "1048576".to_string(), + reason: "进程级文件描述符硬上限过低,高并发服务可能无法设置足够大的 ulimit".to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_arp_announce(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/all/arp_announce"; + if !info.param_exists(path) { + return 0; + } + if info.network.len() < 2 || has_bond() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.conf.all.arp_announce".to_string(), + current_value: "0".to_string(), + recommended_value: "2".to_string(), + reason: "多网卡环境下 ARP 回复可能使用错误接口的 IP,导致通信异常和 ARP 表污染" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_arp_ignore(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/all/arp_ignore"; + if !info.param_exists(path) { + return 0; + } + if info.network.len() < 2 || has_bond() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.conf.all.arp_ignore".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "多网卡环境下默认回复所有接口的 ARP 请求,可能导致流量走错网卡和路由异常" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_default_log_martians(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/default/log_martians"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.conf.default.log_martians".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "新创建的网络接口不会记录火星包日志,可能错过 IP 欺骗和路由异常".to_string(), + confidence: Confidence::Medium, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_laptop_mode(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/laptop_mode"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current != 0 && info.memory_total_gb >= 16 { + recs.push(Recommendation { + param: "vm.laptop_mode".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: "服务器环境启用了笔记本省电模式,会延迟磁盘写入增加数据丢失风险".to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_adv_win_scale(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_adv_win_scale"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = std::fs::read_to_string(path) + .unwrap_or_default() + .trim() + .parse::() + .unwrap_or(1); + if current < 2 { + recs.push(Recommendation { + param: "net.ipv4.tcp_adv_win_scale".to_string(), + current_value: current.to_string(), + recommended_value: "2".to_string(), + reason: "TCP 接收缓冲区开销因子偏低,增大可让更多缓冲区用于应用数据提升吞吐" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_sched_tunable_scaling(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/sched_tunable_scaling"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.cpu_cores <= 16 { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "kernel.sched_tunable_scaling".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: format!( + "{}核 CPU 禁用了调度器自动缩放,内核无法根据 CPU 数量调整调度参数", + info.cpu_cores + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_panic_on_oops(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/panic_on_oops"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "kernel.panic_on_oops".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "内核 oops 后继续运行可能导致数据损坏或安全漏洞,建议 panic 后重启(代价:oops 时会重启)".to_string(), + confidence: Confidence::Medium, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_oom_dump_tasks(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/oom_dump_tasks"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "vm.oom_dump_tasks".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "OOM 时不输出进程列表,无法排查内存泄漏根因,建议启用".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_moderate_rcvbuf(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_moderate_rcvbuf"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_moderate_rcvbuf".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "TCP 接收缓冲区自动调整被禁用,可能导致内存浪费或吞吐受限".to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_flow_limit_table_len(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/core/flow_limit_table_len"; + if !std::path::Path::new(path).exists() { + return 0; + } + let max_speed = info.network.iter().map(|n| n.speed_mbps).max().unwrap_or(0); + if max_speed < 10000 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 8192 { + recs.push(Recommendation { + param: "net.core.flow_limit_table_len".to_string(), + current_value: current.to_string(), + recommended_value: "8192".to_string(), + reason: format!( + "{}Gbps 网络下流控表过小({}),增大可改善高流量场景的公平性", + max_speed / 1000, + current + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_l3mdev_accept(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_l3mdev_accept"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current != 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_l3mdev_accept".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: "启用 L3 master device 接受可能绕过 VRF 隔离,非 VRF 环境应禁用".to_string(), + confidence: Confidence::Medium, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_panic_on_warn(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/panic_on_warn"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current != 0 { + recs.push(Recommendation { + param: "kernel.panic_on_warn".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: "内核 WARN 即 panic 过于激进,正常运行中的 WARN 不应导致系统重启".to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_dirty_background_bytes(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/dirty_background_bytes"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.memory_total_gb < 64 { + return 1; + } + let bytes = read_sysctl_u64(path); + let ratio_path = "/proc/sys/vm/dirty_background_ratio"; + let ratio = if std::path::Path::new(ratio_path).exists() { + read_sysctl_u64(ratio_path) + } else { + 0 + }; + if bytes == 0 && ratio > 5 { + let recommended_mb = 256; + recs.push(Recommendation { + param: "vm.dirty_background_bytes".to_string(), + current_value: format!("0 (ratio={ratio}%)"), + recommended_value: format!("{}", recommended_mb * 1024 * 1024), + reason: format!("{}GB 内存 dirty_background_ratio {}% = {}GB 脏页才开始后台刷盘,用 bytes 可精确控制", + info.memory_total_gb, ratio, info.memory_total_gb * ratio / 100), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_hardlockup_panic(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/hardlockup_panic"; + if !std::path::Path::new(path).exists() { + return 0; + } + // Hard-lockup detection relies on the NMI watchdog. ktuner separately + // recommends disabling nmi_watchdog for perf — if it is already off, this + // panic setting can never fire, so don't recommend a no-op. + let nmi = "/proc/sys/kernel/nmi_watchdog"; + if std::path::Path::new(nmi).exists() && read_sysctl_u64(nmi) == 0 { + return 1; + } + let _ = info; + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "kernel.hardlockup_panic".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "CPU 硬锁死后不 panic 会导致系统假死无法自动恢复,应启用以触发自动重启" + .to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +// NOTE: kernel.softlockup_panic rule removed. A soft lockup is frequently a +// transient false positive (heavy load, hypervisor steal, long-but-legitimate +// work); rebooting the whole server on one is too aggressive a default for the +// beginners ktuner targets. + +fn eval_sched_rt_runtime(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/sched_rt_runtime_us"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = std::fs::read_to_string(path) + .unwrap_or_default() + .trim() + .parse::() + .unwrap_or(950000); + if current == -1 { + recs.push(Recommendation { + param: "kernel.sched_rt_runtime_us".to_string(), + current_value: "-1".to_string(), + recommended_value: "950000".to_string(), + reason: "实时任务可无限占用 CPU(无上限),可能导致普通进程饿死系统无响应".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_tcp_thin_linear_timeouts(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_thin_linear_timeouts"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current != 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_thin_linear_timeouts".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: "稀疏流线性超时模式已启用,可能导致连接在弱网环境下过快断开".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_arp_notify(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/all/arp_notify"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.network.len() < 2 || has_bond() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.conf.all.arp_notify".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "多网卡环境未启用 ARP 通知,IP 变更或故障切换时对端 ARP 缓存可能不更新" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_default_arp_announce(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/default/arp_announce"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.network.len() < 2 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 2 { + recs.push(Recommendation { + param: "net.ipv4.conf.default.arp_announce".to_string(), + current_value: current.to_string(), + recommended_value: "2".to_string(), + reason: "新建网络接口的 ARP 通告策略不佳,可能导致 ARP 回复从错误的源地址发出" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_default_arp_ignore(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/default/arp_ignore"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.network.len() < 2 { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.conf.default.arp_ignore".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "新建网络接口对所有 ARP 请求都回复,多网卡时可能导致 ARP 冲突".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_default_send_redirects(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/default/send_redirects"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 1 { + recs.push(Recommendation { + param: "net.ipv4.conf.default.send_redirects".to_string(), + current_value: "1".to_string(), + recommended_value: "0".to_string(), + reason: "新建网络接口默认发送 ICMP 重定向,非路由器应禁用以防网络拓扑探测".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_suid_dumpable(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/fs/suid_dumpable"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current != 0 { + recs.push(Recommendation { + param: "fs.suid_dumpable".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: "SUID 程序的 core dump 可能泄露敏感信息(如密码哈希),应禁用".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_icmp_ignore_bogus(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/icmp_ignore_bogus_error_responses"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.icmp_ignore_bogus_error_responses".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "未忽略虚假 ICMP 错误响应,可能被利用来做网络探测或拒绝服务".to_string(), + confidence: Confidence::Medium, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_arp_filter(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/all/arp_filter"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.network.len() <= 1 || has_bond() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.conf.all.arp_filter".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: format!( + "多网卡({}个)环境下未启用 ARP 过滤,可能导致 ARP 响应从错误的接口发出", + info.network.len() + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_sched_cfs_bandwidth_slice(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/sched_cfs_bandwidth_slice_us"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.cpu_cores <= 16 { + return 1; + } + let current = read_sysctl_u64(path); + if current > 4000 { + recs.push(Recommendation { + param: "kernel.sched_cfs_bandwidth_slice_us".to_string(), + current_value: current.to_string(), + recommended_value: "3000".to_string(), + reason: format!( + "{}核 CPU CFS 带宽分片 {}μs 偏大,减小可改善 cgroup 带宽限制的响应精度", + info.cpu_cores, current + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_tw_recycle(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_tw_recycle"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current != 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_tw_recycle".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: + "tcp_tw_recycle 在 NAT 环境下会导致大量连接失败(已在 Linux 4.12 中移除),必须关闭" + .to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_orphan_retries(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_orphan_retries"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 || current > 3 { + recs.push(Recommendation { + param: "net.ipv4.tcp_orphan_retries".to_string(), + current_value: if current == 0 { + "0 (默认8)".to_string() + } else { + current.to_string() + }, + recommended_value: "2".to_string(), + reason: "孤儿连接(对端无响应)重试次数过多,占用资源时间过长,减少可加速资源回收" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_early_retrans(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_early_retrans"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_early_retrans".to_string(), + current_value: "0".to_string(), + recommended_value: "3".to_string(), + reason: "TCP 早期重传被禁用,启用可减少丢包后的恢复延迟(ER + TLP)".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_ip_no_pmtu_disc(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/ip_no_pmtu_disc"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current != 0 { + recs.push(Recommendation { + param: "net.ipv4.ip_no_pmtu_disc".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: "PMTU 发现被禁用,可能导致大包被静默丢弃造成连接卡死(黑洞路由)".to_string(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_sched_wakeup_granularity(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/sched_wakeup_granularity_ns"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.cpu_cores <= 16 { + return 1; + } + let current = read_sysctl_u64(path); + if current > 3000000 { + recs.push(Recommendation { + param: "kernel.sched_wakeup_granularity_ns".to_string(), + current_value: current.to_string(), + recommended_value: "3000000".to_string(), + reason: format!( + "{}核 CPU 唤醒粒度 {}ms 过大,降低可减少调度延迟提升响应速度", + info.cpu_cores, + current / 1000000 + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_extfrag_threshold(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/extfrag_threshold"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.memory_total_gb < 32 { + return 1; + } + let current = read_sysctl_u64(path); + if current > 300 { + recs.push(Recommendation { + param: "vm.extfrag_threshold".to_string(), + current_value: current.to_string(), + recommended_value: "100".to_string(), + reason: format!( + "大内存({}GB)机器外部碎片化阈值过高,降低可更积极地进行内存整理避免大页分配失败", + info.memory_total_gb + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_msgmax(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/msgmax"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 65536 { + recs.push(Recommendation { + param: "kernel.msgmax".to_string(), + current_value: current.to_string(), + recommended_value: "65536".to_string(), + reason: "IPC 消息最大字节数过低,数据库和中间件进程间通信可能受限".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_msgmnb(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/msgmnb"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 65536 { + recs.push(Recommendation { + param: "kernel.msgmnb".to_string(), + current_value: current.to_string(), + recommended_value: "65536".to_string(), + reason: "IPC 消息队列最大字节数过低,高吞吐场景下进程间通信可能阻塞".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +// NOTE: kernel.modules_disabled and kernel.kexec_load_disabled rules were +// removed deliberately. Both are one-way runtime latches: once set to 1 the +// kernel refuses to set them back to 0 until reboot, so `ktuner rollback` +// cannot undo them — violating ktuner's core safe/reversible promise — and +// they break on-demand module loading / kdump. Admins who truly want this +// hardening set it themselves; an auto-tuner aimed at beginners must not. + +fn eval_user_reserve_kbytes(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/user_reserve_kbytes"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.memory_total_gb < 64 { + return 1; + } + let current = read_sysctl_u64(path); + let recommended: u64 = 262144; + if current > recommended { + return 1; + } + if current < 65536 { + recs.push(Recommendation { + param: "vm.user_reserve_kbytes".to_string(), + current_value: current.to_string(), + recommended_value: recommended.to_string(), + reason: format!( + "大内存({}GB)机器用户空间预留内存过低,OOM 时可能导致无法登录系统恢复", + info.memory_total_gb + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_shm_rmid_forced(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/shm_rmid_forced"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "kernel.shm_rmid_forced".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "进程退出后孤儿共享内存段不会自动回收,长期运行可能导致共享内存泄漏" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_sem(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/sem"; + if !std::path::Path::new(path).exists() { + return 0; + } + let has_db = info + .processes + .iter() + .any(|p| p.name == "postgres" || p.name == "mysqld" || p.name == "oracle"); + if !has_db { + return 1; + } + let content = std::fs::read_to_string(path).unwrap_or_default(); + let vals: Vec = content + .split_whitespace() + .filter_map(|s| s.parse().ok()) + .collect(); + if vals.len() >= 4 && (vals[0] < 1024 || vals[1] < 65536 || vals[3] < 4096) { + let current_str = vals + .iter() + .map(|v| v.to_string()) + .collect::>() + .join(" "); + recs.push(Recommendation { + param: "kernel.sem".to_string(), + current_value: current_str, + recommended_value: "1024 65536 256 4096".to_string(), + reason: "数据库场景下信号量参数过低,可能导致连接数受限或 semget() 失败".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_gc_stale_time(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/neigh/default/gc_stale_time"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current > 120 { + recs.push(Recommendation { + param: "net.ipv4.neigh.default.gc_stale_time".to_string(), + current_value: current.to_string(), + recommended_value: "120".to_string(), + reason: "ARP 缓存过期时间过长,网络拓扑变化后可能长时间使用过期的 MAC 地址".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_shmmni(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/shmmni"; + if !std::path::Path::new(path).exists() { + return 0; + } + let has_db = info + .processes + .iter() + .any(|p| p.name == "postgres" || p.name == "mysqld" || p.name == "oracle"); + if !has_db && info.memory_total_gb < 128 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 8192 { + recs.push(Recommendation { + param: "kernel.shmmni".to_string(), + current_value: current.to_string(), + recommended_value: "8192".to_string(), + reason: "共享内存段数上限过低,数据库和大内存应用创建共享内存段时可能受限".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_protected_fifos(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/fs/protected_fifos"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "fs.protected_fifos".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "未启用 FIFO 文件保护,/tmp 等全局可写目录下存在 FIFO 劫持攻击风险".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_tcp_fack(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_fack"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_fack".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "TCP Forward Acknowledgement 可改善丢包恢复效率,减少不必要的重传".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_reordering(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_reordering"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current < 3 { + recs.push(Recommendation { + param: "net.ipv4.tcp_reordering".to_string(), + current_value: current.to_string(), + recommended_value: "3".to_string(), + reason: "TCP 乱序容忍度过低,容易误判丢包触发不必要的快速重传".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_sched_energy_aware(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/sched_energy_aware"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 1 { + recs.push(Recommendation { + param: "kernel.sched_energy_aware".to_string(), + current_value: "1".to_string(), + recommended_value: "0".to_string(), + reason: "服务器场景不需要节能调度,关闭可避免性能损失".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_percpu_pagelist_high_fraction(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/percpu_pagelist_high_fraction"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.memory_total_gb < 64 { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "vm.percpu_pagelist_high_fraction".to_string(), + current_value: "0".to_string(), + recommended_value: "8".to_string(), + reason: format!( + "大内存服务器({}GB)设置 per-CPU 页面列表比例可减少跨 NUMA zone lock 竞争", + info.memory_total_gb + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_accept_ra(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv6/conf/default/accept_ra"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current > 0 { + recs.push(Recommendation { + param: "net.ipv6.conf.default.accept_ra".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: "服务器不应接受 IPv6 路由通告,防止路由被外部覆盖导致网络异常".to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_tcp_recovery(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_recovery"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_recovery".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "未启用 RACK 丢包检测,RACK 比传统 dupthresh 更准确地检测丢包和乱序" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_comp_sack_delay(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_comp_sack_delay_ns"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current > 1000000 { + recs.push(Recommendation { + param: "net.ipv4.tcp_comp_sack_delay_ns".to_string(), + current_value: current.to_string(), + recommended_value: "1000000".to_string(), + reason: format!("TCP 压缩 SACK 延迟 {current}ns 过大,减小可加速丢包恢复"), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_skb_frag_coalesce(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/core/skb_defer_max"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 64 { + recs.push(Recommendation { + param: "net.core.skb_defer_max".to_string(), + current_value: current.to_string(), + recommended_value: "64".to_string(), + reason: format!("SKB 延迟释放上限 {current} 偏低,增大可减少跨 CPU 的内存释放开销"), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_neigh_proxy_delay(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/neigh/default/proxy_delay"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current > 80 { + recs.push(Recommendation { + param: "net.ipv4.neigh.default.proxy_delay".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: format!("ARP 代理延迟 {current}*10ms 过大,服务器通常不需要代理 ARP"), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_pacing_ca_ratio(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_pacing_ca_ratio"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current < 120 { + recs.push(Recommendation { + param: "net.ipv4.tcp_pacing_ca_ratio".to_string(), + current_value: current.to_string(), + recommended_value: "120".to_string(), + reason: format!("TCP pacing 拥塞避免阶段速率比 {current}% 偏低,增大可提升发送速率"), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_pacing_ss_ratio(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_pacing_ss_ratio"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current < 200 { + recs.push(Recommendation { + param: "net.ipv4.tcp_pacing_ss_ratio".to_string(), + current_value: current.to_string(), + recommended_value: "200".to_string(), + reason: format!( + "TCP pacing 慢启动速率比 {current}% 偏低,增大可加速慢启动阶段的带宽探测" + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_comp_sack_nr(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_comp_sack_nr"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current > 44 { + recs.push(Recommendation { + param: "net.ipv4.tcp_comp_sack_nr".to_string(), + current_value: current.to_string(), + recommended_value: "44".to_string(), + reason: format!( + "TCP 压缩 SACK 最大数量 {current} 过大,减小可让 SACK 更及时发送加速恢复" + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_thin_dupack(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_thin_dupack"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_thin_dupack".to_string(), + current_value: current.to_string(), + recommended_value: "1".to_string(), + reason: "启用 thin stream 快速重传优化,对低并发长连接场景减少重传等待时间".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_invalid_ratelimit(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_invalid_ratelimit"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 500 { + recs.push(Recommendation { + param: "net.ipv4.tcp_invalid_ratelimit".to_string(), + current_value: current.to_string(), + recommended_value: "500".to_string(), + reason: format!( + "TCP 无效段响应速率限制 {current} ms 过低,增大可防止攻击者利用无效报文探测" + ), + confidence: Confidence::Medium, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_tcp_init_cwnd(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_init_cwnd"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current < 10 { + recs.push(Recommendation { + param: "net.ipv4.tcp_init_cwnd".to_string(), + current_value: current.to_string(), + recommended_value: "10".to_string(), + reason: format!( + "TCP 初始拥塞窗口 {current} 偏小,RFC 6928 推荐 10 以加速新连接首屏加载" + ), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_tso_win_divisor(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_tso_win_divisor"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current > 8 { + recs.push(Recommendation { + param: "net.ipv4.tcp_tso_win_divisor".to_string(), + current_value: current.to_string(), + recommended_value: "3".to_string(), + reason: format!("TSO 窗口分割因子 {current} 过大,会导致 TSO 段过小降低吞吐量"), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_sched_schedstats(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/sched_schedstats"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.cpu_cores < 4 { + return 1; + } + let current = read_sysctl_u64(path); + if current == 1 { + recs.push(Recommendation { + param: "kernel.sched_schedstats".to_string(), + current_value: "1".to_string(), + recommended_value: "0".to_string(), + reason: "调度器统计信息收集已开启,每次上下文切换都有额外开销,生产环境建议关闭" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_inotify_max_queued_events(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/fs/inotify/max_queued_events"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 65536 { + recs.push(Recommendation { + param: "fs.inotify.max_queued_events".to_string(), + current_value: current.to_string(), + recommended_value: "65536".to_string(), + reason: format!( + "inotify 事件队列上限 {current} 偏低,文件变更密集时可能丢失事件导致应用异常" + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_max_reordering(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_max_reordering"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current < 300 { + recs.push(Recommendation { + param: "net.ipv4.tcp_max_reordering".to_string(), + current_value: current.to_string(), + recommended_value: "300".to_string(), + reason: format!("TCP 最大重排序容忍度 {current} 偏低,高延迟网络中可能误判乱序为丢包触发不必要的重传"), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_retrans_collapse(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_retrans_collapse"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 1 { + recs.push(Recommendation { + param: "net.ipv4.tcp_retrans_collapse".to_string(), + current_value: "1".to_string(), + recommended_value: "0".to_string(), + reason: "TCP 重传合并已启用,可能将多个小段合并为一个大段导致接收端解析异常,建议关闭" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_app_win(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_app_win"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current > 31 { + recs.push(Recommendation { + param: "net.ipv4.tcp_app_win".to_string(), + current_value: current.to_string(), + recommended_value: "31".to_string(), + reason: format!( + "TCP 应用窗口保留比例 1/{current} 过高,减小可让更多缓冲区用于实际传输" + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_ip_default_ttl(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/ip_default_ttl"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 64 { + recs.push(Recommendation { + param: "net.ipv4.ip_default_ttl".to_string(), + current_value: current.to_string(), + recommended_value: "64".to_string(), + reason: format!("IP 默认 TTL {current} 低于标准值 64,可能导致远端网络不可达"), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_frto(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_frto"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_frto".to_string(), + current_value: "0".to_string(), + recommended_value: "2".to_string(), + reason: "未启用 F-RTO(Forward RTO-Recovery),无法区分真正的丢包和虚假超时重传" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_icmp_ratelimit(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/icmp_ratelimit"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.icmp_ratelimit".to_string(), + current_value: "0".to_string(), + recommended_value: "1000".to_string(), + reason: "ICMP 响应无速率限制,可能被利用进行反射放大攻击或信息探测".to_string(), + confidence: Confidence::Medium, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_igmp_max_memberships(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/igmp_max_memberships"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 256 { + recs.push(Recommendation { + param: "net.ipv4.igmp_max_memberships".to_string(), + current_value: current.to_string(), + recommended_value: "256".to_string(), + reason: format!("IGMP 组播成员上限 {current} 偏低,大量容器或微服务可能耗尽组播配额"), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_randomize_va_space_full(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/randomize_va_space"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 1 { + recs.push(Recommendation { + param: "kernel.randomize_va_space".to_string(), + current_value: "1".to_string(), + recommended_value: "2".to_string(), + reason: "ASLR 仅部分启用(栈+库),建议设为 2 同时随机化堆地址,提供完整保护" + .to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 0 +} + +fn eval_max_user_instances(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/fs/inotify/max_user_instances"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 512 { + recs.push(Recommendation { + param: "fs.inotify.max_user_instances".to_string(), + current_value: current.to_string(), + recommended_value: "1024".to_string(), + reason: format!( + "inotify 实例上限 {current} 偏低,容器或大量服务场景可能耗尽导致 watch 失败" + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_keys_maxkeys(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/keys/maxkeys"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 2000 { + recs.push(Recommendation { + param: "kernel.keys.maxkeys".to_string(), + current_value: current.to_string(), + recommended_value: "2000".to_string(), + reason: format!("内核密钥环上限 {current} 偏低,大量容器或服务可能耗尽密钥配额"), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +#[allow(clippy::ptr_arg)] +fn eval_numa_stat(info: &SystemInfo, _recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/numa_stat"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.numa_nodes <= 1 { + return 1; + } + 1 +} + +// NOTE: eval_sched_cfs_bw removed — it recommended raising +// sched_cfs_bandwidth_slice_us to 5000 (the kernel default) while +// eval_sched_cfs_bandwidth_slice recommends lowering it to 3000 for throttling +// precision. Two rules pulling the same knob in opposite directions is +// incoherent; keep only the precision-oriented one. + +fn eval_tcp_base_mss(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_base_mss"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current < 1024 { + recs.push(Recommendation { + param: "net.ipv4.tcp_base_mss".to_string(), + current_value: current.to_string(), + recommended_value: "1024".to_string(), + reason: format!("TCP 基础 MSS {current} 过小,PMTU 探测起点过低会降低初始传输效率"), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_min_tso_segs(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_min_tso_segs"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current < 2 { + recs.push(Recommendation { + param: "net.ipv4.tcp_min_tso_segs".to_string(), + current_value: current.to_string(), + recommended_value: "2".to_string(), + reason: "TCP TSO 最小段数过低,增大可提高大包合并效率减少中断次数".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_neigh_default_gc_interval(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/neigh/default/gc_interval"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 30 { + recs.push(Recommendation { + param: "net.ipv4.neigh.default.gc_interval".to_string(), + current_value: current.to_string(), + recommended_value: "30".to_string(), + reason: format!("ARP 垃圾回收间隔 {current}s 过短,频繁 GC 增加 CPU 开销"), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_neigh_default_gc_stale_time(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/neigh/default/gc_stale_time"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 120 { + recs.push(Recommendation { + param: "net.ipv4.neigh.default.gc_stale_time".to_string(), + current_value: current.to_string(), + recommended_value: "120".to_string(), + reason: format!("ARP 缓存过期时间 {current}s 偏短,增大可减少 ARP 请求频率"), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_fastopen_blackhole_timeout(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_fastopen_blackhole_timeout_sec"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current > 3600 { + recs.push(Recommendation { + param: "net.ipv4.tcp_fastopen_blackhole_timeout_sec".to_string(), + current_value: current.to_string(), + recommended_value: "0".to_string(), + reason: "TFO 黑洞超时过长,禁用超时可让每次连接都尝试 TFO 以获得最佳延迟".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_max_queued_signals(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/rtsig-max"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 4096 { + recs.push(Recommendation { + param: "kernel.rtsig-max".to_string(), + current_value: current.to_string(), + recommended_value: "4096".to_string(), + reason: format!("实时信号队列上限 {current} 偏低,高并发 IO 场景可能溢出"), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +#[allow(clippy::ptr_arg)] +fn eval_tcp_available_ulp(info: &SystemInfo, _recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_available_ulp"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + if let Ok(content) = std::fs::read_to_string(path) { + let ulps = content.trim(); + if ulps.contains("tls") { + return 1; + } + } + 0 +} + +fn eval_keys_maxbytes(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/keys/maxbytes"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 25000 { + recs.push(Recommendation { + param: "kernel.keys.maxbytes".to_string(), + current_value: current.to_string(), + recommended_value: "25000".to_string(), + reason: format!("内核密钥环容量 {current} 字节偏低,大量加密操作可能耗尽配额"), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_pipe_max_size(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/fs/pipe-max-size"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 1048576 { + recs.push(Recommendation { + param: "fs.pipe-max-size".to_string(), + current_value: current.to_string(), + recommended_value: "1048576".to_string(), + reason: format!( + "管道最大容量 {}KB 偏小,大数据管道传输可能阻塞", + current / 1024 + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_shmall(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/shmall"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + let target_pages = (info.memory_total_gb * 1024 * 1024 * 1024 / 4096) / 2; + if current < target_pages && target_pages > 0 { + recs.push(Recommendation { + param: "kernel.shmall".to_string(), + current_value: current.to_string(), + recommended_value: target_pages.to_string(), + reason: format!( + "共享内存总页数上限偏低(当前 {} 页),{}GB 内存建议至少 {} 页(内存的一半)", + current, info.memory_total_gb, target_pages + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_compact_memory(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/compact_memory"; + if !std::path::Path::new(path).exists() { + return 0; + } + let proactive_path = "/proc/sys/vm/compaction_proactiveness"; + if std::path::Path::new(proactive_path).exists() { + let current = read_sysctl_u64(proactive_path); + if current == 0 { + recs.push(Recommendation { + param: "vm.compaction_proactiveness".to_string(), + current_value: "0".to_string(), + recommended_value: "20".to_string(), + reason: "未启用主动内存压缩,长时间运行后内存碎片化可能导致高阶分配失败" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + return 1; + } + 0 +} + +fn eval_min_slab_ratio(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/min_slab_ratio"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.memory_total_gb < 64 { + return 1; + } + let current = read_sysctl_u64(path); + if current < 5 { + recs.push(Recommendation { + param: "vm.min_slab_ratio".to_string(), + current_value: current.to_string(), + recommended_value: "5".to_string(), + reason: format!( + "大内存服务器({}GB)应确保最低 slab 回收比例,防止 dentry/inode 缓存膨胀", + info.memory_total_gb + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_autocorking(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_autocorking"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.tcp_autocorking".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "TCP 自动合包可减少小包数量提高网络效率,建议保持开启".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_tcp_workaround_signed_windows(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_workaround_signed_windows"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 1 { + recs.push(Recommendation { + param: "net.ipv4.tcp_workaround_signed_windows".to_string(), + current_value: "1".to_string(), + recommended_value: "0".to_string(), + reason: "此兼容选项限制 TCP 窗口大小,现代系统不需要,关闭可恢复大窗口传输".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_protected_regular(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/fs/protected_regular"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "fs.protected_regular".to_string(), + current_value: "0".to_string(), + recommended_value: "2".to_string(), + reason: + "未启用 regular 文件保护,攻击者可在 sticky 目录中利用符号链接创建文件进行权限提升" + .to_string(), + confidence: Confidence::High, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_bpf_jit_enable(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/core/bpf_jit_enable"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.core.bpf_jit_enable".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "BPF JIT 编译器未启用,启用后 eBPF 程序和包过滤性能大幅提升(10-50 倍)" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_bpf_jit_harden(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/core/bpf_jit_harden"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.core.bpf_jit_harden".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "BPF JIT 编译器加固未启用,启用后可防止利用即时编译的代码注入攻击".to_string(), + confidence: Confidence::Medium, + category: Category::Security, + writable: true, + }); + } + 1 +} + +#[allow(clippy::ptr_arg)] +fn eval_tcp_available_congestion(_info: &SystemInfo, _recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/tcp_available_congestion_control"; + if !std::path::Path::new(path).exists() { + return 0; + } + let content = read_sysctl_string(path); + if !content.contains("bbr") { + return 1; + } + let current_algo = read_sysctl_string("/proc/sys/net/ipv4/tcp_congestion_control"); + if current_algo.trim() != "bbr" { + return 1; + } + 1 +} + +fn eval_somaxconn_large(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/core/somaxconn"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if (4096..65535).contains(¤t) && info.max_net_speed() >= 10000 { + recs.push(Recommendation { + param: "net.core.somaxconn".to_string(), + current_value: current.to_string(), + recommended_value: "65535".to_string(), + reason: format!( + "万兆网络下 somaxconn={current} 可能不够,建议增大到 65535 以应对突发连接" + ), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 0 +} + +fn eval_promote_secondaries(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/conf/default/promote_secondaries"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "net.ipv4.conf.default.promote_secondaries".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "未启用辅助地址自动提升,删除主 IP 地址时同网段的辅助地址也会被删除,可能导致网络中断".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_unres_qlen_bytes(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/neigh/default/unres_qlen_bytes"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current < 131072 && info.max_net_speed() >= 10000 { + recs.push(Recommendation { + param: "net.ipv4.neigh.default.unres_qlen_bytes".to_string(), + current_value: current.to_string(), + recommended_value: "262144".to_string(), + reason: "万兆网络中 ARP 解析未完成时排队缓冲区偏小,突发新目标连接可能导致丢包" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_ip_nonlocal_bind(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/net/ipv4/ip_nonlocal_bind"; + if !std::path::Path::new(path).exists() { + return 0; + } + // HA / VIP setups (keepalived VRRP, HAProxy binding to floating IPs) require + // ip_nonlocal_bind=1 on purpose — don't recommend disabling it there. + if info.has_process("keepalived") || info.has_process("haproxy") { + return 1; + } + let current = read_sysctl_u64(path); + if current == 1 { + recs.push(Recommendation { + param: "net.ipv4.ip_nonlocal_bind".to_string(), + current_value: "1".to_string(), + recommended_value: "0".to_string(), + reason: "允许绑定非本地 IP 地址可能导致安全风险,除非使用高可用(VRRP/keepalived)否则应禁用".to_string(), + confidence: Confidence::Medium, + category: Category::Security, + writable: true, + }); + } + 1 +} + +fn eval_conntrack_tcp_timeout_established( + info: &SystemInfo, + recs: &mut Vec, +) -> usize { + let path = "/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established"; + if !std::path::Path::new(path).exists() { + return 0; + } + if !info.has_listen_sockets() { + return 1; + } + let current = read_sysctl_u64(path); + if current > 86400 { + recs.push(Recommendation { + param: "net.netfilter.nf_conntrack_tcp_timeout_established".to_string(), + current_value: format!("{} ({}天)", current, current / 86400), + recommended_value: "86400".to_string(), + reason: "conntrack 已建立连接的超时默认 5 天太长,高并发下大量条目占满表导致丢包,缩短到 1 天".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_softlockup_all_cpu_backtrace(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/softlockup_all_cpu_backtrace"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.cpu_cores <= 32 { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "kernel.softlockup_all_cpu_backtrace".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "大核数机器出现 softlockup 时只打印触发 CPU 的栈,启用全 CPU backtrace 有助于定位问题".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_compact_unevictable(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/vm/compact_unevictable_allowed"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.memory_total_gb < 64 { + return 1; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "vm.compact_unevictable_allowed".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "允许内存整理不可驱逐页面,减少大内存机器的碎片化问题".to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_perf_cpu_time_max_percent(info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/perf_cpu_time_max_percent"; + if !std::path::Path::new(path).exists() { + return 0; + } + if info.cpu_cores <= 16 { + return 1; + } + let current = read_sysctl_u64(path); + if current > 5 { + recs.push(Recommendation { + param: "kernel.perf_cpu_time_max_percent".to_string(), + current_value: current.to_string(), + recommended_value: "5".to_string(), + reason: "大核数机器限制 perf 采样最大 CPU 占比,避免性能分析工具本身成为瓶颈" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_hung_task_warnings(_info: &SystemInfo, recs: &mut Vec) -> usize { + let path = "/proc/sys/kernel/hung_task_warnings"; + if !std::path::Path::new(path).exists() { + return 0; + } + let current = read_sysctl_u64(path); + if current == 0 { + recs.push(Recommendation { + param: "kernel.hung_task_warnings".to_string(), + current_value: "0".to_string(), + recommended_value: "10".to_string(), + reason: "hung task 警告被禁用,无法发现进程卡死问题,建议至少保留一定数量的告警" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +fn eval_overcommit_ratio(info: &SystemInfo, recs: &mut Vec) -> usize { + let oc_path = "/proc/sys/vm/overcommit_memory"; + let ratio_path = "/proc/sys/vm/overcommit_ratio"; + if !std::path::Path::new(ratio_path).exists() { + return 0; + } + let oc_mode = read_sysctl_u64(oc_path); + if oc_mode != 2 { + return 1; + } + let ratio = read_sysctl_u64(ratio_path); + let db_present = info.processes.iter().any(|p| { + p.name.contains("postgres") || p.name.contains("mysql") || p.name.contains("oracle") + }); + if db_present && ratio < 80 { + recs.push(Recommendation { + param: "vm.overcommit_ratio".to_string(), + current_value: ratio.to_string(), + recommended_value: "80".to_string(), + reason: "overcommit_memory=2 模式下 ratio 过低会限制可用内存,数据库建议设为 80-90" + .to_string(), + confidence: Confidence::Medium, + category: Category::Performance, + writable: true, + }); + } + 1 +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +fn read_sysctl_string(path: &str) -> String { + std::fs::read_to_string(path) + .map(|s| s.split_whitespace().collect::>().join(" ")) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::detect::*; + + fn rec(param: &str, conf: Confidence) -> Recommendation { + Recommendation { + param: param.to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: String::new(), + confidence: conf, + category: Category::Performance, + writable: false, + } + } + + #[test] + fn test_dedupe_keeps_one_per_param() { + let input = vec![ + rec("net.core.somaxconn", Confidence::Medium), + rec("vm.swappiness", Confidence::Medium), + rec("net.core.somaxconn", Confidence::Medium), + ]; + let out = dedupe_recommendations(input); + assert_eq!(out.len(), 2); + assert_eq!( + out.iter() + .filter(|r| r.param == "net.core.somaxconn") + .count(), + 1 + ); + } + + #[test] + fn test_dedupe_prefers_high_confidence() { + let input = vec![ + rec("kernel.randomize_va_space", Confidence::Medium), + rec("kernel.randomize_va_space", Confidence::High), + ]; + let out = dedupe_recommendations(input); + assert_eq!(out.len(), 1); + assert_eq!(out[0].confidence, Confidence::High); + } + + #[test] + fn test_dedupe_preserves_order() { + let input = vec![ + rec("a.b", Confidence::Medium), + rec("c.d", Confidence::Medium), + rec("a.b", Confidence::Medium), + rec("e.f", Confidence::Medium), + ]; + let out = dedupe_recommendations(input); + let names: Vec<&str> = out.iter().map(|r| r.param.as_str()).collect(); + assert_eq!(names, vec!["a.b", "c.d", "e.f"]); + } + + fn make_test_info() -> SystemInfo { + SystemInfo { + kernel_version: "5.4.0".to_string(), + os_distro: "Test Linux".to_string(), + cpu_model: "Test CPU".to_string(), + cpu_cores: 8, + numa_nodes: 1, + memory_total_gb: 64, + disks: vec![DiskInfo { + name: "nvme0n1".to_string(), + disk_type: DiskType::NVMe, + scheduler: "mq-deadline".to_string(), + available_schedulers: vec!["none".to_string(), "mq-deadline".to_string()], + nr_requests: 256, + read_ahead_kb: 128, + rq_affinity: 1, + }], + network: vec![], + sysctl: SysctlValues { + swappiness: 60, + dirty_ratio: 20, + dirty_background_ratio: 10, + somaxconn: 128, + tcp_fastopen: 1, + thp_enabled: "always".to_string(), + }, + processes: vec![ProcessInfo { + name: "postgres".to_string(), + }], + } + } + + #[test] + fn test_nvme_scheduler_recommendation() { + let info = make_test_info(); + let recs = evaluate(&info).unwrap().recommendations; + let sched_rec = recs.iter().find(|r| r.param.contains("scheduler")); + assert!(sched_rec.is_some()); + assert_eq!(sched_rec.unwrap().recommended_value, "none"); + assert_eq!(sched_rec.unwrap().confidence, Confidence::High); + assert_eq!(sched_rec.unwrap().category, Category::Performance); + } + + #[test] + fn test_swappiness_with_database() { + let info = make_test_info(); + let recs = evaluate(&info).unwrap().recommendations; + let swap_rec = recs.iter().find(|r| r.param == "vm.swappiness"); + assert!(swap_rec.is_some()); + assert_eq!(swap_rec.unwrap().recommended_value, "1"); + } + + #[test] + fn test_thp_with_postgres() { + let info = make_test_info(); + let recs = evaluate(&info).unwrap().recommendations; + let thp_rec = recs.iter().find(|r| r.param.contains("hugepage")); + assert!(thp_rec.is_some()); + assert_eq!(thp_rec.unwrap().recommended_value, "madvise"); + } + + #[test] + fn test_no_false_positive_when_optimal() { + let mut info = make_test_info(); + info.disks[0].scheduler = "none".to_string(); + info.disks[0].nr_requests = 1024; + info.sysctl.swappiness = 10; + info.sysctl.thp_enabled = "madvise".to_string(); + info.sysctl.dirty_ratio = 10; + info.sysctl.dirty_background_ratio = 5; + info.sysctl.somaxconn = 65535; + info.sysctl.tcp_fastopen = 3; + info.processes = vec![]; + info.network = vec![]; + + let recs = evaluate(&info).unwrap().recommendations; + // Filter to rules fully controlled by SystemInfo (no live /proc reads) + let live_params = [ + "tcp_slow_start", + "default_qdisc", + "tcp_max_syn_backlog", + "ip_local_port_range", + "watermark_scale_factor", + "max_map_count", + "sched_autogroup", + "netdev_max_backlog", + "tcp_rmem", + "tcp_wmem", + "rmem_max", + "wmem_max", + "tcp_tw_reuse", + "tcp_fin_timeout", + "tcp_keepalive_time", + "file-max", + "pid_max", + "tcp_max_tw_buckets", + "tcp_mtu_probing", + "sched_migration_cost", + "tcp_no_metrics_save", + "overcommit_memory", + "tcp_keepalive_intvl", + "tcp_keepalive_probes", + "panic", + "panic_on_oom", + "tcp_congestion_control", + "nf_conntrack", + "dirty_expire_centisecs", + "dirty_writeback_centisecs", + "tcp_timestamps", + "tcp_window_scaling", + "tcp_ecn", + "ip_forward", + "tcp_retries2", + "tcp_abort_on_overflow", + "log_martians", + "shmmax", + "tcp_max_orphans", + "threads-max", + "nr_hugepages", + "tcp_syn_retries", + "tcp_synack_retries", + "optmem_max", + "oom_kill_allocating_task", + "inotify", + "aio-max-nr", + "dirty_background_ratio", + "sched_min_granularity", + "icmp_echo_ignore_broadcasts", + "accept_source_route", + "busy_read", + "gc_thresh3", + "nr_open", + "arp_announce", + "arp_ignore", + "nmi_watchdog", + "stat_interval", + "hung_task_timeout", + "tcp_rfc1337", + "secure_redirects", + "mmap_min_addr", + "netdev_budget_usecs", + "dirty_bytes", + "sched_child_runs_first", + "default.accept_redirects", + "default.accept_source_route", + "sched_latency_ns", + "challenge_ack_limit", + "conf.all.rp_filter", + "page-cluster", + "rmem_default", + "wmem_default", + "sched_nr_migrate", + "tcp_notsent_lowat", + "max_dgram_qlen", + "rps_sock_flow_entries", + "tcp_dsack", + "kexec_load_disabled", + "ip_no_pmtu_disc", + "sched_wakeup_granularity", + "extfrag_threshold", + "tcp_tw_recycle", + "tcp_orphan_retries", + "tcp_early_retrans", + "arp_filter", + "cfs_bandwidth_slice", + "suid_dumpable", + "icmp_ignore_bogus", + "default.log_martians", + "laptop_mode", + "tcp_adv_win_scale", + "sched_tunable_scaling", + "panic_on_oops", + "oom_dump_tasks", + "tcp_moderate_rcvbuf", + "flow_limit_table_len", + "tcp_l3mdev_accept", + "panic_on_warn", + "dirty_background_bytes", + "hardlockup_panic", + "softlockup_panic", + "sched_rt_runtime", + "tcp_thin_linear", + "arp_notify", + "default.arp_announce", + "default.arp_ignore", + "default.send_redirects", + "gc_thresh1", + "gc_thresh2", + "tcp_retries1", + "tcp_limit_output_bytes", + "dev_weight", + "printk", + "watchdog_thresh", + "admin_reserve_kbytes", + "msgmax", + "msgmnb", + "protected_fifos", + "modules_disabled", + "user_reserve_kbytes", + "shmmni", + "kernel.sem", + "gc_stale_time", + "shm_rmid_forced", + "tcp_fack", + "tcp_reordering", + "sched_energy_aware", + "percpu_pagelist_high_fraction", + "accept_ra", + "compaction_proactiveness", + "min_slab_ratio", + "tcp_autocorking", + "tcp_workaround_signed", + "max_user_instances", + "keys.maxkeys", + "tcp_available_ulp", + "numa_stat", + "sched_cfs_bandwidth_slice_us", + "tcp_base_mss", + "tcp_min_tso_segs", + "neigh.default.gc_interval", + "neigh.default.gc_stale_time", + "tcp_fastopen_blackhole", + "rtsig-max", + "keys.maxbytes", + "pipe-max-size", + "shmall", + "tcp_app_win", + "ip_default_ttl", + "tcp_frto", + "icmp_ratelimit", + "igmp_max_memberships", + "tcp_recovery", + "tcp_comp_sack_delay", + "skb_defer_max", + "proxy_delay", + "tcp_pacing_ca_ratio", + "tcp_pacing_ss_ratio", + "tcp_comp_sack_nr", + "tcp_thin_dupack", + "tcp_invalid_ratelimit", + "tcp_init_cwnd", + "tcp_tso_win_divisor", + "sched_schedstats", + "max_queued_events", + "tcp_max_reordering", + "tcp_retrans_collapse", + "protected_regular", + "bpf_jit_enable", + "bpf_jit_harden", + "promote_secondaries", + "unres_qlen_bytes", + "ip_nonlocal_bind", + "conntrack_tcp_timeout_established", + "softlockup_all_cpu_backtrace", + "compact_unevictable", + "perf_cpu_time_max_percent", + "hung_task_warnings", + "overcommit_ratio", + ]; + let controllable_perf_recs: Vec<_> = recs + .iter() + .filter(|r| r.category == Category::Performance) + .filter(|r| !live_params.iter().any(|p| r.param.contains(p))) + .collect(); + assert!( + controllable_perf_recs.is_empty(), + "Should not recommend perf changes when optimal, got: {controllable_perf_recs:?}" + ); + } + + #[test] + fn test_nvme_nr_requests() { + let mut info = make_test_info(); + info.disks[0].nr_requests = 128; + let recs = evaluate(&info).unwrap().recommendations; + let rec = recs.iter().find(|r| r.param.contains("nr_requests")); + assert!( + rec.is_some(), + "Should recommend increasing nr_requests for NVMe" + ); + assert_eq!(rec.unwrap().recommended_value, "1024"); + assert_eq!(rec.unwrap().confidence, Confidence::High); + } + + #[test] + fn test_nvme_nr_requests_ok_when_high() { + let mut info = make_test_info(); + info.disks[0].nr_requests = 1024; + let recs = evaluate(&info).unwrap().recommendations; + let rec = recs.iter().find(|r| r.param.contains("nr_requests")); + assert!( + rec.is_none(), + "Should not recommend nr_requests when already high" + ); + } + + #[test] + fn test_rq_affinity_only_on_numa() { + let mut info = make_test_info(); + info.numa_nodes = 1; + info.disks[0].rq_affinity = 1; + let recs = evaluate(&info).unwrap().recommendations; + let rec = recs.iter().find(|r| r.param.contains("rq_affinity")); + assert!( + rec.is_none(), + "Should not recommend rq_affinity on single NUMA" + ); + + info.numa_nodes = 2; + let recs = evaluate(&info).unwrap().recommendations; + let rec = recs.iter().find(|r| r.param.contains("rq_affinity")); + assert!( + rec.is_some(), + "Should recommend rq_affinity=2 on multi-NUMA NVMe" + ); + assert_eq!(rec.unwrap().recommended_value, "2"); + } + + #[test] + fn test_dirty_ratio_with_database() { + let mut info = make_test_info(); + info.memory_total_gb = 32; // <64GB uses the ratio form (>=64GB uses bytes) + let recs = evaluate(&info).unwrap().recommendations; + let rec = recs.iter().find(|r| r.param == "vm.dirty_ratio"); + assert!( + rec.is_some(), + "Should recommend lower dirty_ratio with postgres" + ); + assert_eq!(rec.unwrap().recommended_value, "5"); + + let bg_rec = recs.iter().find(|r| r.param == "vm.dirty_background_ratio"); + assert!(bg_rec.is_some()); + assert_eq!(bg_rec.unwrap().recommended_value, "3"); + } + + #[test] + fn test_ssd_scheduler() { + let mut info = make_test_info(); + info.disks = vec![DiskInfo { + name: "sda".to_string(), + disk_type: DiskType::SSD, + scheduler: "cfq".to_string(), + available_schedulers: vec!["noop".to_string(), "cfq".to_string()], + nr_requests: 256, + read_ahead_kb: 128, + rq_affinity: 1, + }]; + let recs = evaluate(&info).unwrap().recommendations; + let rec = recs.iter().find(|r| r.param.contains("scheduler")); + assert!(rec.is_some(), "Should recommend changing cfq on SSD"); + assert_eq!(rec.unwrap().recommended_value, "noop"); + } + + #[test] + fn test_hdd_read_ahead_with_streaming() { + let mut info = make_test_info(); + info.disks = vec![DiskInfo { + name: "sdb".to_string(), + disk_type: DiskType::HDD, + scheduler: "cfq".to_string(), + available_schedulers: vec!["cfq".to_string()], + nr_requests: 128, + read_ahead_kb: 128, + rq_affinity: 1, + }]; + info.processes = vec![ProcessInfo { + name: "kafka".to_string(), + }]; + let recs = evaluate(&info).unwrap().recommendations; + let rec = recs.iter().find(|r| r.param.contains("read_ahead_kb")); + assert!( + rec.is_some(), + "Should recommend read_ahead_kb for HDD with kafka" + ); + assert_eq!(rec.unwrap().recommended_value, "2048"); + } + + #[test] + fn test_hdd_no_read_ahead_without_streaming() { + let mut info = make_test_info(); + info.disks = vec![DiskInfo { + name: "sdb".to_string(), + disk_type: DiskType::HDD, + scheduler: "cfq".to_string(), + available_schedulers: vec!["cfq".to_string()], + nr_requests: 128, + read_ahead_kb: 128, + rq_affinity: 1, + }]; + info.processes = vec![]; + let recs = evaluate(&info).unwrap().recommendations; + let rec = recs.iter().find(|r| r.param.contains("read_ahead_kb")); + assert!( + rec.is_none(), + "Should not recommend read_ahead_kb without streaming process" + ); + } + + #[test] + fn test_somaxconn_not_triggered_without_listen() { + let mut info = make_test_info(); + info.sysctl.somaxconn = 128; + info.processes = vec![]; + // has_listen_sockets() reads live /proc, so on this machine it may or may not trigger + // We just verify the rule exists and has the right recommended value when triggered + let recs = evaluate(&info).unwrap().recommendations; + if let Some(rec) = recs.iter().find(|r| r.param == "net.core.somaxconn") { + assert_eq!(rec.recommended_value, "65535"); + } + } + + #[test] + fn test_pid_max_large_cpu() { + let mut info = make_test_info(); + info.cpu_cores = 96; + let recs = evaluate(&info).unwrap().recommendations; + // pid_max reads /proc/sys/kernel/pid_max live, verify if triggered + if let Some(rec) = recs.iter().find(|r| r.param == "kernel.pid_max") { + assert_eq!(rec.recommended_value, "4194304"); + assert_eq!(rec.confidence, Confidence::Medium); + } + } + + #[test] + fn test_pid_max_not_triggered_small_cpu() { + let mut info = make_test_info(); + info.cpu_cores = 8; + let recs = evaluate(&info).unwrap().recommendations; + let rec = recs.iter().find(|r| r.param == "kernel.pid_max"); + assert!( + rec.is_none(), + "Should not recommend pid_max for small machines" + ); + } + + #[test] + fn test_tcp_keepalive_values() { + // These rules read live /proc state, just verify recommended values if triggered + let info = make_test_info(); + let recs = evaluate(&info).unwrap().recommendations; + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.tcp_keepalive_time") + { + assert_eq!(rec.recommended_value, "600"); + } + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_fin_timeout") { + assert_eq!(rec.recommended_value, "15"); + } + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_tw_reuse") { + assert_eq!(rec.recommended_value, "1"); + } + } + + #[test] + fn test_total_rules_count_dynamic() { + let info = make_test_info(); + let result = evaluate(&info).unwrap(); + assert!( + result.total_checked >= 30, + "Should check at least 30 rules, got {}", + result.total_checked + ); + } + + #[test] + fn test_workload_io_latency_swappiness() { + let mut info = make_test_info(); + info.sysctl.swappiness = 10; + info.processes = vec![ProcessInfo { + name: "postgres".to_string(), + }]; + let recs = evaluate_with_workload(&info, &WorkloadType::IoLatency) + .unwrap() + .recommendations; + let rec = recs.iter().find(|r| r.param == "vm.swappiness"); + assert!( + rec.is_some(), + "IoLatency workload should recommend swappiness=1 even when current is 10" + ); + assert_eq!(rec.unwrap().recommended_value, "1"); + } + + #[test] + fn test_dirty_ratio_bytes_mutually_exclusive_large_ram() { + // dirty_ratio ⊥ dirty_bytes in the kernel. A >=64GB host must never be + // told to set both for the same dimension, and must use the bytes form. + let mut info = make_test_info(); + info.memory_total_gb = 128; + info.sysctl.dirty_ratio = 20; + info.sysctl.dirty_background_ratio = 10; + info.processes = vec![ProcessInfo { + name: "postgres".to_string(), + }]; + let recs = evaluate_with_workload(&info, &WorkloadType::IoLatency) + .unwrap() + .recommendations; + let has_ratio = recs.iter().any(|r| r.param == "vm.dirty_ratio"); + let has_bytes = recs.iter().any(|r| r.param == "vm.dirty_bytes"); + assert!( + !(has_ratio && has_bytes), + "must not recommend both dirty_ratio and dirty_bytes" + ); + let has_bg_ratio = recs.iter().any(|r| r.param == "vm.dirty_background_ratio"); + let has_bg_bytes = recs.iter().any(|r| r.param == "vm.dirty_background_bytes"); + assert!( + !(has_bg_ratio && has_bg_bytes), + "must not recommend both bg ratio and bg bytes" + ); + assert!( + !has_ratio, + "large-RAM host should use dirty_bytes, not dirty_ratio" + ); + } + + #[test] + fn test_workload_io_latency_dirty_ratio() { + let mut info = make_test_info(); + info.memory_total_gb = 32; // <64GB uses the ratio form + info.sysctl.dirty_ratio = 10; + info.sysctl.dirty_background_ratio = 5; + info.processes = vec![ProcessInfo { + name: "postgres".to_string(), + }]; + let recs = evaluate_with_workload(&info, &WorkloadType::IoLatency) + .unwrap() + .recommendations; + let rec = recs.iter().find(|r| r.param == "vm.dirty_ratio"); + assert!(rec.is_some(), "IoLatency should recommend dirty_ratio=5"); + assert_eq!(rec.unwrap().recommended_value, "5"); + let bg = recs.iter().find(|r| r.param == "vm.dirty_background_ratio"); + assert!(bg.is_some()); + assert_eq!(bg.unwrap().recommended_value, "3"); + } + + #[test] + fn test_score_empty() { + let result = EvalResult { + recommendations: vec![], + total_checked: 40, + }; + assert_eq!(result.score(), 100); + } + + #[test] + fn test_score_weighted() { + let high_rec = Recommendation { + param: "test".to_string(), + current_value: "0".to_string(), + recommended_value: "1".to_string(), + reason: "test".to_string(), + confidence: Confidence::High, + ..Default::default() + }; + let medium_rec = Recommendation { + confidence: Confidence::Medium, + ..high_rec.clone() + }; + + let result = EvalResult { + recommendations: vec![high_rec.clone(), medium_rec.clone()], + total_checked: 40, + }; + assert_eq!(result.score(), 95); // 100 - 3 - 2 = 95 + + let result = EvalResult { + recommendations: vec![high_rec; 20], + total_checked: 40, + }; + assert_eq!(result.score(), 40); // 100 - 60 = 40 + + let result = EvalResult { + recommendations: vec![medium_rec; 40], + total_checked: 40, + }; + assert_eq!(result.score(), 30); // 100 - 80 capped at 30 + } + + #[test] + fn test_workload_mixed_no_aggressive_swappiness() { + let mut info = make_test_info(); + info.sysctl.swappiness = 10; + info.processes = vec![]; + let recs = evaluate_with_workload(&info, &WorkloadType::Mixed) + .unwrap() + .recommendations; + let rec = recs.iter().find(|r| r.param == "vm.swappiness"); + assert!( + rec.is_none(), + "Mixed workload with 64GB RAM and swappiness=10 should not trigger" + ); + } + + #[test] + fn test_sched_migration_cost_large_cpu() { + let mut info = make_test_info(); + info.cpu_cores = 64; + let recs = evaluate(&info).unwrap().recommendations; + if let Some(rec) = recs + .iter() + .find(|r| r.param == "kernel.sched_migration_cost_ns") + { + assert_eq!(rec.recommended_value, "5000000"); + } + } + + #[test] + fn test_sched_migration_cost_not_triggered_small_cpu() { + let mut info = make_test_info(); + info.cpu_cores = 8; + let recs = evaluate(&info).unwrap().recommendations; + let rec = recs + .iter() + .find(|r| r.param == "kernel.sched_migration_cost_ns"); + assert!( + rec.is_none(), + "Should not recommend sched_migration_cost for small machines" + ); + } + + #[test] + fn test_tcp_mtu_probing() { + let info = make_test_info(); + let recs = evaluate(&info).unwrap().recommendations; + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_mtu_probing") { + assert_eq!(rec.recommended_value, "1"); + assert_eq!(rec.confidence, Confidence::Medium); + } + } + + #[test] + fn test_tcp_retries2() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_retries2(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_retries2") { + assert_eq!(rec.recommended_value, "8"); + assert_eq!(rec.confidence, Confidence::Medium); + } + } + + #[test] + fn test_file_max() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_file_max(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "fs.file-max") { + assert_eq!(rec.recommended_value, "2000000"); + assert_eq!(rec.confidence, Confidence::High); + } + } + + #[test] + fn test_conntrack_max() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_nf_conntrack_max(&info, &mut recs); + // Just verify it doesn't panic, result depends on system state + let _ = recs; + } + + #[test] + fn test_log_martians() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_log_martians(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.conf.all.log_martians") + { + assert_eq!(rec.recommended_value, "1"); + assert_eq!(rec.category, Category::Security); + } + } + + #[test] + fn test_shmmax_only_with_database() { + let mut info = make_test_info(); + info.processes = vec![]; + let mut recs = Vec::new(); + eval_shmmax(&info, &mut recs); + assert!( + recs.is_empty(), + "Should not recommend shmmax without database process" + ); + + info.processes = vec![ProcessInfo { + name: "postgres".to_string(), + }]; + let mut recs = Vec::new(); + eval_shmmax(&info, &mut recs); + // Depends on current system shmmax value + let _ = recs; + } + + #[test] + fn test_tcp_timestamps_must_be_on() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_timestamps(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_timestamps") { + assert_eq!(rec.recommended_value, "1"); + assert_eq!(rec.confidence, Confidence::High); + } + } + + #[test] + fn test_tcp_window_scaling_must_be_on() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_window_scaling(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.tcp_window_scaling") + { + assert_eq!(rec.recommended_value, "1"); + assert_eq!(rec.confidence, Confidence::High); + } + } + + #[test] + fn test_tcp_sack_must_be_on() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_sack(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_sack") { + assert_eq!(rec.recommended_value, "1"); + assert_eq!(rec.confidence, Confidence::High); + } + } + + #[test] + fn test_overcommit_only_with_redis() { + let mut info = make_test_info(); + info.processes = vec![]; + let mut recs = Vec::new(); + eval_overcommit_memory(&info, &mut recs); + assert!( + recs.is_empty(), + "Should not recommend overcommit without redis" + ); + + info.processes = vec![ProcessInfo { + name: "redis-server".to_string(), + }]; + let mut recs = Vec::new(); + eval_overcommit_memory(&info, &mut recs); + if let Some(rec) = recs.first() { + assert_eq!(rec.recommended_value, "1"); + assert_eq!(rec.confidence, Confidence::High); + } + } + + #[test] + fn test_thp_not_triggered_without_latency_process() { + let mut info = make_test_info(); + info.processes = vec![]; + info.sysctl.thp_enabled = "always".to_string(); + let mut recs = Vec::new(); + eval_thp(&info, &mut recs); + assert!( + recs.is_empty(), + "THP should not trigger without latency-sensitive processes" + ); + } + + #[test] + fn test_nr_hugepages_only_with_db() { + let mut info = make_test_info(); + info.processes = vec![]; + info.memory_total_gb = 64; + let mut recs = Vec::new(); + eval_nr_hugepages(&info, &mut recs); + assert!( + recs.is_empty(), + "Should not recommend hugepages without db process" + ); + } + + #[test] + fn test_tcp_syn_retries() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_syn_retries(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_syn_retries") { + assert_eq!(rec.recommended_value, "3"); + assert_eq!(rec.confidence, Confidence::Medium); + } + } + + #[test] + fn test_tcp_synack_retries() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_synack_retries(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.tcp_synack_retries") + { + assert_eq!(rec.recommended_value, "3"); + } + } + + #[test] + fn test_inotify_watches() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_inotify_max_user_watches(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "fs.inotify.max_user_watches") + { + assert_eq!(rec.recommended_value, "524288"); + assert_eq!(rec.confidence, Confidence::High); + } + } + + #[test] + fn test_aio_max_nr() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_aio_max_nr(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "fs.aio-max-nr") { + assert_eq!(rec.recommended_value, "1048576"); + assert_eq!(rec.confidence, Confidence::Medium); + } + } + + #[test] + fn test_oom_kill_allocating_task() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_oom_kill_allocating_task(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "vm.oom_kill_allocating_task") + { + assert_eq!(rec.recommended_value, "1"); + } + } + + #[test] + fn test_optmem_max() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_optmem_max(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.core.optmem_max") { + assert_eq!(rec.recommended_value, "81920"); + } + } + + #[test] + fn test_score_bounds() { + let result = EvalResult { + recommendations: vec![ + Recommendation { + param: "a".into(), + current_value: "0".into(), + recommended_value: "1".into(), + reason: "test".into(), + confidence: Confidence::High, + category: Category::Performance, + writable: true, + }; + 50 + ], + total_checked: 60, + }; + let score = result.score(); + assert!(score >= 30, "Score should have floor of 30, got {score}"); + } + + #[test] + fn test_dirty_background_ratio_with_db() { + let mut info = make_test_info(); + info.memory_total_gb = 32; // <64GB uses the ratio form + info.sysctl.dirty_background_ratio = 10; + info.processes = vec![ProcessInfo { + name: "postgres".to_string(), + }]; + let mut recs = Vec::new(); + eval_dirty_background_ratio(&info, &WorkloadType::IoLatency, &mut recs); + let rec = recs.iter().find(|r| r.param == "vm.dirty_background_ratio"); + assert!( + rec.is_some(), + "Should recommend lower dirty_background_ratio for DB" + ); + assert_eq!(rec.unwrap().recommended_value, "3"); + } + + #[test] + fn test_icmp_echo_ignore_broadcasts() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_icmp_echo_ignore_broadcasts(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.icmp_echo_ignore_broadcasts") + { + assert_eq!(rec.recommended_value, "1"); + assert_eq!(rec.confidence, Confidence::High); + assert_eq!(rec.category, Category::Security); + } + } + + #[test] + fn test_accept_source_route() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_accept_source_route(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.conf.all.accept_source_route") + { + assert_eq!(rec.recommended_value, "0"); + assert_eq!(rec.confidence, Confidence::High); + assert_eq!(rec.category, Category::Security); + } + } + + #[test] + fn test_sched_min_granularity_large_cpu() { + let mut info = make_test_info(); + info.cpu_cores = 96; + let mut recs = Vec::new(); + eval_sched_min_granularity(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "kernel.sched_min_granularity_ns") + { + assert_eq!(rec.recommended_value, "10000000"); + } + } + + #[test] + fn test_sched_min_granularity_small_cpu() { + let mut info = make_test_info(); + info.cpu_cores = 8; + let mut recs = Vec::new(); + eval_sched_min_granularity(&info, &mut recs); + let rec = recs + .iter() + .find(|r| r.param == "kernel.sched_min_granularity_ns"); + assert!( + rec.is_none(), + "Should not recommend sched_min_granularity for small CPU count" + ); + } + + #[test] + fn test_neigh_gc_thresh3() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_neigh_gc_thresh3(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.neigh.default.gc_thresh3") + { + assert_eq!(rec.recommended_value, "8192"); + assert_eq!(rec.confidence, Confidence::Medium); + } + } + + #[test] + fn test_nr_open() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_nr_open(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "fs.nr_open") { + assert_eq!(rec.recommended_value, "1048576"); + assert_eq!(rec.confidence, Confidence::High); + } + } + + #[test] + fn test_arp_announce_multi_nic() { + let mut info = make_test_info(); + info.network = vec![ + NetInfo { + name: "eth0".to_string(), + speed_mbps: 10000, + }, + NetInfo { + name: "eth1".to_string(), + speed_mbps: 10000, + }, + ]; + let mut recs = Vec::new(); + eval_arp_announce(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.conf.all.arp_announce") + { + assert_eq!(rec.recommended_value, "2"); + } + } + + #[test] + fn test_arp_announce_single_nic() { + let mut info = make_test_info(); + info.network = vec![NetInfo { + name: "eth0".to_string(), + speed_mbps: 10000, + }]; + let mut recs = Vec::new(); + eval_arp_announce(&info, &mut recs); + let rec = recs + .iter() + .find(|r| r.param == "net.ipv4.conf.all.arp_announce"); + assert!( + rec.is_none(), + "Should not recommend arp_announce for single NIC" + ); + } + + #[test] + fn test_nmi_watchdog() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_nmi_watchdog(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "kernel.nmi_watchdog") { + assert_eq!(rec.recommended_value, "0"); + assert_eq!(rec.confidence, Confidence::Medium); + } + } + + #[test] + fn test_stat_interval_large_cpu() { + let mut info = make_test_info(); + info.cpu_cores = 96; + let mut recs = Vec::new(); + eval_stat_interval(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "vm.stat_interval") { + assert_eq!(rec.recommended_value, "5"); + } + } + + #[test] + fn test_stat_interval_small_cpu() { + let mut info = make_test_info(); + info.cpu_cores = 8; + let mut recs = Vec::new(); + eval_stat_interval(&info, &mut recs); + let rec = recs.iter().find(|r| r.param == "vm.stat_interval"); + assert!( + rec.is_none(), + "Should not recommend stat_interval for small CPU count" + ); + } + + #[test] + fn test_hung_task_timeout() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_hung_task_timeout(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "kernel.hung_task_timeout_secs") + { + assert_eq!(rec.recommended_value, "120"); + } + } + + #[test] + fn test_arp_ignore_multi_nic() { + let mut info = make_test_info(); + info.network = vec![ + NetInfo { + name: "eth0".to_string(), + speed_mbps: 10000, + }, + NetInfo { + name: "eth1".to_string(), + speed_mbps: 10000, + }, + ]; + let mut recs = Vec::new(); + eval_arp_ignore(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.conf.all.arp_ignore") + { + assert_eq!(rec.recommended_value, "1"); + } + } + + #[test] + fn test_tcp_rfc1337() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_rfc1337(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_rfc1337") { + assert_eq!(rec.recommended_value, "1"); + assert_eq!(rec.confidence, Confidence::High); + assert_eq!(rec.category, Category::Security); + } + } + + #[test] + fn test_secure_redirects() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_secure_redirects(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.conf.all.secure_redirects") + { + assert_eq!(rec.recommended_value, "0"); + assert_eq!(rec.category, Category::Security); + } + } + + #[test] + fn test_mmap_min_addr() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_mmap_min_addr(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "vm.mmap_min_addr") { + assert_eq!(rec.recommended_value, "65536"); + assert_eq!(rec.category, Category::Security); + } + } + + #[test] + fn test_netdev_budget_usecs_10g() { + let mut info = make_test_info(); + info.network = vec![NetInfo { + name: "eth0".to_string(), + speed_mbps: 10000, + }]; + let mut recs = Vec::new(); + eval_netdev_budget_usecs(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.core.netdev_budget_usecs") + { + assert_eq!(rec.recommended_value, "8000"); + } + } + + #[test] + fn test_netdev_budget_usecs_1g_skip() { + let mut info = make_test_info(); + info.network = vec![NetInfo { + name: "eth0".to_string(), + speed_mbps: 1000, + }]; + let mut recs = Vec::new(); + eval_netdev_budget_usecs(&info, &mut recs); + let rec = recs + .iter() + .find(|r| r.param == "net.core.netdev_budget_usecs"); + assert!( + rec.is_none(), + "Should not recommend netdev_budget_usecs for 1G network" + ); + } + + #[test] + fn test_dirty_bytes_large_ram() { + let mut info = make_test_info(); + info.memory_total_gb = 256; + let mut recs = Vec::new(); + eval_dirty_bytes(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "vm.dirty_bytes") { + assert_eq!(rec.recommended_value, "268435456"); + } + } + + #[test] + fn test_dirty_bytes_small_ram_skip() { + let mut info = make_test_info(); + info.memory_total_gb = 32; + let mut recs = Vec::new(); + eval_dirty_bytes(&info, &mut recs); + let rec = recs.iter().find(|r| r.param == "vm.dirty_bytes"); + assert!( + rec.is_none(), + "Should not recommend dirty_bytes for small RAM" + ); + } + + #[test] + fn test_sched_child_runs_first() { + let mut info = make_test_info(); + info.processes = vec![ProcessInfo { + name: "nginx".to_string(), + }]; + let mut recs = Vec::new(); + eval_sched_child_runs_first(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "kernel.sched_child_runs_first") + { + assert_eq!(rec.recommended_value, "0"); + } + } + + #[test] + fn test_default_accept_redirects() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_default_accept_redirects(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.conf.default.accept_redirects") + { + assert_eq!(rec.recommended_value, "0"); + assert_eq!(rec.category, Category::Security); + } + } + + #[test] + fn test_default_accept_source_route() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_default_accept_source_route(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.conf.default.accept_source_route") + { + assert_eq!(rec.recommended_value, "0"); + assert_eq!(rec.category, Category::Security); + } + } + + #[test] + fn test_sched_latency_ns_large_cpu() { + let mut info = make_test_info(); + info.cpu_cores = 96; + let mut recs = Vec::new(); + eval_sched_latency_ns(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "kernel.sched_latency_ns") { + assert_eq!(rec.recommended_value, "24000000"); + } + } + + #[test] + fn test_sched_latency_ns_small_cpu_skip() { + let mut info = make_test_info(); + info.cpu_cores = 8; + let mut recs = Vec::new(); + eval_sched_latency_ns(&info, &mut recs); + let rec = recs.iter().find(|r| r.param == "kernel.sched_latency_ns"); + assert!( + rec.is_none(), + "Should not recommend sched_latency_ns for small CPU count" + ); + } + + #[test] + fn test_tcp_challenge_ack_limit() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_challenge_ack_limit(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.tcp_challenge_ack_limit") + { + assert_eq!(rec.recommended_value, "999999999"); + assert_eq!(rec.confidence, Confidence::High); + assert_eq!(rec.category, Category::Security); + } + } + + #[test] + fn test_rp_filter_all() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_rp_filter_all(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.conf.all.rp_filter") + { + assert_eq!(rec.recommended_value, "1"); + assert_eq!(rec.category, Category::Security); + } + } + + #[test] + fn test_page_cluster_with_ssd() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_page_cluster(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "vm.page-cluster") { + assert_eq!(rec.recommended_value, "0"); + } + } + + #[test] + fn test_rmem_default_10g() { + let mut info = make_test_info(); + info.network = vec![NetInfo { + name: "eth0".to_string(), + speed_mbps: 10000, + }]; + let mut recs = Vec::new(); + eval_rmem_default(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.core.rmem_default") { + assert_eq!(rec.recommended_value, "262144"); + } + } + + #[test] + fn test_rmem_default_1g_skip() { + let mut info = make_test_info(); + info.network = vec![NetInfo { + name: "eth0".to_string(), + speed_mbps: 1000, + }]; + let mut recs = Vec::new(); + eval_rmem_default(&info, &mut recs); + let rec = recs.iter().find(|r| r.param == "net.core.rmem_default"); + assert!( + rec.is_none(), + "Should not recommend rmem_default for 1G network" + ); + } + + #[test] + fn test_wmem_default_10g() { + let mut info = make_test_info(); + info.network = vec![NetInfo { + name: "eth0".to_string(), + speed_mbps: 10000, + }]; + let mut recs = Vec::new(); + eval_wmem_default(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.core.wmem_default") { + assert_eq!(rec.recommended_value, "262144"); + } + } + + #[test] + fn test_sched_nr_migrate_large_cpu() { + let mut info = make_test_info(); + info.cpu_cores = 96; + let mut recs = Vec::new(); + eval_sched_nr_migrate(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "kernel.sched_nr_migrate") { + assert_eq!(rec.recommended_value, "128"); + } + } + + #[test] + fn test_sched_nr_migrate_small_cpu_skip() { + let mut info = make_test_info(); + info.cpu_cores = 8; + let mut recs = Vec::new(); + eval_sched_nr_migrate(&info, &mut recs); + let rec = recs.iter().find(|r| r.param == "kernel.sched_nr_migrate"); + assert!( + rec.is_none(), + "Should not recommend sched_nr_migrate for small CPU count" + ); + } + + #[test] + fn test_tcp_notsent_lowat() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_notsent_lowat(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.tcp_notsent_lowat") + { + assert_eq!(rec.recommended_value, "131072"); + } + } + + #[test] + fn test_unix_max_dgram_qlen() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_unix_max_dgram_qlen(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.unix.max_dgram_qlen") { + assert_eq!(rec.recommended_value, "1024"); + } + } + + #[test] + fn test_rps_sock_flow_entries_10g() { + let mut info = make_test_info(); + info.network = vec![NetInfo { + name: "eth0".to_string(), + speed_mbps: 10000, + }]; + let mut recs = Vec::new(); + eval_rps_sock_flow_entries(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.core.rps_sock_flow_entries") + { + assert_eq!(rec.recommended_value, "32768"); + } + } + + #[test] + fn test_rps_sock_flow_entries_1g_skip() { + let mut info = make_test_info(); + info.network = vec![NetInfo { + name: "eth0".to_string(), + speed_mbps: 1000, + }]; + let mut recs = Vec::new(); + eval_rps_sock_flow_entries(&info, &mut recs); + let rec = recs + .iter() + .find(|r| r.param == "net.core.rps_sock_flow_entries"); + assert!( + rec.is_none(), + "Should not recommend rps_sock_flow_entries for 1G network" + ); + } + + #[test] + fn test_tcp_dsack() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_dsack(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_dsack") { + assert_eq!(rec.recommended_value, "1"); + assert_eq!(rec.confidence, Confidence::High); + } + } + + #[test] + fn test_sched_wakeup_granularity_large_cpu() { + let mut info = make_test_info(); + info.cpu_cores = 96; + let mut recs = Vec::new(); + eval_sched_wakeup_granularity(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "kernel.sched_wakeup_granularity_ns") + { + assert_eq!(rec.recommended_value, "3000000"); + } + } + + #[test] + fn test_sched_wakeup_granularity_small_cpu_skip() { + let mut info = make_test_info(); + info.cpu_cores = 8; + let mut recs = Vec::new(); + eval_sched_wakeup_granularity(&info, &mut recs); + let rec = recs + .iter() + .find(|r| r.param == "kernel.sched_wakeup_granularity_ns"); + assert!( + rec.is_none(), + "Should not recommend sched_wakeup_granularity for small CPU count" + ); + } + + #[test] + fn test_extfrag_threshold_large_mem() { + let mut info = make_test_info(); + info.memory_total_gb = 256; + let mut recs = Vec::new(); + eval_extfrag_threshold(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "vm.extfrag_threshold") { + assert_eq!(rec.recommended_value, "100"); + } + } + + #[test] + fn test_extfrag_threshold_small_mem_skip() { + let mut info = make_test_info(); + info.memory_total_gb = 16; + let mut recs = Vec::new(); + eval_extfrag_threshold(&info, &mut recs); + let rec = recs.iter().find(|r| r.param == "vm.extfrag_threshold"); + assert!( + rec.is_none(), + "Should not recommend extfrag_threshold for small memory" + ); + } + + #[test] + fn test_default_send_redirects() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_default_send_redirects(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.conf.default.send_redirects") + { + assert_eq!(rec.recommended_value, "0"); + assert_eq!(rec.category, Category::Security); + } + } + + #[test] + fn test_neigh_gc_thresh1() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_neigh_gc_thresh1(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.neigh.default.gc_thresh1") + { + assert_eq!(rec.recommended_value, "2048"); + } + } + + #[test] + fn test_neigh_gc_thresh2() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_neigh_gc_thresh2(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.neigh.default.gc_thresh2") + { + assert_eq!(rec.recommended_value, "4096"); + } + } + + #[test] + fn test_tcp_retries1() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_retries1(&info, &mut recs); + // retries1 default is 3, should not trigger + let rec = recs.iter().find(|r| r.param == "net.ipv4.tcp_retries1"); + assert!( + rec.is_none(), + "Should not recommend tcp_retries1 when default is 3" + ); + } + + #[test] + fn test_tcp_limit_output_bytes_10g() { + let mut info = make_test_info(); + info.network = vec![NetInfo { + name: "eth0".to_string(), + speed_mbps: 10000, + }]; + let mut recs = Vec::new(); + eval_tcp_limit_output_bytes(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.tcp_limit_output_bytes") + { + assert_eq!(rec.recommended_value, "1048576"); + } + } + + #[test] + fn test_tcp_limit_output_bytes_1g_skip() { + let mut info = make_test_info(); + info.network = vec![NetInfo { + name: "eth0".to_string(), + speed_mbps: 1000, + }]; + let mut recs = Vec::new(); + eval_tcp_limit_output_bytes(&info, &mut recs); + let rec = recs + .iter() + .find(|r| r.param == "net.ipv4.tcp_limit_output_bytes"); + assert!( + rec.is_none(), + "Should not recommend tcp_limit_output_bytes for 1G network" + ); + } + + #[test] + fn test_dev_weight_10g() { + let mut info = make_test_info(); + info.network = vec![NetInfo { + name: "eth0".to_string(), + speed_mbps: 10000, + }]; + let mut recs = Vec::new(); + eval_dev_weight(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.core.dev_weight") { + assert_eq!(rec.recommended_value, "128"); + } + } + + #[test] + fn test_dev_weight_1g_skip() { + let mut info = make_test_info(); + info.network = vec![NetInfo { + name: "eth0".to_string(), + speed_mbps: 1000, + }]; + let mut recs = Vec::new(); + eval_dev_weight(&info, &mut recs); + let rec = recs.iter().find(|r| r.param == "net.core.dev_weight"); + assert!( + rec.is_none(), + "Should not recommend dev_weight for 1G network" + ); + } + + #[test] + fn test_watchdog_thresh_large_cpu() { + let mut info = make_test_info(); + info.cpu_cores = 96; + let mut recs = Vec::new(); + eval_watchdog_thresh(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "kernel.watchdog_thresh") { + assert_eq!(rec.recommended_value, "30"); + } + } + + #[test] + fn test_watchdog_thresh_small_cpu_skip() { + let mut info = make_test_info(); + info.cpu_cores = 8; + let mut recs = Vec::new(); + eval_watchdog_thresh(&info, &mut recs); + let rec = recs.iter().find(|r| r.param == "kernel.watchdog_thresh"); + assert!( + rec.is_none(), + "Should not recommend watchdog_thresh for small CPU count" + ); + } + + #[test] + fn test_admin_reserve_large_mem() { + let mut info = make_test_info(); + info.memory_total_gb = 256; + let mut recs = Vec::new(); + eval_admin_reserve_kbytes(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "vm.admin_reserve_kbytes") { + assert_eq!(rec.recommended_value, "131072"); + } + } + + #[test] + fn test_admin_reserve_small_mem_skip() { + let mut info = make_test_info(); + info.memory_total_gb = 16; + let mut recs = Vec::new(); + eval_admin_reserve_kbytes(&info, &mut recs); + let rec = recs.iter().find(|r| r.param == "vm.admin_reserve_kbytes"); + assert!( + rec.is_none(), + "Should not recommend admin_reserve_kbytes for small memory" + ); + } + + #[test] + fn test_msgmax() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_msgmax(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "kernel.msgmax") { + assert_eq!(rec.recommended_value, "65536"); + assert_eq!(rec.category, Category::Performance); + } + } + + #[test] + fn test_msgmnb() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_msgmnb(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "kernel.msgmnb") { + assert_eq!(rec.recommended_value, "65536"); + assert_eq!(rec.category, Category::Performance); + } + } + + #[test] + fn test_protected_fifos() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_protected_fifos(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "fs.protected_fifos") { + assert_eq!(rec.recommended_value, "1"); + assert_eq!(rec.confidence, Confidence::High); + assert_eq!(rec.category, Category::Security); + } + } + + #[test] + fn test_user_reserve_kbytes_large_mem() { + let mut info = make_test_info(); + info.memory_total_gb = 256; + let mut recs = Vec::new(); + eval_user_reserve_kbytes(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "vm.user_reserve_kbytes") { + assert_eq!(rec.recommended_value, "262144"); + } + } + + #[test] + fn test_user_reserve_kbytes_small_mem_skip() { + let mut info = make_test_info(); + info.memory_total_gb = 16; + let mut recs = Vec::new(); + eval_user_reserve_kbytes(&info, &mut recs); + let rec = recs.iter().find(|r| r.param == "vm.user_reserve_kbytes"); + assert!( + rec.is_none(), + "Should not recommend user_reserve_kbytes for small memory" + ); + } + + #[test] + fn test_shmmni_with_database() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_shmmni(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "kernel.shmmni") { + assert_eq!(rec.recommended_value, "8192"); + assert_eq!(rec.category, Category::Performance); + } + } + + #[test] + fn test_shmmni_skip_without_db_small_mem() { + let mut info = make_test_info(); + info.processes = vec![]; + info.memory_total_gb = 32; + let mut recs = Vec::new(); + eval_shmmni(&info, &mut recs); + let rec = recs.iter().find(|r| r.param == "kernel.shmmni"); + assert!( + rec.is_none(), + "Should not recommend shmmni without database on small memory" + ); + } + + #[test] + fn test_sem_with_database() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_sem(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "kernel.sem") { + assert_eq!(rec.recommended_value, "1024 65536 256 4096"); + assert_eq!(rec.category, Category::Performance); + } + } + + #[test] + fn test_sem_skip_without_database() { + let mut info = make_test_info(); + info.processes = vec![]; + let mut recs = Vec::new(); + eval_sem(&info, &mut recs); + let rec = recs.iter().find(|r| r.param == "kernel.sem"); + assert!(rec.is_none(), "Should not recommend sem without database"); + } + + #[test] + fn test_gc_stale_time() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_gc_stale_time(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param.contains("gc_stale_time")) { + assert_eq!(rec.recommended_value, "120"); + } + } + + #[test] + fn test_shm_rmid_forced() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_shm_rmid_forced(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "kernel.shm_rmid_forced") { + assert_eq!(rec.recommended_value, "1"); + assert_eq!(rec.category, Category::Performance); + } + } + + #[test] + fn test_tcp_fack() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_fack(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_fack") { + assert_eq!(rec.recommended_value, "1"); + assert_eq!(rec.category, Category::Performance); + } + } + + #[test] + fn test_tcp_reordering() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_reordering(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_reordering") { + assert_eq!(rec.recommended_value, "3"); + } + } + + #[test] + fn test_sched_energy_aware() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_sched_energy_aware(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "kernel.sched_energy_aware") { + assert_eq!(rec.recommended_value, "0"); + assert_eq!(rec.category, Category::Performance); + } + } + + #[test] + fn test_percpu_pagelist_high_fraction_large_mem() { + let mut info = make_test_info(); + info.memory_total_gb = 256; + let mut recs = Vec::new(); + eval_percpu_pagelist_high_fraction(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "vm.percpu_pagelist_high_fraction") + { + assert_eq!(rec.recommended_value, "8"); + } + } + + #[test] + fn test_percpu_pagelist_skip_small_mem() { + let mut info = make_test_info(); + info.memory_total_gb = 16; + let mut recs = Vec::new(); + eval_percpu_pagelist_high_fraction(&info, &mut recs); + let rec = recs + .iter() + .find(|r| r.param == "vm.percpu_pagelist_high_fraction"); + assert!( + rec.is_none(), + "Should not recommend percpu_pagelist for small memory" + ); + } + + #[test] + fn test_accept_ra() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_accept_ra(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv6.conf.default.accept_ra") + { + assert_eq!(rec.recommended_value, "0"); + assert_eq!(rec.category, Category::Security); + assert_eq!(rec.confidence, Confidence::High); + } + } + + #[test] + fn test_tcp_autocorking() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_autocorking(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_autocorking") { + assert_eq!(rec.recommended_value, "1"); + assert_eq!(rec.category, Category::Performance); + } + } + + #[test] + fn test_tcp_workaround_signed_windows() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_workaround_signed_windows(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.tcp_workaround_signed_windows") + { + assert_eq!(rec.recommended_value, "0"); + } + } + + #[test] + fn test_compact_memory() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_compact_memory(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "vm.compaction_proactiveness") + { + assert_eq!(rec.recommended_value, "20"); + } + } + + #[test] + fn test_min_slab_ratio_large_mem() { + let mut info = make_test_info(); + info.memory_total_gb = 256; + let mut recs = Vec::new(); + eval_min_slab_ratio(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "vm.min_slab_ratio") { + assert_eq!(rec.recommended_value, "5"); + } + } + + #[test] + fn test_min_slab_ratio_skip_small_mem() { + let mut info = make_test_info(); + info.memory_total_gb = 16; + let mut recs = Vec::new(); + eval_min_slab_ratio(&info, &mut recs); + let rec = recs.iter().find(|r| r.param == "vm.min_slab_ratio"); + assert!( + rec.is_none(), + "Should not recommend min_slab_ratio for small memory" + ); + } + + #[test] + fn test_max_user_instances() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_max_user_instances(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "fs.inotify.max_user_instances") + { + assert_eq!(rec.recommended_value, "1024"); + assert_eq!(rec.category, Category::Performance); + } + } + + #[test] + fn test_keys_maxkeys() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_keys_maxkeys(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "kernel.keys.maxkeys") { + assert_eq!(rec.recommended_value, "2000"); + } + } + + #[test] + fn test_tcp_base_mss() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_base_mss(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_base_mss") { + assert_eq!(rec.recommended_value, "1024"); + } + } + + #[test] + fn test_neigh_gc_stale_time() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_neigh_default_gc_stale_time(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.neigh.default.gc_stale_time") + { + assert_eq!(rec.recommended_value, "120"); + } + } + + #[test] + fn test_keys_maxbytes() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_keys_maxbytes(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "kernel.keys.maxbytes") { + assert_eq!(rec.recommended_value, "25000"); + } + } + + #[test] + fn test_pipe_max_size() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_pipe_max_size(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "fs.pipe-max-size") { + assert_eq!(rec.recommended_value, "1048576"); + } + } + + #[test] + fn test_shmall() { + let mut info = make_test_info(); + info.memory_total_gb = 256; + let mut recs = Vec::new(); + eval_shmall(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "kernel.shmall") { + let target = (256u64 * 1024 * 1024 * 1024 / 4096) / 2; + assert_eq!(rec.recommended_value, target.to_string()); + } + } + + #[test] + fn test_tcp_frto() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_frto(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_frto") { + assert_eq!(rec.recommended_value, "2"); + } + } + + #[test] + fn test_icmp_ratelimit() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_icmp_ratelimit(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.icmp_ratelimit") { + assert_eq!(rec.recommended_value, "1000"); + assert_eq!(rec.category, Category::Security); + } + } + + #[test] + fn test_ip_default_ttl() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_ip_default_ttl(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.ip_default_ttl") { + assert_eq!(rec.recommended_value, "64"); + } + } + + #[test] + fn test_tcp_recovery() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_recovery(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_recovery") { + assert_eq!(rec.recommended_value, "1"); + } + } + + #[test] + fn test_tcp_pacing_ca_ratio() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_pacing_ca_ratio(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.tcp_pacing_ca_ratio") + { + assert_eq!(rec.recommended_value, "120"); + } + } + + #[test] + fn test_tcp_pacing_ss_ratio() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_pacing_ss_ratio(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.tcp_pacing_ss_ratio") + { + assert_eq!(rec.recommended_value, "200"); + } + } + + #[test] + fn test_tcp_comp_sack_nr() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_comp_sack_nr(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_comp_sack_nr") { + assert_eq!(rec.recommended_value, "44"); + } + } + + #[test] + fn test_tcp_thin_dupack() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_thin_dupack(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_thin_dupack") { + assert_eq!(rec.recommended_value, "1"); + } + } + + #[test] + fn test_tcp_invalid_ratelimit() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_invalid_ratelimit(&info, &mut recs); + assert!( + recs.iter() + .all(|r| r.param != "net.ipv4.tcp_invalid_ratelimit"), + "Should not trigger when ratelimit is already >= 500" + ); + } + + #[test] + fn test_tcp_init_cwnd() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_init_cwnd(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "net.ipv4.tcp_init_cwnd") { + assert_eq!(rec.recommended_value, "10"); + assert_eq!(rec.confidence, Confidence::High); + } + } + + #[test] + fn test_sched_schedstats() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_sched_schedstats(&info, &mut recs); + if let Some(rec) = recs.iter().find(|r| r.param == "kernel.sched_schedstats") { + assert_eq!(rec.recommended_value, "0"); + } + } + + #[test] + fn test_inotify_max_queued_events() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_inotify_max_queued_events(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "fs.inotify.max_queued_events") + { + assert_eq!(rec.recommended_value, "65536"); + } + } + + #[test] + fn test_tcp_retrans_collapse() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_retrans_collapse(&info, &mut recs); + if let Some(rec) = recs + .iter() + .find(|r| r.param == "net.ipv4.tcp_retrans_collapse") + { + assert_eq!(rec.recommended_value, "0"); + } + } + + #[test] + fn test_tcp_max_reordering() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_tcp_max_reordering(&info, &mut recs); + let triggered = recs + .iter() + .any(|r| r.param == "net.ipv4.tcp_max_reordering"); + if std::path::Path::new("/proc/sys/net/ipv4/tcp_max_reordering").exists() { + let val = read_sysctl_u64("/proc/sys/net/ipv4/tcp_max_reordering"); + if val >= 300 { + assert!( + !triggered, + "Should not trigger when tcp_max_reordering >= 300" + ); + } + } + } + + #[test] + fn test_protected_regular() { + let info = make_test_info(); + let mut recs = Vec::new(); + let checked = eval_protected_regular(&info, &mut recs); + if std::path::Path::new("/proc/sys/fs/protected_regular").exists() { + assert_eq!(checked, 1); + let val = read_sysctl_u64("/proc/sys/fs/protected_regular"); + let triggered = recs.iter().any(|r| r.param == "fs.protected_regular"); + if val == 0 { + assert!(triggered, "Should trigger when protected_regular=0"); + assert_eq!(recs.last().unwrap().category, Category::Security); + } else { + assert!(!triggered); + } + } + } + + #[test] + fn test_bpf_jit_enable() { + let info = make_test_info(); + let mut recs = Vec::new(); + let checked = eval_bpf_jit_enable(&info, &mut recs); + if std::path::Path::new("/proc/sys/net/core/bpf_jit_enable").exists() { + assert!(checked >= 1); + } + } + + #[test] + fn test_bpf_jit_harden() { + let info = make_test_info(); + let mut recs = Vec::new(); + let checked = eval_bpf_jit_harden(&info, &mut recs); + if std::path::Path::new("/proc/sys/net/core/bpf_jit_harden").exists() { + assert_eq!(checked, 1); + let val = read_sysctl_u64("/proc/sys/net/core/bpf_jit_harden"); + let triggered = recs.iter().any(|r| r.param == "net.core.bpf_jit_harden"); + if val == 0 { + assert!(triggered, "Should trigger when bpf_jit_harden=0"); + assert_eq!(recs.last().unwrap().category, Category::Security); + } + } + } + + #[test] + fn test_promote_secondaries() { + let info = make_test_info(); + let mut recs = Vec::new(); + let checked = eval_promote_secondaries(&info, &mut recs); + if std::path::Path::new("/proc/sys/net/ipv4/conf/default/promote_secondaries").exists() { + assert_eq!(checked, 1); + let val = read_sysctl_u64("/proc/sys/net/ipv4/conf/default/promote_secondaries"); + let triggered = recs.iter().any(|r| r.param.contains("promote_secondaries")); + if val == 0 { + assert!(triggered, "Should trigger when promote_secondaries=0"); + } + } + } + + #[test] + fn test_unres_qlen_bytes_10g() { + let mut info = make_test_info(); + info.network = vec![crate::detect::NetInfo { + name: "eth0".to_string(), + speed_mbps: 10000, + }]; + let mut recs = Vec::new(); + eval_unres_qlen_bytes(&info, &mut recs); + if std::path::Path::new("/proc/sys/net/ipv4/neigh/default/unres_qlen_bytes").exists() { + let val = read_sysctl_u64("/proc/sys/net/ipv4/neigh/default/unres_qlen_bytes"); + if val < 131072 { + assert!(recs.iter().any(|r| r.param.contains("unres_qlen_bytes"))); + } + } + } + + #[test] + fn test_ip_nonlocal_bind() { + let info = make_test_info(); + let mut recs = Vec::new(); + let checked = eval_ip_nonlocal_bind(&info, &mut recs); + if std::path::Path::new("/proc/sys/net/ipv4/ip_nonlocal_bind").exists() { + assert_eq!(checked, 1); + let val = read_sysctl_u64("/proc/sys/net/ipv4/ip_nonlocal_bind"); + if val == 1 { + assert!(recs.iter().any(|r| r.param == "net.ipv4.ip_nonlocal_bind")); + assert_eq!(recs.last().unwrap().category, Category::Security); + } + } + } + + #[test] + fn test_conntrack_tcp_timeout_established() { + let info = make_test_info(); + let mut recs = Vec::new(); + let checked = eval_conntrack_tcp_timeout_established(&info, &mut recs); + if std::path::Path::new("/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established") + .exists() + { + assert!(checked >= 1); + } + } + + #[test] + fn test_softlockup_all_cpu_backtrace_large_cpu() { + let mut info = make_test_info(); + info.cpu_cores = 96; + let mut recs = Vec::new(); + let checked = eval_softlockup_all_cpu_backtrace(&info, &mut recs); + if std::path::Path::new("/proc/sys/kernel/softlockup_all_cpu_backtrace").exists() { + assert_eq!(checked, 1); + let val = read_sysctl_u64("/proc/sys/kernel/softlockup_all_cpu_backtrace"); + if val == 0 { + assert!(recs + .iter() + .any(|r| r.param == "kernel.softlockup_all_cpu_backtrace")); + } + } + } + + #[test] + fn test_compact_unevictable_large_mem() { + let mut info = make_test_info(); + info.memory_total_gb = 128; + let mut recs = Vec::new(); + let checked = eval_compact_unevictable(&info, &mut recs); + if std::path::Path::new("/proc/sys/vm/compact_unevictable_allowed").exists() { + assert_eq!(checked, 1); + let val = read_sysctl_u64("/proc/sys/vm/compact_unevictable_allowed"); + if val == 0 { + assert!(recs + .iter() + .any(|r| r.param == "vm.compact_unevictable_allowed")); + } + } + } + + #[test] + fn test_perf_cpu_time_max_percent() { + let mut info = make_test_info(); + info.cpu_cores = 64; + let mut recs = Vec::new(); + let checked = eval_perf_cpu_time_max_percent(&info, &mut recs); + if std::path::Path::new("/proc/sys/kernel/perf_cpu_time_max_percent").exists() { + assert_eq!(checked, 1); + } + } + + #[test] + fn test_hung_task_warnings() { + let info = make_test_info(); + let mut recs = Vec::new(); + let checked = eval_hung_task_warnings(&info, &mut recs); + if std::path::Path::new("/proc/sys/kernel/hung_task_warnings").exists() { + assert_eq!(checked, 1); + let val = read_sysctl_u64("/proc/sys/kernel/hung_task_warnings"); + if val == 0 { + assert!(recs.iter().any(|r| r.param == "kernel.hung_task_warnings")); + } + } + } + + #[test] + fn test_overcommit_ratio_skip_without_mode2() { + let info = make_test_info(); + let mut recs = Vec::new(); + eval_overcommit_ratio(&info, &mut recs); + let oc = read_sysctl_u64("/proc/sys/vm/overcommit_memory"); + if oc != 2 { + assert!(recs.is_empty()); + } + } +} diff --git a/src/ktuner/src/services.rs b/src/ktuner/src/services.rs new file mode 100644 index 000000000..17caa3cbc --- /dev/null +++ b/src/ktuner/src/services.rs @@ -0,0 +1,92 @@ +use crate::detect::SystemInfo; + +const KNOWN_SERVICES: &[(&str, &str)] = &[ + ("postgres", "PostgreSQL"), + ("mysqld", "MySQL"), + ("mongod", "MongoDB"), + ("clickhouse", "ClickHouse"), + ("redis-server", "Redis"), + ("memcached", "Memcached"), + ("elasticsearch", "Elasticsearch"), + ("opensearch", "OpenSearch"), + ("nginx", "Nginx"), + ("httpd", "Apache"), + ("envoy", "Envoy"), + ("haproxy", "HAProxy"), + ("caddy", "Caddy"), + ("kafka", "Kafka"), + ("flink", "Flink"), + ("spark", "Spark"), + ("etcd", "etcd"), + ("java", "Java"), + ("kubelet", "K8s"), + ("rabbitmq", "RabbitMQ"), + ("zookeeper", "ZooKeeper"), + ("consul", "Consul"), + ("prometheus", "Prometheus"), + ("grafana", "Grafana"), + ("dockerd", "Docker"), + ("containerd", "containerd"), + ("coredns", "CoreDNS"), + ("node_export", "NodeExporter"), + ("tidb-server", "TiDB"), + ("tikv-server", "TiKV"), + ("minio", "MinIO"), + ("pulsar", "Pulsar"), +]; + +pub fn detect_services(info: &SystemInfo) -> Vec<&'static str> { + KNOWN_SERVICES + .iter() + .filter(|(proc, _)| info.has_process(proc)) + .map(|(_, label)| *label) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::detect::*; + + fn info_with(procs: &[&str]) -> SystemInfo { + SystemInfo { + kernel_version: String::new(), + os_distro: String::new(), + cpu_model: String::new(), + cpu_cores: 1, + numa_nodes: 1, + memory_total_gb: 1, + disks: vec![], + network: vec![], + sysctl: SysctlValues { + swappiness: 60, + dirty_ratio: 20, + dirty_background_ratio: 10, + somaxconn: 128, + tcp_fastopen: 1, + thp_enabled: "always".into(), + }, + processes: procs + .iter() + .map(|n| ProcessInfo { + name: n.to_string(), + }) + .collect(), + } + } + + #[test] + fn test_detect_services_finds_known() { + let info = info_with(&["nginx", "postgres", "unknown"]); + let svcs = detect_services(&info); + assert!(svcs.contains(&"Nginx")); + assert!(svcs.contains(&"PostgreSQL")); + assert!(!svcs.contains(&"unknown")); + } + + #[test] + fn test_detect_services_empty() { + let info = info_with(&[]); + assert!(detect_services(&info).is_empty()); + } +} diff --git a/src/ktuner/src/tuner/mod.rs b/src/ktuner/src/tuner/mod.rs new file mode 100644 index 000000000..5d42e9f9d --- /dev/null +++ b/src/ktuner/src/tuner/mod.rs @@ -0,0 +1,920 @@ +use anyhow::{Context, Result}; +use colored::Colorize; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; + +use crate::bench::BenchResult; +use crate::rules::Recommendation; + +const ROLLBACK_PATH: &str = "/var/lib/ktuner/rollback.json"; +const SYSCTL_PERSIST_PATH: &str = "/etc/sysctl.d/99-ktuner.conf"; + +#[derive(Serialize, Deserialize)] +struct RollbackEntry { + previous: String, + applied: String, + path: String, +} + +#[derive(Serialize, Deserialize)] +struct RollbackData { + version: u32, + entries: BTreeMap, +} + +pub fn apply(recommendations: &[Recommendation]) -> Result { + apply_inner(recommendations, false) +} + +pub fn apply_quiet(recommendations: &[Recommendation]) -> Result { + apply_inner(recommendations, true) +} + +fn apply_inner(recommendations: &[Recommendation], quiet: bool) -> Result { + let total = recommendations.len(); + let mut applied_recs: Vec = Vec::new(); + for (i, rec) in recommendations.iter().enumerate() { + match apply_single(rec) { + Ok(()) => { + if !quiet { + println!( + " {} [{}/{}] {} → {}", + "✓".green(), + i + 1, + total, + rec.param, + rec.recommended_value + ); + } + applied_recs.push(rec.clone()); + } + Err(e) => { + if !quiet { + println!( + " {} [{}/{}] {} : {}", + "✗".red(), + i + 1, + total, + rec.param, + e + ); + } + } + } + } + + if !applied_recs.is_empty() { + save_rollback(&applied_recs)?; + persist_from_rollback()?; + if !quiet { + println!(); + println!( + " {} 项配置已应用并持久化(重启后自动生效)", + applied_recs.len() + ); + } + } else if !quiet { + println!(); + println!(" 没有配置被成功应用"); + } + Ok(applied_recs.len()) +} + +/// Apply a single recommendation with rollback recording and persistence, but +/// without apply()'s progress output — used by `ktuner fix` so a single fix is +/// just as reversible (and survives reboot) as `tune`. +pub fn apply_one(rec: &Recommendation) -> Result<()> { + apply_single(rec)?; + save_rollback(std::slice::from_ref(rec))?; + persist_from_rollback()?; + Ok(()) +} + +fn apply_single(rec: &Recommendation) -> Result<()> { + write_and_verify(&rec.param, &rec.recommended_value) +} + +/// Write `value` to the kernel path for `param` and verify it took effect by +/// reading it back. This is the single choke point for every live parameter +/// write (tune / fix / import all route through here), so the code-execution +/// deny-list is enforced here too as defense-in-depth — see is_forbidden_param. +pub fn write_and_verify(param: &str, value: &str) -> Result<()> { + if is_forbidden_param(param) { + anyhow::bail!("拒绝写入可执行代码的内核参数 {param}(core_pattern / modprobe 等)"); + } + + let path = param_to_path(param); + + if !Path::new(&path).exists() { + anyhow::bail!("参数路径不存在"); + } + + fs::write(&path, value).with_context(|| { + let is_root = unsafe { libc::geteuid() } == 0; + if is_root { + format!("写入 {path} 失败(容器内参数只读)") + } else { + format!("写入 {path} 失败(需要 sudo 权限)") + } + })?; + + // Verify by reading back. Some tunables are write-only (mode 0200, e.g. + // vm.drop_caches / vm.compact_memory): the write is accepted but the read + // fails — treat that as success, not a spurious verify failure, since the + // kernel took the write. + let readback = match fs::read_to_string(&path) { + Ok(s) => s, + Err(_) => return Ok(()), + }; + + let readback_trimmed = readback.trim(); + if !readback_matches(value, readback_trimmed) { + anyhow::bail!("验证失败: 期望 '{value}', 实际 '{readback_trimmed}'"); + } + + Ok(()) +} + +/// Whether a sysfs/sysctl read-back indicates `value` took effect. sysfs "list" +/// files (block scheduler, transparent_hugepage/enabled|defrag, ...) echo every +/// option and mark the ACTIVE one in brackets, e.g. "always madvise [never]" — +/// the selected value is inside `[ ]`, not necessarily first. So whenever the +/// read-back contains a bracketed token we look for `[value]`; otherwise we +/// compare tokens (tolerating a single written value against a multi-token +/// read-back that leads with it). Previously only params literally named +/// "scheduler" got the bracket-aware path, so THP writes were mis-reported as +/// verify failures. +fn readback_matches(value: &str, readback_trimmed: &str) -> bool { + if readback_trimmed.contains('[') { + return readback_trimmed.contains(&format!("[{value}]")); + } + let rec_tokens: Vec<&str> = value.split_whitespace().collect(); + let read_tokens: Vec<&str> = readback_trimmed.split_whitespace().collect(); + if rec_tokens.len() == 1 && read_tokens.len() > 1 { + read_tokens.first() == rec_tokens.first() + } else { + rec_tokens == read_tokens + } +} + +/// Drop `..`, `.` and empty path components so a parameter name can never +/// escape its intended root (defends against path traversal via `ktuner import` +/// of a malicious .conf — see is_safe_param). Legitimate single/nested segments +/// are preserved unchanged. +fn sanitize_rel(s: &str) -> String { + s.split('/') + .filter(|p| !p.is_empty() && *p != "." && *p != "..") + .collect::>() + .join("/") +} + +pub fn param_to_path(param: &str) -> String { + if let Some(rest) = param.strip_prefix("block/") { + let parts: Vec<&str> = rest.splitn(2, '/').collect(); + if parts.len() == 2 { + format!( + "/sys/block/{}/queue/{}", + sanitize_rel(parts[0]), + sanitize_rel(parts[1]) + ) + } else { + format!("/sys/block/{}", sanitize_rel(rest)) + } + } else if let Some(rest) = param.strip_prefix("transparent_hugepage/") { + format!("/sys/kernel/mm/transparent_hugepage/{}", sanitize_rel(rest)) + } else { + // sysctl: dots become slashes, so any ".." is turned into "//" and + // cannot traverse; the result is always rooted at /proc/sys. + format!("/proc/sys/{}", param.replace('.', "/")) + } +} + +/// Whether a parameter name is structurally legitimate to apply. Used to reject +/// hostile entries from imported config files before they ever reach the +/// filesystem. Rejects traversal, absolute paths and NUL bytes. +pub fn is_safe_param(param: &str) -> bool { + if param.is_empty() || param.starts_with('/') || param.contains('\0') { + return false; + } + // `..` is only a traversal between path separators. The sysctl branch turns + // dots into slashes (so any ".." there collapses to "//" and cannot + // escape), leaving the block// and transparent_hugepage/ branches — both + // '/'-separated — as the real risk. + if param.split('/').any(|seg| seg == "..") { + return false; + } + true +} + +/// Kernel parameters that turn an attacker-controlled string into code the +/// kernel later runs as root (`kernel.core_pattern`'s `|program`, the +/// `modprobe` / `hotplug` / `poweroff_cmd` helper paths, `binfmt_misc` +/// handlers, `usermodehelper` gates), or that flip a one-way switch a +/// *reversible* tuner must never touch (`modules_disabled`, +/// `kexec_load_disabled`). `ktuner import` reads an UNTRUSTED .conf, so these +/// are rejected outright before any write — membership is unconditional, no +/// value is "safe". ktuner's own rules never recommend these, so guarding the +/// write choke point (write_and_verify) with this list is defense-in-depth +/// with zero legitimate-use regression. +pub fn is_forbidden_param(param: &str) -> bool { + // Match on the RESOLVED filesystem path, not on the parameter's spelling, so + // every equivalent spelling that lands on the same file is rejected: dotted + // `kernel.core_pattern`, slashed `kernel/core_pattern`, doubled separators + // `kernel//core_pattern`, or a `..`-laden name. A dotted-name-only deny-list + // was fully bypassable because param_to_path's `.replace('.', "/")` is a + // no-op on an already-slashed name, so `kernel/core_pattern` dodged the list + // yet still resolved to /proc/sys/kernel/core_pattern. + const FORBIDDEN_PATHS: &[&str] = &[ + "/proc/sys/kernel/core_pattern", + "/proc/sys/kernel/modprobe", + "/proc/sys/kernel/hotplug", + "/proc/sys/kernel/poweroff_cmd", + "/proc/sys/kernel/modules_disabled", + "/proc/sys/kernel/kexec_load_disabled", + "/proc/sys/kernel/usermodehelper", // + /bset, /inheritable ... + "/proc/sys/fs/binfmt_misc", // + /register ... + ]; + + let resolved = canonicalize_path(¶m_to_path(param)); + FORBIDDEN_PATHS + .iter() + .any(|p| resolved == *p || resolved.starts_with(&format!("{p}/"))) +} + +/// Collapse empty/`.` segments and resolve `..` in a slash path so equivalent +/// spellings normalise to one comparable absolute form (e.g. `/a//b/../c` -> +/// `/a/c`). Used so is_forbidden_param can compare resolved paths. +fn canonicalize_path(path: &str) -> String { + let mut out: Vec<&str> = Vec::new(); + for seg in path.split('/') { + match seg { + "" | "." => {} + ".." => { + out.pop(); + } + s => out.push(s), + } + } + format!("/{}", out.join("/")) +} + +fn load_rollback() -> RollbackData { + if let Ok(json) = fs::read_to_string(ROLLBACK_PATH) { + if let Ok(data) = serde_json::from_str::(&json) { + return data; + } + } + RollbackData { + version: 1, + entries: BTreeMap::new(), + } +} + +/// Write `content` to `path` atomically (tmp file + rename) so a concurrent +/// reader or a crash mid-write never sees a truncated file. +fn write_atomic(path: &str, content: &[u8]) -> Result<()> { + let pid = unsafe { libc::getpid() }; + let tmp = format!("{path}.tmp.{pid}"); + fs::write(&tmp, content).with_context(|| format!("写入临时文件 {tmp} 失败"))?; + fs::rename(&tmp, path).with_context(|| format!("替换 {path} 失败"))?; + Ok(()) +} + +fn save_rollback(recommendations: &[Recommendation]) -> Result<()> { + merge_rollback(recommendations.iter().map(|r| { + ( + r.param.clone(), + r.current_value.clone(), + r.recommended_value.clone(), + ) + })) +} + +/// Merge `(param, previous, applied)` entries into the cumulative rollback +/// record. For a param already recorded, keep the ORIGINAL `previous` (the true +/// pre-ktuner value) so rollback always restores pristine state even across +/// multiple tune/fix/import runs; only refresh `applied`. New params are added. +fn merge_rollback(entries: I) -> Result<()> +where + I: IntoIterator, +{ + let dir = Path::new(ROLLBACK_PATH).parent().unwrap(); + fs::create_dir_all(dir).context("创建 rollback 目录失败")?; + + let mut data = load_rollback(); + for (param, previous, applied) in entries { + let path = param_to_path(¶m); + data.entries + .entry(param) + .and_modify(|e| e.applied = applied.clone()) + .or_insert_with(|| RollbackEntry { + previous: previous.clone(), + applied: applied.clone(), + path, + }); + } + + let json = serde_json::to_string_pretty(&data)?; + write_atomic(ROLLBACK_PATH, json.as_bytes()).context("保存 rollback 文件失败")?; + Ok(()) +} + +/// Apply one parameter from an imported (untrusted) .conf: enforce the +/// code-execution deny-list + write + read-back verify (all via +/// write_and_verify), then record it in the rollback ledger so `ktuner +/// rollback` can undo it. This gives `import` the same safety net as +/// `fix`/`tune` — previously import did a raw, unguarded, unverified fs::write +/// with no way back. `current` is the pre-write value: rollback is only +/// recorded when it is known, so we never record a bogus "" original to restore. +pub fn apply_import(param: &str, value: &str, current: Option<&str>) -> Result<()> { + write_and_verify(param, value)?; + if let Some(prev) = current { + merge_rollback(std::iter::once(( + param.to_string(), + prev.to_string(), + value.to_string(), + )))?; + } + Ok(()) +} + +const NONSYSCTL_SCRIPT_PATH: &str = "/etc/ktuner/apply-nonsysctl.sh"; +const NONSYSCTL_SERVICE_PATH: &str = "/etc/systemd/system/ktuner-nonsysctl.service"; + +/// Regenerate the persisted config files from the cumulative rollback record, +/// which is the single source of truth for everything ktuner has applied. This +/// keeps persistence cumulative across runs (previously each run overwrote the +/// files with only its own batch, silently dropping earlier params) and never +/// persists a param that failed to apply (those are not in the record). +fn persist_from_rollback() -> Result<()> { + let data = load_rollback(); + + let mut sysctl_content = String::from("# Generated by ktuner - do not edit manually\n"); + sysctl_content.push_str("# Run `sudo ktuner rollback` to revert\n\n"); + + let mut nonsysctl_script = String::from("#!/bin/bash\n"); + nonsysctl_script.push_str("# Generated by ktuner - do not edit manually\n"); + nonsysctl_script.push_str("# Run `sudo ktuner rollback` to revert\n\n"); + + let mut has_sysctl = false; + let mut has_nonsysctl = false; + + for (param, entry) in &data.entries { + if param.starts_with("block/") || param.starts_with("transparent_hugepage/") { + nonsysctl_script.push_str(&format!( + "[ -f '{}' ] && echo '{}' > '{}'\n", + entry.path, entry.applied, entry.path + )); + has_nonsysctl = true; + } else if param.contains('.') { + sysctl_content.push_str(&format!("{} = {}\n", param, entry.applied)); + has_sysctl = true; + } + } + + if has_sysctl { + write_atomic(SYSCTL_PERSIST_PATH, sysctl_content.as_bytes()) + .context("持久化 sysctl 配置失败(需要 root 权限?)")?; + } + + if has_nonsysctl { + let dir = Path::new(NONSYSCTL_SCRIPT_PATH).parent().unwrap(); + fs::create_dir_all(dir).ok(); + fs::write(NONSYSCTL_SCRIPT_PATH, &nonsysctl_script) + .context("写入非 sysctl 持久化脚本失败")?; + + use std::os::unix::fs::PermissionsExt; + fs::set_permissions(NONSYSCTL_SCRIPT_PATH, fs::Permissions::from_mode(0o755)).ok(); + + let service = format!( + "[Unit]\n\ + Description=Apply ktuner non-sysctl kernel parameters\n\ + After=local-fs.target\n\n\ + [Service]\n\ + Type=oneshot\n\ + ExecStart={NONSYSCTL_SCRIPT_PATH}\n\ + RemainAfterExit=yes\n\n\ + [Install]\n\ + WantedBy=multi-user.target\n" + ); + + fs::write(NONSYSCTL_SERVICE_PATH, &service).context("写入 systemd service 失败")?; + + systemctl_quiet(&["daemon-reload"]); + systemctl_quiet(&["enable", "ktuner-nonsysctl.service"]); + } + + Ok(()) +} + +fn systemctl_quiet(args: &[&str]) { + use std::process::Stdio; + std::process::Command::new("systemctl") + .args(args) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .ok(); +} + +pub fn rollback_preview() -> Result> { + if !Path::new(ROLLBACK_PATH).exists() { + return Ok(Vec::new()); + } + + let json = fs::read_to_string(ROLLBACK_PATH).context("读取 rollback 文件失败")?; + let data: RollbackData = serde_json::from_str(&json).context("解析 rollback 文件失败")?; + + let mut result = Vec::new(); + for (param, entry) in &data.entries { + result.push((param.clone(), entry.applied.clone(), entry.previous.clone())); + } + Ok(result) +} + +/// Outcome of a rollback attempt: how many params were restored vs. failed to +/// restore vs. skipped (path absent). `failed`/`skipped` decide whether the +/// rollback ledger is safe to delete and whether the restore was actually total. +pub struct RollbackOutcome { + pub restored: usize, + pub failed: usize, + pub skipped: usize, +} + +/// How to summarise a rollback to the user. Kept as a pure classifier so the +/// "系统恢复原状" (fully restored) claim is only made when it is actually true — +/// the caller previously printed it unconditionally, even when 0 params were +/// restored. +#[derive(Debug, PartialEq, Eq)] +pub enum RollbackStatus { + /// Every recorded param was restored to its original value. + Full, + /// Some params were restored but at least one failed. + Partial, + /// Nothing was restored (0 succeeded), regardless of failures. + Nothing, +} + +pub fn classify_rollback(outcome: &RollbackOutcome) -> RollbackStatus { + if outcome.restored == 0 { + RollbackStatus::Nothing + } else if outcome.failed == 0 && outcome.skipped == 0 { + RollbackStatus::Full + } else { + RollbackStatus::Partial + } +} + +/// Tear down persisted config and delete the rollback ledger ONLY when EVERY +/// recorded param was actually restored. A param that failed to write OR whose +/// path was absent (skipped) is unrestored and its original value is still +/// needed, so the ledger must be kept and `ktuner rollback` can be retried. +/// Gating on `failed==0` alone lost the originals of skipped params (e.g. an +/// offline block device), and deleting the ledger when everything was skipped +/// (restored==0) was a regression over the prior `restored>0` guard. +fn rollback_should_finalize(failed: usize, skipped: usize) -> bool { + failed == 0 && skipped == 0 +} + +pub fn rollback() -> Result { + rollback_inner(false) +} + +pub fn rollback_quiet() -> Result { + rollback_inner(true) +} + +fn rollback_inner(quiet: bool) -> Result { + if !Path::new(ROLLBACK_PATH).exists() { + anyhow::bail!("没有找到 rollback 文件 ({ROLLBACK_PATH}),可能尚未执行过 tune"); + } + + let json = fs::read_to_string(ROLLBACK_PATH).context("读取 rollback 文件失败")?; + let data: RollbackData = serde_json::from_str(&json).context("解析 rollback 文件失败")?; + + let mut restored = 0; + let mut failed = 0; + let mut skipped = 0; + for (param, entry) in &data.entries { + if is_forbidden_param(param) { + if !quiet { + println!(" {} {} : 拒绝恢复(代码执行参数)", "✗".red(), param); + } + failed += 1; + continue; + } + if Path::new(&entry.path).exists() { + match fs::write(&entry.path, &entry.previous) { + Ok(()) => { + if !quiet { + println!(" {} {} → {} (已恢复)", "✓".green(), param, entry.previous); + } + restored += 1; + } + Err(e) => { + if !quiet { + println!(" {} {} : {}", "✗".red(), param, e); + } + failed += 1; + } + } + } else { + if !quiet { + println!(" {} {} : 路径不存在,跳过", "⊘".yellow(), param); + } + skipped += 1; + } + } + + if rollback_should_finalize(failed, skipped) { + if Path::new(SYSCTL_PERSIST_PATH).exists() { + fs::remove_file(SYSCTL_PERSIST_PATH).ok(); + if !quiet { + println!(" 已清理 {SYSCTL_PERSIST_PATH}"); + } + } + + if Path::new(NONSYSCTL_SERVICE_PATH).exists() { + systemctl_quiet(&["disable", "ktuner-nonsysctl.service"]); + fs::remove_file(NONSYSCTL_SERVICE_PATH).ok(); + systemctl_quiet(&["daemon-reload"]); + if !quiet { + println!(" 已清理 {NONSYSCTL_SERVICE_PATH}"); + } + } + + if Path::new(NONSYSCTL_SCRIPT_PATH).exists() { + fs::remove_file(NONSYSCTL_SCRIPT_PATH).ok(); + if !quiet { + println!(" 已清理 {NONSYSCTL_SCRIPT_PATH}"); + } + } + + fs::remove_file(ROLLBACK_PATH).ok(); + } else if !quiet { + println!( + " {} {} 项恢复失败、{} 项路径缺失,已保留 {} 以便重试(未删除持久化配置)", + "⚠".yellow(), + failed, + skipped, + ROLLBACK_PATH + ); + } + + if !quiet { + println!(); + println!(" 共恢复 {restored} 项配置。"); + } + Ok(RollbackOutcome { + restored, + failed, + skipped, + }) +} + +const DEGRADATION_THRESHOLD: f64 = 10.0; +const ROLLBACK_MIN_DEGRADED: usize = 2; + +pub struct VerifyResult { + pub degraded: Vec, +} + +pub fn verify_and_report(before: &[BenchResult], after: &[BenchResult]) -> VerifyResult { + println!(" {}", "性能对比 (before → after)".bold()); + println!( + " {:<24} {:>16} {:>16} {:>8}", + "指标", "调前", "调后", "变化" + ); + println!(" {}", "─".repeat(66)); + + let mut degraded = Vec::new(); + + for (b, a) in before.iter().zip(after.iter()) { + let change = if b.value > 0.0 { + (a.value - b.value) / b.value * 100.0 + } else { + 0.0 + }; + + let is_latency = b.unit.contains("ns") || b.unit.contains("μs"); + + let is_degraded = if is_latency { + change > DEGRADATION_THRESHOLD + } else { + change < -DEGRADATION_THRESHOLD + }; + + if is_degraded { + degraded.push(b.name.clone()); + } + + let change_display = if change.abs() < 1.0 { + "—".dimmed().to_string() + } else if is_latency { + if change < 0.0 { + format!("↓{:.1}%", change.abs()).green().to_string() + } else if is_degraded { + format!("↑{change:.1}% ⚠").red().to_string() + } else { + format!("↑{change:.1}%").yellow().to_string() + } + } else if change > 0.0 { + format!("↑{change:.1}%").green().to_string() + } else if is_degraded { + format!("↓{:.1}% ⚠", change.abs()).red().to_string() + } else { + format!("↓{:.1}%", change.abs()).yellow().to_string() + }; + + let before_val = format!("{:>8.2} {:<10}", b.value, b.unit); + let after_val = format!("{:>8.2} {:<10}", a.value, a.unit); + println!( + " {:<24} {} {} {}", + b.name, before_val, after_val, change_display + ); + } + + VerifyResult { degraded } +} + +/// Returns `None` when degradation is below the auto-rollback threshold (no +/// rollback attempted), or `Some(outcome)` with the ACTUAL restore counts when a +/// rollback was performed. Callers must inspect the outcome before claiming the +/// system was restored — previously this returned a bare `true` even when +/// `rollback()` restored nothing, so the CLI told the user "已回滚,系统恢复原状" +/// while the tuned (degraded) values were still live. +pub fn auto_rollback_on_degradation(result: &VerifyResult) -> Result> { + if result.degraded.len() < ROLLBACK_MIN_DEGRADED { + if result.degraded.len() == 1 { + println!(); + println!( + " {} {} 出现波动,可能是 benchmark 噪声,未自动回滚。", + "△".yellow(), + result.degraded[0] + ); + println!( + " 建议重新运行确认,或手动回滚: {}", + "sudo ktuner rollback".bold() + ); + } + return Ok(None); + } + + println!(); + println!( + " {} 检测到 {} 项指标恶化超过 {}%,执行自动回滚...", + "⚠".yellow(), + result.degraded.len(), + DEGRADATION_THRESHOLD as u32 + ); + for name in &result.degraded { + println!(" - {}", name.red()); + } + println!(); + + let outcome = rollback()?; + Ok(Some(outcome)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_param_to_path_sysctl() { + assert_eq!(param_to_path("vm.swappiness"), "/proc/sys/vm/swappiness"); + assert_eq!( + param_to_path("net.core.somaxconn"), + "/proc/sys/net/core/somaxconn" + ); + assert_eq!( + param_to_path("net.ipv4.tcp_fastopen"), + "/proc/sys/net/ipv4/tcp_fastopen" + ); + assert_eq!( + param_to_path("kernel.randomize_va_space"), + "/proc/sys/kernel/randomize_va_space" + ); + assert_eq!( + param_to_path("net.core.rmem_max"), + "/proc/sys/net/core/rmem_max" + ); + } + + #[test] + fn test_param_to_path_block_device() { + assert_eq!( + param_to_path("block/sda/scheduler"), + "/sys/block/sda/queue/scheduler" + ); + assert_eq!( + param_to_path("block/nvme0n1/nr_requests"), + "/sys/block/nvme0n1/queue/nr_requests" + ); + assert_eq!( + param_to_path("block/sda/read_ahead_kb"), + "/sys/block/sda/queue/read_ahead_kb" + ); + assert_eq!( + param_to_path("block/nvme0n1/rq_affinity"), + "/sys/block/nvme0n1/queue/rq_affinity" + ); + } + + #[test] + fn test_param_to_path_thp() { + assert_eq!( + param_to_path("transparent_hugepage/enabled"), + "/sys/kernel/mm/transparent_hugepage/enabled" + ); + } + + #[test] + fn test_param_to_path_rejects_traversal() { + // Traversal components must be stripped so the result can never escape + // its root, even from a hostile imported .conf. + assert_eq!( + param_to_path("transparent_hugepage/../../../../etc/cron.d/evil"), + "/sys/kernel/mm/transparent_hugepage/etc/cron.d/evil" + ); + assert_eq!( + param_to_path("block/sda/../../../../etc/passwd"), + "/sys/block/sda/queue/etc/passwd" + ); + // None of these may contain a ".." component after sanitization. + for p in ["transparent_hugepage/../x", "block/x/../../y"] { + assert!(!param_to_path(p).split('/').any(|s| s == "..")); + } + } + + #[test] + fn test_is_safe_param() { + assert!(is_safe_param("vm.swappiness")); + assert!(is_safe_param("block/sda/scheduler")); + assert!(is_safe_param("transparent_hugepage/enabled")); + assert!(!is_safe_param("transparent_hugepage/../../../etc/cron.d/x")); + assert!(!is_safe_param("block/x/../../../etc/passwd")); + assert!(!is_safe_param("/etc/passwd")); + assert!(!is_safe_param("")); + assert!(!is_safe_param("..")); + } + + #[test] + fn test_is_forbidden_param_blocks_code_exec() { + // Code-execution / one-way primitives must be rejected — writing these + // from an untrusted imported .conf is a root RCE or an irreversible + // brick. + for p in [ + "kernel.core_pattern", + "kernel.modprobe", + "kernel.hotplug", + "kernel.poweroff_cmd", + "kernel.modules_disabled", + "kernel.kexec_load_disabled", + "kernel.usermodehelper.bset", + "kernel.usermodehelper.inheritable", + "fs.binfmt_misc.register", + "fs.binfmt_misc", + ] { + assert!(is_forbidden_param(p), "{p} must be forbidden"); + } + } + + #[test] + fn test_is_forbidden_param_allows_normal_tunables() { + // Ordinary tunables must stay writable or every `tune` would break. + for p in [ + "vm.swappiness", + "net.core.somaxconn", + "kernel.sched_migration_cost_ns", + "kernel.randomize_va_space", + "kernel.numa_balancing", + ] { + assert!(!is_forbidden_param(p), "{p} must be allowed"); + } + // Prefix guard must respect the dot boundary and not over-match params + // that merely share a stem. + assert!(!is_forbidden_param("kernel.core_uses_pid")); + assert!(!is_forbidden_param("fs.binfmt_misc_unrelated")); + } + + #[test] + fn test_classify_rollback() { + assert_eq!( + classify_rollback(&RollbackOutcome { + restored: 3, + failed: 0, + skipped: 0 + }), + RollbackStatus::Full + ); + assert_eq!( + classify_rollback(&RollbackOutcome { + restored: 2, + failed: 1, + skipped: 0 + }), + RollbackStatus::Partial + ); + // A skipped (path-absent) param means the restore was NOT total, so it + // must be Partial — not Full — even with zero write failures. This is the + // cosmetic contradiction the skipped field fixes. + assert_eq!( + classify_rollback(&RollbackOutcome { + restored: 2, + failed: 0, + skipped: 1 + }), + RollbackStatus::Partial + ); + assert_eq!( + classify_rollback(&RollbackOutcome { + restored: 0, + failed: 2, + skipped: 0 + }), + RollbackStatus::Nothing + ); + // 0 restored must NEVER be reported as a full restore, even with 0 + // failures — this is the exact false-success the fix removes. + assert_eq!( + classify_rollback(&RollbackOutcome { + restored: 0, + failed: 0, + skipped: 0 + }), + RollbackStatus::Nothing + ); + } + + #[test] + fn test_rollback_finalize_only_when_all_restored() { + // Finalize (delete ledger) only when EVERY param was restored: zero + // failures AND zero skipped. A failed write or an absent path must keep + // the ledger so originals aren't lost. + assert!(rollback_should_finalize(0, 0)); + assert!(!rollback_should_finalize(1, 0)); // a write failed + assert!(!rollback_should_finalize(0, 1)); // a path was absent — the missed case + assert!(!rollback_should_finalize(2, 3)); + } + + #[test] + fn test_is_forbidden_param_resists_spelling_bypass() { + // Every spelling that resolves to a forbidden /proc/sys file must be + // caught, not just the canonical dotted name — the original deny-list + // was bypassed by writing kernel/core_pattern (slashes) in a .conf. + for p in [ + "kernel/core_pattern", + "kernel//core_pattern", + "kernel/modprobe", + "fs/binfmt_misc/register", + "kernel/usermodehelper/bset", + ] { + assert!( + is_forbidden_param(p), + "{p} must be forbidden (slash spelling)" + ); + } + } + + #[test] + fn test_readback_matches() { + // Bracketed sysfs list files: the active option is inside [ ], not first. + assert!(readback_matches("never", "always madvise [never]")); + assert!(readback_matches("mq-deadline", "[mq-deadline] none")); + assert!(!readback_matches("never", "[always] madvise never")); + // Plain scalar sysctls. + assert!(readback_matches("1", "1")); + assert!(!readback_matches("1", "0")); + // Single written value leading a multi-token read-back matches on first. + assert!(readback_matches("bbr", "bbr cubic")); + // Multi-token exact match, and its negation. + assert!(readback_matches("250 32000 100 128", "250 32000 100 128")); + assert!(!readback_matches("250 32000 100 128", "250 32000 100 999")); + } + + #[test] + fn test_canonicalize_path() { + assert_eq!( + canonicalize_path("/proc/sys/kernel/core_pattern"), + "/proc/sys/kernel/core_pattern" + ); + assert_eq!( + canonicalize_path("/proc/sys/kernel//core_pattern"), + "/proc/sys/kernel/core_pattern" + ); + assert_eq!( + canonicalize_path("/proc/sys/kernel/./core_pattern"), + "/proc/sys/kernel/core_pattern" + ); + assert_eq!( + canonicalize_path("/proc/sys/kernel/foo/../core_pattern"), + "/proc/sys/kernel/core_pattern" + ); + assert_eq!(canonicalize_path("/a/b/../c"), "/a/c"); + assert_eq!(canonicalize_path("/a/b/../../c"), "/c"); + assert_eq!(canonicalize_path("/"), "/"); + } +} diff --git a/src/os-skills/system-admin/ktuner/SKILL.md b/src/os-skills/system-admin/ktuner/SKILL.md new file mode 100644 index 000000000..694b01109 --- /dev/null +++ b/src/os-skills/system-admin/ktuner/SKILL.md @@ -0,0 +1,108 @@ +--- +name: ktuner +version: 0.1.0 +description: 内核参数自动调优。分析系统配置,输出调优建议并可一键应用,支持自动回滚。使用场景:系统性能优化、安全加固、内核参数诊断。 +layer: core +lifecycle: production +allowedTools: + - run_shell_command +--- + +# ktuner — 内核参数自动调优 + +## 核心能力 + +ktuner 是确定性规则引擎(非 LLM),评估 207 条内核调优规则,输出 JSON 格式的诊断结果和建议。 + +**所有输出在 stdout,格式为 JSON。错误输出在 stderr,格式也是 JSON。** + +## 使用流程 + +### 第一步:诊断 + +```bash +ktuner check +``` + +输出示例: +```json +{ + "score": 30, + "predicted_score": 100, + "total_checked": 196, + "recommendations": [ + { + "param": "net.ipv4.tcp_rfc1337", + "current": "0", + "recommended": "1", + "reason": "防止 TIME_WAIT 状态下的 RST 攻击", + "confidence": "high", + "category": "security", + "writable": true + } + ], + "system": { "kernel": "6.6.102+", "cpu_cores": 2, "memory_gb": 8 }, + "workload": "mixed", + "services": ["Nginx", "PostgreSQL"] +} +``` + +**根据 score 判断是否需要调优**:90+ 优秀,70-89 良好,低于 70 建议调优。 + +可选参数: +- `--category net|mem|io|cpu|security` — 只看某一类 +- `--conservative` — 只看高置信度建议 + +### 第二步:向用户解释建议 + +遍历 recommendations 数组,用中文向用户解释每条建议的 reason 和影响。 + +如果用户想了解某个具体参数: +```bash +ktuner why <参数名> +``` + +### 第三步:应用(需要 root,必须用户确认后才执行) + +**⚠️ 重要:tune 和 fix 会修改内核参数,必须先向用户展示建议内容,得到明确确认后再执行。** + +预览模式(不修改): +```bash +sudo ktuner tune --dry-run +``` + +应用全部建议: +```bash +sudo ktuner tune +``` + +只修一个参数: +```bash +sudo ktuner fix <参数名> +``` + +### 第四步:回滚(如果需要) + +```bash +sudo ktuner rollback +``` + +输出: +```json +{ "restored": 5, "failed": 0, "skipped": 0, "status": "Full" } +``` + +## 退出码 + +| 退出码 | 含义 | +|--------|------| +| 0 | 成功(check 时表示系统已最优) | +| 1 | check 发现有建议(不是错误,表示可以优化) | +| 2 | 错误(详见 stderr 的 JSON) | + +## 约束 + +- tune / fix / rollback 需要 **root 权限**(使用 `sudo`) +- check / why 不需要 root +- `kernel.core_pattern`、`kernel.modprobe` 等代码执行参数被禁止写入 +- 调优后如发现性能劣化,使用 `sudo ktuner rollback` 恢复 From be2e49522604943c34133d6ebb5858c7a4a9ad67 Mon Sep 17 00:00:00 2001 From: Jiangtian Feng Date: Fri, 3 Jul 2026 21:48:25 +0800 Subject: [PATCH 2/4] fix(ktuner): align license with project-wide Apache-2.0 --- src/ktuner/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ktuner/Cargo.toml b/src/ktuner/Cargo.toml index b2dd7d354..f542966e7 100644 --- a/src/ktuner/Cargo.toml +++ b/src/ktuner/Cargo.toml @@ -3,7 +3,7 @@ name = "ktuner" version = "0.1.0" edition = "2021" description = "Deterministic kernel-tuning engine for ANOLISA agents" -license = "MIT" +license = "Apache-2.0" [lib] name = "ktuner_engine" From cef6c27694b44698916ab900b08056286d9001d9 Mon Sep 17 00:00:00 2001 From: Jiangtian Feng Date: Sat, 4 Jul 2026 15:37:19 +0800 Subject: [PATCH 3/4] chore(ktuner): align docs and CI scope - Add ktuner row to root README component table - Add ktuner scope to prelint validScopes (title + branch checks) - Add src/ktuner/CHANGELOG.md ([Unreleased] stub) - test-ktuner: cargo test --lib -> cargo test (parity, no-op today) --- .github/workflows/ci.yaml | 2 +- .github/workflows/prelint.yml | 6 +++--- README.md | 1 + src/ktuner/CHANGELOG.md | 12 ++++++++++++ 4 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 src/ktuner/CHANGELOG.md diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c4e88b048..109e53ad8 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1009,4 +1009,4 @@ jobs: - name: Run tests run: | cd src/ktuner - cargo test --lib + cargo test diff --git a/.github/workflows/prelint.yml b/.github/workflows/prelint.yml index f4c5b2054..f467fba04 100644 --- a/.github/workflows/prelint.yml +++ b/.github/workflows/prelint.yml @@ -68,7 +68,7 @@ jobs: `Examples: feat(cosh): add json output\n` + ` fix(sec-core): handle sandbox escape\n` + `Valid types: feat, fix, refactor, perf, docs, chore, test, ci, build, style, revert\n` + - `Valid scopes: cosh, sec-core, skill, sight, tokenless, ckpt, memory, anolisa, skillfs, deps, ci, docs, chore\n` + + `Valid scopes: cosh, sec-core, skill, sight, tokenless, ckpt, memory, anolisa, skillfs, ktuner, deps, ci, docs, chore\n` + `See CONTRIBUTING.md or AGENT.md for details.` ); return; @@ -79,7 +79,7 @@ jobs: if (scopeMatch) { const scope = scopeMatch[1].slice(1, -1); const validScopes = [ - 'cosh', 'sec-core', 'skill', 'sight', 'tokenless', 'ckpt', 'memory', 'anolisa', 'skillfs', + 'cosh', 'sec-core', 'skill', 'sight', 'tokenless', 'ckpt', 'memory', 'anolisa', 'skillfs', 'ktuner', 'agent-sec-core', 'agentsight', 'os-skills', 'ws-ckpt', 'agent-memory', // legacy aliases 'deps', 'ci', 'docs', 'chore' ]; @@ -125,7 +125,7 @@ jobs: } // Valid scopes — short names preferred; legacy long names accepted for forward compatibility - const validScopes = ['cosh', 'sec-core', 'skill', 'sight', 'tokenless', 'ckpt', 'memory', 'anolisa', 'skillfs', 'agent-sec-core', 'agentsight', 'os-skills', 'ws-ckpt', 'agent-memory', 'ci', 'docs', 'deps']; + const validScopes = ['cosh', 'sec-core', 'skill', 'sight', 'tokenless', 'ckpt', 'memory', 'anolisa', 'skillfs', 'ktuner', 'agent-sec-core', 'agentsight', 'os-skills', 'ws-ckpt', 'agent-memory', 'ci', 'docs', 'deps']; const scopePattern = validScopes.join('|'); // Valid branch patterns per development spec diff --git a/README.md b/README.md index 1fd646bdb..725a7703c 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ system built for AI Agent workloads. | [Agent Memory](src/agent-memory/) | CMA-style persistent filesystem memory for AI agents, served over MCP — sandboxed file tools, SQLite FTS5 BM25 index, optional git versioning and tar.gz snapshots. Linux only. | | [OS Skills](src/os-skills/) | Curated skill library for system administration, monitoring, security, DevOps, and cloud integration. | | [SkillFS](src/skillfs/) | FUSE-backed virtual filesystem for local agent skills and view-based `SKILL.md` exposure. Linux only. | +| [ktuner](src/ktuner/) | Deterministic kernel-tuning engine for AI agents — evaluates 207 rules against the running system and outputs structured JSON tuning recommendations. Linux only. | See each component's README for detailed documentation. diff --git a/src/ktuner/CHANGELOG.md b/src/ktuner/CHANGELOG.md new file mode 100644 index 000000000..61d5d94ee --- /dev/null +++ b/src/ktuner/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to ktuner are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Initial kernel-tuning engine: `check`/`tune`/`fix`/`why`/`rollback` commands + evaluate 207 rules and output structured JSON tuning recommendations. From 8bf35d6af4d42d70003714c746ba4f92db3d6c1a Mon Sep 17 00:00:00 2001 From: Jiangtian Feng Date: Sat, 4 Jul 2026 16:09:32 +0800 Subject: [PATCH 4/4] docs(ktuner): register in AGENTS.md and user-guide Address review feedback on #1278 from @kongche-jbw: - AGENTS.md: add ktuner to component table, dev commands, Rust conventions list, and Scope Inference table - Add docs/user-guide user-entrypoint/ktuner.md (en + zh) covering diagnose / dry-run / apply / rollback and the permission boundary - Link ktuner from the user-guide index (en + zh) --- AGENTS.md | 12 ++- docs/user-guide/en/README.md | 1 + docs/user-guide/en/user-entrypoint/ktuner.md | 93 ++++++++++++++++++++ docs/user-guide/zh/README.md | 1 + docs/user-guide/zh/user-entrypoint/ktuner.md | 93 ++++++++++++++++++++ 5 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 docs/user-guide/en/user-entrypoint/ktuner.md create mode 100644 docs/user-guide/zh/user-entrypoint/ktuner.md diff --git a/AGENTS.md b/AGENTS.md index c277f0d32..9870e2d86 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,8 +17,9 @@ This file provides context for AI coding assistants (Qoder, Claude, etc.) workin | **anolisa** | `src/anolisa/` | Rust | Linux + macOS (arm64) | | **SkillFS** (`skillfs`) | `src/skillfs/` | Rust / FUSE | Linux only | | **ws-ckpt** | `src/ws-ckpt/` | Rust + TypeScript | Linux only | +| **ktuner** | `src/ktuner/` | Rust | Linux only | -> `agent-sec-core`, `agentsight`, `tokenless`, `agent-memory`, and `skillfs` require Linux. Do **not** attempt to build them on macOS or Windows. +> `agent-sec-core`, `agentsight`, `tokenless`, `agent-memory`, `skillfs`, and `ktuner` require Linux. Do **not** attempt to build them on macOS or Windows. ## 2. Development Commands @@ -83,11 +84,17 @@ cargo fmt --all --check cargo clippy --workspace --all-targets -- -D warnings cargo test --workspace scripts/test.sh # FUSE smoke test; skips itself if fuse3 or /dev/fuse is unavailable + +# ktuner (Linux only, per-component) +cd src/ktuner +cargo fmt --all --check +cargo clippy --all-targets -- -D warnings +cargo test ``` ## 3. Rust Common Conventions -> Applies to all Rust components: `anolisa`, `agentsight`, `tokenless`, `agent-memory`, `skillfs`. +> Applies to all Rust components: `anolisa`, `agentsight`, `tokenless`, `agent-memory`, `skillfs`, `ktuner`. ### 3.1 Comment Guidelines @@ -259,6 +266,7 @@ When generating commits, detect the active tool and fill in the actual version. | `src/agent-memory/` | `memory` | | `src/anolisa/` | `anolisa` | | `src/skillfs/` | `skillfs` | +| `src/ktuner/` | `ktuner` | | `.github/workflows/` | `ci` | | `docs/` | `docs` | | `**/package*.json`, `Cargo.lock`, `*.toml` (dep bumps) | `deps` | diff --git a/docs/user-guide/en/README.md b/docs/user-guide/en/README.md index 33e3f8848..03870189e 100644 --- a/docs/user-guide/en/README.md +++ b/docs/user-guide/en/README.md @@ -37,6 +37,7 @@ ANOLISA provides a complete server-side runtime for AI Agent workloads. Componen | [anolisa CLI](user-entrypoint/anolisa-cli.md) | anolisa | Unified CLI for component management | | [Copilot Shell](user-entrypoint/copilot-shell.md) | cosh | AI terminal assistant and command gateway | | [OS Skills](user-entrypoint/os-skills.md) | os-skills | System management and DevOps skills | +| [ktuner](user-entrypoint/ktuner.md) | ktuner | Deterministic kernel-parameter tuning engine | ### Agent Observability (`agent-observability/`) diff --git a/docs/user-guide/en/user-entrypoint/ktuner.md b/docs/user-guide/en/user-entrypoint/ktuner.md new file mode 100644 index 000000000..e014480d2 --- /dev/null +++ b/docs/user-guide/en/user-entrypoint/ktuner.md @@ -0,0 +1,93 @@ +# ktuner + +ktuner is a deterministic kernel-tuning engine for AI agents. It evaluates 207 rules against the running system and outputs structured JSON recommendations, so an agent (or a human) can diagnose, apply, and roll back kernel parameter changes safely. + +--- + +## Overview + +ktuner is a rule engine, not an LLM: every recommendation comes from a hard-coded rule reading `/proc/sys` and `/sys`, so results are reproducible and explainable. It covers network, memory, I/O, CPU, and security parameters, scores the current system, and predicts the score after tuning. + +It is designed to be driven by cosh and other ANOLISA-compatible agents as a tool, but the CLI is equally usable by hand. + +--- + +## Installation + +ktuner ships with the ANOLISA source tree under `src/ktuner/`. Build it from source: + +```bash +cd src/ktuner +cargo build --release +# binary at target/release/ktuner +``` + +Add `target/release/` to your `PATH`, or run the binary directly as `./target/release/ktuner` — the examples below assume `ktuner` is on your `PATH`. + +> Packaged distribution (`anolisa install ktuner` / RPM) is still being planned with the maintainers; until then, build from source. + +--- + +## Quick Start + +```bash +# Diagnose — read-only, no root required +ktuner check # score + all recommendations +ktuner check --category net # limit to one category +ktuner check --conservative # high-confidence recommendations only + +# Preview changes without applying (dry-run) +sudo ktuner tune --dry-run + +# Apply recommendations (requires root) +sudo ktuner tune # apply all +sudo ktuner tune --conservative + +# Fix a single parameter +sudo ktuner fix vm.swappiness + +# Explain why a parameter should change +ktuner why net.core.somaxconn + +# Undo all changes ktuner made +sudo ktuner rollback +``` + +All output is JSON on stdout; errors are JSON on stderr. Exit codes: `0` success, `1` check found recommendations (not an error), `2` error. + +--- + +## Permission Boundary + +| Command | Root | Effect | +|---------|------|--------| +| `check`, `why` | No | Read-only diagnosis; never writes the kernel | +| `tune --dry-run` | No | Previews changes, writes nothing | +| `tune`, `fix`, `rollback` | Yes (`sudo`) | Writes `/proc/sys`; refuses to run if not root | + +Safety guarantees: + +- **Code-execution deny-list**: parameters that can lead to code execution (`kernel.core_pattern`, `kernel.modprobe`, `kernel.hotplug`, and similar) are unconditionally blocked from every write path. Matching is on the resolved filesystem path, so spelling variants cannot bypass it. +- **Rollback safety**: applied changes are recorded; a partial rollback failure never discards the remaining original values. +- **No autonomous root**: ktuner errors out unless run as root. When invoked through cosh, the sandbox guard and permission prompt ensure a human approves before any `sudo ktuner tune` runs. + +--- + +## Usage with cosh + +cosh discovers ktuner automatically via its skill definition (`src/os-skills/system-admin/ktuner/`), so no wiring is needed — ask in natural language: + +``` +> "Check whether this machine's kernel parameters can be improved" +> "Optimize the kernel for a database workload" +``` + +A cosh first-run auto-check — a one-line `ktuner check` report shown at initial login — is landing in a separate PR. Until then, invoke `ktuner check` through the skill as above. cosh never applies changes on its own. + +--- + +## See Also + +- [Copilot Shell](copilot-shell/QUICKSTART.md) +- [OS Skills](os-skills.md) +- Full reference: `src/ktuner/README.md` diff --git a/docs/user-guide/zh/README.md b/docs/user-guide/zh/README.md index 0c8442661..8c1734849 100644 --- a/docs/user-guide/zh/README.md +++ b/docs/user-guide/zh/README.md @@ -37,6 +37,7 @@ ANOLISA 为 AI Agent 提供完整的服务端运行时能力。通过 `anolisa` | [anolisa CLI](user-entrypoint/anolisa-cli.md) | anolisa | 统一 CLI 组件管理 | | [Copilot Shell](user-entrypoint/copilot-shell.md) | cosh | AI 终端助手与命令网关 | | [OS 技能库](user-entrypoint/os-skills.md) | os-skills | 系统管理与 DevOps 技能 | +| [ktuner](user-entrypoint/ktuner.md) | ktuner | 确定性内核参数调优引擎 | ### 可观测性 `agent-observability/` diff --git a/docs/user-guide/zh/user-entrypoint/ktuner.md b/docs/user-guide/zh/user-entrypoint/ktuner.md new file mode 100644 index 000000000..5c7f12383 --- /dev/null +++ b/docs/user-guide/zh/user-entrypoint/ktuner.md @@ -0,0 +1,93 @@ +# ktuner + +ktuner 是面向 AI Agent 的确定性内核调优引擎。它对运行中的系统评估 207 条规则,输出结构化 JSON 建议,让 Agent(或用户)能安全地诊断、应用、回滚内核参数改动。 + +--- + +## 概述 + +ktuner 是规则引擎,不是 LLM:每条建议都来自读取 `/proc/sys` 和 `/sys` 的硬编码规则,因此结果可复现、可解释。它覆盖网络、内存、I/O、CPU、安全类参数,对当前系统打分,并预测调优后的分数。 + +它设计为被 cosh 及其他 ANOLISA 兼容 Agent 作为工具调用,但命令行同样可以手动使用。 + +--- + +## 安装 + +ktuner 随 ANOLISA 源码树发布,位于 `src/ktuner/`。从源码构建: + +```bash +cd src/ktuner +cargo build --release +# 二进制在 target/release/ktuner +``` + +将 `target/release/` 加入 `PATH`,或直接运行 `./target/release/ktuner`——下面的示例假设 `ktuner` 已在 `PATH` 中。 + +> 打包分发方式(`anolisa install ktuner` / RPM)仍在与维护者规划中;在此之前请从源码构建。 + +--- + +## 快速开始 + +```bash +# 诊断 —— 只读,无需 root +ktuner check # 分数 + 所有建议 +ktuner check --category net # 仅某一类别 +ktuner check --conservative # 仅高置信度建议 + +# 预览改动但不应用(dry-run) +sudo ktuner tune --dry-run + +# 应用建议(需要 root) +sudo ktuner tune # 应用全部 +sudo ktuner tune --conservative + +# 修复单个参数 +sudo ktuner fix vm.swappiness + +# 解释某个参数为何应该改 +ktuner why net.core.somaxconn + +# 撤销 ktuner 做的所有改动 +sudo ktuner rollback +``` + +所有输出为 stdout 上的 JSON,错误为 stderr 上的 JSON。退出码:`0` 成功、`1` check 发现可改进项(非错误)、`2` 错误。 + +--- + +## 权限边界 + +| 命令 | Root | 作用 | +|------|------|------| +| `check`、`why` | 否 | 只读诊断;绝不写内核 | +| `tune --dry-run` | 否 | 预览改动,不写入 | +| `tune`、`fix`、`rollback` | 是(`sudo`) | 写 `/proc/sys`;非 root 直接报错 | + +安全保证: + +- **代码执行 deny-list**:可能导致代码执行的参数(`kernel.core_pattern`、`kernel.modprobe`、`kernel.hotplug` 等)在所有写入路径上被无条件阻止。匹配基于解析后的文件系统路径,因此拼写变体无法绕过。 +- **回滚安全**:已应用的改动会被记录;部分回滚失败时绝不丢弃其余参数的原始值。 +- **无自主 root**:ktuner 非 root 运行时一律报错。通过 cosh 调用时,沙箱守卫与权限提示确保任何 `sudo ktuner tune` 都经人工批准才执行。 + +--- + +## 配合 cosh 使用 + +cosh 通过 skill 定义(`src/os-skills/system-admin/ktuner/`)自动发现 ktuner,无需接线——用自然语言提问即可: + +``` +> “看看这台机器的内核参数能不能优化” +> “按数据库负载优化内核” +``` + +cosh 首次运行自动检查——首次登录时展示一行 `ktuner check` 报告——将在独立 PR 中落地。在此之前,请按上文通过 skill 调用 `ktuner check`。cosh 绝不自行应用改动。 + +--- + +## 参见 + +- [Copilot Shell](copilot-shell/QUICKSTART.md) +- [OS Skills](os-skills.md) +- 完整参考:`src/ktuner/README.md`