封装并不总是对的
微服务的价值在于,它是独立可部署的,这一点使得它具有可伸缩性。这个可伸缩性可以是从数据量或用户量的角度来说的,但更多的是从团队和组织角度来看的。
但是独立性本身也是一把双刃剑。服务可以自主的快速迭代;但是如果一个服务实现了一个特性,而因此要求其他服务也做出变化,那么两个服务将不得不同时变更。尽管在单体应用中很容易的实现这种变更,但在微服务架构下实现这种同步变更要难得多,团队间以及发布周期上的协调损害了原本的敏捷性。
通常,为了避免这种麻烦的跨服务变更,我们会在服务间清晰的划清责任边界。以单点登陆服务为例,它清晰的定义了自身所扮演的角色,与其他服务所扮演的角色清晰地划分开。这样一来,即使其他服务出现了剧烈的需求变化,单点登陆服务也不大需要修改。对于这样的服务,我们说它存在于一个严格的限界上下文中。
但问题是,真实环境中的业务服务通常并不能保持这样的 清晰的关注点隔离。例如,业务服务不可避免的重度依赖于彼此的数据。在线商城系统中,大量的服务有着对 订单、产品类目、客户信息 等的需求,需要广泛的访问这些数据集以完成自身的工作。
大部分的业务服务分享相同的事实流,这样他们将不可避免的交织在一起。因此,需要注意的一点是:虽然从基础设施层面来看,服务可以很大程度上独立存在,但是大多数业务服务的未来却紧密的交织在一起。
数据二元性
基于服务的实现方式由来已久,但却很少关注如何在服务间共享重要的数据集。根本问题在于,数据和服务不总是能够完美配合。一方面,我们鼓励封装以隐藏数据,达成服务间解耦以便各服务能够独自演进;另一方面,我们又希望像使用数据系统一样,以各种方式自由地使用共享数据。
但数据系统完全不考虑封装,而是恰恰相反。数据库尽其所能的暴露它持有的数据。他们提供了非常强大的声明式接口以方便用户对数据进行各种转换。
因此,在如何对待数据上出现了矛盾,即数据二元性:数据系统要暴露数据,而服务要做隐藏。
随着微服务系统的演进,这种数据二元性以各种不同方式展现出来。
(1)为了满足越来越多的数据需求,服务接口开始膨胀,到一定程度它就开始越来越像某种古怪的自研数据库了。
且不说,共享数据库是微服务设计首先要避免的。更糟糕的是,数据量实际上放大了服务边界问题。隐藏在服务边界内的共享数据越多,接口可能变得越复杂,而且跨服务间的数据集联结(join)变得更难。
(2)或者,为了共享数据,我们在服务间大量的抽取和移动整个数据集。通常,我们会抽取和移动整个数据集,然后保存在每一个(消费方)服务的本地数据库中。
这里的问题是,不同的服务对这些数据有着各自的解读。相同的数据在各个地方保存,并且在本地进行改变和修订。很快,这些数据就面目全非,再也不代表源数据集了,即数据出现了分化。
数据的可变副本越多,随着时间的推移,数据的差异就越大。更糟糕的是,分化的数据是很难修订的。实际上,一部分业务上遭遇的最难解决的技术问题就来源于各应用间大量存在的分化数据集。
为了解决这个问题,我们需要以一种稍微不同的方式来思考共享数据。我们应当将它们当作架构中的一等公民来看待。我们对数据进行封装是为了不暴露服务的内部状态;但是另一方面我们也需要让共享数据更容易的为服务所访问,以便服务完成自身的工作。这部分暴露出来以供访问的共享数据,我们称之为外部数据。
在我们现有的各种架构方案中,服务接口、消息传递或者共享数据库,没有一个针对外部数据提供了很好的解决方案。服务接口不适合任何规模的数据共享;消息传递能够移动数据,但不提供历史参照,导致随着时间的推移发生数据损坏;共享数据库将大量的数据集中于一处,降低了敏捷性。最终,我们不可避免的陷入了数据不足的死循环中。
数据不足的死循环数据流:解决数据和服务的分布式方案
从前面的讨论中,我们看到对于数据的处理方式一定要解决好数据二元性的问题。虽然没有所谓的技术魔法能够轻而易举的解决这个问题,但是我们可以对问题进行重新定义,以选择一个微妙的折衷方案。
这个折衷方案涉及一定程度的集中。我们可以使用一个分布式日志(Distributed Log)来解决这个问题,因为它提供持久性、可伸缩的数据流。我们想让微服务联结(join)以及操作这些共享数据流,同时也想避免出现一个复杂、集中式的“上帝服务”来做这种类型的处理。因此,一个更好的方式是,将流式处理嵌入每一个(消费)服务中。这意味着,服务可以联结各种共享数据集,同时又可以按照自己的进度迭代它们。
可以使用流式平台达成这点,可选择的产品有几个,不过这个我们主要考虑 Kafka,因为它的 有状态流式处理(Stateful Stream Processing)非常适合处理这类问题。
使用分布式日志,使得我们可以通过消息传递来实现事件驱动的服务。因为将流程控制从发送者转移给了接收者,这种方式通常比请求/响应的方式能够提供更好的伸缩性和解耦。也因此增加了每一个服务的自主性。当然,这也是有代价的:你需要一个 broker。不过对于重要系统而言,这种代价还是值得的(虽然对于一般的web应用,意义不大)
如果 broker 是一个分布式日志,而非传统的消息传递系统,一些额外的特性就可以好好利用起来了。数据传输可以像分布式文件系统一样线性扩展,数据也可在日志中保存很长时间。因此它既可以用作消息传递,也可以用作数据存储。而且存储的数据不可变,避免了那些共享的可变状态带来的问题。
另一方面,可以通过 有状态流式处理引擎 将数据库的声明式工具嵌入(消费)服务中。这一点很重要:尽管数据保存在所有服务共享的数据流中,但是服务所作的联结和处理是私有的。每个限界上下文内的处理逻辑是互相隔离的。
通过共享不可变的状态流来解决数据二元性问题。使用有状态流处理引擎将功能封装在每个服务内。因此如果你的服务需要操作公司的订单、产品类目或库存信息时,它可以尽情访问:你来决定哪些数据集要组合在一起,决定在哪里执行,决定何时以及如何演进。这意味着,尽管数据本身是共享的,但对该共享数据的操作是完全分布式的,它位于每一个服务的边界内。
以一种忠实于源的方式共享数据。不是在源头封装功能 ,而是在需要它的每个服务中。但是有时候数据还是不可避免的需要整体移动。有时,服务需要在自己的数据库引擎中本地保存历史数据集。这里的诀窍是确保这种数据拷贝可以通过分布式日志重新生成。Kafka 的 Connector 可以用来做这个事。
总结一下这个解决方案的优点如下:
◽ 数据作为流被共享,可以长期保存,但是共享数据的操作机制又是嵌入在每一个限界上下文内的,这使得快速而自由的迭代变得容易。
◽ 数据集可以很容易地在不同的服务之间进行联接。和共享数据的交互变得容易了,很多时候不再需要维持本地数据集。
◽ 有状态流式处理仅仅是缓存数据,权威数据依旧是共享日志,因此数据分化问题不再出现。
◽ 服务本质上是事件驱动的,这意味随着数据集的增长,服务仍然能够快速响应业务事件。
◽ 伸缩性问题从服务转移到了 broker。这样,可以构建更简单的服务,不用再担心伸缩性的问题。
◽ 添加新的服务,不会再要求上游服务也改变。这样,插入新的服务变得更简单了。
数据二元性描述了我们在构建业务服务当中需要面临的矛盾冲突。我们应当注意这个事实。诀窍是,换个思路考虑这个问题:将共享数据视为一等公民,并围绕这一点而设计。借助有状态流式处理,避免了有损敏捷性的中心化的 “上帝组件”,同时又具备数据管道的即时性、可伸缩性和容错性。任何服务都可以访问所有共享数据,同时根据自身的要求做出决定。这使得服务更具伸缩性、可替换性和自治性。
网友评论