第05讲:什么是服务网格(Service Meh)

本课时我将带你学习“服务网格”的相关内容。

服务网格(Service Mesh)是随着 Kubernetes 和微服务架构的流行而出现的新技术,它的目的是解决微服务架构的服务之间相互调用时可能存在的各种问题微服务架构的服务之间采用进程间的通讯方式进行交互,比如 REST 或 gRPC 等。在第 01 课时介绍微服务架构的时候,我提到过影响微服务架构复杂度的一个重要因素就是微服务之间的相互调用,这使得应用需要对服务调用时产生的错误进行处理。比如,当调用一个服务出现超时错误时,应该进行重试;如果对某个服务的调用在一段时间内频繁出错,说明该服务可能已经崩溃或是负载过大,没有必要再继续进行尝试下去了。

除了错误处理之外,我们还可能需要对服务之间的调用添加一些策略,比如限制服务被调用的速率,或是添加安全相关的访问控制规则等。这些需求从服务之间的调用而来,并且所有微服务架构的应用都有同样的需求,这些横切的需求,应该由平台或工具来处理,而不需要应用来实现,应用要做的只是提供相关的配置即可。

在 Kubernetes 出现之前,微服务架构已经在很多企业内部得到了应用。同样的,在服务网格之前也有相似的工具来解决服务调用相关的问题,比如 Netflix OSS 栈中的 Hystrix,但服务网格技术是在已有工具上的升级,它提供了一个更完整的解决方案。

严格说来,服务网格并不直接依赖 Kubernetes,但绝大部分服务网格实现都支持 Kubernetes,有些实现甚至只支持 Kubernetes。这是因为 Kubernetes 平台提供的功能可以简化服务网格的使用。下面我来为你介绍 Kubernetes 中的边车模式(Sidecar)。

边车模式

在 Kubernetes 中,Pod 中的容器通常是紧密耦合的,它们共同完成应用的功能。如果需要实现横切功能,则需要在 Pod 中添加与应用无关的容器,这是因为横切功能的实现离不开对应用使用的存储和网络的访问,而 Pod 中的容器之间共享存储和网络。当我们把横切服务的容器添加到 Pod 中后,Pod中就多了与应用无关的容器,这种部署模式称为边车模式,这些容器被称为边车容器,下图是现实世界中的边车。

日志收集是边车模式的一个常见应用,它利用了 Pod 中容器共享存储的特性:应用容器往某个持久卷中写入日志,而日志收集工具的边车容器则监控同一个持久卷中的文件来读取日志。

边车容器在服务网格实现中至关重要。服务网格实现会在每个 Pod 上增加一个新的边车容器来作为其中应用服务的代理,这个容器的代理程序会作为外部调用者和实际服务提供者之间的桥梁。

如下图所示,Pod 某个端口上的请求,首先会被服务代理处理,然后再转发给实际的应用服务;同样的,应用服务对外的请求,也会先被服务代理处理,然后再转发给实际的接收者。代理边车容器的出现,为解决服务调用相关的问题提供了一种新的方案:服务调用的自动重试和断路器模式的实现,都可以由服务代理来完成,从而简化应用服务的实现。

如果仅从最基本的实现方式上来说,服务网格技术并不复杂。打个比方,如果一个 Pod 提供某个应用服务,只需要在该 Pod 中部署一个服务代理的边车容器,由该代理来处理应用容器发送和接收的数据,就实现了服务网格。

但是,服务网格实际上的解决方案非常复杂,我会在下面进行具体的介绍。

值得一提的是,边车模式并不是服务代理的唯一部署方式。有些服务网格实现可以在Kubernetes的节点上部署服务代理来处理该节点上的全部请求。

服务代理

服务代理是服务网格技术实现的核心,可以说,服务代理决定了服务网格能力的上限。从作用上来说,服务代理与我们所熟悉的 Nginx 和 HAProxy 这类代理并没有太大区别。实际上, Nginx 和 HAProxy 同样可以作为服务代理来使用,但服务网格通常使用专门为服务间调用开发的服务代理实现。在下图所示的 OSI 七层模型中,服务代理一般工作在第 3/4 层和第 7 层。

下表列出了常见的服务代理,其中 Envoy、Traefix 和 Linkerd 2 都是新出现的服务代理实现。

服务发出和接收的所有调用都需要经过服务代理。服务代理的功能都与服务之间的调用相关,其主要方面如下表所示。

代理可以在请求层上工作。当服务 A 调用服务 B 时,服务 A 的代理可以使用负载均衡来动态选择实际调用的服务 B 实例,如果对服务 B 的调用失败,并且该调用是幂等的,则代理可以自动进行重试。服务 A 的代理还可以记录与调用相关的指标数据,服务 B 的代理可以根据访问控制的策略决定是否允许该请求,如果服务 B 当前所接收的请求过多,那么它的代理可以拒绝其中某些请求。

代理同样可以工作在连接层,服务 A 和服务 B 的代理之间可以建立 TLS 连接,并验证对方的身份。

由于服务代理需要处理服务所有接收和发送的请求,这对服务代理的性能要求很高,不能增加过长的延迟,这也是 Envoy 等服务代理流行的原因,这些新开发的服务代理对服务之间的调用进行了优化。除了性能之外,服务代理只占用很少的 CPU 和内存资源,这是因为每个服务实例的 Pod 上都可能运行着一个服务代理的容器,当服务数量增加时,服务代理自身的资源开销也会增加。

服务网格

