53

我正在使用 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版本: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 调试工具栏来看看发生了什么。在 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()

..ETC

但是,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

问题是“顶部”显示负载平均值迅速上升,我在本地服务器和网络中的远程机器上运行的 apache 基准测试表明我每秒没有服务很多请求。问题是什么?这是我在分析代码时所能达到的,所以如果有人能指出我在这里做什么,我将不胜感激。

编辑(23/02/2013):根据 Andrew Alcock 的回答添加更多详细信息: 需要我注意/回答的要点是 (3)(3) 我在 MySQL 上执行了“显示全局变量”并发现 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 个工作人员,平均负载 ~ 1.85,19 个请求/秒,每个请求的时间:5229.520,0 个非 2xx
  2. 2 个工作人员,平均负载 ~ 1.5,19 个请求/秒,每个请求的时间:516.520,0 个非 2xx
  3. 4 个工作人员,平均负载 ~ 3,16 个请求/秒,每个请求的时间:5929.921,0 个非 2xx
  4. 8 个工作人员,平均负载 ~ 5,18 个请求/秒,每个请求的时间:5301.458,0 个非 2xx
  5. 16 个工作人员,平均负载 ~ 19,15 个请求/秒,每个请求的时间:6384.720,0 个非 2xx

如您所见,我们拥有的工人越多,系统上的负载就越大。我可以在 uwsgi 的守护进程日志中看到,当我增加工人数量时,响应时间(以毫秒为单位)会增加。

在 16 个工作人员上,运行 500 个并发级别的请求 uwsgi 开始记录错误:

 writev(): Broken pipe [proto/uwsgi.c line 124] 

负载也上升到 ~ 10。并且测试不需要太多时间,因为非 2xx 响应是 1000 中的 923,这就是为什么这里的响应非常快,因为它几乎是空的。这也是对摘要中您的第 4 点的回复。

假设我在这里面临的是基于 I/O 和网络的操作系统延迟,那么建议采取什么措施来扩大它?新硬件?更大的服务器?

谢谢

4

3 回答 3

153

编辑 1看到你有 1 个虚拟核心的评论,通过所有相关点添加评论

编辑 2来自 Maverick 的更多信息,所以我正在消除排除的想法并开发已确认的问题。

编辑 3填写了有关 uwsgi 请求队列和扩展选项的更多详细信息。改进语法。

编辑Maverick 的 4 更新和小改进

评论太少,所以这里有一些想法:

  1. 平均负载基本上是有多少进程正在运行或等待 CPU 关注。对于具有 1 个 CPU 内核的完美负载系统,负载平均值应为 1.0;对于 4 核系统,它应该是 4.0。运行 Web 测试的那一刻,线程迅速增加,并且您有很多进程在等待 CPU。除非负载平均值大大超过 CPU 内核的数量,否则这不是问题
  2. 第一个 'Time per request' 值 4s 与请求队列的长度相关 - 1000 个请求几乎是瞬间转储到 Django 上的,平均需要 4 秒来服务,其中大约 3.4 秒在队列中等待。这是由于请求数量 (100) 与处理器数量 (16) 之间的严重不匹配导致 84 个请求在任何时候都在等待一个处理器。
  3. 以 100 的并发运行,测试需要 41 秒,每秒 24 个请求。您有 16 个进程(线程),因此每个请求的处理时间约为 700 毫秒。鉴于您的交易类型,每个请求需要很长时间。这可能是因为:

    1. 在 Django 中,每个请求的 CPU 成本都很高(鉴于调试工具栏中的 CPU 值较低,这不太可能)
    2. 操作系统经常切换任务(尤其是在平均负载高于 4-8 的情况下),延迟纯粹是因为进程太多。
    3. 没有足够的数据库连接为 16 个进程提供服务,因此进程正在等待有一个可用。每个进程是否至少有一个可用连接?
    4. 数据库周围有相当大的延迟,要么:

      1. 数十个小请求,每一个需要 10 毫秒,其中大部分是网络开销。如果是这样,您能否引入缓存或将 SQL 调用减少到更小的数量。或者
      2. 一个或几个请求占用了 100 毫秒。要检查这一点,请在数据库上运行分析。如果是这样,您需要优化该请求。
  4. 尽管总 CPU 较低,但系统和用户 CPU 成本之间的分配在系统中异常高。这意味着 Django 中的大部分工作都是与内核相关的,例如网络或磁盘。在这种情况下,它可能是网络成本(例如接收和发送 HTTP 请求以及接收和发送请求到数据库)。有时这会因为分页而很高。如果没有分页,那么您可能根本不必担心这一点。

  5. 您已将进程设置为 16,但平均负载较高(您未说明多高)。理想情况下,您应该始终至少有一个进程在等待 CPU(这样 CPU 就不会空转)。这里的进程似乎不受 CPU 限制,但具有显着的延迟,因此您需要比内核更多的进程。还有多少?尝试使用不同数量的处理器(1、2、4、8、12、16、24 等)运行 uwsgi,直到获得最佳吞吐量。如果更改平均进程的延迟,则需要再次调整。
  6. 500并发水平肯定是有问题的,但是是客户端还是服务端?该报告称 50 个(100 个)的内容长度不正确,这意味着服务器存在问题。非 2xx 似乎也指向那里。是否可以捕获非 2xx 响应以进行调试 - 堆栈跟踪或特定错误消息将非常有用(编辑),并且是由运行默认值为 100 的 uwsgi 请求队列引起的。

