This commit is contained in:
12600k-rog-d4
2026-08-24 00:35:30 +08:00
parent 32b308bc08
commit b673ed62f9
24 changed files with 483 additions and 393 deletions

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"net"
"os"
"sort"
"strconv"
"strings"
"time"
@@ -23,40 +22,48 @@ var Results = make(map[string]*Report)
// 执行器
// ============================
// Runner 压力测试执行器
// Runner 压力测试执行器(统一在 Docker 容器中执行,工具预装在镜像里)
type Runner struct {
cfg Config
types []TestType
client *ssh.Client
tools ToolSet
metrics []Metric
installFailed []string // 自动安装仍失败的工具
cfg Config
client *ssh.Client
metrics []Metric
sudoPwd string // 非 root 用户(如 fnOS admin执行 docker 命令时的 sudo 密码
dockerOK bool // 目标机 Docker 是否可用
}
// NewRunner 创建执行器(检查/在线安装 → 构建指标 → 生成脚本 → 执行
func NewRunner(cfg Config, types []TestType, client *ssh.Client, autoInstall bool, sudoPwd string) *Runner {
// 1. 检测工具,缺失时按发行版在线安装(可配置国内镜像源加速)
tools, failed := EnsureTools(client, requiredTools, autoInstall, sudoPwd, cfg.UseMirror)
metrics := BuildMetrics(types, tools)
return &Runner{cfg: cfg, types: types, client: client, tools: tools, metrics: metrics, installFailed: failed}
// NewRunner 创建执行器(检测 Docker → 确保镜像 → 启动容器执行测试
func NewRunner(cfg Config, types []TestType, client *ssh.Client, sudoPwd string) *Runner {
dockerInfo := IsDockerAvailable(client, sudoPwd)
if dockerInfo.Available {
fmt.Printf(" [Docker] 检测到 Docker 版本 %s压测将在容器中执行\n", dockerInfo.Version)
} else {
fmt.Printf(" [Docker] 目标机未检测到 Docker无法执行压测飞牛默认已安装请检查 Docker 服务状态)\n")
}
return &Runner{
cfg: cfg,
client: client,
metrics: BuildMetrics(types),
sudoPwd: sudoPwd,
dockerOK: dockerInfo.Available,
}
}
// Run 执行压力测试(生成脚本 → 远程执行 → 解析结果
// Run 执行压力测试(启动容器 → 解析结果 → 生成报告
func (r *Runner) Run(ip string) *Report {
report := &Report{IP: ip, StartTime: time.Now()}
fmt.Printf("\n════════ [%s] 压力测试开始 ════════\n", ip)
// 显示可用工具
fmt.Printf(" 工具: ")
for name, info := range r.tools.Tools {
fmt.Printf("%s(%s) ", name, info.Version)
}
fmt.Println()
if len(r.installFailed) > 0 {
fmt.Printf(" [安装] 以下工具自动安装失败,相关测试将跳过: %v\n", r.installFailed)
// Docker 是唯一执行模式,不可用时直接判失败
if !r.dockerOK {
report.AddResult(Result{Type: "docker", Status: StatusFail, Error: "目标机 Docker 不可用"})
report.EndTime = time.Now()
report.Duration = report.EndTime.Sub(report.StartTime)
return report
}
// 显示将执行的指标及被跳过的原因
fmt.Printf(" [模式] Docker 容器 (%s)\n", r.cfg.DockerImage)
// 显示将执行的指标及被跳过的原因(未知类型)
fmt.Printf(" 指标: ")
for _, m := range r.metrics {
if m.Enabled {
@@ -70,19 +77,13 @@ func (r *Runner) Run(ip string) *Report {
}
}
// 按需读取各测试脚本,拼装成完整脚本
script := r.assembleScript()
fmt.Printf(" [脚本] 拼装完成,%d 字节\n", len(script))
// 通过 heredoc 传到远程执行
scriptCmd := fmt.Sprintf("cat <<'AUTOCHECKSCRIPT' > /tmp/auto-check.sh\n%s\nAUTOCHECKSCRIPT\nchmod +x /tmp/auto-check.sh && bash /tmp/auto-check.sh 2>&1", script)
output, err := sshclient.RunCommand(r.client, scriptCmd)
// 启动容器执行测试脚本已打包在镜像中,环境变量传参)
output, err := r.runInDocker()
if err != nil {
fmt.Printf(" [执行] 脚本执行出错: %v\n", err)
fmt.Printf(" [执行] 容器执行出错: %v\n", err)
}
// 解析结果
// 解析容器输出(与宿主机模式输出格式相同)
if output != "" {
r.parseResults(report, output)
}
@@ -276,115 +277,82 @@ func (r *Runner) parseResults(report *Report, output string) {
flush()
}
// assembleScript 根据 metrics 读取对应的独立脚本文件,拼装成完整脚本
func (r *Runner) assembleScript() string {
var sb strings.Builder
// ============================
// Docker 容器执行(工具与脚本均预装在镜像中)
// ============================
// shebang + 环境变量导出
sb.WriteString("#!/bin/bash\n")
sb.WriteString("set -e\n\n")
// 导出参数变量
fmt.Fprintf(&sb, "export AUTOCHECK_DURATION=%d\n", int(r.cfg.Duration.Seconds()))
fmt.Fprintf(&sb, "export AUTOCHECK_THREADS=%d\n", r.cfg.Threads)
fmt.Fprintf(&sb, "export AUTOCHECK_DISK_SIZE_MB=%d\n", r.cfg.DiskSizeMB)
fmt.Fprintf(&sb, "export AUTOCHECK_MEM_SIZE_MB=%d\n", r.cfg.MemSizeMB)
fmt.Fprintf(&sb, "export AUTOCHECK_TEMP_INTERVAL=%d\n", int(r.cfg.TempLogInt.Seconds()))
sb.WriteString("export AUTOCHECK_SAMPLE_INTERVAL=2\n")
if r.cfg.DiskDir != "" {
fmt.Fprintf(&sb, "export AUTOCHECK_DISK_DIR=%s\n", r.cfg.DiskDir)
}
sb.WriteString("\n")
// 系统信息
sb.Write(r.readScript("sysinfo"))
sb.WriteString("\n")
// 温度监控启动
hasTemp := false
for _, m := range r.metrics {
if m.Name == "temp" && m.Enabled {
hasTemp = true
break
}
}
if hasTemp {
sb.Write(r.readScript("temp_start"))
sb.WriteString("\n")
// runInDocker 在 Docker 容器中执行压测(工具与脚本预装在镜像中)
// 固定分发模式:确保目标机有镜像(无则上传本地 tar + docker load→ 启动容器 → 解析结果
func (r *Runner) runInDocker() (string, error) {
// 1. 确保目标机存在压测镜像(不存在则上传本地导出的 tar 并 load
if err := r.ensureImage(); err != nil {
return "", err
}
// 按"先单项、后综合"排序测试脚本
ordered := make([]Metric, len(r.metrics))
copy(ordered, r.metrics)
sort.SliceStable(ordered, func(i, j int) bool {
return testPriority(ordered[i]) < testPriority(ordered[j])
})
// 2. 构建 Docker 运行命令(通过环境变量传入参数)
containerName := fmt.Sprintf("auto-check-%d", time.Now().UnixNano())
dockerRunCmd := fmt.Sprintf("docker run --rm --name %s "+
"-e AUTOCHECK_TYPES=%s "+
"-e AUTOCHECK_DURATION=%d "+
"-e AUTOCHECK_THREADS=%d "+
"-e AUTOCHECK_MEM_SIZE_MB=%d "+
"-e AUTOCHECK_DISK_SIZE_MB=%d "+
"-e AUTOCHECK_DISK_DIR=%s "+
"-e AUTOCHECK_TEMP_INTERVAL=%d "+
"-e AUTOCHECK_SAMPLE_INTERVAL=2 "+
"%s %s",
containerName,
r.cfg.Types,
int(r.cfg.Duration.Seconds()),
r.cfg.Threads,
r.cfg.MemSizeMB,
r.cfg.DiskSizeMB,
r.cfg.DiskDir,
int(r.cfg.TempLogInt.Seconds()),
r.cfg.DockerArgs,
r.cfg.DockerImage)
// 逐个测试脚本
for _, m := range ordered {
if m.IsMonitor {
continue
}
if !m.Enabled {
// 缺少依赖时仍输出一个 skip 结果,让报告完整展示原因
fmt.Fprintf(&sb, "echo '===TEST:%s==='\n", m.Name)
sb.WriteString("echo 'status:skip'\n")
fmt.Fprintf(&sb, "echo 'output:%s'\n", m.DisableReason)
sb.WriteString("echo ''\n")
continue
}
data := r.readScript(m.Name)
if data == nil {
fmt.Fprintf(&sb, "echo '===TEST:%s==='\n", m.Name)
sb.WriteString("echo 'status:skip'\n")
fmt.Fprintf(&sb, "echo 'output:脚本文件 scripts/%s.sh 不存在'\n", m.Name)
sb.WriteString("echo ''\n")
continue
}
sb.Write(data)
sb.WriteString("\n")
fmt.Printf(" [Docker] 启动容器: %s\n", containerName)
output, err := runPrivileged(r.client, dockerRunCmd, r.sudoPwd)
if err != nil {
return "", err
}
// 温度监控收尾
if hasTemp {
sb.Write(r.readScript("temp_end"))
sb.WriteString("\n")
}
// dmesg + 结束标记
sb.Write(r.readScript("dmesg"))
return sb.String()
return output, nil
}
// readScript 读取 scripts/ 目录下的脚本文件
func (r *Runner) readScript(name string) []byte {
data, err := os.ReadFile(fmt.Sprintf("scripts/%s.sh", name))
if err != nil {
// ensureImage 确保目标机存在压测镜像(固定分发模式,不依赖 registry
// 镜像已存在则直接使用;否则把工程目录中导出的镜像 tar 通过 SFTP
// 上传到目标机,再 docker load 载入。
func (r *Runner) ensureImage() error {
imageExists, _ := runPrivileged(r.client, fmt.Sprintf("docker images %s --format '{{.Repository}}:{{.Tag}}'", r.cfg.DockerImage), r.sudoPwd)
if strings.TrimSpace(imageExists) != "" {
fmt.Printf(" [Docker] 镜像已存在: %s\n", r.cfg.DockerImage)
return nil
}
return data
}
// testPriority 测试执行顺序:单项测试在前,综合测试(full)最后,监控项最后
func testPriority(m Metric) int {
if m.IsMonitor {
return 1000
// 本地导出的镜像文件docker save -o xxx.tar <image>
if r.cfg.DockerTar == "" {
return fmt.Errorf("目标机不存在镜像 %s 且未配置 docker_tar", r.cfg.DockerImage)
}
switch TestType(m.Name) {
case TestCPU:
return 0
case TestMemory:
return 1
case TestDiskIO:
return 2
case TestMemNative:
return 3
case TestFull:
return 100
default:
return 50
if _, err := os.Stat(r.cfg.DockerTar); err != nil {
return fmt.Errorf("本地镜像文件不存在: %s请先执行 docker save -o %s %s",
r.cfg.DockerTar, r.cfg.DockerTar, r.cfg.DockerImage)
}
fmt.Printf(" [Docker] 目标机镜像不存在,开始上传...\n")
remoteTar := "/tmp/auto-check-image.tar"
if err := sshclient.UploadFile(r.client, r.cfg.DockerTar, remoteTar); err != nil {
return fmt.Errorf("镜像上传失败: %w", err)
}
loadOut, err := runPrivileged(r.client, fmt.Sprintf("docker load -i %s 2>&1", remoteTar), r.sudoPwd)
sshclient.RunCommand(r.client, "rm -f "+remoteTar) // 清理远端临时 tar忽略失败
if err != nil || strings.Contains(loadOut, "Error") {
return fmt.Errorf("docker load 失败: %s", truncate(loadOut, 300))
}
fmt.Printf(" [Docker] 镜像载入成功: %s\n", r.cfg.DockerImage)
return nil
}
// ============================
@@ -423,6 +391,7 @@ func TestDevice(ip string, cfg config.Config) *Report {
// 由默认配置 + yaml 映射构造(避免硬编码,便于后续扩展字段)
stressCfg := DefaultConfig()
stressCfg.Types = cfg.Stress.Types
stressCfg.Duration = cfg.Stress.Duration
stressCfg.Threads = cfg.Stress.Threads
stressCfg.MemSizeMB = cfg.Stress.MemSizeMB
@@ -430,9 +399,12 @@ func TestDevice(ip string, cfg config.Config) *Report {
stressCfg.DiskDir = cfg.Stress.DiskDir
stressCfg.TempLogInt = cfg.Stress.TempInterval
stressCfg.TempLimit = cfg.Stress.TempLimit
stressCfg.UseMirror = cfg.Stress.UseMirror
// Docker 镜像配置(工具与脚本预装在镜像中,本地导出的 tar 上传到目标机后 load
stressCfg.DockerImage = cfg.Stress.DockerImage
stressCfg.DockerTar = cfg.Stress.DockerTar
stressCfg.DockerArgs = cfg.Stress.DockerArgs
return NewRunner(stressCfg, ParseTypes(cfg.Stress.Types), conn, cfg.Stress.AutoInstall, sudoPwd).Run(ip)
return NewRunner(stressCfg, ParseTypes(cfg.Stress.Types), conn, sudoPwd).Run(ip)
}
// NewSSHFailReport 构造 SSH 连接失败报告