Files
ddhv/gsysctl/sysctl.go
T
shishi 2f79b22936 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)
2026-08-09 10:12:57 +08:00

348 lines
9.8 KiB
Go

// 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
}