Dawn's Blogs

分享技术 记录成长

0%

事件溯源

事件溯源是一种以事件为中心的技术,用于实现业务逻辑和持久化。

使用事件溯源的方式的好处是:

  • 提供准确的事件审计信息,保留聚合的历史。
  • 与领域事件的发布是天然的契合的。

弊端在于:

  • 更改了现有的业务实现逻辑,与传统的逻辑完全不同。
  • 事件的结构可能发生改变,那么如何兼容新版本和旧版本事件是需要解决的问题。
  • 删除是通过记录删除事件的形式进行的,这不是真正意义上的物理删除。

持久化

使用事件溯源进行持久化时,与传统的数据库表设计不同。传统的数据库表设计中,仅仅保存聚合的最终状态,一旦聚合的状态发生变化并进行了持久化,那么之前的状态我们是无从知晓的。但是在事件溯源中,存储的是聚合相关的事件,每一个条目都保存了事件类型(创建、修改、删除等),聚合的唯一标识符,以及序列化的事件(如 json 格式)。

使用事件溯源的方式组织表结构,通过检索重放对应聚合的所有事件来重建聚合。

实现业务逻辑

使用事件溯源实现业务逻辑时,与传统的方法不同。在传统的方法中,会更新聚合的一个或者多个字段,更新之后再将聚合的最终状态存储在数据库当中。在事件溯源中,实现业务逻辑的结果就是产生一系列的事件,事件表示进行的状态更改操作,并将这些事件存储在数据库中。

使用快照优化

使用时间溯源的方式存储数据库时,一个聚合可能会产生大量的事件,随着时间的推移,重放事件的成本会越来越高。

优化的方法就是定期的存储聚合状态的快照,然后通过加载快照,以及重放快照之后的事件来达到快速重放事件。

image-20230522223919695

事务脚本模式

对于简单的业务逻辑,可以使用事务脚本模式,事务脚本模式是面向过程而不是面向对象的。事务脚本模式就是,对于每一种系统操作,都有一个方法专门用于实现这个系统操作,使用数据库访问对象(Dao)访问数据库,而数据对象是纯数据几乎没有行为。

事务脚本模式的重要特征就是实现行为的类于存储状态的类是分开的。

image-20230521212736373

领域模型模式

对于复杂的业务,就需要使用领域模型模式了,领域模型模式是面向对象的。领域模型模式将业务逻辑组织为具有状态和行为的类构成的对象模型。这样的设计中,有些类只有状态或者行为,但是很多类同时包含状态和行为。

这样设计的好处是:

  • 易于理解和维护。它不像事务脚本模式由一个完成所有事情的大类组成,而是由许多小类组成,每个小类都有少量的职责。
  • 更容易测试。每一个对象都能够被独立的测试。
  • 更容易扩展。通常而言,面向对象的设计可以使用设计模式,这样提供了易于扩展的可能性。

image-20230521213659409

领域驱动设计

领域模型模式需要用领域驱动设计(DDD)来优化,DDD 是对面向对象设计的改进,是开发复杂业务逻辑的一种方法。在领域驱动设计中,由以下几种基本元素组成:

  • Entity:具有状态的对象。
  • Value Object:不具有状态的对象,仅有值,具有相同的值对象可以互换使用。
  • Factory:复杂实现对象创建的方法或者对象。
  • Repository:用来访问持久化实体的对象,Repository 也封装了访问数据库的底层机制。
  • Service:实现业务逻辑的对象。

领域驱动设计聚合模式

传统的领域模型缺少明确的边界,这会导致一些问题。所以使用领域驱动设计中的聚合模式,聚合具有明确的边界。

聚合是一个边界内领域对象的集群,可以将其视为一个单元。使用聚合有两个好处:

  • 消除了服务之间的对象引用。
  • 确保了本地事务 ACID 在每一个服务内部(对于服务之间的事务性保证,则通过 Saga 实现)。

image-20230522140855911

所有的操作都作用于整个聚合而不是部分聚合。

聚合的规则

领域驱动设计要求聚合遵守一组规则:

  • 只引用聚合根:聚合根(Aggregate Root)是聚合中唯一可以由外部类引用的部分,客户端只能调用聚合根上的方法来更新聚合。
  • 聚合间的引用必须使用主键:聚合间的引用使用主键(如唯一 ID,可以看成是外键),而不是对象引用。它确保聚合之间的边界得到了良好的定义,不会出现跨服务的对象引用问题
  • 一个事务中,只能创建或者更新一个聚合:确保单个事务的范围不超过服务的边界,使用 Saga 可以解决创建或者更新多个聚合。
