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

mqtt_iot_stress.py 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. #!/usr/bin/env python3
  2. """
  3. IoT MQTT 并发压力测试
  4. 使用 asyncio + paho-mqtt 模拟大量 IoT 设备同时上报数据,
  5. 测试消息到达率、延迟、MQTT broker 负载。
  6. 运行方式:
  7. python mqtt_iot_stress.py [--broker HOST] [--port PORT] [--devices N] [--duration S] [--freq S]
  8. 示例:
  9. python mqtt_iot_stress.py --devices 1000 --duration 300 --freq 5
  10. """
  11. import asyncio
  12. import argparse
  13. import json
  14. import time
  15. import random
  16. import statistics
  17. import sys
  18. import threading
  19. from datetime import datetime
  20. from collections import defaultdict
  21. from concurrent.futures import ThreadPoolExecutor
  22. try:
  23. import paho.mqtt.client as mqtt
  24. except ImportError:
  25. print("请先安装 paho-mqtt: pip install paho-mqtt")
  26. sys.exit(1)
  27. # ==================== 配置 ====================
  28. DEFAULT_BROKER = "localhost"
  29. DEFAULT_PORT = 1883
  30. DEFAULT_DEVICES = 100
  31. DEFAULT_DURATION = 120 # 秒
  32. DEFAULT_FREQUENCY = 5 # 上报频率(秒)
  33. DEFAULT_TOPIC_PREFIX = "iot/sensor"
  34. DEFAULT_QOS = 1
  35. # 梯度测试配置
  36. STEP_CONFIGS = [
  37. {"devices": 100, "duration": 60, "freq": 5},
  38. {"devices": 500, "duration": 60, "freq": 5},
  39. {"devices": 1000, "duration": 60, "freq": 5},
  40. {"devices": 5000, "duration": 60, "freq": 5},
  41. {"devices": 1000, "duration": 60, "freq": 1}, # 高频上报
  42. {"devices": 1000, "duration": 60, "freq": 10}, # 低频上报
  43. ]
  44. # ==================== 统计 ====================
  45. class MQTTStats:
  46. """MQTT 统计收集器(线程安全)"""
  47. def __init__(self):
  48. self._lock = threading.Lock()
  49. self.connected = 0
  50. self.disconnected = 0
  51. self.messages_published = 0
  52. self.messages_received = 0
  53. self.publish_errors = 0
  54. self.connect_errors = 0
  55. self.latencies = []
  56. self.publish_times = []
  57. self.throughput_per_second = defaultdict(int)
  58. def record_connect(self):
  59. with self._lock:
  60. self.connected += 1
  61. def record_disconnect(self):
  62. with self._lock:
  63. self.disconnected += 1
  64. def record_connect_error(self):
  65. with self._lock:
  66. self.connect_errors += 1
  67. def record_publish(self, latency_ms):
  68. with self._lock:
  69. self.messages_published += 1
  70. self.publish_times.append(latency_ms)
  71. second = int(time.time())
  72. self.throughput_per_second[second] += 1
  73. def record_publish_error(self):
  74. with self._lock:
  75. self.publish_errors += 1
  76. def record_receive(self, latency_ms):
  77. with self._lock:
  78. self.messages_received += 1
  79. self.latencies.append(latency_ms)
  80. def summary(self):
  81. with self._lock:
  82. total_attempted = self.connected + self.connect_errors
  83. success_rate = (self.connected / total_attempted * 100) if total_attempted > 0 else 0
  84. throughput_values = list(self.throughput_per_second.values())
  85. throughput_stats = {}
  86. if throughput_values:
  87. throughput_stats = {
  88. "min_per_sec": min(throughput_values),
  89. "max_per_sec": max(throughput_values),
  90. "avg_per_sec": round(statistics.mean(throughput_values), 2),
  91. }
  92. publish_time_stats = {}
  93. if self.publish_times:
  94. sorted_times = sorted(self.publish_times)
  95. publish_time_stats = {
  96. "min_ms": round(sorted_times[0], 2),
  97. "avg_ms": round(statistics.mean(sorted_times), 2),
  98. "max_ms": round(sorted_times[-1], 2),
  99. "p95_ms": round(sorted_times[int(len(sorted_times) * 0.95)], 2),
  100. "p99_ms": round(sorted_times[int(len(sorted_times) * 0.99)], 2),
  101. }
  102. latency_stats = {}
  103. if self.latencies:
  104. sorted_lat = sorted(self.latencies)
  105. latency_stats = {
  106. "min_ms": round(sorted_lat[0], 2),
  107. "avg_ms": round(statistics.mean(sorted_lat), 2),
  108. "max_ms": round(sorted_lat[-1], 2),
  109. "p95_ms": round(sorted_lat[int(len(sorted_lat) * 0.95)], 2),
  110. "p99_ms": round(sorted_lat[int(len(sorted_lat) * 0.99)], 2),
  111. }
  112. return {
  113. "total_devices": total_attempted,
  114. "connected": self.connected,
  115. "connect_errors": self.connect_errors,
  116. "connection_success_rate": f"{success_rate:.1f}%",
  117. "disconnected": self.disconnected,
  118. "messages_published": self.messages_published,
  119. "messages_received": self.messages_received,
  120. "publish_errors": self.publish_errors,
  121. "publish_time": publish_time_stats,
  122. "message_latency": latency_stats,
  123. "throughput": throughput_stats,
  124. }
  125. # ==================== 设备模拟 ====================
  126. def generate_sensor_payload(device_id):
  127. """生成传感器上报数据"""
  128. return json.dumps({
  129. "deviceId": f"device_{device_id:06d}",
  130. "timestamp": datetime.now().isoformat(),
  131. "type": random.choice(["pressure", "flow", "quality", "level"]),
  132. "data": {
  133. "pressure": round(random.uniform(0.1, 0.8), 3),
  134. "flow": round(random.uniform(10, 500), 2),
  135. "temperature": round(random.uniform(5, 35), 1),
  136. "ph": round(random.uniform(6.5, 8.5), 2),
  137. "turbidity": round(random.uniform(0, 5), 2),
  138. },
  139. "location": {
  140. "lng": round(random.uniform(116.0, 117.0), 6),
  141. "lat": round(random.uniform(39.5, 40.5), 6),
  142. },
  143. "battery": round(random.uniform(20, 100), 1),
  144. "signal": random.randint(-90, -30),
  145. })
  146. class SimulatedDevice:
  147. """模拟 IoT 设备"""
  148. def __init__(self, device_id, broker, port, stats, topic_prefix, qos):
  149. self.device_id = device_id
  150. self.broker = broker
  151. self.port = port
  152. self.stats = stats
  153. self.topic = f"{topic_prefix}/{device_id:06d}/data"
  154. self.qos = qos
  155. self.client = None
  156. self._running = False
  157. def on_connect(self, client, userdata, flags, rc, properties=None):
  158. if rc == 0:
  159. self.stats.record_connect()
  160. # 订阅确认主题(用于测试端到端延迟)
  161. client.subscribe(f"iot/ack/{self.device_id:06d}", qos=0)
  162. else:
  163. self.stats.record_connect_error()
  164. def on_disconnect(self, client, userdata, rc, properties=None):
  165. self.stats.record_disconnect()
  166. def on_message(self, client, userdata, msg):
  167. """收到 ACK 消息,计算端到端延迟"""
  168. try:
  169. payload = json.loads(msg.payload)
  170. if "sendTime" in payload:
  171. latency = (time.time() - payload["sendTime"]) * 1000
  172. self.stats.record_receive(latency)
  173. except Exception:
  174. pass
  175. def on_publish(self, client, userdata, mid, rc=None, properties=None):
  176. pass
  177. def connect(self):
  178. """连接到 MQTT broker"""
  179. self.client = mqtt.Client(
  180. client_id=f"device_{self.device_id:06d}",
  181. callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
  182. protocol=mqtt.MQTTv311,
  183. )
  184. self.client.on_connect = self.on_connect
  185. self.client.on_disconnect = self.on_disconnect
  186. self.client.on_message = self.on_message
  187. self.client.on_publish = self.on_publish
  188. # 设置连接超时
  189. self.client.connect_timeout = 10
  190. try:
  191. self.client.connect(self.broker, self.port, keepalive=60)
  192. self.client.loop_start()
  193. return True
  194. except Exception:
  195. self.stats.record_connect_error()
  196. return False
  197. def publish(self):
  198. """发布一条传感器数据"""
  199. if not self.client or not self.client.is_connected():
  200. return False
  201. payload = generate_sensor_payload(self.device_id)
  202. start_time = time.time()
  203. try:
  204. result = self.client.publish(self.topic, payload, qos=self.qos)
  205. if result.rc == mqtt.MQTT_ERR_SUCCESS:
  206. latency_ms = (time.time() - start_time) * 1000
  207. self.stats.record_publish(latency_ms)
  208. return True
  209. else:
  210. self.stats.record_publish_error()
  211. return False
  212. except Exception:
  213. self.stats.record_publish_error()
  214. return False
  215. def disconnect(self):
  216. """断开连接"""
  217. self._running = False
  218. if self.client:
  219. try:
  220. self.client.loop_stop()
  221. self.client.disconnect()
  222. except Exception:
  223. pass
  224. # ==================== 测试运行器 ====================
  225. def run_stress_test(broker, port, num_devices, duration, frequency, topic_prefix, qos):
  226. """运行 MQTT 压力测试"""
  227. stats = MQTTStats()
  228. print(f"\n{'=' * 60}")
  229. print(f"📡 IoT MQTT 压力测试")
  230. print(f"{'=' * 60}")
  231. print(f"Broker: {broker}:{port}")
  232. print(f"模拟设备数: {num_devices}")
  233. print(f"测试时长: {duration}s")
  234. print(f"上报频率: 每 {frequency}s")
  235. print(f"QoS: {qos}")
  236. print(f"开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  237. print(f"{'=' * 60}\n")
  238. # 创建并连接所有设备
  239. devices = []
  240. print(f"正在连接 {num_devices} 个设备...")
  241. batch_size = max(10, num_devices // 10)
  242. for i in range(num_devices):
  243. device = SimulatedDevice(i + 1, broker, port, stats, topic_prefix, qos)
  244. device.connect()
  245. devices.append(device)
  246. if (i + 1) % batch_size == 0:
  247. time.sleep(0.5) # 分批连接,避免瞬时压力
  248. # 等待连接完成
  249. time.sleep(3)
  250. print(f"✅ 设备连接完成: {stats.connected}/{num_devices}")
  251. # 开始上报数据
  252. print(f"\n开始数据上报(每 {frequency}s)...")
  253. start_time = time.time()
  254. report_count = 0
  255. while time.time() - start_time < duration:
  256. for device in devices:
  257. device.publish()
  258. report_count += 1
  259. elapsed = time.time() - start_time
  260. if report_count % 10 == 0:
  261. print(f" [{elapsed:.0f}s] 已发送 {stats.messages_published} 条消息, "
  262. f"错误: {stats.publish_errors}")
  263. time.sleep(frequency)
  264. # 停止所有设备
  265. print(f"\n停止所有设备...")
  266. for device in devices:
  267. device.disconnect()
  268. time.sleep(2)
  269. # 输出结果
  270. summary = stats.summary()
  271. print(f"\n{'=' * 60}")
  272. print(f"📊 MQTT 压力测试结果")
  273. print(f"{'=' * 60}")
  274. print(f"模拟设备数: {num_devices}")
  275. print(f"成功连接: {summary['connected']}")
  276. print(f"连接失败: {summary['connect_errors']}")
  277. print(f"连接成功率: {summary['connection_success_rate']}")
  278. print(f"消息发布总数: {summary['messages_published']}")
  279. print(f"发布错误: {summary['publish_errors']}")
  280. if summary['publish_time']:
  281. print(f"\n消息发布时间:")
  282. print(f" 最小: {summary['publish_time']['min_ms']}ms")
  283. print(f" 平均: {summary['publish_time']['avg_ms']}ms")
  284. print(f" 最大: {summary['publish_time']['max_ms']}ms")
  285. print(f" P95: {summary['publish_time']['p95_ms']}ms")
  286. print(f" P99: {summary['publish_time']['p99_ms']}ms")
  287. if summary['throughput']:
  288. print(f"\n吞吐量:")
  289. print(f" 最小: {summary['throughput']['min_per_sec']} msg/s")
  290. print(f" 最大: {summary['throughput']['max_per_sec']} msg/s")
  291. print(f" 平均: {summary['throughput']['avg_per_sec']} msg/s")
  292. if summary['message_latency']:
  293. print(f"\n端到端延迟 (ACK):")
  294. print(f" 最小: {summary['message_latency']['min_ms']}ms")
  295. print(f" 平均: {summary['message_latency']['avg_ms']}ms")
  296. print(f" P95: {summary['message_latency']['p95_ms']}ms")
  297. print(f"{'=' * 60}\n")
  298. return summary
  299. def run_step_test(broker, port, topic_prefix, qos):
  300. """运行梯度负载测试"""
  301. print("\n" + "=" * 60)
  302. print("📈 MQTT 梯度负载测试")
  303. print("=" * 60)
  304. results = []
  305. for config in STEP_CONFIGS:
  306. result = run_stress_test(
  307. broker, port, config["devices"], config["duration"],
  308. config["freq"], topic_prefix, qos,
  309. )
  310. results.append({
  311. "devices": config["devices"],
  312. "frequency": config["freq"],
  313. **result,
  314. })
  315. time.sleep(10) # 阶段间冷却
  316. return results
  317. # ==================== 入口 ====================
  318. def main():
  319. parser = argparse.ArgumentParser(description="IoT MQTT 压力测试")
  320. parser.add_argument("--broker", default=DEFAULT_BROKER, help=f"MQTT broker 地址 (默认: {DEFAULT_BROKER})")
  321. parser.add_argument("--port", type=int, default=DEFAULT_PORT, help=f"MQTT broker 端口 (默认: {DEFAULT_PORT})")
  322. parser.add_argument("--devices", type=int, default=DEFAULT_DEVICES, help=f"模拟设备数 (默认: {DEFAULT_DEVICES})")
  323. parser.add_argument("--duration", type=int, default=DEFAULT_DURATION, help=f"测试时长秒 (默认: {DEFAULT_DURATION})")
  324. parser.add_argument("--freq", type=float, default=DEFAULT_FREQUENCY, help=f"上报频率秒 (默认: {DEFAULT_FREQUENCY})")
  325. parser.add_argument("--topic", default=DEFAULT_TOPIC_PREFIX, help=f"主题前缀 (默认: {DEFAULT_TOPIC_PREFIX})")
  326. parser.add_argument("--qos", type=int, default=DEFAULT_QOS, choices=[0, 1, 2], help=f"QoS 级别 (默认: {DEFAULT_QOS})")
  327. parser.add_argument("--step", action="store_true", help="运行梯度负载测试")
  328. parser.add_argument("--output", default=None, help="结果输出 JSON 文件路径")
  329. args = parser.parse_args()
  330. if args.step:
  331. results = run_step_test(args.broker, args.port, args.topic, args.qos)
  332. else:
  333. results = run_stress_test(
  334. args.broker, args.port, args.devices, args.duration,
  335. args.freq, args.topic, args.qos,
  336. )
  337. if args.output:
  338. with open(args.output, "w") as f:
  339. json.dump(results, f, indent=2, ensure_ascii=False)
  340. print(f"结果已保存到: {args.output}")
  341. return results
  342. if __name__ == "__main__":
  343. main()