locust_test.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. # 导入 Locust 相关模块,用于性能测试
  2. from locust import HttpUser, task, between, events
  3. # 导入 JSON 处理模块,用于解析 WebSocket 消息
  4. import json
  5. # 导入 WebSocket 客户端,用于模拟 WebSocket 连接
  6. from websocket import create_connection
  7. # 导入时间模块,用于记录 token 响应时间
  8. import time
  9. # 导入 CSV 写入模块,用于保存测试结果
  10. import csv
  11. # 导入 UUID 模块,用于生成会话 ID
  12. import uuid
  13. # 导入线程锁模块,确保 CSV 写入线程安全
  14. from threading import Lock
  15. # 定义全局变量,用于存储 token 响应时间
  16. token_response_times = []
  17. # 定义线程锁,确保并发写入 CSV 安全
  18. csv_lock = Lock()
  19. # 定义 Locust 用户类,模拟客户端行为
  20. class QASystemUser(HttpUser):
  21. # 设置用户等待时间,模拟真实用户请求间隔(1-5秒)
  22. wait_time = between(1, 5)
  23. # 设置服务器主机地址
  24. host = "http://localhost:8000"
  25. def on_start(self):
  26. # 在用户启动时执行,初始化会话 ID
  27. self.session_id = str(uuid.uuid4())
  28. # 创建 HTTP 请求头,指定 JSON 格式
  29. self.headers = {"Content-Type": "application/json"}
  30. @task(1)
  31. def test_http_query(self):
  32. # 定义 HTTP 查询测试任务
  33. # 构造查询请求数据
  34. payload = {
  35. "query": "什么是 Python?",
  36. "source_filter": None,
  37. "session_id": self.session_id
  38. }
  39. # 发送 POST 请求到 /api/query 接口
  40. response = self.client.post("/api/query", json=payload, headers=self.headers)
  41. # 检查响应状态,确保请求成功
  42. if response.status_code == 200:
  43. # 记录请求成功
  44. self.environment.events.request.fire(
  45. request_type="POST",
  46. name="/api/query",
  47. response_time=response.elapsed.total_seconds() * 1000, # 响应时间(毫秒)
  48. response_length=len(response.text),
  49. exception=None
  50. )
  51. else:
  52. # 记录请求失败
  53. self.environment.events.request.fire(
  54. request_type="POST",
  55. name="/api/query",
  56. response_time=response.elapsed.total_seconds() * 1000,
  57. response_length=0,
  58. exception=Exception(f"HTTP {response.status_code}")
  59. )
  60. @task(2)
  61. def test_websocket_stream(self):
  62. # 定义 WebSocket 流式查询测试任务
  63. # 构造 WebSocket URL
  64. ws_url = "ws://localhost:8000/api/stream"
  65. # 创建 WebSocket 连接
  66. ws = None
  67. try:
  68. ws = create_connection(ws_url)
  69. # 构造查询请求数据
  70. payload = {
  71. "query": "AI的知识文档目录是什么?",
  72. "source_filter": None,
  73. "session_id": self.session_id
  74. }
  75. # 记录请求发送时间
  76. start_time = time.time()
  77. # 发送查询请求
  78. ws.send(json.dumps(payload))
  79. # 初始化 token 响应时间记录
  80. first_token_time = None
  81. last_token_time = start_time
  82. token_times = []
  83. while True:
  84. # 接收 WebSocket 消息
  85. message = ws.recv()
  86. # 解析消息为 JSON
  87. data = json.loads(message)
  88. # 获取当前接收时间
  89. current_time = time.time()
  90. if data["type"] == "start":
  91. # 收到开始标志,继续接收
  92. continue
  93. elif data["type"] == "token":
  94. # 收到 token 消息
  95. if first_token_time is None:
  96. # 记录首个 token 响应时间
  97. first_token_time = current_time - start_time
  98. else:
  99. # 记录后续 token 的响应时间(与上一个 token 的间隔)
  100. token_times.append(current_time - last_token_time)
  101. # 更新上一个 token 时间
  102. last_token_time = current_time
  103. elif data["type"] == "end":
  104. # 收到结束标志,处理结果
  105. processing_time = current_time - start_time
  106. # 记录 WebSocket 请求成功
  107. self.environment.events.request.fire(
  108. request_type="WebSocket",
  109. name="/api/stream",
  110. response_time=processing_time * 1000, # 总响应时间(毫秒)
  111. response_length=len(message),
  112. exception=None
  113. )
  114. # 保存 token 响应时间到全局变量
  115. with csv_lock:
  116. token_response_times.append({
  117. "session_id": self.session_id,
  118. "first_token_time": first_token_time * 1000, # 首个 token 时间(毫秒)
  119. "other_token_times": [t * 1000 for t in token_times], # 其他 token 时间(毫秒)
  120. "total_time": processing_time * 1000 # 总时间(毫秒)
  121. })
  122. break
  123. elif data["type"] == "error":
  124. # 收到错误消息,记录失败
  125. self.environment.events.request.fire(
  126. request_type="WebSocket",
  127. name="/api/stream",
  128. response_time=(current_time - start_time) * 1000,
  129. response_length=0,
  130. exception=Exception(data["error"])
  131. )
  132. break
  133. except Exception as e:
  134. # 捕获异常,记录 WebSocket 请求失败
  135. self.environment.events.request.fire(
  136. request_type="WebSocket",
  137. name="/api/stream",
  138. response_time=(time.time() - start_time) * 1000,
  139. response_length=0,
  140. exception=e
  141. )
  142. finally:
  143. # 确保 WebSocket 连接关闭
  144. if ws:
  145. ws.close()
  146. # 定义 Locust 事件监听器,保存测试结果到 CSV
  147. @events.quitting.add_listener
  148. def on_quitting(environment, **kwargs):
  149. # 在 Locust 测试结束时执行
  150. # 定义 CSV 文件路径
  151. csv_file = "token_response_times.csv"
  152. # 定义 CSV 头部
  153. headers = ["Session ID", "First Token Time (ms)", "Other Token Times (ms)", "Total Time (ms)"]
  154. # 写入测试结果到 CSV
  155. with open(csv_file, "w", newline="", encoding="utf-8") as f:
  156. writer = csv.writer(f)
  157. # 写入头部
  158. writer.writerow(headers)
  159. # 遍历 token 响应时间记录
  160. for record in token_response_times:
  161. # 写入每行数据
  162. writer.writerow([
  163. record["session_id"],
  164. f"{record['first_token_time']:.2f}",
  165. ";".join([f"{t:.2f}" for t in record["other_token_times"]]),
  166. f"{record['total_time']:.2f}"
  167. ])
  168. # 打印提示信息
  169. print(f"Token response times saved to {csv_file}")
  170. # 定义 Locust 事件监听器,打印实时统计
  171. @events.request.add_listener
  172. def on_request(request_type, name, response_time, response_length, exception, **kwargs):
  173. # 在每次请求完成时执行
  174. if exception:
  175. # 打印请求失败信息
  176. print(f"{request_type} {name} failed: {exception}")
  177. else:
  178. # 打印请求成功信息
  179. print(f"{request_type} {name} succeeded: {response_time:.2f} ms")