Nginx 实现原理

Posted by MartinGuo on August 30, 2016

Nginx 之实现原理

本文主要从 Nginx 的进程模块、事件模块、http网络模块三方面介绍了 Nginx 的底层实现原理,希望你通过本文能对Nginx 的基本实现有一定了解。

进程模块


Nginx 默认采用守护模式启动,守护模式让master进程启动后在后台运行,不在窗口上卡住。

Nginx 启动后会有一个 Master 进程和多个Worker 进程,Master 进程主要用来管理 Worker 进程,对网络事件进程进行收集和分发,调度哪个模块可以占用 CPU 资源,从而处理请求。Worker进程的个数与机器cpu个数一致

进程 icon

Master进程工作原理

Master 进程的工作包括

  1. 接收来自外界的信号

  2. 向各worker进程发送信号

  3. 监控worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程

惊群现象

惊群现象 是指请求到来的时候,只有可以成功accept的那个进程会惊醒,其他进程会继续阻塞。nginx 采用accept-mutex来解决惊群问题,当一个请求到达的时候,只有竞争到锁的worker进程才能处理请求,其他进程结合timer_solution 配置的最大的给你带时间,再去获取监听锁

void
ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
    ngx_uint_t  flags;
    ngx_msec_t  timer, delta;
    if (ngx_timer_resolution) {
        timer = NGX_TIMER_INFINITE;
        flags = 0;
    } else {
        timer = ngx_event_find_timer();
        flags = NGX_UPDATE_TIME;
#if (NGX_THREADS)
    if (timer == NGX_TIMER_INFINITE || timer > 500) {
        timer = 500;
    }
#endif
    }
	/* 检测是否启用mutex,多worker进程下一般都会启用 */
    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) {
                /* 获取到锁之后添加flag */
                flags |= NGX_POST_EVENTS;
            } else {
                /* 如果获取不到锁需要结合timer事件设置下一次抢锁的时间 */
                if (timer == NGX_TIMER_INFINITE
                    || timer > ngx_accept_mutex_delay)
                {
                    timer = ngx_accept_mutex_delay;
                }
            }
        }
    }
    delta = ngx_current_msec;

	/* 开始epoll收集处理事件 */
    (void) ngx_process_events(cycle, timer, flags);

	/* delta就是epoll_wait消耗掉的时间 */
    delta = ngx_current_msec - delta;

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "timer delta: %M", delta);
	/* accept事件已经被加入到单独的任务队列并会被优先处理 */
    ngx_event_process_posted(cycle, &ngx_posted_accept_events);

	/* accept事件处理完之后先释放accept锁,因为其它事件的处理可能耗时较长,不要占着茅坑不睡觉 */
    if (ngx_accept_mutex_held) {
        ngx_shmtx_unlock(&ngx_accept_mutex);
    }
    if (delta) {
        ngx_event_expire_timers();
    }

	/* 之后可以放心处理其它事件了 */
    ngx_event_process_posted(cycle, &ngx_posted_events);
}

Worker进程工作原理

当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了。我们可以看到,一个请求,完全由worker进程来处理,而且只在一个worker进程中处理。

采用这种方式的好处:

  1. 节省锁带来的开销。对于每个worker进程来说,独立的进程,不需要加锁,所以省掉了锁带来的开销,同时在编程以及问题查上时,也会方便很多
  2. 独立进程,减少风险。采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master进程则很快重新启动新的worker进程
  3. 在一次请求里无需进程切换

相关配置

user nobody nobody;
worker_processes  8;
worker_cpu_affinity 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000;
worker_rlimit_nofile 65535;
error_log  logs/error.log  info;

事件模块


对于一个基本的 WEB 服务器来说,事件通常有三种类型,网络事件信号定时器

一个请求的基本过程:建立连接 - 接受连接 - 发送数据,在系统底层就是读写事件

Epoll 模型

Epoll出现在 linux2.6以后,Nginx采用 Epoll 这种异步非阻塞的事件处理机制。这种机制的原理就是把一个完整的请求,划分成多个事件,比如accept(), recv(),磁盘I/O,send(),每个事件都有不同的模块进行处理。一个请求由一个worker进程处理,在请求多的时候,无需频繁的切换进程。

网络事件 icon

  1. master进程先建好需要listen的socket后,然后再fork出多个woker进程,这样每个work进程都可以去accept这个socket
  2. 当一个client连接到来时,所有accept的work进程都会受到通知,但只有一个进程可以accept成功,其它的则会accept失败,Nginx提供了一把共享锁accept_mutex来保证同一时刻只有一个work进程在accept连接,从而解决惊群问题
  3. 当一个worker进程accept这个连接后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完成的请求就结束了

Nginx 使用的是一个进程处理多个连接、非阻塞IO模式 I

Select 模型

Select 模型在启动的时候创建多个进程,放在一个进程池里,并且进程池里的进程数会随着请求数目的增加而增加,对于每一个连接,都是在一个进程内处理完毕。所以Select模型能接收的并发量受到所能开启的进程数影响,进程之间是互相阻塞的,且频繁的切换进程造成大量开销。

Apache 是基于一个线程处理一个请求的非阻塞IO并发策略。

相关配置

events {
	use epoll;
    worker_connections  1024;
}

网络模块

最大连接数

当作为http服务器的时候:

max_clients = worker_processes * worker_connections;

当作为反向代理的时候:

max_clients = worker_processes * worker_connections/4

负载均衡

nginx的upstream目前支持的5种方式的分配

  1. 轮询(默认)

每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。

upstream backserver {
server host:port;
server host:port;
}
  1. weight

