init
This commit is contained in:
commit
99d441c7ac
42
.gitignore
vendored
Normal file
42
.gitignore
vendored
Normal 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
233
Makefile
Normal 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
60
README.md
Normal 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
104
SUMMARY.md
Normal 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
208
TESTPLAN.md
Normal 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
298
cmd/client/main.go
Normal 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
296
cmd/server/main.go
Normal 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
29
config.yaml
Normal 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
11
go.mod
Normal 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
19
go.sum
Normal 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
134
internal/config/config.go
Normal 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
116
internal/model/data.go
Normal 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"`
|
||||||
|
}
|
58
internal/testcase/concurrent_write-test.go
Normal file
58
internal/testcase/concurrent_write-test.go
Normal 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
|
||||||
|
}
|
58
internal/testcase/mixed_read_write-test.go
Normal file
58
internal/testcase/mixed_read_write-test.go
Normal 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
|
||||||
|
}
|
350
internal/testcase/power_loss-test.go
Normal file
350
internal/testcase/power_loss-test.go
Normal 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
|
||||||
|
}
|
276
internal/testcase/random_write-test.go
Normal file
276
internal/testcase/random_write-test.go
Normal 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
|
||||||
|
}
|
249
internal/testcase/sequential_write-test.go
Normal file
249
internal/testcase/sequential_write-test.go
Normal 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
|
||||||
|
}
|
58
internal/testcase/stability-test.go
Normal file
58
internal/testcase/stability-test.go
Normal 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
|
||||||
|
}
|
201
internal/testcase/testcase.go
Normal file
201
internal/testcase/testcase.go
Normal 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
131
internal/utils/utils.go
Normal 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
159
scripts/run_test.sh
Executable 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 "$@"
|
Loading…
Reference in New Issue
Block a user