零拷贝和Nginx的阻塞处理

工作原因,这几天将注意力重新放在了Nginx的文件发送上。在下游使能request_body_no_buffering的情况下,又或者,Nginx提供静态文件缓存的情况下,Nginx如何在保证并发的基础上,将磁盘文件高效的传给对端?

Linxu的零拷贝

代理和负载均衡中常出现TCP粘合的概念,当需要根据连接的七层数据(比如一个HTTP GET请求)进行负载均衡时,负载均衡器不得不和客户端建立连接,以获取连接请求,此时数据从下游连接收至应用层,再从应用层由上游连接发出,可以看到,数据收发过程中,不可避免的了进行了copy_to_user、copy_from_user以及user-kernel空间切换。
零拷贝和Nginx的阻塞处理_第1张图片
为解决这一问题,Linux内核提供了splice的系统调用接口,将上游连接通过管道进行粘合,以达到减少拷贝和空间切换的目的。此时数据不再需要拷贝至应用层,直接在内核空间转移至对端。
零拷贝和Nginx的阻塞处理_第2张图片
下面是简单的功能测试程序

#define _GNU_SOURCE
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
	int p[2];
	int fd, down_fd, up_fd, ret;
	struct pollfd set[2];
	struct sockaddr_in addr, server;
	struct in_addr in;
	
	fd = socket(AF_INET, SOCK_STREAM, 0);
	
	addr.sin_family = AF_INET;
	addr.sin_port = htons(80);
	addr.sin_addr.s_addr = inet_addr("192.168.106.177");
	
	bind(fd,(struct sockaddr *)(&addr), sizeof(struct sockaddr));
	listen(fd, 10);
	
	socklen_t len = sizeof(addr);
	down_fd = accept(fd, (struct sockaddr*)(&addr), &len);
	
	inet_aton("192.168.106.176", &in);
	server.sin_family = AF_INET;
	server.sin_addr = in;
	server.sin_port = htons(8888);
	up_fd = socket(AF_INET, SOCK_STREAM, 0);
	
	connect(up_fd, (struct sockaddr*)&server, sizeof(server));
	
	set[0].fd = down_fd;
	set[0].events = POLLOUT | POLLIN;
	set[1].fd = up_fd;
	set[1].events = POLLOUT | POLLIN;
	
	pipe(p);
	
	while (1) {
		if ((ret = poll(set, 2, -1)) < 0) {
			return -1;
		}
		if(set[0].revents & POLLIN) {
			splice(set[0].fd, NULL, p[1], NULL, 65535, SPLICE_F_MORE);
			splice(p[0], NULL, set[1].fd, NULL, 65535, SPLICE_F_MORE);
		}
	}
}

对于文件的读取和发送,Linux也提供了类似的接口即sendfile,磁盘文件被读取至缓存后,被直接拷贝到上游写缓存,省去了User-Kernel拷贝的消耗。

Nginx目前支持sendfile接口的调用,以满足文章开头提到的几种需要发送文件时的高性能传输。而tcp粘合——splice接口,Nginx仍不支持,至于不支持的原因,我在Nginx官方的邮件列表中找到少许解释,一是性能表现没有达到预期,二是无法支持SSL的加解密。TCP粘合后无法提供SSL卸载的加解密显而易见,但作为高度可配置的Nginx,期待某个版本能为stream模块提供一条tcp粘合后的高速路径。

Nginx的多线程模式

Nginx被熟知的是多进程多worker模式,但这种模式下,一旦worker进程阻塞,将造成其他的并发请求无法被处理,这也是Nginx的套接字工作在非阻塞模式、统一调用非阻塞接口的原因。

阻塞对Nginx的影响可以参考DNS功能,在配置解析阶段,所有配置解析阶段的域名解析均调用可能阻塞的系统调用,而一旦业务启动,Nginx不得不自己构造DNS报文并完成DNS报文的异步收发,以完成域名解析。

阻塞对于静态缓存文件发送的影响是同样的,虽然调用了高性能的sendfile接口,但无论磁盘的读取速度还是文件的大小,都会影响worker的处理速度使其它并发请求在一段时间内被阻塞,这在高并发环境下是不能忍受的。所以,Nginx在worker进程的基础上,为每worker配置若干子线程处理阻塞事务,使主线程专注于非阻塞事务的处理。下面对Nginx线程的处理流程做简述:
零拷贝和Nginx的阻塞处理_第3张图片

  1. 主线程作为生产者,通过队列的方式,将阻塞的任务添加到队列中,并为任务设置触发时的回调;
  2. 多个子线程作为消费者,“争抢”队列中的任务,并在获取任务后触发回调,执行阻塞的任务;
  3. 此时,主线程的处理权回归epoll。继续处理其他并发请求;
  4. 当子线程完成任务的回调,通过写eventfd的方式告知主线程处理完成,eventfd被主线程epoll监听;
  5. 主进程epoll触发eventfd读事件,完成该条连接后续非阻塞事务的处理。

你可能感兴趣的:(Nginx)