服务网格技术起源于 Linkerd 项目,从架构上来说,服务网格的实现很简单,它由服务代理和管理进程组成。服务代理称为服务网格的数据平面(Data Plane),负责拦截服务之间的调用并进行处理;管理进程称为服务网格的控制平面(Control Plane),负责协调代理并提供 API 来管理和监控服务网格。服务网格的能力由这两个平面的能力共同决定。

下图给出了服务网格的基本架构。

服务网格在数据平面的处理能力取决于所使用的服务代理,而服务网格实现通常使用已有的服务代理,因此它们在数据平面方面的能力差别并不大。服务网格实现的价值更多来源于它所提供的控制平面,比如,服务网格实现是否提供了 API 来更新配置,是否提供了图形化界面来查看服务状态,在 Kubernetes 上,是否可以使用自定义资源定义(Custom Resource Definition,CRD)来进行声明式配置。

服务网格技术的优势有以下几个方面。

第 1 个优势在于它与服务实现使用的技术栈无关。服务代理工作在服务调用这个层次上。不论服务采用什么编程语言或框架来实现,服务代理都可以产生作用。Kubernetes 的流行,使得在微服务架构实现中使用多语言开发变得更简单。一个微服务应用的不同服务可以使用完全不同的技术栈来实现,这些服务之间的调用都可以由服务代理来处理。

第 2 个优势在于服务网格技术与应用代码是解耦的,这意味着当我们需要对服务调用相关的策略进行调整时,并不需要修改应用的代码。以服务的访问频率为例,当需要控制对某个服务的调用频率时,可以通过服务网格的控制平面提供的 API 直接进行修改,并不需要对应用做任何改动。这种解耦使得服务网格成为应用运行平台所提供的能力之一,进而促成了新的开源项目和商业产品的出现。

对于大型项目,可以由专门的团队来负责管理服务网格的配置,进行更新和日常维护;对于小型项目,可以从开源社区选择合适的产品。

服务网格功能

服务网格所能提供的功能非常多。每个服务网格实现所提供的功能也各有不同。下面我将对服务网格中的重要功能进行介绍。

自动代理注入

为了使用服务网格提供的功能,应用服务的 Pod 需要添加服务代理容器,服务网格提供了自动的代理注入机制。在 Kubernetes 上,如果 Pod 或控制器对象中添加了某个特定的注解,则服务网格可以自动在 Pod 中添加服务代理容器并完成相关的配置。

流量管理

流量管理指的是管理服务之间的相互调用,由一系列的子功能组成。

(1)服务发现

服务发现指的是发现系统中存在的服务及其对应的访问地址,服务网格会在内部维护一个注册表,包含所有发现的服务及其对应的服务端点。

(2)负载均衡

每个服务通常都有多个运行的实例,在进行调用时,需要根据某些策略选择处理请求的实例。负载均衡的算法可以很简单,比如循环制(round robin);也可以很复杂,比如根据被调用服务的各个实例的负载情况来动态选择。

(3)流量控制

微服务架构的应用强调持续集成和持续部署,应用的每个服务都可以被单独部署。一个常见的需求是在进行更新时,让小部分用户使用新的版本,而大部分用户仍然使用当前的旧版本,这样的更新方式称为金丝雀部署(Canary Deployment)。为了支持这样的更新方式,我们可以同时部署服务的两个版本,并通过服务网格把调用请求分配到两个版本,比如,20% 的请求分配到新版本,剩下 80% 的请求分配到当前版本,经过一段时间的测试之后,再逐步把更多的请求分配到新版本,直到全部请求分配至新版本。

(4)超时处理

服务网格对服务调用添加了超时处理机制。如果调用在设置的时间之后仍然没有返回,则会直接出错,这样就避免了在被调用的服务出现问题时,进行不必要的等待。不过,超时时间也不能设置得过短,否则会有大量相对耗时的调用产生不必要的错误,针对这一点,服务网格提供了基于配置的方式来调整服务的超时时间。

(5)重试

当服务的调用出现错误时,服务网格可以选择进行重试,服务重试看似简单,但要正确的实现并不容易。简单的重试策略,比如固定时间间隔和最大重试次数的做法,很容易产生重试风暴(Retry Storm)。如果某些请求因为服务负载的原因而失败,简单的重试策略会在固定的时间间隔之后,重试全部失败请求,这些请求在重试时又会因为负载过大的原因而再次失败。所造成的结果就是产生大量失败的重试请求,影响整体的性能,有效的重试机制应该避免出现重试风暴。

(6)断路器

断路器(Circuit Breaker)是微服务架构中的一种常见模式。通过断路器,可以在服务的每个实例上设置限制,比如同时允许的最大连接数量,或是调用失败的次数。当设定的限制达到时,断路器会自动断开,禁止对该实例的连接。

断路器的存在,使得服务调用可以快速失败,而不用尝试连接一个已经失败或过载的实例,所以它的一个重要作用是避免服务的级联失败。如果一个服务出现错误,可能导致它的调用者因为超时而积压很多未处理的请求,进而导致它的调用者也由于负载过大而崩溃,这样的级联效应,有可能导致整个应用的崩溃。使用断路器之后,出现错误的服务实例被自动隔离,不会影响系统中的其他服务。

(7)错误注入

在使用服务网格配置了服务的错误处理策略之后,一个重要的需求是对这些策略进行测试。错误注入指的是往系统中引入错误来测试应用的故障恢复能力,比如,错误注入可以在服务调用时自动添加延迟,或是直接返回错误给调用者。

安全

