MyBlog

【读书笔记】《凤凰架构》

last modified: 2021-11-13 12:37

一、演进中的架构

不能简单的叙述,比如“单体架构是坏的,微服务架构是好的”。要用“生长的观念”来看待问题:每一个技术的发展和演进都是有原因的,“好用和优秀”并非放之四海皆准。这也是架构存在的意义,架构师需要依据对业务、团队现实状况的理解,“因地制宜”。

“生长的观念”是我在BitTiger上学来的,正暗合作者的观念。因此用“生成的观念”,梳理一下软件架构发展的脉络。

v1.0 原始分布式时代

20世纪70年代末到80年代初,计算机经历了从大型机到微型机的蜕变。此时的微型机通常具有16位寻址、不足5MHz时钟频率的CPU和128KB左右的内存地址空间。硬件上有限的运算能力,直接影响了单台计算机上软件能够达到的最大规模。于是产生了对分布式最原始的探索:如何使用多台计算机共同协作来支撑同一套软件系统。

期间OSF(Open Software Foundation)联合计算机厂商制定了DCE(Distributed Computing Environment)的分布式技术体系,如RPC的鼻祖DCE/RPC(源自NCA),分布式文件系统DCE/DFS(源自AFS),源自Kerberos的服务认证规范。

然而分布式体系太过复杂,将一个系统拆分到不同的机器运行,为解决这样做带来的服务发现、跟踪、通信、容错、隔离、配置、传输、数据一致性和编码复杂度等方面的问题所付出的代价已远超分布式所带来的收益。并且随着摩尔定律的黄金时代到来,单机的性能问题得以缓解,软件转向单体系统时代。

v2.0 单体系统时代

讨论单体架构前,先要厘清一些基本概念。

大型的单体系统

许多微服务的资料中,单体系统都被贴上反面角色的标签,比如著名的《微服务架构设计模式》中第1章名字就是“逃离单体地狱”。其实这些资料中都有一个隐含的定语“大型的单体系统“。单体系统的不足,必须在软件的性能需求超过了单机、软件的开发人员明显超过了”2 Pizza Team”范畴的前提下才有讨论的价值。

单体系统并非指不可拆分(巨石)

单体与微服务架构的软体一样,也可以纵向分层,比如MVC架构。也可以横向按照技术、功能、职责等维度,将软件拆成各种模块,以便重用和管理代码。

单体系统真正的缺陷不在如何拆分,而在拆分之后的自治和隔离能力上。单体系统的优势在于,所有代码运行在同一进程中,所有模块、方法的调用都无须考虑网络分区、对象复制这些麻烦的事和性能损失,但同时也意味着,任何一部分代码出现缺陷,所造成的影响是全局性的、难以隔离的。同时也无法做到单个模块的停止、更新和升级,无法实现技术异构。(技术异构不是目标,而是解决特定问题的一种思路。)

随着软件架构的演进,构建可靠系统的观念也完成从“追求尽量不出错”到正视“出错是必然”的转变,这个观念转变,是微服务架构挑战和逐步取代单体架构的基石。

v3.0 SOA(Service-Oriented Architecture)时代

再来回顾一下,单体系统的缺点,除了算力之外,还有自治和隔离。自治和隔离需要将大型单体系统进行拆分,让每个子系统都可以独立部署、运行、更新。开发者尝试了“烟囱式架构(Information Silo Architecture)”,“微内核架构(Microkernel Architecture)”,“事件驱动架构(Event-Driven Architecture)”等各种解决方案。

软件架构来到SOA时代,其包含的许多概念、思想都能在微服务中找到对应身影,比如服务之前的松散耦合、注册、发现、治理,隔离、编排等。SOA针对这些问题提出了具体的解决方案,甚至针对“软件开发”这件事情本身,进行了更具体、更系统的的探索

更具体:SOAP协议族,ESB,BPM,SDO,SCA…

更系统:指SOA的宏大理想,它的终极目标是总结出一套自上而下的软件研发方法论,做到企业只需要跟着SOA的思路,就能够一揽子解决掉软件开发过程中的全部问题。

