- 反应式宣言的四个信条
-
- 必须对用户做出反应(及时响应,responsive)
-
- 必须对失败做出反应,并保持可用性(回弹性,resilience)
-
- 必须对不同的负载情况做出反应(弹性,elastic)
-
- 必须对输入做出反应(消息驱动,message-driven)
-
- 反应式的价值结构
- 价值:及时响应,可维护,可扩展
- 手段:弹性,回弹性
- 形式:消息驱动
- 对用户做出反应
- 理解传统做法
- 使用共享资源的延迟分析
- L = x * W;L 被使用的数据库连接,x 负载,W 每个请求花费的时间
- 使用队列限制最大延迟
- 利用并行性
- 用过并行化降低延迟
- 使用可组合的 Future 改善并行性
- 为序列执行表象买单
- 并行执行的限制
- 阿姆达尔定律:S(n) = N/(1+a*(N-1))
- 通用伸缩性法则:S(n) = N/(1+a*(N-1) + b*N*(N-1))
- 对失败做出反应
- 故障
- 软件总有出故障的时候(Software will fail)
- 硬件总有出故障的时候(Hardware will fail)
- 人类总有失误的时候(Human will fail)
- 超时也是一种失败(Timeout is failure)
- 划分与隔离(bulkheading)
- 使用断路器
- 监督
- 故障
- 放弃强一致性
- CAP
- ACID 2.0:BASE
- Associative:可结合的
- Commutative:可交换的
- Idempotent:幂等的
- Distributed:分布式的
- 接受更新
- 无冲突复制数据类型(conflict-free replicated data tyes,CRDT)
- 操作交换(operational transformation)
- 对反应式设计模式的需求
- 管理复杂性
- 固有的复杂性(Essential complexity)是问题领域中所固有的
- 附带的复杂性(Incidental complexity)是仅由解决方案所引入的
- 使编程模型更贴近真实世界
- 管理复杂性
- 反应式的早期解决方案
- Erlang Actor
- 函数式编程
- 本质:洞察到程序实际上可以按照纯粹的数学函数来编写,也就是说,每次给这些函数传递相同的输入时,它们总将返回相同的作用,并且不会产生副作用。
- 不可变性
- 引用透明性
- 副作用
- 函数作为一等公民
- 及时响应用户
- 性能权衡之三选二
- 高吞吐量
- 低延迟,并且流畅、无卡顿
- 资源站用小
- 性能权衡之三选二
- 对反应式设计的现有支持
- 绿色线程(green thread)
- 事件循环
- 通信顺序进程(Communication Sequential Processes, CSP)
- go 的 channel
- Future 和 Promise
- 反应式扩展工具包
- RxJava
- Actor 模型
- 天然的异步性
- 通过监督实现容错性
- 位置透明性
- Actor 内部无并发
- Erlang 与 Akka 之间的不同
- Erlang 的 BEAM 使用了独特的、隔离的进程
- Erlang 的 Actor 使用了一种称为选择性接受的模式(Selective Receive)
- 消息 & 垂直伸缩
- 基于事件 VS. 基于消息
- 基于事件:提供了一种将响应关联到特定事件的方法
- 有事件队列 & 事件循环 & 每个事件一个回调函数
- 事件生产者是可寻址的
- 基于消息:提供了一种将消息发送到特定接受者的方式
- 消费者是可寻址的
- 消费者负责处理其自身所接受的消息的好处
-
- 使得单个消费者对消息的处理过程可以顺序进行,也使得有状态的处理过程不需要同步操作。
-
- 顺序处理使得对于事件的响应依赖于消费者当前的状态。
-
- 消费者可以在系统过载时候选择抛弃事件或者进行短路处理。
-
- 最后但是最重要的一点是:这样做符合人们的工作习惯。
-
- 处理消息时的都能发生的两个基本问题:
-
- 有时我们必须保持特定的、非常重要的信件的送达。
-
- 如果消息发送的速率快于他们能够被送达的速率,那么消息就会在某处堆积起来,并最终导致系统崩溃或消息丢失。
-
- 基于事件:提供了一种将响应关联到特定事件的方法
- 同步 VS. 异步
- 以同步方式通信,生产者和消费者都需要在同一时间准备好。
- 以异步方式通信,不管接受者是否准备好,发送者都可以发送消息。
- 流量控制
- 调整消息流的传输速率,以确保接受者不被淹没的过程。
- ACK:acknowledgement
- 送达保证
- 至多一次送达(at-most-once delivery)
- 发送者发送一次,不管响应如何
- 常规日志
- 至少一次送达(at-least-once delivery)
- 发送者发送消息,接受者响应,发送者确认响应(如果没有得到响应,需要重发)
- 重要日志(比如 财务)
- 确切一次送达(exactly once delivery)
- 在确保至少一次的语义下,接受者需要负责去重。
- 至多一次送达(at-most-once delivery)
- 作为消息的事件
- 消息天然表示事件,消息传递天然就在表达事件驱动的交互。
- 同步消息传递
- 什么是位置透明性
- 无论接收者在哪里处理消息,发送消息的代码看起来都一样。
- 透明化远程处理的谬误
- 基于显示消息传递的纠正方案
- 优化本地消息传递
- 消息丢失
- 水平扩展性的获得
- 位置透明性使得测试更加简单
- 动态组合
- 分层拆解问题
- 定义层级结构
- 依赖与子模块
- 避免矩阵结构
- 康威定律
- 避免矩阵结构
- 构建你自己的大公司
- 规范和测试的优点
- TDD
- 水平扩展性和垂直伸缩性
- 回弹性要求对系统进行分布和划分
- 分布:分布式,避免因单点故障而导致的全局失败
- 划分:隔离不同的分布式单元,使得任意单元的失败都不会扩散到其他单元上。
- 需要对失败做出反应。
- 所有权意味着承诺
- 检验错误:面向的使用服务的用户(user)
- 失败:应该被服务的拥有者(owner)处理
- 所有权隐含生命周期控制
- 所有级别上的回弹性
- 每个模块都是回弹性的一个单元。
- 强一致性 VS. 有界一致性
- 分布式共识
- 封装模块纠正方案
- 从用户的视角来看,唯一重要的是获得的响应放映了请求时服务的正确状态。其阿红状态不过服务内部维护的数据集。
- 根据事务边界对数据和行为进行分组
- DDD:领域驱动设计
- 跨事务边界建模工作流
- 分布式事务
- Saga 模式
- 失败单元即一致性单元
- 分离职责
- 分解的目标
-
- 一个模块只做一件工作并把它做好。
-
- 模块的(职责)范围由其父模块的职责既定。
-
- 模块边界定义了通过复制实现的水平扩展性的可能粒度。
-
- 模块内部封装失败状态,而它们的层级定义监督关系。
-
- 模块的生命周期由其父模块的生命周期界定。
-
- 模块边界与事务边界一致。
-
- 命令和查询职责分离(Command and Query Responsibility Segregation)
- 分解的目标
- 坚持一致性的隔离范围
- 逻辑编程和声明式数据流
- 逻辑编程(Prolog 和 Datalog)
- 函数式编程范式
- 注重函数及其组合
- 数据流编程(dataflow programming)
- 关注数据在连接着计算节点的网络(更确切的说是,有向无环图)中的流动。
- 函数式反应式编程
- Functional Reactive Programming, FRP
- 不共享简化并发
- 无共享并发(shared-nothing concurrency)
- 用于处理不确定性的策略可以分为两类:
-
- 我们可以拒绝那些会引发问题的事件序列集,通过引入显示的同步,来将不确定性的影响降低到其不再改变程序的特性为止。
- 涉及用于协调的运行时成本。
-
- 我们可以限制程序仅使用可交换(commutative)的操作。
- 涉及用于限制可交换数据集以无冲突的形式高效表达的开发成本。
-
- 共享状态的并发
- 底层的线程、锁和 CPU 原子指令
- 如何窘境突破
- 一个具有完全确定性的、可推导的编程模型。
- 尽量使用不可变值的函数式和声明式编程。
- 基于一个只是用消息的分布式设计,使你可在应用内部,将业务处理过程模型化和视觉化成消息流。
- 推动数据的流动
- 模型化领域流程
- 通用语言(Ubiquitous Language)
- 拟人化的比喻便于人们形象化呈现和编排处理流程。
- 认清回弹性的局限性
- 估计速率和部署规模
- 为流量控制进行规划
- TDD(Test-Drivern Design)
- 如何测试
- 错误 VS. 失败
- 失败(failures):是在服务内部发生的意料之外的事件,炳辉阻碍服务继续正常工作。
- 错误(errors):是意料之中的、已经编码处理的情况。
- 测试的种类
-
- 单元测试
-
- 组件测试
-
- 联动测试
- 除了标称和错误情况外,也要考虑事变场景:服务在其各依赖方无法提供应有的功能时应该如何做出反应?
-
- 集成测试
-
- 用户验收测试
-
- 黑盒测试与白盒测试
-
- 错误 VS. 失败
- 测试环境
- 云上测试
- 单机独享还是共享资源
- 异步测试
- 提供阻塞的消息接收者
- 选择超时时间的难题
- 针对不同测试环境缩放超时时间
- 测试服务响应时间
- 测试服务级别协议(SLA,Service Level Agreement)
- 断言消息的缺失
- 提供同步执行引擎
- 异步断言
- 完全异步的测试
- 断言没有发生异步错误
- 测试非确定性系统
- 执行计划的麻烦
- 测试分布式组件
- 模拟 Actor
- 模拟
- 存根
- 逆向洋葱测试模式
- 分布式组件
- AkkaTestKit 的 TestProbe
- 测试弹性
- 了解每个节点吞吐量的上限和下限
- 测试回弹性
- 回弹性:是指通过复制、遏制、隔离以及委托来实现的。
- 应用程序回弹性
- 外部回弹性(external resilience):通过校验来处理
- 内部回弹性(internal resilience):应用程序在校验请求之后,处理请求过程中所发生的内部错误
- 执行回弹性
- API 回弹性
- 基础设施回弹性
-
- 网络弹性(局域网和广域网)
-
- 集群回弹性
-
- 节点和机器回弹性
-
- 数据中心回弹性
-
- 测试及时响应性
- 在特定负载下的延时
- Histogram
- 简单组件(Simple Component)模式(单一职责原则,Single Responsibility priciple)
- 一个组件应该只做一件事情,并且完整做完。
- 高内聚,低耦合
- 错误内核(Error Kernel)模式
- 在监督层级中,将重要的应用程序状态或功能留存在根部附近,而将具有风险的操作委托给叶子节点。
- 层级结构。
- 放任崩溃(Let-It-Crash)模式
- 对于内部失败处理,优先选择完整重启组件。
- SLA:使用失败频率(例如平均失败时间间隔 MTBF)以及中断的时长(也称为平均修复时间 MTTR)。
- 该模式的倒用:奇博士模式(Pacemaker pattern)
- 让组件周期性“崩溃”,而不是等待失败的发生。
- 推论:心跳模式
- heartbeat
- 推论:主动失败信号模式
- 断路器(Circuit Breaker)模式
- 在失败时间延长时,通过断开与用户之间的连接来保护服务。
- 惊群效应(thundering herd)。
- 模式种类
- 主动-被动复制模式(Active-Passibe Replication Pattern)
- 适用于显示失败切换可接受的场景。
- 也称为失败切换(Failover)或主从复制(master-slave replication)
- Kafka 的 partition 就是这种模式
- 多主复制模式(Multiple-Master Replication Pattern)
- 使得客户端可与它们所选的任何副本进行通信。
- 基于共识的复制
- 具有冲突检测与处理方案的复制方式
- 无冲突的可复制数据类型
- 无冲突的可复制数据类型(Conflict-Free Replicated Data Types,CRDT)
- 主动-主动复制模式(Active-Active Replication Pattern)
- 对于一组选定的失败情形,这种模式能提供零宕机的解决方案。
- 主动-被动复制模式(Active-Passibe Replication Pattern)
- 对比
模式 | 定义 | 优点 | 缺点 |
---|---|---|---|
主动-被动复制模式 | 保持服务的多个副本运行在不同的位置,但在任何时刻,只接受对于其中一个位置的状态修改。 | 基于已经存在的集群单例实现,使用起来相对简单。 在正常的操作先它运行的很快。因为只有一个副本,所以能够提供良好的一致性。 |
在失败切换时可能产生宕机时间 |
多主复制模式 | 在不同卫生纸上保持服务的多个副本,每处都可接受修改,并将所有修改在各个副本之间传播 | ||
基于共识的复制模式 | 允许更新被热和难以副本几首,从而获得更好的回弹性。 | 但是围嘴提供完美一致性的后果,它不得不承受高额的协调开销,以及因此造成的低吞吐率。 在发生严重错误时,对于一致性的偏好所赵成的不可避免的代价是系统不可用。 |
|
基于冲突检测与解决的复制模式 | 允许系统在严重失条件下忍让能保持可用性 | 但是这会导致数据丢失,或要求额外的、对于冲突解决的手动处理 | |
基于无冲突的可复制数据类型的定制 | 使得冲突从根本上无法出现。因此应用这种模式,就算在发生严重失败的情况下,系统也能获得完美的可用性 | 但是可用的数据类型却又受到限制,并要求程序代码进行特殊适配,以及将其设计成最终一致性模型 | |
主动-主动复制模式 | 在不同的地方持有服务的多份副本,并在所有副本上执行所有的修改操作 | 处理了如何在失败时避免不可用时段的问题,同时维持了通用的编程模型 | 代价是所有的请求必须通过单个瓶颈点进行发送,以保证副本的一致性行为,或者程序可按关联一致性重组,采用虚拟同步的方法来获得高性能和高可用性 |
- 资源封装模式(Resource Encapsulation patterrn)
- 资源及其生命周期必须由一个组件负责。
- 资源借贷模式(Resource Loan pattern)
- 在不转让所有权的情况下,给予客户端对稀缺资独占的临时访问权。
- 复杂命令模式(Complex Command pattern)
- 向资源发送符合指令以避免过度使用网络。
- 领域特定语言(Domain Specific Language)
- 内部 DSL:语法基于宿主编程语言描述,并能嵌回宿主语言。
- 外部 DSL:完全自成一体的领域专业语言。
- 资源池模式(Resource Pool pattern)
- 在资源的所有者后面隐藏一个弹性资源池。
- 请求响应模式(Request-Response pattern)
- 消息中包含一个用于接受响应的返回地址。
- 案例:HTTP、Actor、AMQP
- 消息自包含模式(Self-Contai�ned Message pattern)
- SMTP 较为复杂,是因为早期网络连接花费太多时间,;因此该协议通过很多小步骤来执行消息交换。
- 询问模式(Ask pattern)
- 将产生响应的过程委托给专用的临时组件。
- 转发流模式(Forward Flow pattern)
- 让信息和消息尽可能直接地流向其目的地。
- 聚合器模式(Aggregator pattern)
- 如果需要多个服务响应来计算服务的调用结果,可专门创建一个临时组件。
- 事物序列模式(Saga pattern)
- 将耗时长的分布式事务切分为快速的本地事务,并通过补偿操作进行恢复。
- 换言之:创建一个临时组建来专门管理分布在多个组件中的一系列动作的执行过程。
- 业务握手模式/可靠投递模式(Business Handshake pattern)
- 在消息中包含标志符和/或序列信息,并在收到确认之前一直重试。
- 拉取模式(Pull pattern):从消费者将回压传送到生产者
- 让消费者向生产者对数据的批量大小提出要求。
- Kafka 的 long pulling
- 让消费者向生产者对数据的批量大小提出要求。
- 托管队列模式(Managed Queue pattern):使得回压可测量、可操作
- 管理一条显示的输入队列,并对其填充级别予以反应。
- 丢弃模式(Drop pattern):在严重过载的情形下保护组件的正常运行
- 丢弃请求,比不受控制地失败更可取。
- 限流(Drop pattern):帮你尽可能避免过载情况
- 根据与其他服务之间的约定来限制自己的输出速率。
- 漏斗、令牌桶
- 领域对象模式(Domain Object pattern):将业务逻辑从通信机制中解耦出来。
- 将业务领域逻辑与通信、状态管理分离。
- 分片模式(Sharding pattern):能使你在弹性集群上存储任意数量的领域信息。
- 基于各类独一无二并且稳定的对象属性,相应地将大量领域对象进行分组分片,从而水平扩展对它们的管理。
- 事件溯源模式(Event-Sourcing pattern):通过将事件日志是为真相的唯一来源,统一(状态的)变更通知和持久化概念。
- 仅通过应用事件来执行状态变更,并通过将事件存储在日志中来持久化状态变更。
- 事件流模式:使用这个真相来获取和传播信息。
- 散布某个组件发出的事件,以便系统的其他部分可从中衍生知识。