安全相关的功能解决应用的 3 个 A 需求,分别是认证(Authentication)、授权(Authorization)和审计(Audit)。这3个需求的英文名称都以字母A开头,所以称为3个A需求。

双向 TLS(mutual TLS,mTLS)指的是在服务调用者和被调用者的服务代理之间建立双向 TLS 连接,这个连接意味着客户端和服务器都需要认证对方的身份。通过 TLS 连接可以对通信进行加密,防止中间人攻击。

用户认证:服务网格应该可以和不同的用户认证服务进行集成,常用的认证方式包括 JWT 令牌认证,以及与 OpenID Connect 提供者进行集成。

访问策略

访问策略用来描述服务调用时的策略。

  • 访问速率控制:通过访问速率控制,可以限制服务的调用速度,防止服务因请求过多而崩溃。

  • 服务访问控制:服务访问控制用来限制对服务的访问,限制的方式包括禁止服务、黑名单和白名单等。

可观察性

服务网格可以收集与服务之间通信相关的遥测数据,这些数据使得运维人员可以观察服务的行为,发现服务可能存在的问题,并对服务进行优化。

性能指标:是指服务网格收集与服务调用相关的性能指标数据,包括延迟、访问量、错误和饱和度。除此之外,服务网格还收集与自身的控制平面相关的数据。

分布式追踪:可以查看单个请求在服务网格中的处理流程,在微服务架构中,应用接收到的请求可能由多个服务协同处理。在请求延迟过高时,需要查看请求在不同服务之间的调用流程,以及每个服务所带来的延迟。分布式追踪是服务网格提供的工具,可以用来收集相关的调用信息。

访问日志:用来记录每个服务实例所接收到的请求。

用户界面

服务网格提供图形化的用户界面,可以查看服务相关的信息,包括收集的性能指标数据、服务调用关系的拓扑图。

服务网格产品介绍

目前有不少开源的服务网格产品,下面将着重对 Istio、Linkerd 和 Maesh 进行介绍。

Istio

Istio 项目由 Google、IBM 和 Lyft 共同发起。由于有大公司的支持,Istio 项目目前所提供的功能是最完备的,这也意味着 Istio 是最复杂的。Istio 所包含的组件非常多,对应的配置也非常复杂,它的学习曲线很陡,上手并不容易。值得一提的是,Lyft 的 Envoy 团队与 Istio 有很好的合作,这就保证了 Istio 有最好的 Envoy 支持。本专栏将使用 Istio 来作为服务网格的实现。

Linkerd

Linkerd 是最早的服务网格实现,目前作为 CNCF 的项目来开发。相对 Istio 而言,Linkerd 提供的功能较少,但是也更简单易用。对很多应用来说,Linkerd 所提供的功能已经足够好。

Maesh

Maesh 是 Containous 提供的服务网格实现。Maesh 使用 Traefik 作为服务代理。相对于 Istio 和 Linkerd,Maesh 还是一个比较新的项目,需要更多的时间来考察。

总结

服务网格技术在本专栏的微服务架构解决方案中非常重要,它解决了服务调用的相关问题。本课时首先介绍了Kubernetes上的边车模式,接着对服务代理和服务网格进行了介绍,然后介绍了服务网格所能提供的功能,最后对常见的服务网格产品进行了介绍。


第06讲:示例应用介绍与用户场景分析

本课时我将带你学习“示例应用与用户场景分析”。

作为云原生微服务应用开发的实战课程,其中最重要的就是课程中所使用的实战项目了。实战项目是贯穿整个课程的主轴,一个好的实战项目应该是真实的、完备的和易懂的,这也是本课程与其他课程的主要区别之一。

项目的真实性是指项目来源于实际存在的需求,而不是虚构的,真实性意味着可以把实战项目中所学到的知识直接应用到实际的项目开发中。

完备性意味着项目的实现是完整的,案例中涵盖主要的用户场景,并且不会对项目的场景和实现做过多的简化,过多的简化会使得项目的实现与实际相差太远,起不到实战的效果。实战项目并不是玩具项目(Toy Project),有些课程会使用多个小项目来作为示例,这种做法的问题在于每个小项目都只包含了完整项目的一部分,没办法得到项目的全貌。

易懂意味着项目在实现中并不涉及复杂的业务逻辑,只需要常识就可以对应用的场景进行分析了。有些应用,比如银行和保险等,有着相对复杂的业务逻辑。如果选用这样的案例作为示例,则需要花费比较大的篇幅来解释相关的业务逻辑。因此,最好选择业务逻辑简单易懂的应用。

基于上述这三个原则,本专栏选择了叫车服务来作为案例,开发的是一个类似滴滴打车和优步的叫车服务,称为快乐出行,叫车服务的示例应用符合上述的三个原则。叫车服务是实际存在的应用,来源于真实的需求。大部分人都使用过叫车服务 App,对于其中的使用场景比较熟悉,并且该示例应用实现了叫车服务相关的重要用户场景。

下面对示例应用的重要用户场景进行介绍。

用户场景

快乐出行应用的用户场景是围绕叫车这一核心业务展开的,除了叫车这一核心业务之外,还包括其他相关的辅助业务。

叫车服务

叫车动作由乘客发起,当乘客在叫车时,需要提供行程的起始地址和结束地址,地址以关键字搜索的方式提供。

在行程创建之后,系统会在起始地址附近的特定范围内,搜索当前可用车辆。当找到可用车辆时,系统会发送通知给司机。

