第3章 如何建模服务

 
3.2什么样的服务是好服务
 
在MusicCorp的团队为了把八轨带(eight track tape)递送到所有人手中而开始辛苦工作、 创建一个又一个的服务之前,让我们先缓缓,讨论一些很重要的基本原则。什么是好的服 务?如果你曾经尝试过SOA并且失败了,大概就知道我下一步要说什么了。不过万一你 没那么幸运(不幸),我希望你专注在两个重要的概念上:松耦合和高内聚。在本书的剩 余部分,我们会讨论更多的实践和细节,因为如果这两点做不到,那么微服务也就没什么 价值了。
 
这两个概念在不同的上下文中被大量使用,尤其是在面向对象编程中,所以,我们先讨论 一下这两个概念在微服务中是什么含义。
 
3.2.1松耦合
如果做到了服务之间的松耦合,那么修改一个服务就不需要修改另一个服务。使用微服务 最重要的一点是,能够独立修改及部署单个服务而不需要修改系统的其他部分,这真的非 常重要。
 
什么会导致紧耦合呢? 一个典型的错误是,使用紧辋含的方式做服务之间的集成,从而使 得一个服务的修改会致使其消费者的修改。第4章会进一步讨论如何避免这种问题。
 
一个松耦合的服务应该尽可能少地知道与之协作的那些服务的信息。这也意味着,应该限 制两个服务之间不同调用形式的数量,因为除了潜在的性能问题之外,过度的通信可能会 导致紧耦合。
 
3.2.2高内聚
我们希望把相关的行为聚集在一起,把不相关的行为放在别处。为什么呢?因为如果你要 改变某个行为的话,最好能够只在一个地方进行修改,然后就可以尽快地发布。如果需要 在很多不同的地方做这些修改,那么可能就需要同时发布多个微服务才能交付这个功能。 在多个不同的地方进行修改会很慢,同时部署多个服务风险也很高,这两者都是我们想要 避免的。
 
所以,找到问题域的边界就可以确保相关的行为能放在同一个地方,并且它们会和其他边 界以尽量松耦合的形式进行通信。
 
3.3限界上下文
 
Eric Evans的《领域驱动设计》一书主要专注如何对现实肚界的领域进行建模。该书中有 很多非常棒的想法,比如通用语言、仓储、抽象等。其中Evans引入的一个很重要的概念 是限界上下木(bounded context),刚听到这个概念的时候,我深受启发。他认为任何一个 给定的领域都包含多个限界上下文,每个限界上下文中的东西(Eric更常使用模型这个词, 应该比“东西”好得多)分成两部分,一部分不需要与外部通信,另一部分则需要。每个 上下文都有明确的接口,该接口决定了它会暴露哪些模型给其他的上下文。
 