阅读全文 »

Saga 分布式事务

Saga 是一种分布式事务实现方式,Saga 通过异步消息来协调一系列的本地事务,从而维护多个服务之间的数据一致性。

异步消息的一个重要好处就是确保 Saga 的所有步骤都被执行,即使一个或者多个 Saga 的参与方暂时不可用。

Saga 缺少了 ACID 事务中的隔离性。此外,因为每个本地事务都提交了更改,必须使用补偿事务(只有修改操作有补偿事务)回滚 Saga。

在 Saga 的一系列的步骤中,若前 N 个步骤后面跟着的步骤可能失败,称为可补偿性事务。第 N+1 个步骤后面全部都是不可能失败的步骤,称 N+1 步骤为关键性事务。关键性之后的步骤称之为可重复性事务

Saga 的协调模式

Saga 由一些列本地事务组成,如何协调 Saga 中一系列的本地事务进行下一步操作还是执行补偿事务尤为关键,总共有两种方式:

  • 协同式:把 Saga 的决策和执行顺序分布在 Saga 的每一个参与方中,它们通过交换事件的方式进行沟通。
  • 编排式:将 Saga 的决策和执行顺序集中在一个集中的编排器中,编排器发送消息给各个Saga 参与方。
阅读全文 »

微服务架构的进程间通信概述

交互方式

交互方式有两个维度来表示:一对一和一对多,同步和异步。

一对一 一对多
同步 请求/响应
异步 异步请求/响应;单向通知 发布/订阅;发布/异步响应
  • 一对一的交互方式:
    • 请求/响应:客户端发起请求,阻塞等待服务器的响应。这样的方式会导致服务的紧耦合。
    • 异步请求/响应:客户端发起,服务端异步响应,客户端在等待时不会阻塞。
    • 单向通知:客户端请求发送到服务端,不期望服务端做出任何响应。
  • 一对多的交互方式:
    • 发布/订阅:客户端发布通知消息,被零个或者多个服务端订阅。
    • 发布/异步响应:客户端发布请求消息,等待从感兴趣的服务发回的响应。

每个服务通常是上述交互方式的组合

服务 API

API 定义

在微服务架构中,使用接口定义语言(IDL)精确定义服务的 API 接口。

IDL 是客户端和服务端之间的契约,一个良好的 API 定义暴露有用功能的同时隐藏了内部实现的细节。

API 的演化

服务的 API 很少一成不变,它可能会随着时间的推移而发展。

在微服务架构中变更 API 是困难的,因为服务 API 的调用方无法控制 API 的实现方式,而 服务 API 的提供方无法控制客户端与服务端的版本保持一致。所以需要对服务 API 进行版本控制

语义化版本控制要求版本号由三部分组成:Major.Minor.Patch

  • Major:可能对之前的版本不兼容。
  • Minor:进行后向兼容的增强。
  • Patch:进行后向兼容的错误修复。

消息的格式

基于文本的消息格式

基于文本的消息格式,如 Json 和 XML。

这种消息格式的好处是:可读性高、后向兼容。而弊端在于消息冗长、传输效率低。

基于二进制的消息格式

基于二进制的消息格式,如 Protobuf,Thrift 和 Avro。

  • Protobuf:
    • 优点:时间和空间效率都不错,支持前向兼容(新加字段采用默认值)和后向兼容(忽略新加的字段),支持多语言。基于 HTTP 2.0。
    • 缺点:可读性差,不支持动态生成消息类型。
  • Thrift:
    • 优点:不仅支持二进制也支持文本类型的消息格式(支持多种序列化方式),支持多种传输协议,支持多语言,支持同步和异步通信。可以直接基于 TCP,省去了额外应用层开销。
    • 缺点:不支持动态生成消息类型。
  • Arvo:
    • 优点:在握手阶段交换模式定义(支持动态生成消息类型)。
    • 缺点:语言支持不丰富,在解析消息之前需要知道它的格式不支持后向兼容。
阅读全文 »

如何定义一个微服务架构

