高性能是最复杂的一环,磁盘、操作系统、CPU、内存、缓存、网络、编程语言、架构等,每个都有可能影响系统达到高性能,一行不恰当的 debug 日志,就可能将服务器的性能从 TPS 30000 降低到 8000;一个 tcp_nodelay 参数,就可能将响应时间从 2 毫秒延长到 40 毫秒。
高性能架构设计:单服务器性能发挥到极致;设计服务器集群方案,还和具体实现及编码相关。
单服务器高性能: 并发模型:如何管理连接、处理请求,和操作系统 I/O及进程模型相关。
I/O 模型:阻塞、非阻塞、同步、异步。
进程模型:单进程、多进程、多线程。
单服务器高性能:PPC 与 TPC。
PPC
Process Per Connection,有新连接就新建进程处理,传统的UNIX 网络服务器用
父进程接受连接(图中 accept)。
父进程“fork”子进程(图中 fork)。
子进程处理连接的读写请求(图中子进程 read、业务处理、write)。
子进程关闭连接(图中子进程中的 close)。
注意,图中有一个小细节,父进程“fork”子进程后,直接调用了 close,看起来好像是关闭了连接,其实只是将连接的文件描述符引用计数减一,真正的关闭连接是等子进程也调用 close 后,连接对应的文件描述符引用计数变为 0 后,操作系统才会真正关闭连接,更多细节请参考《UNIX 网络编程:卷一》。
PPC 模式实现简单,比较适合服务器的连接数没那么多的情况,例如数据库服务器。对于普通的业务服务器,在互联网兴起之前,由于服务器的访问量和并发量并没有那么大,这种模式其实运作得也挺好,世界上第一个 web 服务器 CERN httpd 就采用了这种模式(具体你可以参考https://en.wikipedia.org/wiki/CERN_httpd)。互联网兴起后,服务器的并发和访问量从几十剧增到成千上万,这种模式的弊端就凸显出来了,主要体现在这几个方面:
fork 代价高:站在操作系统的角度,创建一个进程的代价是很高的,需要分配很多内核资源,需要将内存映像从父进程复制到子进程。即使现在的操作系统在复制内存映像时用到了 Copy on Write(写时复制)技术,总体来说创建进程的代价还是很大的。
父子进程通信复杂:父进程“fork”子进程时,文件描述符可以通过内存映像复制从父进程传到子进程,但“fork”完成后,父子进程通信就比较麻烦了,需要采用 IPC(Interprocess Communication)之类的进程通信方案。例如,子进程需要在 close 之前告诉父进程自己处理了多少个请求以支撑父进程进行全局的统计,那么子进程和父进程必须采用 IPC 方案来传递信息。
并发连接数量有限:如果每个连接存活时间比较长,而且新的连接又源源不断的进来,则进程数量会越来越多,操作系统进程调度和切换的频率也越来越高,系统的压力也会越来越大。因此,一般情况下,PPC 方案能处理的并发连接数量最大也就几百。
prefork
PPC 模式中,当连接进来时才 fork 新进程来处理连接请求,由于 fork 进程代价高,用户访问时可能感觉比较慢,prefork 模式的出现就是为了解决这个问题。
顾名思义,prefork 就是提前创建进程(pre-fork)。系统在启动的时候就预先创建好进程,然后才开始接受用户的请求,当有新的连接进来的时候,就可以省去 fork 进程的操作,让用户访问更快、体验更好。prefork 的基本示意图是:
prefork 的实现关键就是多个子进程都 accept 同一个 socket,当有新的连接进入时,操作系统保证只有一个进程能最后 accept 成功。但这里也存在一个小小的问题:“惊群”现象,就是指虽然只有一个子进程能 accept 成功,但所有阻塞在 accept 上的子进程都会被唤醒,这样就导致了不必要的进程调度和上下文切换了。幸运的是,操作系统可以解决这个问题,例如 Linux 2.6 版本后内核已经解决了 accept 惊群问题。
prefork 模式和 PPC 一样,还是存在父子进程通信复杂、支持的并发连接数量有限的问题,因此目前实际应用也不多。Apache 服务器提供了 MPM prefork 模式,推荐在需要可靠性或者与旧软件兼容的站点时采用这种模式,默认情况下最大支持 256 个并发连接。
TPC
Thread Per Connection,新连接就建线程处理。更轻量级,创建线程的消耗比进程要少得多;同时多线程是共享进程内存空间的,线程通信相比进程通信更简单。因此,TPC 实际上是解决或者弱化了 PPC fork 代价高的问题和父子进程通信复杂的问题。
父进程接受连接(图中 accept)。
父进程创建子线程(图中 pthread)。
子线程处理连接的读写请求(图中子线程 read、业务处理、write)。
子线程关闭连接(图中子线程中的 close)。
注意,和 PPC 相比,主进程不用“close”连接了。原因是在于子线程是共享主进程的进程空间的,连接的文件描述符并没有被复制,因此只需要一次 close 即可。
TPC 虽然解决了 fork 代价高和进程通信复杂的问题,但是也引入了新的问题,具体表现在:
创建线程虽然比创建进程代价低,但并不是没有代价,高并发时(例如每秒上万连接)还是有性能问题。
无须进程间通信,但是线程间的互斥和共享又引入了复杂度,可能一不小心就导致了死锁问题。
多线程会出现互相影响的情况,某个线程出现异常时,可能导致整个进程退出(例如内存越界)。
除了引入了新的问题,TPC 还是存在 CPU 线程调度和切换代价的问题。因此,TPC 方案本质上和 PPC 方案基本类似,在并发几百连接的场景下,反而更多地是采用 PPC 的方案,因为 PPC 方案不会有死锁的风险,也不会多进程互相影响,稳定性更高。
prethread
TPC 模式中,当连接进来时才创建新的线程来处理连接请求,虽然创建线程比创建进程要更加轻量级,但还是有一定的代价,而 prethread 模式就是为了解决这个问题。
和 prefork 类似,prethread 模式会预先创建线程,然后才开始接受用户的请求,当有新的连接进来的时候,就可以省去创建线程的操作,让用户感觉更快、体验更好。
由于多线程之间数据共享和通信比较方便,因此实际上 prethread 的实现方式相比 prefork 要灵活一些,常见的实现方式有下面几种:
主进程 accept,然后将连接交给某个线程处理。
子线程都尝试去 accept,最终只有一个线程 accept 成功,方案的基本示意图如下:
Apache 服务器的 MPM worker 模式本质上就是一种 prethread 方案,但稍微做了改进。Apache 服务器会首先创建多个进程,每个进程里面再创建多个线程,这样做主要是为了考虑稳定性,即:即使某个子进程里面的某个线程异常导致整个子进程退出,还会有其他子进程继续提供服务,不会导致整个服务器全部挂掉。
prethread 理论上可以比 prefork 支持更多的并发连接,Apache 服务器 MPM worker 模式默认支持 16 × 25 = 400 个并发处理线程。
小结
什么样的系统比较适合本期所讲的高性能模式?原因是什么?
评论1:
不同并发模式的选择,还要考察三个指标,分别是响应时间(RT),并发数(Concurrency),吞吐量(TPS)。三者关系,吞吐量=并发数/平均响应时间。不同类型的系统,对这三个指标的要求不一样。
三高系统,比如秒杀、即时通信,不能使用。
三低系统,比如ToB系统,运营类、管理类系统,一般可以使用。
高吞吐系统,如果是内存计算为主的,一般可以使用,如果是网络IO为主的,一般不能使用。
评论2:
高并发需要根据两个条件划分:连接数量,请求数量。
1. 海量连接(成千上万)海量请求:例如抢购,双十一等
这样的系统,我觉得这讲所说的模式都不适应,面对海量的连接至少要使用IO复用模型或者异步IO模型,针对海量的请求,无论使用多进程处理还是多线程,单机都是无法支撑的,应该集群了吧。
2. 常量连接(几十上百)海量请求:例如中间件
3. 海量连接常量请求:例如门户网站
这种系统我觉得非常适合使用netty这样的NIO框架来实现,IO复用模型可以处理海量的连接,而每个连接的请求数据量会很小,处理会很长快,如华仔说的门户网站,只要简单返回页面即可。
4. 常量连接常量请求:例如内部运营系统,管理系统
这种系统,本讲的模式就很适合了。
评论3:
BIO:一个线程处理一个请求。缺点:高并发量,线程数多,浪费资源。Tomcat7或以下,在Linux系统中默认使用这种方式。可以适用于小到中规模的客户端并发数场景,无法胜任大规模并发业务。如果编程控制不善,可能造成系统资源耗尽。
NIO:多路复用IO,少量线程处理大量的请求。Tomcat8在Linux系统中默认用。Tomcat7必须修改Connector配置来启动。适用于“高并发”
评论3:
如何根据硬件配置计算可支持的并发数呢?一般按照CPU核数*2来计算