公号:码农充电站pro

主页:https://codeshellme.github.io

这 2 篇文章是我在学习架构的过程中,总结的笔记:

  • 第一篇 架构学习笔记1
    • 0,什么是架构师
    • 1,软件架构出现的历史背景
    • 2,架构设计的目的
    • 3,架构设计三原则
    • 4,架构复杂度的六个来源
    • 5,架构设计流程
    • 6,常用的高性能架构模式
  • 第二篇 架构学习笔记2
    • 7,常用的高可用架构模式
    • 8,常用的可扩展架构模式
    • 9,架构师如何判断技术演进的方向
    • 10,互联网架构模板

架构设计的关键思维是判断和取舍,程序设计的关键思维是逻辑和实现。

0,什么是架构师

如果把程序员类比成建筑师,按照能力水平来分,大体可分为三个层次:

  • 搬砖师:他们的编程能力和业务基本上停留在堆叠代码,按照要求去实现功能需求的层面
  • 工程师:致力于不断提升软件代码的工程质量的程序员,代码在他们眼里是一种艺术,是自己生命的一部分。他们会把写出来的代码改了又改,直到让自己满意为止。
  • 架构师:掌控全局,对软件工程的执行结果负责,包括:按时按质进行软件的迭代和发布、敏捷地响应需求变更、防范软件质量风险(避免发生软件质量事故)、降低迭代维护成本。

1,软件架构出现的历史背景

20 世纪 60 年代第一次软件危机引出了“结构化编程”,创造了“模块”概念;“软件危机”、“软件工程”、“结构化程序设计” 都被提了出来。

第一次软件危机中的重要事件:

  • 1963 年美国的水手一号火箭发射失败事故,是因为一行 FORTRAN 代码错误导致的。
  • 布鲁克斯主导的 IBM 的 System/360 的操作系统开发,投入巨大,却没能做好。

布鲁克斯后来写出了注明的《人月神话》。

20 世纪 80 年代第二次软件危机引出了“面向对象编程”,创造了“对象”概念,这主要得益于 C++,以及后来的 Java、C# 把面向对象推向了新的高峰。

20 世纪 90 年代“软件架构”开始流行,创造了“组件”概念。

随着软件系统规模的增加,计算相关的算法和数据结构不再构成主要的设计问题;当系统由许多部分组成时,整个系统的组织,也就是所说的“软件架构”,导致了一系列新的设计问题。 —— 《软件架构介绍》

2,架构设计的目的

整个软件技术发展的历史,其实就是一部与“复杂度”斗争的历史,架构的出现也不例外。

架构设计的主要目的是为了解决软件系统复杂度带来的问题

3,架构设计三原则

软件架构需要根据业务发展不断变化。

架构设计中的三原则:

  • 合适原则:合适的就是最好的,不需要追求最优。
  • 简单原则:简单的就是最美的。
  • 演化原则:优秀的系统是一步步演化过来的,而不是一步到位的。

4,架构复杂度的六个来源

1,高性能

高性能带来的复杂度主要体现在两方面:

  • 单台计算机内部为了高性能带来的复杂度,最关键的是操作系统。
    • 计算机的性能发展是由硬件(CPU,内存)驱动的,操作系统是跟随硬件的发展而发展的
    • 操作系统是软件系统的运行环境,操作系统的复杂度直接决定了软件系统的复杂度
    • 并发:操作系统和性能最相关的就是进程和线程
      • 要完成一个高性能的软件系统,需要考虑如多进程、多线程、进程间通信、多线程并发等技术点。
    • 并行:多进程多线程虽然让多任务并行处理的性能大大提升,并不能做到时间上真正的并行。并行的解决方案有三种:
      • SMP(Symmetric Multi-Processor,对称多处理器结构):是最常见的一种
      • NUMA(Non-Uniform Memory Access,非一致存储访问结构)
      • MPP(Massive Parallel Processing,海量并行处理结构)
  • 多台计算机集群为了高性能带来的复杂度

2,高可用

高可用是指系统无中断地执行其功能的能力,代表系统的可用性程度,是进行系统设计时的准则之一。

高可用一般都是通过**“冗余”机器**来完成的,通过冗余增强了可用性,但同时也带来了复杂性。

高可用可分为:

  • 计算高可用:无论在哪台机器上进行计算,同样的算法和输入数据,产出的结果都是一样的
  • 存储高可用:其难点不在于如何备份数据,而在于如何减少或者规避数据不一致对业务造成的影响
    • 分布式中的 CAP 定理,存储高可用不可能同时满足“一致性、可用性、分区容错性”,最多满足其中两个,这需要在做架构设计时结合业务进行取舍

