糟糕的Django/uwsgi性能
我正在用 nginx 和 uwsgi 运行一个 django 应用。下面是我运行 uwsgi 的方式:
sudo uwsgi -b 25000 --chdir=/www/python/apps/pyapp --module=wsgi:application --env DJANGO_SETTINGS_MODULE=settings --socket=/tmp/pyapp.socket --cheaper=8 --processes=16 --harakiri=10 --max-requests=5000 --vacuum --master --pidfile=/tmp/pyapp-master.pid --uid=220 --gid=499
还有 nginx 的配置:
server {
listen 80;
server_name test.com
root /www/python/apps/pyapp/;
access_log /var/log/nginx/test.com.access.log;
error_log /var/log/nginx/test.com.error.log;
# https://docs.djangoproject.com/en/dev/howto/static-files/#serving-static-files-in-production
location /static/ {
alias /www/python/apps/pyapp/static/;
expires 30d;
}
location /media/ {
alias /www/python/apps/pyapp/media/;
expires 30d;
}
location / {
uwsgi_pass unix:///tmp/pyapp.socket;
include uwsgi_params;
proxy_read_timeout 120;
}
# what to serve if upstream is not available or crashes
#error_page 500 502 503 504 /media/50x.html;
}
现在问题来了。当我在服务器上使用 "ab"(ApacheBenchmark)进行测试时,得到了以下结果:
nginx 版本:nginx/1.2.6
uwsgi 版本:1.4.5
Server Software: nginx/1.0.15
Server Hostname: pycms.com
Server Port: 80
Document Path: /api/nodes/mostviewed/8/?format=json
Document Length: 8696 bytes
Concurrency Level: 100
Time taken for tests: 41.232 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 8866000 bytes
HTML transferred: 8696000 bytes
Requests per second: 24.25 [#/sec] (mean)
Time per request: 4123.216 [ms] (mean)
Time per request: 41.232 [ms] (mean, across all concurrent requests)
Transfer rate: 209.99 [Kbytes/sec] received
在 500 个并发连接的情况下运行时
oncurrency Level: 500
Time taken for tests: 2.175 seconds
Complete requests: 1000
Failed requests: 50
(Connect: 0, Receive: 0, Length: 50, Exceptions: 0)
Write errors: 0
Non-2xx responses: 950
Total transferred: 629200 bytes
HTML transferred: 476300 bytes
Requests per second: 459.81 [#/sec] (mean)
Time per request: 1087.416 [ms] (mean)
Time per request: 2.175 [ms] (mean, across all concurrent requests)
Transfer rate: 282.53 [Kbytes/sec] received
如你所见,服务器上的所有请求都失败了,要么是超时错误,要么是“客户端过早断开连接”,或者:
writev(): Broken pipe [proto/uwsgi.c line 124] during GET /api/nodes/mostviewed/9/?format=json
关于我的应用程序,这里有一点更多的信息: 基本上,它是一些模型的集合,这些模型反映了 MySQL 表,表中包含所有内容。在前端,我使用 django-rest-framework 来向客户端提供 json 内容。
我安装了 django-profiling 和 django debug toolbar 来查看发生了什么。在 django-profiling 中,当我运行一个请求时,得到的结果是:
Instance wide RAM usage
Partition of a set of 147315 objects. Total size = 20779408 bytes.
Index Count % Size % Cumulative % Kind (class / dict of class)
0 63960 43 5726288 28 5726288 28 str
1 36887 25 3131112 15 8857400 43 tuple
2 2495 2 1500392 7 10357792 50 dict (no owner)
3 615 0 1397160 7 11754952 57 dict of module
4 1371 1 1236432 6 12991384 63 type
5 9974 7 1196880 6 14188264 68 function
6 8974 6 1076880 5 15265144 73 types.CodeType
7 1371 1 1014408 5 16279552 78 dict of type
8 2684 2 340640 2 16620192 80 list
9 382 0 328912 2 16949104 82 dict of class
<607 more rows. Type e.g. '_.more' to view.>
CPU Time for this request
11068 function calls (10158 primitive calls) in 0.064 CPU seconds
Ordered by: cumulative time
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.064 0.064 /usr/lib/python2.6/site-packages/django/views/generic/base.py:44(view)
1 0.000 0.000 0.064 0.064 /usr/lib/python2.6/site-packages/django/views/decorators/csrf.py:76(wrapped_view)
1 0.000 0.000 0.064 0.064 /usr/lib/python2.6/site-packages/rest_framework/views.py:359(dispatch)
1 0.000 0.000 0.064 0.064 /usr/lib/python2.6/site-packages/rest_framework/generics.py:144(get)
1 0.000 0.000 0.064 0.064 /usr/lib/python2.6/site-packages/rest_framework/mixins.py:46(list)
1 0.000 0.000 0.038 0.038 /usr/lib/python2.6/site-packages/rest_framework/serializers.py:348(data)
21/1 0.000 0.000 0.038 0.038 /usr/lib/python2.6/site-packages/rest_framework/serializers.py:273(to_native)
21/1 0.000 0.000 0.038 0.038 /usr/lib/python2.6/site-packages/rest_framework/serializers.py:190(convert_object)
11/1 0.000 0.000 0.036 0.036 /usr/lib/python2.6/site-packages/rest_framework/serializers.py:303(field_to_native)
13/11 0.000 0.000 0.033 0.003 /usr/lib/python2.6/site-packages/django/db/models/query.py:92(__iter__)
3/1 0.000 0.000 0.033 0.033 /usr/lib/python2.6/site-packages/django/db/models/query.py:77(__len__)
4 0.000 0.000 0.030 0.008 /usr/lib/python2.6/site-packages/django/db/models/sql/compiler.py:794(execute_sql)
1 0.000 0.000 0.021 0.021 /usr/lib/python2.6/site-packages/django/views/generic/list.py:33(paginate_queryset)
1 0.000 0.000 0.021 0.021 /usr/lib/python2.6/site-packages/django/core/paginator.py:35(page)
1 0.000 0.000 0.020 0.020 /usr/lib/python2.6/site-packages/django/core/paginator.py:20(validate_number)
3 0.000 0.000 0.020 0.007 /usr/lib/python2.6/site-packages/django/core/paginator.py:57(_get_num_pages)
4 0.000 0.000 0.020 0.005 /usr/lib/python2.6/site-packages/django/core/paginator.py:44(_get_count)
1 0.000 0.000 0.020 0.020 /usr/lib/python2.6/site-packages/django/db/models/query.py:340(count)
1 0.000 0.000 0.020 0.020 /usr/lib/python2.6/site-packages/django/db/models/sql/query.py:394(get_count)
1 0.000 0.000 0.020 0.020 /usr/lib/python2.6/site-packages/django/db/models/query.py:568(_prefetch_related_objects)
1 0.000 0.000 0.020 0.020 /usr/lib/python2.6/site-packages/django/db/models/query.py:1596(prefetch_related_objects)
4 0.000 0.000 0.020 0.005 /usr/lib/python2.6/site-packages/django/db/backends/util.py:36(execute)
1 0.000 0.000 0.020 0.020 /usr/lib/python2.6/site-packages/django/db/models/sql/query.py:340(get_aggregation)
5 0.000 0.000 0.020 0.004 /usr/lib64/python2.6/site-packages/MySQLdb/cursors.py:136(execute)
2 0.000 0.000 0.020 0.010 /usr/lib/python2.6/site-packages/django/db/models/query.py:1748(prefetch_one_level)
4 0.000 0.000 0.020 0.005 /usr/lib/python2.6/site-packages/django/db/backends/mysql/base.py:112(execute)
5 0.000 0.000 0.019 0.004 /usr/lib64/python2.6/site-packages/MySQLdb/cursors.py:316(_query)
60 0.000 0.000 0.018 0.000 /usr/lib/python2.6/site-packages/django/db/models/query.py:231(iterator)
5 0.012 0.002 0.015 0.003 /usr/lib64/python2.6/site-packages/MySQLdb/cursors.py:278(_do_query)
60 0.000 0.000 0.013 0.000 /usr/lib/python2.6/site-packages/django/db/models/sql/compiler.py:751(results_iter)
30 0.000 0.000 0.010 0.000 /usr/lib/python2.6/site-packages/django/db/models/manager.py:115(all)
50 0.000 0.000 0.009 0.000 /usr/lib/python2.6/site-packages/django/db/models/query.py:870(_clone)
51 0.001 0.000 0.009 0.000 /usr/lib/python2.6/site-packages/django/db/models/sql/query.py:235(clone)
4 0.000 0.000 0.009 0.002 /usr/lib/python2.6/site-packages/django/db/backends/__init__.py:302(cursor)
4 0.000 0.000 0.008 0.002 /usr/lib/python2.6/site-packages/django/db/backends/mysql/base.py:361(_cursor)
1 0.000 0.000 0.008 0.008 /usr/lib64/python2.6/site-packages/MySQLdb/__init__.py:78(Connect)
910/208 0.003 0.000 0.008 0.000 /usr/lib64/python2.6/copy.py:144(deepcopy)
22 0.000 0.000 0.007 0.000 /usr/lib/python2.6/site-packages/django/db/models/query.py:619(filter)
22 0.000 0.000 0.007 0.000 /usr/lib/python2.6/site-packages/django/db/models/query.py:633(_filter_or_exclude)
20 0.000 0.000 0.005 0.000 /usr/lib/python2.6/site-packages/django/db/models/fields/related.py:560(get_query_set)
1 0.000 0.000 0.005 0.005 /usr/lib64/python2.6/site-packages/MySQLdb/connections.py:8()
..等等
然而,django-debug-toolbar 显示了以下内容:
Resource Usage
Resource Value
User CPU time 149.977 msec
System CPU time 119.982 msec
Total CPU time 269.959 msec
Elapsed time 326.291 msec
Context switches 11 voluntary, 40 involuntary
and 5 queries in 27.1 ms
问题在于,“top”显示负载平均值迅速上升,而我在本地服务器和网络内的远程机器上运行的 apache benchmark 显示我每秒处理的请求并不多。 问题出在哪里?这是我在分析代码时能找到的结果,如果有人能指出我在做什么,那将不胜感激。
编辑(2013年2月23日):根据 Andrew Alcock 的回答添加更多细节: 需要我关注/回答的要点是 (3)(3) 我在 MySQL 上执行了“show global variables”,发现 MySQL 的 max_connections 设置为 151,这足够支持我为 uwsgi 启动的工作进程。
(3)(4)(2) 我正在分析的单个请求是最重的。根据 django-debug-toolbar,它执行了 4 个查询。查询的执行时间分别是: 3.71 毫秒,2.83 毫秒,0.88 毫秒,4.84 毫秒。
(4) 你在提到内存分页吗?如果是的话,我怎么判断?
(5) 在 16 个工作进程、100 个并发率和 1000 个请求的情况下,负载平均值上升到 ~ 12 我在不同数量的工作进程上进行了测试(并发级别为 100):
- 1 个工作进程,负载平均值 ~ 1.85,每秒 19 个请求,请求时间:5229.520 毫秒,0 个非 2xx 响应
- 2 个工作进程,负载平均值 ~ 1.5,每秒 19 个请求,请求时间:516.520 毫秒,0 个非 2xx 响应
- 4 个工作进程,负载平均值 ~ 3,每秒 16 个请求,请求时间:5929.921 毫秒,0 个非 2xx 响应
- 8 个工作进程,负载平均值 ~ 5,每秒 18 个请求,请求时间:5301.458 毫秒,0 个非 2xx 响应
- 16 个工作进程,负载平均值 ~ 19,每秒 15 个请求,请求时间:6384.720 毫秒,0 个非 2xx 响应
如你所见,工作进程越多,系统的负载就越大。我在 uwsgi 的守护进程日志中看到,当我增加工作进程的数量时,响应时间(毫秒)也在增加。
在 16 个工作进程的情况下,运行 500 个并发请求时,uwsgi 开始记录错误:
writev(): Broken pipe [proto/uwsgi.c line 124]
负载也上升到 ~ 10。而且测试并没有花费太多时间,因为非 2xx 响应有 923 个是 1000 个请求中的,这就是为什么响应速度很快,因为几乎是空的。这也回应了你在总结中的第 4 点。
假设我面临的是基于 I/O 和网络的操作系统延迟,那么有什么推荐的措施来扩展这个系统?新的硬件?更大的服务器?
谢谢
3 个回答
如果你增加了更多的工作者,但请求的处理速度却变慢了,这说明你的请求主要是在用CPU,而没有其他的输入输出等待时间,这样其他工作者就没法利用这些时间来处理其他请求。
如果你想要提升处理能力,就需要使用另一台服务器,配备更多或者更快的CPU。
不过,这只是一个理论测试,你在测试中得到的处理速度是你所测试的请求的最高限度。一旦进入实际生产环境,还有很多其他因素会影响性能。
请进行比一分钟更长时间的基准测试,至少要5到10分钟。因为短时间的测试得不到太多有用的信息。还可以使用uWSGI的carbon插件将统计数据推送到carbon/graphite服务器(你需要有一个这样的服务器),这样你就能获得更多调试信息。
当你同时发送500个请求到你的应用时,如果应用无法处理这么大的负载,后端的请求等待队列会很快被填满(默认是100个请求)。你可能想要增加这个数量,但如果工作进程处理请求的速度跟不上,而等待队列(也叫做积压队列)又满了,Linux的网络系统就会丢弃请求,这样你就会开始收到错误信息。
你的第一次基准测试显示你可以在大约42毫秒内处理一个请求,所以一个工作进程最多可以处理1000毫秒 / 42毫秒 ≈ 23个请求每秒(前提是数据库和应用的其他部分在并发增加时没有变慢)。所以,要处理500个同时请求,你至少需要500 / 23 ≈ 21个工作进程(但实际上我建议至少40个),而你只有16个,难怪在这么大的负载下会崩溃。
编辑:我把速率和并发搞混了——至少21个工作进程可以让你每秒处理500个请求,而不是500个同时请求。如果你真的想处理500个同时请求,那你就需要500个工作进程。除非你以异步模式运行你的应用,可以查看uWSGI文档中的“Gevent”部分。
附言:uWSGI自带一个很棒的负载均衡器,支持后端自动配置(可以查看“订阅服务器”和“快速路由器”下的文档)。你可以设置它,让你在需要时热插拔新的后端,只需在新节点上启动工作进程,它们就会订阅快速路由器并开始接收请求。这是横向扩展的最佳方式。而且在AWS上使用后端时,你可以自动化这个过程,以便在需要时快速启动新的后端。
编辑 1 看到了你提到的你有1个虚拟核心,所以我会在所有相关点上添加一些评论。
编辑 2 收到了Maverick的更多信息,因此我会排除一些不相关的想法,专注于确认的问题。
编辑 3 增加了关于uwsgi请求队列和扩展选项的更多细节,语法也进行了改进。
编辑 4 来自Maverick的更新和一些小改进。
评论太少了,所以这里有一些想法:
- 负载平均值基本上是指有多少个进程正在运行或等待CPU处理。对于一个完美负载的系统,1个CPU核心的负载平均值应该是1.0;对于4核系统,应该是4.0。当你运行网络测试时,线程会迅速增加,很多进程在等待CPU。只要负载平均值没有显著超过CPU核心的数量,就不需要担心。
- 第一个“每个请求的时间”值为4秒,这与请求队列的长度有关——1000个请求几乎是瞬间发送到Django,平均处理时间为4秒,其中大约3.4秒是在队列中等待。这是因为请求数量(100)与处理器数量(16)之间的严重不匹配,导致84个请求在某一时刻等待处理器。
在100的并发情况下,测试需要41秒,处理速度为每秒24个请求。你有16个进程(线程),所以每个请求的处理时间大约是700毫秒。考虑到你的交易类型,这对每个请求来说是个很长的时间。这可能是因为:
每个请求在Django中的CPU消耗很高(这不太可能,因为调试工具栏显示的CPU值很低)- 操作系统频繁切换任务(特别是当负载平均值高于4-8时),延迟主要是因为进程太多。
为16个进程提供的数据库连接不够,所以进程在等待可用的连接。每个进程至少有一个连接可用吗?数据库的延迟很大,可能是:很多小请求每个大约需要10毫秒,大部分是网络开销。如果是这样,可以考虑引入缓存或减少SQL调用的数量。或者一个或几个请求需要几百毫秒。要检查这一点,可以对数据库进行性能分析。如果是这样,你需要优化这个请求。
系统和用户CPU成本的分配在系统方面异常高,尽管总CPU使用率很低。这意味着Django中的大部分工作与内核相关,比如网络或磁盘。在这种情况下,可能是网络成本(例如接收和发送HTTP请求以及与数据库的请求)。有时这会因为分页而变高。如果没有分页发生,那么你可能根本不需要担心这个。
- 你设置了16个进程,但负载平均值很高
(具体有多高你没有说明)。理想情况下,你应该始终有至少一个进程在等待CPU(这样CPU就不会闲着)。这里的进程似乎不是CPU绑定的,但延迟很大,所以你需要的进程数量应该超过核心数量。需要多少个?尝试用不同数量的处理器(1, 2, 4, 8, 12, 16, 24等)运行uwsgi,直到你获得最佳吞吐量。如果你改变了平均进程的延迟,你需要再次调整这个。 - 500的并发水平肯定是个问题
,但问题出在客户端还是服务器?报告显示50(共100)有错误的内容长度,这暗示着服务器的问题。非2xx的响应似乎也指向这一点。是否可以捕获非2xx的响应以进行调试——堆栈跟踪或具体的错误信息将非常有用(编辑)并且是由于uwsgi请求队列以默认值100运行造成的。
所以,总结一下:
- Django似乎没问题
- 负载测试的并发(100或500)与进程(16)之间的不匹配:你向系统推送了太多的并发请求,超出了进程的处理能力。一旦超过进程数量,所有发生的事情就是延长了Web服务器中的HTTP请求队列。
存在较大的延迟,所以要么是:
进程(16)与CPU核心(1)之间的不匹配:如果负载平均值大于3,那么可能是进程太多。尝试用更少的进程再试一次。
负载平均值 > 2 - 尝试8个进程负载平均值 > 4 - 尝试4个进程- 负载平均值 > 8 - 尝试2个进程
如果负载平均值 < 3,可能是数据库的问题,所以对数据库进行性能分析,看看是否有很多小请求(累积造成延迟)或一两个SQL语句是问题所在
- 没有捕获失败的响应,我对500并发的失败没有太多可以说的。
发展思路
你在单核心机器上的负载平均值超过10是非常糟糕的(正如你所观察到的),这会导致很多任务切换和整体缓慢的行为。我个人不记得见过负载平均值为19的机器(你在16个进程时的负载)——恭喜你把它搞得这么高;)
数据库性能很好,所以我现在认为它没问题。
分页:关于如何查看分页的问题,你可以通过几种方式检测操作系统的分页。例如,在top命令中,标题有页面输入和输出(见最后一行):
Processes: 170 total, 3 running, 4 stuck, 163 sleeping, 927 threads 15:06:31 Load Avg: 0.90, 1.19, 1.94 CPU usage: 1.37% user, 2.97% sys, 95.65% idle SharedLibs: 144M resident, 0B data, 24M linkedit. MemRegions: 31726 total, 2541M resident, 120M private, 817M shared. PhysMem: 1420M wired, 3548M active, 1703M inactive, 6671M used, 1514M free. VM: 392G vsize, 1286M framework vsize, 1534241(0) pageins, 0(0) pageouts. Networks: packets: 789684/288M in, 912863/482M out. Disks: 739807/15G read, 996745/24G written.
进程数量:在你当前的配置中,进程数量太高了。将进程数量缩减到2。我们可能会在稍后根据将更多负载转移出这个服务器的情况再提高这个值。
Apache Benchmark的位置:一个进程的负载平均值为1.85,这让我觉得你是在与uwsgi运行在同一台机器上进行负载生成——是这样吗?
如果是这样,你真的需要从另一台机器上运行这个测试,否则测试结果无法代表实际负载——你正在占用Web进程的内存和CPU来进行负载生成。此外,负载生成器的100或500个线程通常会以一种在实际情况下不会发生的方式给你的服务器施加压力。实际上,这可能是整个测试失败的原因。
数据库的位置:一个进程的负载平均值也表明你是在与Web进程运行在同一台机器上——是这样吗?
如果我对数据库的判断是正确的,那么开始扩展的第一步和最佳方法是将数据库迁移到另一台机器。我们这样做有几个原因:
数据库服务器需要与处理节点不同的硬件配置:
- 磁盘:数据库需要大量快速、冗余、备份的磁盘,而处理节点只需要基本的磁盘。
- CPU:处理节点需要你能负担得起的最快CPU,而数据库机器通常可以不需要(它的性能往往受限于磁盘和内存)。
- 内存:数据库机器通常需要尽可能多的内存(最快的数据库将所有数据放在内存中),而许多处理节点需要的内存要少得多(你的每个进程大约需要20MB,非常小)。
- 扩展:原子数据库通过拥有强大的多CPU机器来获得最佳扩展,而Web层(没有状态)可以通过插入许多相同的小机器来扩展。
CPU亲和性:让CPU的负载平均值为1.0,并且进程与单个核心绑定是更好的做法。这样可以最大化CPU缓存的使用,最小化任务切换的开销。通过将数据库和处理节点分开,你在硬件上强制执行了这种亲和性。
500并发与异常:上面图中的请求队列最多为100——如果uwsgi在队列满时接收到请求,该请求将被拒绝并返回5xx错误。我认为这在你的500并发负载测试中发生了——基本上队列在前100个线程中填满,然后其他400个线程发出了剩余的900个请求并立即收到了5xx错误。
要处理每秒500个请求,你需要确保两件事:
- 请求队列的大小配置足以处理突发流量:使用
--listen
参数来配置uwsgi
- 系统能够处理超过500个请求每秒的吞吐量,如果500是正常情况,或者在500是峰值时稍低一点。请参见下面的扩展说明。
我想uwsgi将队列设置为较小的数字是为了更好地应对DDoS攻击;如果承受巨大负载,大多数请求几乎不经过处理就立即失败,从而使整个机器仍然对管理员保持响应。
扩展系统的一般建议
你最重要的考虑可能是最大化吞吐量。另一个可能的需求是最小化响应时间,但我在这里不讨论这个。在最大化吞吐量时,你是想最大化系统,而不是单个组件;某些局部的减少可能会提高整体系统的吞吐量(例如,做出一个改变,虽然在Web层增加了延迟,但为了提高数据库的性能,这是一个净收益)。
具体来说:
- 将数据库迁移到单独的机器。在此之后,在负载测试期间通过运行
top
和你喜欢的MySQL监控工具来分析数据库性能。你需要能够进行性能分析。将数据库迁移到单独的机器会在每个请求中引入一些额外的延迟(几毫秒),所以预计需要稍微增加Web层的进程数量,以保持相同的吞吐量。 - 确保
uswgi
请求队列足够大,以处理突发流量,使用--listen
参数。这应该是系统能够处理的最大稳定状态请求每秒的几倍。 在Web/应用层:平衡进程数量与CPU核心数量以及进程中的固有延迟。进程太多会降低性能,太少则意味着你永远无法充分利用系统资源。没有固定的平衡点,因为每个应用程序和使用模式都不同,所以需要基准测试并进行调整。作为指导,使用进程的延迟,如果每个任务有:
- 0%延迟,那么你需要每个核心1个进程
- 50%延迟(即CPU时间是实际时间的一半),那么你需要每个核心2个进程
- 67%延迟,那么你需要每个核心3个进程
在测试期间检查
top
,确保每个核心的CPU利用率超过90%并且负载平均值略高于1.0。如果负载平均值更高,则减少进程数量。如果一切顺利,某个时刻你将无法达到这个目标,数据库可能会成为瓶颈。- 在某个时刻,你将需要在Web层增加更多的处理能力。你可以选择为机器增加更多的CPU(相对简单),从而增加更多的进程,和/或你可以增加更多的处理节点(横向扩展)。后者可以通过在uwsgi中使用讨论过的方法来实现,这里由Łukasz Mierzwa提供。