05-并发型服务器的其他设计方案
60.4 并发型服务器的其他设计方案
对于许多需要通过TCP连接同时处理多个客户端的应用来说,前面几节描述的传统型并发服务器模型已经足够用了。但是,对于负载很高的服务器来说(例如,Web服务器每分钟要处理成千上万次请求)①,为每个客户端创建一个新的子进程(甚至是线程)所带来的开销对服务器来说是个沉重的负担(参见28.3节),因此需要有其他的设计方案。下面我们主要考虑这几种可选方案。
在服务器上预先创建进程或线程
预先创建进程或线程的服务器已经在[Stevens et al., 2004]的第30章中做了详细的描述。其核心理念有如下几点。
- 服务器程序在启动阶段(即在任何客户端请求到来之前)就立刻预先创建好一定数量的子进程(或线程),而不是针对每个客户端来创建一个新的子进程(或线程)。这些子进程构成了一种服务池(server pool)②。
- 服务池中的每个子进程一次只处理一个客户端。在处理完客户端请求后,子进程并不会终止,而是获取下一个待处理的客户端继续处理,如此类推。
采用上述技术需要在服务器应用中仔细地管理子进程。服务池应该足够大,以确保能充分响应客户端的请求。这意味着服务器父进程必须对未占用的子进程加以监视,并且在服务器处于负载高峰期时增加服务池的大小,这样就总会有足够多的子进程存在,从而可以立刻服务于新的客户端请求。如果负载下降了,那么应该相应地降低服务池的大小,因为过多的空余进程会降低系统的整体性能。
此外,服务池中的子进程必须遵循某些协议,使得它们能以独占的方式选择一个客户端连接。在大多数UNIX实现中(包括Linux),让服务池中的每个子进程在监听描述符的accept()调用上阻塞就足够了。换句话说,服务器父进程在创建任何子进程之前先创建监听套接字,然后每个子进程在fork()调用中继承该套接字的文件描述符。当一个新的客户端连接到来时,只有其中一个子进程能完成accept()调用。但是,由于accept()在一些老式的实现中并不是一个原子化的系统调用,因此可能需要通过一些互斥技术(例如文件锁)来支持,以确保每次只有一个子进程可以执行accept()调用([Stevens et al., 2004])。
还有其他的方法可以让服务池中所有的子进程都执行accept()调用。如果服务池由分离的进程组成,服务器父进程可以执行accept()调用,然后使用61.13.3节中简要描述的技术将代表新连接的文件描述符传递给池中空闲的进程之一。如果服务池由线程组成,主线程可以执行accept()调用,然后通知服务器上的空闲线程,有新的已连接上的客户端正等待处理。
在单个进程中处理多个客户端
在某些情况下,我们可以设计让单个服务器进程来处理多个客户端。为了实现这点,我们必须采用一种能允许单个进程同时监视多个文件描述符上I/O事件的I/O模型(I/O多路复用、信号驱动I/O或者epoll)。本书第63章中描述了这些模型。
在设计单进程服务器时,服务器进程必须做一些通常由内核来处理的调度任务。在每个客户端一个服务器进程地解决方案中,我们可以依靠内核来确保每个服务器进程(从而也确保了客户端)能公平地访问到服务器主机的资源。但当我们用单个服务器进程来处理多个客户端时,服务器进程必须自行确保一个或多个客户端不会霸占服务器,从而使其他的客户端处于饥饿状态。关于这点我们将在63.4.6节中继续讨论。
采用服务器集群
其他用来处理高客户端负载的方法还包括使用多个服务器系统——服务器集群(server farm)。
构建服务器集群最简单的一种方法是DNS轮转负载共享(DNS round-robin load sharing)(或负载分发,load distribution),一个地区的域名权威服务器将同一个域名映射到多个IP地址上(即,多台服务器共享同一个域名)。后续对DNS服务器的域名解析请求将以循环轮转的方式以不同的顺序返回这些IP地址。更多关于DNS轮转负载共享的信息可以在[Albitz & Liu, 2006]中找到。
DNS循环轮转的优势是成本低,而且容易实施。但是,它也存在着一些问题。其中一个问题是远端DNS服务器上所执行的缓存操作,这意味着今后位于某个特定主机(或一组主机)上的客户端发出的请求会绕过循环轮转DNS服务器,并总是由同一个服务器来负责处理。此外,循环轮转DNS并没有任何内建的用来确保达到良好负载均衡(不同的客户端在服务器上产生的负载不同)或者是确保高可用性的机制(如果其中一台服务器宕机或者运行的服务器程序崩溃了怎么办?)。在许多采用多台服务器设备的设计中,另一个我们需要考虑的因素是服务器亲和性(server affinity)。这就是说,确保来自同一个客户端的请求序列能够全部定向到同一台服务器上,这样由服务器维护的任何有关客户端状态的信息都能保持准确。
一个更灵活但也更加复杂的解决方案是服务器负载均衡(server load balancing)。在这种场景下,由一台负载均衡服务器将客户端请求路由到服务器集群中的其中一个成员上。(为了确保高可用性,可能还会有一台备用的服务器。一旦负载均衡主服务器崩溃,备用服务器就立刻接管主服务器的任务。)这消除了由远端DNS缓存所引起的问题,因为服务器集群只对外提供了一个单独的IP地址(也就是负载均衡服务器的IP地址)。负载均衡服务器结合一些算法来衡量或计算服务器负载(可能是根据服务器集群的成员所提供的量值),并智能化地将负载分发到集群中的各个成员之上。负载均衡服务器也会自动检测集群中失效的成员(如果需要,还会自动检测新增加的服务器成员)。最后,负载均衡服务器可能还会提供对服务器亲和力的支持。更多关于服务器负载均衡的信息可以在[Kopparapu, 2002]中找到。