编程技术分享平台

网站首页 > 技术教程 正文

Nginx 引入线程池,性能提升9倍!(nginx线程模型)

xnh888 2024-10-06 05:23:38 技术教程 15 ℃ 0 评论

前言

Nginx 以异步、事件驱动的方式处理连接。传统的方式是每个请求新起一个进程或线程,Nginx没这样做,它通过非阻塞sockets、epoll、kqueue 等高效手段,实现一个 worker 进程处理多个连接和请求。

一般情况下是一个 CPU 内核对应一个 worker 进程,所以 worker 进程数量固定,并且不多,所以在任务切换上消耗的内存和 CPU 减少了。这种方式很不错,在高并发和扩展能力等方面都能体现。

看图说话,任务切换不见了。

但是异步事件模式很不喜欢阻塞(blocking)。很多第三方模块使用了阻塞模式的调用,有时候,用户乃至模块作者都不知道阻塞调用会大大降低 Nginx 的性能。

Nginx 自己的代码都有一些场景需要使用到阻塞,所以在 1.7.11 版本中,引入了新的“线程池”机制,在了解这个机制前,我们先瞅瞅阻塞。

问题

了解阻塞前,先讲两句:

Nginx 其实就是一个事件处理器,接收内核发出的所有与 connections 相关的事件信息,然后告诉操作系统该做什么。操作系统如此复杂和底层,所以 Nginx 的指挥必须叼。

从上图看,有超时、sockets 准备好读写、错误通知等事件。这些事件都放在一个队列中,Nginx 对事件队列进行处理。

如果一个事件对于到的操作非常耗时,那么整个队列的处理就会延迟。

“阻塞操作”就是这样一个导致队列处理延迟的什么鬼。举个例子,CPU 密集型计算,资源访问(硬盘、mutex、同步访问数据库等等)。发生阻塞时,worker 进程只能等待。

就跟过安检时一样,如果你的队伍里面有个小朋友带了一瓶 AD 钙奶,那你只有等他喝完。

有些系统提供的异步文件接口,例如 FreeBSD。Linux 也提供了类似机制,但是不太好用。首先它要求文件或缓存是扇区对齐的,好吧,Nginx 能做到。其次更苛刻的一点是,它要求文件设置 O_DIRECT 标志位,这就是说,所有访问这个文件的操作都是直接读取,不走任何缓存,这样反而会增加磁盘 IO 负担。

问了解决这些问题,1.7.11 版本中引入了线程池。

线程池

你家楼下的顺丰快递就是一个线程池,不用每次寄快递都要去顺丰总部。

对Nginx来说,线程池的作用跟快递点一样。它包括一个任务队列以及配套线程。当一个worker进行需要处理阻塞操作时,它会将这个任务交给线程池来完成。

这里引入了一个新的队列,在例子中,这个队列因为读取资源导致缓慢,读取硬盘虽然慢,至少它不会影响事件队列的继续处理。

任何阻塞操作都可以放到线程池中。目前,我们只尝试了两个核心操作:主流操作系统的read()系统调用和Linux上的sendfile()。后续经过性能测试会考虑纳入更多的操作。

性能测试

为了证实上述理论,进行了如下测试,测试场景包括各种阻塞操作和非阻塞操作。

我们在一台48G内存的机器上生成了总共256的随机文件,每个文件大小为4MB。这样做的目的是保证数据不受内存缓存影响。

简单的配置如下:

配置中进行了一些调优:禁用logging和accpet_mutex,启用sendfile并设置sendfile_max_chunk,有利于减少阻塞调用sendfile时带来的总时间。

测试机器配置为双Intel至强E5645(共12核-24线程),10G网卡,四块西数1003FBYX组成的RAID10,系统为Ubuntu Server 14.04.1 LTS。

两台配置一样的客户端,一台机器通过Lua和wrk随机产生200个并发请求,每个请求都不会命中缓存,所以Nginx处理时会产生读盘阻塞操作。另一台机器则是产生50个并发请求,每个请求读取固定文件,频繁的文件读取会命中缓存,所以一般情况下此类请求处理速度较快,当worker进程阻塞时请求速度会受影响。