高可用中的决策问题

当发现系统中的服务不可用时,要找一个可用的服务替代,这涉及到如何决策的问题。无论是计算高可用还是存储高可用,其基础都是“状态决策”,即系统需要能够判断当前的状态是正常还是异常,如果出现了异常就要采取行动来保证高可用。

如果状态决策本身都是有错误或者有偏差的,那么后续的任何行动和处理无论多么完美也都没有意义和价值。但实际上,恰好存在一个本质的矛盾:通过冗余来实现的高可用系统,状态决策本质上就不可能做到完全正确

几种常见的决策方式:

  • 独裁式
    • 存在一个独立的决策主体,称为“决策者”,负责收集信息然后进行决策;
    • 所有冗余的个体,称为“上报者”,都将状态信息发送给决策者。
    • 缺点:因为决策者只有一个,所以独裁式不会出现决策混乱的问题。但是当决策者本身故障时,整个系统就无法实现准确的状态决策。
  • 协商式:指的是两个独立的个体通过交流信息,然后根据规则进行决策,最常用的协商式决策就是主备决策
    • 协议规则是:
      • 2 台服务器启动时都是备机。
      • 2 台服务器建立连接。
      • 2 台服务器交换状态信息。
      • 某 1 台服务器做出决策,成为主机;另一台服务器继续保持备机身份。
    • 协商式决策的架构不复杂,规则也不复杂,其难点在于,如果两者的信息交换出现问题(比如主备连接中断),此时状态决策应该怎么做
  • 民主式:指的是多个独立的个体通过投票的方式来进行状态决策。例如,ZooKeeper 集群(ZAB 算法)在选举 leader 时就是采用这种方式。
    • 在这里插入图片描述
    • 民主式决策(比较复杂)和协商式决策比较类似,都是独立的个体之间交换信息,每个个体做出自己的决策,然后按照“多数取胜”的规则来确定最终的状态。
    • 民主式决策会出现脑裂的问题:因为连接中断,造成了两个独立分隔的子集群,每个子集群单独进行选举,选出了 2 个主机(两个主节点会各自做出自己的决策,整个系统的状态就混乱了)。
      • 解决办法是:采用“投票节点数必须超过系统总节点数一半”规则来处理。
        • 这种解决方式降低了系统整体的可用性,即如果系统不是因为脑裂问题导致投票节点数过少,而真的是因为节点故障(例如,节点 1、节点 2、节点 3 真的发生了故障),此时系统也不会选出主节点,整个系统就相当于宕机了,尽管此时还有节点 4 和节点 5 是正常的。

综合分析,无论采取什么样的方案,状态决策都不可能做到任何场景下都没有问题,但完全不做高可用方案又会产生更大的问题,如何选取适合系统的高可用方案,也是一个复杂的分析、判断和选择的过程。

3,可扩展性

可扩展性指系统为了应对将来需求变化而提供的一种扩展能力,当有新的需求出现时,系统不需要或者仅需要少量修改就可以支持,无须整个系统重构或者重建。

在软件开发领域,面向对象思想的提出,就是为了解决可扩展性带来的问题;后来的设计模式,更是将可扩展性做到了极致。

设计具备良好可扩展性的系统,有两个基本条件:正确预测变化、完美封装变化

预测变化的复杂性在于

  • 不能每个设计点都考虑可扩展性。
  • 不能完全不考虑可扩展性。
  • 所有的预测都存在出错的可能性。

对于架构师来说,如何把握预测的程度和提升预测结果的准确性,是一件很复杂的事情,而且没有通用的标准可以简单套上去,更多是靠自己的经验、直觉。

即使预测很准确,如果方案不合适,则系统扩展一样很麻烦

  • 第一种应对变化的常见方案是将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”。
    • 系统需要拆分出变化层和稳定层
    • 需要设计变化层和稳定层之间的接口
  • 第二种常见的应对变化的方案是提炼出一个“抽象层”和一个“实现层”。

4,低成本

当架构方案涉及几百上千甚至上万台服务器,成本就会变成一个非常重要的架构设计考虑点。如果能通过一个架构方案的设计,就能轻松节约几千万元,不但展现了技术的强大力量,也带来了可观的收益。

低成本本质上是与高性能和高可用冲突的:

  • 当我们设计“高性能”“高可用”的架构时,通用的手段都是增加更多服务器来满足“高性能”和“高可用”的要求;
  • 而低成本正好与此相反,我们需要减少服务器的数量才能达成低成本的目标。

低成本给架构设计带来的主要复杂度体现在,往往只有“创新”才能达到低成本目标

新技术的例子:

  • NoSQL(Memcache、Redi 等)的出现是为了解决关系型数据库无法应对高并发访问的压力。
  • 全文搜索引擎(Sphinx、Elasticsearch、Solr)的出现是为了解决关系型数据库 like 搜索的低效的问题。
  • Hadoop 的出现是为了解决传统文件系统无法应对海量数据存储和计算的问题。
  • Linkedin 为了处理每天 5 千亿的事件,开发了高效的 Kafka 消息系统。

无论是引入新技术,还是自己创造新技术,都是一件复杂的事情。

  • 引入新技术的主要复杂度在于需要去熟悉新技术,并且将新技术与已有技术结合起来;
  • 创造新技术的主要复杂度在于需要自己去创造全新的理念和技术,并且新技术跟旧技术相比,需要有质的飞跃。

5,安全

安全可以分为两类:

  • 一类是功能上的安全:功能安全其实也是一个“攻”与“防”的矛盾,只能在这种攻防大战中逐步完善,不可能在系统架构设计的时候一劳永逸地解决。
  • 一类是架构上的安全:传统的架构安全主要依靠防火墙,防火墙最基本的功能就是隔离网络,通过将网络划分成不同的区域,制定出不同区域之间的访问控制策略来控制不同信任程度区域间传送的数据流。

6,规模

规模带来复杂度的主要原因就是“量变引起质变”,当数量超过一定的阈值后,复杂度会发生质的变化

常见的规模带来的复杂度有:

  • 功能越来越多,导致系统复杂度指数级上升
  • 数据越来越多,系统复杂度发生质变

5,架构设计流程

1,识别复杂度

只有正确分析出了系统的复杂性,后续的架构设计方案才不会偏离方向。将主要的复杂度问题列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题

2,设计备选方案

如何设计备选方案:

  • 备选方案的数量以 3 ~ 5 个为最佳
  • 备选方案的差异要比较明显
  • 备选方案的技术不要只局限于已经熟悉的技术

3,评估和选择备选方案

列出我们需要关注的质量属性点,然后分别从这些质量属性的维度去评估每个方案,再综合挑选适合当时情况的最优方案。

4,详细方案设计

详细方案设计就是将方案涉及的关键技术细节给确定下来。

6,常用的高性能架构模式

1,读写分离架构

互联网业务兴起之后,海量用户加上海量数据的特点,单个数据库服务器已经难以满足业务需要,必须考虑数据库集群的方式来提升性能。

高性能数据库集群架构:

  • 读写分离架构:将访问压力分散到集群中的多个节点,但是没有分散存储压力。
  • 分库分表架构:既可以分散访问压力,又可以分散存储压力。

读写分离的基本原理是将数据库读写操作分散到不同的节点上,其基本架构图如下:

在这里插入图片描述

读写分离的基本实现是:

  • 数据库服务器搭建主从集群,一主一从、一主多从都可以。
  • 数据库主机负责读写操作,从机只负责读操作。
  • 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
  • 业务服务器将写操作发给数据库主机,将读操作发给数据库从机。

主从与主备的区别:

  • 主从:从机需要提供读数据的功能
  • 主备:备机一般仅仅提供备份功能,不提供访问功能

读写分离将引入两个问题:

  • 主从复制延迟
    • 复制延迟带来的问题是:如果业务服务器将数据写入到主服务器后立刻进行读取,此时读操作访问的是从机,主机还没有将数据复制过来,到从机读取数据是读不到最新数据的,业务上就可能出现问题。
    • 解决主从复制延迟的常见方法:
      • 写操作后的读操作指定发给主服务器:该方式与业务强绑定,对业务的侵入和影响较大
      • 读从机失败后再读一次主机:也称为“二次读取”,不足之处是,如果有很多二次读取,将大大增加主机的读操作压力
      • 关键业务读写操作全部指向主机,非关键业务采用读写分离
        • 例如,对于一个用户管理系统,注册 + 登录的业务读写操作全部访问主机;
        • 用户的介绍、爱好、等级等业务,可以采用读写分离;
        • 因为即使用户改了自己的自我介绍,在查询时却看到了自我介绍还是旧的,业务影响与不能登录相比就小很多,还可以忍受
  • 分配机制:将读写操作区分开来,然后访问不同的数据库服务器,一般有两种方式:
    • 通过程序代码封装来实现,架构如下:

      • 在这里插入图片描述
      • 目前开源的实现方案中,淘宝的 TDDL(Taobao Distributed Data Layer)是比较有名的,具有主备、读写分离、动态数据库配置等功能,基本架构是:
      • 在这里插入图片描述
    • 通过中间件封装来实现:一套独立的系统,实现了读写操作分离和数据库服务器连接的管理。在业务服务器看来,中间件就是一个数据库服务器。

      • 在这里插入图片描述
      • 由于数据库中间件的复杂度要比程序代码封装高出一个数量级,一般情况下建议采用程序语言封装的方式,或者使用成熟的开源数据库中间件。