司机接收到新行程创建的通知之后,可以选择接受行程,在司机发出接受行程的请求之后,系统会从所有愿意接受行程的司机中选择一个。选择司机的算法有很多,简单的选择算法是采用先到先得的策略,谁先接受行程,谁就被选中;复杂的算法可以设定一个等待的时间段,在这个时间段内应答的司机,选中距离最近的司机。

被选中的司机会获取到乘客的相关信息,以方便联系乘客。在接到乘客之后,司机开始行程;当达到目的地之后,行程结束。

乘客管理

乘客管理负责维护乘客相关的信息,乘客的基本信息包括乘客的姓名、Email 地址、联系电话等。典型的场景包括乘客注册和乘客信息更新。

除了乘客的基本信息之外,乘客还可以添加常用的地址,比如家庭住址和工作单位地址等,这些地址可以帮助乘客快速选择行程的起始和结束位置。

乘客在系统中有自己的状态,有些乘客的账号可能因为不恰当的行为,暂时被系统封禁,或处于受限状态。被封禁的乘客账号无法创建行程,而受限的账号则限制了可以创建的行程长度和金额,比如,受限账号不能创建长度超过 20 公里,或是金额超过 100 元的行程。

司机管理

司机管理负责维护司机相关的信息,司机的基本信息包括司机的姓名、Email 地址、联系电话等。典型的场景包括司机注册和司机信息更新。

司机管理的另外一个重要功能是管理司机的车辆,车辆的基本信息包括车辆的厂商、型号、出厂日期和牌照号码等。正规的叫车服务对司机有严格的背景审核流程,这些审核流程是在线下进行的,在示例应用中,由于数据都是模拟的,并没有必要进行审核,因此这样的流程被省略了,而是改为直接在模拟数据中设置不同的状态。

另外,每个司机有不同的状态,没有开始运营的司机处于离线状态;已经运营且没有载客的司机处于可用状态;已经运营且已经载客的司机处于不可用状态。

地址管理

乘客通过输入地址关键字的方式来查询行程的起始位置和目标位置,这时就需要根据用户的输入进行地址查询,并返回对应的地理位置坐标,这个步骤通常需要使用已有的地址数据库或利用第三方提供的相关服务。示例应用使用从 GitHub 下载的中国五级地址数据库来进行简单的地址查询。

行程管理

乘客和司机都需要查询历史行程的信息,所有的历史行程都会被保存,行程所包含的信息包括起始位置、结束位置、乘客标识符、司机标识符和状态等。乘客和司机可以查看与他们相关的行程。

支付服务

在实际的叫车服务中,乘客需要使用第三方支付服务,比如支付宝和微信,来完成行程的费用支付。作为示例应用来说,如何与这些第三方支付服务集成,并不是需要关注的重点,因为这通常是乘客 App 需要完成的功能,对于示例应用的后台服务来说,只需要知道支付的结果即可。

系统组件

示例应用最重要的特征是真实和完备,本专栏的重点是使用微服务架构的后台服务。然而在实际的叫车服务中,除了后台服务之外,还需要有客户端和其他辅助工具的支持。为了保证真实性,同时又不偏离本专栏的主题,示例应用中包含了如下组件。

后台服务

后台服务是本专栏的核心内容,由一系列微服务组成,这些微服务有各自独立的业务能力,同时又互相协作来完成某些业务逻辑。不同微服务所使用的数据存储和支撑服务也不尽相同,比如,乘客管理相关的服务使用关系型数据库作为存储;而叫车相关的服务则使用 Redis 来存储司机的位置;服务则以 REST 或 gRPC 的方式对外提供开放 API。这些服务所提供的 API 可以通过 API 网关来访问。

乘客管理界面

乘客管理界面提供了对乘客的基本管理功能,除了创建乘客之外,还可以为乘客创建行程。

乘客管理界面替代了叫车服务中乘客使用的 App,可以管理所有乘客。

司机模拟器管理界面

司机模拟器用来模拟司机的行为,其作用是替代叫车服务中司机使用的 App。每个模拟器都表示一个司机,模拟器表示的司机会从某个地理位置开始,以随机的速度行驶,并随机改变行驶方向,这个过程中模拟器会定期通知系统它的当前位置,通过管理界面还可以控制每个模拟器的状态。

当行程创建之后,可作为接受行程备选的模拟器都会接收到通知,可以在界面上选中某个模拟器接受行程,从而完成整个叫车的流程。

简化的应用实现

一个完整的打车应用所涵盖的内容很广泛,除了后台服务之外,还包括乘客和司机使用的 App 等。在实现中,除了应用本身的服务之外,还需要集成一些第三方的服务,比如地理位置服务和支付服务等。示例应用不可能完全复刻一个滴滴打车或是优步,因此在实现的功能上必须有所取舍。

在一方面,由于有些组件与后台服务的具体实现无关(比如乘客和司机使用的 App),并且这些组件的实现超出了本专栏的内容范围。但是为了保证示例的完整性,在实现中我们将使用简化的 Web 应用来替代这两个 App。

最后,由于不同功能在实现方式上的重复性,很多功能在实现时的原理其实是相通的。在完成某个功能的实现之后,相关的经验可以应用在相似的功能实现上,并不需要重复介绍,比如,有几个服务都需要使用关系型数据库来存储数据,相关的实现技术只需要介绍一次就可以了。虽然对于相关实现方式的介绍不会重复出现,在示例代码中仍然包含了全部完整的实现。

总结

