分布式集群服务导引:浅析N-ginx的集群服务部署及高并发实现原理
分布与集群
分布式可繁也可以简,最简单的分布式就是大家最常用的,在负载均衡服务器后加一堆web服务器,然后在上面搞一个缓存服务器来保存临时状态,后面共享一个数据库,其实很多号称分布式专家的人也就停留于此,大致结构如下图所示:
这种环境下真正进行分布式的只是web server而已,并且web server之间没有任何联系,所以结构和实现都非常简单。
有些情况下,对分布式的需求就没这么简单,在每个环节上都有分布式的需求,比如Load Balance、DB、Cache和文件等等,并且当分布式节点之间有关联时,还得考虑之间的通讯,另外,节点非常多的时候,得有监控和管理来支撑。这样看起来,分布式是一个非常庞大的体系,只不过你可以根据具体需求进行适当地裁剪。按照最完备的分布式体系来看,可以由以下模块组成:
分布式任务处理服务:负责具体的业务逻辑处理
分布式节点注册和查询:负责管理所有分布式节点的命名和物理信息的注册与查询,是节点之间联系的桥梁
分布式DB:分布式结构化数据存取
分布式Cache:分布式缓存数据(非持久化)存取
分布式文件:分布式文件存取
网络通信:节点之间的网络数据通信
监控管理:搜集、监控和诊断所有节点运行状态
分布式编程语言:用于分布式环境下的专有编程语言,比如Elang、Scala
分布式算法:为解决分布式环境下一些特有问题的算法,比如解决一致性问题的Paxos算法
因此,若要深入研究云计算和分布式,就得深入研究以上领域,而这些领域每一块的水都很深,都需要很底层的知识和技术来支撑,所以说,对于想提升技术的开发者来说,以分布式来作为切入点是非常好的,可以以此为线索,探索计算机世界的各个角落。
集群是个物理形态,分布式是个工作方式。
只要是一堆机器,就可以叫集群,他们是不是一起协作着干活,这个谁也不知道;一个程序或系统,只要运行在不同的机器上,就可以叫分布式,嗯,C/S架构也可以叫分布式。
集群一般是物理集中、统一管理的,而分布式系统则不强调这一点。
所以,集群可能运行着一个或多个分布式系统,也可能根本没有运行分布式系统;分布式系统可能运行在一个集群上,也可能运行在不属于一个集群的多台(2台也算多台)机器上。
以上使读者了解分布与集群的简单关系,下面先简单讲一讲Nginx集群的部署原理——
简单的介绍一下什么是nginx。
nginx是一种代理服务器,客户端请求数据要先经过nginx,然后由nginx去服务器上去取数据,取到数据之后先返回到nginx,再由nginx再返回我们客户端。我相信有很多人都有过去翻墙到国外网站的经历,譬如程序员最爱stack over flow亦或是最大的视频网站You Tube等等。那么nginx的工作原理其实和翻墙原理差不多,作为一个传输中介起到中间枢纽作用,然后你通过中间枢纽获取你所需要的东西。
其次,我们为什么要nginx集群,集群的意义何在呢?由于我进到的是规模比较大型项目,我就我们项目的实际情况与大家共同分享。由于总线服务、webservice服务、网元操作服务处理着大批量的请求,这些服务十分的重要,如果服务器出现异常或是机器出现宕机,那么对客户造成的影响将是巨大的,尤其是对客户的内心的体验感的影响将是无法抹去的。所以我们要尽量避免类似情况的发生,要保证服务具有可适应性、高拓展性和可维护性。给客户塑造一个至善至美的好印象。
然后我们来看一下nginx集群服务的总体架构,在说到总体架构之前我们再来了解一下nginx服务集群的总体架构中的一种高性能可以自动检测服务器状态的Keepalived ,Keepalived 可以用来防止单点故障(单点故障是指一旦某一点出现故障就会导致整个系统架构的不可用)的发生。除此之外,还有nginx服务器、其他服务器、防火墙和用户。其集群的总体架构图如下图所示:
图1 nginx集群服务部署架构图
从上图我们可以清楚的看到nginx服务器共有2台,一台作为主服务器,一台作为备服务器,webservice服务器2台,总线服务器2台,网元操作服务器5台。假设用户首先发布网元操作服务请求,此时12上的nginx代理服务器收到请求,而nginx是以多进程的方式来工作的,启动的时候会有一个master进程和多个worker进程,每个worker进程之间是平等的,master进程用来管理worker进程。Master进程运行之后,便会随机地抽取一个worker进程进行任务分配,而12nginx与网元操作服务器16-20访问通过http请求来进行访问的,16-20网元服务器端口号设置是一样的,根据端口就可以随机地选取一个网元操作服务器进行访问进而返回数据到用户手中。但是此时如果当用户在访问12nginx代理服务器的时候,服务器出现异常了或是宕机了怎么办?这时nginx的集群作用就体现出来了,12nginx与13nginx分别部署上Keepalived 服务,此时12上检测器Keepalived 就开始检测服务器的状态了,发现报了异常或者是宕机,则自动剔除12服务器,13nginx上Keepalived 自动接管,启用13nginx代理服务器代替12nginx服务器的工作。让我们的服务又恢复了正常,避免不必要的损失。当12nginx服务器维护正常后,Keepalived又会自动停掉13nginx的服务而又把12nginx的服务加入。整过过程不需要人工干涉,需要人工做的就是修复故障机器。还有就是我们需要在12nginx和13nginx服务器上设置一个优先级,让Keepalived具有选择性。我们若有需要,可以再利用Keepalived + nginx以此类推再往上层搭建服务,比如说在12nginx与13nginx上再搭建一台14nginx作为主服务器,再搭建一台15nginx作为备服务器,再分别部署上Keepalived。从而更加保障了我们整个服务的可适应性、高拓展性和可维护性。
以上参考:
1、 http://blog.csdn.net/u013144287/article/details/78551398 nginx实现反向代理及负载均衡
2、http://blog.csdn.net/u013144287/article/details/78553496 nginx+keepalived实现主备服务器可高用
以上为nginx的集群服务部署原理简要分享。接下来,聊聊Nginx高并发原理.
Nginx
首先要明白,Nginx 采用的是多进程(单线程) & 多路IO复用模型。使用了 I/O 多路复用技术的 Nginx,就成了”并发事件驱动“的服务器。
多进程的工作模式
1、Nginx 在启动后,会有一个 master 进程和多个相互独立的 worker 进程。2、接收来自外界的信号,向各worker进程发送信号,每个进程都有可能来处理这个连接。3、 master 进程能监控 worker 进程的运行状态,当 worker 进程退出后(异常情况下),会自动启动新的 worker 进程。
注意 worker 进程数,一般会设置成机器 cpu 核数。因为更多的worker 数,只会导致进程相互竞争 cpu,从而带来不必要的上下文切换。
使用多进程模式,不仅能提高并发率,而且进程之间相互独立,一个 worker 进程挂了不会影响到其他 worker 进程。
惊群现象
主进程(master 进程)首先通过 socket() 来创建一个 sock 文件描述符用来监听,然后fork生成子进程(workers 进程),子进程将继承父进程的 sockfd(socket 文件描述符),之后子进程 accept() 后将创建已连接描述符(connected descriptor)),然后通过已连接描述符来与客户端通信。
那么,由于所有子进程都继承了父进程的 sockfd,那么当连接进来时,所有子进程都将收到通知并“争着”与它建立连接,这就叫“惊群现象”。大量的进程被激活又挂起,只有一个进程可以accept() 到这个连接,这当然会消耗系统资源。
Nginx对惊群现象的处理:
Nginx 提供了一个 accept_mutex 这个东西,这是一个加在accept上的一把互斥锁。即每个 worker 进程在执行 accept 之前都需要先获取锁,获取不到就放弃执行 accept()。有了这把锁之后,同一时刻,就只会有一个进程去 accpet(),这样就不会有惊群问题了。accept_mutex 是一个可控选项,我们可以显示地关掉,默认是打开的。
worker进程工作流程
当一个 worker 进程在 accept() 这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,一个完整的请求。一个请求,完全由 worker 进程来处理,而且只能在一个 worker 进程中处理。
这样做带来的好处:
1、节省锁带来的开销。每个 worker 进程都是独立的进程,不共享资源,不需要加锁。同时在编程以及问题查上时,也会方便很多。
2、独立进程,减少风险。采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master 进程则很快重新启动新的 worker 进程。当然,worker 进程的也能发生意外退出。
多进程模型每个进程/线程只能处理一路IO,那么 Nginx是如何处理多路IO呢?
如果不使用 IO 多路复用,那么在一个进程中,同时只能处理一个请求,比如执行 accept(),如果没有连接过来,那么程序会阻塞在这里,直到有一个连接过来,才能继续向下执行。
而多路复用,允许我们只在事件发生时才将控制返回给程序,而其他时候内核都挂起进程,随时待命。
核心:Nginx采用的 IO多路复用模型epoll
epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+树),其工作流程分为三部分:
1、调用 int epoll_create(int size)建立一个epoll对象,内核会创建一个eventpoll结构体,用于存放通过epoll_ctl()向 epoll对象中添加进来的事件,这些事件都会挂载在红黑树中。 2、调用 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 在 epoll 对象中为 fd 注册事件, 所有添加到epoll中的事件都会与设备驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个sockfd的回调方法, 将sockfd添加到eventpoll 中的双链表。 3、调用 int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout) 来等待事件的发生, timeout 为 -1 时,该调用会阻塞知道有事件发生1234
这样,注册好事件之后,只要有 fd 上事件发生,epoll_wait() 就能检测到并返回给用户,用户就能”非阻塞“地进行 I/O 了。
epoll() 中内核则维护一个链表,epoll_wait 直接检查链表是不是空就知道是否有文件描述符准备好了。(epoll 与 select 相比最大的优点是不会随着 sockfd 数目增长而降低效率,使用 select() 时,内核采用轮训的方法来查看是否有fd 准备好,其中的保存 sockfd 的是类似数组的数据结构 fd_set,key 为 fd,value 为 0 或者 1。)
能达到这种效果,是因为在内核实现中 epoll 是根据每个 sockfd 上面的与设备驱动程序建立起来的回调函数实现的。那么,某个 sockfd 上的事件发生时,与它对应的回调函数就会被调用,来把这个 sockfd 加入链表,其他处于“空闲的”状态的则不会。在这点上,epoll 实现了一个”伪”AIO。但是如果绝大部分的 I/O 都是“活跃的”,每个 socket 使用率很高的话,epoll效率不一定比 select 高(可能是要维护队列复杂)。
可以看出,因为一个进程里只有一个线程,所以一个进程同时只能做一件事,但是可以通过不断地切换来“同时”处理多个请求。
例子:Nginx 会注册一个事件:“如果来自一个新客户端的连接请求到来了,再通知我”,此后只有连接请求到来,服务器才会执行 accept() 来接收请求。又比如向上游服务器(比如 PHP-FPM)转发请求,并等待请求返回时,这个处理的 worker 不会在这阻塞,它会在发送完请求后,注册一个事件:“如果缓冲区接收到数据了,告诉我一声,我再将它读进来”,于是进程就空闲下来等待事件发生。
这样,基于 多进程+epoll, Nginx 便能实现高并发。
使用 epoll 处理事件的一个框架,代码转自:http://www.cnblogs.com/fnlingnzb-learner/p/5835573.html
for( ; ; ) // 无限循环 { nfds = epoll_wait(epfd,events,20,500); // 最长阻塞 500s for(i=0;i<nfds;++i) { if(events[i].data.fd==listenfd) //有新的连接 { connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen); //accept这个连接 ev.data.fd=connfd; ev.events=EPOLLIN|EPOLLET; epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //将新的fd添加到epoll的监听队列中 } else if( events[i].events&EPOLLIN ) //接收到数据,读socket { n = read(sockfd, line, MAXLINE)) < 0 //读 ev.data.ptr = md; //md为自定义类型,添加数据 ev.events=EPOLLOUT|EPOLLET; epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改标识符,等待下一个循环时发送数据,异步处理的精髓 } else if(events[i].events&EPOLLOUT) //有数据待发送,写socket { struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取数据 sockfd = md->fd; send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //发送数据 ev.data.fd=sockfd; ev.events=EPOLLIN|EPOLLET; epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改标识符,等待下一个循环时接收数据 } else { //其他的处理 } } }
Nginx 与 多进程模式 Apache 的比较:
事件驱动适合于I/O密集型服务,多进程或线程适合于CPU密集型服务:
1、Nginx 更主要是作为反向代理,而非Web服务器使用。其模式是事件驱动。
2、事件驱动服务器,最适合做的就是这种 I/O 密集型工作,如反向代理,它在客户端与WEB服务器之间起一个数据中转作用,纯粹是 I/O 操作,自身并不涉及到复杂计算。因为进程在一个地方进行计算时,那么这个进程就不能处理其他事件了。
3、Nginx 只需要少量进程配合事件驱动,几个进程跑 libevent,不像 Apache 多进程模型那样动辄数百的进程数。
5、Nginx 处理静态文件效果也很好,那是因为读写文件和网络通信其实都是 I/O操作,处理过程一样。
参考 http://codinglife.sinaapp.com/?p=40