目前的开源数据库中间件方案中,MySQL 官方推荐 MySQL Router,它的主要功能有读写分离、故障自动切换、负载均衡、连接池等,其基本架构如下:

在这里插入图片描述

奇虎 360 公司也开源了自己的数据库中间件 Atlas,Atlas 是基于 MySQL Proxy 实现的,基本架构如下:

在这里插入图片描述

2,分库分表架构

单个数据库服务器存储的数据量不能太大(否则会出现存储,性能等很多问题),需要控制在一定的范围内。为了满足业务数据存储的需求,就需要将存储分散到多台数据库服务器上。

可以通过分库或者分表来实现。

2.1,分库

分库指的是按照业务模块将数据分散到不同的数据库服务器

例如,一个简单的电商网站,包括用户、商品、订单三个业务模块,我们可以将用户数据、商品数据、订单数据分开放到三台不同的数据库服务器上,而不是将所有数据都放在一台数据库服务器上。

在这里插入图片描述

分库带来的问题:

  • join 操作问题:分库之后无法使用 join 操作。
  • 事务问题:原本在同一个数据库中不同的表可以在同一个事务中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改。
  • 成本问题:服务器需要的多了,成本当然也就上去了。
2.1,分表

随着业务的发展,同一业务的单表数据达到单台数据库服务器的处理瓶颈,此时就需要对单表数据进行拆分

单表数据拆分有两种方式:垂直分表和水平分表。示意图如下:

在这里插入图片描述

实际架构设计过程中并不局限切分的次数,可以切两次,也可以切很多次。单表切分为多表后,不一定要分散到不同数据库中,可根据实际需求而定。

分表也会引入额外的问题

  • 垂直分表的问题:
    • 垂直分表适合将表中某些不常用且占了大量空间的列拆分出去
    • 问题是,本来一次就可以获取所有数据,垂直分表后需要两次才能获取所有数据
  • 水平分表的问题
    • 水平分表适合表行数特别大的表,一般表数据行数达到千万级别就需要分表了
    • 水平分表带来的问题有:
      • 路由问题,常见的路由算法有:
        • 范围路由:比如将 id 1~999999,1000000 ~ 1999999 来分表(最终导致的结果可能使得每个表中的数据分配不均)
        • Hash 路由:选取某个列(或者某几个列组合也可以)的值进行 Hash 运算,然后根据 Hash 结果分散到不同的数据库表中。Hash 路由的优点是表分布比较均匀,缺点是扩充新的表很麻烦,所有数据都要重分布。
        • 配置路由:配置路由就是路由表,用一张独立的表来记录路由信息。
      • join 操作:需要进行多次 join 查询,然后将结果合并。
      • count 操作:需要对每个表进行 count() 操作,然后将结果相加。
      • order by 操作:需要分别查询每个子表中的数据,然后汇总进行排序。

3,高性能 NoSQL

NoSQL = Not Only SQL,NoSQL 方案带来的优势,本质上是牺牲 ACID 中的某个或者某几个特性,从而在某些方面比关系型数据库更加优秀。

NoSQL 的含义在不同的时期有着不同的含义,下图供参考:

在这里插入图片描述

常见的 NoSQL 方案可分为 4 类:

  • K-V 存储:解决关系数据库无法存储数据结构的问题,以 Redis 为代表。
  • 文档数据库:解决关系数据库强 schema 约束的问题,以 MongoDB 为代表。
  • 列式数据库:解决关系数据库大数据场景下的 I/O 问题,以 HBase 为代表。
  • 全文搜索引擎:解决关系数据库的全文搜索性能问题,以 Elasticsearch 为代表。

