Nginx 之实现原理
本文主要从 Nginx
的进程模块、事件模块、http网络模块三方面介绍了 Nginx
的底层实现原理,希望你通过本文能对Nginx
的基本实现有一定了解。
进程模块
Nginx 默认采用守护模式启动,守护模式让master进程启动后在后台运行,不在窗口上卡住。
Nginx 启动后会有一个 Master 进程
和多个Worker
进程,Master 进程主要用来管理 Worker 进程,对网络事件进程进行收集和分发,调度哪个模块可以占用 CPU 资源,从而处理请求。Worker进程的个数与机器cpu个数一致
。
Master进程工作原理
Master 进程的工作包括
-
接收来自外界的信号
-
向各worker进程发送信号
-
监控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进程中处理。
采用这种方式的好处:
- 节省锁带来的开销。对于每个worker进程来说,独立的进程,不需要加锁,所以省掉了锁带来的开销,同时在编程以及问题查上时,也会方便很多
- 独立进程,减少风险。采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master进程则很快重新启动新的worker进程
- 在一次请求里无需进程切换
相关配置
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进程处理,在请求多的时候,无需频繁的切换进程。
- master进程先建好需要listen的socket后,然后再fork出多个woker进程,这样每个work进程都可以去accept这个socket
- 当一个client连接到来时,所有accept的work进程都会受到通知,但只有一个进程可以accept成功,其它的则会accept失败,Nginx提供了一把共享锁accept_mutex来保证同一时刻只有一个work进程在accept连接,从而解决惊群问题
- 当一个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种方式的分配
- 轮询(默认)
每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
upstream backserver {
server host:port;
server host:port;
}
- weight
指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况
upstream backserver {
server host:port weight=10;
server host:port weight=10;
}
- ip_hash
每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题
upstream backserver {
ip_hash;
server host:port;
server host:port;
}
- fair(第三方)
按后端服务器的响应时间来分配请求,响应时间短的优先分配。
upstream backserver {
server server1;
server server2;
fair;
}
- 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;
}
}
}
其他
启动流程
- 时间、正则、错误日志、ssl等初始化
- 读入命令行参数
- OS相关初始化
- 读入并解析配置
- 核心模块初始化
- 创建各种暂时文件和目录
- 创建共享内存
- 打开listen的端口
- 所有模块初始化
- 启动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;
}
}