前言
为了满足某些压测需求,比如模拟不同地区的人进行压测,就需要自己编写压测脚本,然而,如何在有限的计算资源的基础上实现最优性能的压测脚本实属不易,本文以python语言为例,分析几种常见的压测脚本的实现方式,并比较各自性能优劣。
服务器压测的实现方式
所谓压测,就是使用计算机模拟真实场景的用户对服务器的性能进行测试,测试服务器是否能抗住某一并发量。其中最重要的两个指标就是QPS和响应时间。因为数据请求是io密集型的操作,其实现方式主要有以下几种:
- 多进程(multiprocessing)
- 多线程(threading)
- 异步协程(asyncio)
- 异步协程+多进程
实验设计
场景模拟
实验固定一个场景需求对http://busdatapractice.map.qq.com/api/common/city接口进行访问,监控其QPS,执行机的CPU使用率,执行机的内存的使用率。
实验过程
分别执行机器监控脚本与以下4中脚本,调整并发数量,执行压测60秒,记录数据并分析。
监控手段的实现
每0.5秒获取电脑cpu和内存的使用情况,最终求平均值
import time
import psutil
def get_cpu_mem():
# 获取物理内存使用
mem = psutil.virtual_memory()
# 获取cpu使用率
cpu = psutil.cpu_percent(1)
return cpu, mem
def main(last_time):
all_cpu = []
all_mem = []
start_time = time.time()
while time.time() - start_time <= last_time:
cpu, mem = get_cpu_mem()
print('cpu使用率为%s%%|内存使用率为%s%%' % (cpu, mem[2]))
all_cpu.append(cpu)
all_mem.append(mem[2])
time.sleep(0.5)
return sum(all_cpu) / len(all_cpu), sum(all_mem) / len(all_mem)
if __name__ == '__main__':
cpu, mem = main(120)
print(cpu, mem)
代码编写
这边进程数量干到100个就上不去了
多进程
from multiprocessing import Event, Process, Queue
import time
import requests
press_time = 60 # 压测时间
worker_num = 100 # 进程数量
# 自定义一个进程池
class Pool(object):
def __init__(self):
self._pool = []
self._process = []
self.queue = Queue()
self.all_msg_from_process = []
def add(self, worker_num):
worker = Worker(worker_num, self.queue)
self._pool.append(worker)
self._process.append(Process(target=worker.run, args=()))
def start(self):
for p in self._process:
p.start()
def stop(self):
for i in range(0, len(self._pool)):
self._pool[i].event.set()
for p in self._process:
p.join()
while not self.queue.empty():
self.all_msg_from_process.append(self.queue.get())
def get_all_avg_qps(self):
sum = 0
for i in self.all_msg_from_process:
sum += i["avg_qps"]
return sum
class Worker(object):
def __init__(self, worker_num, queue):
self.worker_num = worker_num # worker编号
self.queue = queue # 消息队列,用于进程间的通信
self.request_num = 0 # 总的请求次数
self.session = requests.Session()
self.session.mount('http://',
requests.adapters.HTTPAdapter(pool_connections=1, pool_maxsize=20, max_retries=3)) # 设置长连接
self.event = Event() # 控制开关
self.avg_qps = 0 # 计算该worker的平均qps
def run(self):
print("%d开始" % (self.worker_num))
while not self.event.is_set():
try:
resp = self.session.get("http://busdatapractice.map.qq.com/api/common/city")
except:
print("error")
self.request_num += 1
self.avg_qps = self.request_num / press_time
print("%d结束" % (self.worker_num))
self.queue.put({"worker_num": self.worker_num, "avg_qps": self.avg_qps})
def main():
pool = Pool() # 定义一个进程池
for i in range(0, worker_num):
print(i)
pool.add(i)
print("----start----")
pool.start()
time.sleep(press_time)
print("睡眠完成")
pool.stop()
print("qps:", pool.get_all_avg_qps())
print("-----end-----")
if __name__ == '__main__':
main()
多线程
import threading
import time
import requests
press_time = 10 # 压测时间
worker_num = 150 # 线程数量
exitFlag = False
class Worker(threading.Thread):
def __init__(self, worker_num):
threading.Thread.__init__(self)
self.worker_num = worker_num
self.request_num = 0 # 总的请求次数
self.session = requests.Session()
self.session.mount('http://',
requests.adapters.HTTPAdapter(pool_connections=1, pool_maxsize=20, max_retries=3)) # 设置长连接
self.avg_qps = 0 # 计算该worker的平均qps
def run(self):
global exitFlag
print("%d开始" % (self.worker_num))
while not exitFlag:
try:
resp = self.session.get("http://busdatapractice.map.qq.com/api/common/city")
except:
print("error")
self.request_num += 1
self.avg_qps = self.request_num / press_time
print("%d结束" % (self.worker_num))
def main():
pool = []
for i in range(0, worker_num):
print(i)
worker = Worker(i)
pool.append(worker)
print("----start----")
for worker in pool:
worker.start()
time.sleep(press_time)
print("睡眠完成")
global exitFlag
exitFlag = True
for worker in pool:
worker.join()
print("-----end-----")
sum=0
for worker in pool:
sum+=worker.avg_qps
print(worker.avg_qps)
print(sum)
if __name__ == '__main__':
main()
协程
import asyncio
import aiohttp
import time
press_time = 120 # 压测时间
worker_num = 150 # 协程数量
exitFlag = False
class Worker(object):
def __init__(self, worker_num):
self.worker_num = worker_num
self.request_num = 0 # 总的请求次数
self.avg_qps = 0 # 计算该worker的平均qps
async def run(self):
global exitFlag
print("%d开始" % (self.worker_num))
session = aiohttp.ClientSession()
while not exitFlag:
try:
response = await session.get('http://busdatapractice.map.qq.com/api/common/city')
# result = await response.text()
except:
print("error")
self.request_num += 1
await session.close()
self.avg_qps = self.request_num / press_time
print("%d结束" % (self.worker_num))
# 计时器
async def count():
start = time.time()
while time.time() - start <= press_time:
await asyncio.sleep(0.2)
global exitFlag
exitFlag = True
# main entrance
if __name__ == '__main__':
pool = []
tasks = []
for i in range(0, worker_num):
print(i)
worker = Worker(i)
tasks.append(worker.run())
pool.append(worker)
tasks.append(count())
asyncio.get_event_loop().run_until_complete(asyncio.wait(tasks))
print("睡眠完成")
sum = 0
for worker in pool:
sum += worker.avg_qps
print(worker.avg_qps)
print(sum)
协程+多进程
import asyncio
import aiohttp
import time
from multiprocessing import Pool, Queue
press_time = 60 # 压测时间
worker_num = 80 # 协程数量
worker2_num = 8 # 8核cpu,进程数
exitFlag = False
queue = Queue()
class Worker(object):
def __init__(self, worker_num):
self.worker_num = worker_num
self.request_num = 0 # 总的请求次数
self.avg_qps = 0 # 计算该worker的平均qps
async def run(self):
global exitFlag
print("%d开始" % (self.worker_num))
session = aiohttp.ClientSession()
while not exitFlag:
try:
response = await session.get('http://busdatapractice.map.qq.com/api/common/city')
# result = await response.text()
except:
print("error")
self.request_num += 1
await session.close()
self.avg_qps = self.request_num / press_time
# 计时器
async def count():
start = time.time()
while time.time() - start <= press_time:
await asyncio.sleep(0.2)
global exitFlag
exitFlag = True
def main():
pool = []
tasks = []
for i in range(0, worker_num):
print(i)
worker = Worker(i)
tasks.append(worker.run())
pool.append(worker)
tasks.append(count())
asyncio.get_event_loop().run_until_complete(asyncio.wait(tasks))
print("睡眠完成")
sum = 0
for worker in pool:
sum += worker.avg_qps
queue.put(sum)
if __name__ == '__main__':
po = Pool(worker2_num)
for i in range(0, worker2_num):
po.apply_async(main, ())
po.close()
po.join()
qps = []
while not queue.empty():
qps.append(queue.get())
print("qps", sum(qps))
实验结论
实验数据
多进程
进程数量 | qps | cpu | 内存 |
---|---|---|---|
20 | 429 | 65% | 46% |
50 | 597 | 80% | 50% |
80 | 813 | 93% | 52% |
100 | 700 | 99% | 54% |
多线程
线程数量 | qps | cpu | 内存 |
---|---|---|---|
20 | 242 | 17% | 40% |
80 | 257 | 18% | 40% |
200 | 368 | 22% | 40% |
协程
协程数量 | qps | cpu | 内存 |
---|---|---|---|
20 | 674 | 15% | 40% |
80 | 1170 | 21% | 40% |
150 | 1140 | 26% | 40% |
协程+多进程
进程数量 | 协程数量 | qps | cpu | 内存 |
---|---|---|---|---|
4 | 80 | 2463 | 81% | 40% |
6 | 80 | 2402 | 94% | 40% |
8 | 80 | 2286 | 99% | 43% |
4 | 100 | 2541 | 81% | 39% |
4 | 120 | 2409 | 83% | 40% |
结论
在低qps需求中,使用协程的方式编写压测脚本是最优方案,能在不浪费cpu与内存资源的情况下轻松干到千级以上
在较高qps需求中,使用协程+多进程的编写方式实现压测脚本最佳,既能合理使用cpu资源,又能将qps干到最大
在更高的qps的需求下,只能用分布式压测工具了。
原理分析
- 操作系统中,进程的创建与销毁是十分消耗系统资源的,进程的上下文切换会进行系统调用进入内核态,且步骤繁琐,有大量状态需要保存,所以多线程的数据中cpu占比与内存占比显著高于其他实验数据。
- 操作系统中,线程可以看作是轻量级的进程,线程是进程的一部分。线程的创建与销毁虽然也需要进行系统调用进入内核态,也需要进行cpu的调度以及上下文切换,但是开销比进程要小的多。
- 为什么线程创那么多了,cpu还是上不去?因为这是单进程多线程应用,不能利用cpu的多核机制。
- 为什么线程的qps这么菜?python中的多线程是假的多线程。任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。(https://www.zhihu.com/question/23474039/answer/269526476)
-
所谓协程,也就是不走系统调用的线程,也可以叫做用户态线程,所以上下文切换的资源和时间比线程还要少的多,这也是为什么协程qps轻松干到上千的原因。
-
但是python因为GIL锁协程也是有缺点的就是不能充分利用cpu的多核,于是就有了多进程+协程的模式,进程的设置与核心数量刚好是最棒的。
后记
哈哈哈,突然心血来潮做了这个实验,仿佛又回到了大学做写作业,肝代码,做实验的时光,原理只是自己的理解,有什么不妥欢迎call我。