4,高性能缓存架构

缓存是为了弥补存储系统在一些复杂业务场景下的不足,其基本原理是将可能重复使用的数据放到内存中,一次生成、多次使用,避免每次使用都去访问存储系统。

5,单服务器高性能模式

高性能是一件很复杂很有挑战的事情,高性能架构设计主要集中在两方面:

  • 尽量提升单服务器的性能,将单服务器的性能发挥到极致。
  • 当单服务器无法支撑性能,设计服务器集群方案。

单服务器高性能的关键是采取的并发模型,这都和操作系统的 I/O 模型及进程模型相关:

  • 进程模型:单进程、多进程、多线程。
  • I/O 模型:阻塞、非阻塞、同步、异步。
1,PPC

PPCProcess Per Connection 的缩写,其含义是指每次有新的连接就新建一个进程去专门处理这个连接的请求。

PPC 模式实现简单,比较适合服务器的连接数没那么多的情况。世界上第一个 web 服务器 CERN httpd 就采用了这种模式。

在这里插入图片描述

主要步骤:

  • 父进程接受连接(图中 accept)
  • 父进程“fork”子进程(图中 fork)
    • 将连接的文件描述符引用计数减一(父进程中的 close)
    • 连接对应的文件描述符引用计数变为 0 后,操作系统才会真正关闭连接
  • 子进程处理连接的读写请求(图中子进程 read、业务处理、write)
  • 子进程关闭连接(图中子进程中的 close)

prefork

prefork 就是提前创建进程。系统在启动的时候就预先创建好进程,然后才开始接受用户的请求,当有新的连接进来的时候,就可以省去 fork 进程的操作,速度更快。

在这里插入图片描述

prefork 中的惊群问题

当有新的连接进入时,多个子进程都去 accept 同一个 socket,但最终只会有一个进程能 accept 成功。

当所有阻塞在 accept 上的子进程都被唤醒时,就导致了不必要的进程调度和上下文切换,会影响系统性能,这就是惊群问题。Linux 2.6 版本后内核已经解决了 accept 惊群问题

2,TPC

TPC 是 Thread Per Connection 的缩写,是指每次有新的连接就新建一个线程去专门处理这个连接的请求

与进程相比,TPC 的优点:

  • 线程更轻量级,创建线程的消耗比进程要少得多;
  • 同时多线程是共享进程内存空间的,线程通信相比进程通信更简单。

TPC 实际上是解决或者弱化了 PPC fork 代价高的问题和父子进程通信复杂的问题

在这里插入图片描述

主要步骤:

  • 父进程接受连接(图中 accept)
  • 父进程创建子线程(图中 pthread)
    • 注意,这里的主进程不用 close 连接
    • 原因是在于子线程是共享主进程的进程空间的,连接的文件描述符并没有被复制,因此只需要一次 close 即可
  • 子线程处理连接的读写请求(图中子线程 read、业务处理、write)
  • 子线程关闭连接(图中子线程中的 close)

TPC 存在的问题:

  • 创建线程虽然比创建进程代价低,但并不是没有代价,高并发时(例如每秒上万连接)还是有性能问题。
  • 无须进程间通信,但是线程间的互斥和共享又引入了复杂度,可能一不小心就导致了死锁问题。
  • TPC 还是存在 CPU 线程调度和切换代价的问题。

prethread

和 prefork 类似,prethread 模式会预先创建线程。prethread 的实现方式相比 prefork 要灵活一些,常见的实现方式有:

  • 主进程 accept,然后将连接交给某个线程处理。
  • 子线程都尝试去 accept,最终只有一个线程 accept 成功,方案的基本示意图如下:

在这里插入图片描述

PPC 和 TPC 模式,它们的优点是实现简单,缺点是都无法支撑高并发的场景。

3,Reactor

I/O 多路复用技术

  • 当多条连接共用一个阻塞对象后,进程只需要在一个阻塞对象上等待,而无须再轮询所有连接,常见的实现方式有 select、epoll、kqueue 等。
  • 当某条连接有新的数据可以处理时,操作系统会通知进程,进程从阻塞状态返回,开始进行业务处理。

Reactor 的中文是“反应堆”,其实就是 I/O 多路复用结合线程池,其完美地解决了 PPC 和 TPC 的问题。

Reactor 模式也叫 Dispatcher 模式,即 I/O 多路复用统一监听事件,收到事件后分配(Dispatch)给某个进程

