Nginx

Nginx

Nginx1

ctx context ccf core config

架构2

生产环境下,一个 master 进程,用于加载配置文件和管理 worker 进程, worker 进程间同等竞争,处理请求。

重启 nginx (< 0.8.0) : kill -HUP ngin_pid

首先,每个 worker 进程都是从 master 进程 fork 过来,在 master 进程里面,先建立好需要 listen 的 socket(listenfd)之后,然后再 fork 出多个 worker 进程。所有 worker 进程的 listenfd 会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有 worker 进程在注册 listenfd 读事件前抢 accept_mutex ,抢到互斥锁的那个进程注册 listenfd 读事件,在读事件里调用 accept 接受该连接。当一个 worker 进程在 accept 这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了。

  • 网络事件 - 异步非阻塞 - 高并发
    • select/poll/epoll/kqueue
    • Synchronous/Asynchronous
    • Concurrency/Parallel
    • 并发请求,是指未处理完的请求,线程只有一个,所以同时能处理的请求当然只有一个了,只是在请求间进行不断地切换而已,切换也是因为异步事件未准备好,而主动让出的。这里的切换是没有任何代价,可以理解为循环处理多个准备好的事件。
  • 信号
  • 定时器
    • 由于 epoll_wait 等函数在调用的时候是可以设置一个超时时间的,所以 nginx 借助这个超时时间来实现定时器。nginx 里面的定时器事件是放在一颗维护定时器的红黑树里面,每次在进入 epoll_wait 前,先从该红黑树里面拿到所有定时器事件的最小时间,在计算出 epoll_wait 的超时时间后进入 epoll_wait 。所以,当没有事件产生,也没有中断信号时, epoll_wait 会超时,也就是说,定时器事件到了。这时,nginx 会检查所有的超时事件,将他们的状态设置为超时,然后再去处理网络事件。由此可以看出,当我们写 nginx 代码时,在处理网络事件的回调函数时,通常做的第一个事情就是判断超时,然后再去处理网络事件。
while (true) {
    for t in run_tasks {
        t.handler();
    }
    update_time(&now);
    timeout = ETERNITY;
    for t in wait_tasks { /* sorted already */
        if (t.time <= now) {
            t.timeout_handler();
        } else {
            timeout = t.time - now;
            break;
        }
    }
    nevents = poll_function(events, timeout);
    for i in nevents {
        task t;
        if (events[i].type == READ) {
            t.handler = read_handler;
        } else { /* events[i].type == WRITE */
            t.handler = write_handler;
        }
        run_tasks_add(t);
    }
}

请求流程

worker 进程的 ngx_worker_process_cycle() 函数是无限循环的处理函数,简单处理流程:

  1. 操作系统提供的机制(例如epoll, kqueue等)产生相关的事件。
  2. 接收和处理这些事件,如是接受到数据,则产生更高层的 request 对象。
  3. 处理 request 的 header 和 body 。
  4. 产生响应,并发送回客户端。
  5. 完成 request 的处理。
  6. 重新初始化定时器及其他事件。

当 nginx 读取到一个 HTTP Request 的 header 的时候, nginx 首先查找与这个请求关联的虚拟主机的配置。如果找到了这个虚拟主机的配置,那么通常情况下,这个 HTTP Request 将会经过以下几个阶段的处理(phase handlers):

  1. NGX_HTTP_POST_READ_PHASE: 读取请求内容阶段
  2. NGX_HTTP_SERVER_REWRITE_PHASE: Server 请求地址重写阶段
  3. NGX_HTTP_FIND_CONFIG_PHASE: 配置查找阶段:
  4. NGX_HTTP_REWRITE_PHASE: Location 请求地址重写阶段
  5. NGX_HTTP_POST_REWRITE_PHASE: 请求地址重写提交阶段
  6. NGX_HTTP_PREACCESS_PHASE: 访问权限检查准备阶段
  7. NGX_HTTP_ACCESS_PHASE: 访问权限检查阶段
  8. NGX_HTTP_POST_ACCESS_PHASE: 访问权限检查提交阶段
  9. NGX_HTTP_TRY_FILES_PHASE: 配置项try_files处理阶段
  10. NGX_HTTP_CONTENT_PHASE: 内容产生阶段
  11. NGX_HTTP_LOG_PHASE: 日志模块处理阶段

