commit 2f79b22936f2f9086b36031e0deb3d10e3026d3c Author: shishi Date: Sun Aug 9 10:12:57 2026 +0800 feat: 系统调优 Go 版后端完整源码 - 19 个接口全部实测验收通过 (2026-08-08) - 结构: main.go + core/consts + gsysctl + gtuner + server - 含 build_go.sh 官方部署结构构建脚本 (amd64/arm64 交叉编译) - 数据无缝衔接 Python 版遗留 (快照/state/审计日志) - 备份自 156:/root/goddhv (2026-08-09) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..480b0d9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist/ +mock_deploy*/ +*.sock +*.log diff --git a/build_go.sh b/build_go.sh new file mode 100755 index 0000000..9504bdb --- /dev/null +++ b/build_go.sh @@ -0,0 +1,80 @@ +#!/bin/bash +# GMSSH「系统调优」Go 版后端构建脚本 +# 产出官方标准结构: dist/app/bin/{main, config.json, i18n/} +set -euo pipefail + +export PATH=$PATH:/usr/local/go/bin +export GOPROXY=https://goproxy.cn,direct + +CURDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$CURDIR" + +# 架构: 默认 amd64, 可用 ARCH=arm64 交叉编译 +ARCH="${ARCH:-amd64}" +DIST_BIN="$CURDIR/dist/app/bin" + +echo "🚀 开始构建 Go 版后端 (arch=$ARCH)..." + +# 1. 编译 main +echo "⚙️ 编译 main.go (GOOS=linux GOARCH=$ARCH)..." +GOOS=linux GOARCH="$ARCH" CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o "$DIST_BIN/main" . +if [ ! -f "$DIST_BIN/main" ]; then + echo "❌ 编译失败, 未找到产物" + exit 1 +fi +echo "✅ 编译完成: $DIST_BIN/main ($(stat -c%s "$DIST_BIN/main") bytes)" + +# 2. 生成 config.json (官方格式, socket=app.sock) +echo "📝 生成 config.json..." +mkdir -p "$DIST_BIN" +cat > "$DIST_BIN/config.json" <<'JSONEOF' +{ + "test": { + "jsonrpc": { + "sockets": "app.sock" + }, + "logger": { + "path": "logs/", + "file": "{Y-m-d}.log", + "level": "info", + "stdout": true, + "rotateBackupLimit": 7, + "rotateExpire": "1d" + } + }, + "prod": { + "jsonrpc": { + "sockets": "app.sock" + }, + "logger": { + "path": "../../logs/", + "file": "{Y-m-d}.log", + "level": "error", + "stdout": false, + "rotateBackupLimit": 30, + "rotateExpire": "30d" + } + } +} +JSONEOF +echo "✅ config.json 已生成" + +# 3. 生成 i18n (基础国际化文件) +echo "📝 生成 i18n..." +mkdir -p "$DIST_BIN/i18n" +cat > "$DIST_BIN/i18n/zh-CN.ini" <<'INIIEOF' +REQUIRE_VALIDATION_TM="该参数不能为空" +STATUS_OK="操作成功" +INIIEOF +cat > "$DIST_BIN/i18n/en.ini" <<'INIIEOF' +REQUIRE_VALIDATION_TM="This parameter cannot be empty" +STATUS_OK=" Operation successful" +INIIEOF +echo "✅ i18n 已生成" + +# 4. 赋权 +chmod +x "$DIST_BIN/main" + +echo "" +echo "✅ 构建完成! 产物结构:" +find "$CURDIR/dist/app/bin" -type f | sed "s|$CURDIR/||" diff --git a/build_verify.sh b/build_verify.sh new file mode 100755 index 0000000..9db9f17 --- /dev/null +++ b/build_verify.sh @@ -0,0 +1,11 @@ +#!/bin/bash +export PATH=$PATH:/usr/local/go/bin +export GOPROXY=https://goproxy.cn,direct +cd /root/goddhv +echo "=== go vet 全部包 ===" +go vet ./... 2>&1 | tail -30 +echo "=== go build 全部 ===" +go build ./... 2>&1 | tail -30 +echo "=== 完整构建二进制 ===" +go build -o /tmp/goddhv_main . 2>&1 | tail -20 +ls -la /tmp/goddhv_main 2>/dev/null && echo "BUILD_MAIN_OK" diff --git a/core/consts/consts.go b/core/consts/consts.go new file mode 100644 index 0000000..fbb38ed --- /dev/null +++ b/core/consts/consts.go @@ -0,0 +1,33 @@ +package consts + +import ( + "os" + "path/filepath" +) + +// 应用标识:{组织名}/{应用名},与部署一致 +const APP_NAME = "user_TGPJp0KLpNsa5TAo/ddhv" + +// GMSSH 平台根路径 +const GMSSH_PATH = "/.__gmssh" + +// 部署相关路径 +var ( + // APP_INSTALLED_PATH: 应用安装目录 + APP_INSTALLED_PATH = filepath.Join(GMSSH_PATH, "plugin", APP_NAME) + // APP_TMP_PATH: 临时/socket 目录 + APP_TMP_PATH = filepath.Join(APP_INSTALLED_PATH, "tmp") + // APP_BIN_PATH: 二进制目录 + APP_BIN_PATH = filepath.Join(APP_INSTALLED_PATH, "app", "bin") + // APP_DATA_PATH: 数据目录(快照/状态/审计) + APP_DATA_PATH = filepath.Join(APP_INSTALLED_PATH, "data") + // APP_SOCKET_FILE_PATH: unix socket 文件 + APP_SOCKET_FILE_PATH = filepath.Join(APP_TMP_PATH, "app.sock") +) + +// 确保目录存在 +func EnsureDirs() { + for _, d := range []string{APP_TMP_PATH, APP_DATA_PATH} { + os.MkdirAll(d, 0o755) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..354b531 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/user_TGPJp0KLpNsa5TAo/ddhv + +go 1.23.4 + +require github.com/DemonZack/simplejrpc-go v0.0.0 + +require ( + github.com/sourcegraph/jsonrpc2 v0.2.1 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.27.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect +) + +replace github.com/DemonZack/simplejrpc-go => /root/gmssh-src/gmssh-app-skeleton-go diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..eeac297 --- /dev/null +++ b/go.sum @@ -0,0 +1,20 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sourcegraph/jsonrpc2 v0.2.1 h1:2GtljixMQYUYCmIg7W9aF2dFmniq/mOr2T9tFRh6zSQ= +github.com/sourcegraph/jsonrpc2 v0.2.1/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gsysctl/sysctl.go b/gsysctl/sysctl.go new file mode 100644 index 0000000..a7549ba --- /dev/null +++ b/gsysctl/sysctl.go @@ -0,0 +1,347 @@ +// Package gsysctl 提供内核参数(sysctl)读写、持久化、快照备份/回滚、审计日志能力。 +// 对应 Python 版 backend/app/services/sysctl.py。 +package gsysctl + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "regexp" + "sort" + "strings" + "time" + + "github.com/user_TGPJp0KLpNsa5TAo/ddhv/core/consts" +) + +// ErrSysctl 表示 sysctl 操作错误 +type ErrSysctl struct{ Msg string } + +func (e *ErrSysctl) Error() string { return e.Msg } + +// NewErrSysctl 构造 sysctl 错误 +func NewErrSysctl(msg string) error { return &ErrSysctl{Msg: msg} } + +func newErr(format string, a ...any) error { + return &ErrSysctl{Msg: fmt.Sprintf(format, a...)} +} + +// Manager 内核参数管理器 +type Manager struct { + backupDir string // 快照备份目录 + logFile string // 审计日志文件 +} + +// 持久化配置文件(独立文件,不污染系统默认配置) +const persistConf = "/etc/sysctl.d/99-gm-tune.conf" + +// 需要同步的系统配置文件(避免 sysctl --system 重载时被旧值覆盖) +var systemConfFiles = []string{ + "/etc/sysctl.conf", + "/etc/sysctl.d/99-sysctl.conf", + "/etc/sysctl.d/99-bbr.conf", +} + +// 快照前缀 +const snapshotPrefix = "snapshot_" + +// NewManager 创建管理器,确保目录存在 +func NewManager() *Manager { + backup := filepath.Join(consts.APP_DATA_PATH, "backups") + logF := filepath.Join(consts.APP_DATA_PATH, "logs", "audit.log") + os.MkdirAll(backup, 0o755) + os.MkdirAll(filepath.Dir(logF), 0o755) + return &Manager{backupDir: backup, logFile: logF} +} + +// run 执行命令并返回 stdout +func run(cmd ...string) (string, error) { + c := exec.Command(cmd[0], cmd[1:]...) + out, err := c.Output() + if err != nil { + if ee, ok := err.(*exec.ExitError); ok { + return "", newErr("命令执行失败: %s => %s", strings.Join(cmd, " "), strings.TrimSpace(string(ee.Stderr))) + } + return "", newErr("命令执行失败: %s => %v", strings.Join(cmd, " "), err) + } + return string(out), nil +} + +// GetValue 读取单个内核参数当前值 +func GetValue(key string) (string, error) { + out, err := run("sysctl", "-n", key) + if err != nil { + return "", err + } + return strings.TrimSpace(out), nil +} + +// GetAll 读取全部内核参数 +func GetAll() (map[string]string, error) { + out, err := run("sysctl", "-a") + if err != nil { + return nil, err + } + result := map[string]string{} + for _, line := range strings.Split(out, "\n") { + line = strings.TrimSpace(line) + if line == "" || !strings.Contains(line, "=") { + continue + } + parts := strings.SplitN(line, "=", 2) + result[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) + } + return result, nil +} + +// GetValues 批量读取指定参数 +func GetValues(keys []string) (map[string]string, error) { + result := map[string]string{} + for _, k := range keys { + v, err := GetValue(k) + if err != nil { + v = "" + } + result[k] = v + } + return result, nil +} + +// SetValue 设置单个内核参数并持久化 +func SetValue(key, value string, persist bool) error { + if _, err := run("sysctl", "-w", fmt.Sprintf("%s=%s", key, value)); err != nil { + return err + } + if persist { + return persistValue(key, value) + } + return nil +} + +// SetValues 批量设置,返回每个参数的执行结果 +func SetValues(pairs map[string]string, persist bool) map[string]any { + results := map[string]any{} + for k, v := range pairs { + if err := SetValue(k, v, persist); err != nil { + results[k] = map[string]any{"ok": false, "error": err.Error()} + } else { + results[k] = map[string]any{"ok": true, "value": v} + } + } + return results +} + +// syncSystemConf 同步同名参数到系统配置文件(只更新已存在的同名非注释行) +func syncSystemConf(key, value string) { + re := regexp.MustCompile(fmt.Sprintf(`^\s*%s\s*=`, regexp.QuoteMeta(key))) + for _, path := range systemConfFiles { + data, err := os.ReadFile(path) + if err != nil { + continue + } + lines := strings.Split(string(data), "\n") + changed := false + for i, line := range lines { + trimmed := strings.TrimSpace(line) + if re.MatchString(line) && !strings.HasPrefix(trimmed, "#") { + lines[i] = fmt.Sprintf("%s = %s", key, value) + changed = true + } + } + if changed { + os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0o644) + } + } +} + +// persistValue 将参数写入持久化配置并同步系统配置 +func persistValue(key, value string) error { + lines := []string{} + if data, err := os.ReadFile(persistConf); err == nil { + lines = strings.Split(string(data), "\n") + } + re := regexp.MustCompile(fmt.Sprintf(`^\s*%s\s*=`, regexp.QuoteMeta(key))) + newLines := []string{} + for _, line := range lines { + if !re.MatchString(line) { + newLines = append(newLines, line) + } + } + newLines = append(newLines, fmt.Sprintf("%s = %s", key, value)) + os.MkdirAll(filepath.Dir(persistConf), 0o755) + if err := os.WriteFile(persistConf, []byte(strings.Join(newLines, "\n")+"\n"), 0o644); err != nil { + return newErr("写入持久化配置失败: %v", err) + } + syncSystemConf(key, value) + return nil +} + +// RemovePersist 从持久化配置中移除参数 +func RemovePersist(key string) error { + data, err := os.ReadFile(persistConf) + if err != nil { + return nil + } + re := regexp.MustCompile(fmt.Sprintf(`^\s*%s\s*=`, regexp.QuoteMeta(key))) + lines := []string{} + for _, line := range strings.Split(string(data), "\n") { + if !re.MatchString(line) { + lines = append(lines, line) + } + } + return os.WriteFile(persistConf, []byte(strings.Join(lines, "\n")), 0o644) +} + +// SnapshotMeta 快照元信息 +type SnapshotMeta struct { + Name string `json:"name"` + CreatedAt string `json:"created_at"` + Reason string `json:"reason"` + Kind string `json:"kind"` + ParamCount int `json:"param_count"` +} + +// CreateSnapshot 创建当前全部内核参数快照 +func (m *Manager) CreateSnapshot(reason, kind string) (SnapshotMeta, error) { + ts := time.Now().Format("20060102_150405") + name := snapshotPrefix + ts + dir := filepath.Join(m.backupDir, name) + if err := os.MkdirAll(dir, 0o755); err != nil { + return SnapshotMeta{}, err + } + // 全部参数快照 + params, err := GetAll() + if err != nil { + return SnapshotMeta{}, err + } + keys := make([]string, 0, len(params)) + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + var sb strings.Builder + for _, k := range keys { + sb.WriteString(fmt.Sprintf("%s = %s\n", k, params[k])) + } + os.WriteFile(filepath.Join(dir, "sysctl_all.conf"), []byte(sb.String()), 0o644) + // 持久化配置副本 + if data, err := os.ReadFile(persistConf); err == nil { + os.WriteFile(filepath.Join(dir, "persist.conf.bak"), data, 0o644) + } + // 元信息 + meta := SnapshotMeta{ + Name: name, CreatedAt: time.Now().Format("2006-01-02 15:04:05"), + Reason: reason, Kind: kind, ParamCount: len(params), + } + mb, _ := json.MarshalIndent(meta, "", " ") + os.WriteFile(filepath.Join(dir, "meta.json"), mb, 0o644) + return meta, nil +} + +// ListSnapshots 列出所有快照(按时间倒序) +func (m *Manager) ListSnapshots() []SnapshotMeta { + snapshots := []SnapshotMeta{} + entries, err := os.ReadDir(m.backupDir) + if err != nil { + return snapshots + } + for _, e := range entries { + if !e.IsDir() || !strings.HasPrefix(e.Name(), snapshotPrefix) { + continue + } + meta := SnapshotMeta{Name: e.Name(), Kind: "manual"} + if data, err := os.ReadFile(filepath.Join(m.backupDir, e.Name(), "meta.json")); err == nil { + json.Unmarshal(data, &meta) + } + snapshots = append(snapshots, meta) + } + sort.Slice(snapshots, func(i, j int) bool { return snapshots[i].Name > snapshots[j].Name }) + return snapshots +} + +// RestoreSnapshot 回滚到指定快照 +func (m *Manager) RestoreSnapshot(name string) (map[string]any, error) { + dir := filepath.Join(m.backupDir, name) + confPath := filepath.Join(dir, "sysctl_all.conf") + if _, err := os.Stat(confPath); err != nil { + return nil, newErr("快照不存在: %s", name) + } + data, err := os.ReadFile(confPath) + if err != nil { + return nil, err + } + restored := map[string]string{} + failed := map[string]string{} + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") || !strings.Contains(line, "=") { + continue + } + parts := strings.SplitN(line, "=", 2) + k, v := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]) + if _, err := run("sysctl", "-w", fmt.Sprintf("%s=%s", k, v)); err != nil { + failed[k] = err.Error() + } else { + restored[k] = v + } + } + // 恢复持久化配置 + persistBackup := filepath.Join(dir, "persist.conf.bak") + if _, err := os.Stat(persistBackup); err == nil { + if pb, err := os.ReadFile(persistBackup); err == nil { + os.WriteFile(persistConf, pb, 0o644) + for k, v := range restored { + syncSystemConf(k, v) + } + } + } + return map[string]any{ + "snapshot": name, "restored_count": len(restored), "failed_count": len(failed), "failed": failed, + }, nil +} + +// LogAction 记录操作审计日志 +func (m *Manager) LogAction(action string, detail map[string]any) { + entry := map[string]any{"time": time.Now().Format("2006-01-02 15:04:05"), "action": action} + for k, v := range detail { + entry[k] = v + } + os.MkdirAll(filepath.Dir(m.logFile), 0o755) + jb, _ := json.Marshal(entry) + f, _ := os.OpenFile(m.logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if f != nil { + f.WriteString(string(jb) + "\n") + f.Close() + } +} + +// AuditLogs 读取审计日志(最近 n 条,倒序) +func (m *Manager) AuditLogs(limit int) []map[string]any { + lines := []string{} + if data, err := os.ReadFile(m.logFile); err == nil { + lines = strings.Split(strings.TrimSpace(string(data)), "\n") + } + // 倒序(最新在前) + result := []map[string]any{} + for i := len(lines) - 1; i >= 0 && len(result) < limit; i-- { + line := strings.TrimSpace(lines[i]) + if line == "" { + continue + } + var entry map[string]any + if json.Unmarshal([]byte(line), &entry) == nil { + result = append(result, entry) + } + } + return result +} + +// BoolToInt bool 转 int(0/1),供前端逻辑使用 +func BoolToInt(b bool) int { + if b { + return 1 + } + return 0 +} diff --git a/gtuner/tuner.go b/gtuner/tuner.go new file mode 100644 index 0000000..f376524 --- /dev/null +++ b/gtuner/tuner.go @@ -0,0 +1,354 @@ +// Package gtuner 提供系统调优核心服务(检测/一键优化/场景预设/TCP加速/Swappiness)。 +// 对应 Python 版 backend/app/services/tuner.py。 +package gtuner + +import ( + "encoding/json" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + "time" + + "github.com/user_TGPJp0KLpNsa5TAo/ddhv/core/consts" + "github.com/user_TGPJp0KLpNsa5TAo/ddhv/gsysctl" +) + +// TunerService 系统调优服务 +type TunerService struct { + S *gsysctl.Manager +} + +// NewTuner 构造服务 +func NewTuner() TunerService { + return TunerService{S: gsysctl.NewManager()} +} + +// ========== 状态持久化 ========== + +// statePath state.json 路径 +func statePath() string { + return filepath.Join(consts.APP_DATA_PATH, "state.json") +} + +// SaveState 保存应用状态(当前预设等) +func (t TunerService) SaveState(state map[string]any) { + path := statePath() + os.MkdirAll(filepath.Dir(path), 0o755) + if data, err := json.MarshalIndent(state, "", " "); err == nil { + os.WriteFile(path, data, 0o644) + } +} + +// LoadState 读取应用状态 +func (t TunerService) LoadState() map[string]any { + path := statePath() + if data, err := os.ReadFile(path); err == nil { + var m map[string]any + if json.Unmarshal(data, &m) == nil { + return m + } + } + return map[string]any{} +} + +// GetCurrentPreset 获取当前生效的预设方案 +func (t TunerService) GetCurrentPreset() (map[string]any, error) { + state := t.LoadState() + key := "" + if v, ok := state["current_preset"].(string); ok { + key = v + } + presets := t.GetPresets() + if key != "" { + if p, ok := presets[key].(map[string]any); ok { + name, _ := p["name"].(string) + appliedAt := "" + if v, ok := state["preset_applied_at"].(string); ok { + appliedAt = v + } + return map[string]any{"current": key, "name": name, "applied_at": appliedAt}, nil + } + } + return map[string]any{"current": nil, "name": nil, "applied_at": ""}, nil +} + +// ========== 系统检测 ========== + +// CheckRoot 检查 root 权限 +func (t TunerService) CheckRoot() (map[string]any, error) { + isRoot := os.Geteuid() == 0 + return map[string]any{ + "is_root": isRoot, "proc_writable": isRoot, "ok": isRoot, + }, nil +} + +// DetectHardware 检测硬件环境 +func (t TunerService) DetectHardware() (map[string]any, error) { + info := map[string]any{ + "os": runtime.GOOS + " linux", "arch": runtime.GOARCH, + "cpu_cores": runtime.NumCPU(), "memory_gb": 0.0, + "disk_type": "unknown", "is_container": false, "virtualization": "none", + } + // 内存 + if data, err := os.ReadFile("/proc/meminfo"); err == nil { + for _, line := range strings.Split(string(data), "\n") { + if strings.HasPrefix(line, "MemTotal:") { + fields := strings.Fields(line) + if len(fields) >= 2 { + if kb, err := strconv.ParseInt(fields[1], 10, 64); err == nil { + gb := float64(kb) / 1024 / 1024 + info["memory_gb"] = float64(int(gb*10+0.5)) / 10 + } + } + break + } + } + } + // 磁盘类型 + for _, dev := range []string{"sda", "vda"} { + if data, err := os.ReadFile("/sys/block/" + dev + "/queue/rotational"); err == nil { + if strings.TrimSpace(string(data)) == "1" { + info["disk_type"] = "hdd" + } else { + info["disk_type"] = "ssd" + } + break + } + } + // 容器检测 + if data, err := os.ReadFile("/proc/1/cgroup"); err == nil { + c := string(data) + if strings.Contains(c, "docker") || strings.Contains(c, "kubepods") || strings.Contains(c, "lxc") { + info["is_container"] = true + info["virtualization"] = "container" + } + } + if _, err := os.Stat("/.dockerenv"); err == nil { + info["is_container"] = true + info["virtualization"] = "docker" + } + return info, nil +} + +// DetectSwap 检测 swap +func (t TunerService) DetectSwap() (map[string]any, error) { + enabled := false + if data, err := os.ReadFile("/proc/swaps"); err == nil { + for _, line := range strings.Split(string(data), "\n")[1:] { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + if !strings.HasPrefix(line, "Filename") && len(strings.Fields(line)) > 0 { + enabled = true + break + } + } + } + return map[string]any{"swap_enabled": enabled}, nil +} + +// DetectTCP 检测 TCP 配置 +func (t TunerService) DetectTCP() (map[string]any, error) { + keys := []string{ + "net.ipv4.tcp_congestion_control", "net.core.default_qdisc", + "net.ipv4.tcp_fastopen", "net.ipv4.tcp_syncookies", + "net.ipv4.tcp_rmem", "net.ipv4.tcp_wmem", + "net.core.rmem_max", "net.core.wmem_max", + "net.core.somaxconn", "net.ipv4.tcp_max_syn_backlog", + "net.ipv4.tcp_tw_reuse", "net.ipv4.tcp_slow_start_after_idle", + "net.ipv4.tcp_mtu_probing", "net.ipv4.ip_local_port_range", + } + values, _ := gsysctl.GetValues(keys) + // BBR 支持检测 + result := map[string]any{} + for k, v := range values { + result[k] = v + } + bbrAvailable := false + if out, err := gsysctl.GetValue("net.ipv4.tcp_available_congestion_control"); err == nil { + if strings.Contains(strings.ToLower(out), "bbr") { + bbrAvailable = true + } + } + result["bbr_available"] = bbrAvailable + return result, nil +} + +// Detect 综合检测 +func (t TunerService) Detect() (map[string]any, error) { + hw, _ := t.DetectHardware() + tcp, _ := t.DetectTCP() + swap, _ := t.DetectSwap() + preset, _ := t.GetCurrentPreset() + root, _ := t.CheckRoot() + init, _ := t.InitStatus() + return map[string]any{ + "hardware": hw, "tcp": tcp, "swap": swap, "preset": preset, + "root": root, "init": init, + }, nil +} + +// ========== 场景预设 ========== + +// GetPresets 返回可用的调优场景预设 +func (t TunerService) GetPresets() map[string]any { + return map[string]any{ + "balanced": map[string]any{ + "name": "均衡模式", + "desc": "适合大多数服务器,平衡性能与稳定性", + "params": map[string]string{ + "vm.swappiness": "10", + "net.core.default_qdisc": "fq", + "net.ipv4.tcp_congestion_control": "bbr", + "net.ipv4.tcp_fastopen": "3", + "net.ipv4.tcp_slow_start_after_idle": "0", + "net.ipv4.tcp_mtu_probing": "1", + "net.core.somaxconn": "1024", + "net.ipv4.tcp_max_syn_backlog": "4096", + }, + }, + "performance": map[string]any{ + "name": "高性能模式", + "desc": "追求极限吞吐,适合高并发业务(内存≥4G推荐)", + "params": map[string]string{ + "vm.swappiness": "5", + "vm.vfs_cache_pressure": "50", + "net.core.default_qdisc": "fq", + "net.ipv4.tcp_congestion_control": "bbr", + "net.ipv4.tcp_fastopen": "3", + "net.ipv4.tcp_slow_start_after_idle": "0", + "net.ipv4.tcp_mtu_probing": "1", + "net.core.somaxconn": "4096", + "net.ipv4.tcp_max_syn_backlog": "8192", + "net.core.rmem_max": "16777216", + "net.core.wmem_max": "16777216", + "net.ipv4.tcp_rmem": "4096 87380 16777216", + "net.ipv4.tcp_wmem": "4096 16384 16777216", + "net.ipv4.tcp_tw_reuse": "1", + }, + }, + "conservative": map[string]any{ + "name": "保守模式", + "desc": "最小改动,仅调整最安全的参数", + "params": map[string]string{ + "vm.swappiness": "10", + "net.ipv4.tcp_slow_start_after_idle": "0", + "net.ipv4.tcp_mtu_probing": "1", + }, + }, + } +} + +// ApplyPreset 应用场景预设 +func (t TunerService) ApplyPreset(key string) (map[string]any, error) { + presets := t.GetPresets() + p, ok := presets[key].(map[string]any) + if !ok { + return nil, gsysctl.NewErrSysctl("未知的预设场景: " + key) + } + name, _ := p["name"].(string) + params, _ := p["params"].(map[string]string) + results := gsysctl.SetValues(params, true) + okCount, failCount := 0, 0 + for _, r := range results { + if m, ok := r.(map[string]any); ok { + if v, ok := m["ok"].(bool); ok && v { + okCount++ + } else { + failCount++ + } + } + } + t.SaveState(map[string]any{ + "current_preset": key, "preset_applied_at": time.Now().Format("2006-01-02 15:04:05"), + }) + t.S.LogAction("apply_preset", map[string]any{ + "preset": key, "preset_name": name, "ok": okCount, "fail": failCount, + }) + return map[string]any{ + "preset": key, "preset_name": name, "results": results, + "ok_count": okCount, "fail_count": failCount, + }, nil +} + +// InitStatus 检查是否已初始化 +func (t TunerService) InitStatus() (map[string]any, error) { + snaps := t.S.ListSnapshots() + for _, s := range snaps { + if s.Kind == "init" { + return map[string]any{"initialized": true, "snapshot": s}, nil + } + } + return map[string]any{"initialized": false, "snapshot": nil}, nil +} + +// InitSnapshot 首次使用初始化快照 +func (t TunerService) InitSnapshot() (map[string]any, error) { + snaps := t.S.ListSnapshots() + for _, s := range snaps { + if s.Kind == "init" { + return map[string]any{"already": true, "snapshot": s}, nil + } + } + meta, err := t.S.CreateSnapshot("首次使用初始化", "init") + if err != nil { + return nil, err + } + t.S.LogAction("init", map[string]any{"snapshot": meta.Name, "param_count": meta.ParamCount}) + return map[string]any{"already": false, "snapshot": meta}, nil +} + +// ========== 单独调优项 ========== + +// SetSwappiness 设置 swappiness +func (t TunerService) SetSwappiness(value int, persist bool) (map[string]any, error) { + if value < 0 || value > 100 { + return nil, gsysctl.NewErrSysctl("swappiness 必须在 0-100 之间") + } + err := gsysctl.SetValue("vm.swappiness", strconv.Itoa(value), persist) + if err != nil { + return nil, err + } + current, _ := gsysctl.GetValue("vm.swappiness") + t.S.LogAction("set_swappiness", map[string]any{"value": value, "persist": persist}) + return map[string]any{"ok": true, "value": value, "current": current}, nil +} + +// EnableBBR 一键开启 BBR +func (t TunerService) EnableBBR() (map[string]any, error) { + results := gsysctl.SetValues(map[string]string{ + "net.core.default_qdisc": "fq", + "net.ipv4.tcp_congestion_control": "bbr", + }, true) + current, _ := gsysctl.GetValue("net.ipv4.tcp_congestion_control") + t.S.LogAction("enable_bbr", map[string]any{"current": current}) + return map[string]any{"results": results, "current": current}, nil +} + +// DisableBBR 关闭 BBR +func (t TunerService) DisableBBR() (map[string]any, error) { + results := gsysctl.SetValues(map[string]string{ + "net.ipv4.tcp_congestion_control": "cubic", + }, true) + current, _ := gsysctl.GetValue("net.ipv4.tcp_congestion_control") + t.S.LogAction("disable_bbr", map[string]any{"current": current}) + return map[string]any{"results": results, "current": current}, nil +} + +// ========== 危险参数解锁 ========== + +// GetDangerousParams 危险参数清单 +func (t TunerService) GetDangerousParams() map[string]any { + return map[string]any{ + "vm.swappiness": map[string]any{"risk": "danger", "desc": "内存交换策略,过低可能导致内存压力,过高会导致频繁交换"}, + "vm.vfs_cache_pressure": map[string]any{"risk": "danger", "desc": "内核缓存回收倾向,过高可能导致缓存频繁失效"}, + "net.core.rmem_max": map[string]any{"risk": "danger", "desc": "最大接收缓冲区,过大可能浪费内存"}, + "net.core.wmem_max": map[string]any{"risk": "danger", "desc": "最大发送缓冲区,过大可能浪费内存"}, + "net.ipv4.tcp_rmem": map[string]any{"risk": "danger", "desc": "TCP 接收缓冲动态范围,不当设置影响吞吐"}, + "net.ipv4.tcp_wmem": map[string]any{"risk": "danger", "desc": "TCP 发送缓冲动态范围,不当设置影响吞吐"}, + "net.ipv4.tcp_tw_reuse": map[string]any{"risk": "danger", "desc": "TIME_WAIT 复用,NAT 环境下可能引发问题"}, + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..346db68 --- /dev/null +++ b/main.go @@ -0,0 +1,109 @@ +// 主入口: GMSSH「系统调优」Go 版后端服务。 +// 基于官方 simplejrpc-go SDK, JSON-RPC 2.0 over Unix Socket。 +package main + +import ( + "fmt" + "log" + "os" + "path/filepath" + + rpc "github.com/DemonZack/simplejrpc-go" + "github.com/DemonZack/simplejrpc-go/core" + "github.com/DemonZack/simplejrpc-go/core/config" + "github.com/DemonZack/simplejrpc-go/net/gsock" + "github.com/DemonZack/simplejrpc-go/os/gpath" + "github.com/user_TGPJp0KLpNsa5TAo/ddhv/core/consts" + "github.com/user_TGPJp0KLpNsa5TAo/ddhv/server" +) + +// env 运行环境(test/prod),可通过环境变量 GMSSH_ENV 覆盖 +func startupEnv() string { + if e := os.Getenv("GMSSH_ENV"); e == "prod" || e == "test" { + return e + } + // 平台通常以 prod 运行正式版 + return "prod" +} + +func main() { + // 确保数据目录存在 + consts.EnsureDirs() + + var socketPath string + + // 1. 优先支持环境变量显式指定(本地测试用) + if env := os.Getenv("APP_SOCKET"); env != "" { + socketPath = env + } else { + // 2. 尝试按官方 SDK 规范从 config.json 读取 socket + func() { + defer func() { + if r := recover(); r != nil { + log.Printf("[*] config.json 读取失败(忽略): %v", r) + } + }() + env := startupEnv() + // 设置配置基础路径: 优先 main 所在目录(与 config.json 同目录), 兜底 consts + binDir, err := filepath.Abs(filepath.Dir(os.Args[0])) + if err == nil { + gpath.GmCfgPath = binDir + } else { + gpath.GmCfgPath = consts.APP_BIN_PATH + } + if os.Getenv("CONFIG_PATH") != "" { + gpath.GmCfgPath = "" + } + core.InitContainer(config.WithConfigEnvFormatterOptionFunc(env)) + val, err := core.Container.CfgFmt().GetValue("jsonrpc.sockets").String() + if err == nil && val != "" { + socketPath = val + // 若为相对路径, 从 app/bin 定位到部署根 tmp (bin -> ../.. -> /tmp) + if !filepath.IsAbs(socketPath) { + socketPath = filepath.Join(binDir, "..", "..", "tmp", filepath.Base(socketPath)) + } + } + }() + + // 3. 兜底用 consts 绝对路径 + if socketPath == "" { + socketPath = consts.APP_SOCKET_FILE_PATH + } + } + + fmt.Printf("Starting GMSSH system tuner backend, socket=%s\n", socketPath) + if err := ds0(socketPath); err != nil { + log.Fatalf("server error: %v", err) + } +} + +// ds0 创建并启动 RPC 服务 +func ds0(socketPath string) error { + ds := rpc.NewDefaultServer( + gsock.WithJsonRpcSimpleServiceHandler(gsock.NewJsonRpcSimpleServiceHandler()), + gsock.WithJsonRpcSimpleServiceMiddlewares(), + ) + + h := server.NewHandler() + ds.RegisterHandle("hello", h.Hello) + ds.RegisterHandle("ping", h.Ping) + ds.RegisterHandle("tuner_check_root", h.TunerCheckRoot) + ds.RegisterHandle("tuner_detect", h.TunerDetect) + ds.RegisterHandle("tuner_current_preset", h.TunerCurrentPreset) + ds.RegisterHandle("tuner_snapshots", h.TunerSnapshots) + ds.RegisterHandle("tuner_init_status", h.TunerInitStatus) + ds.RegisterHandle("tuner_init", h.TunerInit) + ds.RegisterHandle("tuner_create_snapshot", h.TunerCreateSnapshot) + ds.RegisterHandle("tuner_restore_snapshot", h.TunerRestoreSnapshot) + ds.RegisterHandle("tuner_presets", h.TunerPresets) + ds.RegisterHandle("tuner_apply_preset", h.TunerApplyPreset) + ds.RegisterHandle("tuner_set_swappiness", h.TunerSetSwappiness) + ds.RegisterHandle("tuner_enable_bbr", h.TunerEnableBBR) + ds.RegisterHandle("tuner_disable_bbr", h.TunerDisableBBR) + ds.RegisterHandle("tuner_get_param", h.TunerGetParam) + ds.RegisterHandle("tuner_set_param", h.TunerSetParam) + ds.RegisterHandle("tuner_dangerous_params", h.TunerDangerousParams) + ds.RegisterHandle("tuner_audit_logs", h.TunerAuditLogs) + + return ds.StartServer(socketPath) +} diff --git a/run_and_test.sh b/run_and_test.sh new file mode 100755 index 0000000..fe69649 --- /dev/null +++ b/run_and_test.sh @@ -0,0 +1,16 @@ +#!/bin/bash +export LANG=C +export PATH=$PATH:/usr/local/go/bin +pkill -f goddhv_main 2>/dev/null +rm -f /tmp/goddhv_test.sock +APP_SOCKET=/tmp/goddhv_test.sock nohup /tmp/goddhv_main > /tmp/goddhv_main.log 2>&1 & +PID=$! +sleep 1.5 +echo "server pid=$PID" +ls -la /tmp/goddhv_test.sock 2>/dev/null && echo "SOCKET_READY" +echo "=== log ===" +cat /tmp/goddhv_main.log +echo "=== client test ===" +python3 /tmp/test_client.py +pkill -f goddhv_main 2>/dev/null +echo "done" diff --git a/run_srv.sh b/run_srv.sh new file mode 100755 index 0000000..423ed7f --- /dev/null +++ b/run_srv.sh @@ -0,0 +1,11 @@ +#!/bin/bash +export LANG=C +# 测试用 socket 路径(不碰真实部署) +export APP_SOCKET=/tmp/goddhv_test.sock +pkill -f goddhv_main 2>/dev/null +rm -f /tmp/goddhv_test.sock +nohup /tmp/goddhv_main > /tmp/goddhv_main.log 2>&1 & +sleep 1 +echo "started, pid=$!" +ls -la /tmp/goddhv_test.sock 2>/dev/null +cat /tmp/goddhv_main.log 2>/dev/null | head -5 diff --git a/server/handlers.go b/server/handlers.go new file mode 100644 index 0000000..0776173 --- /dev/null +++ b/server/handlers.go @@ -0,0 +1,257 @@ +// Package server 提供 GMSSH JSON-RPC 接口层(19 个接口)。 +// 对应 Python 版 backend/app/server.py。 +package server + +import ( + "encoding/json" + "strconv" + + "github.com/DemonZack/simplejrpc-go/net/gsock" + "github.com/user_TGPJp0KLpNsa5TAo/ddhv/gsysctl" + "github.com/user_TGPJp0KLpNsa5TAo/ddhv/gtuner" +) + +// Handler 聚合所有接口 handler +type Handler struct { + Sysctl *gsysctl.Manager + Tuner gtuner.TunerService +} + +// NewHandler 构造 handler +func NewHandler() *Handler { + return &Handler{Sysctl: gsysctl.NewManager(), Tuner: gtuner.NewTuner()} +} + +// params 提取请求参数为 map +func params(req *gsock.Request) map[string]any { + if req.RawRequest() == nil || req.RawRequest().Params == nil { + return map[string]any{} + } + raw, _ := json.Marshal(req.RawRequest().Params) + var m map[string]any + _ = json.Unmarshal(raw, &m) + return m +} + +// str 取字符串参数 +func str(p map[string]any, key string) string { + if v, ok := p[key]; ok { + switch t := v.(type) { + case string: + return t + case float64: + return strconv.FormatFloat(t, 'f', -1, 64) + default: + return "" + } + } + return "" +} + +// boolP 取布尔参数 +func boolP(p map[string]any, key string) bool { + if v, ok := p[key]; ok { + if b, ok := v.(bool); ok { + return b + } + if f, ok := v.(float64); ok { + return f != 0 + } + if s, ok := v.(string); ok { + return s == "true" || s == "1" + } + } + return false +} + +// trueP 取布尔参数(默认 true) +func trueP(p map[string]any, key string) bool { + if v, ok := p[key]; ok { + if b, ok := v.(bool); ok { + return b + } + if f, ok := v.(float64); ok { + return f != 0 + } + if s, ok := v.(string); ok { + return s == "true" || s == "1" + } + return true + } + return true +} + +// intP 取整数参数 +func intP(p map[string]any, key string) int { + if v, ok := p[key]; ok { + if f, ok := v.(float64); ok { + return int(f) + } + if s, ok := v.(string); ok { + if i, err := strconv.Atoi(s); err == nil { + return i + } + } + } + return -1 +} + +// ========== 基础 ========== + +// Hello 测试接口 +func (h *Handler) Hello(req *gsock.Request) (any, error) { + return map[string]any{"message": "hello"}, nil +} + +// Ping 健康检查(平台要求返回 pong) +func (h *Handler) Ping(req *gsock.Request) (any, error) { + return "pong", nil +} + +// ========== 系统调优接口 ========== + +func (h *Handler) TunerCheckRoot(req *gsock.Request) (any, error) { + return h.Tuner.CheckRoot() +} + +func (h *Handler) TunerDetect(req *gsock.Request) (any, error) { + return h.Tuner.Detect() +} + +func (h *Handler) TunerCurrentPreset(req *gsock.Request) (any, error) { + return h.Tuner.GetCurrentPreset() +} + +func (h *Handler) TunerSnapshots(req *gsock.Request) (any, error) { + snaps := h.Sysctl.ListSnapshots() + list := make([]map[string]any, 0, len(snaps)) + for _, s := range snaps { + list = append(list, map[string]any{ + "name": s.Name, "created_at": s.CreatedAt, "reason": s.Reason, + "kind": s.Kind, "param_count": s.ParamCount, + }) + } + return list, nil +} + +func (h *Handler) TunerInitStatus(req *gsock.Request) (any, error) { + return h.Tuner.InitStatus() +} + +func (h *Handler) TunerInit(req *gsock.Request) (any, error) { + return h.Tuner.InitSnapshot() +} + +func (h *Handler) TunerCreateSnapshot(req *gsock.Request) (any, error) { + p := params(req) + reason := str(p, "reason") + if reason == "" { + reason = "手动备份" + } + return h.Sysctl.CreateSnapshot(reason, "manual") +} + +func (h *Handler) TunerRestoreSnapshot(req *gsock.Request) (any, error) { + p := params(req) + name := str(p, "snapshot_name") + if name == "" { + return nil, errParam("缺少 snapshot_name 参数") + } + return h.Sysctl.RestoreSnapshot(name) +} + +func (h *Handler) TunerPresets(req *gsock.Request) (any, error) { + return h.Tuner.GetPresets(), nil +} + +func (h *Handler) TunerApplyPreset(req *gsock.Request) (any, error) { + p := params(req) + key := str(p, "preset_key") + if key == "" { + return nil, errParam("缺少 preset_key 参数") + } + return h.Tuner.ApplyPreset(key) +} + +func (h *Handler) TunerSetSwappiness(req *gsock.Request) (any, error) { + p := params(req) + value := intP(p, "value") + if value < 0 { + return nil, errParam("缺少 value 参数") + } + persist := trueP(p, "persist") + return h.Tuner.SetSwappiness(value, persist) +} + +func (h *Handler) TunerEnableBBR(req *gsock.Request) (any, error) { + return h.Tuner.EnableBBR() +} + +func (h *Handler) TunerDisableBBR(req *gsock.Request) (any, error) { + return h.Tuner.DisableBBR() +} + +func (h *Handler) TunerGetParam(req *gsock.Request) (any, error) { + p := params(req) + key := str(p, "key") + if key == "" { + return nil, errParam("缺少 key 参数") + } + value, err := gsysctl.GetValue(key) + if err != nil { + value = "" + } + return map[string]any{"key": key, "value": value}, nil +} + +func (h *Handler) TunerSetParam(req *gsock.Request) (any, error) { + p := params(req) + key := str(p, "key") + value := str(p, "value") + if key == "" || value == "" { + return nil, errParam("缺少 key/value 参数") + } + unlocked := boolP(p, "unlocked") + // 危险参数必须解锁 + if _, danger := h.Tuner.GetDangerousParams()[key]; danger && !unlocked { + return map[string]any{"need_unlock": true}, errNeedUnlock() + } + if err := gsysctl.SetValue(key, value, true); err != nil { + return nil, err + } + current, _ := gsysctl.GetValue(key) + h.Sysctl.LogAction("set_param", map[string]any{"key": key, "value": value, "unlocked": unlocked}) + return map[string]any{"key": key, "value": value, "ok": true, "current": current}, nil +} + +func (h *Handler) TunerDangerousParams(req *gsock.Request) (any, error) { + return h.Tuner.GetDangerousParams(), nil +} + +func (h *Handler) TunerAuditLogs(req *gsock.Request) (any, error) { + p := params(req) + limit := intP(p, "limit") + if limit <= 0 || limit > 500 { + limit = 100 + } + return h.Sysctl.AuditLogs(limit), nil +} + +// ========== 辅助错误 ========== + +// errParam 参数错误 +func errParam(msg string) error { + return &handleError{code: 400, msg: msg} +} + +// errNeedUnlock 需要解锁危险参数 +func errNeedUnlock() error { + return &handleError{code: 403, msg: "危险参数需要先解锁才能修改"} +} + +type handleError struct { + code int + msg string +} + +func (e *handleError) Error() string { return e.msg }