Reactor 模式的核心组成部分包括 Reactor 和处理资源池(进程池或线程池):

  • Reactor 负责监听和分配事件
  • 处理资源池负责处理事件

Reactor 模式有这三种典型的实现方案:

  • 单 Reactor,单进程 / 线程
  • 单 Reactor,多线程
  • 多 Reactor,多进程 / 线程
1,单 Reactor,单进程 / 线程

单 Reactor 单进程 / 线程的方案示意图(以进程为例):

在这里插入图片描述

步骤说明:

  • Reactor 对象通过 select 监控连接事件,收到事件后通过 dispatch 进行分发。
  • 如果是连接建立的事件,则由 Acceptor 处理,Acceptor 通过 accept 接受连接,并创建一个 Handler 来处理连接后续的各种事件。
  • 如果不是连接建立事件,则 Reactor 会调用连接对应的 Handler(第 2 步中创建的 Handler)来进行响应。
  • Handler 会完成 read-> 业务处理 ->send 的完整业务流程。

单 Reactor 单进程模式的优缺点:

  • 优点是很简单,没有进程间通信,没有进程竞争,全部都在同一个进程内完成。
  • 缺点有:
    • 只有一个进程,无法发挥多核 CPU 的性能
    • Handler 在处理某个连接上的业务时,整个进程无法处理其他连接的事件,容易导致性能瓶颈

单 Reactor 单进程的方案在实践中应用场景不多,只适用于业务处理非常快的场景,比较著名的是 Redis

对于不同的编程语言,需要注意的是:

  • C 语言,一般使用单 Reactor 单进程,因为没有必要在进程中再创建线程
  • Java 语言,一般使用单 Reactor 单线程,因为 Java 虚拟机是一个进程,虚拟机中有很多线程,业务线程只是其中的一个线程
2,单 Reactor,多线程

流程图如下:

在这里插入图片描述

主要步骤:

  • 主线程中,Reactor 对象通过 select 监控连接事件,收到事件后通过 dispatch 进行分发。
  • 如果是连接建立的事件,则由 Acceptor 处理,Acceptor 通过 accept 接受连接,并创建一个 Handler 来处理连接后续的各种事件。
  • 如果不是连接建立事件,则 Reactor 会调用连接对应的 Handler(第 2 步中创建的 Handler)来进行响应。
  • Handler 只负责响应事件,不进行业务处理;Handler 通过 read 读取到数据后,会发给 Processor 进行业务处理。
  • Processor 会在独立的子线程中完成真正的业务处理,然后将响应结果发给主进程的 Handler 处理;Handler 收到响应后通过 send 将响应结果返回给 client。

单 Reator 多线程方案能够充分利用多核多 CPU 的处理能力,但同时也存在下面的问题:

  • 多线程数据共享和访问比较复杂。
    • 例如,子线程完成业务处理后,要把结果传递给主线程的 Reactor 进行发送,这里涉及共享数据的互斥和保护机制。
  • Reactor 承担所有事件的监听和响应,只在主线程中运行,瞬间高并发时会成为性能瓶颈。
3,多 Reactor,多进程 / 线程

为了解决单 Reactor 多线程的问题,最直观的方法就是将单 Reactor 改为多 Reactor

多 Reactor 多进程 / 线程方案示意图是(以进程为例):

在这里插入图片描述

主要步骤:

  • 父进程中 mainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 接收,将新的连接分配给某个子进程。
  • 子进程的 subReactor 将 mainReactor 分配的连接加入连接队列进行监听,并创建一个 Handler 用于处理连接的各种事件。
  • 当有新的事件发生时,subReactor 会调用连接对应的 Handler(即第 2 步中创建的 Handler)来进行响应。
  • Handler 完成 read→业务处理→send 的完整业务流程。

著名的开源系统 Nginx 采用的是多 Reactor 多进程,采用多 Reactor 多线程的实现有 Memcache 和 Netty

Nginx 采用的是多 Reactor 多进程的模式,但方案与标准的多 Reactor 多进程有差异。具体差异表现为主进程中仅仅创建了监听端口,并没有创建 mainReactor 来“accept”连接,而是由子进程的 Reactor 来“accept”连接,通过锁来控制一次只有一个子进程进行“accept”,子进程“accept”新连接后就放到自己的 Reactor 进行处理,不会再分配给其他子进程。

4,Proactor

