Nginx实现高并发

注:文章是4年前在自己网站上写的,迁移过来了。现在看我之前写的这篇文章,描述得不是特别详细,但描述了Nginx的整体架构思想。如果对Nginx玩得透得或者想了解深入的,可以在网上找找其他的文章。

...................................................................分割线...........................................................................

Nginx实现高并发的模型是多进程+IO多路复用的方式,实现了系统的异步非阻塞IO。

其实乍看到这是有些疑惑的。使用的epoll这种IO多路复用方式,OS层本身是同步非阻塞的,那为什么说实现了异步呢。这就跟Nginx的实现机制有关系了。关于IO多路复用可见: 并发编程之I/O多路复用

Nginx的多进程模型是一个master+多个worker。master的作用主要是创建,管理子进程worker。之前写过一篇关于gunicorn介绍的,其进程模型也是采用了相同的思想,只是每个worker的处理方式不同。具体可见: Gunicorn介绍

当启动Nginx时,master进程建立了socket文件描述符,然后pre-fork出子进程,子进程会继承master的socket,并执行accept开始listen。

刚才说了,master不负责处理客户端的request请求。真正处理请求的是worker。当有request到来时,会有一个worker进程采用IO多路复用的机制进行处理。

master的主要作用:

  • 接收来自外界的信号; 比如我们在kill进程时,可以直接kill主进程。
  • 向各个worker进程发送信号;
  • 监控worker进程状态;
  • 当worker退出后(异常情况下),自动重新启动新的worker进程

来一张网络上请求处理的示意图:

那么现在问题来了,因为同时有多个woker都在监听,那如何保证同时只有一个worker处理,不会同时唤起所有worker,引起惊群效应呢?

nginx引入了一个叫accept_mutex的互斥锁,同一时刻可以保证只有一个worker获取到互斥锁,只有成功获得锁的worker才可以accept请求,执行后续的请求处理流程,同时ngx_accept_disabled减1。accept处理完之后会释放锁。如果某个worker的队列中连接数已经超过了最大连接的7/8就不再accept请求。

关于nginx的accep_mutax具体可参考: accept_mutex与性能的关系 (nginx)

不过对于只有几个worker的时候,互斥锁没必要打开,就算是多个worker去竞争所带来的消耗也没有多大。此外,对于长连接,可能会导致某个worker长时间占用锁,拖垮整个nginx的性能。

其实后来nginx还引入了两种方式:EPOLLEXCLUSIVE和SO_REUSEPORT,不过这个还要依赖于操作系统的支持。SO_REUSEPORT这个应该在Netty中应该都见过,它会允许多个进程绑定到同一个socket上,相互独立。内核会自动实现多个进程的负载均衡。具体可参考: 惊群和OP_REUSEPORT

Nginx是基于事件驱动的,一个请求过程可以根据事件的触发方式划分为多个阶段,每个阶段都可以由事件收集、分发器来触发。

比如一个请求的读阶段被一个读消费者处理之后,然后就进入空闲状态去处理其他请求片段。等下一次事件出现时,事件分发器会调用事件消费者去处理。这里需要注意的是虽然一个请求分了很多阶段,但他们都是在同一个worker内的。

可以看到,通过这种方式,Nginx的IO多路复用不出现空闲事件,间接上实现了异步非阻塞,是框架层面实现的异步,而非OS层面epoll的同步。

下面代码展示了Nginx主要处理请求的大致过程:

复制代码
for( ; ; )  //  无限循环
{
	nfds = epoll_wait(epfd,events,20,500);  //  最长阻塞 500s
	for(i=0;i<nfds;++i)
	{
		if(events[i].data.fd==listenfd) //有新的连接
		{
			connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接
			ev.data.fd=connfd;
			ev.events=EPOLLIN|EPOLLET;
			epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中
		}
		else if( events[i].events&EPOLLIN ) //接收到数据,读socket
		{
			n = read(sockfd, line, MAXLINE)) < 0    //读
			ev.data.ptr = md;     //md为自定义类型,添加数据
			ev.events=EPOLLOUT|EPOLLET;
			epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓
		}
		else if(events[i].events&EPOLLOUT) //有数据待发送,写socket
		{
			struct myepoll_data* md = (myepoll_data*)events[i].data.ptr;    //取数据
			sockfd = md->fd;
			send( sockfd, md->ptr, strlen((char*)md->ptr), 0 );        //发送数据
			ev.data.fd=sockfd;
			ev.events=EPOLLIN|EPOLLET;
			epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据
		}
		else
		{
			//其他的处理
		}
	}
}
相关推荐
谷新龙0016 分钟前
Elasticsearch服务器开发(第2版) - 读书笔记 第二章 索引
服务器·elasticsearch
dessler17 分钟前
RabbitMQ-镜像队列(Mirrored Queues)
linux·运维·rabbitmq
瑾曦21 分钟前
Docker相关命令
linux
发抖吧小喵喵24 分钟前
rpm包直接安装新系统缺少依赖问题处理
linux·运维·服务器
码农101号1 小时前
Linux中Docker Swarm介绍和使用
linux·spring cloud·docker
Nazi61 小时前
dockerfile基础
linux·运维·docker·容器·云计算
跑不了的你1 小时前
Ubuntu 开启wifi 5G 热点
服务器·5g·ubuntu
所念皆为东辞1 小时前
elk部署加日志收集
linux·elk·elasticsearch·centos
TLucas2 小时前
Centos 7部署.NET 8网站项目
linux·nginx·postgresql·centos·.net