Django 性能测试——一个现实世界的例子
大约一个星期前,安德鲁 和 我 启动 一个新的 Django 打造的网站,站名叫 Hey!Wall 。这是一个按照社交网络中的“墙”的概念建立的社交网站,它为各类朋友提供了一个留言及分享照片、视频和链接的空间。
我们想对其进行性能评估,并进行一些服务器配置和代码修改来决定采取何种步骤进行改进。我们使用 httperf
进行了测试,并通过优化将其性能提高了整整一倍。
服务器和客户端
服务器一是一台 Slicehost 提供的 Xen VPS ,配有 256MB 内存,运行的是 Debian Etch 系统。部署在美国中西部。
为了测试,采用了一台位于英国的 Xtraordinary Hosting 提供的 Xen VPS,作为客户端。通常我们使用的 ADSL 访问互联网络,但这让我们很难向服务器发起足够多的访问请求。使用连接良好的 VPS 作为客户端使我们可以真正地对服务器加以考验。
服务器规格说明
很难确切地描述服务器的规格。该 VPS 配有 256MB 内存,与数个类似的 VPS 同居一台主机(大概是一台 装有 16GB 内存的 Quad Core 服务器)之上。假定装满了 256MB 切片的话,物理服务器上最多装有 64 台 VPS 。如果四个处理器都是 2.4GHz,那么共 9.6 GHz ,除以 64 得到最少 150MHz 的 CPU 。
在 Xen VPS 上,无需竞争你就可以获得稳定的内存和 CPU 分配,但通常 主机上任何空闲的 CPU 都将得到使用。如果在同一机器上的其它 VPS 处于空闲状态,你的 VPS 将能够使用更多的 CPU 。这也许意味着在测试期间使用了更多的 CPU ,即某些测试可能比其它的使用了更多的 CPU 资源。
使用 httperf 评估性能
现有各式各样的网络性能测试工具,主要包括 ab (来自 Apache), Flood 和 httperf。我们使用 httperf 并没有任何特别理由。
httperf 命令看起来如下所示:
httperf --hog --server=example.com --uri=/ --timeout=10 --num-conns=200 --rate=5
在该例中,我们向 http://example.com/
发起了 200 次访问请求,每秒最多 5 次。
测试计划
某些工具支持进程,可以模仿用户对网站提交任务。我们使用了一种简单的“暴力”测试来了解该站点每秒能够处理多少请求。
该基本方法是发起一定数量的请求,能够判断服务器如何反应:状态 200 为成功,状态 500 为失败。提高频率(每秒制造的请求数量)然后再试一遍。如果开始返回大量的 500 ,则已经达到极限。
监测服务器资源
另一个方面是要掌握服务器在内存和 CPU 使用方面的情况。要跟踪这一情况,我们运行 top
并将输出记录为日志文件以供稍后查阅。该 top 命令如下所示:
top -b -d 3 -U www-data > top.txt
在该例中,我们以用户 www-data
每三秒记录一次进程的日志信息。如果你想更加明确的指定目标,可以使用 -p 1, 2, 3
,而不是 -U username
,其中 1、2 和 3 是 pid(即要观测进程的进程ID)。
网页服务器为配有以 FastCGI processes 方式运行的 Python 2.5 的 Lighttpd 。尽管数据库的日志也是很有用的信息,但我们没有记录该进程(PostgreSQL)的日志。
另一个有用的工具是 vmstat
,特别是 swap 列显示了有多少内存进行了交换。交换的意思是你没有足够的内存,它是一种性能杀手。要想反复运行 vmstat 的话
,必须指定每次检查间隔的秒数。如:
vmstat 2
使用 httperf 进行已认证访问
httperf
只是简单地向某个 URL 发出简单的 GET
请求,并下载 html 文本(但不包括任何媒体文件)。对公共/匿名(public/anonymous)网页发起的访问请求是件轻松的事情,但如果要访问需要登录的页面怎么办呢?
httperf
可以传递请求头部信息。Django 身份校验( authentication)(由 django.contrib.auth
提供)使用的进程依赖于在客户端 cookie 中所保存的进程 id 。而客户端在请求的头部信息中传递 cookie 。你可以看到这一切是如何进行的。
登录站点,查看 cookies 信息。其中应该有个类似 sessionid=97d674a05b2614e98411553b28f909de
的数值。要通过 httperf 传递该 cookie,可以使用 --add-header
参数选项。如:
httperf ... --add-header='Cookie: sessionid=97d674a05b2614e98411553b28f909den'
小心头部信息之后的 n
。如果少了该字符,你的每个请求可能都会返回超时信息。
测试哪个页面?
考虑到这一点,我们测试了网站的两个网页:
- 主页: 对主页的匿名访问
- “墙”: 对某个“墙”已认证访问,该网页包括从数据库获取的内容
事实静态与高度动态
对于匿名用户来说,主页基本上是静态的,它只是简单的渲染某个模板而无需数据库的任何数据。
“墙”页面则非常动态,包括了从数据库获取的主要数据。该模板在被渲染时,针对不同时区用户的日期设置“删除”了指向某些物件的链接,等等。某些“墙”包含了大约50个物件,在被优化前,大约要发起 80 条数据库查询。
第一次测试时,我们运行了两个可以从 Django 接受请求 FastCGI 后端。
Home: 175 req/s (即每秒请求数量)Wall: 8 req/s.
经压缩的内容
第一个配置优化是使用 GZipMiddleware
激活输出的 gzip 压缩。性能轻微提高,没有大的变化。但无论如何为了节约带宽,这么做还是值得的。
Home: 200 req/s.
Wall: 8 req/s.
更多进程,更短的队列
然后,我们将 FastCGI 后端的数量从 2 个提升到 5 个。这项改进减少了 500 响应的数量,因为更多的请求可以由额外的后端来处理。
Home: 200 req/s.
Wall: 11 req/s.
更多的进程,更多的问题
从 2 到 5 的改进非常不错,因此我们决定将 FastCGI 后端数量提升到 10 。性能却显著地 下降 了。
经查看服务器上的 vmstat
,可以看到原因是出现了内存交换。太多的进程,每个都为 Python 使用了内存,导致 VPS 内存耗尽,从而不得不从硬盘往返交换内存。
Home: 150 req/s.
Wall: 7 req/s.
基于此,我们将 FastCGI 后端数量降回 5 以进行更多测试。
分析——时间耗到哪里去了
“墙”页面的性能令人失望,因此我们开始进行优化。我们所做第一件事情是分析代码以确定时间都被花费在何处。
使用一些简单的 分析中间件 之后,很清楚地发现时间被消耗在数据库查询之上。“墙”页面包括许多查询,且数量与其所包含的物件数量呈正比。测试墙页面上引发了大约 80 个查询。毫无疑问其性能是糟糕的。
进行优化
通过优化物件附加媒体的处理方式,我们直接给每个物件剔除了一次查询。该措施稍微地减少了请求所需时间,因此也提高了每秒可处理的查询数量。
Wall: 12 req/s.
导致低效的另一个原因是无论页面是否被请求,对每个物件的内容都应用了多个过滤器(Filter)。经我们修改,被过滤内容的 HTML 输出都被存储在物件当中,节约了页面被查阅时的需要进程。这又带来一点小小改进。
Wall: 13 req/s.
通过减少数据库查询,我们以修改用户配置文件(用于显示是谁将该物件粘贴到墙上)的获取方式为每个物件剔除了一次查询。这次修改又提高了不少。
Wall: 15 req/s.
这场测试的最后一次优化目标是减少获取物件所附加媒体的查询数量。我们再一次削减了一些查询,稍微地提高了性能。
Wall: 17 req/s.
下一步:缓存
在尽可能地减少查询之后,接下来要进行一些缓冲工作。获取缓存数据通常比查询数据库更加快捷,因此我们期待性能有一个显著提升。
对整个页面的输出进行缓存是没有意义的,因为每个页面根据发出请求的用户不同而截然不同。只有当用户对同一页面的两次请求之间,情况没有发生任何变化,才可能出现缓存命中。
对墙、物件及用户的列表进行缓存的作用更大。被缓存的数据将被用于从同一用户发出的多个请求,及在对于墙壁的不同程度和不同用户访问之间共享。这未必是巨大的胜利,因为每个“墙”很可能只有极少数的用户,而数据必须在高速缓存中停留足够长的时间以被别人获取。
在这种情况下,我们简化的 httperf
测试将会被极大地误导。每个请求都由同一用户发出,因此缓存命中几乎是100%,而性能将因此极高!这反映不出真实世界的站点使用情况,因此我们最好进行一些更好的测试。
目前我们还没有使用缓存,因为站点可以轻松地应对的当前活动水平,但一旦 Hey! Wall 流行起来,这将是我们的下一个步骤。
多少用户能够导致每秒17次请求?
提供每秒 17 次请求相应看起来仍然非常少,但将该数据翻译成站点的实际用户量是非常有趣的事情。显然,这数据不包括提供像图片、CSS 和 JavaScript 文件之类的媒体文件服务。相对来说,媒体文件个头要大一些,但它们直接由 Lighttpd (而不是 Django)处理,并提供了 Expires
头部信息来允许客户端对它们进行缓存。不过,为了在测试中更好地进行评估,我们还是需要对服务器进行一些处理。
现在说采用何种通用模式还为时过早,因此我说的只能是猜测。请允许我这么说!
我将假定每个用户平均访问三个“墙”,并按顺序查看它们的内容,暂停10至20秒时间以阅读新的评论,或查看一些照片和打开一些链接。该用户每天进行三次这种操作。
只看墙页面,不看媒体的话,用户将每天对墙页面发起 9 次访问请求。每个用户一次只能发起一次访问请求,因此在时间上,任何一秒内 17 个用户可以同时进行该操作。一分钟内,用户只发出3次访问请求,因此17个并发用户只用去了60 秒中的 3 秒(或20秒中的1秒)。
如果一段时间内用户的请求分布是完全平衡的(提示:不可能的!),那也就意味着每分钟可以有 340 用户(17 * 20)访问该网站。延续这个不真实的例子,我们可以说每天有 1440 分钟,而每个用户每天访问网站 3 分钟,因此该网站可以应对大约 163,000 个用户。这对于一个每月 20 美元的 VPS 来说已经非常棒了!
为了更多统计一下这些数字,让我们假定每天6小时内,我们每分钟应对 200 个并发用户,另 6 个小时内(每分钟)应对 100 个并发用户,剩下的 12 小时内(每分钟)应对 10 个并发用户。基于每秒 17 次请求的最大负荷,网站每天仍然可以应对的大约 115,000 个用户。
我确信这些数字并不虚假和荒谬。如果有人在评论中提出更好的评估方案或者真实世界的数据,我将非常感兴趣。
我们学到了什么?
概括:
- 测试网站性能可能会产生令人惊异的结果
- 过多的数据库查询对性能(duh)来说不是件好事
- 对站点的某类内容进行缓存比对其他一些更好
- 一台廉价的 VPS 所能应对的用户数量比你所想像的要多得多