Reactor 是非阻塞同步网络模型,因为真正的 read 和 send 操作都需要用户进程同步操作。这里的“同步”指用户进程在执行 read 和 send 这类 I/O 操作的时候是同步的,如果把 I/O 操作改为异步就能够进一步提升性能,这就是异步网络模型 Proactor

Reactor 可以理解为“来了事件我通知你,你来处理”,而 Proactor 可以理解为“来了事件我来处理,处理完了我通知你”。

  • “我”就是操作系统内核
  • “事件”就是有新连接、有数据可读、有数据可写的这些 I/O 事件
  • “你”就是我们的程序代码

Proactor 模型示意图:

在这里插入图片描述

主要步骤:

  • Proactor Initiator 负责创建 Proactor 和 Handler,并将 Proactor 和 Handler 都通过 Asynchronous Operation Processor 注册到内核。
  • Asynchronous Operation Processor 负责处理注册请求,并完成 I/O 操作。
  • Asynchronous Operation Processor 完成 I/O 操作后通知 Proactor。
  • Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理。
  • Handler 完成业务处理,Handler 也可以注册新的 Handler 到内核进程。

理论上 Proactor 比 Reactor 效率要高一些,异步 I/O 能够充分利用 DMA 特性,让 I/O 操作与计算重叠,但要实现真正的异步 I/O,操作系统需要做大量的工作。

  • 目前 Windows 下通过 IOCP 实现了真正的异步 I/O
  • 而在 Linux 系统下的 AIO 并不完善,因此在 Linux 下实现高并发网络编程时都是以 Reactor 模式为主。
  • 所以即使 Boost.Asio 号称实现了 Proactor 模型,其实:
    • 它在 Windows 下采用 IOCP,
    • 而在 Linux 下是用 Reactor 模式(采用 epoll)模拟出来的异步模型。

6,高性能负载均衡

高性能集群的复杂性主要体现在需要增加一个任务分配器,以及为任务选择一个合适的任务分配算法

对于任务分配器,现在更流行的通用叫法是“负载均衡器”,这个名称有一定的误导性,会让人认为任务分配的目的是要保持各个计算单元的负载达到均衡状态。

而实际上任务分配并不只是考虑计算单元的负载均衡,不同的任务分配算法目标是不一样的,有的基于负载考虑,有的基于性能(吞吐量、响应时间)考虑,有的基于业务考虑。

常见的负载均衡系统包括 3 种:

  • DNS负载均衡
  • 硬件负载均衡
  • 软件负载均衡
1,DNS 负载均衡

DNS 是最简单也是最常见的负载均衡方式,一般用来实现地理级别的均衡。DNS 负载均衡的本质是:DNS 解析同一个域名可以返回不同的 IP 地址

例如,北方的用户访问北京的机房,南方的用户访问深圳的机房。那么同样是 www.baidu.com

  • 北方用户解析后获取的地址是 61.135.165.224(这是北京机房的 IP)
  • 南方用户解析后获取的地址是 14.215.177.38(这是深圳机房的 IP)

在这里插入图片描述

DNS 负载均衡的优缺点:

  • 优点
    • 简单、成本低:负载均衡工作交给 DNS 服务器处理,无须自己开发或者维护负载均衡设备。
    • 就近访问,提升访问速度:DNS 解析时可以根据请求来源 IP,解析成距离用户最近的服务器地址,可以加快访问速度,改善性能。
  • 缺点
    • 更新不及时:DNS 缓存的时间比较长,修改 DNS 配置后,由于缓存的原因,还是有很多用户会继续访问修改前的 IP,这样的访问会失败,达不到负载均衡的目的,并且也影响用户正常使用业务。
    • 扩展性差DNS 负载均衡的控制权在域名商那里,无法根据业务特点针对其做更多的定制化功能和扩展特性。
    • 分配策略比较简单:DNS 负载均衡支持的算法少;不能区分服务器的差异(不能根据系统与服务的状态来判断负载);也无法感知后端服务器的状态。
2,硬件负载均衡

硬件负载均衡是通过单独的硬件设备来实现负载均衡功能,这类设备和路由器、交换机类似,可以理解为一个用于负载均衡的基础网络设备。目前业界典型的硬件负载均衡设备有两款:F5 和 A10。