所以,总结一下:

在此处输入图像描述

  1. Django 看起来不错
  2. 负载测试的并发性(100 或 500)与进程(16)之间的不匹配:您将太多的并发请求推送到系统中,无法处理的进程数。一旦超过了进程数,将会发生的只是你将延长 Web 服务器中的 HTTP 请求队列
  3. 有很大的延迟,所以要么

    1. 进程 (16) 和 CPU 内核 (1) 不匹配:如果平均负载 > 3,则可能是进程过多。使用较少数量的进程重试

      1. 平均负载 > 2 -> 尝试 8 个进程
      2. 平均负载 > 4 -> 尝试 4 个进程
      3. 平均负载 > 8 -> 尝试 2 个进程
    2. 如果平均负载<3,则可能是在数据库中,因此对数据库进行分析以查看是否存在大量小请求(额外导致延迟)或一两个 SQL 语句的问题

  4. 如果不捕获失败的响应,关于 500 并发的失败我无话可说

发展思路

您在单核机器上的平均负载 > 10真的很讨厌,并且(正如您所观察到的)会导致大量任务切换和一般的缓慢行为。我个人不记得看到一台平均负载为 19 的机器(你有 16 个进程) - 恭喜它变得如此之高;)

数据库性能很棒,所以我现在就说清楚了。

分页:回答您有关如何查看分页的问题 - 您可以通过多种方式检测操作系统分页。例如,在顶部,标题有页入和页出(见最后一行):

进程:总共 170 个,运行 3 个,卡住 4 个,休眠 163 个,线程 927 个 15:06:31
平均负载:0.90、1.19、1.94 CPU 使用率:1.37% 用户、2.97% 系统、95.65% 空闲共享库:144M 驻留,0B 数据,24M 链接。
MemRegions:总共 31726 个,2541M 常驻,120M 私有,817M 共享。PhysMem:1420M 有线,3548M 活跃,1703M 不活跃,6671M 使用,1514M 免费。
虚拟机:392G vsize,1286M 框架 vsize,1534241(0) 个分页,0(0) 个分页。网络:数据包:789684/288M 输入,912863/482M 输出。磁盘:读取 739807/15G,写入 996745/24G。

进程数:在您当前的配置中,进程数太高了将进程数缩放回 2。我们可能会在稍后提高这个值,这取决于进一步转移该服务器的负载。

Apache Benchmark 的位置:一个进程的平均负载为 1.85,这表明您在与 uwsgi 相同的机器上运行负载生成器 - 对吗?

如果是这样,您确实需要从另一台机器上运行它,否则测试运行不代表实际负载 - 您从 Web 进程中获取内存和 CPU 以在负载生成器中使用。此外,负载生成器的 100 或 500 个线程通常会对您的服务器造成压力,而这在现实生活中是不会发生的。事实上,这可能是整个测试失败的原因。

数据库的位置:一个进程的平均负载也表明您在与 Web 进程相同的机器上运行数据库 - 这是正确的吗?

