最近测试一个 python 的分布式系统,整理了一些性能测试经验与技术的总结。

系统架构

arch.png

图为系统架构,基于 python celery 的一套分布式消息处理系统,分为几个部份:

  1. web:用于提供 web 服务,并把用户的任务(如发邮箱、保存数据等)发送到 redis 中。
  2. worker:消息处理者,从 redis 取出任务进行处理,如保存数据到数据库、发邮件。
  3. redis:存储任务消息。
  4. postgresql:数据持久化。

部署在两台机器中,分别如下:

  1. web 和 worker 放在一台。
  2. redis 和 postgresql 放在一台。

现在要评估机器可以处理的请求量,优化 web 调用链路上的处理时间,可以处理更多的用户请求。采用 AB 进行压测试,压到一定量时,客户端会收到很多 “connection reset by peer”、服务端报如 “too many open files” 、“速度非常慢”等问题,这些问题是怎么产生的?系统可以支撑多少的访问量?怎么样去优化?接下来解答这些问题。

一次 http 请求的处理过程

request.png

图中是一个请求的处理过程,可以看到,经过了 5 个步骤把这个请求处理完,其中第 1 步和第 5 步为网络传输的时间,要注意的是在用 ab 做压力测试的时候是有把这两个时间算到 qps 里面的,从上图来解释我的疑问。

文件描述符

首先,服务端要和客户端建立 socket 连接,接受客户端的请求。linux 是面向文件的操作系统,通过文件描述符操作文件、socket、或其它的 io,文件描述符是一个非负的整数,每个 socket 连接为一个文件描述符,要接受更多的 socket 连接,就必须要保证有足够多的文件描述符,当文件描述符不够用了,程序会抛出 “too many open files” 的错误,如:

1
2
3
4
5
{ [Error: EMFILE: too many open files, open '/home/parallels/work/fs/index.js']
errno: -24,
code: 'EMFILE',
syscall: 'open',
path: '/home/parallels/work/fs/index.js' }

可以通过修改系统的参数来提高文件描述符数量,参考:

1
2
3
4
5
6
7
修改最大的文件描述符:
1. 打开文件: vi /etc/sysctl.conf
2. 修改最大文件描述符的值: fs.file-max = 10000
3. 启用: sysctl -p
查看最大文件描述符:
sysctl -a | grep fs.file-max

socket 连接队列

客户端并发多个请求时,每接受一个客户端 socket 的请求就会占用一个文件描述符,通过这个描述符,我们就可以接受客户端发送的内容、发送内容到客户端、关闭连接等操作,服务端要把这些文件描述符保存起来,由应用程序去处理,操作系统会维护一个 socket 全连接队列 记录这些描述符,当有新的客户端连接建立时,则插到这个队列中,当客户端发送的量太大了,把这个队列压满了,则服务端会抛 “connect reset by peer” 的错误到客户端。参考代码

1
2
3
4
5
6
7
/* Accept backlog is full. If we have already queued enough
* of warm entries in syn queue, drop request. It is better than
* clogging syn queue with openreqs with exponentially increasing
* timeout.
*/
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
goto drop;

保证这个队列不被压满有多个方法,增大队列大小、快速完成队列中的每个任务。

在创建服务端的 socket 时,可通过 listen 方法设置队列的大小,如

1
listen(serverfd, 10000)

uwsgi 有一个配置参数 listen 用来设置 socket 队列大小,如下为 uwsgi 在创建服务端的 socket 时的代码:参考

1
2
3
bind_to_tcp(shared_sock->name, uwsgi.listen_queue, tcp_port)
....
if (listen(serverfd, listen_queue) != 0)

业务处理

快速消费队列中的任务,则取绝于我们的业务代码的处理速度,每个任务处理完后就会关闭,从而从队列中移除,业务代码处理的越快,队列就会越来越小,这时可以增加更多的进程来并发处理更多的任务,uwsgi 可以通过 worker 参数设置开启的进程数量。我做了个测试开启 40 个进程,如下图,可以看到当前 socket 连接队列(lq)有 4737 个,处理速度是1895 RPS。此进队列积的很多,这就造成了客户端响应慢,同时也可能会造成客户端超时

qps.png

总结

  1. 碰到 “too many open files” 时,加大文件描述符的数量
  2. 碰到 “connection reset by peer “时,加大 socket 全连接的数量
  3. 提高业务代码的处理速度

接下来还有两个问题,后续再整理:

  1. 系统可以支撑多少的访问量?
  2. 怎么样去优化?