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

websocket_stress.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. #!/usr/bin/env python3
  2. """
  3. WebSocket 长连接并发压力测试
  4. 使用 asyncio + websockets 模拟大量并发 WebSocket 连接,
  5. 测试连接成功率、消息延迟、断连率及服务器资源消耗。
  6. 运行方式:
  7. python websocket_stress.py [--host HOST] [--port PORT] [--clients N] [--duration S]
  8. 示例:
  9. python websocket_stress.py --clients 1000 --duration 300
  10. """
  11. import asyncio
  12. import argparse
  13. import json
  14. import time
  15. import random
  16. import statistics
  17. import resource
  18. import sys
  19. from datetime import datetime
  20. from collections import defaultdict
  21. try:
  22. import websockets
  23. from websockets.asyncio.client import connect as ws_connect
  24. except ImportError:
  25. try:
  26. import websockets
  27. from websockets import connect as ws_connect
  28. except ImportError:
  29. print("请先安装 websockets: pip install websockets")
  30. sys.exit(1)
  31. # ==================== 配置 ====================
  32. DEFAULT_HOST = "localhost"
  33. DEFAULT_PORT = 8765
  34. DEFAULT_CLIENTS = 100
  35. DEFAULT_DURATION = 120 # 秒
  36. DEFAULT_MESSAGE_INTERVAL = 2 # 秒
  37. WS_PATH = "/ws"
  38. # 梯度测试配置
  39. STEP_CONFIGS = [
  40. {"clients": 100, "duration": 60},
  41. {"clients": 500, "duration": 60},
  42. {"clients": 1000, "duration": 60},
  43. {"clients": 5000, "duration": 60},
  44. ]
  45. # ==================== 统计 ====================
  46. class Stats:
  47. """线程安全的统计收集器"""
  48. def __init__(self):
  49. self.connected = 0
  50. self.disconnected = 0
  51. self.messages_sent = 0
  52. self.messages_received = 0
  53. self.errors = 0
  54. self.latencies = []
  55. self.connect_times = []
  56. self._lock = asyncio.Lock()
  57. async def record_connect(self, connect_time_ms):
  58. async with self._lock:
  59. self.connected += 1
  60. self.connect_times.append(connect_time_ms)
  61. async def record_disconnect(self):
  62. async with self._lock:
  63. self.disconnected += 1
  64. async def record_send(self):
  65. async with self._lock:
  66. self.messages_sent += 1
  67. async def record_receive(self, latency_ms):
  68. async with self._lock:
  69. self.messages_received += 1
  70. self.latencies.append(latency_ms)
  71. async def record_error(self):
  72. async with self._lock:
  73. self.errors += 1
  74. def summary(self):
  75. total_attempted = self.connected + self.errors
  76. success_rate = (self.connected / total_attempted * 100) if total_attempted > 0 else 0
  77. latency_stats = {}
  78. if self.latencies:
  79. latency_stats = {
  80. "min_ms": round(min(self.latencies), 2),
  81. "max_ms": round(max(self.latencies), 2),
  82. "avg_ms": round(statistics.mean(self.latencies), 2),
  83. "median_ms": round(statistics.median(self.latencies), 2),
  84. "p95_ms": round(sorted(self.latencies)[int(len(self.latencies) * 0.95)], 2)
  85. if len(self.latencies) > 1 else round(self.latencies[0], 2),
  86. "p99_ms": round(sorted(self.latencies)[int(len(self.latencies) * 0.99)], 2)
  87. if len(self.latencies) > 1 else round(self.latencies[0], 2),
  88. }
  89. connect_time_stats = {}
  90. if self.connect_times:
  91. connect_time_stats = {
  92. "min_ms": round(min(self.connect_times), 2),
  93. "avg_ms": round(statistics.mean(self.connect_times), 2),
  94. "max_ms": round(max(self.connect_times), 2),
  95. }
  96. return {
  97. "total_connections": total_attempted,
  98. "successful_connections": self.connected,
  99. "failed_connections": self.errors,
  100. "connection_success_rate": f"{success_rate:.1f}%",
  101. "disconnections": self.disconnected,
  102. "messages_sent": self.messages_sent,
  103. "messages_received": self.messages_received,
  104. "latency": latency_stats,
  105. "connect_time": connect_time_stats,
  106. }
  107. # ==================== 客户端模拟 ====================
  108. def generate_message():
  109. """生成模拟的传感器上报消息"""
  110. return json.dumps({
  111. "type": "sensor_data",
  112. "sensorId": random.randint(1, 5000),
  113. "timestamp": datetime.now().isoformat(),
  114. "data": {
  115. "pressure": round(random.uniform(0.1, 0.8), 3),
  116. "flow": round(random.uniform(10, 500), 2),
  117. "temperature": round(random.uniform(5, 35), 1),
  118. },
  119. })
  120. async def client_worker(client_id, uri, stats, duration, message_interval):
  121. """单个 WebSocket 客户端工作线程"""
  122. start_time = time.time()
  123. connect_start = time.time()
  124. try:
  125. async with ws_connect(uri) as ws:
  126. connect_time_ms = (time.time() - connect_start) * 1000
  127. await stats.record_connect(connect_time_ms)
  128. # 发送订阅消息
  129. subscribe_msg = json.dumps({
  130. "type": "subscribe",
  131. "channels": [f"sensor_{random.randint(1, 100)}"],
  132. })
  133. await ws.send(subscribe_msg)
  134. # 定期发送消息
  135. while time.time() - start_time < duration:
  136. try:
  137. msg = generate_message()
  138. send_time = time.time()
  139. await ws.send(msg)
  140. await stats.record_send()
  141. # 尝试接收消息(非阻塞)
  142. try:
  143. response = await asyncio.wait_for(ws.recv(), timeout=1.0)
  144. latency_ms = (time.time() - send_time) * 1000
  145. await stats.record_receive(latency_ms)
  146. except asyncio.TimeoutError:
  147. pass # 没有响应是正常的
  148. except Exception:
  149. pass
  150. await asyncio.sleep(message_interval + random.uniform(0, 1))
  151. except Exception:
  152. await stats.record_error()
  153. break
  154. except Exception as e:
  155. await stats.record_error()
  156. finally:
  157. await stats.record_disconnect()
  158. # ==================== 资源监控 ====================
  159. async def monitor_resources(duration, interval=5):
  160. """监控系统资源"""
  161. resource_stats = {
  162. "cpu_percent": [],
  163. "memory_mb": [],
  164. "timestamps": [],
  165. }
  166. start = time.time()
  167. while time.time() - start < duration:
  168. try:
  169. usage = resource.getrusage(resource.RUSAGE_SELF)
  170. mem_mb = usage.ru_maxrss / 1024 # Linux: KB → MB
  171. resource_stats["memory_mb"].append(round(mem_mb, 2))
  172. resource_stats["timestamps"].append(round(time.time() - start, 1))
  173. except Exception:
  174. pass
  175. await asyncio.sleep(interval)
  176. return resource_stats
  177. # ==================== 主测试函数 ====================
  178. async def run_stress_test(host, port, num_clients, duration, message_interval):
  179. """运行压力测试"""
  180. uri = f"ws://{host}:{port}{WS_PATH}"
  181. stats = Stats()
  182. print(f"\n{'=' * 60}")
  183. print(f"🔌 WebSocket 压力测试")
  184. print(f"{'=' * 60}")
  185. print(f"目标: {uri}")
  186. print(f"并发连接数: {num_clients}")
  187. print(f"测试时长: {duration}s")
  188. print(f"消息间隔: {message_interval}s")
  189. print(f"开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  190. print(f"{'=' * 60}\n")
  191. # 启动所有客户端(分批启动避免瞬时压力)
  192. tasks = []
  193. batch_size = max(10, num_clients // 10)
  194. resource_task = asyncio.create_task(monitor_resources(duration + 30))
  195. for i in range(num_clients):
  196. task = asyncio.create_task(
  197. client_worker(i, uri, stats, duration, message_interval)
  198. )
  199. tasks.append(task)
  200. # 分批启动
  201. if (i + 1) % batch_size == 0:
  202. await asyncio.sleep(0.5)
  203. print(f"✅ 已启动 {num_clients} 个 WebSocket 客户端")
  204. # 等待测试完成
  205. await asyncio.sleep(duration + 5) # 额外 5 秒等待收尾
  206. # 取消剩余任务
  207. for task in tasks:
  208. task.cancel()
  209. # 等待资源监控结束
  210. try:
  211. resource_data = await asyncio.wait_for(resource_task, timeout=10)
  212. except asyncio.TimeoutError:
  213. resource_data = {"memory_mb": [], "timestamps": []}
  214. # 输出结果
  215. summary = stats.summary()
  216. print(f"\n{'=' * 60}")
  217. print(f"📊 WebSocket 压力测试结果")
  218. print(f"{'=' * 60}")
  219. print(f"并发连接数: {num_clients}")
  220. print(f"成功连接: {summary['successful_connections']}")
  221. print(f"失败连接: {summary['failed_connections']}")
  222. print(f"连接成功率: {summary['connection_success_rate']}")
  223. print(f"断连次数: {summary['disconnections']}")
  224. print(f"消息发送: {summary['messages_sent']}")
  225. print(f"消息接收: {summary['messages_received']}")
  226. if summary['latency']:
  227. print(f"\n消息延迟:")
  228. print(f" 最小: {summary['latency']['min_ms']}ms")
  229. print(f" 最大: {summary['latency']['max_ms']}ms")
  230. print(f" 平均: {summary['latency']['avg_ms']}ms")
  231. print(f" 中位数: {summary['latency']['median_ms']}ms")
  232. print(f" P95: {summary['latency']['p95_ms']}ms")
  233. print(f" P99: {summary['latency']['p99_ms']}ms")
  234. if summary['connect_time']:
  235. print(f"\n连接建立时间:")
  236. print(f" 最小: {summary['connect_time']['min_ms']}ms")
  237. print(f" 平均: {summary['connect_time']['avg_ms']}ms")
  238. print(f" 最大: {summary['connect_time']['max_ms']}ms")
  239. if resource_data.get("memory_mb"):
  240. print(f"\n资源消耗:")
  241. print(f" 峰值内存: {max(resource_data['memory_mb']):.2f} MB")
  242. print(f" 平均内存: {statistics.mean(resource_data['memory_mb']):.2f} MB")
  243. print(f"{'=' * 60}\n")
  244. return summary
  245. async def run_step_test(host, port, message_interval):
  246. """运行梯度负载测试"""
  247. print("\n" + "=" * 60)
  248. print("📈 WebSocket 梯度负载测试")
  249. print("=" * 60)
  250. results = []
  251. for config in STEP_CONFIGS:
  252. result = await run_stress_test(
  253. host, port, config["clients"], config["duration"], message_interval
  254. )
  255. results.append({
  256. "clients": config["clients"],
  257. **result,
  258. })
  259. await asyncio.sleep(10) # 阶段间冷却
  260. return results
  261. # ==================== 入口 ====================
  262. def main():
  263. parser = argparse.ArgumentParser(description="WebSocket 压力测试")
  264. parser.add_argument("--host", default=DEFAULT_HOST, help=f"目标主机 (默认: {DEFAULT_HOST})")
  265. parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"目标端口 (默认: {DEFAULT_PORT})")
  266. parser.add_argument("--clients", type=int, default=DEFAULT_CLIENTS, help=f"并发连接数 (默认: {DEFAULT_CLIENTS})")
  267. parser.add_argument("--duration", type=int, default=DEFAULT_DURATION, help=f"测试时长秒 (默认: {DEFAULT_DURATION})")
  268. parser.add_argument("--interval", type=float, default=DEFAULT_MESSAGE_INTERVAL, help=f"消息间隔秒 (默认: {DEFAULT_MESSAGE_INTERVAL})")
  269. parser.add_argument("--step", action="store_true", help="运行梯度负载测试 (100/500/1000/5000)")
  270. parser.add_argument("--output", default=None, help="结果输出 JSON 文件路径")
  271. args = parser.parse_args()
  272. if args.step:
  273. results = asyncio.run(run_step_test(args.host, args.port, args.interval))
  274. else:
  275. results = asyncio.run(
  276. run_stress_test(args.host, args.port, args.clients, args.duration, args.interval)
  277. )
  278. # 保存结果
  279. if args.output:
  280. with open(args.output, "w") as f:
  281. json.dump(results, f, indent=2, ensure_ascii=False)
  282. print(f"结果已保存到: {args.output}")
  283. return results
  284. if __name__ == "__main__":
  285. main()