commit 99d441c7ac8432e2826dd70bea94b9b516115cda Author: Netlops Date: Tue Apr 22 16:53:06 2025 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e415227 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..37769a4 --- /dev/null +++ b/Makefile @@ -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" \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..467e34e --- /dev/null +++ b/README.md @@ -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. 长期稳定性测试 + +详细测试指标和结果分析请参阅测试报告。 \ No newline at end of file diff --git a/SUMMARY.md b/SUMMARY.md new file mode 100644 index 0000000..6579f31 --- /dev/null +++ b/SUMMARY.md @@ -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数据完整性的框架。通过系统化的测试,可以量化评估存储系统在断电场景下的数据保护能力,帮助用户在关键业务场景中做出更明智的存储选择。 \ No newline at end of file diff --git a/TESTPLAN.md b/TESTPLAN.md new file mode 100644 index 0000000..aaf1794 --- /dev/null +++ b/TESTPLAN.md @@ -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方法,以确保测试的一致性和可比性。 \ No newline at end of file diff --git a/cmd/client/main.go b/cmd/client/main.go new file mode 100644 index 0000000..f314a22 --- /dev/null +++ b/cmd/client/main.go @@ -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("所有测试已完成") +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..7e414c0 --- /dev/null +++ b/cmd/server/main.go @@ -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("服务器已优雅关闭") +} diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..607308b --- /dev/null +++ b/config.yaml @@ -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" \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..57fcd7d --- /dev/null +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..726fc6d --- /dev/null +++ b/go.sum @@ -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= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..f7b06e0 --- /dev/null +++ b/internal/config/config.go @@ -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) +} diff --git a/internal/model/data.go b/internal/model/data.go new file mode 100644 index 0000000..f06d3e0 --- /dev/null +++ b/internal/model/data.go @@ -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"` +} diff --git a/internal/testcase/concurrent_write-test.go b/internal/testcase/concurrent_write-test.go new file mode 100644 index 0000000..a2ce10b --- /dev/null +++ b/internal/testcase/concurrent_write-test.go @@ -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 +} diff --git a/internal/testcase/mixed_read_write-test.go b/internal/testcase/mixed_read_write-test.go new file mode 100644 index 0000000..81c2fab --- /dev/null +++ b/internal/testcase/mixed_read_write-test.go @@ -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 +} diff --git a/internal/testcase/power_loss-test.go b/internal/testcase/power_loss-test.go new file mode 100644 index 0000000..b2ddfe4 --- /dev/null +++ b/internal/testcase/power_loss-test.go @@ -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 +} diff --git a/internal/testcase/random_write-test.go b/internal/testcase/random_write-test.go new file mode 100644 index 0000000..1db8630 --- /dev/null +++ b/internal/testcase/random_write-test.go @@ -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 +} diff --git a/internal/testcase/sequential_write-test.go b/internal/testcase/sequential_write-test.go new file mode 100644 index 0000000..bc8785d --- /dev/null +++ b/internal/testcase/sequential_write-test.go @@ -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 +} diff --git a/internal/testcase/stability-test.go b/internal/testcase/stability-test.go new file mode 100644 index 0000000..bd9f2bb --- /dev/null +++ b/internal/testcase/stability-test.go @@ -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 +} diff --git a/internal/testcase/testcase.go b/internal/testcase/testcase.go new file mode 100644 index 0000000..ef7e17e --- /dev/null +++ b/internal/testcase/testcase.go @@ -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 +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 0000000..ca11f1d --- /dev/null +++ b/internal/utils/utils.go @@ -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() +} diff --git a/scripts/run_test.sh b/scripts/run_test.sh new file mode 100755 index 0000000..eb5327e --- /dev/null +++ b/scripts/run_test.sh @@ -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 "$@" \ No newline at end of file