hi。
众所周知,nginx分为多个worker子进程(一般每个CPU核一个worker),每个子进程独立去accept新连接。
一旦连接进入了某个worker之后,就只能被这个worker来处理了,这种连接的分配并不是以每个worker的工作负载来决定的。
假设每个连接都是长连接,连接内的每个请求的处理都是CPU-bound类型,例如jpeg到webp的图片格式转换,图片大小是随机的,所以处理时间在1ms到10s的范围里浮动,那么这时候就有问题了,假设A worker和B worker都accept了10个连接,而B worker里面的请求很快做完了,A worker的某个图片还在处理中,要等3s后才能处理完,而A的请求队列里还有3个请求在等待,但却不能切换到B去处理,导致明明有CPU资源也用不上。
如果使用多线程,线程间抢夺请求来处理,这样整体性能会好很多,平均响应时间也会小很多。
对于http 2.0来说,每个连接内的多个请求可以独立进行处理和响应,上述问题会更加严重。
> 众所周知,nginx分为多个worker子进程(一般每个CPU核一个worker),每个子进程独立去accept新连接。
> 一旦连接进入了某个worker之后,就只能被这个worker来处理了,这种连接的分配并不是以每个worker的工作负载来决定的。
事实上,如果某个 worker 进程较忙的话,就会由其他 worker 进程 accept 新连接。毕竟繁忙的 worker 响应
accept 事件的机会同比小一些。这里重要的是,禁掉 accept_mutex 配置,因为它会导致各 worker accept
新连接不够均衡:
http://nginx.org/en/docs/ngx_core_module.html#accept_mutex
同时不要开启 multi_accept 配置选项。
> 假设每个连接都是长连接,连接内的每个请求的处理都是CPU-bound类型,例如jpeg到webp的图片格式转换,图片大小是随机的,所以处理时间在1ms到10s的范围里浮动,那么这时候就有问题了,假设A
> worker和B worker都accept了10个连接,
你这里的前提就不成立,显然 CPU 使用率不同的 nginx worker 进程响应 accept 事件的频率也不尽相同。
> 对于http 2.0来说,每个连接内的多个请求可以独立进行处理和响应,上述问题会更加严重。
http 2.0 和 spdy 在协议设计上面确实很容易被攻击者滥用而导致服务器过载。所以一般最好在做累活的 nginx 之前放一个专用的
https 网关 nginx 实例(二者之间可以通过 unix domain socket 进行本地通信,这样也利于规避 SSL
计算引入的额外的阻塞效应)。
这里我再澄清一下,我知道繁忙的worker能接受的连接数同比会降低,在连接层面,我觉得是worker间是能均衡的。但是我这里指的是长连接进入某个worker后,连接内的请求被处理的不均匀,而请求无法在worker之间迁移。
举个例子,某个时刻A worker很繁忙,所以它只接受了1个新连接,而B worker比较空闲,它接受了10个新连接,在那个时刻,这样的分配是合理的。
但是,如果A的那1个连接里面的请求对应的处理时间比那B的那10个连接的请求要大的话,就会导致CPU资源分配不均匀了。
例如A的那1个连接里的多个请求平均处理时间是3s,而B的10个连接的请求平均处理时间为3ms,那到达某个时间点,B worker做完所有请求了,而A worker里面还有大量请求被堆积,但却没法被迁移到B worker去处理。
总而言之,我的问题就是nginx的worker之间的调度单位是连接,而不是请求,所以如果连接对应的是长连接,并且连接里面的请求对应的处理时间浮动很大很随机,那么nginx的处理能力就有问题了。
http1.1的长连接尚且如此,那http2.0天生就利用长连接来干活,连接内的请求互相独立,这个问题就更严重了。
反观golang的goroutine设计,每个goroutine可以用来表示一个请求的处理,而goroutine可以在多个线程(对应多个CPU核)之间迁移,使得每个CPU核都能够被充分利用,这样的模型会不会比nginx要好呢?
我还有一点困惑,nginx使用的是进程模型,每个进程的地址空间是独立的,那么进程间需要共享数据的时候就要用到共享内存。但进程间对共享内存的操作绝大多数都不是原子操作,例如nginx的slab,分配一块内存的操作分为很多个步骤,要对共享内存中不同的位置进行写。假设进程在执行操作期间崩溃了,有些步骤没做完,例如重置指针的值,就会使得共享内存处于一个不一致的状态,这种状态是没法恢复的,因为其他进程无法知道这个进程留下什么烂摊子给它收拾,也没有一个进程为此负责,因为进程间都是对等的,所以这个不一致的状态很难被察觉和调试的,而系统继续运行会产生很多意想不到的错误;另外,操作前后难免要上锁解锁,例如pthread锁(文件锁倒是进程退出后OS可以帮你清理的,但相比pthread锁更耗时,因为涉及系统调用和内核介入),那么崩溃后,这个锁没法被重置,导致其他等待该锁的进程被锁死。所以一般多进程程序如果要对共享内存读写,都是由一个主控进程来进行写,其他进程来读,这样才可以避免上述情况。nginx似乎没考虑到这个问题?