目录
- 架构设计原则
-
- 架构设计流程
-
- 有的放矢:识别复杂度
- 按图索骥:设计备选方案
- 深思熟虑:评估和选择备选方案
- 计算高性能
-
- 单服务器场景下的高性能
-
- PPC
- prefock
- TPC
- prethread
- Reactor
-
- 单Reactor单进程/线程
- 单Reactor多线程
- 多Reactor多进程/线程
- 集群高性能
-
架构设计原则
架构设计需要遵循的三大原则:合适原则、简单原则、演化原则
合适原则
合适优于业界领先
举个栗子,几个人规模的团队想做一个类似QQ的“亿级用户平台”,最终会导致整个项目的开发和后续的迭代成为灾难,主要有三个原因:
- 没那么多人,却想干那么多活,是失败的第一个主要原因
- 没有那么多积累,却想一步登天,是失败的第二个主要原因
- 没有那么卓越的业务场景,却幻想灵光一闪成为天才,是失败的第三个主要原因
真正优秀的架构都是在企业当前人力、条件、业务等各种约束下设计出来的,能够合理地将资源整合在一起并发挥出最大功效,满足当前的业务场景并能够快速落地
简单原则
简单优于复杂
总的来说,架构应该在满足业务场景及可扩展性前提下,尽可能简单地设计;软件领域的复杂性主要体现在两个方面:
- 结构复杂:结构复杂的特点是组成系统的组件更多,同时组件之间的依赖关系更加复杂,存在两个问题:
- 组件越多,就越有可能因为某个组件的异常导致系统不可用
- 某个组件改动,会影响关联的所有组件
- 逻辑复杂:减少组件复杂性,会带来业务逻辑的复杂,存在以下问题:
- 系统庞大,维护困难
- 几十上百人维护同一套代码,开发、测试、部署会异常混乱
- 故障排查困难,很难定位具体问题
- 。。。等等
因此,架构设计时如果一个简单的方案和复杂的方案都可以满足需求,一定要选简单的方案。KISS原则:Keep It Simple, Stupid!
演化原则
演化优于一步到位
软件“架构”的概念源自于建筑的“架构”,但是两者存在一个根本的区别:对于建筑来说,永恒是主题;而对于软件来说,变化才是主题!!!
软件结构需要根据业务的发展而不断变化。所以,想一步到位设计一个软件系统是不可取的。软件架构设计的过程:
- 首先,设计出来的架构要满足当时的业务需要
- 其次,架构要不断地在实际应用过程中迭代,保留优秀的设计、修复有缺陷的设计、改正错误的设计、去掉无用的设计,使得架构逐渐完善
- 最后,当业务发生变化时,架构要扩展、重构、甚至重写;代码也许会重写,但有价值的经验、教训、逻辑、设计等却可以在新架构中完善
架构设计流程
有的放矢:识别复杂度
架构设计本质上也是为了解决系统问题,而解决问题的第一步应该是了解问题。对于架构设计而言,了解问题即了解系统复杂度
按图索骥:设计备选方案
架构设计时,架构师需要将视野放宽,考虑更多可能性。因此在架构设计中有三个常见错误:
- 设计最优秀的方案:根据前面说到的合适原则和简单原则,方案并非越“优秀”越好
- 只做一个方案:架构师一般会有自己的倾向,解决一个问题可能也能想到多个解决方案,但是只因为自己的倾向就做简单的决策而仅得到一个方案,会出现一些问题:
- 心理评估过于简单,可能没有考虑全面。可能某个方案因为自己想到的某个缺点就否决了,但是事实上所有的方案都不是完美的,留下的方案可能也有缺点而自己不自知。
- 架构师可能评估的标准和方向并不正确
- 单一方案设计会出现过度辩护的情况,即在架构评审时,针对方案的问题和疑问,架构师会竭尽全力去辩护,甚至强词夺理;
- 备选方案过于详细:将注意力集中到细节中而忽略了整体的技术设计,可能会导致备选方案间的差异不大,且不核心;正确的做法是备选阶段关注技术选型,而不是技术细节
深思熟虑:评估和选择备选方案
每种方案都是可行的,每种方案都不是完美的,那怎样选出最优的解决方案呢?
- 最简派?选择最简单的方案
- 最牛派?选择最牛最复杂的技术
- 最熟派?架构师自己对哪块技术比较熟,就使用哪个技术方案
- 领导派?给出几种备选方案,让领导做决定
上面几种“派别”不能说对错,各有各自的使用场景。对于通用的架构设计来说,评估方案的具体方式建议为:列出我们需要关注的质量属性点,然后分别从这些质量属性的维度去评估每个方案,再综合挑选适合当时场景的最优方案。(可以考虑“360度环评”)
计算高性能
单服务器场景下的高性能
单服务器性能关键之一即是网络模型,网络模型编程有两个关键设计点:
- 服务器是如何管理连接的?
- 服务器是如何处理请求的?
以上两个设计点最终都和操作系统的I/O模型有关及进程模型有关
- I/O模型:阻塞、非阻塞、同步、异步
- 进程模型:单进程、多进程、多线程
PPC
ppc即Process per Connection
,其含义是每次有新的连接就新建一个进程来处理,是传统的UNIX网络服务器所采用的模型,其流程如下:
- 父进程监听并接收连接
- 父进程
fock
子进程
- 子进程处理连接的读写请求(包括read、业务处理、write)
- 子进程关闭连接(子进程close)
存在的问题:
fock
代价高,需要分配很多内核资源,需要将内存映像从父进程复制到子进程
- 父子进程通信复杂:需要采用IPC之类的进程通信机制
- 对于多请求的系统来说,进程处理时间稍久就会造成进程堆积,从而产生大量进程调度和切换成本
prefock
pre-fock
即提前创建子进程,不用在请求来的时候再fock,可以降低请求的时间消耗,但是PPC的核心问题如IPC何进程调度切换成本问题不能解决
TPC
tpc即Thread per Connection
,其含义是每次有新的连接时就新建一个线程来处理。与进程相比,线程更加轻量,创建线程的消耗和线程切换的代价更小,同时线程间通信比进程间通信更加简单;TPC的基本流程如下:
- 父进程监听并接收连接
- 父进程创建子线程
- 子线程处理连接的读写请求(子线程read、业务处理、write)
- 子线程关闭连接
TPC解决了(或者弱化了)PPC中的进程fock代价高和父子进程通信复杂的问题。但是也引入了其他问题:
- 线程间共享内存空间,共享资源的处理需要加锁,甚至导致死锁问题
- 线程间可能互相影响,某个线程异常时,可能导致整个进程退出
prethread
和prefock类似,预先创建线程,可以减少新连接到来时新建线程的时间消耗
Reactor
不管是PPC还是TPC,都有一个问题即每次请求来之后,进程/线程都需要同步去read,如果当前连接没有数据可读,则进程/线程会处于阻塞状态,这种阻塞状态会使进程/线程的使用率更低,因此出现了I/O多路复用
I/O多路复用中“多路”就是指多条连接,“复用”指多条连接使用同一个阻塞对象,如select中的阻塞对象是fd_set
, epoll中阻塞对象为epoll_creat
创建的文件描述符
I/O多路复用技术的两个关键实现点:
- 当多条连接共用一个阻塞对象后,进程只需要在一个阻塞对象上等待,而无需再轮询所有连接
- 当某条连接有新的数据可以处理时,操作系统会通知进程,进程从阻塞状态返回,开始进行业务处理
I/O多路复用结合线程池,即Reactor模式; Reactor模式又叫“Dispatcher模式”,即I/O多路复用统一监听事件,收到事件后分配(dispatch)给某个进程;
Reactor模式的核心组成部分包括Reactor和资源处理池(进程池或者线程池),其中Reactor负责监听和分配事件,资源处理池负责处理事件
单Reactor单进程/线程
- Reactor对象通过select监控连接事件,收到事件后通过dispatch进行分发
- 如果是连接建立事件,则通知Acceptor创建连接,并新建一个Handler,以备处理后续的各种事件
- 如果不是连接建立事件,则Reactor会调用第2步中创建的Handler来进行响应
- Handler会完成read -> 业务处理 -> send的完整业务流程
但Reactor单进程/线程的典型代表是Redis
单Reactor多线程
单Reactor多线程解决了单Reactor单进程/线程中不能利用多核CPU的问题,通过建立线程池的方式来处理业务逻辑,其过程大致如下:
- 主线程中,Reactor对象通过select监控连接事件,收到事件后进行分发
- 如果是连接建立事件,则由Acceptor处理,接受连接并创建对应的Handler
- 如果不是连接建立事件,则Reactor会调用第2步创建的Handler来进行响应(以上三步和单Reactor单进程比较相同)
- Handler只负责响应事件,不进行业务处理;Handler通过read读取到数据后,会发给Processor进行业务处理
- Processor会在独立的子线程中进行业务处理,并将结果发送给主线程的Handler
- Handler收到Processor的结果后,通过send将结果返回给Client
这里只有多线程而不用多进程的原因在于,如果采用多进程,子进程完成业务处理后将结果返回给父进程,并通知父进程发送给那个client会比较麻烦(应该还是进程间通信的复杂度?)
单Reactor多线程的问题在于Reactor承担所有事件的监听和响应,可能会成为性能瓶颈;二是多线程的数据共享和访问比较复杂。
多Reactor多进程/线程
通过引入多Reactor,可以解决单Reactor多线程的问题
- 父进程中的
MainReactor
对象通过select监控连接建立事件,收到事件后通过Acceptor接收,将新的连接分配给某个子进程
- 子进程的
SubReactor
负责将MainReactor
分配的连接加入连接队列进行监听,并创建一个Handler用于后续事件处理
- 当有新的事件发生时,
SubReactor
会调用对应Handler响应,Handler在当前进程中完成Read -> 业务处理 -> send的整个处理流程
优点有如下几个:
- 父子Reactor分工明确,父Reactor只负责接收新连接,子Reactor负责后续事件处理(并发)
- 子Reactor所在进程完成整个业务处理,无需再返回数据给父进程
采用多Reactor多进程的软件代表为Nginx,采用多Reactor多线程的代表有Memcache和Netty
集群高性能
负载均衡分类
- DNS负载均衡: 通过DNS解析进行负载均衡
- 优点:简单、成本低
- 缺点:负载粒度太粗(地区级别),更新不及时(DNS缓存时间一般较长),扩展性不太好
- 硬件负载均衡:通过硬件设备实现负载均衡,代表F5和A10;贵
- 软件负责均衡:通过软件实现负载均衡,常见Nginx和LVS
- Nginx是七层负载均衡,一般通过http中url不同,负载到不同的实例进行处理
- LVS是四层负载均衡,主要在TCP层对不同的端口进行流量分发