示例应用在本专栏中扮演了重要的角色,并会贯穿专栏的各个课时。本课时对示例应用的主要场景进行了介绍,这些场景是示例应用需要满足的需求;接着我们简要介绍了示例应用中的组件;最后介绍了示例应用在实现时的一些简化方式。


第07讲:如何进行领域驱动设计

领域驱动设计(Domain-Driven Design,DDD)这个词,可能一部分人听说过,也有一部分人觉得比较陌生。自从 Eric Evans 在其著名的《领域驱动设计 - 软件核心复杂性应对之道》一书中,提出了领域驱动设计的概念之后,领域驱动设计的思想在开发社区得到了广泛的流行和应用,很多软件开发的布道者开始推广领域驱动设计的思想。

之所以会在本专栏中提到领域驱动设计,是因为领域驱动设计在微服务架构中有着它独特的应用,尤其是在划分微服务和定义微服务的交互方式时。要在一个课时中完整的介绍领域驱动设计,显然是不现实的,本课时将着重介绍领域驱动设计中的基本概念,下一课时将介绍领域驱动设计在微服务架构中的应用。

领域和子领域

领域驱动设计是一种软件设计的方法学,以领域作为设计的起点和驱动力,这里的核心关键词是领域。软件系统存在的价值在于帮助提升现实的业务,如果不能帮助现实的业务取得成功,那么设计再好的软件系统,也是无用的。一个软件系统的好坏,不在于它是否使用了最新的技术,也不在于它的开发流程多么的规范,而在于它能否解决业务中存在的问题。一个软件系统所工作的业务范畴,就是它的领域。从领域中,我们可以知道现实世界中的业务是如何进行的,而软件系统可以如何提供帮助来提升业务。

领域本身是一个很宽泛的概念。真实的软件系统所工作的领域通常都很大,比如,银行系统、保险系统、电子商务应用、叫车应用和外卖应用,它们所涉及的业务领域都庞大而复杂。而在实际操作中,通常把整个业务领域划分成多个子领域(Subdomain)。在实际的业务中,这通常与公司的不同部门相对应。

比如,一个电子商务应用的领域,可以被划分成产品目录、订单处理、付款处理、库存管理和送货服务等子领域,不同的子领域在业务中的重要程度不尽相同。核心领域(Core Domain)指的是业务领域中最核心和最重要的部分,也是软件系统开发中最需要关注的部分。支撑子领域(Supporting Subdomain)和通用子领域(Generic Subdomain)都是业务系统中必不可少的部分,但并不是核心。两者的区别在于,支撑子领域包含与业务相关的内容,而通用子领域则完全与业务无关。下图给出了电子商务应用的领域中的子领域的示例。

除了核心领域之外,其他子领域可以由外部系统来实现。以电子商务应用中的子领域为例,库存管理、付款处理和送货服务等子领域都可以集成外部系统,产品目录和订单处理都是支撑子领域。那核心领域是什么?这其实是在软件系统开发之前要解决的核心问题,也是这个系统的卖点。电子商务应用的核心领域应该是如何帮助客户提高销量,比如利用大数据技术预测产品的销售趋势等。只有聚焦在核心领域,软件系统才能取得成功。

模型

领域表示的是现实世界中的事物。如果要在软件开发中应用领域中的概念,则需要对领域进行抽象,也就是对领域建模,建模的结果是得到一个关于领域的模型。这个模型的获得,是一个迭代的过程,这个过程需要软件设计人员和业务人员的协同工作。软件设计人员通过与业务人员进行交流,发现领域中包含的概念,并把它们添加到模型中。在交流的过程中,模型不断的得到修正,最终得到的模型,是领域的一个真实映射。

模型源于领域,但是高于领域,是领域的提炼与升华。领域中无关的概念被剔除,只留下有价值的概念,以及概念的属性和操作。以电子商务应用为例,从领域中,我们可以提炼出客户、订单、产品、付款和送货等概念,每个概念有其相关的属性和操作。比如,客户的属性有姓名、Email 地址、联系电话和送货地址等,相关的操作有下订单、支付订单、退换货等。

模型可以采用不同的方式来表达,比如图片、图表、文档或是白板上的草图。模型的表达形式并不重要,重要的是模型所传递出来的思想。在经过辛勤的努力,得到了模型之后,需要交由开发团队来进行代码设计和实现,模型是代码设计和实现的基础。在设计和实现中,一个重要的目标是确保模型的完整性不被破坏。开发团队中人员众多,每个人对模型都有自己的理解,造成具体的实现可能偏离模型原本的定义。如果不加以控制,最终得到的代码,会无法反应出模型真实的面貌,从而无法满足业务的真正需求。领域驱动设计的本质是模型驱动设计,领域驱动设计提供了一些模式来帮助确保模型的完整性。

通用语言

提到领域驱动设计,就一定会提到通用语言(Ubiquitous Language),该含义其实并不复杂。前面提到了理解领域和从领域中提炼模型的重要性,这就需要去了解现实中的业务流程是如何工作的,以及软件系统如何能够帮助进行提升。谁最了解现实中的业务?当然是领域专家和业务相关的人员。软件设计人员需要与业务人员进行充分的交流来理解领域和提炼模型,那交流的时候应该使用什么语言呢?业务人员有自己的行话和术语,软件设计人员则倾向于从代码实现的角度去理解问题。

为了避免沟通上的误解,一个团队应该形成自己的通用语言,团队中的所有人都使用这个语言来交流。随着对于领域的理解的深入,这个语言也在交流中不断的调整,通用语言和模型之间关系密切,模型实际上是通用语言的骨干,当团队中的所有人都用通用语言进行交流时,就可以避免不必要的混淆和误解。

界定的上下文

界定的上下文(Bounded Context)是领域驱动设计中非常重要的概念。它反映了模型的一个重要特征,那就是模型只有在特定的上下文中才有意义。这个论断,明确指出了在传统的软件设计中经常会存在的一个误区,那就是设计出大而全的模型,而忽略模型所应用的上下文。

以电子商务应用为例,客户是模型中的一个基本概念,但是客户这个概念,在不同的上下文中的含义是不同的。当客户在浏览产品目录时,我们关注的是该客户的历史购买记录,以方便推荐合适的产品;当客户下订单时,我们关注的是该客户的支付方式和收货地址;当给客户送货时,客户的概念变得不再重要,只留下了收货人的地址和联系方式。在传统的面向对象设计方式中,在不同上下文中会共用一个客户类,所造成的结果就是不同上下文之间出现了紧密耦合关系。随着代码的更新,这个公用类会变得非常臃肿,另外,对这个公用类的修改,则需要多个小团队之间的协调。

界定的上下文指的是模型存在的边界,在这个边界之内,通用语言中的术语都有特定的含义。在电子商务应用中,同样是客户这样一个术语,在不同的界定上下文中的含义是不同的。当每次提到客户时,都是在特定的上下文中,比如产品目录上下文中的客户,和订单处理上下文中的客户,虽然名字一样,但是含义却不相同。工作在不同的上下文中的团队成员,都清楚的了解客户所代表的含义,客户不再是一个大而全的概念,在不同的上下文中变得具体和简洁。

在下图中,左侧是单一的客户概念,可以看到其中包含了很多的属性;右侧是不同界定的上下文中的模型。虽然这些模型中都有名为客户的概念,但是它们的含义和包含的属性是不同的,有些属性虽然重复出现,但是这样的划分是很有必要的。比如,在订单处理上下文中,客户有一个属性是收货地址列表,因为客户在下订单时可以从地址列表中进行选择;而送货服务上下文中,客户则只有一个收货地址,也就是实际要派送的地址。经过这样的划分之后,每个模型中的概念更加的清晰和具体。

模型中的元素

领域驱动设计中说明了模型中可能存在的不同元素。

分层架构

为了防止领域相关的逻辑散落在应用的不同部分,应该使用分层架构,每个层次都是高内聚的。下表给出了领域驱动设计推荐的 4 个层次,按照从上到下的方式出现。

严格来说,每个层次都应该只与直接在它下面的那一层进行交互,不过,这样的限制在具体的实现中可能过于严格,可以适当放松。

下图给出了这 4 个层次之间的交互关系,在上面的层次可以访问下面的所有层次。

实体

提到实体(Entity),很多人会联系到 JPA 和 Hibernate 中的实体。领域驱动设计中的实体,与 JPA 中的实体并没有实际上的关联。实体指的是一类特殊的对象,这类对象有唯一的标识符,并且在其生命周期中,对象的标识符保持不变。对于实体来说,重要的不是其中包含的属性,而是其标识符。实体之间通过标识符来进行区分。

值对象

值对象(Value Object)与实体不同,值对象并没有自己的标识符,而是由属性值来确定相等性。如果两个值对象的全部属性值都是相等的,那么这两个值对象就是相等的,值对象一般是不可变的,方便进行共享。

服务

有些操作并不能添加到实体或值对象上,而这些操作本来就是行为和动作,需要以对象的方式来表达。对于这样的操作,一般使用服务来进行表示。服务所表示的操作与一个领域中的概念相关,并且是无状态的。服务对象并不包含内部的状态,只是为了提供相关的功能。

模块

模块(Module)是相关概念的一种组织方式,模块内部是高内聚的,模块之间是松散耦合的,模块的作用在于降低理解的复杂度。每次只需要专注于一个模块中的有限概念即可,模块由功能上或逻辑上紧密关联的元素组成。模块对外提供定义良好的接口,模块也属于通用语言的一部分。

聚合

领域驱动设计中的聚合(Aggregate)是一个很重要的概念,在说明什么是聚合之前,先了解一些聚合所要解决的问题是什么。在建模的过程中,我们会创建很多实体和值对象,比如,在电子商务应用的模型中,会包含客户、产品、订单和订单项等实体。这些实体和值对象之间存在关联关系,比如,订单实体中包含多个订单项实体,订单项实体则引用产品实体,客户实体关联多个订单实体。对某个实体进行的操作,可能会影响多个实体,产生级联式的反应。

在更新操作时,一个挑战是如何保证由业务规则决定的不变量不被破坏。比如,由于新冠肺炎造成了卫生纸的短缺,在线购物网站需要应用一个业务规则,那就是每个订单中的卫生纸不能超过 3 个,由于卫生纸类别下的产品有很多,每个订单中的订单项实体可能会引用不同的产品。如果更新订单的服务可以直接访问订单中的订单项实体,那么在并发操作时可能造成不变量被破坏。

比如,当前订单中的两个订单项分别包含了卫生纸 A 和 B 各 1 件,这个时候可以把卫生纸 A 和 B 的数量增加 1。如果只在修改订单项的操作之前进行检查,那么当两个数量加 1 的操作并发执行时,有可能两个操作的检查都通过,从而都可以继续执行。所产生的结果是订单中卫生纸的数量变成了 4,破坏了业务规则设置的不变量。