定义一个应用程序的微服务架构分为三步式流程,注意这些只是一个大概的方法。

  • 第一步,定义系统操作。系统操作指的是应用程序必须处理的请求的一种抽象描述,所以这一步就是将需求提炼为各种关键请求。
  • 第二步,定义服务。就是如何分解服务。
  • 第三步,定义服务 API 和协作方式。确定每一个服务的 API,将第一步中标识的系统操作分配给服务,服务可以独立的实现操作,也可能需要与其他服务进行写作。

image-20230517172041533

阅读全文 »

微服务架构

应用程序的扩展方式

在解释什么是微服务架构之前,先看看一个应用程序的三种扩展方式:

  • X 轴扩展:又称为水平复制,通过克隆实例再加上负载均衡器进行的扩展方法,负载均衡器在 N 个相同的实例之间分配请求。

image-20230517144048395

  • Z 轴扩展:又称为数据分区,也需要运行单体应用程序的多个实例,但是每一个实例仅负责数据的一个子集。路由器根据请求中的特定属性将请求路由到特定的实例中去。

image-20230517144336551

  • Y 轴扩展:又称为功能性分解,把单体应用拆解为了一组服务,每一种服务都负责特定的功能。服务可以在需要的时候再进行 X 轴或者 Z 轴扩展

image-20230517144500811

微服务架构定义

所以,微服务架构就是把应用程序根据功能分解为一组服务的架构风格。每一种服务都是由一组专注的、内聚的功能职责组成。

微服务架构就是将服务作为模块化的单元。你只能通过 API 去访问服务,而无法越过 API 去访问服务内部的类。这使得服务可以独立的进行部署和扩展。

微服务架构的一个关键的特性就是每一种服务之间都是松耦合的,它们仅仅通过 API 进行通信。而为了实现这一种松耦合,每一个服务都拥有自己的私有数据库。这样对于一个服务,可以自由的修改数据库表的 Schema,而不必与其他服务进行协调。服务不会因为其他服务锁住了数据库而进入了堵塞状态。

微服务架构 vs SOA

SOA(Service Oriented Architecture)指的是面向服务的架构,SOA 也是进行了服务划分,它与微服务架构看似相同,但是深入研究就会发现二者的巨大差异。

  • 服务间的通信:SOA 常吃采用重量级的技术,如 ESB(Enterprise Service Bus)、SOAP 和 WebService。而微服务架构倾向于使用轻量级的技术,如 REST 或者 gRPC 这类轻量级的协议。
  • 数据管理:SOA 一般有一个全局的数据库。而微服务与之相反,每一个服务都有一个自己的私有数据模型。
  • 服务规模:SOA 应用通常包含若干个大型的服务。而微服务架构则常常由属实个甚至上百个更小的服务组成。
阅读全文 »

Pod

Pod 是可以在 Kubernetes 中创建和管理的、最小的可部署的计算单元,是一组容器,这些容器共享存储、网络(容器间可以使用 localhost 相互通信)、以及怎样运行这些容器的声明。有些 Pod 具有 Init 容器和应用容器,Init 容器会在启动应用容器之前运行并完成。

如下面是一个 Nginx Pod 示例:

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80

通常情况下,使用 Deployment、ReplicaSet 或者 Job 这类工作负载来创建 Pod,如果 Pod 需要跟踪状态,可以考虑 StatefulSet 资源。

阅读全文 »

Finalizers

带有 Finalizers 的对象删除流程如下:

  • 对带有 Finalizer 的对象的第一个删除请求会为其 metadata.deletionTimestamp 设置一个值(标记要删除的对象),但不会真的删除对象。并且 API 服务会禁止对象被删除,直到其 metadata.finalizers 字段为空。返回 202 状态码。
  • 此时控制平面会通过轮询对该对象的更新请求来执行它们所要处理的所有 Finalizer。metadata.deletionGracePeriodSeconds 的取值控制对更新的轮询周期。
  • 当所有 Finalizer 都被执行过,资源被删除。每执行完一个就从 finalizers 中移除一个,直到 finalizers 为空,之后其宿主资源才会被真正的删除。

一个常见的 Finalizer 的例子是 kubernetes.io/pv-protection, 它用来防止意外删除 PersistentVolume 对象。 当一个 PersistentVolume 对象被 Pod 使用时, Kubernetes 会添加 pv-protection Finalizer。 如果你试图删除 PersistentVolume,它将进入 Terminating 状态, 但是控制器因为该 Finalizer 存在而无法删除该资源。 当 Pod 停止使用 PersistentVolume 时, Kubernetes 清除 pv-protection Finalizer,控制器就会删除该卷。