在内容产生阶段,为了给一个 request 产生正确的响应, nginx 必须把这个 request 交给一个合适的 content handler 去处理。如果这个request对应的location在配置文件中被明确指定了一个 content handler ,那么 nginx 就可以通过对 location 的匹配,直接找到这个对应的 handler ,并把这个 request 交给这个 content handler 去处理。例如 perl, flv, proxy_pass, mp4 等。

如果一个 request 对应的 location 并没有直接有配置的 content handler ,那么 nginx 依次尝试:

  1. random_index on ,那么随机选择一个文件,发送给客户端
  2. index指令,那么发送 index 指令指明的文件,给客户端
  3. autoindex on ,那么就发送请求地址对应的服务端路径下的文件列表给客户端
  4. gzip_static on ,那么就查找是否有对应的 .gz 文件存在,有的话,就发送这个给客户端(客户端支持gzip的情况下)
  5. 请求的URI如果对应一个静态文件, static module 就发送静态文件的内容到客户端

内容产生阶段完成以后,生成的输出会被传递到 filter 模块去进行处理。 filter 模块也是与 location 相关的。所有的 fiter 模块都被组织成一条链。输出会依次穿越所有的 filter ,直到有一个 filter 模块的返回值表明已经处理完成。

这里列举几个常见的filter模块,例如:

  • server-side includes
  • XSLT filtering
  • 图像缩放之类的
  • gzip 压缩

在所有的filter中,有几个filter模块需要关注一下。按照调用的顺序依次说明如下:

  • write: 写输出到客户端,实际上是写到连接对应的socket上
  • postpone: 这个 filter 是负责 subrequest 的,也就是子请求的
  • copy: 将一些需要复制的 buf,文件或者内存,重新复制一份然后交给剩余的 body filter 处理

nginx 将各功能模块组织成一条链,当有请求到达的时候,请求依次经过这条链上的部分或者全部模块,进行处理。每个模块实现特定的功能。

图形化分析 nginx 源码可以用 Sourcetrail3

connection2

nginx 中 connection 就是对 TCP 连接的封装,其中包括连接的 Socket ,读事件,写事件。利用 nginx 封装的 connection 可以很方便的使用 nginx 来处理与连接相关的事情,如 Web 服务、邮件、端口转发等。

ngx_connection_t

连接竞争: 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 越大,于是让出的机会就越多,这样其它进程获取锁的机会也就越大。

ngx_accept_disabled = ngx_cycle->connection_n / 8
    - ngx_cycle->free_connection_n;

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;
        }
    }
}

request

指 http 请求, ngx_http_request_t ,包括 请求行、请求头、请求体、响应行、响应头、响应体。

ngx_http_init_request
- ngx_http_process_request_line
  -- ngx_http_process_request_line
  [schema host uri version]
- ngx_http_process_request_headers
  -- ngx_http_read_request_header
     --- ngx_http_parse_header_line > ngx_http_request_t->headers_in
     --- ngx_http_headers_in (e.g. ngx_http_process_host)
  [headers]
- ngx_http_process_request
  -- ngx_http_request_handler
  -- ngx_http_handler
     -- read_event_handler  > ngx_http_block_reading
     *- write_event_handler > ngx_http_core_run_phases > headers_out
                              *- header filters: ngx_http_header_filter
                              -- ngx_http_copy_filter
                              -- body   filters: ngx_http_write_filter

pipe

nginx 支持 pipeline 的,但对 pipeline 中的多个请求的处理却不是并行的,依然是一个请求接一个请求的处理,只是在处理第一个请求的时候,客户端就可以发起第二个请求。这样,nginx 利用 pipeline 减少了处理完一个请求后,等待第二个请求的请求头数据的时间。

其实 nginx 的做法很简单,在读取数据时,会将读取的数据放到一个 buffer 里面,所以,如果 nginx 在处理完前一个请求后,如果发现 buffer 里面还有数据,就认为剩下的数据是下一个请求的开始,然后就接下来处理下一个请求,否则就设置 keepalive。

lingering_close

延迟关闭,当 nginx 要关闭连接时,并非立即关闭连接,而是先关闭 TCP 连接的写,再等待一段时间后再关掉连接的读。参考 SO_LINGER

非延迟关闭的情况:

nginx 在接收客户端的请求时,可能由于客户端或服务端出错了,要立即响应错误信息给客户端,而 nginx 在响应错误信息后,大分部情况下是需要关闭当前连接。nginx 执行完 write() 系统调用把错误信息发送给客户端, write() 系统调用返回成功并不表示数据已经发送到客户端,有可能还在 TCP 连接的 write buffer 里。接着如果直接执行 close() 系统调用关闭 TCP 连接,内核会首先检查 TCP 的 read buffer 里有没有客户端发送过来的数据留在内核态没有被用户态进程读取:

  • 如果有则发送给客户端 RST 报文来关闭 TCP 连接丢弃 write buffer 里的数据;
  • 如果没有则等待 write buffer 里的数据发送完毕,然后再经过正常的 4 次分手报文断开连接。

所以,当在某些场景下出现 TCP write buffer 里的数据在 write() 系统调用之后到 close() 系统调用执行之前没有发送完毕,且 TCP read buffer 里面还有数据没有读,close() 系统调用会导致客户端收到 RST 报文且不会拿到服务端发送过来的错误信息数据。

而对于延迟关闭,只需要关掉写就行了,读可以继续进行,只需要丢掉读到的任何数据就行了,当关掉连接后,客户端再发过来的数据,就不会再收到 RST 了。

Data Structure

nginx-0.1.0

ngx_str_t

typedef struct {
    size_t    len;
    u_char   *data;
} ngx_str_t;

不以 ’\0’ 结尾,以 len 表示长度,这样可以实现字符串的浅拷贝(指针 + 长度),但是在修改字符串的时候需要十分小心。

这里特别要提醒的是,格式化 ngx_str_t 结构,其对应的转义符是 %V (nginx 自己定义的转义符),传给函数的一定要是指针类型,否则程序就会 coredump 掉。 0.1.0 未实现。

ngx_pool_t

提供了一种机制,帮助管理一系列的资源(如内存,文件等),使得对这些资源的使用和释放统一进行。

typedef struct ngx_pool_s        ngx_pool_t;

struct ngx_pool_s {
    char              *last;
    char              *end;
    ngx_pool_t        *next;
    ngx_pool_large_t  *large;
    ngx_log_t         *log;
};
typedef struct ngx_pool_cleanup_s  ngx_pool_cleanup_t;
typedef void (*ngx_pool_cleanup_pt)(void *data);
/* ngx_pool_cleanup_pt 是指针,指向 没有返回值的 接受一个指针类型 的参数 的函数 的别名 */

struct ngx_pool_cleanup_s {
    ngx_pool_cleanup_pt   handler;
    void                 *data;
    ngx_pool_cleanup_t   *next;
};

ngx_array_t

typedef struct ngx_array_s       ngx_array_t;

struct ngx_array_s {
    void        *elts;
    ngx_uint_t   nelts;
    size_t       size;
    ngx_uint_t   nalloc;
    ngx_pool_t  *pool;
};
/*
elts:    指向实际的数据存储区域。
nelts:   数组实际元素个数。
size:    数组单个元素的大小,单位是字节。
nalloc:  数组的容量。表示该数组在不引发扩容的前提下,可以最多存储的元素的个数。当 nelts 增长到达 nalloc  时,如果再往此数组中存储元素,则会引发数组的扩容。数组的容量将会扩展到原有容量的 2 倍大小。实际上是分配新的一块内存,新的一块内存的大小是原有内存大小的 2 倍。原有的数据会被拷贝到新的一块内存中。
pool:    该数组用来分配内存的内存池。
*/

由于使用 ngx_palloc 分配内存,数组在扩容时,旧的内存不会被释放,会造成内存的浪费。

ngx_hash_t

0.1.0 未实现。

对于常用的解决冲突的方法有线性探测,二次探测和开链法等。ngx_hash_t 使用的是最常用的一种,也就是开链法,这也是 STL 中的 hash 表使用的方法。

  1. ngx_hash_t 不像其他的 hash 表的实现,可以插入删除元素,它只能一次初始化,就构建起整个 hash 表。之后既不能删除,也不能插入元素;
  2. ngx_hash_t 的开链并不是真的开了一个链表,实际上是开了一段连续的存储空间,几乎可以看做是一个数组。这是因为 ngx_hash_t 在初始化的时候,会经历一次预计算的过程,提前把每个桶里面会有多少元素放进去给计算出来,这样就提前知道每个桶的大小了。那么就不需要使用链表,一段连续的存储空间就足够了。这也从一定程度上节省了内存的使用。

ngx_chain_t

