从0开始学架构

1. 基础架构

1.1 基本概念

  • 系统:由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能单独完成的工作的群体。它的意思是“总体”“整体”或“联盟”。
  • 模块与组件:模块是一套一致而互相有紧密关连的软件组织;而组件是自包含的、可编程的、可重用的、与语言无关的软件单元。其实模块和组件都是系统的组成部分,只是从不同的角度拆分系统而已。
  • 框架关注的是规范,架构关注的是结构
  • 架构设计的主要目的:为了解决软件系统复杂度带来的问题。
  • 复杂度来源
    • 高性能:单台计算机内部/多台计算机集群 为了高性能带来的复杂度。
    • 高可用:无中断,高性能增加机器目的在于“扩展”处理性能;高可用增加机器目的在于“冗余”处理单元。
    • 可扩展性:系统为了应对将来需求变化而提供的一种扩展能力,当有新的需求出现时,系统不需要或者仅需要少量修改就可以支持,无须整个系统重构或者重建。具备好的扩展性的两个条件:正确预测变化、完美封装变化。
    • 低成本、安全和规模。

1.2 架构设计三原则

1.2.1 合适原则

  • 核心:合适优于业界领先
  • 需要脚踏实地,否则失败原因:
    • 没那么多人,却想干那么多活
    • 没有那么多积累,却想一步登天
    • 没有那么卓越的业务场景,却幻想灵光一闪成为天才

1.2.2 简单原则

  • 核心:简单优于复杂
  • 复杂性的体现 — 结构的复杂性
    • 定义:组成复杂系统的组件数量更多;同时这些组件之间的关系也更加复杂。
    • 引发的问题:组件越多,就越有可能其中某个组件出现故障;某个组件改动,会影响关联的所有组件。定位一个复杂系统中的问题总是比简单系统更加困难
  • 复杂性的体现 — 逻辑的复杂性
    • 逻辑复杂的组件:单个组件承担了太多的功能
    • 复杂的算法:导致难以理解

1.2.3 演化原则

  • 核心:演化优于一步到位
  • 软件架构设计其实更加类似于大自然“设计”一个生物,通过演化让生物适应环境,逐步变得更加强大
  • 软件架构设计的演化过程
    • 设计出来的架构要满足当时的业务需要;
    • 架构要不断地在实际应用过程中迭代,保留优秀的设计,修复有缺陷的设计,改正错误的设计,去掉无用的设计,使得架构逐渐完善;
    • 当业务发生变化时,架构要扩展、重构,甚至重写;代码也许会重写,但有价值的经验、教训、逻辑、设计等却可以在新架构中延续。

1.3 架构设计流程

1.3.1 识别复杂度

  • 将主要的复杂度问题列出来,然后根据业务、技术、团队等综合情况进行排序,优先解决当前面临的最主要的复杂度问题
  • 例如:多个系统之前解耦需要引入消息队列,可采用“排查法”来分析复杂度,从以下几个方面来考虑:是否需要高性能、是否需要高可用性、是否需要高可扩展性。

1.3.2 设计备选方案

  • 技术组合定义:新技术都是在现有技术的基础上发展起来的,现有技术又来源于先前的技术。将技术进行功能性分组,可以大大简化设计过程,这是技术“模块化”的首要原因。
  • 误区:设计最优秀的方案、只做一个方案、备选方案过于详细。
  • 合理的做法:备选方案的数量3到5个、备选方案的差异要比较明显、备选方案的技术不要只局限于已经熟悉的技术

1.3.3 评估和选择备选方案

  • 列出我们需要关注的质量属性点,然后分别从这些质量属性的维度去评估每个方案,再综合挑选适合当时情况的最优方案
  • 按优先级选择,综合当前的业务发展情况、团队人员规模和技能、业务发展预测等因素,将质量属性按照优先级排序

1.3.4 详细方案设计

  • 详细设计方案阶段可能遇到的一种极端情况就是在详细设计阶段发现备选方案不可行,一般情况下主要的原因是备选方案设计时遗漏了某个关键技术点或者关键的质量属性
  • 关键步骤
    • 架构师不但要进行备选方案设计和选型,还需要对备选方案的关键细节有较深入的理解
    • 通过分步骤、分阶段、分系统等方式,尽量降低方案复杂度
    • 如果方案复杂,可采取设计团队的方式来进行设计,博采众长,防止只有一个架构师可能出现的思维盲点或者经验盲区。
  • 之前说的消息队列的设计,详细方案设计示例:
    • 数据库表的设计:分为日志表和消息表两类表,具体的表名、字段设计;
    • 数据如何复制:MySQL主从复制;
    • 主备服务器如何切换:采用ZooKeeper来做主备决策;
    • 业务服务器如何写入消息:分为生产者和消费者两个角色,消息队列系统提供SDK供各业务系统调用,SDK采取轮询算法发起消息写入请求给主服务器;
    • 业务服务器如何读取消息:SDK从配置中读取所有消息队列系统的服务器信息,轮流向所有服务器发起消息读取请求。消息队列服务器需要记录每个消费者的消费状态,
    • 业务服务器和消息队列服务器之间的通信协议如何设计:为了提升兼容性,传输协议用TCP,数据格式为ProtocolBuffer。

2. 高性能架构模式

2.1 高性能数据库

  • 高性能数据库的两种方式
    • 读写分离:将访问压力分散到集群中的多个节点,但是没有分散存储压力;
    • 分库分表:既可以分散访问压力,又可以分散存储压力。

2.1.1 读写分离

  • 读写分离架构图
    读写分离架构图
  • 读写分离基本实现原理
    • 数据库服务器搭建主从集群,一主一从(一主多从);
    • 数据库主机负责读写操作,从机只负责读操作;
    • 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据;
    • 业务服务器将写操作发给数据库主机,将读操作发给数据库从机。
  • 读写分离的两个细节点主从复制延迟和分配机制。
  • 主从复制延迟:可能达到1秒,如果有大量数据同步,延迟几分钟也是有可能的,解决主从延迟的方法:
    • 写操作后的读操作指定发给数据库主服务器
    • 读从机失败后再读一次主机
    • 关键业务读写操作全部指向主机,非关键业务采用读写分离
  • 分配机制:将读写操作区分开来,然后访问不同的数据库服务器,一般有程序代码封装(如:TDDL)和中间件封装(如:Atlas)两种方式
    • 代码封装的特点:实现简单,可定制化;每种编程语言都要实现一次,无法通用;如果主从发生切换,可能需要所有系统都修改配置并重启。
    • 中间件封装的特点:支持多种编程语;主从切换业务无感知;实现复杂;性能要求高

2.2.2 分库分表

  • 背景:当数据量达到千万甚至上亿条的时候,单台数据库服务器的存储能力会成为系统的瓶颈,主要体现在这几个方面:
    • 数据量太大,读写的性能会下降,即使有索引,索引也会变得很大,性能同样会下降。
    • 数据文件会变得很大,数据库备份和恢复需要耗费很长时间。
    • 数据文件越大,极端情况下丢失数据的风险越高(例如,机房火灾导致数据库主备机都发生故障)。
  • 分库:按照业务模块将数据分散到不同的数据库服务器。分库后带来的问题:无法使用join查询;无法使用数据库的事务,只能使用分布式事务,但性能低;成本变高
  • 分表:分为垂直分表和水平分表
    • 垂直分表:适合将表中某些不常用且占了大量空间的列拆分出去,但表操作的数量要增加。
    • 水平分表:适合表行数特别大的表
  • 水平分表的路由选择
    • 范围路由:选取有序的数据列(一般是userId)作为路由的条件,不同分段分散到不同的数据库表中。优点:可以随着数据的增加平滑地扩充新的表,缺点:分布不均匀
    • Hash路由:选取某个列(或者某几个列组合也可以)的值进行Hash运算,然后根据Hash结果分散到不同的数据库表中。初始表数量的选取较复杂,优点:表分布比较均匀,缺点:增加子表数量非常麻烦,因为所有数据都要重分布。
    • 配置路由:即路由表,用一张独立的表来记录路由信息。优点:设计简单,使用灵活,方便扩充表,缺点:必须多查询一次,会影响整体性能
  • 水平分表后无法进行的SQL操作
    • join操作:在业务代码或者数据库中间件中进行多次join查询,然后将结果合并;
    • count操作:每个表的count()相加,或 新建一张表用来记录数;
    • order by操作:由业务代码或者数据库中间件分别查询每个子表中的数据,然后汇总进行排序。
  • 分库分表的实现方法:“程序代码封装”和“中间件封装”,但实现会更复杂。

2.2 高性能缓存

2.2.1 常见的NoSQL方案

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

2.2.2 功能

  • Redis特点:高性能,但不支持完整的ACID事务;
  • MongoDB特点:no-schema、新增字段简单、历史数据不会出错、容易存储复杂数据,但不支持事务,无法实现join操作;
  • HBase特点:同时读取多个列时效率高,一次性完成对一行中的多个列的写操作,但列式存储高压缩率在更新场景下会成为劣势。一般列式存储应用在离线的大数据分析和统计场景中;
  • Elasticsearch特点:可以存储和检索复杂的数据结构。全文搜索引擎能够基于JSON文档建立全文索引,然后快速进行全文搜索。

2.2.3 缓存架构设计要点

  • 缓存穿透:
    • 原因一:存储数据不存在,解决方法:缓存空值
    • 原因二:缓存数据生成耗费大量时间/资源,解决方法:提前预热
  • 缓存雪崩:当缓存失效(过期)后引起系统性能急剧下降的情况。解决方法:
    • 更新锁:对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新,未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
    • 后台更新:由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存。
  • 缓存热点:访问频率很高的数据所在的缓存服务器的压力会很大,解决方法:复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。注意:不同的缓存副本不要设置统一的过期时间,否则就会出现所有缓存副本同时生成同时失效的情况,从而引发缓存雪崩效应。正确的做法是设定一个过期时间范围,不同的缓存副本的过期时间是指定范围内的随机值。

2.3 高性能单服务器模式

  • 高性能架构设计主要体现在:尽量提升单服务器的性能,将单服务器的性能发挥到极致。如果单服务器无法支撑性能,设计服务器集群方案。
  • 单服务器高性能的关键之一:服务器采取的并发模型,并发模型有两个关键设计点:如何管理连接和如何处理请求。
  • 单服务器高性能模式:PPC与TPC
    • PPC(Process Per Connection):每次有新的连接就新建一个进程去专门处理这个连接的请求,这是传统的UNIX网络服务器所采用的模型。特点:fork代价高,父子进程通信复杂,支持的并发连接数量有限。实现简单,适合连接数少的场景。
    • TPC(Thread Per Connection):每次有新的连接就新建一个线程去专门处理这个连接的请求。特点:轻量级,线程间的互斥和共享容易导致死锁,创建线程也有代码,多线程会出现互相影响的情况,某个线程出现异常时,可能导致整个进程退出(例如内存越界)。
  • 应对高并发场景的单服务器高性能架构模式:Reactor和Proactor
    • Reactor:非阻塞同步网络模型,I/O多路复用统一监听事件,收到事件后分配(Dispatch)给某个进程。Reactor模式包括Reactor和处理资源池(进程池或线程池),其中Reactor负责监听和分配事件,处理资源池负责处理事件。Reactor模式有三种典型的实现方案:单Reactor单进程/线程;单Reactor多线程;多Reactor多进程/线程。例如:Nginx采用的是多Reactor多进程,Netty采用的是多Reactor多线程。
    • Proactor:异步网络模型,可理解为来了事件(新连接/读IO/写IO)我(操作系统内核)来处理,处理完了我通知你(程序代码)。理论上Proactor比Reactor效率要高一些,异步I/O能够充分利用DMA特性,让I/O操作与计算重叠,但要实现真正的异步I/O,操作系统需要做大量的工作。目前Windows下通过IOCP实现了真正的异步I/O,而在Linux系统下的AIO并不完善。以Reactor模式为主,采用epoll模拟异步模型。
      Proactor网络模型