但因SOA过于精密的流程和理论需要懂得复杂概念的专业人员才能够驾驭,它注定只能是阳春白雪式的奢侈品,很难作为普适性的软件架构风格推广。

v4.0 微服务时代

2012年,在波兰克拉科夫举行的“33rd Degree Conference”大会上,Thoughtworks首席咨询师James Lewis做了题为“Microservices - Java, the UNIX Way”的演讲,其中提到单一职责、康威定律、自动扩展、领域驱动设计等原则,却只字未提SOA,反而号召应该重拾UNIX的设计哲学(As Well Behaved UNIX Service)。微服务于2014年,Martin Fowler与James Lewis合作的文章“Microservices: A Definition of This New Architectural Term”发表后“兴起”。

对于SOA“过于精密的流程和理论”,UNIX设计哲学成为治病良方,微服务追求更加自由的架构风格,摒弃了几乎所有SOA里面可以抛弃的约束和规范,提倡以“实践标准”代替“规范标准”。

Fowler的文中列举了微服务的9个核心的业务与技术特征:

  1. Organized around Business Capability(围绕业务能力构建),强调了康威定律的重要性。
  2. Decentralized Governance(分散治理)服务对应的开发团队对服务运行质量负责,也有不受外界干扰的掌握服务各个方面的权力,比如选择与其它服务异构的技术来实现自己的服务。
  3. Componentization via Service(通过服务来实现独立自治的组件),Service而不是Library。
  4. Product not Project(产品化思维),团队为软件产品整个生命周期负责,不仅应该知道如何开发,也应该知道用户是谁,用户如何使用、反馈。这里的用户不一定是最终用户,也可能是调用这个服务的另一个服务。
  5. Decentralized Data Management(数据去中心化)
  6. Smart Endpoint and Dumb Pipe(强终端弱管道),对SOAP和ESB的反对,如何服务需要额外通信能力,应该在自己的Endpoint上解决,而不是在通信管道上一揽子解决。
  7. Design for Failure(容错性设计)
  8. Evolutionary Design(演进式设计)
  9. Infrastructure Automation(基础设施自动化)

微服务的风格,注重的是一种意境,但却不教给我们具体的方式。好处是让我们抛却SOA的心智负担,但同时亦是一把双刃剑。对于服务的注册发现、跟踪治理、负载均衡、故障隔离、认证授权、伸缩扩展、传输通信、事务处理等问题,微服务中不再有统一的解决方案。比如一个服务发现,就有Netfix的Eureka,HashiCorp的Consul,阿里的Nacos,Apache的Zookper,CoreOS的etcd,CNCF的CoreDNS等。

微服务时代充满着自由的气息,微服务时代充斥着迷茫的选择

v5.0 后微服务时代

服务注册发现、跟踪治理等,是分布式中无法避免的问题,但换个思路思考一下,这些问题一定要由软件系统自己来解决吗?

答案是:No。虚拟化和容器技术(Docker,Kubernetes)、Service Mesh技术的发展,为在基础设施层面解决分布式系统的问题带来了答案。这是后微服务的时代,也称为云原生时代。

二、架构师的视角

RPC

所有的RPC实现,都是在解决这3个问题:

  • 如何表示数据

不同语言,不同操作系统,不同硬件指令集,不同数据宽度、字节序。解决方案是各种序列化和反序列化协议

  • 如何传递数据

不仅传递参数和返回结果,还要处理如异常、超时、安全、认证授权、事务等信息交换的需求。专门有一个名词Wire Protocol来表示这种在两个Endpoint之间交换数据的行为。

  • 如何表示方法

IDL(Interface Description Language)

事务

事务保障系统中的数据都符合预期,且相关联的数据不产生矛盾。用一个词描述就是C(Consistency),要达成C,按经典数据库理论,需要A(Atomic)、I(Isolation)和D(Durability)来共同保障。