造成上述问题的原因在于,订单项实体可以被外部直接访问,而不变量的规则定义在包含它们的订单实体中。聚合是实体和值对象的一个集群,有自己的边界,每个聚合都有一个根。聚合的根是聚合中包含的一个实体,也是外部对象唯一可以访问的聚合中的对象。聚合内部的实体和值对象可以互相引用。

聚合有如下特征:

  • 作为聚合根的实体拥有全局的标识符,并且负责检查不变量。

  • 聚合中除了根之外的其他实体只有局部的标识符,只在聚合内部唯一。

  • 聚合边界之外的对象只能引用聚合的根实体。虽然外部对象可以通过聚合的根实体获取到内部对象的引用,不过这些引用是临时的,不能用来改变内部对象的状态。一个常见的做法是将内部对象转换成值对象之后,再返回给使用者。

  • 聚合中的对象可以引用其他聚合的根对象。

在应用了聚合的概念之后,可以创建一个订单相关的聚合,订单实体是这个聚合的根,而订单项则变成了该聚合的内部实体。外部对象只能通过订单实体来进行更新,这就确保了订单的不变量可以在每次更新操作时都得到检查。

工厂

工厂(Factory)用来创建聚合或对象,对象一般可通过构造器来创建,对于一些复杂的对象或聚合来说,创建的逻辑可能很复杂。工厂的作用是提供了一个专有的接口来创建对象或聚合。

资源库

在使用对象之前,首先需要获得该对象的引用,为了得到引用,要么创建一个新的对象,要么根据对象的引用关系进行遍历,进行遍历的前提条件是找到作为起点的那个对象,这个起点对象通常是聚合的根。资源库(Repository)封装了获取对象引用的逻辑,同时也提供了对象的添加、删除和查询操作,只需要对聚合的根提供资源库即可。

熟悉 Spring 的人可能发现了 Spring 中的注解 Service 和 Repository 与领域驱动设计中的元素有一样的名称,这是因为这两个注解本来就来源于领域驱动设计。

上下文映射

由于多个上下文的存在,同一个概念在不同的上下文模型中有不同的表达形式。当需要把这些不同的上下文进行集成时,则需要考虑不同上下文之间的同一个概念,如何进行映射的问题。下面介绍常用的映射方式。

共享内核

共享内核(Shared Kernel)指的是两个界定的上下文共享同一个很小的模型,因为这个模型是共享的,就意味着在两个上下文之间建立了紧密耦合的关系。当对这个共享模型进行修改时,需要两个上下文团队的沟通与合作,为了避免造成冲突,共享模型一般由其中一个团队负责维护。共享内核的做法虽然看起来并不是很理想,但是在很多情况下都有用武之地。

下图给出了共享内核的示例,两个界定的上下文中间交界的地方就是共享内核。

客户—供应商

客户—供应商(Customer - Supplier)指的是两个界定的上下文之间存在生产者和消费者的关系,供应商是上游的提供者,客户是下游的消费者。客户可以对供应商提出要求,而供应商要尽可能满足客户的要求,但最终的决定权在供应商手中。

下图给出了客户—供应商的示例。

顺从者

顺从者(Conformist)可以看成是客户—供应商的一种特殊情况,也同样分为上游的生产者和下游的消费者。不同之处在于,作为上游的供应商完全可以不考虑客户的需求,客户只能选择全盘接受供应商提供的模型,这也是顺从者这个名称的含义。从另一个角度来看,顺从者模式又像是共享内核,只不过客户并不能对这个共享内核做出任何修改。

防腐蚀层

防腐蚀层(Anticorruption Layer)指的是作为下游的团队,当与上游的模型进行集成时,在两个模型之间创建一个独立的隔离层,这个层次称为防腐蚀层,防腐蚀层的存在,使得下游的团队可以根据其自身的实际业务来定义模型。与上游模型的转换工作,由防腐蚀层来完成,这就保证了下游模型的稳定性,避免受到外部模型的侵蚀。防腐蚀层有着自己不小的代价,不过这样的代价所带来的好处也是值得的。

下图给出了防腐蚀层的示例。

开放主机服务

开放主机服务(Open Host Service)指的是界定的上下文以开放服务的方式对外提供访问,所开放的服务有设计良好的 API,这使得其他团队可以更容易的进行集成。

下图给出了开放主机服务的示例。

公开语言

当多个模型需要协同工作时,在这些模型之间传递消息是必不可少的,一个现实的问题是如何定义消息的格式。如果两个模型之间是顺从者的关系,那么直接用上游的模型来作为消息的格式即可。在大多数情况下,两个模型是相对独立的,不存在以其中某一个为主的情况。

一种好的做法是定义一种公开语言(Published Language)作为沟通的中间格式,两个模型在进行消息传递时,都需要转换成这个中间格式。公开语言通常与开放主机服务一块使用,如果开放主机服务提供的 API 以公开的方式发布出来,就成为了公开语言。

分道扬镳

上面介绍的这些上下文映射的模式,其目的还是希望可以与上游的上下文中的模型进行集成。为了要进行集成,就必须添加类似防腐蚀层这样的结构,这就意味着附加的实现成本。在有些情况下,这些附加的成本所带来的好处,可能还抵不上它的成本,在这样的情况下,集成就不是一个好的选项。可能更好的做法是不去集成,而是自己实现所需要的模型,这就是分道扬镳(Seperate Way)模式。

总结

