智慧水务管理系统 - 精河县供水工程综合管理平台

locustfile.py 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. #!/usr/bin/env python3
  2. """
  3. REST API 性能压力测试 - Locust
  4. 使用 Locust 框架对水务管理系统的 REST API 进行并发压力测试。
  5. 覆盖主要 API 端点:登录、数据查询、数据上报、报表查询等。
  6. 运行方式:
  7. locust -f locustfile.py --host=http://localhost:8080
  8. # 无头模式:
  9. locust -f locustfile.py --host=http://localhost:8080 --headless -u 100 -r 10 --run-time 5m
  10. 支持 10/50/100/500/1000 并发用户梯度测试。
  11. """
  12. import random
  13. import time
  14. import json
  15. from datetime import datetime, timedelta
  16. try:
  17. from locust import HttpUser, task, between, events, LoadTestShape
  18. except ImportError:
  19. print("请先安装 locust: pip install locust")
  20. raise SystemExit(1)
  21. # ==================== 配置 ====================
  22. # 默认 API Token(实际使用时通过环境变量覆盖)
  23. API_TOKEN = "test-token"
  24. # 模拟的管网节点 ID 范围
  25. NODE_ID_RANGE = (1, 10000)
  26. # 模拟的传感器 ID 范围
  27. SENSOR_ID_RANGE = (1, 5000)
  28. # ==================== 用户行为 ====================
  29. class WaterManagementUser(HttpUser):
  30. """模拟水务管理系统的用户行为"""
  31. wait_time = between(1, 5) # 每次请求间隔 1-5 秒
  32. def on_start(self):
  33. """用户初始化:模拟登录获取 token"""
  34. self.token = API_TOKEN
  35. self.headers = {
  36. "Authorization": f"Bearer {self.token}",
  37. "Content-Type": "application/json",
  38. }
  39. # 尝试真实登录
  40. try:
  41. with self.client.post(
  42. "/api/auth/login",
  43. json={"username": "admin", "password": "admin123"},
  44. catch_response=True,
  45. timeout=10,
  46. ) as resp:
  47. if resp.status_code == 200:
  48. data = resp.json()
  49. if data.get("data", {}).get("token"):
  50. self.token = data["data"]["token"]
  51. self.headers["Authorization"] = f"Bearer {self.token}"
  52. resp.success()
  53. else:
  54. resp.failure("No token in response")
  55. else:
  56. resp.failure(f"Login failed: {resp.status_code}")
  57. except Exception:
  58. pass # 使用默认 token
  59. # ---------- 数据查询类任务 ----------
  60. @task(10)
  61. def query_monitoring_data(self):
  62. """查询实时监测数据(高频操作)"""
  63. node_id = random.randint(*NODE_ID_RANGE)
  64. start = (datetime.now() - timedelta(hours=24)).isoformat()
  65. end = datetime.now().isoformat()
  66. self.client.get(
  67. f"/api/monitoring/data?nodeId={node_id}&startTime={start}&endTime={end}",
  68. headers=self.headers,
  69. name="/api/monitoring/data",
  70. timeout=30,
  71. )
  72. @task(5)
  73. def query_pressure_data(self):
  74. """查询管网压力数据"""
  75. area_id = random.randint(1, 100)
  76. self.client.get(
  77. f"/api/monitoring/pressure?areaId={area_id}",
  78. headers=self.headers,
  79. name="/api/monitoring/pressure",
  80. timeout=30,
  81. )
  82. @task(5)
  83. def query_flow_data(self):
  84. """查询流量数据"""
  85. sensor_id = random.randint(*SENSOR_ID_RANGE)
  86. self.client.get(
  87. f"/api/monitoring/flow?sensorId={sensor_id}",
  88. headers=self.headers,
  89. name="/api/monitoring/flow",
  90. timeout=30,
  91. )
  92. @task(3)
  93. def query_water_quality(self):
  94. """查询水质数据"""
  95. station_id = random.randint(1, 50)
  96. self.client.get(
  97. f"/api/monitoring/water-quality?stationId={station_id}",
  98. headers=self.headers,
  99. name="/api/monitoring/water-quality",
  100. timeout=30,
  101. )
  102. # ---------- 数据上报类任务 ----------
  103. @task(8)
  104. def report_sensor_data(self):
  105. """上报传感器数据(高频操作)"""
  106. payload = {
  107. "sensorId": random.randint(*SENSOR_ID_RANGE),
  108. "timestamp": datetime.now().isoformat(),
  109. "pressure": round(random.uniform(0.1, 0.8), 3),
  110. "flow": round(random.uniform(10, 500), 2),
  111. "temperature": round(random.uniform(5, 35), 1),
  112. "quality": {
  113. "turbidity": round(random.uniform(0, 5), 2),
  114. "ph": round(random.uniform(6.5, 8.5), 2),
  115. "chlorine": round(random.uniform(0.1, 0.8), 3),
  116. },
  117. }
  118. with self.client.post(
  119. "/api/data/report",
  120. json=payload,
  121. headers=self.headers,
  122. catch_response=True,
  123. name="/api/data/report",
  124. timeout=15,
  125. ) as resp:
  126. if resp.status_code in (200, 201, 401, 404):
  127. resp.success()
  128. @task(3)
  129. def report_alarm(self):
  130. """上报告警事件"""
  131. alarm_types = ["PRESSURE_HIGH", "PRESSURE_LOW", "FLOW_ABNORMAL",
  132. "QUALITY_WARNING", "LEAK_DETECTED"]
  133. payload = {
  134. "sensorId": random.randint(*SENSOR_ID_RANGE),
  135. "alarmType": random.choice(alarm_types),
  136. "severity": random.choice(["INFO", "WARNING", "CRITICAL"]),
  137. "timestamp": datetime.now().isoformat(),
  138. "description": f"自动告警 - {random.choice(alarm_types)}",
  139. "value": round(random.uniform(0, 100), 2),
  140. "threshold": round(random.uniform(50, 100), 2),
  141. }
  142. with self.client.post(
  143. "/api/alarm/report",
  144. json=payload,
  145. headers=self.headers,
  146. catch_response=True,
  147. name="/api/alarm/report",
  148. timeout=15,
  149. ) as resp:
  150. if resp.status_code in (200, 201, 401, 404):
  151. resp.success()
  152. # ---------- 报表查询类任务 ----------
  153. @task(4)
  154. def query_daily_report(self):
  155. """查询日报表"""
  156. date = (datetime.now() - timedelta(days=random.randint(0, 30))).strftime("%Y-%m-%d")
  157. self.client.get(
  158. f"/api/report/daily?date={date}",
  159. headers=self.headers,
  160. name="/api/report/daily",
  161. timeout=30,
  162. )
  163. @task(2)
  164. def query_monthly_report(self):
  165. """查询月报表"""
  166. year = datetime.now().year
  167. month = random.randint(1, 12)
  168. self.client.get(
  169. f"/api/report/monthly?year={year}&month={month}",
  170. headers=self.headers,
  171. name="/api/report/monthly",
  172. timeout=30,
  173. )
  174. @task(2)
  175. def export_report(self):
  176. """导出报表(较重量级操作)"""
  177. start = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
  178. end = datetime.now().strftime("%Y-%m-%d")
  179. with self.client.get(
  180. f"/api/report/export?startDate={start}&endDate={end}&format=xlsx",
  181. headers=self.headers,
  182. catch_response=True,
  183. name="/api/report/export",
  184. timeout=60,
  185. ) as resp:
  186. if resp.status_code in (200, 401, 404):
  187. resp.success()
  188. # ---------- 应急调度类任务 ----------
  189. @task(1)
  190. def query_emergency_list(self):
  191. """查询应急事件列表"""
  192. self.client.get(
  193. "/api/emergency/list?page=1&size=20",
  194. headers=self.headers,
  195. name="/api/emergency/list",
  196. timeout=30,
  197. )
  198. @task(1)
  199. def simulate_pipe_burst(self):
  200. """模拟爆管事件(重量级操作)"""
  201. payload = {
  202. "lng": round(random.uniform(116.0, 117.0), 4),
  203. "lat": round(random.uniform(39.5, 40.5), 4),
  204. "pipeDiameter": random.choice(["DN50", "DN100", "DN200", "DN300"]),
  205. "operatorName": f"operator_{random.randint(1, 50)}",
  206. }
  207. with self.client.post(
  208. "/api/emergency/dispatch/quick-pipe-burst",
  209. json=payload,
  210. headers=self.headers,
  211. catch_response=True,
  212. name="/api/emergency/dispatch/quick-pipe-burst",
  213. timeout=60,
  214. ) as resp:
  215. if resp.status_code in (200, 201, 401, 404):
  216. resp.success()
  217. # ---------- GIS 空间查询 ----------
  218. @task(3)
  219. def gis_query_nearby(self):
  220. """GIS 空间查询 - 附近设备"""
  221. lng = round(random.uniform(116.0, 117.0), 6)
  222. lat = round(random.uniform(39.5, 40.5), 6)
  223. radius = random.choice([500, 1000, 2000, 5000])
  224. self.client.get(
  225. f"/api/gis/nearby?lng={lng}&lat={lat}&radius={radius}",
  226. headers=self.headers,
  227. name="/api/gis/nearby",
  228. timeout=30,
  229. )
  230. @task(2)
  231. def gis_query_area_stats(self):
  232. """GIS 区域统计"""
  233. area_id = random.randint(1, 50)
  234. self.client.get(
  235. f"/api/gis/area-stats?areaId={area_id}",
  236. headers=self.headers,
  237. name="/api/gis/area-stats",
  238. timeout=30,
  239. )
  240. # ==================== 梯度负载模型 ====================
  241. class StepLoadShape(LoadTestShape):
  242. """
  243. 梯度负载测试:10 → 50 → 100 → 500 → 1000 用户
  244. 每个阶段持续 3 分钟,总共约 15 分钟
  245. """
  246. stages = [
  247. {"duration": 180, "users": 10, "spawn_rate": 5}, # 预热 10 用户
  248. {"duration": 360, "users": 50, "spawn_rate": 10}, # 50 用户
  249. {"duration": 540, "users": 100, "spawn_rate": 20}, # 100 用户
  250. {"duration": 720, "users": 500, "spawn_rate": 50}, # 500 用户
  251. {"duration": 900, "users": 1000, "spawn_rate": 100}, # 1000 用户
  252. {"duration": 1080, "users": 0, "spawn_rate": 100}, # 逐步停止
  253. ]
  254. def tick(self):
  255. run_time = self.get_run_time()
  256. for stage in self.stages:
  257. if run_time < stage["duration"]:
  258. tick_data = (
  259. stage["users"],
  260. stage["spawn_rate"],
  261. )
  262. return tick_data
  263. return None
  264. # ==================== 事件钩子:自定义统计 ====================
  265. @events.test_stop.add_listener
  266. def on_test_stop(environment, **kwargs):
  267. """测试结束时输出摘要"""
  268. stats = environment.runner.stats
  269. print("\n" + "=" * 60)
  270. print("📊 REST API 压力测试报告")
  271. print("=" * 60)
  272. print(stats.serialize_current_response_times())
  273. print("=" * 60)