AID在保障单服务访问单数据源场景下是有用的,但分布式场景中会存在多服务访问多数据源的场景需求,因此事务的讨论中,我们需要分场景:

  1. 单服务访问单数据源(本地事务,更精确应称局部事务),Redo/Undo Log(AD)锁,MVCC(I)
  2. 单服务访问多数据源(全局事务,也是DTP的一种),XA(语言无关的,事务处理规范,JAVA中是JTA) 2PC
  3. 多服务访问单数据源(共享事务,无实际应用场景,仅为叙事完备放在这里)
  4. 多服务访问多数据源(分布式事务)。CAP,BASE柔性事务。TCC,可靠事件队列,SAGA

本地事务:实现原子性和持久性(A&D)

要实现事务的原子性和持久性,数据必须落入磁盘。但写入磁盘过程中,程序可能崩溃,另外写入磁盘不仅有”已写入”和”未写入”两处状态,还存在着”正在写”的中间状态。

这种崩溃和中间状态要求我们有其它机制保证原子性和持久性。

修改磁盘数据不能像修改内存中的变量值一样直接修改,而应该把这个操作的所有信息,包括修改什么数据,数据物理上位于哪个磁盘块,从什么值修改成什么值等,以及日志的形式,以append的方式记录到文件中。

只有在日志安全落盘,数据库在日志中看到代表事务成功的”Commit Record”之后,才根据日志上的数据真正修改数据,修改完成后再在日志追加一条”End Record”表示事务已经完成持久化。这种实现方式称为”Commit Logging”。

Redo Log就是Commit Logging的具体实现。

它的原理不难理解,当数据库未写”Commit Record”时,事务失败,将上一个成功commit日志后的日志标记为回滚即可。

当Commit Record成功,但End Record失败,数据库根据日志重做事务,然后写入End Record。注意这里日志中的操作都应该是幂等的。

Commit Logging有一个很大的缺陷,因为只有在Commit Record日志写入后,才进行真实的事务的数据修改,在这之前即使I/O负载低,也不能利用到。

因此ARIES(后面介绍,这是数据库的一篇重要论文)提出了优化方案:”Write-Ahead Logging”,即允许在事务提交前写入变动的数据。

Write-Ahead Logging依据事务提交的时间点,将何时写入数据变动分为FORCE和STEAL两种情况。

  • FORCE:当事务提交后,要求数据修改同步完成称为FORCE,不要求同步完成称为NON-FORCE。现代数据库为I/O效率原因,都使用NON-FORCE方式。
  • STEAL:在事务提交前,允许数据修改称为STEAL,不允许修改称为NON-STEAL。NON-STEAL同样可提升I/O效率。

Commit Logging支持NON-FORCE,但不支持STEAL。

Write-Ahead Logging即支持NON-FORCE,也支持STEAL。它实现的方式是增加另一种Undo Log的日志类型,当变动数据写入磁盘前,必须先记录Undo Log标注修改了哪里的什么数据,由什么值改为什么值,以便在崩溃或回滚时,对提前写入的数据进行擦除。Undo Log也应该被设计成幂等的。

本地事务:实现隔离性(I)

隔离性保证每个事务各自读取,不相互影响。隔离跟并发相关,如果所有事务串行,那不需要任何隔离。

要实现并发访问数据,最简单的方案是加锁。现代数据库都提供3种锁,写锁(X-Lock,eXclusive Lock)、读锁(S-Lock,Shared Lock)和范围锁(Range Lock)。

以锁为手段来解决数据库隔离性问题,导致数据库表现出不同的隔离级别:串行化(Serializable),可重复读(Repeatable Read),读已提交(Read Committed)和读未提交(Read Uncommitted)。以及幻读、不可重复读和脏读等现象。事务隔离级别并不是数据库固有的属性,这点要有清晰认知。

除了锁的实现外,以上幻读、不可重复读和脏读等问题,都有一个共同特点:都是读的过程中,另一个写数据的事务影响而破坏了隔离性,针对这种“一个事务读+一个事务写”的问题,目前主流数据库采用MVCC(Multi-Version Concurrency Control)的无锁优化方案。MVCC不再赘述。

