【读书笔记】《从零开始学架构》
📔【读书笔记】《从零开始学架构》
2023-9-7
| 2024-3-24
0  |  0 分钟
type
status
date
slug
summary
tags
category
icon
password
Sub-item
Last edited time
Mar 24, 2024 03:12 AM
Parent item
领域
💡
架构设计的主要目的是为了解决软件系统复杂度带来的问题
架构即(重要)决策,是在一个有约束的盒子里去求解或接近最合适的解。这个有约束的盒子是团队经验、成本、资源、进度、业务所处阶段等所编织、掺杂在一起的综合体(人,财,物,时间,事情等)。架构无优劣,但是存在恰当的架构用在合适的软件系统中,而这些就是决策的结果。

1.系统复杂度的来源

高性能

  • 单机高性能
    • 背景:提高任务执行效率
      目标:充分利用CPU资源
      方案:
      • 分时并发系统:多进程、多线程、多协程
      • 多核处理器并行:SMP对称多处理
      技术方案:进程间通信、多线程并发、支持协程等技术方案
  • 集群高性能
    • 背景:单机无法满足业务处理
      目标:提升系统整体的处理业务的能力
      方案:集群通过增加机器的方式,但同时引入新的问题需要解决。
      任务分配
      • 额外增加一台任务分配器,实现业务服务器之间负载均衡(LVS,nginx,HAProxy):轮询、权重
      • 任务分配器和业务服务器之间的连接管理:活性检查
      • 任务分配器本身也需要拓展为多台机器
      任务分解:
      • 按照业务拆分,不同仔细业务采取不同的扩展策略
      • 采用DDD思想,拆分复杂业务

高可用

保证系统无中断地执行业务功能。本质上通过“冗余”来实现。
  • 高可用计算。通过ZooKeeper采用的就是1主多备,而MemCached采用的就是全主0备
  • 高可用存储。存储高可用的难点不在于如何备份数据,而在于如何减少或者规避数据不一致对业务造成的影响。著名的 CAP 定理论证了存储高可用不可能同时满足“一致性、可用性、分区容错性”,最多满足其中两个。
  • 高可用的状态决策。是高可用计算和存储的基础。系统需要能够判断当前的状态是正常还是异常,如果出现了异常就要采取行动来保证高可用。通过冗余实现的高可用系统,状态决策本质上就不可能做到完全正确。
    • 独裁式。所有状态上报给决策者
    • 协商式。启动后都是备机,协商一台作为主机
    • 民主式。投票选举。例如zookeeper

可扩展性

正确预测变化,完美封装变化。
代码层面有设计模式,整洁架构等等。

低成本、安全、规模

  • 往往只有“创新”才能达到低成本目标。
  • 功能安全
  • 架构安全
    • 防火墙:隔离不同的网络,功能强大,性能一般。
      一般需要借助云服务商和运营商提供带宽和流量清洗的功能。
  • 规模带来复杂度的主要原因就是“量变引起质变”。
      1. 功能越来越多,导致系统复杂度指数级上升
      1. 数据越来越多,系统复杂度发生质变

架构设计原则

合适原则

团队资源、实际运行的积累、业务场景

简单原则

软件领域有2方面复杂性:结构复杂性和逻辑复杂性

演化原则

 

架构设计流程

1. 有的放矢-识别复杂度

复杂度包括高性能,高可用,可扩展性,安全,低成本,规模等。实际情况下,复杂度只是其中一个,少数情况下包含其中2个。如果出现3个及以上的复杂度,说明系统有问题,或者架构师判断出现严重失误。
如果一个系统的多个复杂度都存在问题,那么不要幻想一次架构重构解决所有问题。
  1. 列出所有复杂度问题,按照优先级排序
    1. 不用担心解决高优先级的问题后,解决后面复杂度问题时,需要推翻重来,在前面决绝问题时肯定有多个方案可选,总可以挑出性价比最高的方案。即使决定要推倒从来,新的方案也必须能够同时解决之前的问题,这种情况一般是引入了新技术,同时解决了几个问题。

2.按图索骥-设计备选方案

  • 设计3-5个备选方案
    • 不要设想能设计一个最优秀的方案
    • 不要只做一个方案
  • 备选方案的差异要明显
  • 备选方案不要局限于熟悉的技术
  • 备选方案无需过于详细。关注技术选型,而不是技术细节

3.深思熟虑-评估和选择备选方案

360度环评
  1. 分析架构质量属性:性能、可用性、方案复杂度(规模)、可扩展性、硬件成本、安全性等等,按照优先级排序
  1. 设计备选方案。
  1. 对备选方案的每个指标进行分析
  1. 选择最终方案。应该按照各项质量属性的优先级选择,而不能按照数量对比,或者加权计分的方式。

4.精雕细琢-详细方案设计

在具体的技术方案中的技术点和备选方案中的选择。
极端情况下,发现备选方案不可行。如何避免:
  1. 架构师需要熟悉技术关键细节
  1. 方案尽量降低复杂度
  1. 采用团队的方式进行设计

存储高性能

1. 关系数据库

读写分离 - 分散读写压力

数据库部署为一主多从的集群,主服务器负责写入操作(以及个别实时性要求高的数据的读取操作),从服务器负责读取操作
mysql的复制延时在秒级。
读写分离需要解决复制延迟:
  • 二次读取。读从机失败后再读一次主机。
  • 关键业务读写都在主机,非关键业务采用读写分离。

分库分表 - 分散存储压力,以及读写压力

  • 业务分库。
    • 分库后的问题:
      1. 无法join
      1. 无法做事务
      1. 服务器成本
  • 分表
    • 垂直分表。
      • 数据记录数一样,列内容不一样。
        表的操作次数增加。
    • 水平分表。
      • 列内容一样,但是记录条数不一样。
        一般超过5000万需要分表,如果是复杂的表,超过1000万也可以分表,对于简单的表,1亿也可以不分表
        分表的复杂性1:分表后的路由
        • 范围路由:根据ID值。缺点分布可能不均匀
        • Hash路由:分布平均,扩展新表很麻烦
        • 配置路由:需要多查一次配置路由表
        分表的复杂性2:join操作
        分表的复杂性3:count操作
        分表的复杂性4:order by操作

2. NoSQL

not only sql. 作为关系型数据库的补充,不是代替。
  • K-V存储:解决关系数据库无法存储数据结构的问题,以Redis为代表。
    • 没有严格遵循ACID;
  • 文档数据库:解决关系数据库强schema约束的问题,以MongoDB为代表。
    • 可以存储任意数据,No-schema,以json/bson格式存储;缺点是没有事务,无法join;
  • 列式数据库:解决关系数据库大数据场景下的I/O问题,以HBase为代表。
  • 全文搜索引擎:解决关系数据库的全文搜索性能问题,以Elasticsearch为代表。

缓存

绝大部分业务是读多写少,一次生成,多次使用。增加缓存减少对数据库的访问压力
  • 缓存穿透
    • 缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据。
    • 数据库中数据存在,第一次读取后,放入缓存
    • 数据库中数据不存在,第一次读取后,为防止恶意攻击缓存穿透,需要在缓存中加入一个默认空值
    • 数据库中数据存在,但是数据量太大,全部生成缓存耗时耗资源,导致访问都集中到数据库,此时,可以只缓存被访问的数据,并设置过期时间,例如1天。
    • 如果是爬虫,只能加强监控。
  • 缓存雪崩
    • 当缓存批量失效(过期)后,大量并发请求到数据库存储系统,引起系统性能急剧下降。
      解决方案
      1. 缓存更新时加锁保护,对于集群,加分布式锁。
      1. 后台主动更新缓存,缓存永久有效,对于缓存内存不足,就会“踢”掉不被访问的缓存,这是需要在业务层面发现缓存失效之后,通过消息队列通知后台更新缓存,后台主动更新还能支持缓存预热

单机计算高性能

需要解决下面2个问题
  • 服务器如果管理连接
  • 服务器如何处理请求

PPC

主进程监听,每个连接都动态创建一个子进程处理,适合连接数较少的服务,例如数据库服务器。
在主进程listen并accept, 每次accept之后,创建一个子进程。
缺点: foek进程代价较高,进程减需要通信,并发连接数最大几百,太多容易导致频繁调度,系统压力大。

Pre-fork

解决PPC动态创建子进程成本过高的问题,提前创建好子进程。
在主进程listen后,创建一批子进程,每个子进程都在accept同一个socket,这里需要解决一个惊群问题:所有的子进程都会收到通知,在linux2.6之后的内核已解决该问题。

TPC

主进程listen并accept, 在子线程中进行业务处理。解决fork开销大和进程间通信复杂的问题。但需要解决数据共享的互斥保护,并且容易导致死锁,并且线程异常容易导致进程退出,因此还没有PPC使用的多。

Pre-thread

和Pre-fork类似,预先创建线程
  1. 主进程listen,并accept,有连接时,把连接交给线程处理
  1. 主进程listen,在线程中accept(同样需要靠内核解决惊群问题)

Reactor

在上述方案中,经常和线程在连接处理完之后就释放了,造成资源浪费,Reactor的思路就是资源复用,即不再为每个连接创建进程,而是创建进程池,并在一个进程中处理多个连接。
一个连接的处理一般为“read-process-write”,如果没有数据,那么阻塞在read上,这时候进程就空闲在那边,浪费资源,此时如果改为一个进程处理多个连接,只要有一个连接read阻塞,其他连接也无法被处理。 最简单的方式是把read改为非阻塞,经常不断轮询所有连接的read,谁有数据就处理谁,如果连接很多,成千上万, 那么效率就很低。
IO多连接复用的方案解决了上述问题。内核监听全部连接,当某个连接有数据时,就通知用户进程处理。

IO多连接复用

select的方式。每次需要用户指定监听的socket数组。让操作系统去遍历,确定哪个文件描述符可以读写, 然后告诉我们去处理,处理时也需要再遍历一次:
notion image
虽然服务器进程会被select阻塞,但是select会利用内核不断轮询监听其他客户端的IO操作是否完成。
select机制的问题
  1. 一旦某个socket收到数据后,需要重新select所有IO。每次调用select,都需要把fd_set集合从用户态拷贝到内核态,如果fd_set集合很大时,那这个开销也很大;
  1. 同时,每次调用select都需要在内核遍历传递进来的所有fd_set,如果fd_set集合很大时,那这个开销也很大;
  1. 为了减少数据拷贝带来的性能损坏,内核对被监控的fd_set集合大小做了限制,并且这个是通过宏控制的,大小不可改变(32个整数大小,1024bits) — — 一次最多只能select 1024个socket
  1. 进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次。
poll的方式。poll和select一样,但是对select进行了一些改进。但只解决了上述select的第3个问题,有了以下的改进:
  • 不限制监听槽的个数。poll使用 pollfd 结构,而不是select结构fd_set结构,所以poll是链式的,没有最大连接数的限制。
  • 重复通知。poll有一个特点是水平触发,也就是通知程序fd就绪后,这次没有被处理,那么下次poll的时候会再次通知同个fd已经就绪。
epoll的方式。epoll相比poll更进一步,除了没有对描述符数目的限制,它所支持的文件描述符上限是整个系统最大可以打开的文件数目
notion image
因为select|/poll每次有数据到来并处理完后都需要重新传递列表给内核,把是将“维护等待队列”和“阻塞进程”两个步骤合二为一,导致效率比较低。然而大多数应用场景中,需要监视的socket相对固定,并不需要每次都修改。epoll将这两个操作分开,先用epoll_ctl维护等待队列再调用epoll_wait阻塞进程。显而易见的,效率就能得到提升。
另一个低效的原因是序不知道哪些socket收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的socket,就能避免遍历。计算机共有三个socket,收到数据的sock2和sock3被rdlist(就绪列表)所引用。当进程被唤醒后,只要获取rdlist的内容,就能够知道哪些socket收到数据。
综上所述,多连接复用结合线程池就是Reactor。
  • 单Reactor单进程/线程。无法发挥多核优势。适合业务处理非常快的场景,如redis。
  • 单Reactor多线程。在Handler上只负责响应事件,不负责处理业务,数据发给processer业务线程处理。需哟啊处理数据共享问题
  • 多Reactor多进程/线程。父进程Reactor负责处理连接事件,子进程Reactor负责调用Handler。多Reactor多进程代表是nginx,多Reactor多线程代表是memcache。

集群计算高性能

童工负载均衡器来分配任务给多台业务服务器。
  • DNS负载均衡。一般需要HTTP-DNS,来解决传统DNS的更新慢、扩展性差、分配策略简单的问题。实现地理级别的负载均衡。
  • 硬件负载均衡。F5。贵。性能强,百万级TPS。实现集群级别的负载均衡。
  • 软件负载均衡。nginx和LVS。nginx用于应用层, LVS用于TCP/UDP等4层协议。nginx是5万TPS左右,LVS是几十万TPS左右。实现机器级别的负载均衡。

负载均衡算法

  • 轮询。实现最简单。无法感知机器状态。
  • 根据系统负载:连接数、请求数、cpu负载、IO负载。实现复杂。
  • 根据请求响应时间
  • 根据信息Hash

高可用—CAP理论

在一个分布式系统中(相互连接并共享数据的节点集合),当涉及操作时,只能保证一致性、可用性、分区容错性三者中的两个,另外一个必须被牺牲。
  • Consistency一致性:对某个指定的客户端来说,读操作能够返回最新的写的结果
  • Availability可用性:非故障节点在合理的时间内返回合理的响应
  • Partition Tolerance分区容忍性:当出现网络分区后,系统能够继续履行职责
系统只能选择CP或AP,不存在只能保证CA的系统。应为如果要保证CA,那么系统不能有数据写入,只能读,否则数据将不一致。
CP代表:在有分区的情况下,为了保证数据一致性,访问未同步节点时,应该返回错误
AP代表:在有分区的情况下,为了保证数据可用性,访问未同步节点时,数据可能不一致

CAP细节

  • 实际项目中,有的数据需要保证CP,比如账号信息,有的需要保证AP,比如用户信息
  • 实际系统不存在严格意义的一致性,因为不可忽略网络延时
  • 在不发生分区的情况下,可以保证同时满足CA
  • 发生分区后需要做相应的日志,便于恢复

ACID

数据库中保证事务正确性的理论。
  • Atomicity原子性:一个事务中,要么全部完成,要么全部不完成
  • Consistency一致性:事务开始之前和结束之后,数据库完整性没有被破坏
  • Isolation隔离性:允许事务并发执行
  • Durability持久性:事务结束后,数据的修改是永久的

BASE

如果无法保证AP强一致性,那么系统可以采用合适的方式达到基本可用的最终一致性

FEMA

对系统范围内的潜在故障模式加以分析,按照严重程度进行分类,以确定失效对系统的最终影响。
FEMA是从用户角度分析功能点,生成一个FEMA分析表

存储高可用

本质都是将数据复制到多个存储设备,

主备复制(适用:后台管理系统的数据)

  1. 正常运行时,主机的数据通过复制通道备份到备机,备机平时不对外提供任何读写服务
  1. 主机故障后,系统处于不可用状态,客户端的请求不会被发给备机
  1. 如果主机短时间恢复,那么继续提供服务
  1. 如果短时间无法恢复,手动将备机升为主机,增加新的机器作为新的备机,还没有来得及同步到备机的数据,需要进行人工恢复, 也可能永远丢失
特点是:故障需要人工干预

主从复制

  1. 正常运行时,主机的数据通过复制通道复制到从机
  1. 主机提供客户端的写操作, 从机提供客户端的读操作,一些实时性高的数据也可以读主机
  1. 主机故障后,客户端无法写, 但是可以继续从从机读,因此写业务不可用
  1. 如果主机短时间恢复,那么继续提供写服务
  1. 如果短时间无法恢复,手动将从机升为主机,增加新的机器作为新的从机,还没有来得及同步到从机的数据,需要进行人工恢复, 也可能永远丢失
特点是:故障需要人工干预

双机切换

把上述人工干预的步骤由系统主动完成。
关键设计:
  • 主备间状态判断
    • 互连式:有专门的状态通道传递状态,但如果通道本身出现故障,将导致错误的切换操作
    • 中介式:状态上报给第三方中介。相比互连式,连接管理更简单、状态决策更简单。mongoDB采用该方式。zookeeper也是该方式,作为主备倒换的状态中介
  • 倒换时机
  • 倒换策略
  • 倒换触发
  • 数据冲突解决

数据集中集群

不管是一主一从,一主一备等,数据都只能往主机上写。典型应用是zookeeper管理数据集群,解决主备的状态检测、主备切换。
适合数据量不大,集群数量不多的场景。

数据分散集群

每台服务器负责存储和备份一部分数据
每台服务器都可以读写数据,但必须有一个角色来负责执行数据分配算法,典型应用是Hadoop。

分布式事务算法

保证分布在多个节点上的数据统一提交或回滚,满足ACID的要求。

2PC - 2阶段提交

先广播询问,收到回复之后,再广播commit,再等待ack消息。

3PC - 3阶段提交

先广播询问,收到回复之后,再广播precommit,记录undo和redo的信息,再等待ack消息,再广播发送正式提交的命令。

2PC 二阶段提交(强一致性)

