如何用微服务煮粥

微服务流行的原因之一是自主和独立开发的可能性。 本质上,微服务体系结构交换了自主开发的可能性,以便进行更复杂的(与整体相比)部署,测试,调试和监视。 但是请记住,微服务不能原谅职责分离。 如果职责划分不正确,则在不同服务中会发生频繁的相关更改。 与整块内部不同模块或封装的框架内的协调更改相比,这要痛苦得多,也要复杂得多。 微服务的一致更改由于一致的布局,部署,测试等而变得复杂。

我想谈谈将责任划分为微服务的各种模式和反模式。

服务实体为反模式


“服务实体”是微服务体系结构设计的可能(反)模式之一,它导致不同服务中的代码高度依赖,并在服务中松散耦合。

对于大多数开发人员而言,似乎在根据主题领域的本质(“交易”,“人”,“客户”,“订单”,“图片”)选择服务时,他遵循唯一责任的原则,而且,这似乎合乎逻辑。 但是服务实体方法可以变成反模式。 发生这种情况是因为大多数功能或更改会影响多个实体,而不是一个。 结果,每个这样的服务都结合了不同业务流程的逻辑。

例如,在网上商店。 我们决定突出显示服务“产品”,“订单”,“客户”。

我应该进行哪些更改和服务才能增加送货到家?
例如,您可以这样做:

  • 在服务“订单”中添加交货地址,所需时间和交货人
  • 在客户服务中,为客户添加所选送货地址的列表
  • 在服务“产品”中添加商品的实体列表

对于供应商的界面,有必要在“订单”服务中使用单独的API方法,该方法将提供分配给该特定提供商的订单的列表。 此外,还需要采取一些方法从订单中删除不合适或客户在交货时拒绝的商品。

或者,为了在促销代码上增加折扣,我需要进行哪些更改以及需要进行哪些服务?
至少您需要:

  • 在“订购”服务中添加促销代码
  • 在“产品”服务中,添加折扣是否适用于该产品的促销代码
  • 在客户服务中,添加已发布给客户的促销代码列表

在经理的界面中,向客户添加个性化促销代码是客户服务中的另一种方法,该方法仅对商店经理可用,但对客户本人不可用。 然后,在“产品”服务中,制定一种方法,以列出受促销代码影响的产品列表,以便客户更轻松地在其界面中进行选择。

服务更改的来源可以是几个业务流程-选择和设计,付款和计费,交付。 每个问题领域都有其自身的局限性,不变性和顺序要求。 结果,事实证明,在“产品”服务中,我们将有关产品,折扣和产品余额的信息存储在仓库中。 并在“订单”中存储送货员的逻辑。

换句话说,分布在多个服务中的业务逻辑更改会导致多个服务中的相关更改。 同时,一项服务中的代码没有相互连接。

仓储服务


如果在封装整个逻辑的实体服务上创建单独的“层”服务,似乎可以解决此问题。 但是通常这也很糟糕地结束。 因为那时实体服务变成了存储服务,即 除了存储,所有业务逻辑都被淘汰。

如果数据存储在不同的数据库,不同的机器上,那么我们

  • 我们会因为不直接从数据库而是通过服务层提供数据而导致性能下降
  • 我们失去了灵活性,因为服务API的灵活性通常比SQL或任何其他查询语言低得多
  • 我们失去了灵活性,因为很难从不同的服务进行数据合并

如果不同的实体服务可以访问其他数据库,则服务之间的通信会隐式发生-通过一个公共数据库,然后进行任何影响数据架构更改的更改,只有在检查到此更改不会破坏使用该数据库或平板电脑的所有其他服务之后, 。

除了复杂的开发之外,此类服务变得过于关键和繁重-几乎对顶级服务的每个请求都必须向不同的服务实体提出多个请求,这意味着编辑它们变得更加困难,以满足日益增长的可靠性和性能要求。

由于纯格式实体服务的开发和支持存在此类困难,因此您很少会看到一种模式;通常,实体服务会变成一个或两个中央“微服务整体”,它们通常会更改并包含主要业务逻辑和小型微服务(通常是基础架构)的布局和很少改变的小东西。

按问题区域分开


改变本身不是天生的,它们来自某个问题领域。 问题区域是一个任务区域,在该任务区域中,使用一组概念或通过业务逻辑将所需代码更改的问题用一种语言提出。 因此,在一个问题区域的框架内,最有可能存在一组约束,即在编写代码时可以依赖的不变式。