nginx 的 filter 模块在处理从别的 filter 模块或者是 handler 模块传递过来的数据(实际上就是需要发送给客户端的http response)。这个传递过来的数据是以一个链表的形式 ngx_chain_t 。而且数据可能被分多次传递过来。

typedef struct ngx_chain_s       ngx_chain_t;

struct ngx_chain_s {
    ngx_buf_t    *buf;
    ngx_chain_t  *next;
};

ngx_buf_t

ngx_buf_t 就是 ngx_chain_t 链表的每个节点的实际数据。该结构实际上是一种抽象的数据结构,它代表某种具体的数据。这个数据可能是指向内存中的某个缓冲区,也可能指向一个文件的某一部分,也可能是一些纯元数据(元数据的作用在于指示这个链表的读取者对读取的数据进行不同的处理)。

typedef struct ngx_buf_s  ngx_buf_t;

struct ngx_buf_s {
    u_char          *pos;
    u_char          *last;
    off_t            file_pos;
    off_t            file_last;

    int              type;
    u_char          *start;         /* start of buffer */
    u_char          *end;           /* end of buffer */
    ngx_buf_tag_t    tag;
    ngx_file_t      *file;
    ngx_buf_t       *shadow;


    /* the buf's content could be changed */
    unsigned         temporary:1;

    /*
     * the buf's content is in a memory cache or in a read only memory
     * and must not be changed
     */
    unsigned         memory:1;

    /* the buf's content is mmap()ed and must not be changed */
    unsigned         mmap:1;

    unsigned         recycled:1;
    unsigned         in_file:1;
    unsigned         flush:1;
    unsigned         last_buf:1;

    unsigned         last_shadow:1;
    unsigned         temp_file:1;

    unsigned         zerocopy_busy:1;

    /* STUB */ int   num;
};

ngx_list_t

ngx_list_t 的节点实际上是一个固定大小的数组,首节点需要初始化(自身),之后的节点为 ngx_list_part_t 结构。

typedef struct ngx_list_part_s  ngx_list_part_t;

struct ngx_list_part_s {
    void             *elts;
    ngx_uint_t        nelts;
    ngx_list_part_t  *next;
};

typedef struct {
    ngx_list_part_t  *last;
    ngx_list_part_t   part;
    size_t            size;
    ngx_uint_t        nalloc;
    ngx_pool_t       *pool;
} ngx_list_t;

ngx_queue_t

0.1.0 未实现。

ngx_queue_t 是一种的双向链表。

typedef struct ngx_queue_s ngx_queue_t;

struct ngx_queue_s {
    ngx_queue_t  *prev;
    ngx_queue_t  *next;
};

ngx_queue_t 只是声明了前向和后向指针。在使用的时候,我们首先需要定义一个哨兵节点(对于后续具体存放数据的节点,我们称之为数据节点),比如:

ngx_queue_t free;

接下来初始化:

ngx_queue_init(&free);

#define ngx_queue_init(q)     \
    (q)->prev = q;            \
    (q)->next = q;

q->next 头节点; q->prev 尾节点。

取 middle 节点, middle 向后移动 1 步, next 向后移动 2 步,当 next 等于尾节点时,此时 middle 就指向中间节点,时间复杂度 O(N) 。

Configuration & Directives

在 nginx.conf 中,包含若干配置项。每个配置项由配置指令和指令参数 2 个部分构成。指令参数也就是配置指令对应的配置值。

指令概述:配置指令是一个字符串,可以用单引号或者双引号括起来,也可以不括。但是如果配置指令包含空格,一定要引起来。

指令参数:指令的参数使用一个或者多个空格或者 TAB 字符与指令分开。指令的参数有一个或者多个 TOKEN 串组成。 TOKEN 串之间由空格或者 TAB 键分隔。TOKEN 串分为简单字符串或者是复合配置块。复合配置块即是由大括号括起来的一堆内容。一个复合配置块中可能包含若干其他的配置指令。

环境上下文: main http server location mail 。

模块:nginx 将各功能模块组织成一条链,当有请求到达的时候,请求依次经过这条链上的部分或者全部模块,进行处理。每个模块实现特定的功能。模块的分类:

  • event module
  • phase handler
  • output filter
  • upstream
  • load-balancer

sendfile

默认 off ,配置是否由 Linux 内核直接读取文件( System Call ),而不是通过 read, write 来操作。

tcp_nopush

默认 off ,配置是否开启 socket 的 TCP_CORK 选项。

