工作原因,这几天将注意力重新放在了Nginx的文件发送上。在下游使能request_body_no_buffering的情况下,又或者,Nginx提供静态文件缓存的情况下,Nginx如何在保证并发的基础上,将磁盘文件高效的传给对端?
代理和负载均衡中常出现TCP粘合的概念,当需要根据连接的七层数据(比如一个HTTP GET请求)进行负载均衡时,负载均衡器不得不和客户端建立连接,以获取连接请求,此时数据从下游连接收至应用层,再从应用层由上游连接发出,可以看到,数据收发过程中,不可避免的了进行了copy_to_user、copy_from_user以及user-kernel空间切换。
为解决这一问题,Linux内核提供了splice的系统调用接口,将上游连接通过管道进行粘合,以达到减少拷贝和空间切换的目的。此时数据不再需要拷贝至应用层,直接在内核空间转移至对端。
下面是简单的功能测试程序
#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被熟知的是多进程多worker模式,但这种模式下,一旦worker进程阻塞,将造成其他的并发请求无法被处理,这也是Nginx的套接字工作在非阻塞模式、统一调用非阻塞接口的原因。
阻塞对Nginx的影响可以参考DNS功能,在配置解析阶段,所有配置解析阶段的域名解析均调用可能阻塞的系统调用,而一旦业务启动,Nginx不得不自己构造DNS报文并完成DNS报文的异步收发,以完成域名解析。
阻塞对于静态缓存文件发送的影响是同样的,虽然调用了高性能的sendfile接口,但无论磁盘的读取速度还是文件的大小,都会影响worker的处理速度使其它并发请求在一段时间内被阻塞,这在高并发环境下是不能忍受的。所以,Nginx在worker进程的基础上,为每worker配置若干子线程处理阻塞事务,使主线程专注于非阻塞事务的处理。下面对Nginx线程的处理流程做简述: