前言
- Nginx的Master-Worker模式
- Nginx如何做到热部署
- Nginx的反向代理服务
- Nginx的epoll模型
- Keepalived实现Nginx高可用
Nginx
是一款由俄罗斯程序员Igor Sysoev所开发的轻量级WEB服务器
、反向代理服务器
以及电子邮件(IMAP/POP3)代理服务器
。相较于Apache
、lighttpd具有占用内存少、稳定性高等优势
,依靠其强大的并发能力、丰富的模板库以及友好灵活的配置
而闻名。
Nginx的Master-Worker模式
启动nginx服务后,在80端口启动socket服务进行监听,可以使用netstat来查看这个监听端口:
Nginx之所以被称为 高性能服务器
,这与他的设计架构与工作原理
是密不可分的。如下图所示,它采用master进程和worker协同工作:
Nginx在启动之后,会有一个master进程和多个worker进程,两者的作用如下:
master进程
:读取并配置文件nginx.conf;管理worker进程:向多个worker进程发送signal,监控workder进程的运行状态,当worker进程异常退出
时,会自动启动新的worker
进程。
worker进程
:为了避免线程切换
,每一个worker进程
都维护一个线程
来处理连接和请求。
多个worker进程之间是对等
的,他们同等竞争来自client的请求
,并且一个请求只能在一个worker进程中处理(进程间的相互独立性)。worker进程的个数由conf文件
决定,一般和CPU的核心数一致。
如果你对上述文字描述不解,那我们模拟一下master-worker的工作原理流程图:
下述代码是从nginx的源码提取出来的master工作部分
,可以看出,master进程中的for(;;)
死循环内有一个关键的sigsuspend()函数调用
,该函数调用使得master进程
的大部分时间都处于挂起等待状态,直到master进程接收的信号为止:
void ngx_master_process_cycle(ngx_cycle_t *cycle)
{
char *title;
u_char *p;
size_t size;
ngx_int_t i;
ngx_uint_t n, sigio;
sigset_t set;
struct itimerval itv;
ngx_uint_t live;
ngx_msec_t delay;
ngx_listening_t *ls;
ngx_core_conf_t *ccf;
//信号处理设置工作
sigemptyset(&set);
sigaddset(&set, SIGCHLD);
sigaddset(&set, SIGALRM);
sigaddset(&set, SIGIO);
sigaddset(&set, SIGINT);
sigaddset(&set, ngx_signal_value(NGX_RECONFIGURE_SIGNAL));
sigaddset(&set, ngx_signal_value(NGX_REOPEN_SIGNAL));
sigaddset(&set, ngx_signal_value(NGX_NOACCEPT_SIGNAL));
sigaddset(&set, ngx_signal_value(NGX_TERMINATE_SIGNAL));
sigaddset(&set, ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
sigaddset(&set, ngx_signal_value(NGX_CHANGEBIN_SIGNAL));
if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"sigprocmask() failed");
}
sigemptyset(&set);
size = sizeof(master_process);
for (i = 0; i < ngx_argc; i++) {
size += ngx_strlen(ngx_argv[i]) + 1;
}
title = ngx_pnalloc(cycle->pool, size);
p = ngx_cpymem(title, master_process, sizeof(master_process) - 1);
for (i = 0; i < ngx_argc; i++) {
*p++ = ' ';
p = ngx_cpystrn(p, (u_char *) ngx_argv[i], size);
}
ngx_setproctitle(title);
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module);
//其中包含了fork产生子进程的内容
ngx_start_worker_processes(cycle, ccf->worker_processes,
NGX_PROCESS_RESPAWN);
//Cache管理进程与cache加载进程的主流程
ngx_start_cache_manager_processes(cycle, 0);
ngx_new_binary = 0;
delay = 0;
sigio = 0;
live = 1;
for ( ;; ) {//循环
if (delay) {
if (ngx_sigalrm) {
sigio = 0;
delay *= 2;
ngx_sigalrm = 0;
}
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"termination cycle: %d", delay);
itv.it_interval.tv_sec = 0;
itv.it_interval.tv_usec = 0;
itv.it_value.tv_sec = delay / 1000;
itv.it_value.tv_usec = (delay % 1000 ) * 1000;
if (setitimer(ITIMER_REAL, &itv, NULL) == -1) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"setitimer() failed");
}
}
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "sigsuspend");
sigsuspend(&set);//master进程休眠,等待接受信号被激活
ngx_time_update();
ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"wake up, sigio %i", sigio);
//标志位为1表示需要监控所有子进程
if (ngx_reap) {
ngx_reap = 0;
ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0, "reap children");
live = ngx_reap_children(cycle);//管理子进程
}
//当live标志位为0(表示所有子进程已经退出)、ngx_terminate标志位为1或者ngx_quit标志位为1表示要退出master进程
if (!live && (ngx_terminate || ngx_quit)) {
ngx_master_process_exit(cycle);//退出master进程
}
//ngx_terminate标志位为1,强制关闭服务,发送TERM信号到所有子进程
if (ngx_terminate) {
if (delay == 0) {
delay = 50;
}
if (sigio) {
sigio--;
continue;
}
sigio = ccf->worker_processes + 2 /* cache processes */;
if (delay > 1000) {
ngx_signal_worker_processes(cycle, SIGKILL);
} else {
ngx_signal_worker_processes(cycle,
ngx_signal_value(NGX_TERMINATE_SIGNAL));
}
continue;
}
//ngx_quit标志位为1,优雅的关闭服务
if (ngx_quit) {
ngx_signal_worker_processes(cycle,
ngx_signal_value(NGX_SHUTDOWN_SIGNAL));//向所有子进程发送quit信号
ls = cycle->listening.elts;
for (n = 0; n < cycle->listening.nelts; n++) {//关闭监听端口
if (ngx_close_socket(ls[n].fd) == -1) {
ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_socket_errno,
ngx_close_socket_n " %V failed",
&ls[n].addr_text);
}
}
cycle->listening.nelts = 0;
continue;
}
//ngx_reconfigure标志位为1,重新读取配置文件
//nginx不会让原来的worker子进程再重新读取配置文件,其策略是重新初始化ngx_cycle_t结构体,用它来读取新的额配置文件
//再创建新的额worker子进程,销毁旧的worker子进程
if (ngx_reconfigure) {
ngx_reconfigure = 0;
//ngx_new_binary标志位为1,平滑升级Nginx
if (ngx_new_binary) {
ngx_start_worker_processes(cycle, ccf->worker_processes,
NGX_PROCESS_RESPAWN);
ngx_start_cache_manager_processes(cycle, 0);
ngx_noaccepting = 0;
continue;
}
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reconfiguring");
//初始化ngx_cycle_t结构体
cycle = ngx_init_cycle(cycle);
if (cycle == NULL) {
cycle = (ngx_cycle_t *) ngx_cycle;
continue;
}
ngx_cycle = cycle;
ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx,
ngx_core_module);
//创建新的worker子进程
ngx_start_worker_processes(cycle, ccf->worker_processes,
NGX_PROCESS_JUST_RESPAWN);
ngx_start_cache_manager_processes(cycle, 1);
/* allow new processes to start */
ngx_msleep(100);
live = 1;
//向所有子进程发送QUIT信号
ngx_signal_worker_processes(cycle,
ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
}
//ngx_restart标志位在ngx_noaccepting(表示正在停止接受新的连接)为1的时候被设置为1.
//重启子进程
if (ngx_restart) {
ngx_restart = 0;
ngx_start_worker_processes(cycle, ccf->worker_processes,
NGX_PROCESS_RESPAWN);
ngx_start_cache_manager_processes(cycle, 0);
live = 1;
}
//ngx_reopen标志位为1,重新打开所有文件
if (ngx_reopen) {
ngx_reopen = 0;
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reopening logs");
ngx_reopen_files(cycle, ccf->user);
ngx_signal_worker_processes(cycle,
ngx_signal_value(NGX_REOPEN_SIGNAL));
}
//平滑升级Nginx
if (ngx_change_binary) {
ngx_change_binary = 0;
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "changing binary");
ngx_new_binary = ngx_exec_new_binary(cycle, ngx_argv);
}
//ngx_noaccept为1,表示所有子进程不再处理新的连接
if (ngx_noaccept) {
ngx_noaccept = 0;
ngx_noaccepting = 1;
ngx_signal_worker_processes(cycle,
ngx_signal_value(NGX_SHUTDOWN_SIGNAL));
}
}
}
相较于master,worker进程就显得简单多了:它的主要任务是完成具体的任务逻辑
,即client或server之间的数据读取、I/O交互事件,所以worker进程的阻塞点
是在像select()、epoll_wait()
等这样的I/O多路复用函数调用处,以等待发生数据可读/写事件
,以及被可能收到的进程信号中断。
static void ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type)
{
ngx_int_t i;
ngx_channel_t ch;
ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "start worker processes");
ch.command = NGX_CMD_OPEN_CHANNEL;
//循环创建n个worker子进程
for (i = 0; i < n; i++) {
//完成fok新进程的具体工作
ngx_spawn_process(cycle, ngx_worker_process_cycle,
(void *) (intptr_t) i, "worker process", type);
//全局数组ngx_processes就是用来存储每个子进程的相关信息,如:pid,channel,进程做具体事情的接口指针等等,这些信息就是用结构体ngx_process_t来描述的。
ch.pid = ngx_processes[ngx_process_slot].pid;
ch.slot = ngx_process_slot;
ch.fd = ngx_processes[ngx_process_slot].channel[0];
/*在ngx_spawn_process创建好一个worker进程返回后,master进程就将worker进程的pid、worker进程在ngx_processes数组中的位置及channel[0]传递给前面已经创建好的worker进程,然后继续循环开始创建下一个worker进程。刚提到一个channel[0],这里简单说明一下:channel就是一个能够存储2个整型元素的数组而已,这个channel数组就是用于socketpair函数创建一个进程间通道之用的。master和worker进程以及wor的一个通道进行通信,这个通道就是在ngx_spawn_process函数中fork之前调用socketpair创建的。*/
ngx_pass_open_channel(cycle, &ch);
}
Nginx如何做到热部署
鉴于master管理进程
与worker工作进程
的分离设计
,使得nginx具备热部署
的功能。所谓热部署
,就是对nginx.conf进行修改后,不需要restart nginx,也不需要中断请求,就能让配置文件生效。Nginx对此的做法是:在修改配置文件nginx.conf后,重新生成新的worker进程,当然会以新的配置进行处理,至于旧的worker进程,等执行完以前的请求后,发送信号kill即可。
因此在7*24小时
不间断服务的前提下,就可以对Nginx服务器升级
、修改配置文件
、更换日志文件
等操作。
Nginx的反向代理服务
在介绍反向代理
之前,我先给大家科普一下正向代理
:它更像是一个跳板,代理访问目标资源。比如你想在Youtube上看视频、上Google搜索信息,但是直接访问肯定是不行的,它们的服务器都设立在国外,这时你需要连接上可以访问国外网站的代理服务器,通过代理服务器获取到资源后,然后返回给你。
总结来说
:正向代理
是一个位于client和目标服务器之间的代理服务器
,client向代理发送一个请求并指定目标服务器的ip和port,然后目标服务器将请求获取的内容通过代理再返回给client(注意:client需要设置代理的ip和port
)。
正向代理的主要作用如下:
(1)访问国外资源,如Youtube、google
(2)可以做缓存,加速访问资源
(3)对客户端访问授权,上网进行认证
(4)代理可以记录用户访问记录(上网行为管理),对外隐藏用户信息
反向代理
:以代理服务器
来接收Internet上的连接请求,然后将请求转发给内部网络上的服务器
,并将从服务器上得到的结果返回给Internet上请求连接的client
,此时代理服务器对外就表现为一个服务器。反向代理对外是透明的,访问者并不知道自己访问的是一个代理,client也是无感知代理的存在的,因此客户端是不需要任何配置
就可以直接访问
的。
反向代理的主要作用如下:
(1)保证内网的安全。可以使用反向代理提供WAF功能,阻止web攻击(大型网站通常将反向代理作为公网访问地址,将Web服务器作为内网)。
(2)负载均衡。当单机无法支撑一个网站应用时,就要考虑使用多台机器横向扩展的方式
来处理多个请求,把请求分发
到多台机器上的技术
就是负载均衡。
Nginx使用upstream
来定义一组参与负载均衡的服务器
(可以在nginx.conf的http段中配置,默认路径:/usr/local/nginx/conf/),我介绍一个简单的配置:
upstream 1024do.com {
server 192.168.1.20;
server 192.168.1.21;
server 192.168.1.22;
}
server{
listen 80;
server_name 1024do.com;
location / {
proxy_pass https://1024do.com;
}
}
上述配置定义了三个服务器
,然后在server配置段中使用proxy_pass来定义使用的服务器组,就非常容易的将
1024do.com这个站点配置成了负载均衡的。上述的配置默认是按
顺序轮询,因服务器的
所处位置、
硬件性能`等可以配置的更灵活。
Nginx的负载均衡
可以划分为两大类:内置策略和扩展策略。内置策略包含加权轮询和IP hash
,在默认情况下会编译进Nginx内核,只需在Nginx配置中指明参数。扩展策略有第三方模块策略
:fair、url hash等。下面主要分析一下内置策略:
(1)加权轮询策略
upstream 1024do.com {
server 192.168.1.20 weight=3;
server 192.168.1.21 weight=1;
server 192.168.1.22 weight=5;
}
在上述的服务器列表后面加上weight参数来设置权重
,数字越大权重越大
,分配的请求就越多。加权轮询策略不依赖于客户端的任何信息,完全依靠后端服务器的情况来进行选择,但是同一个客户端的多次请求
可能会被分配到不同的后端服务器进行处理,无法满足做会话保持的需求。
需要注意的是,Nginx每次选出的服务器并不一定是当前权重最大的,整体上是根据服务器的权重
在各个服务器上按照比例分布
的。
(2)IP Hash策略
这种轮询策略是将请求ip和服务器建立起稳固的关系
,每个请求按访问ip进行hash分配,这样每个client会固定访问一个后端服务器。因此IP Hash
可以轻松的解决负载均衡时单机session变化的问题。
upstream 1024do.com {
ip_hash;
server 192.168.1.20;
server 192.168.1.21;
server 192.168.1.22;
}
IP Hash虽然解决了会话保持的需求,但是如果hash后的结果拥挤在一台服务器上时,将导致某台服务器的压力非常大。如果仅仅为了会话保持,可以考虑将session迁移至数据库。
关于负载均衡服务器
的主要配置参数如下:
1)自定义端口
server 192.168.1.24:8080;
2)使用down参数指定服务器不参与分发请求
server 192.168.2.24 down;
3)backup指定候补服务器
正常情况下不会使用候补服务器,只有后端服务器比较繁忙或压力大时才会使用。
server 192.168.3.21 backup;
4)max_fairs审核服务器的健康状况
max_fairs可以设定一个请求失败的次数,超过限度则被认为服务器不可用,不再分发请求到此服务器上。
server 192.168.21.169 max_fairs=3;
Nginx的epoll模型
谈及epoll就不得不提select、poll两种事件驱动。
Nginx的诞生主要是为了解决C10k问题,这与它设计之初的架构是分不开的。在Linux早期很长一段时间内都是使用select来监听事件的,直到Linux2.6内核才提出了epoll,它也是Nginx之所以高并发、高性能的核心。
epoll不是使用一个函数,而是使用C库封装的3个接口:
1.int epoll_create(int size);
功能:创建epoll模型,并返回新的epoll对象的文件描述符。这个文件描述符用于后续的epoll操作;
如果不需要使用这个描述符,请使用close关闭;
参数:
size:内核保证能够正确处理的最大句柄数,不起实际作用;
返回值:成功返回一个非负数(实际为文件描述符),失败返回-1并设置error;
2.int epoll_ctl(int epfd, int op, int fd, struct epoll_envent* event);
功能:维护epoll模型,新增、删除、修改特定的事件。例如,将刚建立的socket加入到epoll中让其监控,
或者把 epoll正在监控的某个socket句柄移出epoll,不再监控它等等(也就是将I/O流放到内核);
参数:
epfd:待操作的内核事件表的文件描述符;
op:指定操作的类型,分别有三种:
EPOLL_CTL_ADD:注册;
EPOLL_CTL_MOD:修改;
EPOLL_CTL_DEL:删除;
fd:待操作的文件描述符;
event:用来指定事件,它是epoll_event结构指针类型
struct epoll_event {
_uint32_t events; //epoll事件
epoll_data_t data; //用户数据
}
返回值:成功返回0,失败返回-1并设置error;
3.int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);
功能:等待事件发生;
参数:
epfd: 待监测的内核事件表;
events:表示一个结构体数组,是一个输出型参数,用来获取从已经就绪的事件的相关信息。
events不可以是空指针,内核只负责把数据复制到这个events数组中,而不会去
帮助我们在用户态中分配内存;
maxevents:指明events的大小,每次能处理的事件数的大小,值不能大于epoll_create的size;
timeout:设置超时时间;-1:永不超时,直到有事件产生才触发;0:立即返回
返回值:成功返回就绪文件描述符的个数,失败返回-1并设置error;
epoll如何巧妙利用上述三个接口在User Space和Kernel Space中提高并发与性能?我们来看一张图:
上图未对具体操作说明,下面我补充一下:
步骤一
:首先执行eoll_create在内核维护一块epoll的高速cache区,并在该缓冲区建立红黑树和就绪链表,用户传入的文件句柄将被放到红黑树中(这也是第一次拷贝)。
步骤二
:内核针对读缓冲区和写缓冲区来判断是否可读可写,这个动作与epoll无关。
步骤三
:epoll_ctl执行EPOLL_CTL_ADD动作时除了将文件句柄挂在红黑树上之外,还向内核注册了该文件句柄的回调函数,内核在检测到某句柄可读可写时则调用该回调函数,回调函数将文件句柄挂到就绪链表上。
步骤四
:epoll_wait负责监控就绪链表,如果就绪链表存在文件句柄,则表示该文件句柄可读可写,则返回到用户态(少量的二次拷贝)。
步骤五
:由于内核不修改文件句柄的位,因此只需要在第一次传入时就可以重复监控,直到使用epoll_ctl删除,否则不需要重新传入,因此无需多次拷贝。
简单来说,epoll是继承了select、poll的I/O多路复用的思想,并在二者的基础上从监控I/O流,查找I/O事件等角度来提高效率,从内核句柄列表到红黑书,再到就绪链表来实现的。
总结一下epoll的优点如下:
(1)监视的fd数量不受限制。它所支持的fd上限是最大能打开文件的数目,受限于内存大小。具体数目可以在/proc/sys/fs/file-max目录下查看。
(2)IO的效率不会随着监视fd的数量增多而下降。epoll不同于select和poll的轮询方式,而是通过每个fd定义的回调函数来实现的,只有就绪的fd才会执行回调函数。
(3)mmap加速内核与用户空间的信息传递。epoll是通过内核与用户空间内存映射的同一块内存,避免了无谓的内存拷贝。
(4)获取就绪事件的时间复杂度为O(1)。调用epoll_wait时,无需遍历,只要list中有数据就返回,没有数据就sleep,等到timeout后即使list有数据也返回。
(5)支持ET模式。下列图示并说明一下epoll的两种模式:
LT(level triggered)
:作为epoll缺省的工作模式,同时支持阻塞和非阻塞方式。LT模式下,内核告诉你一个文件描述符是否就绪了,然后你可以处理这个就绪事件,如果你不做任何操作,内核会继续通知你,直到事件被处理,在一定程序上降低了出错率。
ET(edge triggered
):Nginx的默认工作模式,仅支持非阻塞方式。与LT的区别在于,当一个新的事件到来时,ET模式下可以从epoll_wait中获取到这个事件,应用程序应该立即处理该事件,因为epoll_wait后续调用将不再通知此事件(因此ET模式下缓冲区数据要一次性读干净,防止其他事件得不到处理)。ET模式在很大程度上降低了同一个事件被多次触发的可能,因此更加高效。
Keepalived实现Nginx高可用
Nginx作为入口网关,如果出现单点问题,显然是不可接受的,因此Keepalived应运而生(当然还有heartbeat,corosync等)。Keepalived作为一个高可用的解决方案,主要是用来防止服务器单点发生故障,可以通过和Nginx配合来实现Web服务的高可用。
Keepalived是以VRRP(Virtual Router Redundancy Protocol)协议来实现的,即虚拟路由冗余协议。它是将多台提供相同功能的路由器构成一个路由器组,这个组里面存在一个master和多个backup,master上有一个对外提供服务的虚拟ip,它会发组播给backup,当backup收不到VRRP包时就认为master宕掉了,需要根据VRRP的优先级来选举一个backup充当master。
VRRP的工作逻辑如下图:
介绍完VRRP之后,我们回到Keepalived实现Nginx的高可用上,它的实现思路主要包含两步:
① 请求不会直接打到Nginx上,会先通过虚拟IP;
② Keepalived可以监控Nginx的生命状态,提供一个用户自定义的脚本,来定期检查Nginx进程的状态,进行权重变化,从而实现Nginx故障切换。
简单来说,外界过来一个request请求会先通过VRRP得到虚拟IP,虚拟IP通过脚本来检测Nginx进程的健康状况,当Nginx1发生故障时,会将资源切换至备用的Nginx2上,从而体现Nginx的可高用性。
总结
:本篇主要从Nginx的稳定性、高并发、高性能以及高可用四个方面进行了剖析,理解透这些相信你对Nginx会有更为清晰的认识,试着将这些原理运用于实际之中吧!