本文介绍性能测试,比如模拟100个用户同时使用,你的django项目表现如何。
为什么需要性能测试
不进行性能测试,可能发现不了隐藏的性能问题,尤其是一些重要的不能中断的系统更需要进行性能测试。不能等到最后才进行,而是开发构建过程中就进行,否则性能测试可能过于匆忙或干脆取消。
性能测试很复杂,系统学习可能需要一整本书。本文只是简单介绍基本的要点。
安装Locust
性能测试工具有不少,这里用Locust,因为它简单、UI漂亮,测试脚本可以用python编写。
安装:
pipenv install –dev locust
创建一个测试脚本
首先我们要确定核心的应用场景,或必不可少的功能。对于我们这个项目来说,浏览商品、登录、退出、注册是比较核心的业务用例。我们对这些场景建立测试用例。
根目录新建文件夹 locustfiles,名字可以随意取,没约定要求。
然后建立第一个测试用例 browse_products.py。
在浏览商品用例中,我们可能有浏览商品列表view_products,查看具体的某个商品view_product,将商品加入购物车add_to_cart等操作。我们用一个派生于HttpUser的类组织,然后分别设计各个方法。每个方法需要用装饰符 @task表示。
对于view_products,实际的场景可能是从一个分类查看商品,再跳到另一个分类查看,所以引入collection, 并用随机函数randint选择分类id,由于查看不同的collection_id的商品URL不同,在locust生成报表时可能会分离,我们需要将之形成组放在一个报表里,下节细讲,这里就设置一个name为 ’store/products’
view_product与view_products类似,当然它指定一个特定的product_id,我们指定随机范围为全部商品 1-1000。
add_to_cart的商品我们限制一下比如1-10,这样才有更新购物车商品数量的场景,否则很可能都是新增商品条目。同时该操作首先得有购物车,我们需要事先创建一个,于是在 on_start方法里创建。on_start是个生命周期的方法,它在用户登录开始操作时执行,它不是任务,不用@task装饰符。
另外,我们需要设置各个操作的权重,比如查看具体商品的操作比查看商品列表频繁,而加入购物车的操作相对更少一点,我们加上一些自己大致估计的权重数字,比如2,在装饰符上就是 @task(2) 。还有,实际过程中每个操作之间可能一些间隔的,可能会间隔个几秒,我们使用 between函数设置随机秒数,比如我们设了1-5秒。
from random import randint
from locust import HttpUser, task, between
class WebsiteUser(HttpUser):
wait_time = between(1, 5)
@task(2)
def view_products(self):
collection_id = randint(2, 6)
self.client.get(
f’/store/products/?product_id={collection_id}’,
name=’/store/products’)
@task(4)
def view_product(self):
product_id = randint(1, 1000)
self.client.get(
f’/store/products/{product_id}/’,
name=’/store/products/:id’)
@task(1)
def add_to_cart(self):
product_id = randint(1, 10)
self.client.post(f’/store/carts/{self.cart_id}/items/’,
name=’/store/carts/items’,
json={‘product_id’: product_id, ‘quantity’: 1})
def on_start(self):
response = self.client.post(‘/store/carts/’)
result = response.json()
self.cart_id = result[‘id’]
利用代码脚本测试性能的好处是,在程序有修改时,我们只要运行一下测试代码就知道有没有性能问题。下一节我们介绍如何运行测试脚本。
运行一次测试脚本
首先我们在每个测试方法添加陈述说明。以便更好地了解测试过程。
class WebsiteUser(HttpUser):
…
def view_products(self):
print(“View products”)
…
def view_product(self):
print(“View product details”)
…
def add_to_cart(self):
print(“Add to cart”)
…
然后确保django runserver 在运行,再新开一个终端窗口,运行locust,注意 -f 指定具体的测试模块。:
locust -f locustfiles/browse_products.py
locust 运行后,提示打开 http://0.0.0.0:8089. 先按如图示设置 1个用户,并输入网址http://127.0.0.1:8000,点 ‘Start swarming’.
然后我们就能看到相关测试在运行了。可以看到根据我们测试代码中各操作设置的name进行了分组,并能显示出请求数,失败数,响应时间的中位数、99%的值、最大值、最小值、平均值等。也能看到响应内容的大小,这里明显/store/products 是最大的。
我们也可切换标签到 Charts, 更直观地查看图形展示。再切换Failures、Exception、Current Ratio等查看,我们也可将相关数据下载下来(Download Data)。
在运行locust的控制台里,我们能看到输出相关提示信息,这是我们在代码中故意加上print语句指示的。
…
View product details
View products
View product details
View product details
View product details
Add to cart
…
现在我们停掉locust,下一节再进行一个恰当的性能测试。
运行一次性能测试
我们准备运行一次性能测试,用于指示哪些API是比较慢的。
先注释掉之前为了显示的print语句。
再转到ProductViewSet中,将queryset的prefetch_related去掉。在获取Product时我们希望同时获取images,这里去掉预加载images,会导致性能问题,主要用于演示locust的性能测试。
queryset = Product.objects.prefetch_related(“images”).all()
再注释掉 settings.py中 MIDDLEWARE 的 网页调试工具,因为该工具会使性能测试有些干扰。
# ‘debug_toolbar.middleware.DebugToolbarMiddleware’,
然后再重新运行 locust:
locust -f locustfiles/browse_products.py
再访问 http://127.0.0.1:8089
这次我们设定每秒添加10个用户,直到500个,然后这500个用户不停发出请求。看看结果如何。
我们需要等待一段时间使用户达到500个,数据稳定时,我们再停掉测试。我们不要对测试的结果的绝对值在意,这只是在测试环境,而且用的是Django自带的web服务,如果换到正式环境,数据会不一样。而且即使同一环境,再测一次可能数据也有差异。我们要关注的是相对的值。
我们注意到,/store/products/ 端点响应时间明显有点长,主要是我们去掉prefetch_related(‘images’)的原因。
性能优化技术
大部分的性能问题(可能是90% 以上)是由于查询或数据库问题。有一些技术可以解决这些问题。
一是优化python代码。
python是通ORM映射到数据库,我们要检查不要产生成本较高的数据库操作。这里是一些建议:使用select_related或prefetch_related预加载;利用only或defer只加载一些字段;利用values或values_list,这些生成的是字典或列表,成本要低于生成对象,在不需要执行一些行为如create,update等操作时可以使用。
另外要注意统计的方法,不用全部加载到内存再用len统计。在批量创建或批量更新时使用bulk_create或bulk_update,而不是循环语句里更新。
二是重写查询。
django生成的查询必是最优化,如果您精通SQL,可以按照优化手册重新编写。
三是调整数据库。
比如调整数据表和索引。
四是缓存结果。
如果查询还是慢,可以将结果放在内存中,后面的请求可以直接从内存中取,这叫缓存技术。但请记住,使用缓存技术不一定更快,有时一些简单的查询数据库比缓存更快,尤其是缓存服务器是独立的情况下,因调用缓存就需要网络调用。
五就是增加硬件或添加服务器,当然前提是可以承担得起。
即便因为成本等原因解决不了性能问题,也要学会忍受,并非所有应用程序都快如闪电的,最重要是优化关键部分。
用SILK分析
前面我们说过,大部分性能问题都源自查询和数据库。我们使用Django的Silk来分析具体的源头。Silk能给到我们的应用程序的执行档案,能帮我们看到应用程序互相执行情况,应用程序发送给数据库的SQL语句及执行的时间。
Google查询”Django Silk”, 能查到Silk的github存储库,查看安装配置方法。
https://github.com/jazzband/django-silk
安装库:
pipenv install --dev django-silk
配置settings.py。之前我们介绍过中间件,他会加上一些属性再传给下一个中间件,但如果中间件也可以处理request并返回给用户,我们要确保在silk中间件之前没有处理request的中间件,这里我们没有,就把silk中间件放在最后,由于只需要开发环境使用,需要判断是否是DEGUG。
INSTALLED_APPS也加上silk应用。
MIDDLEWARE = [ ...] if DEBUG: MIDDLEWARE += ['silk.middleware.SilkyMiddleware'] INSTALLED_APPS = ( ... 'silk', ...
再设置urls.py,同样判断是否是开发环境。
if settings.DEBUG: ... urlpatterns += [path('silk/', include('silk.urls', namespace='silk'))]
再运行迁移:
python manage.py migrate
完成设置后,就能访问:http://127.0.0.1:8000/silk/
目前没数据。
当我们访问网站各端点时,silk就会拦截所有请求,并记录相关信息。我们不想手动访问各个端点,我们重新启动locust,不过这次我们只使用1个用户。可能我们会注意到,这次的响应时间比之前要大一点,主要是silk会拦截请求,做相关处理,开销会增加。
运行locust一段时间就稳定了,我们停止。然后转到silk站点。
我们能看到花的时间最大的端点,以及花销最大的数据库操作、花销最大的查询等,并可点进去查看详细。在Requests标签中,我们可以看到所有发送给数据库的请求。在使用结束后,可以在Clear DB标签中,清除收集的数据。
验证优化
我们查到了问题在于product未预加载images导致了查询增加很多,于是
我们在ProductViewSet中,加回 .prefetch_related(“images”) 。
然后注释掉silk中间件,否则会影响效果。
# if DEBUG: # MIDDLEWARE += ['silk.middleware.SilkyMiddleware']
然后用同样的用户和速率运行locust,请求个数也与之前一致,然后我们看一下前后差别。
我们看到,两次的请求数基本一致,响应时间明显减少,性能有了明显提升,RPS能力也有了提升。
压力测试
最后介绍压力测试。压力测试用于测试应用服务的能力上限,达到这个极限时应用程序将崩溃。这对了解我们的应用程序是重要的,有时我们要了解能不能顶过流量高峰,像在线商城的抢购活动。
我们用ctrl+c停掉locust,然后重新启动:
locust -f locustfiles/browse_products.py
然后我们在locust 启动界面 设置一个较大的用户数,比如1000个,然后观察何时出现Fail。根据我的开发机器测试,到近500个用户时出现了Fails,然后RPS也开始急剧下降,从100多降到10以下。这样我们就基本知道这台机器的上限是多少了。
当然每台机器的性能都不一样,如果要测试正式环境,最好搭一个差不多性能的环境进行压力测试。
小结
本文介绍了性能测试相关知识,下一篇介绍缓存技术,也是性能优化的一种技术。