通过ifstat和在第二台机器上wrk来监控系统吞吐性能。

无线程池结果:

吞吐量大约是1Gbps,从top看,所有的worker进程主要消耗在阻塞I/O上(top中的D状态)

IO受磁盘限制,CPU多数处于空闲状态。wrk结果表明性能也较低。

需要提醒的是,这些请求原本是应该命中缓存非常快速的,但是因为worker进程受第一台服务器的200并发影响,所以最终比较慢。

接下来对照线程池实验,在location配置中添加一个aio线程指令

重新加载Nginx配置后,重复上述测试

哇,产生了9.5Gbps的吞吐性能。

性能没准还能更改,因为已经达到了网卡瓶颈。这次,worker进程主要消耗在sleeping和时间等待上(top中的S状态)。

就是说,CPU还是很富裕。

wrk的结果相差无几

4MB文件的请求时间从7.41秒提升至了226.32毫秒(约33倍),QPS提升了大约31倍(250比8)。

提升的原因不再赘述,大约就是事件队列没有受阻罢了。

不是万丹灵

看到这里,是不是立马就想去修改你的生产环境了,且慢。

事实上,绝大多数的read和sendfile都是在缓存页中进行的,操作系统会把频繁使用的文件放在缓存页中。

当你的数据量较小,并且内存足够大时,Nginx已经是处于最佳状态了,开线程池反倒会引入开销。线程池能够良好应对的一个场景,是数据无法被完全缓存,例如流媒体服务器,我们上面的测试环境,就是模拟的流媒体服务。

能否用线程池来提升读操作的性能呢?唯一需要做的,就是能有效区分哪些文件已经被缓存,哪些文件未缓存。

咱们的系统没有提供这样的信息。早在2010年Linux尝试通过fincore()来实现未果。接下来是preadv2()和RWF_NONBLOCK标志位方式,可惜也不好用,具体可以参考内核bikeshedding一文。

哈哈,至少FreeBSD用户可以先喝咖啡了,无需在线程池问题上伤脑筋。

线程池配置

如果你确信引入线程池对性能提升有效,那么咱们可以继续了解一些调优参数。

这些调优都是基于1.7.11+ 版本,编译选项为--with-threads参数。最简单的场景下,仅需在http、server或location区块配置aio thread参数即可

它对应的完整配置是:

默认情况下包括一个32个线程的线程池,长度为65536的请求队列。如果队列溢出,Nginx会输出如下错误并拒绝请求

这个错误表示这个线程池消费小于生产,所以可以增加队列长度,如果调整无效,说明系统达到了瓶颈。

另外,我们可以调整线程相关的参数,例如对不同场景,可以提供独立的线程池。

在未定义max_queue时默认为65536,当设置成0时,服务能力等同线程数量。

假如你的缓存代理服务器有3块磁盘,内存不能放下预期需要缓存的文件,所以我们首先需要让磁盘工作最大化。

一个方式是RAID,好坏兼并。另一个方式是Nginx

使用了 3 个独立的缓存,每个缓存指定到一块磁盘,然后有 3 个独立的线程池。

split_clients 模块用于缓存间的负载均衡。

use_temp_path=off 参数让 Nginx 将缓存文件保存至文件同级目录,可以避免缓存更新时磁盘间的文件数据交换。

总结

综上所述,线程池是一个伟大的功能,将 NGINX 推向了新的性能水平,除掉了一个众所周知的长期危害——阻塞——尤其是当我们真正面对大量内容的时候。

甚至,还有更多的惊喜。正如前面提到的,这个全新的接口,有可能没有任何性能损失地卸载任何长期阻塞操作。 NGINX 在拥有大量的新模块和新功能方面,开辟了一方新天地。许多流行的库仍然没有提供异步非阻塞接口,这使得它们无法与 NGINX 兼容。我们可以花大量的时间和资源,去开发我们自己的无阻塞原型库,但这么做始终都是值得的吗?现在,有了线程池,我们可以相对容易地使用这些库,而不会影响这些模块的性能。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表