硬件负载均衡的优缺点是:

  • 优点:
    • 功能强大:全面支持各层级的负载均衡,支持全面的负载均衡算法,支持全局负载均衡。
    • 性能强大:对比一下,软件负载均衡支持到 10 万级并发已经很厉害了,硬件负载均衡可以支持 100 万以上的并发。
    • 稳定性高:商用硬件负载均衡,经过了良好的严格测试,经过大规模使用,稳定性高。
    • 支持安全防护:硬件均衡设备除具备负载均衡功能外,还具备防火墙、防 DDoS 攻击等安全功能。
  • 缺点:
    • 价格昂贵
    • 扩展能力差:硬件设备,可以根据业务进行配置,但无法进行扩展和定制。
3,软件负载均衡

软件负载均衡通过负载均衡软件来实现负载均衡功能,常见的有 Nginx 和 LVS:

  • Nginx 是软件的 7 层负载均衡,支持 HTTP、E-mail 协议
  • LVS 是 Linux 内核的 4 层负载均衡,和协议无关,几乎所有应用都可以做,例如,聊天、数据库等
  • 4 层和 7 层的区别就在于协议和灵活性

软件和硬件的最主要区别就在于性能,硬件负载均衡性能远远高于软件负载均衡性能:

  • Nginx 的性能是万级,一般的 Linux 服务器上装一个 Nginx 大概能到 5 万 / 秒;
  • LVS 的性能是十万级,据说可达到 80 万 / 秒;
  • F5 性能是百万级,从 200 万 / 秒到 800 万 / 秒都有。

下面是 Nginx 的负载均衡架构示意图:

在这里插入图片描述

软件负载均衡的优点:

  • 优点:
    • 简单:部署、维护都比较简单。
    • 便宜:只要买个 Linux 服务器,装上软件即可。
    • 灵活:4 层和 7 层负载均衡可以根据业务进行选择;也可以根据业务进行比较方便的扩展。
  • 缺点:
    • 性能与功能都没有硬件负载均衡那么强大。
    • 一般不具备防火墙和防 DDoS 攻击等安全功能。

4,组合使用负载均衡

每种方式都有一些优缺点,在实际应用中,我们可以基于它们的优缺点进行组合使用。组合的基本原则为:

  • DNS 负载均衡用于实现地理级别的负载均衡
  • 硬件负载均衡用于实现集群级别的负载均衡
  • 软件负载均衡用于实现机器级别的负载均衡

下面是一个大型的负载均衡应用:

在这里插入图片描述

整个系统的负载均衡分为三层:

  • 地理级别负载均衡:
    • www.xxx.com 部署在北京、广州、上海三个机房
    • 当用户访问时,DNS 会根据用户的地理位置来决定返回哪个机房的 IP
  • 集群级别负载均衡:
    • 广州机房的负载均衡用的是 F5 设备,
    • F5 收到用户请求后,进行集群级别的负载均衡,将用户请求发给 3 个本地集群中的一个
  • 机器级别的负载均衡:
    • 广州集群 2 的负载均衡用的是 Nginx
    • Nginx 收到用户请求后,将用户请求发送给集群里面的某台服务器

一般在大型业务场景下才会这样用,如果业务量没这么大,则没有必要严格照搬这套架构

4,高性能负载均衡算法

常见的负载均衡算法:

  • 轮询:负载均衡系统收到请求后,按照顺序轮流分配到服务器上
    • 轮询是最简单的一个策略,无须关注服务器本身的状态
    • 如果服务器直接宕机了,这时负载均衡系统需要做出相应的处理。例如,将服务器从可分配服务器列表中删除
  • 加权轮询:负载均衡系统根据服务器权重进行任务分配,一般是根据硬件配置进行静态配置
    • 其解决了轮询算法中无法根据服务器的配置差异进行任务分配的问题
  • 负载最低优先:负载均衡系统将任务分配给当前负载最低的服务器。
    • 根据不同的任务类型和业务场景,负载可以用不同的指标来衡量,比如:
      • 连接数、HTTP 请求数、CPU 负载程度、IO 负载程度
    • 负载最低优先算法的缺点是其复杂度比较高,因此,实际上应用的场景并没有轮询(包括加权轮询)那么多。
  • 性能最优类:优先将任务分配给处理速度最快的服务器,通过这种方式达到最快响应客户端的目的。
    • 负载均衡系统需要收集和分析每个服务器每个任务的响应时间
    • 可以使用采样统计的方法来计算响应时间
  • Hash 类:负载均衡系统根据任务中的某些关键信息进行 Hash 运算,将相同 Hash 值的请求分配到同一台服务器上,比如:
    • 源地址 Hash:对 IP 进行 Hash 计算
    • ID Hash