另一个我比较喜欢的限界上下文的定义是:“一个由显式边界限定的特定职责。” (http:// blog.sapiensworks.com/post/2012/04/17/DDD-The-Bounded-Context-Explained.aspx)如果你 想要从一个限界上下文中获取信息,或者向其发起请求,需要使用模型和它的显式边界进 行通信。在这本书中,Evans使用细胞作为比喻:“细胞之所以会存在,是因为细胞膜定义 了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜。”
 
3.3.1共享的隐藏模型
对于MusicCorp来说,财务部门和仓库就可以是两个独立的限界上下文。它们都有明确的 对外接口(在存货报告、工资单等方面),也都有着只需要自己知道的一些细节(铲车、 计算器)。
 
财务部门不需要知道仓库的内部细节。但它确实也需要知道一些事情,比如,需要知道库 存水平以便于更新账户。图3-1展示了一个上下文图表示例。可以看到其中包含了仓库的 内部概念,比如订单提取员、货架等。类似地,公司的总账是财务部必备的一部分,但是 不会对外共享。
 
3-1:财务部门和仓库之间共享的模型
 
为了算出公司的估值,财务部的雇员需要库存信息,所以库存项就变成了两个上下文之间 的共享模型。然而,我们不会盲目地把库存项在仓库上下文中的所有内容都暴露出去。比 如,尽管在仓库内部有相应的模型来表示库存项,但是我们不会直接把这个模型暴露出 去。也就是对泫模型来说,存在内部和外部两种表示方式。很多情况下,这都会导致是否 要采用REST的讨论。第4章会对REST做更多的讨论。
 
有时候,同一个名字在不同的上下文中有着完全不同的含义。比如,退货表示的是客户退 回的一些东西。在客户的上下文中,退货意味着打印运送标签、寄送包裹,然后等待退 款。在仓库的上下文中,退货表示的是一个即将到来的包裹,而且这个包裹会重新人库。 退货这个概念会与将要执行的任务相关,比如我们可能会发起一个重新入库的请求。这个 退货的共享模型会在多个不同的进程中使用,并且在每个限界上下文中都会存在相应的实 体,不过,这些实体仅仅是在每个上下文的内部表示而已。
 
3.3.2模块和服务
明白应该共享特定的模型,而不应该共享内部表示这个道理之后,就可以避免潜在的紧耦 合(即我们不希望成为的样子)风险。我们还识别出了领域内的一些边界,边界内部是相 关性比较髙的业务功能,从而得到高内聚。这些限界上下文可以很好地形成组合边界。
 
就像在第1章中讨论过的,在同一个进程内使用模块来减少彼此之间的耦合也是一种选 择。刚开始开发一个代码库的时候,这可能是比较好的办法。所以一旦你发现了领域内部 的限界上下文,一定要使用模块对其进行建模,同时使用共享和隐藏模型。
 
这些模块边界就可以成为绝佳的微服务候选。一般来讲,微服务应该清晰地和限界上下文保持一致。熟练之后,就可以省掉在单块系统中先使用模块的这个步骤,而直接使用单独 的服务。然而对于一个新系统而言,可以先使用一段时间的单块系统,因为如果服务之间 的边界搞错了,后面修复的代价会很大。所以最好能够等到系统稳定下来之后,再确定把 哪些东西作为一个服务划分出去。第5章会对此做更多讨论,同时也会介绍一些技术来把 已有的单块系统划分成微服务。
 
所以,如果服务边界和领域的限界上下文能保持一致,并且微服务可以很好地表示这些限 界上下文的话,那么恭喜你,你跨出了走向髙内聚低耦合的微服务架构的第一步。
 
3.3.3过早划分
几个月之后,我们发现SnapCI的用例和之前想的有所不同,而这些不同足以证明之前的 服务划分方式是有问题的。这导致了很多跨服务的修改,而这些修改的代价相当高。团队 逐渐又把这些服务合并成了一个单块系统,从而给所有人时间去理解服务边界到底应该 在哪。一年之后,团队识别了出非常稳定的边界,并据此将这个单块系统拆分成多个微服 务。当然这并不是我见过的唯一一个过早划分的例子。过早将一个系统划分成为微服务的 代价非常髙,尤其是在面对新领域时。很多时候,将一个已有的代码库划分成微服务,要 比从头开始构建微服务简单得多。
3.4业务功能
 
当你在思考组织内的限界上下文时,不应该从共享数据的角度来考虑,而应该从这些上下 文能够提供的功能来考虑。比如,仓库的一个功能是提供当前的库存清单,财务上下文能 够提供月末账目或者为一个新招的员工创建工资单。为了实现这些功能,可能需要交换存 储信息的模型,但是我见过太多只考虑模型从而导致贫血的、基于CRUD (create, read, update, delete)的服务。所以首先要问自己“这个上下文是做什么用的”,然后再考虑 “它需要什么样的数据”。
 
建模服务时,应该将这些功能作为关键操作提供给其协作者(其他服务)。
 
3.5逐步划分上下文
 
一开始你会识别出一些粗粒度的限界上下文,而这些限界上下文可能又包含一些嵌套的限 界上下文。举个例子,你可以把仓库分解成为不同的部分:订单处理、库存管理、货物接 受等。当考虑微服务的边界时,首先考虑比较大的、粗粒度的那些上下文,然后当发现合 适的缝隙后,再进一步划分出那些嵌套的上下文。
 
我见过有一种做法是,使这些嵌套的上下文不直接对外可见。对于外界来说,它们用的还 是仓库的功能,但发出的请求其实被透明地映射到了两个或者更多的服务上,如图3-2所 示。有时候你会认为,高层次的限界上下文不应该被显式地建模成为一个服务,如图3-3 所示,也就是说,不存在一个单独的仓库边界,而是把库存管理、订单处理和货物接收等 这些服务分离开来。
图3-2:在仓库内部使用微服务表示嵌套限界上下文
图3-3:仓库内部的限界上下文被提升到顶层上下文的层次
 
通常很难说哪种规则更合理,但是你应该根据组织结构来决定,到底是使用嵌套的方法还 是完全分离的方法。如果订单处理、库存管理及货物接收是由不同的团队维护的,那么他 们大概会希望这些服务都是顶层微服务。另一方面,如果它们都是由一个团队一管理的,那 么嵌套式结构会更合理。其原因在于,组织结构和软件架构会互相影响,第10章会对此 做详细I寸论。
 
另一个倾向于嵌套式方法的原因是,它可以使得架构更成块儿从而更好地测试。举个例 子,当测试仓库的消费方服务时,不需要对仓库上下文中的每个服务进行打桩,只需要专注于粗粒度的API即可。当考虑更大范围的测试时,这也能够给你一定的单元隔离。比 如,我可以有这样一种端到端测试,该测试会使用仓库上下文中的所有服务,但其他的所 有协作者可以做打桩处理。第7章会对测试和隔离做更多讨论。
 
3.6关于业务概念的沟通
 
修改系统的目的是为了满足业务需求。我们会修改面向客户的功能。如果把系统分解成为 限界上下文来表示领域的话,那么对于某个功能所要做的修改,就更倾向于局限在一个单 独的微服务边界之内。这样就减小了修改的范围,并能够更快地进行部署。
 
微服务之间如何就同一个业务概念进行通信,也是一件很重要的事情。基于业务领域的软 件建模不应该止于限界上下文的概念。在组织内部共享的那些相同的术语和想法,也应该 被反映到服务的接口上。以跟组织内通信相同的方式,来思考微服务之间的通信形式是非 常有用的。事实上,通信形式在整个组织范围内都非常重要。
3.8 小结
Eric Evans在《领域驱动设计》中提到的概念对于寻找明显的服务边界来说非常有用。在 本章中我只提到了其中的一小部分。我推荐你看一看Vaughn Vernon的《实现领域驱动设 计》,它能够帮助你理解如何实践这些方法。
 
本章讨论的内容比较宽泛,下一章的内容技术性会更强。在实现服务间接口方面存在很多 的陷阱,从而会引人各种各样的麻烦。如果不想系统乱成一团麻,就必须深人讨论一下该 话题。
 
 
 
 
 
posted @ 2019-12-05 21:28  mongotea  阅读(116)  评论(0编辑  收藏  举报