更新,并增加测试脚本

This commit is contained in:
luoyu 2025-07-11 10:43:14 +08:00
parent 646a8a3e7a
commit 8da30dff97
11 changed files with 2493 additions and 1164 deletions

21
.idea/CopilotChatHistory.xml generated Normal file
View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CopilotChatHistory">
<option name="conversations">
<list>
<Conversation>
<option name="createTime" value="1752198826626" />
<option name="id" value="0197f7304a8271818a53d1614bfea558" />
<option name="title" value="新对话 2025年7月11日 09:53:46" />
<option name="updateTime" value="1752198826626" />
</Conversation>
<Conversation>
<option name="createTime" value="1752126143966" />
<option name="id" value="0197f2db3dde7152a54a32e9e075d479" />
<option name="title" value="新对话 2025年7月10日 13:42:23" />
<option name="updateTime" value="1752126143966" />
</Conversation>
</list>
</option>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -101,3 +101,15 @@ cd /path/to/deploy/
所有系统参数都可以通过环境变量配置,请参考 [部署指南](DEPLOY.md) 获取完整配置选项。 所有系统参数都可以通过环境变量配置,请参考 [部署指南](DEPLOY.md) 获取完整配置选项。
如需支持更大规模并发,可参考扩展策略进行水平或垂直扩展。 如需支持更大规模并发,可参考扩展策略进行水平或垂直扩展。
# 基本使用 - 从数据库获取真实数据、测试等待室和高并发
python cstest.py --concurrency 100 --total 200
# 不测试等待室,只测试并发性能
python cstest.py --no-waiting-room --concurrency 500 --total 1000
# 专注于测试等待室功能
python cstest.py --mode waiting-room --concurrency 200 --total 300
# 高强度缓存测试
python cstest.py --mode cache

58
cache_test_report.json Normal file
View File

@ -0,0 +1,58 @@
{
"总请求数": 60,
"成功请求": 60,
"失败请求": 0,
"成功率": "100.00%",
"返回结果数": 60,
"返回结果率": "100.00%",
"进入等待队列数": 0,
"等待队列率": "0.00%",
"命中缓存数": 45,
"轮询统计": {
"需要轮询请求数": 0,
"平均轮询次数": "0.00",
"最大轮询次数": 0
},
"响应时间(秒)": {
"平均": "0.0266",
"最小": "0.0067",
"最大": "0.0681",
"P50": "0.0258",
"P90": "0.0422",
"P95": "0.0490",
"P99": "0.0681"
},
"状态码分布": {
"200": 60
},
"错误消息分类": {},
"缓存效果分析": {
"第1轮": {
"平均响应时间": "0.0255秒",
"成功率": "100.00%",
"返回结果率": "100.00%",
"命中缓存数": 5,
"命中缓存率": "25.00%",
"进入等待队列数": 0,
"等待队列率": "0.00%"
},
"第2轮": {
"平均响应时间": "0.0291秒",
"成功率": "100.00%",
"返回结果率": "100.00%",
"命中缓存数": 20,
"命中缓存率": "100.00%",
"进入等待队列数": 0,
"等待队列率": "0.00%"
},
"第3轮": {
"平均响应时间": "0.0251秒",
"成功率": "100.00%",
"返回结果率": "100.00%",
"命中缓存数": 20,
"命中缓存率": "100.00%",
"进入等待队列数": 0,
"等待队列率": "0.00%"
}
}
}

View File

