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,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
|
||||
}
|
||||
Reference in New Issue
Block a user