tcp_nodelay

默认 on ,配置是否关闭 nagle 算法,即 TCP_NODELAY 选项。

worker_connections

默认 1024 ,代表每个 worker 进程的一个 worker_connections 大小的一个 ngx_connection_t 结构的数组。并且,nginx 会通过一个链表 free_connections 来保存所有的空闲 ngx_connection_t ,每次获取一个连接时,就从空闲连接链表中获取一个,用完后,再放回空闲连接链表里面。

对于静态资源服务器, nginx 的最大连接数为: worker_connections × worker_processes ; 对于反向代理服务器, nginx 的最大连接数为: worker_connections × worker_processes / 2 。

client_header_buffer_size

nginx 会将整个请求头都放在 buffer 里面,配置这个 buffer 的大小。

keepalive_timeout

默认 60 ,配置长连接的超时时间。

large_client_header_buffers

默认 4 8k ,表示 4 个 8k 的 buffer ,在 client_header_buffer_size 装不下, nginx 重新分配一个更大的 buffer 情况下,这个 buffer 的数量和大小。

请求行大于 buffer ,返回 414 ;请求头大于 buffer ,返回 400 。

lingering_close

配置开启或关闭延迟关闭。

lingering_timeout

配置延迟关闭超时时间。

access_log

log_format combined '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';
access_log logs/access.log combined;

使用 JSON 格式:

log_format json_analytics escape=json '{'
    '"msec": "$msec", '                                         # request unixtime in seconds with a milliseconds resolution
    '"connection": "$connection", '                             # connection serial number
    '"connection_requests": "$connection_requests", '           # number of requests made in connection
    '"pid": "$pid", '                                           # process pid
    '"request_id": "$request_id", '                             # the unique request id
    '"request_length": "$request_length", '                     # request length (including headers and body)
    '"remote_addr": "$remote_addr", '                           # client IP
    '"remote_user": "$remote_user", '                           # client HTTP username
    '"remote_port": "$remote_port", '                           # client port
    '"time_local": "$time_local", '
    '"time_iso8601": "$time_iso8601", '                         # local time in the ISO 8601 standard format
    '"request": "$request", '                                   # full path no arguments if the request
    '"request_uri": "$request_uri", '                           # full path and arguments if the request
    '"args": "$args", '
    '"status": "$status", '                                     # response status code
    '"body_bytes_sent": "$body_bytes_sent", '                   # the number of body bytes exclude headers sent to a client
    '"bytes_sent": "$bytes_sent", '                             # the number of bytes sent to a client
    '"referer": "$http_referer", '
    '"user_agent": "$http_user_agent", '
    '"x_forwarded_for": "$http_x_forwarded_for", '              # http_x_forwarded_for
    '"host": "$http_host", '                                    # the request Host: header
    '"server_name": "$server_name", '                           # the name of the vhost serving the request
    '"request_time": "$request_time", '                         # request processing time in seconds with msec resolution
    '"upstream": "$upstream_addr", '                            # upstream backend server for proxied requests
    '"upstream_connect_time": "$upstream_connect_time", '       # upstream handshake time incl. TLS
    '"upstream_header_time": "$upstream_header_time", '         # time spent receiving upstream headers
    '"upstream_response_time": "$upstream_response_time", '     # time spend receiving upstream body
    '"upstream_response_length": "$upstream_response_length", ' # upstream response length
    '"upstream_cache_status": "$upstream_cache_status", '       # cache HIT/MISS where applicable
    '"ssl_protocol": "$ssl_protocol", '                         # TLS protocol
    '"ssl_cipher": "$ssl_cipher", '                             # TLS cipher
    '"scheme": "$scheme", '                                     # http or https
    '"request_method": "$request_method", '                     # request method
    '"server_protocol": "$server_protocol", '                   # request protocol, like HTTP/1.1 or HTTP/2.0
    '"geoip_country_code": "$geoip_country_code", '
    '"geoip_city": "$geoip_city"'
'}';

access_log     /var/data/log/nginx/json_access.log json_analytics buffer=4k flush=60 if=$logable;

Variables

upstream_response_length

$upstream_response_length keeps the length of the response obtained from the upstream server (0.7.27); the length is kept in bytes. Lengths of several responses are separated by commas and colons like addresses in the $upstream_addr variable.

upstream_response_length 在配置了 error_page 等内部重定向时为 0。

Footnotes:

湘ICP备19014083号-1