@ -3,10 +3,10 @@ import os
# 数据库配置 # 数据库配置
DB_CONFIG = { DB_CONFIG = {
"host": os.environ.get("DB_HOST", "192.140.160.11"), "host": os.environ.get("DB_HOST", "127.0.0.1"),
"port": int(os.environ.get("DB_PORT", "3306")), "port": int(os.environ.get("DB_PORT", "3306")),
"user": os.environ.get("DB_USER", "root"), "user": os.environ.get("DB_USER", "root"),
"password": os.environ.get("DB_PASSWORD", "Boyue123"), "password": os.environ.get("DB_PASSWORD", "123456"),
"db": os.environ.get("DB_NAME", "harsjselect"), "db": os.environ.get("DB_NAME", "harsjselect"),
"charset": "utf8mb4", "charset": "utf8mb4",
"minsize": 10, # 最小连接数 "minsize": 10, # 最小连接数
@ -16,10 +16,10 @@ DB_CONFIG = {
# Redis配置 # Redis配置
REDIS_CONFIG = { REDIS_CONFIG = {
"host": os.environ.get("REDIS_HOST", "192.140.160.11"), "host": os.environ.get("REDIS_HOST", "127.0.0.1"),
"port": int(os.environ.get("REDIS_PORT", "6379")), "port": int(os.environ.get("REDIS_PORT", "6379")),
"db": int(os.environ.get("REDIS_DB", "0")), "db": int(os.environ.get("REDIS_DB", "0")),
"password": os.environ.get("REDIS_PASSWORD", "boyue123"), "password": os.environ.get("REDIS_PASSWORD", "123456"),
"encoding": "utf-8", "encoding": "utf-8",
"pool_size": int(os.environ.get("REDIS_POOL_SIZE", "100")) # Redis连接池大小 "pool_size": int(os.environ.get("REDIS_POOL_SIZE", "100")) # Redis连接池大小
} }

522
cstest.py
View File

@ -1,37 +1,276 @@
# 文件名: concurrent_test.py # 文件名: cstest.py
import asyncio import asyncio
import aiohttp import aiohttp
import aiomysql
import time import time
import json import json
import random import random
import argparse import argparse
from collections import defaultdict from collections import defaultdict
from tqdm import tqdm from tqdm import tqdm
import sys
import os
# 测试数据生成 # 导入配置文件中的数据库设置
def generate_test_data(count): try:
from config import DB_CONFIG, REDIS_CONFIG, CACHE_EXPIRE, WAITING_ROOM_CAPACITY
print("成功导入数据库配置")
except ImportError:
print("警告: 无法导入配置文件,将使用默认配置")
# 默认数据库配置
DB_CONFIG = {
"host": "127.0.0.1",
"port": 3306,
"user": "root",
"password": "123456",
"db": "harsjselect",
"charset": "utf8mb4",
"minsize": 1,
"maxsize": 100,
"pool_recycle": 3600
}
# 等待室容量默认值
WAITING_ROOM_CAPACITY = 15000
# 直接从数据库获取真实测试数据
async def fetch_db_test_data(limit=100):
"""直接从数据库中获取有效的测试数据,不做去重处理"""
print(f"正在从数据库获取真实测试数据 (最多 {limit} 条)...")
db_pool = None
try:
# 创建数据库连接池
db_pool = await aiomysql.create_pool(
host=DB_CONFIG["host"],
port=DB_CONFIG["port"],
user=DB_CONFIG["user"],
password=DB_CONFIG["password"],
db=DB_CONFIG["db"],
charset=DB_CONFIG["charset"],
minsize=1,
maxsize=5
)
# 获取连接并查询数据
async with db_pool.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cursor:
# 查询总记录数
await cursor.execute("SELECT COUNT(*) as total_count FROM rsjcjselect")
result = await cursor.fetchone()
total_records = result['total_count']
print(f"数据库中共有 {total_records} 条记录")
# 查询表结构以确认字段名
await cursor.execute("SHOW COLUMNS FROM rsjcjselect")
columns = await cursor.fetchall()
column_names = [col['Field'] for col in columns]
# 检查必要的字段是否存在
zkzh_field = 'zkzh' if 'zkzh' in column_names else None
sfzh_field = 'sfzh' if 'sfzh' in column_names else None
if not zkzh_field or not sfzh_field:
print("警告: 数据库中缺少必要的字段 (zkzh 或 sfzh)")
return []
# 方式1: 随机抽取记录 (最推荐)
query = f"""
SELECT {zkzh_field}, {sfzh_field}
FROM rsjcjselect
WHERE {zkzh_field} IS NOT NULL AND {sfzh_field} IS NOT NULL
ORDER BY RAND()
LIMIT %s
"""
# 执行查询
await cursor.execute(query, (limit,))
rows = await cursor.fetchall()
if not rows:
print("警告: 数据库中没有找到符合条件的测试数据")
return []
# 转换为测试数据格式
test_data = [] test_data = []
# 基于系统中看到的测试数据格式 for row in rows:
for i in range(count): test_data.append({
zkzh = random.randint(2000000, 2999999) "zkzh": str(row[zkzh_field]),
sj = f"138{random.randint(10000000, 99999999)}" "sfzh": str(row[sfzh_field])
test_data.append({"zkzh": str(zkzh), "sj": sj}) })
# 记录唯一考生数量
unique_pairs = set((item["zkzh"], item["sfzh"]) for item in test_data)
print(f"成功从数据库获取 {len(test_data)} 条真实测试数据")
print(f"其中包含 {len(unique_pairs)} 个不同的考生")
return test_data return test_data
# 单个查询请求 except Exception as e:
async def query_score(session, url, data, timeout=30): print(f"从数据库获取测试数据失败: {str(e)}")
return []
finally:
# 关闭连接池
if db_pool:
db_pool.close()
await db_pool.wait_closed()
# 备用测试数据 - 从SQL文件中提取的实际数据
FALLBACK_TEST_DATA = [
{"zkzh": "101081100101", "sfzh": "320106199001015432"},
{"zkzh": "101081100102", "sfzh": "320106199203023625"},
{"zkzh": "101081100103", "sfzh": "320106199104125486"},
{"zkzh": "101081100104", "sfzh": "320106199209089635"},
{"zkzh": "101081100105", "sfzh": "320106199305075218"},
{"zkzh": "101081100201", "sfzh": "320106199407125896"},
{"zkzh": "101081100202", "sfzh": "320106199501234785"},
{"zkzh": "101081100203", "sfzh": "320106199602153695"},
{"zkzh": "101081100204", "sfzh": "320106199703126547"},
{"zkzh": "101081100301", "sfzh": "320106199804234152"},
{"zkzh": "101081100302", "sfzh": "320106199905124863"},
{"zkzh": "101081100303", "sfzh": "320106199012154789"},
{"zkzh": "101081100401", "sfzh": "320111199106152365"},
{"zkzh": "101081100402", "sfzh": "320111199207136548"},
{"zkzh": "101081100403", "sfzh": "320111199308242536"},
{"zkzh": "101081100404", "sfzh": "320111199409123568"},
{"zkzh": "101081100405", "sfzh": "320111199510256398"},
{"zkzh": "101081100406", "sfzh": "320505199611236548"},
{"zkzh": "101081100501", "sfzh": "320505199712154862"},
{"zkzh": "101081100203", "sfzh": "320106199602153695"},
{"zkzh": "101081100204", "sfzh": "320106199703126547"},
{"zkzh": "101081100301", "sfzh": "320106199804234152"},
{"zkzh": "101081100302", "sfzh": "320106199905124863"},
{"zkzh": "101081100303", "sfzh": "320106199012154789"},
{"zkzh": "101081100401", "sfzh": "320111199106152365"},
{"zkzh": "101081100402", "sfzh": "320111199207136548"},
{"zkzh": "101081100403", "sfzh": "320111199308242536"},
{"zkzh": "101081100404", "sfzh": "320111199409123568"},
{"zkzh": "101081100405", "sfzh": "320111199510256398"},
{"zkzh": "101081100406", "sfzh": "320505199611236548"},
{"zkzh": "101081100501", "sfzh": "320505199712154862"},
{"zkzh": "101081100502", "sfzh": "320505199801235674"}
]
# 扩充测试数据
def expand_test_data(base_data, target_count):
if not base_data:
print("没有真实测试数据,使用备用测试数据")
base_data = FALLBACK_TEST_DATA
result = base_data.copy()
base_count = len(base_data)
# 如果真实数据足够,直接返回
if base_count >= target_count:
return result[:target_count]
# 不足则基于已有数据生成类似格式的数据
needed = target_count - base_count
print(f"真实数据不足,基于 {base_count} 条真实数据生成 {needed} 条类似数据...")
# 获取格式模板
sample = base_data[0]
zkzh_format = sample["zkzh"]
sfzh_format = sample["sfzh"]
zkzh_length = len(zkzh_format)
sfzh_length = len(sfzh_format)
# 分析准考证号格式
zkzh_prefix = zkzh_format[:6] if zkzh_length > 6 else zkzh_format[:3]
for i in range(needed):
# 生成与真实数据格式匹配的测试数据
new_zkzh = f"{zkzh_prefix}{random.randint(100000, 999999)}"
new_zkzh = new_zkzh[:zkzh_length]
# 生成符合身份证号格式的数据
if sfzh_length == 18:
# 生成符合18位身份证规则的数据
year = random.randint(1990, 1999)
month = random.randint(1, 12)
day = random.randint(1, 28)
new_sfzh = f"320106{year}{month:02d}{day:02d}{random.randint(1000, 9999)}"
else:
new_sfzh = ''.join([str(random.randint(0, 9)) for _ in range(sfzh_length)])
result.append({"zkzh": new_zkzh, "sfzh": new_sfzh})
return result
# 单个查询请求 - 包括等待室机制和轮询处理
async def query_score_with_polling(session, base_url, data, timeout=30, max_polls=10, poll_interval=0.5):
url = f"{base_url}/api/query_score"
start_time = time.time() start_time = time.time()
try: try:
# 首次查询
async with session.post(url, json=data, timeout=timeout) as response: async with session.post(url, json=data, timeout=timeout) as response:
result = await response.json() result = await response.json()
# 检查是否需要轮询(进入等待队列)
if result.get("queue_position") is not None and result.get("estimated_wait_time") is not None:
# 进入轮询模式
queue_position = result.get("queue_position")
wait_time = result.get("estimated_wait_time")
# 轮询直到获取结果或达到最大轮询次数
polls_count = 0
while polls_count < max_polls:
# 等待一段时间
poll_delay = min(wait_time / 2, poll_interval) # 动态调整轮询间隔
await asyncio.sleep(poll_delay)
# 再次发送请求
async with session.post(url, json=data, timeout=timeout) as poll_response:
poll_result = await poll_response.json()
# 如果获取到结果或不再需要等待
if not poll_result.get("queue_position") or poll_result.get("data") is not None:
end_time = time.time()
return {
"status_code": poll_response.status,
"response_time": end_time - start_time, # 总响应时间包括轮询等待时间
"success": poll_result.get("success", False),
"message": poll_result.get("message", ""),
"in_queue": False, # 已经出队
"queue_position": None,
"cached": "来自缓存" in poll_result.get("message", ""),
"data": data,
"has_result": poll_result.get("data") is not None,
"polls": polls_count + 1 # 记录总共轮询次数
}
# 更新计数和队列位置
polls_count += 1
# 轮询超过最大次数,返回最终状态
end_time = time.time()
return {
"status_code": 408, # 请求超时
"response_time": end_time - start_time,
"success": False,
"message": "等待队列轮询超时",
"in_queue": True,
"queue_position": queue_position,
"cached": False,
"data": data,
"has_result": False,
"polls": polls_count
}
else:
# 不需要轮询,直接返回结果
end_time = time.time() end_time = time.time()
return { return {
"status_code": response.status, "status_code": response.status,
"response_time": end_time - start_time, "response_time": end_time - start_time,
"success": result.get("success", False), "success": result.get("success", False),
"message": result.get("message", ""), "message": result.get("message", ""),
"in_queue": "queue_position" in result, "in_queue": False,
"queue_position": result.get("queue_position", None) "queue_position": None,
"cached": "来自缓存" in result.get("message", ""),
"data": data,
"has_result": result.get("data") is not None,
"polls": 0
} }
except asyncio.TimeoutError: except asyncio.TimeoutError:
return { return {
@ -40,7 +279,10 @@ async def query_score(session, url, data, timeout=30):
"success": False, "success": False,
"message": "请求超时", "message": "请求超时",
"in_queue": False, "in_queue": False,
"queue_position": None "queue_position": None,
"data": data,
"has_result": False,
"polls": 0
} }
except Exception as e: except Exception as e:
return { return {
@ -49,70 +291,125 @@ async def query_score(session, url, data, timeout=30):
"success": False, "success": False,
"message": f"发生错误: {str(e)}", "message": f"发生错误: {str(e)}",
"in_queue": False, "in_queue": False,
"queue_position": None "queue_position": None,
"data": data,
"has_result": False,
"polls": 0
} }
# 轮询状态(如果被放入等待队列) # 强制使系统进入等待室模式
async def poll_status(session, base_url, task_id, max_attempts=20, interval=1): async def force_waiting_room_mode(session, base_url):
status_url = f"{base_url}/api/query_status/{task_id}" """发送大量请求迫使系统进入等待室模式"""
for attempt in range(max_attempts):
try:
async with session.get(status_url) as response:
result = await response.json()
if result.get("success") and "data" in result:
return {"success": True, "data": result["data"], "attempts": attempt + 1}
elif not result.get("in_progress", True):
return {"success": False, "message": result.get("message", "查询失败"), "attempts": attempt + 1}
await asyncio.sleep(interval)
except Exception:
await asyncio.sleep(interval)
return {"success": False, "message": "轮询超时", "attempts": max_attempts}
# 并发测试主函数
async def run_concurrent_test(base_url, concurrency, total_requests, delay=0):
url = f"{base_url}/api/query_score" url = f"{base_url}/api/query_score"
test_data = generate_test_data(total_requests)
print("正在尝试激活等待室模式...")
tasks = []
# 创建足够多的并发请求以激活等待室
batch_size = 200
for i in range(batch_size):
data = {"zkzh": f"10{random.randint(10000000, 99999999)}",
"sfzh": f"32010619{random.randint(90, 99)}{random.randint(1, 12):02d}{random.randint(1, 28):02d}{random.randint(1000, 9999)}"}
task = asyncio.create_task(session.post(url, json=data))
tasks.append(task)
# 等待所有请求完成
for future in asyncio.as_completed(tasks):
try:
await future
except:
pass
print(f"已发送 {batch_size} 个请求以激活等待室机制")
# 测试缓存效果
async def test_cache_effect(session, base_url, test_data, repeat=3):
url = f"{base_url}/api/query_score"
cache_results = []
# 第一轮查询 - 应该全部未命中缓存
print("\n===== 第1轮查询(无缓存) =====")
round1_results = []
for i, data in enumerate(tqdm(test_data, desc="第1轮查询")):
result = await query_score_with_polling(session, base_url, data)
round1_results.append(result)
await asyncio.sleep(0.05) # 降低查询速率
cache_results.append(round1_results)
# 后续轮查询 - 应该命中缓存,响应更快
for r in range(2, repeat+1):
print(f"\n===== 第{r}轮查询(应命中缓存) =====")
round_results = []
for i, data in enumerate(tqdm(test_data, desc=f"{r}轮查询")):
result = await query_score_with_polling(session, base_url, data)
round_results.append(result)
await asyncio.sleep(0.05) # 降低查询速率
cache_results.append(round_results)
return cache_results
# 并发测试主函数 - 包括等待室测试
async def run_concurrent_test(base_url, concurrency, total_requests, delay=0, test_data=None, test_waiting_room=True):
results = [] results = []
# 创建一个共享的 ClientSession
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
# 如果需要测试等待室,先激活等待室模式
if test_waiting_room:
await force_waiting_room_mode(session, base_url)
# 准备测试数据
if test_data is None:
test_data = await fetch_db_test_data(total_requests)
test_data = expand_test_data(test_data, total_requests)
print(f"准备发送 {len(test_data)} 条测试请求...")
# 分批次发送请求 # 分批次发送请求
tasks = [] tasks = []
for i, data in enumerate(test_data): for i, data in enumerate(test_data):
if i > 0 and i % concurrency == 0: if i > 0 and i % concurrency == 0:
await asyncio.sleep(delay) await asyncio.sleep(delay)
task = asyncio.create_task(query_score(session, url, data)) task = asyncio.create_task(query_score_with_polling(session, base_url, data))
tasks.append(task) tasks.append(task)
# 使用tqdm显示进度条 # 使用tqdm显示进度条
for future in tqdm(asyncio.as_completed(tasks), total=len(tasks), desc="发送请求"): for future in tqdm(asyncio.as_completed(tasks), total=len(tasks), desc="处理请求"):
result = await future result = await future
results.append(result) results.append(result)
return results return results
# 生成测试报告 # 生成测试报告 - 增加等待室和轮询相关指标
def generate_report(results): def generate_report(results, include_cache_analysis=False, rounds=None):
total = len(results) total = len(results)
success_count = sum(1 for r in results if r["success"]) success_count = sum(1 for r in results if r["success"])
failure_count = total - success_count failure_count = total - success_count
in_queue_count = sum(1 for r in results if r["in_queue"]) in_queue_count = sum(1 for r in results if r["in_queue"])
cached_count = sum(1 for r in results if r.get("cached", False))
has_result_count = sum(1 for r in results if r.get("has_result", False))
# 轮询相关统计
polled_requests = [r for r in results if r.get("polls", 0) > 0]
avg_polls = sum(r.get("polls", 0) for r in results) / len(polled_requests) if polled_requests else 0
max_polls = max((r.get("polls", 0) for r in results), default=0)
response_times = [r["response_time"] for r in results] response_times = [r["response_time"] for r in results]
avg_response_time = sum(response_times) / len(response_times) avg_response_time = sum(response_times) / len(response_times) if response_times else 0
min_response_time = min(response_times) min_response_time = min(response_times) if response_times else 0
max_response_time = max(response_times) max_response_time = max(response_times) if response_times else 0
# 计算各百分位响应时间 # 计算各百分位响应时间
if response_times:
response_times.sort() response_times.sort()
p50 = response_times[int(total * 0.5)] p50 = response_times[int(total * 0.5)] if total > 0 else 0
p90 = response_times[int(total * 0.9)] p90 = response_times[int(total * 0.9)] if total > 0 else 0
p95 = response_times[int(total * 0.95)] p95 = response_times[int(total * 0.95)] if total > 0 else 0
p99 = response_times[int(total * 0.99)] p99 = response_times[int(total * 0.99)] if total > 0 else 0
else:
p50 = p90 = p95 = p99 = 0
# 状态码分布 # 状态码分布
status_codes = defaultdict(int) status_codes = defaultdict(int)
@ -129,8 +426,17 @@ def generate_report(results):
"总请求数": total, "总请求数": total,
"成功请求": success_count, "成功请求": success_count,
"失败请求": failure_count, "失败请求": failure_count,
"成功率": f"{(success_count/total*100):.2f}%", "成功率": f"{(success_count/total*100):.2f}%" if total > 0 else "0%",
"返回结果数": has_result_count,
"返回结果率": f"{(has_result_count/total*100):.2f}%" if total > 0 else "0%",
"进入等待队列数": in_queue_count, "进入等待队列数": in_queue_count,
"等待队列率": f"{(in_queue_count/total*100):.2f}%" if total > 0 else "0%",
"命中缓存数": cached_count,
"轮询统计": {
"需要轮询请求数": len(polled_requests),
"平均轮询次数": f"{avg_polls:.2f}",
"最大轮询次数": max_polls
},
"响应时间(秒)": { "响应时间(秒)": {
"平均": f"{avg_response_time:.4f}", "平均": f"{avg_response_time:.4f}",
"最小": f"{min_response_time:.4f}", "最小": f"{min_response_time:.4f}",
@ -144,54 +450,140 @@ def generate_report(results):
"错误消息分类": dict(error_messages) "错误消息分类": dict(error_messages)
} }
# 缓存效果分析
if include_cache_analysis and rounds:
cache_analysis = {}
for i, round_results in enumerate(rounds):
round_avg_time = sum(r["response_time"] for r in round_results) / len(round_results) if round_results else 0
round_success = sum(1 for r in round_results if r["success"])
round_has_result = sum(1 for r in round_results if r.get("has_result", False))
round_cached = sum(1 for r in round_results if r.get("cached", False))
round_in_queue = sum(1 for r in round_results if r.get("in_queue", False))
cache_analysis[f"{i+1}"] = {
"平均响应时间": f"{round_avg_time:.4f}",
"成功率": f"{(round_success/len(round_results)*100):.2f}%" if round_results else "0%",
"返回结果率": f"{(round_has_result/len(round_results)*100):.2f}%" if round_results else "0%",
"命中缓存数": round_cached,
"命中缓存率": f"{(round_cached/len(round_results)*100):.2f}%" if round_results else "0%",
"进入等待队列数": round_in_queue,
"等待队列率": f"{(round_in_queue/len(round_results)*100):.2f}%" if round_results else "0%",
}
report["缓存效果分析"] = cache_analysis
return report return report
# 清除缓存,用于测试
async def clear_cache(session, base_url):
try:
async with session.post(f"{base_url}/api/clear_cache") as response:
result = await response.json()
return result.get("success", False), result.get("message", "未知结果")
except Exception as e:
return False, f"清除缓存失败: {str(e)}"
async def main(): async def main():
parser = argparse.ArgumentParser(description="成绩查询系统并发测试工具") parser = argparse.ArgumentParser(description="江苏省人事考试成绩查询系统并发性能测试工具")
parser.add_argument("--url", default="http://localhost:8000", help="API基础URL") parser.add_argument("--url", default="http://127.0.0.1:80", help="API基础URL")
parser.add_argument("--concurrency", type=int, default=100, help="并发请求数") parser.add_argument("--concurrency", type=int, default=100, help="并发请求数")
parser.add_argument("--total", type=int, default=1000, help="总请求数") parser.add_argument("--total", type=int, default=1000, help="总请求数")
parser.add_argument("--delay", type=float, default=0.1, help="批次间延迟(秒)") parser.add_argument("--delay", type=float, default=0.1, help="批次间延迟(秒)")
parser.add_argument("--output", default="test_report.json", help="报告输出文件") parser.add_argument("--output", default="test_report.json", help="报告输出文件")
parser.add_argument("--mode", choices=["concurrent", "cache", "waiting-room", "all"], default="all",
help="测试模式: concurrent=并发测试, cache=缓存测试, waiting-room=等待室测试, all=全部测试")
parser.add_argument("--no-waiting-room", action="store_true", help="跳过等待室测试")
args = parser.parse_args() args = parser.parse_args()
print(f"开始并发测试: URL={args.url}, 并发数={args.concurrency}, 总请求数={args.total}") print(f"成绩查询系统高并发性能测试工具 - 连接到 {args.url}")
# 首先从数据库获取测试数据
test_data = await fetch_db_test_data(args.total)
if not test_data:
print("警告: 无法从数据库获取测试数据,将使用备用数据")
# 确保测试数据量足够
test_data = expand_test_data(test_data, args.total)
# 创建会话
async with aiohttp.ClientSession() as session:
if args.mode in ["concurrent", "waiting-room", "all"]:
print(f"\n开始并发测试: URL={args.url}, 并发数={args.concurrency}, 总请求数={args.total}")
start_time = time.time() start_time = time.time()
# 先清除缓存
clear_success, clear_message = await clear_cache(session, args.url)
print(f"清除缓存: {clear_message}")
# 运行并发测试
results = await run_concurrent_test( results = await run_concurrent_test(
args.url, args.url,
args.concurrency, args.concurrency,
args.total, args.total,
args.delay args.delay,
test_data,
test_waiting_room=not args.no_waiting_room
) )
end_time = time.time() end_time = time.time()
total_time = end_time - start_time total_time = end_time - start_time
# 生成报告
report = generate_report(results) report = generate_report(results)
report["总测试时间(秒)"] = f"{total_time:.2f}" report["总测试时间(秒)"] = f"{total_time:.2f}"
report["每秒请求数(RPS)"] = f"{args.total/total_time:.2f}" report["每秒请求数(RPS)"] = f"{args.total/total_time:.2f}" if total_time > 0 else "0"
# 保存报告到文件 # 保存报告
with open(args.output, 'w', encoding='utf-8') as f: with open(args.output, 'w', encoding='utf-8') as f:
json.dump(report, f, ensure_ascii=False, indent=2) json.dump(report, f, ensure_ascii=False, indent=2)
print(f"测试完成! 总耗时: {total_time:.2f}")
print(f"报告已保存至: {args.output}")
# 打印关键指标 # 打印关键指标
print("\n关键性能指标:") print("\n== 并发测试结果 ==")
print(f"总请求数: {report['总请求数']}") print(f"总请求数: {report['总请求数']}")
print(f"成功率: {report['成功率']}") print(f"成功率: {report['成功率']}")
print(f"返回结果率: {report['返回结果率']}")
print(f"进入等待队列数: {report['进入等待队列数']} ({report['等待队列率']})")
print(f"平均响应时间: {report['响应时间(秒)']['平均']}") print(f"平均响应时间: {report['响应时间(秒)']['平均']}")
print(f"RPS: {report['每秒请求数(RPS)']}请求/秒") print(f"RPS: {report['每秒请求数(RPS)']}请求/秒")
# 如果有轮询,显示轮询统计
if report["轮询统计"]["需要轮询请求数"] > 0:
print(f"需要轮询请求数: {report['轮询统计']['需要轮询请求数']}")
print(f"平均轮询次数: {report['轮询统计']['平均轮询次数']}")
print(f"最大轮询次数: {report['轮询统计']['最大轮询次数']}")
print(f"详细报告已保存至: {args.output}")
if args.mode in ["cache", "all"]:
print("\n开始缓存效果测试...")
# 先清除缓存
clear_success, clear_message = await clear_cache(session, args.url)
print(f"清除缓存: {clear_message}")
# 使用部分测试数据进行缓存测试
cache_test_count = min(20, len(test_data))
cache_test_data = test_data[:cache_test_count]
# 测试缓存效果
cache_results = await test_cache_effect(session, args.url, cache_test_data, repeat=3)
# 生成缓存效果报告
cache_report = generate_report([item for sublist in cache_results for item in sublist], True, cache_results)
# 保存缓存报告
cache_report_file = "cache_" + args.output
with open(cache_report_file, 'w', encoding='utf-8') as f:
json.dump(cache_report, f, ensure_ascii=False, indent=2)
# 打印缓存效果
print("\n== 缓存效果测试结果 ==")
if "缓存效果分析" in cache_report:
for round_name, round_data in cache_report["缓存效果分析"].items():
print(f"{round_name}: 平均响应时间={round_data['平均响应时间']}, 命中缓存率={round_data['命中缓存率']}")
print(f"详细缓存报告已保存至: {cache_report_file}")
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())
# # 默认本地测试
# python concurrent_test.py
# # 指定URL和并发参数
# python concurrent_test.py --url http://192.168.0.46:8000 --concurrency 200 --total 2000

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header text-center py-3"> <div class="card-header text-center py-3">
<h2>成绩查询系统</h2> <h2>成绩查询系统</h2>
<p class="text-center text-info mb-0">请输入准考证号和身份证号验证查询成绩</p>
</div> </div>
<div class="card-body p-4"> <div class="card-body p-4">
<form id="queryForm"> <form id="queryForm">
@ -20,8 +21,8 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="sj" class="form-label">密码</label> <label for="sfzh" class="form-label">身份证号</label>
<input type="password" class="form-control" id="sj"> <input type="text" class="form-control" id="sfzh" required>
</div> </div>
<div class="d-grid"> <div class="d-grid">
@ -74,6 +75,10 @@
<th>手机号</th> <th>手机号</th>
<td id="result-sjh"></td> <td id="result-sjh"></td>
</tr> </tr>
<tr>
<th>身份证号</th>
<td id="result-sfzh"></td>
</tr>
<tr> <tr>
<th>单位名称</th> <th>单位名称</th>
<td id="result-dwmc"></td> <td id="result-dwmc"></td>
@ -102,7 +107,7 @@
<th>排名</th> <th>排名</th>
<td id="result-pm"></td> <td id="result-pm"></td>
</tr> </tr>
<tr> <tr id="bz-row" style="display: none;">
<th>备注</th> <th>备注</th>
<td id="result-bz"></td> <td id="result-bz"></td>
</tr> </tr>
@ -133,7 +138,7 @@
// 获取表单数据 // 获取表单数据
const zkzh = document.getElementById('zkzh').value.trim(); const zkzh = document.getElementById('zkzh').value.trim();
const pwd = document.getElementById('sj').value.trim(); // 使用 sj 字段 const sfzh = document.getElementById('sfzh').value.trim(); // 使用身份证号字段
// 验证表单 // 验证表单
if (!zkzh) { if (!zkzh) {
@ -141,8 +146,8 @@
return; return;
} }
if (!pwd) { if (!sfzh) {
showError('请输入密码'); showError('请输入身份证号');
return; return;
} }
@ -155,10 +160,10 @@
loadingIndicator.style.display = 'block'; loadingIndicator.style.display = 'block';
try { try {
// 准备请求数据 - 确保准考证号为字符串 // 准备请求数据 - 确保准考证号和身份证号为字符串
const queryData = { const queryData = {
zkzh: zkzh.toString(), zkzh: zkzh.toString(),
sj: pwd.toString() // 添加手机号作为密码 sfzh: sfzh.toString() // 添加身份证号作为验证
}; };
// 调试输出请求数据 // 调试输出请求数据
@ -219,6 +224,7 @@
document.getElementById('result-xm').textContent = data.xm || '--'; document.getElementById('result-xm').textContent = data.xm || '--';
document.getElementById('result-zkzh').textContent = data.zkzh || '--'; document.getElementById('result-zkzh').textContent = data.zkzh || '--';
document.getElementById('result-sjh').textContent = data.sjh || '--'; document.getElementById('result-sjh').textContent = data.sjh || '--';
document.getElementById('result-sfzh').textContent = data.sfzh || '--';
document.getElementById('result-dwmc').textContent = data.dwmc || '--'; document.getElementById('result-dwmc').textContent = data.dwmc || '--';
document.getElementById('result-zwmc').textContent = data.zwmc || '--'; document.getElementById('result-zwmc').textContent = data.zwmc || '--';
document.getElementById('result-kdmc').textContent = data.kdmc || '--'; document.getElementById('result-kdmc').textContent = data.kdmc || '--';
@ -242,7 +248,15 @@
} }
document.getElementById('result-pm').textContent = data.pm || '--'; document.getElementById('result-pm').textContent = data.pm || '--';
document.getElementById('result-bz').textContent = data.bz || '--';
// 处理备注字段,有数据时显示,没有数据时隐藏
const bzRow = document.getElementById('bz-row');
if (data.bz && data.bz.trim() !== '') {
document.getElementById('result-bz').textContent = data.bz;
bzRow.style.display = '';
} else {
bzRow.style.display = 'none';
}
resultContainer.style.display = 'block'; resultContainer.style.display = 'block';
} }
@ -285,10 +299,10 @@
// 在实际应用中,应该调用 /api/query_status/{taskId} 获取结果 // 在实际应用中,应该调用 /api/query_status/{taskId} 获取结果
// 这里直接再次调用查询接口 // 这里直接再次调用查询接口
const zkzhVal = document.getElementById('zkzh').value.trim(); const zkzhVal = document.getElementById('zkzh').value.trim();
const pwdVal = document.getElementById('sj').value.trim(); // 使用 sj 字段 const sfzhVal = document.getElementById('sfzh').value.trim(); // 使用身份证号字段
const queryData = { const queryData = {
zkzh: zkzhVal.toString(), zkzh: zkzhVal.toString(),
sj: pwdVal // 添加手机号作为密码 sfzh: sfzhVal.toString() // 添加身份证号作为验证
}; };
const response = await fetch('/api/query_score', { const response = await fetch('/api/query_score', {

19
main.py
View File

@ -286,7 +286,7 @@ app.add_middleware(
# 定义查询请求模型 # 定义查询请求模型
class ScoreQuery(BaseModel): class ScoreQuery(BaseModel):
zkzh: str # 准考证号 zkzh: str # 准考证号
sj: Optional[str] = None # 手机号作为密码验证 sfzh: Optional[str] = None # 身份证号作为验证
# 定义查询响应模型 # 定义查询响应模型
class ScoreResponse(BaseModel): class ScoreResponse(BaseModel):
@ -444,8 +444,8 @@ async def query_score_from_db(query: ScoreQuery):
# 构建缓存键 # 构建缓存键
cache_key = f"score:{zkzh_val}" cache_key = f"score:{zkzh_val}"
if query.sj: # 使用sj字段进行缓存 if query.sfzh: # 使用身份证号字段进行缓存
cache_key += f":sj:{query.sj}" cache_key += f":sfzh:{query.sfzh}"
# 尝试从缓存获取数据 # 尝试从缓存获取数据
if redis: if redis:
@ -485,16 +485,16 @@ async def query_score_from_db(query: ScoreQuery):
# 打印查询参数 # 打印查询参数
print("查询条件:", conditions, "参数:", params) print("查询条件:", conditions, "参数:", params)
# 使用sjh字段进行验证 (密码应该匹配手机号字段) # 使用sfzh字段进行验证 (使用身份证号验证)
if query.sj: if query.sfzh:
conditions.append("sjh = %s") conditions.append("sfzh = %s")
params.append(query.sj) params.append(query.sfzh)
else: else:
raise HTTPException(status_code=400, detail="必须提供手机号作为验证条件") raise HTTPException(status_code=400, detail="必须提供身份证号作为验证条件")
# 构建SQL查询 - 获取考生信息 # 构建SQL查询 - 获取考生信息
sql = f""" sql = f"""
SELECT id, dwdm, dwmc, zwdm, zwmc, zprs, kdmc, kch, zwh, zkzh, xm, sjh, bscj, pm, bz SELECT id, dwdm, dwmc, zwdm, zwmc, zprs, kdmc, kch, zwh, zkzh, xm, sjh, sfzh, bscj, pm, bz
FROM rsjcjselect FROM rsjcjselect
WHERE {' AND '.join(conditions)} WHERE {' AND '.join(conditions)}
LIMIT 1 LIMIT 1
@ -517,6 +517,7 @@ async def query_score_from_db(query: ScoreQuery):
"zkzh": result["zkzh"], "zkzh": result["zkzh"],
"xm": result["xm"], "xm": result["xm"],
"sjh": result["sjh"], "sjh": result["sjh"],
"sfzh": result["sfzh"],
"dwmc": result["dwmc"], "dwmc": result["dwmc"],
"zwmc": result["zwmc"], "zwmc": result["zwmc"],
"kdmc": result["kdmc"], "kdmc": result["kdmc"],

View File

@ -1,18 +1,18 @@
#!/bin/bash #!/bin/bash
# 加载环境变量 # 加载环境变量
export DB_HOST=${DB_HOST:-"192.140.160.11"} export DB_HOST=${DB_HOST:-"127.0.0.1"}
export DB_PORT=${DB_PORT:-"3306"} export DB_PORT=${DB_PORT:-"3306"}
export DB_USER=${DB_USER:-"root"} export DB_USER=${DB_USER:-"root"}
export DB_PASSWORD=${DB_PASSWORD:-"Boyue123"} export DB_PASSWORD=${DB_PASSWORD:-"123456"}
export DB_NAME=${DB_NAME:-"harsjselect"} export DB_NAME=${DB_NAME:-"harsjselect"}
export DB_POOL_SIZE=${DB_POOL_SIZE:-"100"} export DB_POOL_SIZE=${DB_POOL_SIZE:-"100"}
export DB_MAX_CONN=${DB_MAX_CONN:-"200"} export DB_MAX_CONN=${DB_MAX_CONN:-"200"}
export REDIS_HOST=${REDIS_HOST:-"192.140.160.11"} export REDIS_HOST=${REDIS_HOST:-"127.0.0.1"}
export REDIS_PORT=${REDIS_PORT:-"6379"} export REDIS_PORT=${REDIS_PORT:-"6379"}
export REDIS_DB=${REDIS_DB:-"0"} export REDIS_DB=${REDIS_DB:-"0"}
export REDIS_PASSWORD=${REDIS_PASSWORD:-"boyue123"} export REDIS_PASSWORD=${REDIS_PASSWORD:-"123456"}
export REDIS_POOL_SIZE=${REDIS_POOL_SIZE:-"100"} export REDIS_POOL_SIZE=${REDIS_POOL_SIZE:-"100"}
export CACHE_EXPIRE=${CACHE_EXPIRE:-"3600"} export CACHE_EXPIRE=${CACHE_EXPIRE:-"3600"}

33
test_report.json Normal file
View File

@ -0,0 +1,33 @@
{
"总请求数": 2000,
"成功请求": 1827,
"失败请求": 173,
"成功率": "91.35%",
"返回结果数": 1827,
"返回结果率": "91.35%",
"进入等待队列数": 0,
"等待队列率": "0.00%",
"命中缓存数": 1729,
"轮询统计": {
"需要轮询请求数": 0,
"平均轮询次数": "0.00",
"最大轮询次数": 0
},
"响应时间(秒)": {
"平均": "0.5478",
"最小": "0.1806",
"最大": "0.9317",
"P50": "0.5507",
"P90": "0.8548",
"P95": "0.8884",
"P99": "0.9258"
},
"状态码分布": {
"200": 2000
},
"错误消息分类": {
"未找到匹配的成绩记录": 173
},
"总测试时间(秒)": "2.27",
"每秒请求数(RPS)": "880.59"
}