除了“读+写”,另外还一种“写+写”的情形,只能通过锁来实现,区别是“乐观锁”还是”悲观锁“,这里要注意的一点,不要迷信乐观锁一定比悲观锁性能好,要看具体场景,在竞争激烈的情况下,悲观锁性能更优。

全局事务

全局事务这里指单服务访问多数据源。XA是处理全局事务的通用规范 ,JAVA中的实现为JTA。核心是2PC即两阶段提交。

  • 准备阶段。协调者向事务参与者询问是否准备好提交。这里参与者做的是,写事务日志,它与本地事务唯一的不同是暂时还不写Commit Record.
  • 提交阶段。任意一个参与者在准备阶段返回Not-prepared或超时,则协调者向所有参与者发送Abort,事务失败。否则协调者本地持久化事务状态后,向所有参与者发送Commit命令。

2PC的缺点是:

  1. 协调者单点
  2. 性能问题:期间有2次RPC,三次持久化(重做日志、协调者状态持久化、Commit Record)
  3. 一致性问题:如果准备阶段完成,但提交阶段时,网络不可达导致协调者本地持久化了事务状态,但他与其它参与者网络断开,导致参与者未收到Commit命令,则数据不一致。(这里感觉作者描述的不太好,我理解网络不通导致协调者不停重试直到恢复,是一个可用性的问题,系统容错能力较差)

2pc

分布式事务

这里的分布式事务与DTP(Distributed Transaction Processing)模型中的概念不同。DTP中的分布式是针对多数据源来说的,不涉及服务。而这里的分布式是相对于服务来说的。

事务中,我们追求的是C,手段是ADI,而分布式下,由于CAP理论,我们通常放弃C而追求AP。这与事务的目的相违背。我们把CAP,ADIC下讨论的一致性称为“强一致性”,把在分布式下,实现AP但又追求的C称为“弱一致性”。

由于一致性的定义变化,事务一词的含义也被扩展了,人们把ACID的事务称为“刚性事务”,把接下来的几种分布式事务统称为“柔性事务”。

分布式事务一:可靠事件队列

2008, Dan Pritchett, “Base: An Acid Alternative”

使用可靠事件队列的一次购物行为事务过程:账号服务扣款,商家服务收款,仓库服务扣库存

best effort delivery

分布式事务二:TCC(Try, Confirm/Cancel)事务

2007, Pat Helland, “Life beyond Distributed Transactions: an Apostate’s Opinion”

可靠事件队列的问题在于隔离性,它会导致超卖的问题。而对于刚性事务,只要隔离级别足够,比如Repeatable Read,则该问题可完全避免,只要加锁失败即可。

而TCC方案天生适合对隔离性有要求的业务。以下是购物使用TCC的事务过程

TCC

TCC类似2PC,但它不是在基础设施层面实现,而是在业务代码层面实现,对业务有比较大的侵入性。

分布式事务三:SAGA事务

TCC在隔离性、性能上表现都非常好,但是对业务有较强的侵入性。除了上面提到的需要业务编码,更重要的是它要求的技术可控性上的约束。比如上述购物场景,如果用户不需要在系统充值,而是直接走网银服务,银行不太可能配合实现冻结、解冻这些操作,所以TCC的第一步Try将无法实施。这种情况只能考虑另一种柔性事务方案:SAGA。

1987, SAGA, SAGAS

SAGA事务原理:

将一个大的分布式事务,拆解成一系列子事务(本地事务)集合。SAGA由两部分操作组成:

  • 将大事务拆成若干小事务,将分布式事务T拆解成n个子事务,命名为T1, T2, ……, Tn. 每个子事务都应该是或者能被理解为原子行为。如果分布式事务能正常提交,其对数据的影响(即最终一致性)应与连续按顺序成功提交Ti等价
  • 为每一个子事务设计对应的补偿动作,命名为C1, C2, ……, Cn。Ci必须满足以下条件。
    • Ti与Ci都具备幂等性
    • Ti与Ci满足交换律。即无论先执行Ti还是Ci,其效果是一样的
    • Ci必须能成功提交,即失败时持续重试,或者被人工介入为止