有2个角色:协调者和参与者。
  1. Commit请求阶段(投票阶段)。
    1. 协调者询问是否可以提交,并等待参与者回应
    2. 参与者执行事务操作,并写入Undo和Redo信息到日志,执行成功,返回Yes, 执行失败,返回No
    3. 协调者收到全部Yes,那么发出COMMIT消息给所有参与者;或者协调者收到No,那么发出ROLLBACK消息
  1. Commit提交阶段(完成阶段)
    1. 参与者完成COMMIT, 提交事务,并释放事务占用的资源,回复ACK消息;或者收到ROLLBACK消息,执行 Undo, 并释放事务占用的资源
    2. 协调者收到全部ACK,完成事务;或者取消事务
优点:实现简单
缺点:同步阻塞,消息通信期间不能做其他事;状态可能不一致,比如COMMIT有可能丢失,导致参与者超时回滚;单点故障,协调者故障后,参与者会一直阻塞;

3PC 三阶段提交(强一致性)

解决了2PC的单点故障问题,在2PC的二个阶段之间插入一个准备阶段。
  1. canCommit请求阶段(提交判断阶段)。
    1. 协调者询问是否可以提交,并等待参与者回应
    2. 参与者判断自身是否可以提交,返回Yes或No
    3. 协调者如果收到全部Yes,那么进入第二阶段,否则终止事务
  1. preCommit请求阶段(准备提交阶段)。
    1. 协调者发出preCommit,,并等待参与者回应
    2. 参与者执行事务操作,并写入Undo和Redo信息到日志,返回ACK
  1. doCommit提交阶段(提交执行阶段)
    1. 协调者收到全部ACK,那么发出doCommit消息给所有参与者,否则发出ROLLBACK消息
    2. 参与者完成COMMIT, 提交事务,并释放事务占用的资源,回复haveCommited消息;或者等待doCommit消息超时,继续提交事务,并释放事务占用的资源
缺点:同样存在数据不一致问题

分布式一致性算法

分布式事务算法是保证多节点数据的统一提交或回滚,满足ACID要求。
分布式一致性算法是保证同一数据在多个节点上的一致性。满足CAP理论中的CP要求。
常用技术是复制状态机。
  • Paxos。理论上证明正确的算法,但是复杂且缺少细节。
  • Raft。为了工程实现而设计的简化版Paxos,较为简单。
  • ZAB。是zookeeper采用的算法。

计算高可用

本质是通过冗余来规避部分故障的风险。复杂度体现在任务管理。

主备(人工切换)

和存储的主备类似,但不需要复制数据。
  • 冷备,只有执行环境,没有实际运行
  • 温备:系统已启动,可以随时接入

主从(人工切换)

部分任务在主机执行,部分任务在从机执行

对称集群(自主管理)

也叫负载均衡集群。

非对称集群(自主管理)

不同角色服务器担任不同的职责。
 

业务高可用

系统级故障 — 异地多活

相同的系统在不同的地理位置部署,访问任何一个系统,都能达到相同的结果。
  • 同城异区。通过网络相连,当做一般系统来设计
  • 跨城异地。
  • 跨国异地。面向不同用户提供业务,无特殊的架构。
设计技巧:
  1. 保证核心业务的异地多活
  1. 核心数据最终一致性
  1. 采用多种手段同步数据
  1. 只保证绝大部分用户异地多活
异地多活设计步骤
  1. 业务分级
  1. 数据分类
  1. 数据同步
  1. 异常处理

接口级故障 — 降级熔断限流

降级。应对系统自身的故障。停掉部分接口或业务,保证核心业务正常运行。
熔断。应对依赖的外部系统的故障。当发现第三方系统接口响应慢,则不再继续请求该接口。
限流。限制用户的访问,降低系统访问压力
排队。

可扩展

面向流程拆分

分层架构。接入层-控制层-服务层-逻辑层-数据层-存储层

面向服务拆分

微服务。注册服务、登录服务等

面向功能拆分

依赖倒置、微内核

微服务拆分方法

  1. 基于业务逻辑拆分。
  1. 基于可扩展拆分。稳定的服务粒度可以粗一些,经常变化和迭代的拆分为微服务。
  1. 基于可靠性拆分。
  1. 基于性能拆分。
基础设施
  • 自动化测试
  • 自动化部署
  • 配置中心
  • 接口框架
  • API网关
  • 服务发现
  • 服务路由
  • 服务容错
    • 请求重试
    • 流控
    • 服务隔离
    • 服务监控
    • 服务跟踪
    • 服务安全
架构设计
  • 读书笔记
  • 技术架构
  • 八种架构设计模式及其优缺点概述 【读书笔记】《Go With The Domain》9. 基础CQRS
    目录