属主与附属

属主(Owner)和附属(Dependent)用于描述 Kubernetes 对象之间的关系。

属主引用

附属对象有一个 metadata.ownerReferences 字段,用于引用其属主对象。一个有效的属主引用, 包含与附属对象同在一个命名空间下的对象名称一个 UID。Kubernetes 自动为一些对象的附属资源设置属主引用的值, 这些对象包含 ReplicaSet、DaemonSet、Deployment、Job、CronJob、ReplicationController 等。

附属对象还有一个 ownerReferences.blockOwnerDeletion 字段,该字段使用布尔值, 用于控制特定的附属对象是否可以阻止垃圾收集删除其属主对象。

k8s 不允许跨越命名空间指定属主。

属主关系与 Finalizer

在前台删除中,会添加 foreground Finalizer,这样控制器必须在删除了拥有 ownerReferences.blockOwnerDeletion=true 的附属资源后,才能删除属主对象。 如果你指定了孤立删除策略,Kubernetes 会添加 orphan Finalizer, 这样控制器在删除属主对象后,会忽略附属资源。

命名空间

在 k8s 中,命名空间(Namespace)提供一种机制,将同一集群中的资源划分为相互隔离的组。 同一名字空间内的资源名称要唯一,但跨名字空间时没有这个要求。名字空间作用域仅针对带有名字空间的对象,作用域对集群范围的对象不适用。

大多数 Kubernetes 资源(如 Pod、Service 等)都在某些命名空间中,但是名字空间资源本身并不在名字空间中

而且底层资源,如节点和持久化卷,不属于任何命名空间

查看 Kubernetes 资源哪些在命名空间中,哪些不在:

1
2
3
4
5
# 位于名字空间中的资源
kubectl api-resources --namespaced=true

# 不在名字空间中的资源
kubectl api-resources --namespaced=false

初始命名空间

Kubernetes 启动时会创建四个初始命名空间:

  • default:默认的命名空间,以便无需创建新的命名空间就可以使用集群。
  • kube-node-lease:包含用于与各个节点关联的租约对象。节点租约允许 Kubelet 发送心跳, 由此控制面能够检测到节点故障。
  • kube-public:所有的客户端都可以读取该命名空间。这个命名空间主要预留为集群使用,以便某些资源需要在整个集群中可见可读。
  • kube-system:用于 Kubernetes 系统创建的对象。

命名空间和 DNS

创建一个服务时,Kubernetes 会创建一个相应的 DNS 条目

条目的内容为 <服务名称>.<名字空间名称>.svc.cluster.local,这意味着对于本地命名空间的服务,可以只使用 <服务名称> 就可以访问到。而对于不同的命名空间,则需要使用使用完全限定域名(FQDN)。

注解

使用 Kubernetes 注解为对象附加任意的非标识的元数据。客户端程序能够获取这些元数据信息。

标签可以用来选择对象和查找满足某些条件的对象集合。 相反,注解不用于标识和选择对象

1
2
3
4
metadata:
annotations:
key1: value1
key2: value2

对象名称和 ID

集群中每一个对象都有一个名称(name)来标识在同类资源中的唯一性,每一个对象也有一个 UID 来标识在整个集群中的唯一性。

名称

名称在同类资源中是唯一的。

当对象所代表的是一个物理实体(例如代表一台物理主机的 Node)时, 如果在 Node 对象未被删除并重建的条件下重新创建了同名的物理主机, 则 Kubernetes 会将新的主机看作是老的主机,这可能会带来某种不一致性。

UID

UID 是 k8s 系统生成的全局唯一标识符。

标签和选择符

标签是附加到 Kubernetes 对象上的键值对。

1
2
3
4
5
metadata:
name: demo
labels:
key1: value1
key2: value2

选择符

通过标签选择符,用户可以识别一组对象。

API 目前支持两种类型的选择算符:基于等值的基于集合的。 标签选择算符可以由逗号分隔的多个需求组成。 在多个需求的情况下,必须满足所有要求,因此逗号分隔符充当逻辑运算符。

基于等值的运算符

基于等值的运算符有三种 ===!=,前两者标识同义的相等,第三个是不相等。

基于集合的运算符

基于集合的运算符有三种 innotinexists

  • key in (v1, v2)
  • key:有 key 标签的对象。
  • !key:没有 key 标签的对象。
阅读全文 »