按问题区域而不是按实体来区分服务责任通常会导致更受支持和易于理解的体系结构。 问题区域通常与业务流程相对应。 对于在线商店,最可能出现的问题区域是“付款和计费”,“交付”,“订购过程”。

同时影响多个问题区域的更改小于影响多个实体的更改。

此外,将来可以重用按业务流程细分的服务。 例如,如果我们要在在线商店旁边再次出售机票,则可以重用常规服务“账单和付款”。 并且不要做出其他类似的,而是特定于售票的。

例如,我们可以因此分为服务:

  • 一个服务或一组服务“交付”,将存储特定订单的交付,工作的组织,供应商的工作质量评估,供应商的移动应用程序等工作逻辑。
  • 一个服务或一组服务的“账单和付款”,将存储付款的工作逻辑,法人的付款帐户,合同的生成和结帐文件。
  • 服务或服务组“订单流程”,用于存储客户选择产品,目录,品牌,购物篮逻辑等的逻辑。
  • 服务“授权和认证”。
  • 分开打折服务甚至可能很有意义。

为了彼此交互,服务可以使用事件模型或彼此交换简单的对象(静态api,grpc等)。 没错,值得注意的是,正确组织此类服务之间的交互并不容易。 至少,数据去中心化有时会带来一致性(最终一致性)和事务性(在重要的情况下)的问题。

数据去中心化,简单对象的交换有其利弊。 一方面,分散化使独立开发和运营多种服务成为可能。 另一方面,存储两个或三个数据副本以及在不同系统中保持一致性的成本。

在现实生活中,通常会在两者之间发生某些事情。 消费者拥有所有服务使用的具有最少属性集的服务实体。 还有一些最小的逻辑层-例如,状态模型和队列中的事件以及实体中所有更改的通知。 同时,消费者服务仍然经常保留数据的“缓存”。 尽一切可能使这种服务中的更改尽可能少,并且从原则上讲,这很难做到,因为有太多的消费者。

同时,重要的是要了解,无论是按实体还是按问题区域划分的任何分区都不是灵丹妙药,总会有一些功能需要在若干服务中进行相关更改。 只是发生一次故障会比发生另一次故障更多。 开发的任务是最大程度地减少相关更改的数量。

仅当您拥有两个完全独立的产品时,才可以进行理想的拆分。 在任何企业中,您都将所有事物与一切联系在一起,唯一的问题是联系多少。

问题在于责任的分离和抽象障碍的高度。

设计服务API


在服务中设计接口仅在较小的规模上重复了细分为服务的历史记录。 更改界面(不仅仅是扩展)是复杂且耗时的。 在复杂的应用程序中,该接口应该足够通用以不会引起不断的更改,并且应该足够具体并且不足以导致职责和语义的传播。

因此,必须对服务接口进行设计,以使其语义能够抵抗更改。 如果接口的语义或职责范围依赖于问题区域的限制,则这是可能的。

具有复杂业务逻辑的服务的CRUD接口


太宽泛且非特定的接口会导致责任的削弱或过于复杂。

例如,CRUD API用于具有复杂业务逻辑的服务,此类接口不封装行为。 它们不仅使业务逻辑泄漏到其他服务中并侵蚀了服务的责任,而且激起了业务逻辑的传播-其他服务中现在存在限制,不变性和处理数据的方法。 接口用户服务(API)必须自己实现逻辑。

如果我们尝试在不显着更改接口的情况下将业务逻辑转移到服务,我们将获得一种过于通用和过于复杂的方法。

例如,有票务服务。 票可以是不同类型的。 每种类型都有一组不同的字段和略有不同的验证。 票证还具有状态模型-用于从一种状态转换为另一种状态的状态机。