指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况

upstream backserver {
server host:port weight=10;
server host:port weight=10;
}
  1. ip_hash

每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题

upstream backserver {
ip_hash;
server host:port;
server host:port;
}
  1. fair(第三方)

按后端服务器的响应时间来分配请求,响应时间短的优先分配。

upstream backserver {
server server1;
server server2;
fair;
}
  1. url_hash(第三方)

按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效

upstream backserver {
server squid1:3128;
server squid2:3128;
hash $request_uri;
hash_method crc32;
}

在需要使用负载均衡的server中增加

proxy_pass http://backserver/ ;
upstream backserver{

ip_hash;
server host:port down; (down 表示单前的server暂时不参与负载)
server host:port weight=2; (weight 默认为1.weight越大,负载的权重就越大)
server host:port
server host:port backup; (其它所有的非backup机器down或者忙的时候,请求backup机器)
}

代理缓存

# 代理缓冲区相关配置
    proxy_buffer_size   128k;
    proxy_buffers   4 256k;
    proxy_busy_buffers_size   256k;

	# 通过令牌桶原理实现用户访问次数限制
    limit_req_zone  $http_x_forwarded_for zone=req_one:10m rate=30r/s;

    #设置Web缓存区名称为cache_web,内存缓存空间大小为300MB,1天没有被访问的内容自动清除,硬盘缓存空间
    #大小为3GB。
    proxy_temp_path   /dev/shm/proxy_temp_dir 1 2;
    proxy_cache_path  /dev/shm/proxy_cache_dir levels=1:2 keys_zone=cache_web:300m inactive=1d max_size=1g;


访问控制

location ~ /\.ht {
      deny all;
}

相关配置

一个http指令下可以配置多个server指令块,一个server指令块里可以根据不同的url做配置

http {
    include   /apps/srv/nginx/conf/mime.types;
    default_type  application/octet-stream;

    log_format    main  '$remote_addr^A $remote_user^A [$time_local]^A $request^A '
                        '$status^A $body_bytes_sent^A $http_referer^A '
                        '$http_user_agent^A $http_x_forwarded_for^A '
            '$request_body^A $http_X_Cache^A $upstream_http_X_Cache^A '
            '$upstream_cache_status^A $http_x_accel_expires^A $dna_device';

    access_log    /apps/srv/nginx/logs/access.log  main;

    sendfile      on;
    tcp_nopush    on;

    #server_names_hash_bucket_size 128;
    #client_header_buffer_size 8k;
    open_file_cache max=10240 inactive=20s;// max打开文件指定缓存数量,inactive指多长时间没请求就删除缓存
    open_file_cache_valid 30s; // 30s检查一次缓存的有效信息
    open_file_cache_min_uses 1;// 最少使用次数,如果超过这个数字,就一直在缓存中打开
    keepalive_timeout  60;

	# 代理缓冲区相关配置
    proxy_buffer_size   128k;
    proxy_buffers   4 256k;
    proxy_busy_buffers_size   256k;

	# 通过令牌桶原理实现用户访问次数限制
    limit_req_zone  $http_x_forwarded_for zone=req_one:10m rate=30r/s;

    #设置Web缓存区名称为cache_web,内存缓存空间大小为300MB,1天没有被访问的内容自动清除,硬盘缓存空间
    #大小为3GB。
    proxy_temp_path   /dev/shm/proxy_temp_dir 1 2;
    proxy_cache_path  /dev/shm/proxy_cache_dir levels=1:2 keys_zone=cache_web:300m inactive=1d max_size=1g;

    upstream www_backend_server {
        server   172.16.2.3:8081;
    }
     upstream m_backend_server {
        server   172.16.2.3:8082;
    }

    server {
        listen          80;
        server_name     www.xxx.com;
        access_log      logs/xxx.access.log main;
        location ~ \.php {
        	include pay_white_list;
        	fastcgi_pass 127.0.0.1:9000;
        	fastcgi_index /index.php;
        	include /apps/srv/nginx/conf/fastcgi_params;

        	fastcgi_buffer_size 128k;
        	fastcgi_buffers 4 256k;
        	fastcgi_busy_buffers_size 256k;

        	fastcgi_split_path_info       ^(.+\.php)(/.+)$;
        	fastcgi_param PATH_INFO       $fastcgi_path_info;
        	fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
        	fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    	}

    	location ~ /\.ht {
        	deny all;
   	 	}
    }

    server {
        listen          80;
        server_name     www.Androidj.com;
        access_log      logs/androidj.access.log main;
        location / {
            index index.html;
            root  /var/www/androidj.com/htdocs;
        }
    }
}

其他

启动流程


  1. 时间、正则、错误日志、ssl等初始化
  2. 读入命令行参数
  3. OS相关初始化
  4. 读入并解析配置
  5. 核心模块初始化
  6. 创建各种暂时文件和目录
  7. 创建共享内存
  8. 打开listen的端口
  9. 所有模块初始化
  10. 启动worker进程

Nginx和PHP交互

通过fastcgi模块进行交互,交互模式有两种:

fastcgi_pass unix:/xxx/php/var/php-cgi.sock;

fastcgi_pass 127.0.0.1:9000;

Mail配置


mail {
    auth_http  127.0.0.1:80/auth.php;
    pop3_capabilities  "TOP"  "USER";
    imap_capabilities  "IMAP4rev1"  "UIDPLUS";

    server {
        listen     110;
        protocol   pop3;
        proxy      on;
    }
    server {
        listen      25;
        protocol    smtp;
        proxy       on;
        smtp_auth   login plain;
        xclient     off;
    }
}