版权声明:本文由韩伟原创文章,转载请注明出处:
文章原文连接:https://www.qcloud.com/community/article/165nginx
来源:腾云阁 https://www.qcloud.com/community程序员
任何的服务器的性能都是有极限的,面对海量的互联网访问需求,是不可能单靠一台服务器或者一个CPU来承担的。因此咱们通常都会在运行时架构设计之初,就考虑如何能利用多个CPU、多台服务器来分担负载,这就是所谓分布的策略。分布式的服务器概念很简单,可是实现起来却比较复杂。由于咱们写的程序,每每都是以一个CPU,一块内存为基础来设计的,因此要让多个程序同时运行,而且协调运做,这须要更多的底层工做。算法
首先出现能支持分布式概念的技术是多进程。在DOS时代,计算机在一个时间内只能运行一个程序,若是你想一边写程序,同时一边听mp3,都是不可能的。可是,在WIN95操做系统下,你就能够同时开多个窗口,背后就是同时在运行多个程序。在Unix和后来的Linux操做系统里面,都广泛支持了多进程的技术。所谓的多进程,就是操做系统能够同时运行咱们编写的多个程序,每一个程序运行的时候,都好像本身独占着CPU和内存同样。在计算机只有一个CPU的时候,实际上计算机会分时复用的运行多个进程,CPU在多个进程之间切换。可是若是这个计算机有多个CPU或者多个CPU核,则会真正的有几个进程同时运行。因此进程就好像一个操做系统提供的运行时“程序盒子”,能够用来在运行时,容纳任何咱们想运行的程序。当咱们掌握了操做系统的多进程技术后,咱们就能够把服务器上的运行任务,分为多个部分,而后分别写到不一样的程序里,利用上多CPU或者多核,甚至是多个服务器的CPU一块儿来承担负载。数据库
多进程利用多CPUapache
这种划分多个进程的架构,通常会有两种策略:一种是按功能来划分,好比负责网络处理的一个进程,负责数据库处理的一个进程,负责计算某个业务逻辑的一个进程。另一种策略是每一个进程都是一样的功能,只是分担不一样的运算任务而已。使用第一种策略的系统,运行的时候,直接根据操做系统提供的诊断工具,就能直观的监测到每一个功能模块的性能消耗,由于操做系统提供进程盒子的同时,也能提供对进程的全方位的监测,好比CPU占用、内存消耗、磁盘和网络I/O等等。可是这种策略的运维部署会稍微复杂一点,由于任何一个进程没有启动,或者和其余进程的通讯地址没配置好,均可能致使整个系统没法运做;而第二种分布策略,因为每一个进程都是同样的,这样的安装部署就很是简单,性能不够就多找几个机器,多启动几个进程就完成了,这就是所谓的平行扩展。编程
如今比较复杂的分布式系统,会结合这两种策略,也就是说系统既按一些功能划分出不一样的具体功能进程,而这些进程又是能够平行扩展的。固然这样的系统在开发和运维上的复杂度,都是比单独使用“按功能划分”和“平行划分”要更高的。因为要管理大量的进程,传统的依靠配置文件来配置整个集群的作法,会显得愈来愈不实用:这些运行中的进程,可能和其余不少进程产生通讯关系,当其中一个进程变动通讯地址时,势必影响全部其余进程的配置。因此咱们须要集中的管理全部进程的通讯地址,当有变化的时候,只须要修改一个地方。在大量进程构建的集群中,咱们还会碰到容灾和扩容的问题:当集群中某个服务器出现故障,可能会有一些进程消失;而当咱们须要增长集群的承载能力时,咱们又须要增长新的服务器以及进程。这些工做在长期运行的服务器系统中,会是比较常见的任务,若是整个分布系统有一个运行中的中心进程,能自动化的监测全部的进程状态,一旦有进程加入或者退出集群,都能即时的修改全部其余进程的配置,这就造成了一套动态的多进程管理系统。开源的ZooKeeper给咱们提供了一个能够充当这种动态集群中心的实现方案。因为ZooKeeper自己是能够平行扩展的,因此它本身也是具有必定容灾能力的。如今愈来愈多的分布式系统都开始使用以ZooKeeper为集群中心的动态进程管理策略了。缓存
动态进程集群安全
在调用多进程服务的策略上,咱们也会有必定的策略选择,其中最著名的策略有三个:一个是动态负载均衡策略;一个是读写分离策略;一个是一致性哈希策略。动态负载均衡策略,通常会搜集多个进程的服务状态,而后挑选一个负载最轻的进程来分发服务,这种策略对于比较同质化的进程是比较合适的。读写分离策略则是关注对持久化数据的性能,好比对数据库的操做,咱们会提供一批进程专门用于提供读数据的服务,而另一个(或多个)进程用于写数据的服务,这些写数据的进程都会每次写多份拷贝到“读服务进程”的数据区(可能就是单独的数据库),这样在对外提供服务的时候,就能够提供更多的硬件资源。一致性哈希策略是针对任何一个任务,看看这个任务所涉及读写的数据,是属于哪一片的,是否有某种能够缓存的特征,而后按这个数据的ID或者特征值,进行“一致性哈希”的计算,分担给对应的处理进程。这种进程调用策略,能很是的利用上进程内的缓存(若是存在),好比咱们的一个在线游戏,由100个进程承担服务,那么咱们就能够把游戏玩家的ID,做为一致性哈希的数据ID,做为进程调用的KEY,若是目标服务进程有缓存游戏玩家的数据,那么全部这个玩家的操做请求,都会被转到这个目标服务进程上,缓存的命中率大大提升。而使用“一致性哈希”,而不是其余哈希算法,或者取模算法,主要是考虑到,若是服务进程有一部分因故障消失,剩下的服务进程的缓存依然能够有效,而不会整个集群全部进程的缓存都失效。具体有兴趣的读者能够搜索“一致性哈希”一探究竟。服务器
以多进程利用大量的服务器,以及服务器上的多个CPU核心,是一个很是有效的手段。可是使用多进程带来的额外的编程复杂度的问题。通常来讲咱们认为最好是每一个CPU核心一个进程,这样能最好的利用硬件。若是同时运行的进程过多,操做系统会消耗不少CPU时间在不一样进程的切换过程上。可是,咱们早期所得到的不少API都是阻塞的,好比文件I/O,网络读写,数据库操做等。若是咱们只用有限的进程来执行带这些阻塞操做的程序,那么CPU会大量被浪费,由于阻塞的API会让有限的这些进程停着等待结果。那么,若是咱们但愿能处理更多的任务,就必需要启动更多的进程,以便充分利用那些阻塞的时间,可是因为进程是操做系统提供的“盒子”,这个盒子比较大,切换耗费的时间也比较多,因此大量并行的进程反而会无谓的消耗服务器资源。加上进程之间的内存通常是隔离的,进程间若是要交换一些数据,每每须要使用一些操做系统提供的工具,好比网络socket,这些都会额外消耗服务器性能。所以,咱们须要一种切换代价更少,通讯方式更便捷,编程方法更简单的并行技术,这个时候,多线程技术出现了。网络
在进程盒子里面的线程盒子
多线程的特色是切换代价少,能够同时访问内存。咱们能够在编程的时候,任意让某个函数放入新的线程去执行,这个函数的参数能够是任何的变量或指针。若是咱们但愿和这些运行时的线程通讯,只要读、写这些指针指向的变量便可。在须要大量阻塞操做的时候,咱们能够启动大量的线程,这样就能较好的利用CPU的空闲时间;线程的切换代价比进程低得多,因此咱们能利用的CPU也会多不少。线程是一个比进程更小的“程序盒子”,他能够放入某一个函数调用,而不是一个完整的程序。通常来讲,若是多个线程只是在一个进程里面运行,那实际上是没有利用到多核CPU的并行好处的,仅仅是利用了单个空闲的CPU核心。可是,在JAVA和C#这类带虚拟机的语言中,多线程的实现底层,会根据具体的操做系统的任务调度单位(好比进程),尽可能让线程也成为操做系统能够调度的单位,从而利用上多个CPU核心。好比Linux2.6以后,提供了NPTL的内核线程模型,JVM就提供了JAVA线程到NPTL内核线程的映射,从而利用上多核CPU。而Windows系统中,听说自己线程就是系统的最小调度单位,因此多线程也是利用上多核CPU的。因此咱们在使用JAVA\C#编程的时候,多线程每每已经同时具有了多进程利用多核CPU、以及切换开销低的两个好处。
早期的一些网络聊天室服务,结合了多线程和多进程使用的例子。一开始程序会启动多个广播聊天的进程,每一个进程都表明一个房间;每一个用户链接到聊天室,就为他启动一个线程,这个线程会阻塞的读取用户的输入流。这种模型在使用阻塞API的环境下,很是简单,但也很是有效。
当咱们在普遍使用多线程的时候,咱们发现,尽管多线程有不少优势,可是依然会有明显的两个缺点:一个内存占用比较大且不太可控;第二个是多个线程对于用一个数据使用时,须要考虑复杂的“锁”问题。因为多线程是基于对一个函数调用的并行运行,这个函数里面可能会调用不少个子函数,每调用一层子函数,就会要在栈上占用新的内存,大量线程同时在运行的时候,就会同时存在大量的栈,这些栈加在一块儿,可能会造成很大的内存占用。而且,咱们编写服务器端程序,每每但愿资源占用尽可能可控,而不是动态变化太大,由于你不知道何时会由于内存用完而当机,在多线程的程序中,因为程序运行的内容致使栈的伸缩幅度可能很大,有可能超出咱们预期的内存占用,致使服务的故障。而对于内存的“锁”问题,一直是多线程中复杂的课题,不少多线程工具库,都推出了大量的“无锁”容器,或者“线程安全”的容器,而且还大量设计了不少协调线程运做的类库。可是这些复杂的工具,无疑都是证实了多线程对于内存使用上的问题。
同时排多条队就是并行
因为多线程仍是有必定的缺点,因此不少程序员想到了一个釜底抽薪的方法:使用多线程每每是由于阻塞式API的存在,好比一个read()操做会一直中止当前线程,那么咱们能不能让这些操做变成不阻塞呢?——selector/epoll就是Linux退出的非阻塞式API。若是咱们使用了非阻塞的操做函数,那么咱们也无需用多线程来并发的等待阻塞结果。咱们只须要用一个线程,循环的检查操做的状态,若是有结果就处理,无结果就继续循环。这种程序的结果每每会有一个大的死循环,称为主循环。在主循环体内,程序员能够安排每一个操做事件、每一个逻辑状态的处理逻辑。这样CPU既无需在多线程间切换,也无需处理复杂的并行数据锁的问题——由于只有一个线程在运行。这种就是被称为“并发”的方案。
服务员兼了点菜、上菜就是并发
实际上计算机底层早就有使用并发的策略,咱们知道计算机对于外部设备(好比磁盘、网卡、显卡、声卡、键盘、鼠标),都使用了一种叫“中断”的技术,早期的电脑使用者可能还被要求配置IRQ号。这个中断技术的特色,就是CPU不会阻塞的一直停在等待外部设备数据的状态,而是外部数据准备好后,给CPU发一个“中断信号”,让CPU转去处理这些数据。非阻塞的编程实际上也是相似这种行为,CPU不会一直阻塞的等待某些I/O的API调用,而是先处理其余逻辑,而后每次主循环去主动检查一下这些I/O操做的状态。
多线程和异步的例子,最著名就是Web服务器领域的Apache和Nginx的模型。Apache是多进程/多线程模型的,它会在启动的时候启动一批进程,做为进程池,当用户请求到来的时候,从进程池中分配处理进程给具体的用户请求,这样能够节省多进程/线程的建立和销毁开销,可是若是同时有大量的请求过来,仍是须要消耗比较高的进程/线程切换。而Nginx则是采用epoll技术,这种非阻塞的作法,可让一个进程同时处理大量的并发请求,而无需反复切换。对于大量的用户访问场景下,apache会存在大量的进程,而nginx则能够仅用有限的进程(好比按CPU核心数来启动),这样就会比apache节省了很多“进程切换”的消耗,因此其并发性能会更好。
Nginx的固定多进程,一个进程异步处理多个客户端
Apache的多态多进程,一个进程处理一个客户
在现代服务器端软件中,nginx这种模型的运维管理会更简单,性能消耗也会稍微更小一点,因此成为最流行的进程架构。可是这种好处,会付出一些另外的代价:非阻塞代码在编程的复杂度变大。