让API看起来像这样:POST / PATCH / GET方法,URL /api/v1/tickets/{ticket_idasket.json

因此,您可以更新票证

PATCH /api/v1/tickets/{ticket_id}.json { "type": "bug", "status": "closed", "description": "   " } 

如果状态模型将取决于故障单,那么业务逻辑可能会发生冲突。 首先,根据旧的状态模型更改状态,然后更改票证的类型。 反之亦然?

事实证明,在API方法内部将存在彼此不连接的代码-更改实体字段,可用字段列表(取决于票证的类型)和状态模型。 它们由于各种原因而发生变化,因此有必要根据不同的API方法和接口进行分配。

如果在API CRUD方法的框架内更改字段不仅是数据更改,而且是与实体状态的协调更改相关的操作,则应将此操作带入单独的方法中,并且不允许直接更改。 如果不向后兼容而更改API(对于公共API)非常不好,那么在设计API时最好立即考虑一下。

因此,为了避免此类问题,最好使接口较小,特定且尽可能以问题为导向,而不是以通用数据为中心的接口。

这种(反)模式通常是RESTful接口的特征,这是因为默认情况下,只有少数几个以数据为中心的“动词”动作可以创建,删除,更新和读取。 没有特定业务实体的运营

如何使RESTful更面向问题?
首先,您可以向实体添加方法。 界面变得越来越安静。 但是有这样的机会。 我们仍然不为种族的纯洁而战,而是解决实际问题

代替通用资源/api/v1/tickets.json添加更多资源:

/api/v1/tickets/{ticket_id}/migrate.json从一种类型迁移到另一种类型
/api/v1/tickets/{ticket_id}/status.json如果有状态模型

其次,您可以将任何操作想象为REST框架内的资源。 是否存在从一种类型到另一种(或从一个项目到另一种的?)的票证迁移操作。 好的,所以会有资源
/api/v1/tickets/migration.json

是否有业务运营来创建试用订阅?
/api/v1/subscriptions/trial.json

是否有汇款操作?
/api/v1/money_transfers.json

等等

具有以数据为中心的API的反模式实际上也指rpc交互。 例如,存在过于通用的方法,例如editAccount()或editTicket()。 “修改对象”不承担与问题区域相关的语义负荷。 这意味着将由于各种原因而调用此方法,由于各种原因而需要更改。

应该注意的是,如果问题区域仅涉及存储,接收和修改数据,则以数据为中心的接口就可以了。

事件模型


解开代码段的一种方法是通过消息队列来组织服务之间的交互。

例如,如果在服务中注册用户时,我们需要向他发送欢迎信,在CRM中创建对客户经理的请求等,那么逻辑上不拨打外部服务电话,而是在注册服务中放置“用户123已注册”消息是合乎逻辑的”,所有必要的服务都会阅读此消息并采取必要的措施。 同时,更改业务逻辑将不需要更改注册服务。

大多数情况下,不仅消息被放入队列,而且事件也被放入队列。 由于队列只是一种传输协议,因此对数据接口和常规同步接口的限制相同。 因此,为了避免在其他服务中更改界面和后续编辑时出现问题,最好使事件尽可能以问题为导向。 此类事件通常仍称为领域事件。 同时,事件模型的使用通常不会极大地影响(微)服务竞争的边界。

由于领域事件实际上是1比1转换为同步API方法,因此有时它们甚至建议使用事件流而不是事件流而不是API调用(事件源)。 通过事件流,您始终可以恢复对象的状态,但也可以拥有免费的历史记录。 实际上,通常这种方法不是很灵活-您需要支持所有事件,并且通常将API与故事保持在一起通常会更容易。

微服务和性能。 Cqrs


原则上,问题领域意味着代码的变化不仅与功能性业务需求相关,而且还与非功能性需求相关,例如性能。 如果有两段具有不同性能要求的代码,则意味着这两段代码可能有意义。 而且它们通常分为单独的服务,以便能够使用更适合该任务的不同语言和技术。

例如,用PHP编写的服务中有一个cpu绑定计算器方法可以执行复杂的计算。 随着负载和数据量的增加,他停止了应对。 当然,作为一种选择,有意义的不是在php代码中进行计算,而是在单独的高性能系统守护程序中进行计算。

作为按生产力原理划分服务的示例之一-将服务分为读取和修改(CQRS)。 通常会提供这种分隔,因为阅读服务和书写的性能要求是不同的。 读取负载通常比写入负载高一个数量级。 读取请求的响应速度要求比写入请求高得多。

客户在搜索商品上花费了99%的时间,在订购过程中仅花费了1%的时间。 对于处于搜索状态的客户,显示速度很重要,并且与过滤器,显示商品的各种选项等相关的功能也很重要。 因此,有必要突出显示一个单独的服务,该服务负责商品的搜索,过滤和显示。 这样的服务最有可能在某种ELK上工作,ELK是具有非规范化数据的面向文档的数据库。

显然,天真地划分为阅读和修改服务可能并不总是一件好事。

一个例子。 对于负责填充产品范围的经理来说,主要功能是可以方便地添加商品,删除,更改和查看商品。 几乎没有什么负担,如果我们将读取和更改分成单独的服务,那么除了需要在服务中进行协调更改时遇到的问题之外,我们从这种分离中不会得到任何好处。

Source: https://habr.com/ru/post/zh-CN469001/


All Articles