如果T1到Tn都成功提交,那事务顺利完成。否则采取下面两种恢复策略之一

  • 正向恢复(Forward Recovery),如果Ti失败,则一直重试直到其成功,这种策略应用在事务必须成功的场景,不需要补偿,比如给人扣钱了后一定要发货
  • 反向恢复(Backward Recovery),如果事务Ti失败,则执行Ci对事务进行补偿,直到成功为止

没有一个包治百病的分布式事务方案,都是在隔离性、性能及架构上做的权衡。

透明、多级分流系统

这章讲的都是基础知识,但作者的归纳很新颖:“客户端缓存、域名解析、传输链路、CDN、负载均衡、服务端缓存,都是为了达成‘透明分流’这个目标所采用的工具和手段,高可用架构、高并发则是通过‘透明分流’所获得的价值。”

  • 客户端缓存
  • 域名解析
  • 传输链路
    • 连接数优化
    • 传输压缩
    • 快速UDP网络连接(QUIC)
  • CDN(内容分发网络)

CDN

CDN的核心是通过CNAME,将解析主导权交由CDN厂商架设的CNAME权威DNS服务器,该服务器可以根据网络拓扑、时延、容量等信息和一定策略,选择一个最合适的IP给用户。

  • 负载均衡
    • 数据链路层负载均衡
    • 网络层负载均衡
    • IP隧道模式
    • NAT模式
    • 应用层负载均衡
    • 四层
    • 七层
  • 服务端缓存
    • 缓存属性
    • 吞吐量
    • 命中率与淘汰策略
    • 扩展功能(事件通知、命中率统计、容量控制)
    • 分布式缓存
    • 缓存风险
    • 缓存穿透:KEY不存在
    • 缓存击穿:KEY Miss时(如过期),客户端并发读取数据库并更新缓存
    • 缓存雪崩:大量KEY同时Miss,请求都打到数据库(如大量KEY同时过期)
    • 缓存污染(缓存与数据库数据不一致)
      • Cache Aside
      • Read/Write Through
      • Write Behind Caching

安全性

三、分布式的基石

分布式共识

  • Paxos
  • Gossip

从类库到服务

  • 服务发现
  • 网关路由
  • 客户端负载均衡

流量治理

  • 服务容错
    • 容错策略
    • 故障转移(Failover)
    • 快速失败(Failfast)
    • 安全失败(Failsafe)
    • 沉默失败(Failsilent)
    • 故障恢复(Failback)
    • 并行调用(Forking)
    • 广播调用(Broadcast)
    • 容错设计模式
    • 断路器模式
    • 舱壁隔离模式
    • 重试模式
  • 流量控制
    • 滑动窗口模式
    • 漏桶模式
    • 令牌桶模式

可靠通信

可观测性

  • Logging
  • Tracing
  • Metrics

四、不可变基础设施

虚拟化容器

  • 隔离文件:chroot
  • 隔离访问:名称空间(Linux Namspace)
  • 隔离资源:cgroups
  • 封装系统:LXC(LinuX Container)
  • 封装应用:Docker
  • 封装集群:Kubernetes

容器间网络

持久化存储

资源与调度

服务网格(Sevice Mesh)

五、技术方法论

软件研发中任何一项技术、方法、架构都不可能是银弹

目的

前提

  • 决策者与执行者都能意识到康威定律在软件设计中的关键作用
  • 组织中具备一些对微服务有充分理解、有一定实践经验的技术专家
  • 系统应具有以自治为目标的自动化与监控度量能力
  • 复杂性已经成为制约生产力的主要矛盾

边界

  • 微服务粒度的下界是它至少应满足独立——能够独立发布、独立部署、独立运行与独立测试。内聚——强相关的功能与数据在同一个服务中处理。完备——一个服务包含至少一项业务实体与对应的完整操作
  • 微服务粒度的上界是一个2 Pizza Team能够在一个研发周期内完成的全部需求范围

治理