python服务器压测脚本性能大比拼

本文通过实验对比了Python中多进程、多线程、协程以及协程+多进程四种方式在服务器压力测试(压测)中的性能。实验发现,在低QPS需求时,协程表现最优;在高QPS需求时,协程+多进程组合能最大化性能。实验还揭示了Python多线程由于GIL限制无法充分利用多核CPU的问题。
摘要由CSDN通过智能技术生成

前言

为了满足某些压测需求,比如模拟不同地区的人进行压测,就需要自己编写压测脚本,然而,如何在有限的计算资源的基础上实现最优性能的压测脚本实属不易,本文以python语言为例,分析几种常见的压测脚本的实现方式,并比较各自性能优劣。

服务器压测的实现方式

所谓压测,就是使用计算机模拟真实场景的用户对服务器的性能进行测试,测试服务器是否能抗住某一并发量。其中最重要的两个指标就是QPS和响应时间。因为数据请求是io密集型的操作,其实现方式主要有以下几种:

  1. 多进程(multiprocessing)
  2. 多线程(threading)
  3. 异步协程(asyncio)
  4. 异步协程+多进程

实验设计

场景模拟

实验固定一个场景需求对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))

实验结论

实验数据

多进程

进程数量qpscpu内存
2042965%46%
5059780%50%
8081393%52%
10070099%54%

多线程

线程数量qpscpu内存
2024217%40%
8025718%40%
20036822%40%

协程

协程数量qpscpu内存
2067415%40%
80117021%40%
150114026%40%

协程+多进程

进程数量协程数量qpscpu内存
480246381%40%
680240294%40%
880228699%43%
4100254181%39%
4120240983%40%

结论

在低qps需求中,使用协程的方式编写压测脚本是最优方案,能在不浪费cpu与内存资源的情况下轻松干到千级以上

在较高qps需求中,使用协程+多进程的编写方式实现压测脚本最佳,既能合理使用cpu资源,又能将qps干到最大

在更高的qps的需求下,只能用分布式压测工具了。

原理分析

  1. 操作系统中,进程的创建与销毁是十分消耗系统资源的,进程的上下文切换会进行系统调用进入内核态,且步骤繁琐,有大量状态需要保存,所以多线程的数据中cpu占比与内存占比显著高于其他实验数据。
  2. 操作系统中,线程可以看作是轻量级的进程,线程是进程的一部分。线程的创建与销毁虽然也需要进行系统调用进入内核态,也需要进行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)
  1. 所谓协程,也就是不走系统调用的线程,也可以叫做用户态线程,所以上下文切换的资源和时间比线程还要少的多,这也是为什么协程qps轻松干到上千的原因。

  2. 但是python因为GIL锁协程也是有缺点的就是不能充分利用cpu的多核,于是就有了多进程+协程的模式,进程的设置与核心数量刚好是最棒的。

后记

哈哈哈,突然心血来潮做了这个实验,仿佛又回到了大学做写作业,肝代码,做实验的时光,原理只是自己的理解,有什么不妥欢迎call我。

  • 6
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值