拆书先生Java 杂谈基础知识

《从零开始学架构》读书笔记四:计算高性能

2019-04-19  本文已影响2人  李子悟

 高性能计算是每个程序员的追求,好的算法,合理的数据结构这些都对性能起着至关重要的作用。但是站在架构师的角度,则自然考虑的是高性能架构设计
 高性能架构无非集中在单机和集群上,对于单机来说,我们的目标就是将单机服务器的性能发挥到极致。至于集群则是我们的单机无法支撑我们的业务所采用的架构方案,自然这也会给我们带来相应的复杂度。
 单机服务器高性能的关键之一就是我们服务器所采取的网络编程模型,网络编程模型有如下两个设计点,即如何管理连接和如何处理响应。这两个设计点最终都和操作系统的I/O模型和进程模型相关:
 1.I/O模型:阻塞,非阻塞,同步,异步
 2.进程模型:单进程,多进程,多线程
首先来看一看PPC(process per connection)其含义就是每次有新的连接就创建一个进程去专门处理这个连接的请求,这是传统的unix网络服务器所采用的模型。基本的流程如下:


PPC模型.jpg

注:图中有一个小细节,父进程fork子进程后,直接调用了close,看起来好像关闭了连接,其实只是将引用计数器减一,等子进程也调用close后,连接对应的文件描述符引用变为0后,系统才会真正的关闭连接。
1.父进程接受连接(accept操作)
2.父进程fork子进程
3.子进程处理连接的请求
4.子进程关闭连接
这个模型显然处理不了高并发场景,一来fork进程的代价十分巨大,二来当请求的连接时常比较大时,会堆积大量的进程。导致操作系统进程调度和切换的频率越来越高,所以这个方案也就解决200左右的并发。在PPC模型中,当连接进来时才fork新进程,为了提高处理效率于是就有了prefork模型,见名知意,就是提前创建进程,系统在启动的时候就预先创建好进程,然后才开始接受用户请求。如下入所示:


prefork模型.jpg
虽然Prefork解决了fork进程慢的问题,但是在高并发场景下还是不合适。因为进程间通信的成本也很高,但是自然也有好的一面就是进程是独自享有自己的内存空间的,不用过多关注数据安全问题。
既然进程这么笨重,就有了TPC模型的诞生(Thread per connection)相比进程线程更加轻量级,实际上TPC解决或弱化了PPC的问题(fork代价高和进程间通信的复杂度问题)
 TPC的基本流程图如下:
TPC模型.jpg
注:图中有一个小细节,和PPC相比,主进程不需要close连接了,原因是子线程是共享主进程的内存空间的,连接的文件描述符并没有被复制,因此只需要close一次即可。
TPC解决了fork进程代价过高和通信复杂的问题,同时也带来了新的问题,那就是线程间会出现的互斥和共享,一不小心就导致了死锁问题
,prethread和prefork的思想是想通的这里就不多提及了。

 无论是PPC还是TPC,连接结束后就销毁了,这样做其实是一个巨大的浪费,为了解决资源复用的问题就引入了进程池和线程池的概念,这里我们以进TPC的进程池为例,当引入这种资源池的处理方式后,会引出一个新的问题:进程如何才能高效的处理多个业务,当一个进程处理一个连接时,进程采用的是read-业务处理-write的处理流程,因为read是阻塞操作,当前连接如果没有数据可读那么就阻塞到read操作上,这在PPC模型中不会有问题的,但是一个进程处理多个连接那么就有问题了。如果这个进程阻塞在某个连接的read操作上,此时其他连接有数据可读,进程也无法进行处理,很显然这样和高性能是相悖的。解决这个问题最简单的方式就是将read操作改为非阻塞操作,然后进程不断轮询多个连接。这种方式虽然能解决阻塞的问题,但并不是一个优雅的解决方案,如果连接数过多那是十分消耗时间的。为了能够更好的解决上述问题,一种自然而然的想法就是当连接上有数据的时候进程才去处理,这就是I/O多路复用的技术来源。这里有必要解释一下,我们有时候会想当然的认为多路复用就是多条连接复用同一通道。这是一个严重的误区,其正确的语意就是多条连接复用同一个阻塞对象,这个阻塞对象和具体的实现有关,如果使用select,则这个公共的阻塞对象就是select用到的fd_set,如果使用epoll,就是epoll_create创建的文件描述符。多路复用技术归纳起来有如下两个关键点:
1.当多条连接公用一个阻塞对象后,进程只需要在一个阻塞对象上等待,而无需轮询所有连接
2.当某条连接有新的数据可以处理时 ,操作系统会通知进程,将阻塞的状态返回,开始进行业务处理。
 多路复用技术结合线程池或进程池,完美的解决了PPC和TPC模型的问题,这也就是大名鼎鼎的reactor模式,也就是非阻塞同步网络模型。其本质就是I/O多路复用统一监听事件,收到事件后分配给某个进程处理。reactor可以有多个。这就有了许多排列组合方案了。为我们提供了很多的灵活性。下面来看一看经典的reactor实现方案:
1.单reactor单进程/单线程


单reactor单进程.jpg
方案流是,reactor对象通过select监控连接事件,收到事件后通过dispatch进行分发。如果是连接建立的事件,则由Acceptor处理,Acceptor通过accept接受连接建立一个handler来处理后续的各种事件。如果不是连接建立事件,则reactor会调用连接对应的Handler进行响应。Handler会完成read->业务处理->send的完整业务流程。单reactor单进程的优点就是简单,没有进程间通信,没有竞争,全部都在同一个进程内完成。但缺点也同样明显就是无法利用多核CPU的优势。还有handler在处理某个连接的业务时,整个进程都无法处理其他的。连接事件,很容易导致性能瓶颈,因此这种模式只适用于业务处理非常快的场景,目前redis的通信模型就采用了此模型。
2.单reactor多进程/多线程
单reactor多线程.jpg
这种模式可以利用多核CPU的优势,但是线程通信直接的数据共享和互斥问题是需要去解决的,通信模型比较复杂,reactor承担所有的事件和响应,只在主线程中运行,瞬间高并发时会成为性能瓶颈。
3.多reactor多进程/多线程
多reactor多进程.jpg
目前开源软件中的nginx,netty,memcache都是采用的此模型。
 单台服务器始终会有它的瓶颈所在,当单机无法满足我们的 业务时就需要采用集群策略了,也就是增加更多的服务器 来提升系统的处理能力。那么负载均衡就是其中必不可少的部分了。常见的负载均衡有三种DNS,硬件和软件负载均衡。DNS负载均衡的缓存更新时间比较长,修改DNS配置后不能立即生效,很多用户还会继续访问修改前的IP,导致访问失败。硬件负载均衡的特点就是功能强大,拿F5来说,可以达到百万/每秒的处理能力,而且支持安全防护比如防火墙和ddos攻击,缺点就是太贵非土豪公司 请勿尝试。软件负载均衡方面有基于网络层的LVS负载均衡,可以达到10万/秒的处理能力。因为基于网络层所以适用面积比较广,几乎所有的应用都可以用它来做。另一个比较著名的就是nginx,可以支撑5万/秒的处理能力。它是基于应用层的负载均衡,通常和协议挂钩,支持HTTP,E-MAIL协议等。这些负载均衡可以自由搭配使用来满足自己的业务。
 常见的负载均衡算法
1.任务平分类(轮询,加权轮询)
2.负载最低优先
3.性能最优类
4.Hash类(源地址Hash,ID Hash)
上一篇 下一篇

猜你喜欢

热点阅读