2.4 高性能负载均衡

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

2.4.1 负载均衡分类

  • 常见的负载均衡系统包括3种:DNS负载均衡、硬件负载均衡和软件负载均衡。
    • DNS负载均衡,优点:简单、成本低,就近访问,提升访问速度。缺点:更新不及时(DNS缓存时间长),扩展性差,分配策略简单。
    • 硬件负载均衡:通过单独的硬件设备(类似与路由器、交换机)来实现负载均衡功能,常见的硬件负载均衡设备是F5和A10。优点:功能强大、性能高、稳定性高、支持安全防护。缺点:价格昂贵、扩展能力差。
    • 软件负载均衡:常见的有Nginx(7层)和LVS(4层),硬件的性能远高于软件,Nginx万级,LVS十万级,F5性能是百万级。优点:简单、便宜、灵活。缺点:性能一般、功能没有硬件负载均衡那么强大、一般不具备防火墙和防DDoS攻击等安全功能。

2.4.2 负载均衡算法

  • 轮询:负载均衡系统收到请求后,按照顺序轮流分配到服务器上。
  • 加权轮询:根据服务器权重进行任务分配,这里的权重一般是根据硬件配置进行静态配置的,为了解决不同服务器处理能力有差异的问题。
  • 负载最低优先:将任务分配给当前负载最低的服务器,这里的负载根据不同的任务类型和业务场景,可以用不同的指标来衡量。
  • 性能最优类:站在客户端的角度来进行分配的,优先将任务分配给处理速度最快的服务器,通过这种方式达到最快响应客户端的目的。
  • Hash类:根据任务中的某些关键信息进行Hash运算,将相同Hash值的请求分配到同一台服务器上,这样做的目的主要是为了满足特定的业务需求。

3.高可用架构模式

3.1 CAP