如果我对数据库的看法是正确的,那么开始扩展的第一个也是最好的方法是将数据库移动到另一台机器上。我们这样做有几个原因:

  1. 数据库服务器需要与处理节点不同的硬件配置文件:

    1. 磁盘:数据库需要大量快速、冗余、备份的磁盘,而一个处理节点只需要一个基本磁盘
    2. CPU:处理节点需要你能负担得起的最快的 CPU,而 DB 机器通常可以不用(通常它的性能受磁盘和 RAM 的限制)
    3. RAM:DB 机器通常需要尽可能多的 RAM(最快的 DB 将所有数据都保存在 RAM 中),而许多处理节点需要的内存要少得多(每个进程需要大约 20MB - 非常小
    4. 扩展性:原子数据库通过拥有具有许多 CPU 的巨型机器实现最佳扩展性,而 Web 层(没有状态)可以通过插入许多相同的小盒子来扩展。
  2. CPU 亲和性:CPU 的平均负载最好为 1.0,进程与单个核心具有亲和性。这样做可以最大限度地利用 CPU 缓存并最大限度地减少任务切换开销。通过分离 DB 和处理节点,您可以在硬件中强制执行这种关联。

500并发异常上图中的请求队列最多为100——如果uwsgi在队列满的时候接收到请求,请求会被拒绝并报5xx错误。我认为这发生在您的 500 并发负载测试中 - 基本上队列被前 100 个左右的线程填满,然后其他 400 个线程发出剩余的 900 个请求并立即收到 5xx 错误。

要每秒处理 500 个请求,您需要确保两件事:

  1. 请求队列大小配置为处理突发:使用--listen参数uwsgi
  2. 如果 500 是正常情况,系统可以处理每秒超过 500 个请求的吞吐量,或者如果 500 是峰值,则略低于每秒。请参阅下面的缩放说明。

我想 uwsgi 将队列设置为较小的数字以更好地处理 DDoS 攻击;如果置于巨大的负载下,大多数请求会立即失败,几乎没有任何处理,从而使整个盒子仍然可以响应管理员。

扩展系统的一般建议

您最重要的考虑可能是最大化吞吐量。另一个可能需要最小化响应时间,但我不会在这里讨论这个。在最大化吞吐量时,您正在尝试最大化系统,而不是单个组件;一些局部降低可能会提高整体系统吞吐量(例如,为了提高数据库的性能而进行的更改恰好增加了 Web 层的延迟是净收益)。

具体到:

  1. 将数据库移动到单独的机器上top在此之后,通过运行和您最喜欢的 MySQL 监控工具在负载测试期间分析数据库。您需要能够配置文件。将数据库移动到单独的机器上会为每个请求引入一些额外的延迟(几毫秒),因此希望稍微增加 Web 层的进程数以保持相同的吞吐量。
  2. 使用参数确保uswgi请求队列足够大以处理突发流量--listen。这应该是您的系统每秒可以处理的最大稳态请求数的几倍。
  3. 在 web/app 层:平衡进程数与 CPU 内核数和进程中的固有延迟。进程过多会降低性能,过少则意味着您永远无法充分利用系统资源。没有固定的平衡点,因为每个应用程序和使用模式都不同,所以要进行基准和调整。作为指导,使用进程的延迟,如果每个任务都有:

    • 0% 延迟,那么每个核心需要 1 个进程
    • 50% 延迟(即 CPU 时间是实际时间的一半),那么每个核心需要 2 个进程
    • 67% 的延迟,那么每个核心需要 3 个进程
  4. 在测试期间检查top以确保您的 CPU 利用率高于 90%(对于每个核心),并且您的平均负载略高于 1.0。如果平均负载较高,请缩减进程。如果一切顺利,在某些时候你将无法实现这个目标,而 DB 现在可能是瓶颈

  5. 在某些时候,您将需要 Web 层的更多功能。您可以选择向机器添加更多 CPU(相对容易)并因此添加更多进程,和/或您可以添加更多处理节点(水平可扩展性)。后者可以使用Łukasz Mierzwa讨论的方法在uwsgi 中实现
于 2013-02-23T06:53:14.330 回答
7

请运行超过一分钟(至少 5-10 分钟)的基准测试,你真的不会从这么短的测试中获得太多信息。并使用 uWSGI 的 carbon 插件将统计信息推送到 carbon/graphite 服务器(您需要拥有一个),您将获得更多用于调试的信息。

当您向您的应用程序发送 500 个并发请求并且它无法处理此类负载时,每个后端上的侦听队列将很快被填满(默认情况下为 100 个请求),您可能希望增加它,但如果工作人员无法处理请求快速和侦听队列(也称为积压)已满,linux 网络堆栈将丢弃请求,您将开始收到错误。

您的第一个基准测试表明您可以在 ~42 毫秒内处理单个请求,因此单个工作人员最多可以处理 1000 毫秒 / 42 毫秒 = 每秒约 23 个请求(如果 db 和应用程序堆栈的其他部分没有随着并发性的增加而减慢) . 因此,要处理 500 个并发请求,您至少需要 500 / 23 = 21 个工作人员(但实际上我会说至少 40 个),您只有 16 个,难怪它在这样的负载下会中断。

编辑:我混合了并发率 - 至少 21 个工作人员将允许您每秒处理 500 个请求,而不是 500 个并发请求。如果您真的想处理 500 个并发请求,那么您只需要 500 个工作人员。除非您将在异步模式下运行您的应用程序,否则请检查 uWSGI 文档中的“Gevent”部分。

PS。uWSGI 带有带有后端自动配置的出色负载均衡器(阅读“订阅服务器”和“FastRouter”下的文档)。您可以将其设置为允许您根据需要热插拔新后端,您只需在新节点上启动工作人员,他们将订阅 FastRouter 并开始获取请求。这是水平扩展的最佳方式。借助 AWS 上的后端,您可以自动执行此操作,以便在需要时快速启动新的后端。

于 2013-02-24T09:44:55.957 回答
1

添加更多工作人员并获得更少的 r/s 意味着您的请求“是纯 CPU”,并且没有 IO 等待另一个工作人员可以用来服务另一个请求。

如果要扩展,则需要使用具有更多(或更快)cpu 的另一台服务器。

然而,这是一个综合测试,您获得的 r/s 数量是您正在测试的确切请求的上限,一旦投入生产,就会有更多变量会影响性能。

于 2013-02-24T06:47:02.673 回答