这 2 篇文章是我在学习架构的过程中,总结的笔记:
- 第一篇 架构学习笔记1
- 0,什么是架构师
- 1,软件架构出现的历史背景
- 2,架构设计的目的
- 3,架构设计三原则
- 4,架构复杂度的六个来源
- 5,架构设计流程
- 6,常用的高性能架构模式
- 第二篇 架构学习笔记2
- 7,常用的高可用架构模式
- 8,常用的可扩展架构模式
- 9,架构师如何判断技术演进的方向
- 10,互联网架构模板
架构设计的关键思维是判断和取舍,程序设计的关键思维是逻辑和实现。
如果把程序员类比成建筑师,按照能力水平来分,大体可分为三个层次:
20 世纪 60 年代第一次软件危机引出了“结构化编程”,创造了“模块”概念;“软件危机”、“软件工程”、“结构化程序设计” 都被提了出来。
第一次软件危机中的重要事件:
布鲁克斯后来写出了注明的《人月神话》。
20 世纪 80 年代第二次软件危机引出了“面向对象编程”,创造了“对象”概念,这主要得益于 C++,以及后来的 Java、C# 把面向对象推向了新的高峰。
20 世纪 90 年代“软件架构”开始流行,创造了“组件”概念。
随着软件系统规模的增加,计算相关的算法和数据结构不再构成主要的设计问题;当系统由许多部分组成时,整个系统的组织,也就是所说的“软件架构”,导致了一系列新的设计问题。 —— 《软件架构介绍》
整个软件技术发展的历史,其实就是一部与“复杂度”斗争的历史,架构的出现也不例外。
架构设计的主要目的是为了解决软件系统复杂度带来的问题。
软件架构需要根据业务发展不断变化。
架构设计中的三原则:
高性能带来的复杂度主要体现在两方面:
高可用是指系统无中断地执行其功能的能力,代表系统的可用性程度,是进行系统设计时的准则之一。
高可用一般都是通过**“冗余”机器**来完成的,通过冗余增强了可用性,但同时也带来了复杂性。
高可用可分为:
高可用中的决策问题
当发现系统中的服务不可用时,要找一个可用的服务替代,这涉及到如何决策的问题。无论是计算高可用还是存储高可用,其基础都是“状态决策”,即系统需要能够判断当前的状态是正常还是异常,如果出现了异常就要采取行动来保证高可用。
如果状态决策本身都是有错误或者有偏差的,那么后续的任何行动和处理无论多么完美也都没有意义和价值。但实际上,恰好存在一个本质的矛盾:通过冗余来实现的高可用系统,状态决策本质上就不可能做到完全正确。
几种常见的决策方式:
综合分析,无论采取什么样的方案,状态决策都不可能做到任何场景下都没有问题,但完全不做高可用方案又会产生更大的问题,如何选取适合系统的高可用方案,也是一个复杂的分析、判断和选择的过程。
可扩展性指系统为了应对将来需求变化而提供的一种扩展能力,当有新的需求出现时,系统不需要或者仅需要少量修改就可以支持,无须整个系统重构或者重建。
在软件开发领域,面向对象思想的提出,就是为了解决可扩展性带来的问题;后来的设计模式,更是将可扩展性做到了极致。
设计具备良好可扩展性的系统,有两个基本条件:正确预测变化、完美封装变化。
预测变化的复杂性在于:
对于架构师来说,如何把握预测的程度和提升预测结果的准确性,是一件很复杂的事情,而且没有通用的标准可以简单套上去,更多是靠自己的经验、直觉。
即使预测很准确,如果方案不合适,则系统扩展一样很麻烦:
当架构方案涉及几百上千甚至上万台服务器,成本就会变成一个非常重要的架构设计考虑点。如果能通过一个架构方案的设计,就能轻松节约几千万元,不但展现了技术的强大力量,也带来了可观的收益。
低成本本质上是与高性能和高可用冲突的:
低成本给架构设计带来的主要复杂度体现在,往往只有“创新”才能达到低成本目标。
新技术的例子:
Memcache、Redi
等)的出现是为了解决关系型数据库无法应对高并发访问的压力。Sphinx、Elasticsearch、Solr
)的出现是为了解决关系型数据库 like
搜索的低效的问题。Hadoop
的出现是为了解决传统文件系统无法应对海量数据存储和计算的问题。Kafka
消息系统。无论是引入新技术,还是自己创造新技术,都是一件复杂的事情。
安全可以分为两类:
规模带来复杂度的主要原因就是“量变引起质变”,当数量超过一定的阈值后,复杂度会发生质的变化。
常见的规模带来的复杂度有:
只有正确分析出了系统的复杂性,后续的架构设计方案才不会偏离方向。将主要的复杂度问题列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题。
如何设计备选方案:
列出我们需要关注的质量属性点,然后分别从这些质量属性的维度去评估每个方案,再综合挑选适合当时情况的最优方案。
详细方案设计就是将方案涉及的关键技术细节给确定下来。
互联网业务兴起之后,海量用户加上海量数据的特点,单个数据库服务器已经难以满足业务需要,必须考虑数据库集群的方式来提升性能。
高性能数据库集群架构:
读写分离的基本原理是将数据库读写操作分散到不同的节点上,其基本架构图如下:
读写分离的基本实现是:
主从与主备的区别:
读写分离将引入两个问题:
目前的开源数据库中间件方案中,MySQL 官方推荐 MySQL Router
,它的主要功能有读写分离、故障自动切换、负载均衡、连接池等,其基本架构如下:
奇虎 360 公司也开源了自己的数据库中间件 Atlas,Atlas 是基于 MySQL Proxy 实现的,基本架构如下:
单个数据库服务器存储的数据量不能太大(否则会出现存储,性能等很多问题),需要控制在一定的范围内。为了满足业务数据存储的需求,就需要将存储分散到多台数据库服务器上。
可以通过分库或者分表来实现。
分库指的是按照业务模块将数据分散到不同的数据库服务器。
例如,一个简单的电商网站,包括用户、商品、订单三个业务模块,我们可以将用户数据、商品数据、订单数据分开放到三台不同的数据库服务器上,而不是将所有数据都放在一台数据库服务器上。
分库带来的问题:
随着业务的发展,同一业务的单表数据会达到单台数据库服务器的处理瓶颈,此时就需要对单表数据进行拆分。
单表数据拆分有两种方式:垂直分表和水平分表。示意图如下:
实际架构设计过程中并不局限切分的次数,可以切两次,也可以切很多次。单表切分为多表后,不一定要分散到不同数据库中,可根据实际需求而定。
分表也会引入额外的问题:
id 1~999999,1000000 ~ 1999999
来分表(最终导致的结果可能使得每个表中的数据分配不均)NoSQL = Not Only SQL,NoSQL 方案带来的优势,本质上是牺牲 ACID 中的某个或者某几个特性,从而在某些方面比关系型数据库更加优秀。
NoSQL 的含义在不同的时期有着不同的含义,下图供参考:
常见的 NoSQL 方案可分为 4 类:
缓存是为了弥补存储系统在一些复杂业务场景下的不足,其基本原理是将可能重复使用的数据放到内存中,一次生成、多次使用,避免每次使用都去访问存储系统。
高性能是一件很复杂很有挑战的事情,高性能架构设计主要集中在两方面:
单服务器高性能的关键是采取的并发模型,这都和操作系统的 I/O 模型及进程模型相关:
PPC 是 Process Per Connection
的缩写,其含义是指每次有新的连接就新建一个进程去专门处理这个连接的请求。
PPC 模式实现简单,比较适合服务器的连接数没那么多的情况。世界上第一个 web 服务器 CERN httpd
就采用了这种模式。
主要步骤:
prefork
prefork 就是提前创建进程。系统在启动的时候就预先创建好进程,然后才开始接受用户的请求,当有新的连接进来的时候,就可以省去 fork 进程的操作,速度更快。
prefork 中的惊群问题
当有新的连接进入时,多个子进程都去 accept 同一个 socket,但最终只会有一个进程能 accept 成功。
当所有阻塞在 accept 上的子进程都被唤醒时,就导致了不必要的进程调度和上下文切换,会影响系统性能,这就是惊群问题。Linux 2.6 版本后内核已经解决了 accept 惊群问题。
TPC 是 Thread Per Connection
的缩写,是指每次有新的连接就新建一个线程去专门处理这个连接的请求。
与进程相比,TPC 的优点:
TPC 实际上是解决或者弱化了 PPC fork 代价高的问题和父子进程通信复杂的问题。
主要步骤:
TPC 存在的问题:
prethread
和 prefork 类似,prethread 模式会预先创建线程。prethread 的实现方式相比 prefork 要灵活一些,常见的实现方式有:
PPC 和 TPC 模式,它们的优点是实现简单,缺点是都无法支撑高并发的场景。
I/O 多路复用技术:
Reactor 的中文是“反应堆”,其实就是 I/O 多路复用结合线程池,其完美地解决了 PPC 和 TPC 的问题。
Reactor 模式也叫 Dispatcher 模式,即 I/O 多路复用统一监听事件,收到事件后分配(Dispatch)给某个进程。
Reactor 模式的核心组成部分包括 Reactor 和处理资源池(进程池或线程池):
Reactor 模式有这三种典型的实现方案:
单 Reactor 单进程 / 线程的方案示意图(以进程为例):
步骤说明:
单 Reactor 单进程模式的优缺点:
单 Reactor 单进程的方案在实践中应用场景不多,只适用于业务处理非常快的场景,比较著名的是 Redis。
对于不同的编程语言,需要注意的是:
流程图如下:
主要步骤:
单 Reator 多线程方案能够充分利用多核多 CPU 的处理能力,但同时也存在下面的问题:
为了解决单 Reactor 多线程的问题,最直观的方法就是将单 Reactor 改为多 Reactor。
多 Reactor 多进程 / 线程方案示意图是(以进程为例):
主要步骤:
著名的开源系统 Nginx 采用的是多 Reactor 多进程,采用多 Reactor 多线程的实现有 Memcache 和 Netty。
Nginx 采用的是多 Reactor 多进程的模式,但方案与标准的多 Reactor 多进程有差异。具体差异表现为主进程中仅仅创建了监听端口,并没有创建 mainReactor 来“accept”连接,而是由子进程的 Reactor 来“accept”连接,通过锁来控制一次只有一个子进程进行“accept”,子进程“accept”新连接后就放到自己的 Reactor 进行处理,不会再分配给其他子进程。
Reactor 是非阻塞同步网络模型,因为真正的 read 和 send 操作都需要用户进程同步操作。这里的“同步”指用户进程在执行 read 和 send 这类 I/O 操作的时候是同步的,如果把 I/O 操作改为异步就能够进一步提升性能,这就是异步网络模型 Proactor。
Reactor 可以理解为“来了事件我通知你,你来处理”,而 Proactor 可以理解为“来了事件我来处理,处理完了我通知你”。
Proactor 模型示意图:
主要步骤:
理论上 Proactor 比 Reactor 效率要高一些,异步 I/O 能够充分利用 DMA 特性,让 I/O 操作与计算重叠,但要实现真正的异步 I/O,操作系统需要做大量的工作。
高性能集群的复杂性主要体现在需要增加一个任务分配器,以及为任务选择一个合适的任务分配算法。
对于任务分配器,现在更流行的通用叫法是“负载均衡器”,这个名称有一定的误导性,会让人认为任务分配的目的是要保持各个计算单元的负载达到均衡状态。
而实际上任务分配并不只是考虑计算单元的负载均衡,不同的任务分配算法目标是不一样的,有的基于负载考虑,有的基于性能(吞吐量、响应时间)考虑,有的基于业务考虑。
常见的负载均衡系统包括 3 种:
DNS 是最简单也是最常见的负载均衡方式,一般用来实现地理级别的均衡。DNS 负载均衡的本质是:DNS 解析同一个域名可以返回不同的 IP 地址。
例如,北方的用户访问北京的机房,南方的用户访问深圳的机房。那么同样是 www.baidu.com
:
DNS 负载均衡的优缺点:
硬件负载均衡是通过单独的硬件设备来实现负载均衡功能,这类设备和路由器、交换机类似,可以理解为一个用于负载均衡的基础网络设备。目前业界典型的硬件负载均衡设备有两款:F5 和 A10。
硬件负载均衡的优缺点是:
软件负载均衡通过负载均衡软件来实现负载均衡功能,常见的有 Nginx 和 LVS:
软件和硬件的最主要区别就在于性能,硬件负载均衡性能远远高于软件负载均衡性能:
下面是 Nginx 的负载均衡架构示意图:
软件负载均衡的优点:
4,组合使用负载均衡
每种方式都有一些优缺点,在实际应用中,我们可以基于它们的优缺点进行组合使用。组合的基本原则为:
下面是一个大型的负载均衡应用:
整个系统的负载均衡分为三层:
www.xxx.com
部署在北京、广州、上海三个机房一般在大型业务场景下才会这样用,如果业务量没这么大,则没有必要严格照搬这套架构。
常见的负载均衡算法: