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)
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
dist/
|
||||||
|
mock_deploy*/
|
||||||
|
*.sock
|
||||||
|
*.log
|
||||||
Executable
+80
@@ -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/||"
|
||||||
Executable
+11
@@ -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"
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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=
|
||||||
@@ -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
|
||||||
|
}
|
||||||
+354
@@ -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 环境下可能引发问题"},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
Executable
+16
@@ -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"
|
||||||
Executable
+11
@@ -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
|
||||||
@@ -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 }
|
||||||
Reference in New Issue
Block a user