领域驱动设计的思想在微服务架构中有其独特的应用。本课时对领域驱动设计的基本概念进行了介绍,包括领域、子领域、模型、通用语言和界定的上下文等;接着介绍了模型中包含的元素,包括实体、值对象、服务、模块、聚合、工厂和资源库等;最后介绍了在不同的界定的上下文之间进行映射的方式,包括共享内核、客户—供应商、顺从者、防腐蚀层、开放主机服务、公开语言和分道扬镳等。


云原生微服务架构实战精讲第三节 示例用户场景分析和领域驱动DDD相关推荐

  1. 云原生微服务架构实战精讲第二节 云原生和Kubernete

    第03讲:云原生应用的 15 个特征 本课时我将带你学习云原生应用. 微服务架构只是一种软件架构风格,并不限制所采用的实现技术,开发团队可以自由选择最合适的技术来实现.在第 01 课时介绍微服务架构的 ...

  2. 云原生微服务架构实战精讲第八节 访问控制与更新策略

    第24讲:服务调用失败的处理策略与实践 在微服务架构的应用中,微服务之间一般有两种类型的交互方式,一种是使用消息中间件的异步消息模式,也就是第 14 课时中提到的事件驱动设计,微服务之间进行交互的是消 ...

  3. 云原生微服务架构实战精讲第七节 调度算法与司机乘客行程查询

    第19讲:如何实现行程派发与调度算法 第 18 课时介绍了司机模拟器如何发布位置更新事件,以及行程派发服务如何处理这些事件,并维护所有可用的司机信息,本课时紧接着第 18 课时的内容,主要介绍行程派发 ...

  4. 云计算与云原生 — 云原生微服务架构的技术内涵

    目录 文章目录 目录 微服务框架的演进 第一代微服务框架 Spring Cloud Dubbo 下一代微服务框架 - Service Mesh Istio Envoy Kubernetes + Ser ...

  5. 云原生微服务架构的技术内涵

    目录 文章目录 目录 微服务框架的演进 第一代微服务框架 Spring Cloud Dubbo 下一代微服务框架 - Service Mesh Istio Envoy Kubernetes + Ser ...

  6. [云原生]微服务架构是什么

    作者简介:大家好,我是?让我们一起共同进步吧!?? ??个人主页:的csdn博客 ??系列专栏: 数据结构与算法 ??哲学语录: 承认自己的无知,乃是开启智慧的大门 ??如果觉得博主的文章还不错的话, ...

  7. consul命令行查看服务_Go语言微服务架构实战:第十三节 微服务管理--Docker安装及运行consul节点...

    微服务管理--Docker安装及运行consul节点 搭建集群 在真实的生产环境中,需要真实的部署consul集群.在一台机器上想要模拟多台集群部署的效果,有两种方案:一种是借助虚拟机,另一种是借助容 ...

  8. 【ArchSummit】阿里云原生微服务架构治理最佳实践

    前言

  9. 干货 | 基于开源体系的云原生微服务治理实践与探索

    作者简介 CH3CHO,携程高级研发经理,负责微服务.网关等中间件产品的研发工作,关注云原生.微服务等技术领域. 一.携程微服务产品的发展历程 携程微服务产品起步于2013年.最初,公司基于开源项目S ...

  10. 基于开源体系的云原生微服务治理实践与探索

    作者:董艺荃|携程服务框架负责人 携程微服务产品的发展历程 携程微服务产品起步于 2013 年.最初,公司基于开源项目 ServiceStack 进行二次开发,推出 .Net 平台下的微服务框架 CS ...

最新文章

  1. 浅谈几种区块链网络攻击以及防御方案之其它网络攻击
  2. 用数据分析《你好,李焕英》“斐妈”爆红的真相
  3. python随机排列图片_python 随机打乱 图片和对应的标签方法
  4. mysql 字符大对象_第02期:MySQL 数据类型的艺术 - 大对象字段
  5. win7旗舰版6l打印机咋安驱动_在w7旗舰版上怎么安装HPlaserjet6L打印机?
  6. oracle 11g b表空间什么情况下自动增加,Oracle 11g表空间——创建和扩展(永久)表空间...
  7. css块元素与行内元素特点,CSS区分块级元素和行内元素
  8. python相对路径下的shell_shell,python获取当前路径(脚本的当前路径) (aso项目记录)...
  9. Windows Phone 7 Belling‘s课堂(一) 磁贴的学习
  10. Android代码优化
  11. flowable 查询完成的流程_中注协正在调试注册会计师成绩查询系统?
  12. Anaconda tensorflow 安装笔记
  13. Windows Server 2012中的多元密策略
  14. 用C语言实现简单小游戏
  15. C6000 DSP技术深度探索(3)-中断向量表
  16. 设计模式实践-装饰器
  17. 群晖用php装aria2,群晖Synology安装Aria2实现迅雷离线下载,安装IPKG
  18. Linux nm命令详解
  19. 网易2018校园招聘编程题真题集合
  20. 南京林业大学本科毕业论文答辩PPT模板

热门文章

  1. css的nth选择器,CSS选择器之nth
  2. 易经读书笔记16 雷地豫
  3. ffmpeg生成裸眼3D、伪3D视频
  4. Android 常见的抓log方法总结
  5. MongoDB(shel)-表增删改
  6. Android性能优化(一)启动优化
  7. 口袋电子秤方案芯片CSU18P88
  8. 安卓逆向_6 --- Dalvik 字节码、Smali 详解
  9. 常州2021高考成绩查询,2021江苏常州高考选课分班情况(数据)
  10. Zynga完成对快速增长的超休闲游戏领域的领导者——伊斯坦布尔的Rollic的收购