This commit is contained in:
Netlops 2025-04-22 16:53:06 +08:00
commit 99d441c7ac
21 changed files with 3090 additions and 0 deletions

42
.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# 编译生成的文件
/bin/
*.exe
*.exe~
*.dll
*.so
*.dylib
# 测试二进制文件和覆盖率报告
*.test
*.out
coverage.html
# 日志目录和文件
/logs/
*.log
# 项目特定文件
cpu.pprof
mem.pprof
# Go语言相关
*.o
*.a
/pkg/
/vendor/
# IDE和编辑器文件
.idea/
.vscode/
*.swp
*.swo
*~
.DS_Store
# 临时配置文件
*.yaml.backup
*.yaml.bak
config.yaml.local
# 可能包含敏感信息的文件
.env

233
Makefile Normal file
View File

@ -0,0 +1,233 @@
# Open-CAS 断电 PLP SSD 数据完整性测试 Makefile
# 项目设置
PROJECT_NAME := plp-test
SERVER_BIN := bin/server
CLIENT_BIN := bin/client
CONFIG_FILE := config.yaml
LOG_DIR := logs
BIN_DIR := bin
# Go设置
GO := go
GO_BUILD := $(GO) build -v
GO_TEST := $(GO) test -v
GO_CLEAN := $(GO) clean
GO_LINT := golint
GO_FMT := $(GO) fmt
GO_VET := $(GO) vet
GO_MOD := $(GO) mod
# Delve调试器
DLV := dlv
# 测试参数
TEST_TYPE ?= sequential
DATA_SIZE ?= 100
BLOCK_SIZE ?= 4
CONCURRENT ?= false
# 版本信息
VERSION := 1.0.0
BUILD_TIME := $(shell date +%FT%T%z)
# 构建标志
DEBUG_FLAGS := -gcflags="all=-N -l"
# 其他参数
LOG_LEVEL ?= info
SERVER_PORT ?= 8080
# 默认目标
.PHONY: all
all: check_deps build
# 创建必要的目录
$(BIN_DIR) $(LOG_DIR):
mkdir -p $@
# 编译目标
.PHONY: build build_server build_client build_debug
build: check_deps $(BIN_DIR) build_server build_client
build_server: $(BIN_DIR)
$(GO_BUILD) -o $(SERVER_BIN) ./cmd/server
build_client: $(BIN_DIR)
$(GO_BUILD) -o $(CLIENT_BIN) ./cmd/client
# 编译调试版本
build_debug: check_deps $(BIN_DIR)
$(GO_BUILD) $(DEBUG_FLAGS) -o $(SERVER_BIN) ./cmd/server
$(GO_BUILD) $(DEBUG_FLAGS) -o $(CLIENT_BIN) ./cmd/client
# 测试目标
.PHONY: test test_sequential test_random test_mixed test_concurrent test_power_loss test_stability test_all
test: build
./scripts/run_test.sh $(TEST_TYPE) $(DATA_SIZE) $(BLOCK_SIZE) $(CONCURRENT)
test_sequential: TEST_TYPE=sequential
test_sequential: test
test_random: TEST_TYPE=random
test_random: test
test_mixed: TEST_TYPE=mixed
test_mixed: test
test_concurrent: TEST_TYPE=concurrent
test_concurrent: test
test_power_loss: TEST_TYPE=power_loss
test_power_loss: test
test_stability: TEST_TYPE=stability
test_stability: test
test_all: TEST_TYPE=all
test_all: test
# 单独启动服务器和客户端
.PHONY: run_server run_client
run_server: build_server
$(SERVER_BIN) -config $(CONFIG_FILE) -log-level $(LOG_LEVEL)
run_client: build_client
$(CLIENT_BIN) -config $(CONFIG_FILE) -test $(TEST_TYPE) -data-size $(DATA_SIZE) -block-size $(BLOCK_SIZE)
# 调试目标
.PHONY: debug_server debug_client
debug_server: build_debug
$(DLV) --listen=:2345 --headless=true --api-version=2 --accept-multiclient exec $(SERVER_BIN) -- -config $(CONFIG_FILE) -log-level debug
debug_client: build_debug
$(DLV) --listen=:2346 --headless=true --api-version=2 --accept-multiclient exec $(CLIENT_BIN) -- -config $(CONFIG_FILE) -test $(TEST_TYPE) -data-size $(DATA_SIZE) -block-size $(BLOCK_SIZE)
# 运行单元测试
.PHONY: unit_test unit_test_coverage
unit_test:
$(GO_TEST) ./...
unit_test_coverage:
$(GO_TEST) -coverprofile=coverage.out ./...
$(GO) tool cover -html=coverage.out -o coverage.html
# 代码质量检查
.PHONY: fmt lint vet check_code_quality
fmt:
$(GO_FMT) ./...
lint:
$(GO_LINT) ./...
vet:
$(GO_VET) ./...
# 组合所有代码质量检查
check_code_quality: fmt vet
@echo "代码质量检查完成"
# 依赖管理
.PHONY: check_deps deps_update check_dlv
check_deps:
@command -v casadm >/dev/null 2>&1 || (echo "错误: Open-CAS (casadm) 未安装,请先安装 Open-CAS" && exit 1)
@$(GO_MOD) verify || (echo "错误: Go 模块验证失败" && exit 1)
check_dlv:
@command -v dlv >/dev/null 2>&1 || (echo "警告: Delve 调试器未安装,部分调试功能将不可用。可以通过 'go install github.com/go-delve/delve/cmd/dlv@latest' 安装" && exit 1)
deps_update:
$(GO_MOD) tidy
# 环境检查
.PHONY: check_env
check_env:
@nvme_device=$$(grep -oP 'devices_nvme: "\K[^"]+' $(CONFIG_FILE)); \
hdd_device=$$(grep -oP 'devices_hdd: "\K[^"]+' $(CONFIG_FILE)); \
[ -b "$$nvme_device" ] || (echo "错误: NVMe设备 $$nvme_device 不存在或不是块设备" && exit 1); \
[ -b "$$hdd_device" ] || (echo "错误: HDD设备 $$hdd_device 不存在或不是块设备" && exit 1); \
echo "环境检查通过:设备 $$nvme_device 和 $$hdd_device 存在"
# 性能测试
.PHONY: bench_test
bench_test:
$(GO_TEST) -bench=. -benchmem ./...
# 清理
.PHONY: clean clean_logs clean_all help
clean:
rm -rf $(BIN_DIR)
$(GO_CLEAN) ./...
rm -f coverage.out coverage.html
clean_logs:
rm -rf $(LOG_DIR)/*
clean_all: clean clean_logs
@if casadm -L 2>/dev/null | grep -q "Cache Instance"; then \
echo "警告: 存在活动的缓存实例,尝试强制停止..."; \
cache_id=$$(grep -oP 'cache_instance_id: "\K[^"]+' $(CONFIG_FILE)); \
casadm -T -i $$cache_id -f 2>/dev/null || true; \
fi
# 帮助信息
help:
@echo "用法: make [目标]"
@echo ""
@echo "编译目标:"
@echo " all 编译所有组件 (默认)"
@echo " build 编译所有组件"
@echo " build_server 只编译服务器"
@echo " build_client 只编译客户端"
@echo " build_debug 编译调试版本 (包含调试符号)"
@echo ""
@echo "运行目标:"
@echo " run_server 只运行服务器"
@echo " run_client 只运行客户端"
@echo ""
@echo "测试目标:"
@echo " test 运行测试(默认顺序写入测试)"
@echo " test_sequential 运行顺序写入测试"
@echo " test_random 运行随机写入测试"
@echo " test_mixed 运行混合读写测试"
@echo " test_concurrent 运行高并发写入测试"
@echo " test_power_loss 运行断电恢复测试"
@echo " test_stability 运行长期稳定性测试"
@echo " test_all 运行所有测试"
@echo " unit_test 运行单元测试"
@echo " unit_test_coverage 生成单元测试覆盖率报告"
@echo " bench_test 运行性能测试"
@echo ""
@echo "调试目标:"
@echo " debug_server 使用Delve调试服务器 (需安装dlv)"
@echo " debug_client 使用Delve调试客户端 (需安装dlv)"
@echo ""
@echo "代码质量目标:"
@echo " fmt 格式化代码"
@echo " lint 运行 lint 代码质量检查"
@echo " vet 运行 Go vet 代码检查"
@echo " check_code_quality 运行所有代码质量检查"
@echo ""
@echo "依赖管理目标:"
@echo " check_deps 检查依赖项"
@echo " check_dlv 检查Delve调试器是否安装"
@echo " deps_update 更新依赖项"
@echo " check_env 检查测试环境"
@echo ""
@echo "清理目标:"
@echo " clean 清理编译产物"
@echo " clean_logs 清理日志"
@echo " clean_all 清理所有产物和缓存实例"
@echo ""
@echo "参数:"
@echo " TEST_TYPE=type 测试类型 (sequential, random, mixed, concurrent, power_loss, stability, all)"
@echo " DATA_SIZE=size 测试数据大小 (MB)"
@echo " BLOCK_SIZE=size 数据块大小 (KB)"
@echo " CONCURRENT=bool 是否并发运行测试 (true/false)"
@echo " LOG_LEVEL=level 日志级别 (debug, info, warn, error)"
@echo " SERVER_PORT=port 服务器端口 (默认:8080)"
@echo ""
@echo "示例:"
@echo " make test TEST_TYPE=power_loss DATA_SIZE=200 BLOCK_SIZE=8"
@echo " make test_all CONCURRENT=true"
@echo " make debug_server"

60
README.md Normal file
View File

@ -0,0 +1,60 @@
# Open-CAS 断电 PLP SSD 数据完整性测试
本项目提供了一套用于测试Open-CAS在断电情况下的PLP SSD数据完整性的测试框架。测试涵盖了多种数据写入场景并通过模拟断电来验证数据的持久性和一致性。
## 环境要求
- Go 1.20+
- NVMe SSD (支持PLP - Power Loss Protection)
- HDD 硬盘
- 已安装配置的Open-CAS
## 目录结构
```
.
├── cmd/
│ ├── client/ # 客户端命令行工具
│ └── server/ # 服务器命令行工具
├── internal/
│ ├── config/ # 配置处理
│ ├── model/ # 数据模型
│ ├── testcase/ # 测试用例实现
│ └── utils/ # 工具函数
├── pkg/ # 可被外部导入的包
│ └── opencastools/ # Open-CAS操作工具
├── scripts/ # 测试脚本
├── go.mod # Go模块文件
└── README.md # 本文档
```
## 使用方法
1. 配置测试环境
在运行测试前请确保您的Open-CAS已正确配置并将需要测试的NVMe和HDD设备路径在配置文件中指定。
2. 启动服务器
```bash
go run cmd/server/main.go -config config.yaml
```
3. 运行客户端测试
```bash
go run cmd/client/main.go -server localhost:8080 -test all
```
## 测试场景
本测试框架包含以下测试场景:
1. 顺序写入测试
2. 随机写入测试
3. 混合读写测试
4. 高并发写入测试
5. 断电恢复测试
6. 长期稳定性测试
详细测试指标和结果分析请参阅测试报告。

104
SUMMARY.md Normal file
View File

@ -0,0 +1,104 @@
# Open-CAS 断电 PLP SSD 数据完整性测试方案总结
## 项目概述
本项目提供了一套完整的测试方案用于评估在Open-CAS环境中使用具有电源损耗保护PLP的NVMe SSD作为缓存设备时在断电情况下的数据完整性保护能力。测试框架采用Go语言开发包含服务器端和客户端组件实现了多种测试场景以全面评估系统性能和数据安全性。
## 主要特点
1. **全面的测试场景**:包括顺序写入、随机写入、混合读写、高并发写入、断电恢复和长期稳定性测试
2. **模块化设计**:测试用例实现统一接口,便于扩展
3. **客户端-服务器架构**:分离测试控制和执行,提高灵活性
4. **详细的性能和数据完整性指标收集**:全面评估存储系统性能
5. **断电模拟机制**:通过软件方式模拟突然断电场景
## 技术架构
### 1. 服务器端
- 负责管理Open-CAS实例
- 执行数据写入和验证
- 模拟断电场景
- 收集和报告测试指标
### 2. 客户端
- 提供测试选择和控制界面
- 监控测试进度
- 展示测试结果
### 3. 核心组件
- **配置管理**:统一管理测试参数和环境配置
- **测试用例框架**:提供统一的测试生命周期管理
- **数据生成和验证**:确保数据完整性检查的可靠性
- **指标收集**:收集性能和数据完整性指标
## 测试方法论
本测试方案采用了系统化的测试方法:
1. **基准测试**:建立系统正常运行时的性能和数据完整性基准
2. **异常场景测试**:在各种工作负载下模拟断电,测试数据恢复能力
3. **长期稳定性测试**:验证系统在长时间运行和多次断电后的稳定性
## 测试指标
测试收集的关键指标包括:
1. **数据完整性指标**
- 数据丢失率
- 数据损坏率
- 元数据一致性
2. **性能指标**
- 读写吞吐量MB/s
- IOPS每秒I/O操作数
- 读写延迟ms
3. **恢复指标**
- 系统恢复时间
- 缓存重建速度
## 实现特点
1. **Go语言实现**利用Go语言的并发特性和系统编程能力
2. **模块化设计**:每个测试场景独立实现,便于扩展
3. **统一日志系统**:详细记录测试过程和结果
4. **RESTful API**服务器端提供API接口支持远程控制和监控
5. **优雅的错误处理**:确保测试失败时能够正确清理环境
## 测试结果解读
测试结果将根据以下标准进行评估:
1. **数据完整性**断电后数据丢失率应小于0.01%才能视为优秀
2. **性能影响**PLP机制不应显著影响正常工作负载下的性能
3. **恢复能力**:系统应能在合理时间内恢复正常运行
## 使用指南
1. 安装和配置Open-CAS
2. 更新配置文件中的设备路径
3. 运行服务器端:`go run cmd/server/main.go -config config.yaml`
4. 运行客户端来执行测试:`go run cmd/client/main.go -test power_loss`
5. 或使用脚本一键执行:`./scripts/run_test.sh power_loss 100 4`
## 注意事项
1. 测试需要root权限才能操作块设备
2. 断电测试可能导致文件系统损坏,请在测试环境中进行
3. 确保使用的NVMe SSD确实支持PLP功能否则可能导致更多数据丢失
4. 在生产环境应用前,建议进行充分的测试和验证
## 未来改进方向
1. 添加图形用户界面,简化测试操作
2. 支持更多缓存策略的测试
3. 集成更多文件系统和工作负载类型
4. 实现真实物理断电测试(需要硬件支持)
5. 添加分布式测试支持,模拟更复杂的存储环境
## 结论
本测试方案提供了一个全面评估Open-CAS环境中PLP SSD数据完整性的框架。通过系统化的测试可以量化评估存储系统在断电场景下的数据保护能力帮助用户在关键业务场景中做出更明智的存储选择。

208
TESTPLAN.md Normal file
View File

@ -0,0 +1,208 @@
# Open-CAS 断电 PLP SSD 数据完整性测试计划
## 1. 测试目标
本测试计划旨在评估在使用带有电源损耗保护Power Loss Protection, PLP的NVMe SSD作为缓存设备的Open-CAS环境中在断电情况下数据的完整性保护能力。测试将验证不同工作负载和断电场景下缓存数据的持久性和一致性。
## 2. 测试环境
### 2.1 硬件环境
- NVMe SSD: 一块带有PLP功能的企业级NVMe SSD缓存设备
- HDD: 一块传统机械硬盘(后端存储设备)
- 服务器: 支持Open-CAS的Linux服务器
### 2.2 软件环境
- 操作系统: Linux (推荐使用CentOS 7或Ubuntu 18.04以上版本)
- Open-CAS: 最新稳定版本
- 文件系统: ext4
### 2.3 缓存配置
- 缓存模式: Write-Back回写模式这是测试断电数据完整性的关键模式
- 缓存策略: Default (LRU)
- 清理策略: 默认
## 3. 测试场景
本测试方案包含以下六个关键测试场景:
### 3.1 顺序写入测试Sequential Write Test
- **测试目的**: 验证在顺序写入工作负载下PLP SSD的数据完整性保护能力
- **测试流程**:
1. 创建Open-CAS缓存实例使用NVMe SSD作为缓存设备HDD作为后端存储
2. 使用顺序写入方式写入固定大小默认100MB的数据到缓存
3. 验证所有数据的正确性
4. 记录写入性能指标
### 3.2 随机写入测试Random Write Test
- **测试目的**: 验证在随机写入工作负载下PLP SSD的数据完整性保护能力
- **测试流程**:
1. 创建Open-CAS缓存实例
2. 使用随机写入方式写入固定大小的数据到缓存
3. 验证所有数据的正确性
4. 记录写入性能指标
### 3.3 混合读写测试Mixed Read-Write Test
- **测试目的**: 验证在混合读写工作负载下PLP SSD的数据完整性保护能力
- **测试流程**:
1. 创建Open-CAS缓存实例
2. 执行混合读写操作70%读30%写)
3. 验证所有数据的正确性
4. 记录读写性能指标
### 3.4 高并发写入测试Concurrent Write Test
- **测试目的**: 验证在高并发写入工作负载下PLP SSD的数据完整性保护能力
- **测试流程**:
1. 创建Open-CAS缓存实例
2. 使用多个线程并发写入数据
3. 验证所有数据的正确性
4. 记录性能指标和并发能力
### 3.5 断电恢复测试Power Loss Recovery Test- 核心测试
- **测试目的**: 验证在断电场景下依靠PLP机制保护数据的能力
- **测试流程**:
1. 创建Open-CAS缓存实例使用Write-Back模式
2. 写入一部分数据并通过sync命令确保部分数据已刷新到磁盘
3. 继续写入数据但不同步,保证有部分数据仍在缓存中
4. 模拟断电通过强制终止Open-CAS进程
5. 重启系统并恢复缓存
6. 验证数据完整性,确定数据丢失量
7. 记录恢复时间和数据丢失率
### 3.6 长期稳定性测试Stability Test
- **测试目的**: 验证在长时间运行情况下,系统的稳定性和数据完整性
- **测试流程**:
1. 创建Open-CAS缓存实例
2. 运行混合读写工作负载持续较长时间默认60分钟
3. 在测试期间随机模拟多次断电
4. 验证数据完整性和系统稳定性
5. 记录长期运行性能指标
## 4. 关键测试指标
### 4.1 数据完整性指标
- **数据丢失率**: 断电后丢失的数据量占总数据量的百分比
- **数据损坏率**: 断电后被损坏(内容错误)的数据块数量占总数据块的百分比
- **元数据一致性**: 文件系统元数据在断电后是否保持一致
### 4.2 性能指标
- **写入吞吐量**: MB/s测量数据写入速度
- **读取吞吐量**: MB/s测量数据读取速度
- **IOPS**: 每秒输入/输出操作数衡量系统处理小型随机I/O的能力
- **写入延迟**: 毫秒,写操作的平均响应时间
- **读取延迟**: 毫秒,读操作的平均响应时间
### 4.3 恢复指标
- **恢复时间**: 毫秒,断电后系统恢复正常操作所需时间
- **元数据重建时间**: 元数据重建所需时间
- **缓存重建速度**: MB/s缓存重建速度
## 5. 测试方法与步骤
### 5.1 测试准备
1. 确保硬件环境符合要求特别是NVMe SSD具备PLP功能
2. 安装最新版本的Open-CAS
3. 配置系统,创建测试分区
4. 准备测试数据和验证工具
### 5.2 测试执行
1. 对每个测试场景,执行以下步骤:
- 重置测试环境,清理缓存
- 启动相应的测试程序
- 收集性能数据和日志
- 验证数据完整性
- 记录测试结果
2. 特别是对于断电恢复测试:
- 记录写入时的数据校验和
- 模拟断电(使用模拟断电工具或强制终止进程)
- 重启系统并恢复缓存
- 验证数据与原始校验和
- 计算数据丢失率和损坏率
### 5.3 测试频率与持续时间
- 基础性能测试每种测试执行3次取平均值
- 断电恢复测试进行至少10次断电模拟在不同写入阶段进行断电
- 长期稳定性测试持续运行至少24小时期间随机进行断电测试
## 6. 测试结果评估标准
### 6.1 数据完整性评估
| 等级 | 描述 | 数据丢失率 |
|------|------|------------|
| 优秀 | 几乎无数据丢失 | < 0.01% |
| 良好 | 极少数据丢失 | 0.01% ~ 0.1% |
| 一般 | 有少量数据丢失 | 0.1% ~ 1% |
| 差 | 明显数据丢失 | > 1% |
### 6.2 性能评估
性能评估将根据实际硬件配置而有所不同,但一般来说:
- 写入吞吐量应达到NVMe设备理论值的70%以上
- 读取吞吐量应达到NVMe设备理论值的80%以上
- 写入延迟应保持在10ms以下
- 恢复时间应在30秒内完成
## 7. 测试结果报告
测试报告将包含以下内容:
1. 测试环境详细配置
2. 各测试场景的详细结果,包括:
- 数据完整性指标
- 性能指标
- 恢复指标
3. 断电恢复测试的详细分析
4. 测试结论和建议
5. 发现的问题和限制
## 8. 注意事项
- 确保测试过程中不会影响生产环境
- 在断电测试前备份重要数据
- 记录详细的测试日志,便于问题分析
- 考虑不同文件系统对测试结果的影响
- 确保测试覆盖不同的缓存写入策略特别是Write-Back vs. Write-Through
- 考虑温度对SSD性能的影响记录测试过程中的环境温度
## 9. 测试工具实现细节
本测试工具采用Go语言实现包含服务器端和客户端两部分
1. **服务器端**
- 管理Open-CAS实例的创建和配置
- 执行实际的数据写入和验证
- 模拟断电场景
- 收集性能指标和数据完整性指标
2. **客户端**
- 提供用户界面,允许选择测试场景
- 显示测试进度和结果
- 生成测试报告
主要的测试模块包括:
- `Sequential Write Test`: 顺序写入测试模块
- `Random Write Test`: 随机写入测试模块
- `Mixed Read-Write Test`: 混合读写测试模块
- `Concurrent Write Test`: 高并发写入测试模块
- `Power Loss Test`: 断电恢复测试模块
- `Stability Test`: 长期稳定性测试模块
每个测试模块实现了共同的接口包含Setup、Run和Cleanup方法以确保测试的一致性和可比性。

298
cmd/client/main.go Normal file
View File

@ -0,0 +1,298 @@
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"net/http"
"time"
"plp-test/internal/config"
"plp-test/internal/model"
"plp-test/internal/utils"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
var (
configFile string
logLevel string
serverAddr string
testType string
timeout int
dataSizeMB int
blockSize int
concurrent bool
)
func init() {
flag.StringVar(&configFile, "config", "config.yaml", "配置文件路径")
flag.StringVar(&logLevel, "log-level", "info", "日志级别 (debug, info, warn, error)")
flag.StringVar(&serverAddr, "server", "", "服务器地址,格式为 host:port")
flag.StringVar(&testType, "test", "sequential", "测试类型 (sequential, random, mixed, concurrent, power_loss, stability, all)")
flag.IntVar(&timeout, "timeout", 0, "测试超时时间(秒)")
flag.IntVar(&dataSizeMB, "data-size", 0, "测试数据大小MB")
flag.IntVar(&blockSize, "block-size", 0, "数据块大小KB")
flag.BoolVar(&concurrent, "concurrent", false, "是否并发执行所有测试")
}
// Client 客户端
type Client struct {
config *config.Config
logger *logrus.Logger
httpClient *http.Client
serverAddr string
clientID string
}
// NewClient 创建客户端
func NewClient(cfg *config.Config, logger *logrus.Logger, serverAddr string) *Client {
if serverAddr == "" {
serverAddr = cfg.Client.ServerAddr
}
return &Client{
config: cfg,
logger: logger,
httpClient: &http.Client{
Timeout: time.Duration(cfg.Client.TimeoutSec) * time.Second,
},
serverAddr: serverAddr,
clientID: uuid.New().String(),
}
}
// RunTest 运行测试
func (c *Client) RunTest(testType string, dataSizeMB, blockSize int) error {
c.logger.Infof("运行测试 %s", testType)
// 准备请求数据
req := model.TestRequest{
TestType: testType,
DataSizeMB: dataSizeMB,
BlockSize: blockSize,
Concurrency: c.config.Client.Concurrency,
ClientID: c.clientID,
RequestTime: time.Now(),
Parameters: make(map[string]string),
}
// 设置默认值
if req.DataSizeMB == 0 {
req.DataSizeMB = c.config.Test.DataSizeMB
}
if req.BlockSize == 0 {
req.BlockSize = c.config.Test.BlockSize
}
// 序列化请求数据
reqData, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("序列化请求数据失败: %v", err)
}
// 发送请求
url := fmt.Sprintf("http://%s/run", c.serverAddr)
resp, err := c.httpClient.Post(url, "application/json", bytes.NewBuffer(reqData))
if err != nil {
return fmt.Errorf("发送请求失败: %v", err)
}
defer resp.Body.Close()
// 检查响应状态
if resp.StatusCode != http.StatusAccepted {
return fmt.Errorf("服务器返回错误状态码: %d", resp.StatusCode)
}
// 解析响应
var testResp model.TestResponse
if err := json.NewDecoder(resp.Body).Decode(&testResp); err != nil {
return fmt.Errorf("解析响应失败: %v", err)
}
c.logger.Infof("测试请求已接受RequestID: %s", testResp.RequestID)
// 监控测试状态
return c.MonitorTestStatus(testResp.RequestID)
}
// MonitorTestStatus 监控测试状态
func (c *Client) MonitorTestStatus(testID string) error {
c.logger.Infof("监控测试状态: %s", testID)
for {
// 获取测试状态
url := fmt.Sprintf("http://%s/status?test_id=%s", c.serverAddr, testID)
resp, err := c.httpClient.Get(url)
if err != nil {
c.logger.Warnf("获取测试状态失败: %v", err)
time.Sleep(2 * time.Second)
continue
}
// 检查响应状态
if resp.StatusCode == http.StatusNotFound {
c.logger.Infof("测试 %s 已完成", testID)
resp.Body.Close()
break
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
c.logger.Warnf("服务器返回错误状态码: %d", resp.StatusCode)
time.Sleep(2 * time.Second)
continue
}
// 解析响应
var status model.TestStatus
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
resp.Body.Close()
c.logger.Warnf("解析响应失败: %v", err)
time.Sleep(2 * time.Second)
continue
}
resp.Body.Close()
// 显示测试状态
c.logger.Infof("测试状态: %s, 进度: %.2f%%, 阶段: %s",
status.Status, status.Progress, status.CurrentPhase)
// 检查测试是否结束
if status.Status == "completed" || status.Status == "failed" || status.Status == "aborted" {
c.logger.Infof("测试 %s %s", testID, status.Status)
break
}
// 等待一段时间再次检查
time.Sleep(1 * time.Second)
}
return nil
}
// CheckServerHealth 检查服务器健康状态
func (c *Client) CheckServerHealth() error {
c.logger.Info("检查服务器健康状态")
url := fmt.Sprintf("http://%s/health", c.serverAddr)
resp, err := c.httpClient.Get(url)
if err != nil {
return fmt.Errorf("连接服务器失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("服务器返回错误状态码: %d", resp.StatusCode)
}
var health model.HealthStatus
if err := json.NewDecoder(resp.Body).Decode(&health); err != nil {
return fmt.Errorf("解析响应失败: %v", err)
}
c.logger.Infof("服务器状态: %s, 消息: %s", health.Status, health.Message)
return nil
}
// RunAllTests 运行所有测试
func (c *Client) RunAllTests(concurrent bool) error {
c.logger.Info("运行所有测试")
tests := c.config.Test.EnabledTests
if concurrent {
c.logger.Info("并发执行所有测试")
errCh := make(chan error, len(tests))
for _, test := range tests {
go func(t string) {
errCh <- c.RunTest(t, dataSizeMB, blockSize)
}(test)
}
// 等待所有测试完成
for range tests {
if err := <-errCh; err != nil {
c.logger.Errorf("测试失败: %v", err)
}
}
} else {
c.logger.Info("顺序执行所有测试")
for _, test := range tests {
if err := c.RunTest(test, dataSizeMB, blockSize); err != nil {
c.logger.Errorf("测试 %s 失败: %v", test, err)
}
}
}
return nil
}
func main() {
flag.Parse()
// 初始化日志级别
var level logrus.Level
switch logLevel {
case "debug":
level = logrus.DebugLevel
case "info":
level = logrus.InfoLevel
case "warn":
level = logrus.WarnLevel
case "error":
level = logrus.ErrorLevel
default:
level = logrus.InfoLevel
}
// 初始化日志
logger := logrus.New()
logger.SetLevel(level)
logger.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
})
// 加载配置
logger.Infof("加载配置文件: %s", configFile)
cfg, err := config.Load(configFile)
if err != nil {
logger.Fatalf("加载配置失败: %v", err)
}
// 初始化日志文件
if cfg.Client.LogFile != "" {
utils.InitLogger(cfg.Client.LogFile, level)
logger = utils.Logger
}
// 设置超时时间
if timeout > 0 {
cfg.Client.TimeoutSec = timeout
}
// 创建客户端
client := NewClient(cfg, logger, serverAddr)
// 检查服务器健康状态
if err := client.CheckServerHealth(); err != nil {
logger.Fatalf("服务器健康检查失败: %v", err)
}
// 运行测试
if testType == "all" {
if err := client.RunAllTests(concurrent); err != nil {
logger.Fatalf("运行所有测试失败: %v", err)
}
} else {
if err := client.RunTest(testType, dataSizeMB, blockSize); err != nil {
logger.Fatalf("运行测试 %s 失败: %v", testType, err)
}
}
logger.Info("所有测试已完成")
}

296
cmd/server/main.go Normal file
View File

@ -0,0 +1,296 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
"plp-test/internal/config"
"plp-test/internal/model"
"plp-test/internal/testcase"
"plp-test/internal/utils"
"github.com/sirupsen/logrus"
)
var (
configFile string
logLevel string
)
func init() {
flag.StringVar(&configFile, "config", "config.yaml", "配置文件路径")
flag.StringVar(&logLevel, "log-level", "info", "日志级别 (debug, info, warn, error)")
}
// TestRunner 测试运行器
type TestRunner struct {
config *config.Config
logger *logrus.Logger
factory *testcase.TestCaseFactory
tests map[string]testcase.TestCase
testsMu sync.RWMutex
testResult map[string]*model.TestResult
resultMu sync.RWMutex
}
// NewTestRunner 创建测试运行器
func NewTestRunner(cfg *config.Config, logger *logrus.Logger) *TestRunner {
return &TestRunner{
config: cfg,
logger: logger,
factory: testcase.NewTestCaseFactory(cfg, logger),
tests: make(map[string]testcase.TestCase),
testResult: make(map[string]*model.TestResult),
}
}
// RunTest 运行指定的测试
func (r *TestRunner) RunTest(testType string) (*model.TestResult, error) {
r.logger.Infof("准备运行测试: %s", testType)
// 创建测试实例
test, err := r.factory.CreateTestCase(testType)
if err != nil {
return nil, fmt.Errorf("创建测试用例失败: %v", err)
}
if test == nil {
return nil, fmt.Errorf("未找到测试用例: %s", testType)
}
// 存储测试实例
testID := test.Status().TestID
r.testsMu.Lock()
r.tests[testID] = test
r.testsMu.Unlock()
// 创建上下文以便可以取消测试
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 设置测试环境
r.logger.Info("设置测试环境")
if err := test.Setup(ctx); err != nil {
r.logger.Errorf("设置测试环境失败: %v", err)
return nil, err
}
// 运行测试
r.logger.Info("运行测试")
result, err := test.Run(ctx)
if err != nil {
r.logger.Errorf("测试运行失败: %v", err)
// 尝试清理
cleanupErr := test.Cleanup(ctx)
if cleanupErr != nil {
r.logger.Errorf("测试清理失败: %v", cleanupErr)
}
return nil, err
}
// 清理测试环境
r.logger.Info("清理测试环境")
if err := test.Cleanup(ctx); err != nil {
r.logger.Errorf("测试清理失败: %v", err)
return nil, err
}
// 存储测试结果
r.resultMu.Lock()
r.testResult[testID] = result
r.resultMu.Unlock()
// 移除测试实例
r.testsMu.Lock()
delete(r.tests, testID)
r.testsMu.Unlock()
r.logger.Infof("测试 %s 完成", testType)
return result, nil
}
// GetTestStatus 获取测试状态
func (r *TestRunner) GetTestStatus(testID string) *model.TestStatus {
r.testsMu.RLock()
defer r.testsMu.RUnlock()
if test, ok := r.tests[testID]; ok {
return test.Status()
}
return nil
}
// GetAllTestStatus 获取所有测试状态
func (r *TestRunner) GetAllTestStatus() []*model.TestStatus {
r.testsMu.RLock()
defer r.testsMu.RUnlock()
statuses := make([]*model.TestStatus, 0, len(r.tests))
for _, test := range r.tests {
statuses = append(statuses, test.Status())
}
return statuses
}
// StartServer 启动HTTP服务器
func StartServer(cfg *config.Config, runner *TestRunner, logger *logrus.Logger) *http.Server {
mux := http.NewServeMux()
// 健康检查接口
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
health := &model.HealthStatus{
Status: "ok",
Timestamp: time.Now(),
Message: "服务正常运行",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(health)
})
// 运行测试接口
mux.HandleFunc("/run", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req model.TestRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
logger.Infof("收到测试请求: %+v", req)
// 异步运行测试
go func() {
result, err := runner.RunTest(req.TestType)
if err != nil {
logger.Errorf("测试运行失败: %v", err)
} else {
logger.Infof("测试完成: %+v", result)
}
}()
resp := model.TestResponse{
RequestID: req.TestType + "-" + time.Now().Format("20060102-150405"),
Status: "accepted",
Message: "测试已接受并开始执行",
ServerTime: time.Now(),
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(resp)
})
// 获取测试状态接口
mux.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
testID := r.URL.Query().Get("test_id")
var status interface{}
if testID == "" {
// 获取所有测试状态
status = runner.GetAllTestStatus()
} else {
// 获取指定测试状态
status = runner.GetTestStatus(testID)
if status == nil {
http.Error(w, "Test not found", http.StatusNotFound)
return
}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(status)
})
// 启动服务器
addr := fmt.Sprintf("%s:%d", cfg.Server.ListenAddr, cfg.Server.Port)
server := &http.Server{
Addr: addr,
Handler: mux,
}
go func() {
logger.Infof("服务器启动在 %s", addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatalf("服务器启动失败: %v", err)
}
}()
return server
}
func main() {
flag.Parse()
// 初始化日志级别
var level logrus.Level
switch logLevel {
case "debug":
level = logrus.DebugLevel
case "info":
level = logrus.InfoLevel
case "warn":
level = logrus.WarnLevel
case "error":
level = logrus.ErrorLevel
default:
level = logrus.InfoLevel
}
// 初始化日志
logger := logrus.New()
logger.SetLevel(level)
logger.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
})
// 加载配置
logger.Infof("加载配置文件: %s", configFile)
cfg, err := config.Load(configFile)
if err != nil {
logger.Fatalf("加载配置失败: %v", err)
}
// 初始化日志文件
if cfg.Server.LogFile != "" {
utils.InitLogger(cfg.Server.LogFile, level)
logger = utils.Logger
}
// 创建测试运行器
runner := NewTestRunner(cfg, logger)
// 启动服务器
server := StartServer(cfg, runner, logger)
// 等待终止信号
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
logger.Info("正在关闭服务器...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
logger.Fatalf("服务器强制关闭: %v", err)
}
logger.Info("服务器已优雅关闭")
}

29
config.yaml Normal file
View File

@ -0,0 +1,29 @@
server:
listen_addr: "0.0.0.0"
port: 8080
log_file: "server.log"
devices_nvme: "/dev/nvme0n1"
devices_hdd: "/dev/sda"
cache_instance_id: "1"
mount_point: "/opt/sdf"
client:
server_addr: "localhost:8080"
log_file: "client.log"
concurrency: 5
timeout_sec: 30
report_format: "json"
test:
data_size_mb: 100
verification_frequency: 10
power_cut_method: "signal" # signal或physical
test_duration: 60
block_size: 4 # 4KB
enabled_tests:
- "sequential"
- "random"
- "mixed"
- "concurrent"
- "power_loss"
- "stability"

11
go.mod Normal file
View File

@ -0,0 +1,11 @@
module plp-test
go 1.20
require (
github.com/google/uuid v1.3.1
github.com/sirupsen/logrus v1.9.3
gopkg.in/yaml.v3 v3.0.1
)
require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect

19
go.sum Normal file
View File

@ -0,0 +1,19 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

134
internal/config/config.go Normal file
View File

@ -0,0 +1,134 @@
package config
import (
"fmt"
"io/ioutil"
"path/filepath"
"gopkg.in/yaml.v3"
)
// Config 结构体包含测试所需的所有配置项
type Config struct {
Server ServerConfig `yaml:"server"`
Client ClientConfig `yaml:"client"`
Test TestConfig `yaml:"test"`
}
// ServerConfig 包含服务器配置
type ServerConfig struct {
ListenAddr string `yaml:"listen_addr"`
Port int `yaml:"port"`
LogFile string `yaml:"log_file"`
DevicesNVMe string `yaml:"devices_nvme"` // NVMe设备路径
DevicesHDD string `yaml:"devices_hdd"` // HDD设备路径
CacheInstanceID string `yaml:"cache_instance_id"` // Open-CAS缓存实例ID
MountPoint string `yaml:"mount_point"` // 文件系统挂载点
}
// ClientConfig 包含客户端配置
type ClientConfig struct {
ServerAddr string `yaml:"server_addr"`
LogFile string `yaml:"log_file"`
Concurrency int `yaml:"concurrency"`
TimeoutSec int `yaml:"timeout_sec"`
ReportFormat string `yaml:"report_format"` // json, csv, html
}
// TestConfig 包含测试相关配置
type TestConfig struct {
DataSizeMB int `yaml:"data_size_mb"` // 每次测试的数据量
VerificationFrequency int `yaml:"verification_frequency"` // 验证频率
PowerCutMethod string `yaml:"power_cut_method"` // 模拟断电方法
TestDuration int `yaml:"test_duration"` // 测试持续时间(分钟)
BlockSize int `yaml:"block_size"` // 块大小(KB)
EnabledTests []string `yaml:"enabled_tests"` // 启用的测试用例
}
// Load 从指定的文件路径加载配置
func Load(path string) (*Config, error) {
filename, err := filepath.Abs(path)
if err != nil {
return nil, err
}
yamlFile, err := ioutil.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("error reading config file: %s", err)
}
config := &Config{}
err = yaml.Unmarshal(yamlFile, config)
if err != nil {
return nil, fmt.Errorf("error parsing config file: %s", err)
}
// 设置默认值
if config.Server.Port == 0 {
config.Server.Port = 8080
}
if config.Client.Concurrency == 0 {
config.Client.Concurrency = 5
}
if config.Client.TimeoutSec == 0 {
config.Client.TimeoutSec = 30
}
if config.Test.DataSizeMB == 0 {
config.Test.DataSizeMB = 100
}
if config.Test.BlockSize == 0 {
config.Test.BlockSize = 4 // 4KB
}
if config.Test.TestDuration == 0 {
config.Test.TestDuration = 60 // 60分钟
}
return config, nil
}
// Save 将配置保存到指定路径
func (c *Config) Save(path string) error {
data, err := yaml.Marshal(c)
if err != nil {
return fmt.Errorf("error marshaling config: %s", err)
}
err = ioutil.WriteFile(path, data, 0644)
if err != nil {
return fmt.Errorf("error writing config file: %s", err)
}
return nil
}
// CreateDefaultConfig 创建默认配置文件
func CreateDefaultConfig(path string) error {
config := &Config{
Server: ServerConfig{
ListenAddr: "0.0.0.0",
Port: 8080,
LogFile: "server.log",
DevicesNVMe: "/dev/nvme0n1",
DevicesHDD: "/dev/sda",
CacheInstanceID: "cache1",
MountPoint: "/mnt/opencas",
},
Client: ClientConfig{
ServerAddr: "localhost:8080",
LogFile: "client.log",
Concurrency: 5,
TimeoutSec: 30,
ReportFormat: "json",
},
Test: TestConfig{
DataSizeMB: 100,
VerificationFrequency: 10,
PowerCutMethod: "signal", // signal或physical
TestDuration: 60,
BlockSize: 4,
EnabledTests: []string{"sequential", "random", "mixed", "concurrent", "power_loss", "stability"},
},
}
return config.Save(path)
}

116
internal/model/data.go Normal file
View File

@ -0,0 +1,116 @@
package model
import (
"crypto/sha256"
"encoding/hex"
"time"
"github.com/google/uuid"
)
// TestBlock 表示单个测试数据块
type TestBlock struct {
ID string `json:"id"`
Data []byte `json:"data"`
Size int `json:"size"` // 数据块大小(字节)
Checksum string `json:"checksum"` // SHA-256校验和
CreatedAt time.Time `json:"created_at"` // 创建时间
Sequence int `json:"sequence"` // 数据块序列号
}
// NewTestBlock 创建一个新的测试数据块
func NewTestBlock(data []byte, sequence int) *TestBlock {
hash := sha256.Sum256(data)
checksum := hex.EncodeToString(hash[:])
return &TestBlock{
ID: uuid.New().String(),
Data: data,
Size: len(data),
Checksum: checksum,
CreatedAt: time.Now(),
Sequence: sequence,
}
}
// Verify 验证数据块的完整性
func (tb *TestBlock) Verify() bool {
hash := sha256.Sum256(tb.Data)
checksum := hex.EncodeToString(hash[:])
return checksum == tb.Checksum
}
// TestResult 表示测试结果
type TestResult struct {
ID string `json:"id"`
TestName string `json:"test_name"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Duration float64 `json:"duration"` // 持续时间(秒)
BlocksWritten int `json:"blocks_written"`
BlocksVerified int `json:"blocks_verified"`
DataWrittenMB float64 `json:"data_written_mb"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
WriteSpeedMBs float64 `json:"write_speed_mbs"` // MB/s
Metrics TestMetrics `json:"metrics"`
}
// TestMetrics 包含测试的性能指标
type TestMetrics struct {
WriteLatencyMs float64 `json:"write_latency_ms"` // 平均写入延迟(毫秒)
ReadLatencyMs float64 `json:"read_latency_ms"` // 平均读取延迟(毫秒)
IOPS float64 `json:"iops"` // 每秒IO操作数
DataIntegrityLoss int `json:"data_integrity_loss"` // 数据完整性丢失次数
RecoveryTimeMs float64 `json:"recovery_time_ms"` // 断电后恢复时间(毫秒)
}
// TestRequest 表示客户端发送的测试请求
type TestRequest struct {
TestType string `json:"test_type"`
BlockSize int `json:"block_size"`
DataSizeMB int `json:"data_size_mb"`
Concurrency int `json:"concurrency"`
Parameters map[string]string `json:"parameters"`
ClientID string `json:"client_id"`
RequestTime time.Time `json:"request_time"`
}
// TestResponse 表示服务器对测试请求的响应
type TestResponse struct {
RequestID string `json:"request_id"`
Status string `json:"status"` // success, error
Message string `json:"message,omitempty"`
Result *TestResult `json:"result,omitempty"`
ServerTime time.Time `json:"server_time"`
}
// PowerCutInfo 表示断电信息
type PowerCutInfo struct {
Timestamp time.Time `json:"timestamp"`
Duration int `json:"duration"` // 断电持续时间(秒)
BlocksWritten int `json:"blocks_written"`
RecoverySuccess bool `json:"recovery_success"`
DataLossMB float64 `json:"data_loss_mb"`
}
// TestStatus 表示测试状态
type TestStatus struct {
TestID string `json:"test_id"`
TestType string `json:"test_type"`
Status string `json:"status"` // running, completed, failed
Progress float64 `json:"progress"` // 0-100%
StartTime time.Time `json:"start_time"`
CurrentPhase string `json:"current_phase"`
Message string `json:"message,omitempty"`
}
// HealthStatus 表示服务器健康状态
type HealthStatus struct {
Status string `json:"status"` // ok, error
Timestamp time.Time `json:"timestamp"`
DiskSpace float64 `json:"disk_space"`
CpuUsage float64 `json:"cpu_usage"`
MemoryUsage float64 `json:"memory_usage"`
Message string `json:"message,omitempty"`
}

View File

@ -0,0 +1,58 @@
package testcase
import (
"context"
"plp-test/internal/config"
"plp-test/internal/model"
"github.com/sirupsen/logrus"
)
// ConcurrentWriteTest 并发写入测试
type ConcurrentWriteTest struct {
*BaseTestCase
}
// NewConcurrentWriteTest 创建并发写入测试
func NewConcurrentWriteTest(cfg *config.Config, logger *logrus.Logger) *ConcurrentWriteTest {
baseTest := NewBaseTestCase(
"concurrent",
"测试在Open-CAS环境中进行高并发写入时的数据完整性",
cfg,
logger,
)
return &ConcurrentWriteTest{
BaseTestCase: baseTest,
}
}
// Setup 设置测试环境
func (t *ConcurrentWriteTest) Setup(ctx context.Context) error {
if err := t.BaseTestCase.Setup(ctx); err != nil {
return err
}
// 实现并发写入测试的设置逻辑
return nil
}
// Run 运行测试
func (t *ConcurrentWriteTest) Run(ctx context.Context) (*model.TestResult, error) {
// 实现并发写入测试逻辑
return &model.TestResult{
ID: t.testID,
TestName: t.name,
Success: true,
}, nil
}
// Cleanup 清理测试环境
func (t *ConcurrentWriteTest) Cleanup(ctx context.Context) error {
if err := t.BaseTestCase.Cleanup(ctx); err != nil {
return err
}
// 实现并发写入测试的清理逻辑
return nil
}

View File

@ -0,0 +1,58 @@
package testcase
import (
"context"
"plp-test/internal/config"
"plp-test/internal/model"
"github.com/sirupsen/logrus"
)
// MixedReadWriteTest 混合读写测试
type MixedReadWriteTest struct {
*BaseTestCase
}
// NewMixedReadWriteTest 创建混合读写测试
func NewMixedReadWriteTest(cfg *config.Config, logger *logrus.Logger) *MixedReadWriteTest {
baseTest := NewBaseTestCase(
"mixed",
"测试在Open-CAS环境中进行混合读写操作时的数据完整性",
cfg,
logger,
)
return &MixedReadWriteTest{
BaseTestCase: baseTest,
}
}
// Setup 设置测试环境
func (t *MixedReadWriteTest) Setup(ctx context.Context) error {
if err := t.BaseTestCase.Setup(ctx); err != nil {
return err
}
// 实现混合读写测试的设置逻辑
return nil
}
// Run 运行测试
func (t *MixedReadWriteTest) Run(ctx context.Context) (*model.TestResult, error) {
// 实现混合读写测试逻辑
return &model.TestResult{
ID: t.testID,
TestName: t.name,
Success: true,
}, nil
}
// Cleanup 清理测试环境
func (t *MixedReadWriteTest) Cleanup(ctx context.Context) error {
if err := t.BaseTestCase.Cleanup(ctx); err != nil {
return err
}
// 实现混合读写测试的清理逻辑
return nil
}

View File

@ -0,0 +1,350 @@
package testcase
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"plp-test/internal/config"
"plp-test/internal/model"
"plp-test/internal/utils"
"github.com/sirupsen/logrus"
)
// PowerLossTest 断电测试
type PowerLossTest struct {
*BaseTestCase
testDir string
blockSize int
totalBlocks int
writtenBlocks int
verifiedBlocks int
corruptedBlocks int
blocks []*model.TestBlock
recoveryTimeMs float64
powerCutInfo *model.PowerCutInfo
}
// NewPowerLossTest 创建断电测试
func NewPowerLossTest(cfg *config.Config, logger *logrus.Logger) *PowerLossTest {
baseTest := NewBaseTestCase(
"power_loss",
"测试在断电情况下Open-CAS的数据完整性保护能力",
cfg,
logger,
)
return &PowerLossTest{
BaseTestCase: baseTest,
blockSize: utils.MBToBytes(float64(cfg.Test.BlockSize)),
totalBlocks: utils.MBToBytes(float64(cfg.Test.DataSizeMB)) / utils.MBToBytes(float64(cfg.Test.BlockSize)),
}
}
// Setup 设置测试环境
func (t *PowerLossTest) Setup(ctx context.Context) error {
if err := t.BaseTestCase.Setup(ctx); err != nil {
return err
}
t.setMessage("创建Open-CAS缓存实例")
id := t.config.Server.CacheInstanceID
nvme := t.config.Server.DevicesNVMe
hdd := t.config.Server.DevicesHDD
// 创建缓存实例 - 使用Write-Back模式以测试断电恢复
err := t.casManager.CreateCacheInstance(id, nvme, hdd, "wb")
if err != nil {
t.setStatus(StatusFailed)
return fmt.Errorf("创建缓存实例失败: %v", err)
}
// 获取缓存设备路径
cacheDevice := fmt.Sprintf("/dev/cas%s-1", id)
t.setMessage(fmt.Sprintf("格式化缓存设备 %s", cacheDevice))
// 格式化缓存设备
err = t.casManager.FormatDevice(cacheDevice, "ext4")
if err != nil {
t.setStatus(StatusFailed)
return fmt.Errorf("格式化缓存设备失败: %v", err)
}
// 挂载缓存设备
mountPoint := t.config.Server.MountPoint
t.setMessage(fmt.Sprintf("挂载缓存设备到 %s", mountPoint))
err = t.casManager.MountDevice(cacheDevice, mountPoint)
if err != nil {
t.setStatus(StatusFailed)
return fmt.Errorf("挂载缓存设备失败: %v", err)
}
// 创建测试目录
t.testDir = filepath.Join(mountPoint, "power_loss_test")
t.setMessage(fmt.Sprintf("创建测试目录 %s", t.testDir))
err = utils.CreateDirIfNotExist(t.testDir)
if err != nil {
t.setStatus(StatusFailed)
return fmt.Errorf("创建测试目录失败: %v", err)
}
// 初始化测试数据
t.blocks = make([]*model.TestBlock, 0, t.totalBlocks)
t.powerCutInfo = &model.PowerCutInfo{}
t.setProgress(10)
return nil
}
// Run 运行测试
func (t *PowerLossTest) Run(ctx context.Context) (*model.TestResult, error) {
t.setMessage("开始断电测试")
startTime := time.Now()
var totalBytesWritten int
// 写入阶段 - 只写入一部分数据,断电前
t.setMessage("写入数据 (断电前)")
blocksBeforePowerCut := t.totalBlocks / 2 // 写入一半的数据块
for i := 0; i < blocksBeforePowerCut; i++ {
select {
case <-ctx.Done():
t.setStatus(StatusAborted)
return nil, ctx.Err()
default:
// 生成随机数据
data, err := utils.GenerateRandomData(t.blockSize)
if err != nil {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("生成随机数据失败: %v", err)
}
// 创建测试数据块
block := model.NewTestBlock(data, i)
t.blocks = append(t.blocks, block)
// 写入文件
filePath := filepath.Join(t.testDir, fmt.Sprintf("block_%d.dat", i))
err = os.WriteFile(filePath, data, 0644)
if err != nil {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("写入文件 %s 失败: %v", filePath, err)
}
t.writtenBlocks++
totalBytesWritten += len(data)
// 更新进度
progress := float64(i+1) / float64(t.totalBlocks) * 30 // 第一阶段占30%
t.setProgress(progress)
}
}
// 记录写入数据的时间点
t.powerCutInfo.BlocksWritten = t.writtenBlocks
// 执行同步以确保部分数据已经写入到磁盘,但部分仍在缓存中
t.setMessage("执行sync同步部分数据到磁盘")
_, err := utils.ExecuteCommand("sync")
if err != nil {
t.logger.Warnf("执行sync命令失败: %v", err)
}
// 再写入一些数据但不同步,保证有缓存中的数据
t.setMessage("写入额外数据到缓存中 (这些数据可能会在断电后丢失)")
additionalBlocks := t.totalBlocks / 4 // 额外写入1/4的数据块
for i := blocksBeforePowerCut; i < blocksBeforePowerCut+additionalBlocks; i++ {
select {
case <-ctx.Done():
t.setStatus(StatusAborted)
return nil, ctx.Err()
default:
// 生成随机数据
data, err := utils.GenerateRandomData(t.blockSize)
if err != nil {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("生成随机数据失败: %v", err)
}
// 创建测试数据块
block := model.NewTestBlock(data, i)
t.blocks = append(t.blocks, block)
// 写入文件
filePath := filepath.Join(t.testDir, fmt.Sprintf("block_%d.dat", i))
err = os.WriteFile(filePath, data, 0644)
if err != nil {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("写入文件 %s 失败: %v", filePath, err)
}
t.writtenBlocks++
totalBytesWritten += len(data)
// 更新进度
progress := 30 + float64(i-blocksBeforePowerCut+1)/float64(additionalBlocks)*10 // 额外写入占10%
t.setProgress(progress)
}
}
// 模拟断电
t.setMessage("模拟断电...")
t.powerCutInfo.Timestamp = time.Now()
err = t.casManager.SimulatePowerCut(t.config.Server.CacheInstanceID)
if err != nil {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("模拟断电失败: %v", err)
}
// 模拟等待一段时间,就像真正断电后的重启
t.setMessage("模拟系统重启中...")
time.Sleep(2 * time.Second)
// 恢复阶段
t.setMessage("恢复阶段")
recoveryStart := time.Now()
// 重新创建缓存实例
id := t.config.Server.CacheInstanceID
nvme := t.config.Server.DevicesNVMe
hdd := t.config.Server.DevicesHDD
// 尝试修复/加载现有缓存
t.setMessage("尝试加载和修复缓存")
err = t.casManager.RepairCache(id, nvme)
if err != nil {
// 如果修复失败,尝试重新创建
t.logger.Warnf("修复缓存失败,尝试重新创建: %v", err)
err = t.casManager.CreateCacheInstance(id, nvme, hdd, "wb")
if err != nil {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("断电后重新创建缓存实例失败: %v", err)
}
}
// 获取缓存设备路径
cacheDevice := fmt.Sprintf("/dev/cas%s-1", id)
// 重新挂载缓存设备
mountPoint := t.config.Server.MountPoint
t.setMessage(fmt.Sprintf("重新挂载缓存设备到 %s", mountPoint))
err = t.casManager.MountDevice(cacheDevice, mountPoint)
if err != nil {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("断电后重新挂载缓存设备失败: %v", err)
}
// 记录恢复时间
t.recoveryTimeMs = float64(time.Since(recoveryStart).Milliseconds())
t.setProgress(50)
// 验证阶段 - 检查断电前写入的数据是否完整
t.setMessage("验证阶段 - 检查数据完整性")
t.corruptedBlocks = 0
for i, block := range t.blocks {
// 检查文件是否存在
filePath := filepath.Join(t.testDir, fmt.Sprintf("block_%d.dat", i))
if !utils.FileExists(filePath) {
t.logger.Warnf("文件 %s 在断电后丢失", filePath)
t.corruptedBlocks++
continue
}
// 读取文件数据
data, err := os.ReadFile(filePath)
if err != nil {
t.logger.Warnf("读取文件 %s 失败: %v", filePath, err)
t.corruptedBlocks++
continue
}
// 验证数据完整性
if string(data) != string(block.Data) {
t.logger.Warnf("文件 %s 数据损坏", filePath)
t.corruptedBlocks++
continue
}
t.verifiedBlocks++
// 更新进度
progress := 50 + float64(i+1)/float64(len(t.blocks))*40 // 验证占40%
t.setProgress(progress)
}
// 写入断电后的额外数据
t.setMessage("断电后写入额外数据")
for i := t.writtenBlocks; i < t.totalBlocks; i++ {
select {
case <-ctx.Done():
t.setStatus(StatusAborted)
return nil, ctx.Err()
default:
// 生成随机数据
data, err := utils.GenerateRandomData(t.blockSize)
if err != nil {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("生成随机数据失败: %v", err)
}
// 写入文件
filePath := filepath.Join(t.testDir, fmt.Sprintf("block_%d.dat", i))
err = os.WriteFile(filePath, data, 0644)
if err != nil {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("断电后写入文件 %s 失败: %v", filePath, err)
}
// 更新进度
progress := 90 + float64(i-t.writtenBlocks+1)/float64(t.totalBlocks-t.writtenBlocks)*10 // 最后10%
t.setProgress(progress)
}
}
// 记录断电信息
t.powerCutInfo.RecoverySuccess = t.corruptedBlocks == 0
t.powerCutInfo.DataLossMB = utils.BytesToMB(t.corruptedBlocks * t.blockSize)
t.setProgress(100)
t.setStatus(StatusCompleted)
t.setMessage("断电测试完成")
// 构造测试结果
result := t.getTestResult()
result.BlocksWritten = t.writtenBlocks
result.BlocksVerified = t.verifiedBlocks
result.DataWrittenMB = utils.BytesToMB(totalBytesWritten)
result.WriteSpeedMBs = utils.BytesToMB(totalBytesWritten) / time.Since(startTime).Seconds()
result.Metrics = model.TestMetrics{
DataIntegrityLoss: t.corruptedBlocks,
RecoveryTimeMs: t.recoveryTimeMs,
}
return result, nil
}
// Cleanup 清理测试环境
func (t *PowerLossTest) Cleanup(ctx context.Context) error {
if err := t.BaseTestCase.Cleanup(ctx); err != nil {
return err
}
t.setMessage("卸载缓存设备")
err := t.casManager.UnmountDevice(t.config.Server.MountPoint)
if err != nil {
t.logger.Warnf("卸载缓存设备失败: %v", err)
}
t.setMessage("停止缓存实例")
err = t.casManager.StopCacheInstance(t.config.Server.CacheInstanceID)
if err != nil {
t.logger.Warnf("停止缓存实例失败: %v", err)
}
return nil
}

View File

@ -0,0 +1,276 @@
package testcase
import (
"context"
"fmt"
"math/rand"
"os"
"path/filepath"
"time"
"plp-test/internal/config"
"plp-test/internal/model"
"plp-test/internal/utils"
"github.com/sirupsen/logrus"
)
// RandomWriteTest 随机写入测试
type RandomWriteTest struct {
*BaseTestCase
testDir string
blockSize int
totalBlocks int
writtenBlocks int
verifiedBlocks int
writeSequence []int // 记录写入顺序
blocks map[int]*model.TestBlock
writeLatencies []float64
readLatencies []float64
}
// NewRandomWriteTest 创建随机写入测试
func NewRandomWriteTest(cfg *config.Config, logger *logrus.Logger) *RandomWriteTest {
baseTest := NewBaseTestCase(
"random",
"测试在Open-CAS环境中进行随机写入时的数据完整性",
cfg,
logger,
)
return &RandomWriteTest{
BaseTestCase: baseTest,
blockSize: utils.MBToBytes(float64(cfg.Test.BlockSize)),
totalBlocks: utils.MBToBytes(float64(cfg.Test.DataSizeMB)) / utils.MBToBytes(float64(cfg.Test.BlockSize)),
blocks: make(map[int]*model.TestBlock),
writeSequence: make([]int, 0),
writeLatencies: make([]float64, 0),
readLatencies: make([]float64, 0),
}
}
// Setup 设置测试环境
func (t *RandomWriteTest) Setup(ctx context.Context) error {
if err := t.BaseTestCase.Setup(ctx); err != nil {
return err
}
t.setMessage("创建Open-CAS缓存实例")
id := t.config.Server.CacheInstanceID
nvme := t.config.Server.DevicesNVMe
hdd := t.config.Server.DevicesHDD
// 创建缓存实例
err := t.casManager.CreateCacheInstance(id, nvme, hdd, "wb") // 使用write-back模式
if err != nil {
t.setStatus(StatusFailed)
return fmt.Errorf("创建缓存实例失败: %v", err)
}
// 获取缓存设备路径
cacheDevice := fmt.Sprintf("/dev/cas%s-1", id)
t.setMessage(fmt.Sprintf("格式化缓存设备 %s", cacheDevice))
// 格式化缓存设备
err = t.casManager.FormatDevice(cacheDevice, "ext4")
if err != nil {
t.setStatus(StatusFailed)
return fmt.Errorf("格式化缓存设备失败: %v", err)
}
// 挂载缓存设备
mountPoint := t.config.Server.MountPoint
t.setMessage(fmt.Sprintf("挂载缓存设备到 %s", mountPoint))
err = t.casManager.MountDevice(cacheDevice, mountPoint)
if err != nil {
t.setStatus(StatusFailed)
return fmt.Errorf("挂载缓存设备失败: %v", err)
}
// 创建测试目录
t.testDir = filepath.Join(mountPoint, "random_test")
t.setMessage(fmt.Sprintf("创建测试目录 %s", t.testDir))
err = utils.CreateDirIfNotExist(t.testDir)
if err != nil {
t.setStatus(StatusFailed)
return fmt.Errorf("创建测试目录失败: %v", err)
}
// 生成随机的写入顺序
rand.Seed(time.Now().UnixNano())
indices := make([]int, t.totalBlocks)
for i := 0; i < t.totalBlocks; i++ {
indices[i] = i
}
// 洗牌算法生成随机序列
for i := range indices {
j := rand.Intn(i + 1)
indices[i], indices[j] = indices[j], indices[i]
}
t.writeSequence = indices
t.setProgress(10)
return nil
}
// Run 运行测试
func (t *RandomWriteTest) Run(ctx context.Context) (*model.TestResult, error) {
t.setMessage("开始随机写入测试")
startTime := time.Now()
var totalBytesWritten int
// 写入阶段
t.setMessage("写入阶段")
for i, blockIdx := range t.writeSequence {
select {
case <-ctx.Done():
t.setStatus(StatusAborted)
return nil, ctx.Err()
default:
// 生成随机数据
data, err := utils.GenerateRandomData(t.blockSize)
if err != nil {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("生成随机数据失败: %v", err)
}
// 创建测试数据块
block := model.NewTestBlock(data, blockIdx)
t.blocks[blockIdx] = block
// 写入文件
filePath := filepath.Join(t.testDir, fmt.Sprintf("block_%d.dat", blockIdx))
writeStart := time.Now()
err = os.WriteFile(filePath, data, 0644)
if err != nil {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("写入文件 %s 失败: %v", filePath, err)
}
// 记录写入延迟
writeLatency := time.Since(writeStart).Seconds() * 1000 // 毫秒
t.writeLatencies = append(t.writeLatencies, writeLatency)
t.writtenBlocks++
totalBytesWritten += len(data)
// 更新进度
progress := float64(i+1) / float64(t.totalBlocks) * 50 // 写入阶段占50%
t.setProgress(progress)
// 每隔一定数量的块,强制刷新缓存
if i > 0 && i%100 == 0 {
t.logger.Infof("已写入 %d 个数据块,强制刷新缓存", i)
t.casManager.FlushCache(t.config.Server.CacheInstanceID)
}
}
}
// 验证阶段 - 按随机顺序读取
t.setMessage("验证阶段")
// 再次生成随机顺序,用于随机读取
readIndices := make([]int, len(t.writeSequence))
copy(readIndices, t.writeSequence)
rand.Shuffle(len(readIndices), func(i, j int) {
readIndices[i], readIndices[j] = readIndices[j], readIndices[i]
})
for i, blockIdx := range readIndices {
select {
case <-ctx.Done():
t.setStatus(StatusAborted)
return nil, ctx.Err()
default:
block, ok := t.blocks[blockIdx]
if !ok {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("找不到索引为 %d 的数据块", blockIdx)
}
filePath := filepath.Join(t.testDir, fmt.Sprintf("block_%d.dat", blockIdx))
readStart := time.Now()
data, err := os.ReadFile(filePath)
if err != nil {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("读取文件 %s 失败: %v", filePath, err)
}
// 记录读取延迟
readLatency := time.Since(readStart).Seconds() * 1000 // 毫秒
t.readLatencies = append(t.readLatencies, readLatency)
// 验证数据完整性
if string(data) != string(block.Data) {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("数据验证失败,块 %d 内容不匹配", blockIdx)
}
t.verifiedBlocks++
// 更新进度
progress := 50 + float64(i+1)/float64(len(readIndices))*50 // 验证阶段占50%
t.setProgress(progress)
}
}
// 计算性能指标
duration := time.Since(startTime)
writeSpeedMBs := utils.BytesToMB(totalBytesWritten) / duration.Seconds()
// 计算平均延迟
var totalWriteLatency, totalReadLatency float64
for _, latency := range t.writeLatencies {
totalWriteLatency += latency
}
for _, latency := range t.readLatencies {
totalReadLatency += latency
}
avgWriteLatency := totalWriteLatency / float64(len(t.writeLatencies))
avgReadLatency := totalReadLatency / float64(len(t.readLatencies))
// 计算IOPS
operations := t.writtenBlocks + t.verifiedBlocks
iops := utils.CalculateIOPS(operations, duration)
t.setProgress(100)
t.setStatus(StatusCompleted)
t.setMessage("随机写入测试完成")
// 构造测试结果
result := t.getTestResult()
result.BlocksWritten = t.writtenBlocks
result.BlocksVerified = t.verifiedBlocks
result.DataWrittenMB = utils.BytesToMB(totalBytesWritten)
result.WriteSpeedMBs = writeSpeedMBs
result.Metrics = model.TestMetrics{
WriteLatencyMs: avgWriteLatency,
ReadLatencyMs: avgReadLatency,
IOPS: iops,
DataIntegrityLoss: 0, // 没有数据完整性丢失
}
return result, nil
}
// Cleanup 清理测试环境
func (t *RandomWriteTest) Cleanup(ctx context.Context) error {
if err := t.BaseTestCase.Cleanup(ctx); err != nil {
return err
}
t.setMessage("卸载缓存设备")
err := t.casManager.UnmountDevice(t.config.Server.MountPoint)
if err != nil {
t.logger.Warnf("卸载缓存设备失败: %v", err)
}
t.setMessage("停止缓存实例")
err = t.casManager.StopCacheInstance(t.config.Server.CacheInstanceID)
if err != nil {
t.logger.Warnf("停止缓存实例失败: %v", err)
}
return nil
}

View File

@ -0,0 +1,249 @@
package testcase
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"plp-test/internal/config"
"plp-test/internal/model"
"plp-test/internal/utils"
"github.com/sirupsen/logrus"
)
// SequentialWriteTest 顺序写入测试
type SequentialWriteTest struct {
*BaseTestCase
testDir string
blockSize int
totalBlocks int
writtenBlocks int
verifiedBlocks int
blocks []*model.TestBlock
writeLatencies []float64
readLatencies []float64
}
// NewSequentialWriteTest 创建顺序写入测试
func NewSequentialWriteTest(cfg *config.Config, logger *logrus.Logger) *SequentialWriteTest {
baseTest := NewBaseTestCase(
"sequential",
"测试在Open-CAS环境中进行顺序写入时的数据完整性",
cfg,
logger,
)
return &SequentialWriteTest{
BaseTestCase: baseTest,
blockSize: utils.MBToBytes(float64(cfg.Test.BlockSize)),
totalBlocks: utils.MBToBytes(float64(cfg.Test.DataSizeMB)) / utils.MBToBytes(float64(cfg.Test.BlockSize)),
}
}
// Setup 设置测试环境
func (t *SequentialWriteTest) Setup(ctx context.Context) error {
if err := t.BaseTestCase.Setup(ctx); err != nil {
return err
}
t.setMessage("创建Open-CAS缓存实例")
id := t.config.Server.CacheInstanceID
nvme := t.config.Server.DevicesNVMe
hdd := t.config.Server.DevicesHDD
// 创建缓存实例
err := t.casManager.CreateCacheInstance(id, nvme, hdd, "wb") // 使用write-back模式
if err != nil {
t.setStatus(StatusFailed)
return fmt.Errorf("创建缓存实例失败: %v", err)
}
// 获取缓存设备路径
cacheDevice := fmt.Sprintf("/dev/cas%s-1", id)
t.setMessage(fmt.Sprintf("格式化缓存设备 %s", cacheDevice))
// 格式化缓存设备
err = t.casManager.FormatDevice(cacheDevice, "ext4")
if err != nil {
t.setStatus(StatusFailed)
return fmt.Errorf("格式化缓存设备失败: %v", err)
}
// 挂载缓存设备
mountPoint := t.config.Server.MountPoint
t.setMessage(fmt.Sprintf("挂载缓存设备到 %s", mountPoint))
err = t.casManager.MountDevice(cacheDevice, mountPoint)
if err != nil {
t.setStatus(StatusFailed)
return fmt.Errorf("挂载缓存设备失败: %v", err)
}
// 创建测试目录
t.testDir = filepath.Join(mountPoint, "sequential_test")
t.setMessage(fmt.Sprintf("创建测试目录 %s", t.testDir))
err = utils.CreateDirIfNotExist(t.testDir)
if err != nil {
t.setStatus(StatusFailed)
return fmt.Errorf("创建测试目录失败: %v", err)
}
// 初始化测试数据
t.blocks = make([]*model.TestBlock, 0, t.totalBlocks)
t.writeLatencies = make([]float64, 0, t.totalBlocks)
t.readLatencies = make([]float64, 0, t.totalBlocks)
t.setProgress(10)
return nil
}
// Run 运行测试
func (t *SequentialWriteTest) Run(ctx context.Context) (*model.TestResult, error) {
t.setMessage("开始顺序写入测试")
startTime := time.Now()
var totalBytesWritten int
// 写入阶段
t.setMessage("写入阶段")
for i := 0; i < t.totalBlocks; i++ {
select {
case <-ctx.Done():
t.setStatus(StatusAborted)
return nil, ctx.Err()
default:
// 生成随机数据
data, err := utils.GenerateRandomData(t.blockSize)
if err != nil {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("生成随机数据失败: %v", err)
}
// 创建测试数据块
block := model.NewTestBlock(data, i)
t.blocks = append(t.blocks, block)
// 写入文件
filePath := filepath.Join(t.testDir, fmt.Sprintf("block_%d.dat", i))
writeStart := time.Now()
err = os.WriteFile(filePath, data, 0644)
if err != nil {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("写入文件 %s 失败: %v", filePath, err)
}
// 记录写入延迟
writeLatency := time.Since(writeStart).Seconds() * 1000 // 毫秒
t.writeLatencies = append(t.writeLatencies, writeLatency)
t.writtenBlocks++
totalBytesWritten += len(data)
// 更新进度
progress := float64(i+1) / float64(t.totalBlocks) * 50 // 写入阶段占50%
t.setProgress(progress)
// 每隔一定数量的块,强制刷新缓存
if i > 0 && i%100 == 0 {
t.logger.Infof("已写入 %d 个数据块,强制刷新缓存", i)
t.casManager.FlushCache(t.config.Server.CacheInstanceID)
}
}
}
// 验证阶段
t.setMessage("验证阶段")
for i, block := range t.blocks {
select {
case <-ctx.Done():
t.setStatus(StatusAborted)
return nil, ctx.Err()
default:
filePath := filepath.Join(t.testDir, fmt.Sprintf("block_%d.dat", i))
readStart := time.Now()
data, err := os.ReadFile(filePath)
if err != nil {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("读取文件 %s 失败: %v", filePath, err)
}
// 记录读取延迟
readLatency := time.Since(readStart).Seconds() * 1000 // 毫秒
t.readLatencies = append(t.readLatencies, readLatency)
// 验证数据完整性
if string(data) != string(block.Data) {
t.setStatus(StatusFailed)
return nil, fmt.Errorf("数据验证失败,块 %d 内容不匹配", i)
}
t.verifiedBlocks++
// 更新进度
progress := 50 + float64(i+1)/float64(len(t.blocks))*50 // 验证阶段占50%
t.setProgress(progress)
}
}
// 计算性能指标
duration := time.Since(startTime)
writeSpeedMBs := utils.BytesToMB(totalBytesWritten) / duration.Seconds()
// 计算平均延迟
var totalWriteLatency, totalReadLatency float64
for _, latency := range t.writeLatencies {
totalWriteLatency += latency
}
for _, latency := range t.readLatencies {
totalReadLatency += latency
}
avgWriteLatency := totalWriteLatency / float64(len(t.writeLatencies))
avgReadLatency := totalReadLatency / float64(len(t.readLatencies))
// 计算IOPS
operations := t.writtenBlocks + t.verifiedBlocks
iops := utils.CalculateIOPS(operations, duration)
t.setProgress(100)
t.setStatus(StatusCompleted)
t.setMessage("顺序写入测试完成")
// 构造测试结果
result := t.getTestResult()
result.BlocksWritten = t.writtenBlocks
result.BlocksVerified = t.verifiedBlocks
result.DataWrittenMB = utils.BytesToMB(totalBytesWritten)
result.WriteSpeedMBs = writeSpeedMBs
result.Metrics = model.TestMetrics{
WriteLatencyMs: avgWriteLatency,
ReadLatencyMs: avgReadLatency,
IOPS: iops,
DataIntegrityLoss: 0, // 没有数据完整性丢失
}
return result, nil
}
// Cleanup 清理测试环境
func (t *SequentialWriteTest) Cleanup(ctx context.Context) error {
if err := t.BaseTestCase.Cleanup(ctx); err != nil {
return err
}
t.setMessage("卸载缓存设备")
err := t.casManager.UnmountDevice(t.config.Server.MountPoint)
if err != nil {
t.logger.Warnf("卸载缓存设备失败: %v", err)
}
t.setMessage("停止缓存实例")
err = t.casManager.StopCacheInstance(t.config.Server.CacheInstanceID)
if err != nil {
t.logger.Warnf("停止缓存实例失败: %v", err)
}
return nil
}

View File

@ -0,0 +1,58 @@
package testcase
import (
"context"
"plp-test/internal/config"
"plp-test/internal/model"
"github.com/sirupsen/logrus"
)
// StabilityTest 长期稳定性测试
type StabilityTest struct {
*BaseTestCase
}
// NewStabilityTest 创建长期稳定性测试
func NewStabilityTest(cfg *config.Config, logger *logrus.Logger) *StabilityTest {
baseTest := NewBaseTestCase(
"stability",
"测试在长时间运行下Open-CAS的稳定性和数据完整性",
cfg,
logger,
)
return &StabilityTest{
BaseTestCase: baseTest,
}
}
// Setup 设置测试环境
func (t *StabilityTest) Setup(ctx context.Context) error {
if err := t.BaseTestCase.Setup(ctx); err != nil {
return err
}
// 实现长期稳定性测试的设置逻辑
return nil
}
// Run 运行测试
func (t *StabilityTest) Run(ctx context.Context) (*model.TestResult, error) {
// 实现长期稳定性测试逻辑
return &model.TestResult{
ID: t.testID,
TestName: t.name,
Success: true,
}, nil
}
// Cleanup 清理测试环境
func (t *StabilityTest) Cleanup(ctx context.Context) error {
if err := t.BaseTestCase.Cleanup(ctx); err != nil {
return err
}
// 实现长期稳定性测试的清理逻辑
return nil
}

View File

@ -0,0 +1,201 @@
package testcase
import (
"context"
"sync"
"time"
"plp-test/internal/config"
"plp-test/internal/model"
"plp-test/pkg/opencastools"
"github.com/sirupsen/logrus"
)
// TestCaseStatus 表示测试用例状态
type TestCaseStatus string
const (
StatusReady TestCaseStatus = "ready"
StatusRunning TestCaseStatus = "running"
StatusCompleted TestCaseStatus = "completed"
StatusFailed TestCaseStatus = "failed"
StatusAborted TestCaseStatus = "aborted"
)
// TestCase 定义测试用例接口
type TestCase interface {
// Name 返回测试用例名称
Name() string
// Description 返回测试用例描述
Description() string
// Setup 设置测试环境
Setup(ctx context.Context) error
// Run 运行测试
Run(ctx context.Context) (*model.TestResult, error)
// Cleanup 清理测试环境
Cleanup(ctx context.Context) error
// Status 获取测试状态
Status() *model.TestStatus
}
// BaseTestCase 基础测试用例实现
type BaseTestCase struct {
name string
description string
status TestCaseStatus
statusMu sync.RWMutex
config *config.Config
logger *logrus.Logger
casManager *opencastools.OpenCASManager
progress float64
message string
startTime time.Time
testID string
}
// NewBaseTestCase 创建基础测试用例
func NewBaseTestCase(name, description string, cfg *config.Config, logger *logrus.Logger) *BaseTestCase {
return &BaseTestCase{
name: name,
description: description,
status: StatusReady,
config: cfg,
logger: logger,
casManager: opencastools.NewOpenCASManager(logger),
testID: name + "-" + time.Now().Format("20060102-150405"),
}
}
// Name 返回测试用例名称
func (b *BaseTestCase) Name() string {
return b.name
}
// Description 返回测试用例描述
func (b *BaseTestCase) Description() string {
return b.description
}
// Setup 设置测试环境
func (b *BaseTestCase) Setup(ctx context.Context) error {
b.setStatus(StatusRunning)
b.setProgress(0)
b.setMessage("设置测试环境")
b.startTime = time.Now()
return nil
}
// Run 运行测试
func (b *BaseTestCase) Run(ctx context.Context) (*model.TestResult, error) {
// 由子类实现
return nil, nil
}
// Cleanup 清理测试环境
func (b *BaseTestCase) Cleanup(ctx context.Context) error {
b.setMessage("清理测试环境")
return nil
}
// Status 获取测试状态
func (b *BaseTestCase) Status() *model.TestStatus {
b.statusMu.RLock()
defer b.statusMu.RUnlock()
return &model.TestStatus{
TestID: b.testID,
TestType: b.name,
Status: string(b.status),
Progress: b.progress,
StartTime: b.startTime,
CurrentPhase: b.message,
}
}
// setStatus 设置测试状态
func (b *BaseTestCase) setStatus(status TestCaseStatus) {
b.statusMu.Lock()
defer b.statusMu.Unlock()
b.status = status
}
// setProgress 设置测试进度
func (b *BaseTestCase) setProgress(progress float64) {
b.statusMu.Lock()
defer b.statusMu.Unlock()
b.progress = progress
}
// setMessage 设置测试消息
func (b *BaseTestCase) setMessage(message string) {
b.statusMu.Lock()
defer b.statusMu.Unlock()
b.message = message
b.logger.Info(message)
}
// getTestResult 获取测试结果的基本信息
func (b *BaseTestCase) getTestResult() *model.TestResult {
return &model.TestResult{
ID: b.testID,
TestName: b.name,
StartTime: b.startTime,
EndTime: time.Now(),
Duration: time.Since(b.startTime).Seconds(),
Success: b.status == StatusCompleted,
}
}
// TestCaseFactory 测试用例工厂,用于创建所有测试用例
type TestCaseFactory struct {
config *config.Config
logger *logrus.Logger
}
// NewTestCaseFactory 创建测试用例工厂
func NewTestCaseFactory(cfg *config.Config, logger *logrus.Logger) *TestCaseFactory {
return &TestCaseFactory{
config: cfg,
logger: logger,
}
}
// CreateTestCase 根据名称创建测试用例
func (f *TestCaseFactory) CreateTestCase(name string) (TestCase, error) {
switch name {
case "sequential":
return NewSequentialWriteTest(f.config, f.logger), nil
case "random":
return NewRandomWriteTest(f.config, f.logger), nil
case "mixed":
return NewMixedReadWriteTest(f.config, f.logger), nil
case "concurrent":
return NewConcurrentWriteTest(f.config, f.logger), nil
case "power_loss":
return NewPowerLossTest(f.config, f.logger), nil
case "stability":
return NewStabilityTest(f.config, f.logger), nil
default:
return nil, nil
}
}
// CreateAllEnabledTestCases 创建所有启用的测试用例
func (f *TestCaseFactory) CreateAllEnabledTestCases() []TestCase {
var testCases []TestCase
for _, name := range f.config.Test.EnabledTests {
testCase, err := f.CreateTestCase(name)
if err == nil && testCase != nil {
testCases = append(testCases, testCase)
}
}
return testCases
}

131
internal/utils/utils.go Normal file
View File

@ -0,0 +1,131 @@
package utils
import (
"crypto/rand"
"fmt"
"math"
"os"
"os/exec"
"strings"
"time"
"github.com/sirupsen/logrus"
)
// Logger 全局日志实例
var Logger *logrus.Logger
// InitLogger 初始化日志系统
func InitLogger(logFile string, level logrus.Level) {
Logger = logrus.New()
Logger.SetLevel(level)
// 设置日志格式
Logger.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2006-01-02 15:04:05",
})
// 如果提供了日志文件,则输出到文件
if logFile != "" {
file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err == nil {
Logger.SetOutput(file)
} else {
Logger.Info("无法写入日志文件,使用标准输出")
}
}
}
// GenerateRandomData 生成指定大小的随机数据
func GenerateRandomData(size int) ([]byte, error) {
data := make([]byte, size)
_, err := rand.Read(data)
if err != nil {
return nil, err
}
return data, nil
}
// BytesToMB 将字节转换为MB
func BytesToMB(bytes int) float64 {
return float64(bytes) / (1024 * 1024)
}
// MBToBytes 将MB转换为字节
func MBToBytes(mb float64) int {
return int(mb * 1024 * 1024)
}
// FormatDuration 格式化持续时间
func FormatDuration(d time.Duration) string {
if d < time.Minute {
return fmt.Sprintf("%.2f秒", d.Seconds())
} else if d < time.Hour {
minutes := math.Floor(d.Minutes())
seconds := d.Seconds() - minutes*60
return fmt.Sprintf("%.0f分%.2f秒", minutes, seconds)
} else {
hours := math.Floor(d.Hours())
minutes := math.Floor(d.Minutes()) - hours*60
seconds := d.Seconds() - hours*3600 - minutes*60
return fmt.Sprintf("%.0f时%.0f分%.2f秒", hours, minutes, seconds)
}
}
// CalculatePercentage 计算百分比
func CalculatePercentage(part, total float64) float64 {
if total == 0 {
return 0
}
return (part / total) * 100
}
// ExecuteCommand 执行系统命令
func ExecuteCommand(command string, args ...string) (string, error) {
cmd := exec.Command(command, args...)
output, err := cmd.CombinedOutput()
if err != nil {
return string(output), err
}
return string(output), nil
}
// IsMounted 检查指定路径是否已挂载
func IsMounted(mountPoint string) bool {
output, err := ExecuteCommand("mount")
if err != nil {
return false
}
return strings.Contains(output, mountPoint)
}
// FileExists 检查文件是否存在
func FileExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}
// CreateDirIfNotExist 如果目录不存在则创建
func CreateDirIfNotExist(path string) error {
if !FileExists(path) {
return os.MkdirAll(path, 0755)
}
return nil
}
// CalculateWriteSpeed 计算写入速度
func CalculateWriteSpeed(bytesWritten int, duration time.Duration) float64 {
if duration.Seconds() == 0 {
return 0
}
return BytesToMB(bytesWritten) / duration.Seconds()
}
// CalculateIOPS 计算IOPS
func CalculateIOPS(operations int, duration time.Duration) float64 {
if duration.Seconds() == 0 {
return 0
}
return float64(operations) / duration.Seconds()
}

159
scripts/run_test.sh Executable file
View File

@ -0,0 +1,159 @@
#!/bin/bash
# 检查是否已安装Open-CAS
check_opencas() {
if ! command -v casadm &> /dev/null; then
echo "错误: Open-CAS未安装或无法在PATH中找到。"
echo "请先安装Open-CAS: https://open-cas.github.io/"
exit 1
fi
echo "Open-CAS已安装"
}
# 检查设备是否存在
check_devices() {
local nvme_device=$(grep -oP 'devices_nvme: "\K[^"]+' config.yaml)
local hdd_device=$(grep -oP 'devices_hdd: "\K[^"]+' config.yaml)
if [ ! -b "$nvme_device" ]; then
echo "错误: NVMe设备 $nvme_device 不存在或不是块设备"
exit 1
fi
if [ ! -b "$hdd_device" ]; then
echo "错误: HDD设备 $hdd_device 不存在或不是块设备"
exit 1
fi
echo "设备检查通过: $nvme_device$hdd_device 存在"
}
# 编译项目
build_project() {
echo "编译项目..."
go build -o bin/server cmd/server/main.go
go build -o bin/client cmd/client/main.go
if [ $? -ne 0 ]; then
echo "编译失败"
exit 1
fi
echo "编译成功"
}
# 启动服务器
start_server() {
echo "启动服务器..."
mkdir -p logs
./bin/server -config config.yaml -log-level info > logs/server.log 2>&1 &
SERVER_PID=$!
echo "服务器进程ID: $SERVER_PID"
echo "等待服务器启动..."
sleep 3
# 检查服务器是否正常启动
if ! ps -p $SERVER_PID > /dev/null; then
echo "服务器启动失败,请检查日志: logs/server.log"
exit 1
fi
echo "服务器已成功启动"
}
# 运行指定测试
run_test() {
local test_type=$1
local data_size=$2
local block_size=$3
echo "运行测试: $test_type (数据大小: ${data_size}MB, 块大小: ${block_size}KB)"
./bin/client -config config.yaml -test "$test_type" -data-size "$data_size" -block-size "$block_size" > "logs/client_${test_type}.log" 2>&1
if [ $? -ne 0 ]; then
echo "测试 $test_type 失败,请检查日志: logs/client_${test_type}.log"
return 1
fi
echo "测试 $test_type 完成"
return 0
}
# 运行所有测试
run_all_tests() {
local data_size=$1
local block_size=$2
local concurrent=$3
if [ "$concurrent" = "true" ]; then
echo "并发运行所有测试 (数据大小: ${data_size}MB, 块大小: ${block_size}KB)"
./bin/client -config config.yaml -test all -data-size "$data_size" -block-size "$block_size" -concurrent > logs/client_all.log 2>&1
else
echo "顺序运行所有测试 (数据大小: ${data_size}MB, 块大小: ${block_size}KB)"
./bin/client -config config.yaml -test all -data-size "$data_size" -block-size "$block_size" > logs/client_all.log 2>&1
fi
if [ $? -ne 0 ]; then
echo "运行所有测试失败,请检查日志: logs/client_all.log"
return 1
fi
echo "所有测试完成"
return 0
}
# 停止服务器
stop_server() {
if [ -n "$SERVER_PID" ]; then
echo "停止服务器 (PID: $SERVER_PID)..."
kill $SERVER_PID
wait $SERVER_PID 2>/dev/null
echo "服务器已停止"
fi
}
# 清理资源
cleanup() {
echo "清理资源..."
stop_server
# 检查是否有遗留的缓存实例
local cache_id=$(grep -oP 'cache_instance_id: "\K[^"]+' config.yaml)
if casadm -L | grep -q "Cache Instance $cache_id"; then
echo "停止缓存实例 $cache_id..."
casadm -T -i $cache_id -f
fi
echo "清理完成"
}
# 主函数
main() {
local test_type=${1:-"all"}
local data_size=${2:-100} # 默认100MB
local block_size=${3:-4} # 默认4KB
local concurrent=${4:-"false"} # 默认非并发
mkdir -p bin logs
# 注册清理函数
trap cleanup EXIT
check_opencas
check_devices
build_project
start_server
if [ "$test_type" = "all" ]; then
run_all_tests "$data_size" "$block_size" "$concurrent"
else
run_test "$test_type" "$data_size" "$block_size"
fi
echo "测试执行完毕"
}
# 执行主函数,参数: 测试类型 数据大小(MB) 块大小(KB) 是否并发
main "$@"