nginx学习笔记(一)

nginx架构

nginx在启动之后,会有一个master进程和多个worker进程。master进程主要用来管理worker进程,master进程的作用主要在于:接受来着外部的信号(例如reload、quit等信号命令),向各worker进程发送信号,监控各个worker进程的运行状态。
nginx学习笔记(一)_第1张图片

由worker进程来真正处理相应的请求业务。

进程模型:

那么,worker进程又是如何处理请求的呢?每个进程处理请求的机会是相等的,当我们提供80端口的http服务时,一个连接请求过来,每个进程都有可能处理这个连接,这是因为每个worker都是由master fork过来的,并且在fork之前都进行了listen的动作,所以每个worker都拥有listenfd,所有的worker进程的listenfd会在新连接到来的时候变得可读,但是为了保证只有一个进程处理该连接,所有worker进程会在注册listenfd的读事件的时候争夺accept_mutex。

采用这种方式的好处:1. 进程独立,不需要加锁,省掉了锁带来的开销。 2. 相对于多线程来讲好调试。等等

事件模型:

nginx采用了异步非阻塞的方式来处理请求,这是为什么呢?看看一个完整的请求过程。首先,请求过来,要建立连接,然后才是接收数据,再发送数据。具体到系统底层就是读写事件,而当读写事件没有准备好时,必然无法操作,这个时候cpu会阻塞等待,阻塞调用会进入内核等待,cpu就会给别人用了,但是对于单线程的worker来说就不合适了,当网络事件越来越多的时候,大家都在等待io完成,cpu无人使用,这个时候cpu使用率就低下。所以,才会有了异步非阻塞的事件处理机制,具体到系统调用就是像 select/poll/epoll/kqueue 这样的系统调用。它们提供了一种机制,让你可以同时监控多个事件,调用他们是阻塞的,但可以设置超时时间,在超时时间之内,如果有事件准备好了,就返回。这种机制正好解决了我们上面的两个问题,拿 epoll 为例(在后面的例子中,我们多以 epoll 为例子,以代表这一类函数),当事件没准备好时,放到 epoll里面,事件准备好了,我们就去读写,当读写返回 EAGAIN 时,我们将它再次加入到 epoll里面。这样,只要有事件准备好了,我们就去处理它,只有当所有事件都没准备好时,才在epoll 里面等着。这样,我们就可以并发处理大量的并发了,当然,这里的并发请求,是指未处理完的请求,线程只有一个,所以同时能处理的请求当然只有一个了,只是在请求间进行不断地切换而已,切换也是因为异步事件未准备好,而主动让出的。这里的切换是没有任何代价,你可以理解为循环处理多个准备好的事件,事实上就是这样的。与多线程相比,这种事件处理方式是有很大的优势的,不需要创建线程,每个请求占用的内存也很少,没有上下文切换,事件处理非常的轻量级。并发数再多也不会导致无谓的资源浪费(上下文切换)。
更多的并发数,只是会占用更多的内存而已。 我之前有对连接数进行过测试,在 24G 内存
的机器上,处理的并发请求数达到过 200 万。现在的网络服务器基本都采用这种方式,这也
是 nginx 性能高效的主要原因

nginx基本概念

在 nginx 中,每个进程会有一个连接数的最大上限,这个上限与系统对 fd 的限制不一
样。在操作系统中,通过 ulimit -n,我们可以得到一个进程所能够打开的 fd 的最大数,即
nofile,因为每个 socket 连接会占用掉一个 fd,所以这也会限制我们进程的最大连接数,当
然也会直接影响到我们程序所能支持的最大并发数,当 fd 用完后,再创建 socket 时,就会
失败。 nginx 通过设置 worker_connectons 来设置每个进程支持的最大连接数。如果该值大于nofile,那么实际的最大连接数是 nofile, nginx 会有警告。 nginx 在实现时,是通过一个连接池来管理的,每个worker进程都有一个独立的连接池,连接池的大小是worker_connections。这里的连接池里面保存的其实不是真实的连接,它只是一个 worker_connections 大小的一个ngx_connection_t 结构的数组。并且, nginx 会通过一个链表 free_connections 来保存所有的空闲 ngx_connection_t,每次获取一个连接时,就从空闲连接链表中获取一个,用完后,再放回空闲连接链表里面 。

ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;    
if (ngx_use_accept_mutex) {
        if (ngx_accept_disabled > 0) {
            ngx_accept_disabled--;

        } else {
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {
                return;
            }

            if (ngx_accept_mutex_held) {
                flags |= NGX_POST_EVENTS;

            } else {
                if (timer == NGX_TIMER_INFINITE
                    || timer > ngx_accept_mutex_delay)
                {
                    timer = ngx_accept_mutex_delay;
                }
            }
        }
    }

那么,我们前面有说过一个客户端连接过来后,多个空闲的进程,会竞争这个连接,很 容易看到,这种竞争会导致不公平,如果某个进程得到 accept 的机会比较多,它的空闲连接很快就用完了,如果不提前做一些控制,当 accept 到一个新的 tcp 连接后,因为无法得到空闲连接,而且无法将此连接转交给其它进程,最终会导致此 tcp 连接得不到处理,就中止掉了。很显然,这是不公平的,有的进程有空余连接,却没有处理机会,有的进程因为没有空余连接,却人为地丢弃连接。那么,如何解决这个问题呢?首先, nginx 的处理得先打开accept_mutex 选项,此时,只有获得了 accept_mutex 的进程才会去添加 accept 事件,也就是说, nginx 会控制进程是否添加 accept 事件。 nginx 使用一个叫 ngx_accept_disabled 的变量来控制是否去竞争 accept_mutex 锁。在第一段代码中,计算 ngx_accept_disabled 的值,这个值是 nginx 单进程的所有连接总数的八分之一,减去剩下的空闲连接数量,得到的这个ngx_accept_disabled 有一个规律,当剩余连接数小于总连接数的八分之一时,其值才大于 0,而且剩余的连接数越小,这个值越大。再看第二段代码,当 ngx_accept_disabled 大于 0 时,不会去尝试获取 accept_mutex 锁,并且将 ngx_accept_disabled 减 1,于是,每次执行到此处时,都会去减 1,直到小于 0。不去获取 accept_mutex 锁,就是等于让出获取连接的机会,很显然可以看出,当空余连接越少时, ngx_accept_disable 越大,于是让出的机会就越多,这样其它进程获取锁的机会也就越大。不去 accept,自己的连接就控制下来了,其它进程的连接池就会得到利用,这样, nginx 就控制了多进程间连接的平衡了。

你可能感兴趣的:(nginx,c)