3.1.1 CAP理论

  • CAP定义:在一个分布式系统(指互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。
    • C:对某个指定的客户端来说,读操作保证能够返回最新的写操作结果。在事务执行过程中,系统其实处于一个不一致的状态,不同的节点的数据并不完全一致
    • A:非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。
    • P:当出现网络分区后,系统能够继续“履行职责”。
  • CAP应用:必须选择P(分区容忍)要素,因为网络本身无法做到100%可靠,有可能出故障,所以分区是一个必然的现象。所以一般根据业务进行取舍采用AP或CP

3.1.2 CAP细节

  • CAP关注的粒度是数据,而不是整个系统。
  • CAP是忽略网络延迟的。
  • ACID:数据库管理系统为了保证事务的正确性而提出来的理论。分别是Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)、Durability(持久性)
  • BASE:基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency),核心思想是即使无法做到强一致性(CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性。
  • CAP中AP方案的一个补充,因为CAP理论是忽略延时的,而实际应用中延时是无法避免的。AP方案中牺牲一致性只是指分区期间,而不是永远放弃一致性。

3.1.3 FMEA

  • FMEA(Failure mode and effects analysis,故障模式与影响分析):通过对系统范围内潜在的故障模式加以分析,并按照严重程度进行分类,以确定失效对于系统的最终影响。
  • FMEA的具体分析方法是:
    • 给出初始的架构设计图。
    • 假设架构中某个部件发生故障。
    • 分析此故障对系统功能造成的影响。
    • 根据分析结果,判断架构是否需要进行优化。
  • 常见的FMEA分析表格包含功能点、故障模式、故障影响、严重程度、故障原因、故障概率、风险程度、已有措施、规避措施、解决措施、后续规划。一个示例如下
    FMEA分析表

3.2 高可用存储架构

3.2.1 高可用存储架构分类

  • 存储高可用方案的本质都是通过将数据复制到多个存储设备,通过数据冗余的方式来实现高可用,其复杂性主要体现在如何应对复制延迟和中断导致的数据不一致问题。
  • 高可用存储架构有主备、主从、主主、集群、分区
    • 主备,常见的主备切换架构有三种形式:互连式、中介式和模拟式。优点:简单,客户端无感知,缺点:备机没有读写操作,浪费硬件成本,故障后需要人工干预,无法自动恢复。
    • 主从,优点:主机故障时,读操作相关的业务可以继续运行,相比主备节约了硬件成本。缺点:复杂度高,出现主从延迟会导致数据不一致,故障需要人工干预。
  • 双机切换,为了解决主机故障不能写和无法自动恢复这两个问题,包括主备切换和主从切换两种方案。即系统自动决定主机角色,并完成角色切换。
    • 双机切换方案的关键点:主备间状态判断(状态传递的渠道和状态检测的内容),切换决策(切换时机、切换策略、自动程度)
    • 常见的主备切换架构有三种形式:互连式、中介式和模拟式。
  • 集群:多台机器(至少3台)组合在一起形成一个统一的系统,分为数据集中集群和数据分散集群。
    • 数据集中集群:一般为1主多备或1主多从,数据都只能往主机中写
    • 数据分散集群:多个服务器组成一个集群,每台服务器都会负责存储一部分数据;为了提升硬件利用率,每台服务器又会备份一部分数据。复杂点在于如何将数据分配到不同的服务器上,需要考虑均衡性、容错性、伸缩性。每台服务器都可以处理读写请求。
    • 适用场景:数据集中集群适合数据量不大,集群机器数量不多的场景,例如:Zookeeper;数据分散集群,可伸缩性强,适合业务数据量巨大、集群机器数量庞大的业务场景。例如:Hadoop。
  • 数据分区:将数据按照一定的规则进行分区,不同分区分布在不同的地理位置上,每个分区存储一部分数据,通过这种方式来规避地理级别的故障所造成的巨大影响。
  • 数据分区架构的设计点:数据量、分区规则、复制规则(集中式、互备式和独立式)

3.2.2 计算高可用

  • 计算高可用:通过增加更多服务器来达到计算高可用。
    • 复杂度:任务管理,即当任务在某台服务器上执行失败后,如何将任务重新分配到新的服务器进行执行。
    • 设计点:哪些服务器可以执行任务,任务如何重新执行
  • 常见的计算高可用架构:主备、主从和集群。主备和主从故障时都需要人工切换,而集群可以自动切换。
  • 高可用计算的集群方案分为两类:
    • 对称集群(负载均衡集群),即集群中每个服务器的角色都是一样的,都可以执行所有任务
    • 非对称集群,集群中的服务器分为多个不同的角色,不同的角色执行不同的任务,例如最常见的Master-Slave角色。

3.3 异地多活

3.3.1 异地多活架构

  • 异地多活保证在灾难的情况下业务都不受影响,但采用异地多活后复杂度变高,成本上升。
  • 异地多活架构可以分为同城异区、跨城异地、跨国异地
    • 同城异区:将业务部署在同一个城市不同区的多个机房。
    • 跨城异地:业务部署在不同城市的多个机房,而且距离最好要远一些。例如:北京与广州
    • 跨国异地:业务部署在不同国家的多个机房。适用场景:为不同地区用户提供服务、只读类业务做多活。
  • 异地多活的设计理念:采用多种手段,保证绝大部分用户的核心业务异地多活。4大设计技巧:保证核心业务的异地多活、 保证核心数据最终一致性、采用多种手段同步数据、只保证绝大部分用户的异地多活

3.3.2 异地多活步骤

  • 异地多活的4个步骤

  • 业务分级:按照一定的标准将业务进行分级,挑选出核心的业务,只为核心业务设计异地多活,降低方案整体复杂度和实现成本。常见的分级标准:访问量大的业务,核心业务,有大量收入的业务。

  • 数据分类:数据量,唯一性,实时性,可丢失性,可恢复性。例如:用户管理系统的登录业务
    数据分类示例

  • 数据同步:存储系统同步,消息队列同步,重复生成(例如:session数据)

  • 异常处理:常见的异常有同步延迟、数据丢失、数据不一致等,具体的处理措施

    • 多通道同步:采取多种方式来进行数据同步,其中某条通道故障的情况下,系统可以通过其他方式来进行同步。
    • 同步和访问结合:异地机房通过系统的接口来进行数据访问。优先读取本地数据,本地数据无法读取到再通过接口去访问,这样降低了跨机房的异地接口访问数量,适合于实时性要求非常高的数据。
    • 日志记录:用于用户故障恢复后对数据进行恢复,每个关键操作前后都记录相关一条日志,然后将日志保存在一个独立的地方(服务器/独立系统/异地),当故障恢复后,拿出日志跟数据进行对比,对数据进行修复。
    • 用户补偿:一般通过送用户代金券、礼包、礼品、红包等来进行补偿。

3.3.3 应对接口级的故障

  • 接口故障的常见原因
    • 内部原因:代码bug导致死循环,某个接口导致数据库慢查询,OOM。频繁FullGC,线程死锁,线程耗尽等。
    • 外部原因: 黑客攻击、促销或者抢购引入了超出平时几倍甚至几十倍的用户,第三方系统大量请求,第三方系统响应慢等。
  • 解决接口级故障的核心思想和异地多活基本类似:优先保证核心业务和优先保证绝大部分用户。
  • 降级:系统将某些业务或者接口的功能降低,可以是只提供部分功能,也可以是完全停掉所有功能。降级分为系统后门降级和独立降级系统
  • 熔断:熔断的目的是应对依赖的外部系统故障的情况,而降级的目的是应对系统自身的故障,熔断设计关键点:需要有一个统一的API调用层,由API调用层来进行采样或者统计。阈值的设计。
  • 限流:只允许系统能够承受的访问量进来,超出系统访问能力的请求将被丢弃。降级是从系统功能优先级的角度,而限流则是从用户访问压力的角度来考虑如何应对故障。常见的限流方式可以分为两类:基于请求限流和基于资源限流。
    • 基于请求限流:限制总量(限制某个指标的累积上限)、限制时间量(限制一段时间内某个指标的上限)。问题:难以找到合适的阈值。
    • 基于资源限流:找到系统内部影响性能的关键资源,对其使用上限进行限制。常见的内部资源有:CPU利用率、内存使用率、load、可用线程数、连接数、文件句柄、线程数、请求队列等。
  • 排队:让用户等待一段时间,而限流是直接拒绝用户。由于排队需要临时缓存大量的业务请求,单个系统内部无法缓存这么多数据,一般情况下,排队需要用独立的系统去实现,例如 kafka

4. 可扩展架构模式

4.1 基本思想和模式

  • 可扩展的基本思想:拆,将原本大一统的系统拆分成多个规模小的部分,扩展时只修改其中一部分即可,无须整个系统到处都改,通过这种方式来减少改动范围,降低改动风险。
  • 常见的拆分思路有如下三种。
    • 面向流程拆分:将整个业务流程拆分为几个阶段,每个阶段作为一部分。优点:扩展时大部分情况只需要修改某一层,少部分情况可能修改关联的两层,不会出现所有层都同时要修改。
    • 面向服务拆分:将系统提供的服务拆分,每个服务作为一部分。优点:扩展时只需要扩展相关服务即可,无须修改所有的服务。
    • 面向功能拆分:将系统提供的功能拆分,每个功能作为一部分。优点:只需要扩展相关功能即可,无须修改所有的服务。
  • 常见的可扩展系统架构有:
    • 面向流程拆分:分层架构。
    • 面向服务拆分:SOA、微服务。
    • 面向功能拆分:微内核架构。

4.2 可扩展架构

4.2.1 分层架构

  • 分层架构:也叫N层架构,通常情况下,N至少是2层。例如,C/S架构、B/S架构。常见的是3层架构(例如,MVC、MVP架构)、4层架构,5层架构的比较少见,一般是比较复杂的系统才会达到或者超过5层,比如操作系统内核架构。
  • C/S架构或B/S架构:划分的对象是整个业务系统,划分的维度是用户交互,即将和用户交互的部分独立为一层,支撑用户交互的后台作为另外一层
  • MVC架构或MVP架构:划分的对象是单个业务子系统,划分的维度是职责,将不同的职责划分到独立层,但各层的依赖关系比较灵活。
  • 逻辑分层架构:划分的对象可以是单个业务子系统,也可以是整个业务系统,划分的维度也是职责。逻辑分层架构中的层是自顶向下依赖的
  • 分层架构设计的核心:需要保证各层之间的差异足够清晰,边界足够明显,让人看到架构图后就能看懂整个架构。
  • 分层架构的本质:隔离关注点(separation of concerns),即每个层中的组件只会处理本层的逻辑。
  • 分层结构的特点:层层传递,即一旦分层确定,整个业务流程是按照层进行依次传递的,不能在层之间进行跳跃。
  • 分层架构的缺点:性能,因为每一次业务请求都需要穿越所有的架构分层,有一些事情是多余的,多少都会有一些性能的浪费。

4.2.2 SOA

  • SOA(Service Oriented Architecture)面向服务的架构,关键概念:服务、ESB、松耦合。
    • 服务:所有业务功能都是一项服务,服务就意味着要对外提供开放的能力,当其他系统需要使用这项功能时,无须定制化开发。
    • ESB(Enterprise Service Bus) 企业服务总线,屏蔽异构系统对外提供各种不同的接口方式,以此来达到服务间高效的互联互通。
    • 松耦合:为了减少各个服务间的依赖和互相影响。
  • SOA解决了传统IT系统重复建设和扩展效率低的问题,但其本身也引入了更多的复杂性。尤其是ESB,ESB需要实现与各种系统间的协议转换、数据转换、透明的动态路由等功能。协议转换是需要耗费大量计算性能的,当ESB承载的消息太多时,ESB本身会成为整个系统的性能瓶颈。

4.2.3 微服务

  • 微服务是一种和SOA相似但本质上不同的架构理念,small、lightweight、automated 三个词基本上浓缩了微服务的精华,也是微服务与SOA的本质区别所在。
  • 微服务的缺点:
    • 微服务拆分过细,服务间关系复杂,过分强调“small”。
    • 微服务基础设施不健全,调用链太长,问题定位困难,忽略了“automated”。
    • 没有自动化支撑,无法快速交付。微服务并不轻量级,规模大了后,“lightweight”不再适应。
  • 服务粒度:基于团队规模进行拆分,一个微服务三个人负责开发,即“三个火枪手”的原则。
  • 拆分方法:
    • 基于业务逻辑拆分:将系统中的业务模块按照职责范围识别出来,每个单独的业务模块拆分为一个独立的服务。
    • 基于可扩展拆分:将系统中的业务模块按照稳定性排序,将已经成熟和改动不大的服务拆分为稳定服务,将经常变化和迭代的服务拆分为变动服务。为了提升项目快速迭代的效率,减少影响范围。
    • 基于可靠性拆分:将系统中的业务模块按照优先级排序,将可靠性要求高的核心服务和可靠性要求低的非核心服务拆分开来,然后重点保证核心服务的高可用。优点:避免非核心服务故障影响核心服务,核心服务高可用方案可以更简单,能够降低高可用成本。
    • 基于性能拆分:将性能要求高或者性能压力大的模块拆分出来,避免性能压力大的服务影响其他服务。
  • 基础设施:
    • 服务发现、服务路由、服务容错:这是最基本的微服务基础设施。
    • 接口框架、API网关:主要是为了提升开发效率,接口框架是提升内部服务的开发效率,API网关是为了提升与外部服务对接的效率。
    • 自动化部署、自动化测试、配置中心,主要是为了提升测试和运维效率。
    • 服务监控、服务跟踪、服务安全,主要是为了进一步提升运维效率。

4.2.4 微内核架构

  • 微内核架构(Microkernel Architecture),也被称为插件化架构(Plug-in Architecture),是一种面向功能进行拆分的可扩展性架构,通常用于实现基于产品(product-based指存在多个版本需要下载安装才能使用)的应用。
  • 基本架构:包含两类组件:核心系统(core system)和插件模块(plug-in modules)。核心系统负责和具体业务功能无关的通用功能,插件模块负责实现具体的业务逻辑。
  • 设计关键点:插件管理、插件连接和插件通信。
    • 插件管理:核心系统需要知道当前有哪些插件可用,如何加载这些插件,什么时候加载插件。常见的实现方法是插件注册表机制。
    • 插件连接:核心系统必须制定插件和核心系统的连接规范,然后插件按照规范实现,核心系统按照规范加载即可。
    • 插件通信:指插件间的通信。虽然设计的时候插件间是完全解耦的,但实际业务运行过程中,必然会出现某个业务流程需要多个插件协作,这就要求两个插件间进行通信。
  • OSGi架构:Open Services Gateway initiative,分为Module、Lifecycle、Service三层。
  • 规则引擎架构:微内核架构的一种实现,执行引擎可以看作是微内核,执行引擎解析配置好的业务流,执行其中的条件和规则,通过这种方式来支持业务的灵活多变。
  • 规则引擎架构优点:可扩展、易理解、高效率。
  • 规则引擎基本架构:
    • 开发人员将业务功能分解提炼为多个规则,将规则保存在规则库中。
    • 业务人员根据业务需要,通过将规则排列组合,配置成业务流程,保存在业务库中。
    • 规则引擎执行业务流程实现业务功能。
  • 规则引擎常用框架:开源的Drools,阿里集团内部的规则运营平台

5. 架构实战

5.1 技术演进模式

  • 影响一个企业业务的发展主要有3个因素:市场、技术、管理,这三者构成支撑业务发展的铁三角,任何一个因素的不足,都可能导致企业的业务停滞不前。而业务处于三角形的中心。
  • 企业的业务可以分为产品和服务两类
    • 产品类:技术创新推动业务发展,例如:360、iPhone、UC浏览器等,
    • 服务类:业务发展推动技术的发,例如:百度、淘宝、微博、QQ、微信等
  • 技术发展主要的驱动力是业务发展,架构师要基于业务发展阶段进行判断。
  • 互联网业务发展一般分为几个时期:初创期、发展期、竞争期、成熟期。不同时期的差别主要体现在两个方面:复杂性、用户规模。
  • 业务复杂性:
    • 初创期:快速迭代,能买就买,有开源就用开源。
    • 发展期:堆功能期 → 优化期 → 架构期(拆)
    • 竞争期:系统数量的量变带来了技术工作的质变,体现在重复造轮子,系统交互一团乱麻。主要解决手段:平台化、服务化。
    • 成熟期:找出自己的弱项,然后逐项优化。
  • 用户规模:用户量增大对技术的影响主要体现在两个方面:性能要求高、可用性要求高。
  • 量变到质变参考模型
    业务发展阶段图

5.2 互联网架构模板

5.2.1 存储层

  • SQL:一般情况下互联网行业都是用MySQL、PostgreSQL这类开源数据库。这类数据库的特点是开源免费,拿来就用;
  • NoSQL:弥补了关系数据库的不足,一般使用Redis、MongoDB等;
  • 小文件存储:互联网行业有很多用于展示的数据,这些数据具有数据小、数量巨大、访问量巨大三个典型特征,可以在开源方案的基础上封装一个小文件存储平台,也可以使用开源框架,例如:FastDFS、Hypertable、HBase等;
  • 大文件存储:分为业务上的大数据和海量的日志数据,可使用开源框架,例如Hadoop、Storm、Hive等

5.2.2 开发层

  • 开发框架:互联网公司一般会指定一个大的技术方向,然后使用统一的开发框架。例如,Java相关的开发框架SpringMVC、SpringBoot,Python的Django等。使用统一的开发框架能够解决上面提到的各种问题,提升组织和团队的开发效率。
  • 选择框架原则:优选成熟的框架,避免盲目追逐新技术。
  • Web服务器:选择一个服务器主要和开发语言相关,例如,Java的有Tomcat、JBoss、Resin等,PHP/Python的用Nginx
  • 容器:Docker的容器技术,虽然没有跨平台,但启动快,几乎不占资源。

5.2.3 服务层

  • 服务层的主要目标就是为了降低系统间相互关联的复杂度。
  • 配置中心:集中管理各个系统的配置;
  • 服务中心:为了解决跨系统依赖的“配置”和“调度”问题,一般有服务名字系统和服务总线系统两种方式;
  • 消息队列:实现跨系统异步通知的中间件系统。

5.2.4 网络层

  • 负载均衡:将请求均衡地分配到多个系统上。常见的负载均衡策略:
    • DNS:实现地理级别的均衡,优点:通用(全球通用)、成本低(申请域名,注册DNS即可),缺点:缓存时间长,不灵活
    • Nginx 、LVS 、F5:同一地点内机器级别的负载均衡,其中Nginx是软件的7层负载均衡,LVS是内核的4层负载均衡,F5是硬件的4层负载均衡。F5成本高,而4层和7层的区别就在于协议和灵活性。
  • CDN:一种“以空间换时间”的加速策略,即将内容缓存在离用户最近的地方,用户访问的是缓存的内容,而不是站点实时的内容。
  • 多机房:核心的因素就是如何处理时延带来的影响,常见策略:同城多机房、跨城多机房、跨国多机房。
  • 多中心:多中心必须以多机房为前提,多中心更复杂。多中心设计的关键就在于“数据一致性”和“数据事务性”如何保证

5.2.5 用户层

  • 用户管理:通过互联网将众多分散的用户连接起来。
  • 用户管理目标:
    • 单点登录(SSO),开源单点登录方案CAS
    • 授权登录:最流行的授权登录是OAuth 2.0协议
  • 消息推送:分为短信、邮件、站内信、App推送。
  • 存储云/图片云:基于“CDN + 小文件存储”

5.2.6 业务层

  • 业务层面对的主要技术挑战是“复杂度”
  • 解决方案:拆,化整为零、分而治之,将整体复杂性分散到多个子业务或者子系统中。

5.2.7 平台技术

  • 随着业务规模越来越大,系统复杂度越来越高,子系统数量越来越多,将这些支撑功能做成平台,避免重复造轮子,减少不规范带来的沟通和协作成本。
  • 运维平台:核心的职责分为配置、部署、监控、应急四大块。核心设计要素是“四化”:标准化、平台化、自动化、可视化。
  • 测试平台:包括单元测试、集成测试、接口测试、性能测试等。测试平台的核心目的是提升测试效率,从而提升产品质量,其设计关键就是自动化。
  • 测试平台的基本架构:用例管理、资源管理、任务管理、数据管理。
  • 数据平台:核心职责主要包括数据管理、数据分析和数据应用三部分。
    • 数据管理:包含数据采集、数据存储、数据访问和数据安全四个核心职责,是数据平台的基础功能。
    • 数据分析:包括数据统计、数据挖掘、机器学习、深度学习等几个细分领域。
    • 数据应用:既包括在线业务,也包括离线业务,只有当数据的规模达到一定程度,基于数据的分析、挖掘才能发现有价值的规律、现象、问题等。
  • 管理平台:核心职责是权限管理,分为身份认证和权限控制两部分

5.3 架构重构内功修炼

5.3.1 有的放矢

  • 架构师的首要任务是从一大堆纷繁复杂的问题中识别出真正要通过架构重构来解决的问题,集中力量快速解决,而不是想着通过架构重构来解决所有的问题。
  • 架构师需要透过问题表象看到问题本质,找出真正需要通过架构重构解决的核心问题,从而做到有的放矢,既不会耗费大量的人力和时间投入,又能够解决核心问题。
  • 优化还是重构的判断方法:假设我们现在需要从0开始设计当前系统,新架构和老架构是否类似?如果差异不大,说明采取系统优化即可;如果差异很大,那可能就要进行系统重构了。

5.3.2 合纵连横

  • 合纵:架构重构是大动作,持续时间比较长,而且会占用一定的研发资源,包括开发和测试,因此不可避免地会影响业务功能的开发。在沟通协调时,将技术语言转换为通俗语言,以事实说话,以数据说话,是沟通的关键!
  • 连横:除了和上下游沟通协调,有的重构还需要和其他相关或者配合的系统的沟通协调。站在对方的角度思考,重构对他有什么好处,能够帮他解决什么问题,带来什么收益。

5.3.3 运筹帷幄

  • 分阶段实施:将要解决的问题根据优先级、重要性、实施难度等划分为不同的阶段,每个阶段聚焦于一个整体的目标,集中精力和资源解决一类问题
  • 分阶段实施的优点:
    • 每个阶段都有明确目标,做完之后效果明显,团队信心足,后续推进更加容易。
    • 每个阶段的工作量不会太大,可以和业务并行。
    • 每个阶段的改动不会太大,降低了总体风险。
  • 制定“分阶段实施”的策略
    • 优先级排序:将明显且又比较紧急的事项优先落地,解决目前遇到的主要问题。
    • 问题分类:将问题按照性质分类,每个阶段集中解决一类问题。
    • 先易后难:对最难的事项判断可能会出错,较快地看到成果,有效地保证整个重构的效果
    • 循序渐进:按照固定的步骤和节奏,更有利于项目推进,一般每个阶段最少1个月,最长不要超过3个月。

5.4 开源项目如何选择

软件开发领域有一个流行的原则:DRY,Don’t repeat yourself。即不要重复造轮子。架构师需要更加聪明地选择和使用开源项目。即不要重复发明轮子,但要找到合适的轮子。

5.4.1 如何选择一个开源项目

  • 聚焦是否满足业务,而不需要过于关注开源项目是否优秀。
  • 聚焦是否成熟,不成熟的开源项目应用到生产环境,风险极大,所以尽量选择成熟的开源项目,降低风险。
    • 判断开源项目是否成熟的几点:版本号、使用的公司数量、社区活跃度。
  • 聚焦运维能力:看开源项目日志是否齐全、是否有命令行、管理控制台等维护工具,是否有故障检测和恢复的能力。

5.4.2 如何使用开源项目

  • 深入研究,仔细测试:
    • 通读开源项目的设计文档或者白皮书,了解其设计原理。
    • 核对每个配置项的作用和影响,识别出关键配置项。
    • 进行多种场景的性能测试。进行压力测试,连续跑几天,观察CPU、内存、磁盘I/O等指标波动。
    • 进行故障测试:kill、断电、拔网线、重启100次以上、切换等。
  • 小心应用,灰度发布:测试不能100%覆盖,时刻对线上环境和风险要有敬畏之心。
  • 做好应急,以防万一

5.4.3 如何基于开源项目做二次开发

  • 保持纯洁,加以包装
  • 发明你要的轮子:主要问题是没有完全适合你的轮子,所以去发明完美符合自己业务特点的轮子

5.5 架构设计模板

5.5.1 需求介绍

  • 需求介绍主要描述需求的背景、目标、范围等
  • 随着前浪微博业务的不断发展,业务上拆分的子系统越来越多,目前系统间的调用都是同步调用,由此带来几个明显的系统问题:
    • 性能问题:当用户发布了一条微博后,微博发布子系统需要同步调用“统计子系统”“审核子系统”“奖励子系统”等共8个子系统,性能很低。
    • 耦合问题:当新增一个子系统时,例如如果要增加“广告子系统”,那么广告子系统需要开发新的接口给微博发布子系统调用。
    • 效率问题:每个子系统提供的接口参数和实现都有一些细微的差别,导致每次都需要重新设计接口和联调接口,开发团队和测试团队花费了许多重复工作量。
  • 基于以上背景,我们需要引入消息队列进行系统解耦,将目前的同步调用改为异步通知。

5.5.2 需求分析

  • 需求分析主要全方位地描述需求相关的信息
  • 5W(Who、When、What、Why、Where)
    • Who:需求利益干系人,包括开发者、使用者、购买者、决策者等。
    • When:需求使用时间,包括季节、时间、里程碑等。
    • What:需求的产出是什么,包括系统、数据、文件、开发库、平台等。
    • Where:需求的应用场景,包括国家、地点、环境等,例如测试平台只会在测试环境使用。
    • Why:需求需要解决的问题,通常和需求背景相关
  • 1H:关键业务流程
  • 8C:即Constraints,指8个约束和限制,包括性能Performance、成本Cost、时间Time、可靠性Reliability、安全性Security、合规性Compliance、技术性Technology、兼容性Compatibility
  • 消息队列的5W分析如下:
    • Who:消息队列系统主要是业务子系统来使用,子系统发送消息或者接收消息。
    • When:当子系统需要发送异步通知的时候,需要使用消息队列系统。
    • What:需要开发消息队列系统。
    • Where:开发环境、测试环境、生产环境都需要部署。
    • Why:消息队列系统将子系统解耦,将同步调用改为异步通知。
  • 消息队列的1H,两大核心功能:
    • 业务子系统发送消息给消息队列。
    • 业务子系统从消息队列获取消息。
  • 消息队列的8C
    • 性能:需要达到Kafka的性能水平。
    • 成本:参考XX公司的设计方案,不超过10台服务器。
    • 时间:期望3个月内上线第一个版本,在两个业务尝试使用。
    • 可靠性:按照业务的要求,消息队列系统的可靠性需要达到99.99%。
    • 安全性:消息队列系统仅在生产环境内网使用,无需考虑网络安全;如消息中有敏感信息,消息发送方需要自行进行加密,消息队列系统本身不考虑通用的加密。
    • 合规性:消息队列系统需要按照公司目前的DevOps规范进行开发。
    • 技术性:目前团队主要研发人员是Java,最好用Java开发。
    • 兼容性:之前没有类似系统,无需考虑兼容性。

5.5.3 复杂度分析

  • 复杂度常见的有高可用、高性能、可扩展等
  • 高可用:消息队列需要高可用性,包括消息写入、消息存储、消息读取都需要保证高可用性。
  • 高性能:前浪微博系统用户每天发送1000万条微博,平均一条消息有10个子系统读取,那么其他子系统读取的消息大约是1亿次。将数据按照秒来计算,一天内平均每秒写入消息数为115条,每秒读取的消息数是1150条;再考虑系统的读写并不是完全平均的,设计的目标应该以峰值来计算。峰值一般取平均值的3倍,那么消息队列系统的TPS是345,QPS是3450,考虑一定的性能余量。将设计目标设定为峰值的4倍,因此最终的性能要求是:TPS为1380,QPS为13800。因此高性能读取是复杂度之一。
  • 可扩展:消息队列的功能很明确,基本无须扩展,因此可扩展性不是这个消息队列的关键复杂度。

5.5.4 备选方案

  • 至少3个备选方案,每个备选方案需要描述关键的实现,无须描述具体的实现细节。
  • 消息队列的备选方案有直接引入开源Kafka、集群 + MySQL存储、集群 + 自研存储
  • 备选方案评估:备选方案360度环评。注意备选方案评估的内容会根据评估会议的结果进行修改,也就是说架构师首先给出自己的备选方案评估,然后举行备选方案评估会议,再根据会议结论修改备选方案文档

5.5.5 架构设计模板

  • 备选方案评估后会选择一个方案落地实施,架构设计文档就是用来详细描述细化方案的
  • 总体方案:需要从整体上描述方案的结构,其核心内容就是架构图,以及针对架构图的描述,包括模块或者子系统的职责描述、核心流程。
  • 架构总览:给出架构图以及架构的描述,架构关键设计点。
  • 核心流程:消息发送读取。
  • 详细设计:需要描述具体的实现细节
    • 高可用设计:消息发送可靠性、消息存储可靠性、消息读取可靠性。
    • 高性能设计:集群
    • 安全设计:消息队列系统需要提供权限控制功能,包括身份识别和队列权限控制两部分。
    • 其他设计:包括上述以外的其他设计考虑点,例如指定开发语言、符合公司的某些标准等
    • 部署方案:包括硬件要求、服务器部署方式、组网方式等
  • 架构演进规划:规划和设计的需求比较完善,但如果一次性全部做完,项目周期可能会很长,因此可以采取分阶段实施。整个消息队列系统分三期实现:
    • 第一期:实现消息发送、权限控制功能,预计时间3个月。
    • 第二期:实现消息读取功能,预计时间1个月。
    • 第三期:实现主备基于ZooKeeper切换的功能,预计时间2周。

参考:《从0开始学架构》- 李运华

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>