微服务与领域驱动设计(DDD)有着共生关系。所谓领域驱动设计是一种设计方法,在这种方法中我们基于业务领域涉及的内容用软件精心搭建出一套模型,这套模型随着时间的推移而逐渐发展,但并不受运行系统的管道约束。我发现人们喜欢将这种模式与 Apache Kafka 结合起来,这种组合在实践中也运用得越来越多了。
在这类项目中,微服务架构通常使用 Kafka 作为事件流平台;而领域驱动设计方法则用来定义各种有界上下文,这些上下文表示应用需要执行的各种业务流程。它们与各种事件结合在一起,创建了一个将各个有界上下文与下游出现的上下文分离的单向依赖图,以创建丰富多样的事件流业务应用。
本文将探讨为什么 Apache Kafka 会成为微服务架构事实上的标准和主干——Kafka 不仅取代了其他传统的中间件,而且人们还使用 DDD 和 Kafka 原生 API(如 Kafka Streams、KSQL 和 Kafka Connect)直接构建微服务。
微服务
如今人们都想用微服务(https://martinfowler.com/articles/microservices.html)创建敏捷而灵活的架构,微服务这个术语在很多环境中都很常见。
虽然微服务并不是什么免费的午餐(http://highscalability.com/blog/2014/4/8/microservices-not-a-free-lunch.html),但它们确实提供了许多好处,解耦就是其中一项优势。解耦是围绕业务功能来组织系统,以形成分散的体系结构的过程。其中智能端点和哑管道(dumb pipe)会确保:
基于微服务构建的应用程序需要尽可能分散开来,同时还保持紧密的协作关系——它们拥有自己的领域逻辑(这些逻辑针对的是各自需要处理的业务问题),且行为更像是经典的 Unix 系统中的过滤器——接收一个请求,对其应用合适的逻辑并生成回应。
Martin Fowler(https://martinfowler.com/articles/microservices.html)
Apache Kafka——微服务的事件流平台
应该使用哪些技术来构建微服务架构?这个问题可以分为两部分来回答:
1. 微服务之间怎样相互通信?
当我们考虑微服务之间的通信问题时(比如说与同步 HTTP(S)调用进行通信),大多数人一开始会使用 REST 这个方法。很多用户场景都可以使用这种方法。但是,请求——响应模式所创建的点对点连接会将发送方与接收方的通信来往耦合在一起,这样就很难在不影响其他组件的情况下更改某个组件。
因此许多架构师使用中间件作为微服务通信的主干,以创建解耦、可扩展和高度可用的系统。很多东西都能拿来用作中间件——比如说一些自定义粘合代码或框架、像 RabbitMQ 这样的消息传递系统、像 Talend 这样的 ETL 工具、像 WSO2 这样的 ESB,或者像 Apache Kafka 这样的事件流平台。
2. 如果你要使用中间件,该用哪个?
Apache Kafka 之所以能成为微服务事实上的标准,主要原因是它融合了三个强大的概念:
发布和订阅事件流,类似于消息队列或企业消息传递系统
以容错方式存储事件流
在事件流发生时实时处理
Kafka 将这三大支柱一起内置到了一个分布式事件流平台中,这样用户就可以用可靠、可扩展和容错的方式将各种微服务(例如生产者和消费者)分离开来。
为了更好地理解 Apache Kafka 相比传统中间件(如 MQ、ETL 或 ESB 工具)的优势,请参阅”Apache Kafka vs 企业服务总线——朋友,敌人还是亦敌亦友?”(https://www.confluent.io/blog/apache-kafka-vs-enterprise-service-bus-esb-friends-enemies-or-frenemies/)
下面探讨像 Apache Kafka 这样的事件流平台是怎样与领域驱动设计方法建立联系的。
用于构建和解耦微服务的领域驱动设计方法(DDD)
领域驱动设计(DDD)最早是由 Eric Evans 在他的一本著作中提出来的方法,用于构建包含复杂业务领域的系统。也就是说你不会将 DDD 应用于基础架构软件或构建路由器、代理或缓存层之类的项目中,而是会应用到解决实际业务问题的软件项目上。这种技术很好用,可以将业务模型与将各种模型连接在一起的管道代码分离开来。将这两部分在软件层面分离开来之后,团队就很容易去设计、建模、构建并改进具体的产品实现。
DDD 方法基于下列原则(https://techbeacon.com/app-dev-testing/get-your-feet-wet-domain-driven-design-3-guiding-principles):
团队需要与领域专家交流,从而使用领域术语搭建领域模型
在领域代码中嵌入领域术语
保护领域知识免受来自其他领域和技术子域的损害
本文讨论的 DDD 核心概念是有界上下文(https://martinfowler.com/bliki/BoundedContext.html)。大型项目通常有许多种领域模型和有界上下文。但开发人员将不同种类有界上下文中的代码组合在一起的过程中,软件系统可能会变得越来越不可靠且难以理解。团队成员之间越来越难沟通,而且人们通常很难搞清楚某个模型不该应用在哪些上下文中。
因此,DDD 要求我们明确定义每个模型所应用的上下文对象。我们在设置边界时需要考虑下列因素:哪个团队拥有该模型、应用程序特定部分的用途、以及诸如代码库和数据库 schema 之类的物理表现等。将各个模型严格约束在自己的边界内后,各个部分就更容易实现和理解,因为我们只需要考虑每个部分所属的一个有界上下文就够了。我们不用再因为其他人泄露的代码而分散精力或感到困惑。正如 Dan North 所说:“专心构建你负责的代码,不用想太多”(https://www.youtube.com/watch?v=4Y0tOi7QWqM)。
一个事件流平台可能看起来像这样:
在平台中每个微服务都有自己的有界上下文。从技术角度来看,这可能涉及不同的 API、框架、通信协议和数据存储等。有些部分遵循请求——响应模式,而其他部分则根据需要解决的问题来使用事件。总而言之,每个部分都是一个单独的有界上下文,拥有属于自己的领域模型,并在该模型、业务流程和它与其他部分共享的数据之间建立映射。
那么为什么 Kafka 就是事件流平台的不二之选呢?
Apache Kafka 和领域驱动的微服务
Apache Kafka 结合了消息传递和存储能力,使不同的生产者和消费者之间能够完全解耦:
服务端(Kafka broker、ZooKeeper 和 Confluent Schema 注册表)可以与业务应用之间分离开来。
生产者不知道或不关心是谁在消费它们创造的事件。Kafka 为他们处理背压、解决可扩展性和高可用性需求。
生产者的生产工作不受消费者下线影响。
就算新的消费者需要从较早的时间戳开始消费事件,它们也可以随时添加进来。
消费者可以以自己的节奏(批量或实时)处理数据。
消费者可以反复处理数据(例如训练不同的分析模型或从错误和数据损坏中恢复)。
有了这些特性,项目团队就都能拥有自己的领域了;这些领域可以有不同的职责、SLA、版本控制和技术选择。
这种方法不仅适用于业务应用,也适用于公司 IT 团队的运营工作;运营团队可以拥有用于内部自助服务的 Kafka 集群。Kafka 集群通常基于 PaaS 架构部署,例如 Kubernetes 和 Confluent Operator(https://www.confluent.io/confluent-operator/)等。如果使用基于 Confluent Cloud(https://www.confluent.io/confluent-cloud/)等托管服务的云部署方案,则通常不需要此类基础架构团队。
领域模型、有界上下文和通用语言
如上所述,领域驱动设计(DDD)的关键元素之一是将业务问题分离为许多独立的有界上下文的集合。每个上下文都有一个领域模型,将所需数据完全封装在软件里,还包括需要执行的业务操作以及用于描述这些元素的语言。但是,某个有界上下文中的领域模型该怎样与其他上下文中的模型建立联系呢?我们如何保证一个模型中的更改不会对其他领域模型产生负面影响?
答案就是使用在 DDD 中被称为反腐层(anti-corruption layer)的方法:反腐层将领域模型中使用的数据映射到在各个微服务或有界上下文之间传输的数据上。这个模式与具体的实现无关,这意味着无论你的服务是通过事件还是通过请求——响应协议通信,都可以使用反腐层。在这两种通信方式下通常都会有一种有线格式(可以是用来从 REST 端点返回数据的 schema,或是用来描述事件的 schema,例如存储在 Schema 注册表(https://www.confluent.io/confluent-schema-registry/)中的 Avro 消息)。
反腐层有两大职责:
它让领域模型不受其他模型的更改影响
它封装了上下文之间的边界,并描述了它们之间的映射。这种映射既能从技术意义上描述,例如一条消息中的字段 A 映射到模型中的字段 B;也能基于 DDD 的通用语言(https://martinfowler.com/bliki/UbiquitousLanguage.html)描述——将事件 schema 中的对手(counterparty)映射到领域模型中的客户(customer)。
每个有界上下文中的模型都会进化发展,而团队在设计改进模型的过程中,要经历的一项关键步骤就是设计将各个模型连接在一起的接口。要走好这一步,首先应该由开发人员和拥有系统的利益相关方共同开发一种通用语言。
使用 Apache Kafka、Kafka Streams、KSQL 和 Kafka Connect 将各个领域连接起来
还有一个关键要点我们还没讨论过:Apache Kafka 不仅是一个消息系统或者一个集成层——它是一个事件流平台。这意味着它不仅负责提供用来解耦微服务的中间件,还允许你在客户端代码中执行复杂的数据操作,如拆分、连接、过滤和汇总等。这是 Apache Kafka 和传统中间件之间的另一大区别所在,正如 ThoughtWorks 所解释的那样:
……我们看到一些组织将许多 Kafka 生态系统组件(例如连接器和流处理器)集中在一起,使用 Kafka 重建了 ESB 这种反模式,而不是让这些组件与产品或服务团队共存。ESB 模式有着很严重的问题,人们将越来越多的逻辑、编排和转换推入集中管理的 ESB 中,不得不愈加依赖中心化的团队。我们指出这一现象就是想让人们不要再推进这种有缺陷的模式了。
“使用 Kafka 重建 ESB 反模式”(https://www.thoughtworks.com/radar/techniques/recreating-esb-antipatterns-with-kafka)
这一点是很重要的。ESB 之所以会包含复杂的逻辑、编排和转换并不是偶然产物,而是因为人们正在构建的业务流程需要它们。ThoughtWorks 的观点并不是说这些过程本身是问题所在,或者没什么必要,而是说问题出在这些过程被推出了本应拥有它们的应用所在的边界,并推入了中心化的基础架构中。这种中心化的基础架构和业务逻辑导致软件愈加脆弱,难以发展——这正是现代化的敏捷软件项目最不想看到的结果。
事件流系统则使用另一种方式处理这个问题。它们基于哑管道(可高度扩展)和智能过滤器构建;要注意这些过滤器比以往的设计更强大、功能更多。嵌入在微服务中的过滤器具有现代流处理引擎的所有功能。没有中心化的逻辑,一切都是完全分散的,这意味着每个有界上下文都有自己的业务逻辑、编排和转换等等。
因此,使用 Kafka 作为中枢系统后,你就可以使用流处理工具提供的更高级抽象来连接在各个有界上下文中创建的模型了。
这样你就可以充分利用 DDD 的所有优势,同时避免 ESB 导致的种种麻烦——诸如紧密耦合、集中管理整个业务流程等。
具体选择实现哪个微服务完全取决于团队的工作需要。你们的微服务可以使用简单的技术接口,如 REST 或 JMS 这样只用来通信的接口。你们也可以构建真正的事件流式微服务,充分利用流处理的能力来操纵事件数据流并将其映射到你们的内部模型上。
前面的例子中提到了许多种微服务。有些情况下会使用 REST(例如在微服务和 UI 之间通信),有些则使用 Kafka Streams 或 KSQL 将不同来源的事件连接在一起。还有的情况下会使用 Kafka Connect 将事件简单地推送到数据库中,以便进一步操作或直接通过事件源模式(https://www.confluent.io/blog/messaging-single-source-truth/)使用事件。每个微服务都可以选择自己独立的技术实现,与其他微服务和它们选择的技术无关。
如何构建这样的系统
人们很容易想到使用 REST、gRPC 或其他一些请求——响应协议构建分散的事件流微服务。但对许多人来说,构建一种基于事件的系统更是观念上的变革。用事件构建系统的方式是多种多样的。它们可以用于“发后即忘”消息传输,也可以用作协作手段。Ben Stopford 的博客文章“在事件的基础上构建服务“(https://www.confluent.io/blog/build-services-backbone-events/)进一步解释了这些模式。
你还必须决定该如何管理状态。可以使用 Kafka Connect 等技术在数据库中管理,也可以使用 Kafka Streams API 在托管服务中管理。关于构建有状态事件流的微服务,可以参阅 Ben Stopford 的博客文章“使用 Kafka Streams 和 KSQL 构建微服务生态系统”(https://www.confluent.io/blog/building-a-microservices-ecosystem-with-kafka-streams-and-ksql/)。
要从宏观层面了解如何将 Connect、Kafka Streams 和微服务组合在一起,可以参阅 Yeva Byzek 的一篇写得很棒的文章(https://www.confluent.io/blog/stream-processing-part-1-tutorial-developing-streaming-applications)。最后,构建微服务生态系统只是问题的第一部分。生态系统构建完成后,你需要对其检测、控制和操作。具体内容可参阅这篇文章(https://www.confluent.io/blog/journey-to-event-driven-part-4-four-pillars-of-event-streaming-microservices)。
还有一件事值得一提,就是 Confluent 的 RBAC 功能;它允许在 Confluent 平台上执行基于角色的访问控制。你可以详细配置每个域组可以访问的资源(如 Kafka 主题、Schema 注册表或连接器等)。
只需挑选最适合你的架构即可。例如,你可以让每个 Kafka Connect 群集由负责它的领域团队运营,或者把 Kafka Connect 托管为 Kafka 群集的一部分,并允许团队为其部署连接器。
有这么多工具帮助你使用 DDD 方法构建和运行基于微服务的系统,我希望你能从中获益多多。
Apache Kafka+领域驱动设计(DDD)=解耦事件流微服务
虽然有许多方法可以构建微服务架构,但领域驱动设计(DDD)中描述的方法无疑是最强大的,特别是当你构建的系统具有复杂的业务领域(例如医疗保健、金融、保险、 零售)时更是如此。
DDD 中的许多设计原则都能直接用于事件驱动系统,有一些原则本文还没具体涉及到;我强调的是最常见的技术挑战:如何将应用分离到各个有界上下文中,为什么这些上下文的独立性如此重要,为什么需要领域模型,以及这些概念与消息传递、Apache Kafka 和事件的使用有什么关系。
在当今众多工具的帮助下,微服务架构得以使用事件流平台将各个微服务分离开来,并从中获益匪浅。实现可以是请求驱动的,可以是简单的事件驱动系统,也可以是整个事件流业务应用。它们还可以是这些概念的某种混合产物。DDD 提供了一套管理各个部分之间相互作用的基本技术,包括通用语言、有界上下文,schema 和反腐层等。如你所见,事件和消息传递是让这些系统在实践中良好运行的关键因素。
作者介绍
Kai Waehner 在 Confluent 担任技术传播人。Kai 的主要专业领域包括大数据分析、机器学习/深度学习、云/混合架构、消息传递、集成、微服务、流处理、物联网和区块链等。他经常在 JavaOne、O’Reilly Software Architecture 和 ApacheCon 等国际会议上发表演讲,还为专业期刊撰写文章。
评论 1 条评论