打走企业级落地微服务的拦路虎:数据

Markdown

▲扫码报名活动 数人云11月Meetup报名开启, 看中西方大神如何论道云原生与微服务!

数人云上几天给大家分享了:《踢开绊脚石:微服务难点之服务调用的解决方案》剖析了微服务的难点之一:服务调用的解决方案。今天再来跟大家聊聊微服务的另外一个难点:数据。

在尝试使用微服务架构的因由中,最主要的是允许团队能够以不同的速度在系统的不同部分进行工作,而且尽量将影响团队的相关因素最小化。因此,我们希望团队能够自治,做出关于实现和运营服务最好的决策,并且可以自由地进行更改。

为了获得这种自主权,需要“摆脱依赖”,很多人在某种程度上都引用了这句话,因为“每个微服务都应该拥有和控制自己的数据库,而不是两个服务去共享一个数据库。”这种理念是合情合理的,不要在服务之间共享一个数据库是因为会遇到读/写模式冲突、数据模型冲突、协调性问题等等。

当构建微服务时,如何安全地将数据库分切成多个小的数据库?首先对于企业级构建微服务,需要明确以下内容:

  • 域是什么?它实现了什么
  • 事物边界在哪里?
  • 微服务如何跨越边界进行通信?
  • 如果将数据库打开,会怎样?

什么是域

这似乎在很多地方都被忽视了,但在互联网公司如何实践微服务和传统企业如何(或者可能因为忽视了这一点而失败)实现微服务之间存在的巨大差异。

在构建微服务之前,以及它使用的数据(产品/消费等)的原因,需要对数据的表示有一个明确清晰的理解,例如,在我们可以将信息存储到一个数据库中,关于TicketMonster的预定和它向微服务的迁移,需要了解“什么是预定”,就如同在其他领域里需要了解什么是账户、什么是雇员、什么是索赔等等。

要做到这一点,需要深入了解现实中的“IT”是什么,例如,“什么是书?”,试着停下来想一想,因为这是一个很简单的例子,试着去想什么是一本书,那么,如何在数据模型中表达这一点呢?

关于“什么是一本书”没有一个客观的定义,因此,要回答这样的问题,必须知道“谁在问问题,什么是背景”,需要根据上下文去进行判断。作为人类,我们可以迅速(甚至是无意识地)解决着一理解的模糊性,因为在头脑、环境和问题中都有一个背景,但计算机没有,当构建应用并对数据建模时,需要明确地说出上下文,比如上面提到的企业拥有账户、用户、预定索赔等等,这让整个应用变得更为复杂,并且会产生很多的冲突和模糊,所以,需要界限。

在哪里划定界限?域驱动设计社区中的工作帮助处理整个域的复杂性,在实体、值对象和集合中绘制一个有界的上下文,换句话说,构建和细化一个代表域的模型,整个模型包含一个定义上下文的边界内,这些边界最终会成为微服务,或边界内的组件最终会成为微服务,抑或两者都是,无论哪种方式,微服务都是关于边界的,DDD(领域驱动设计)也是如此。

Markdown

数据模型(希望如何在物理存储中表示的概念——注意,这里的显示差异)是由领域模型驱动的,当有这个边界时,可以知道并断言,在模型中什么是正确的,什么是不正确的,这些界限也意味着一定程度的自制,有界上下文的“A”可能对“Book”有不同的理解,而不是有界上下文的“B”(例如,可能有界上下文A是搜索服务,搜索标题为“Book”的标题,也许有界的上下文“B”是一种结账服务,它根据你购买的数据(标题+副本)来处理交易)。

关于DDD:

一些人试图复制Netflix,但他们只是复制了他们看到的那些,比如结果,而不是整个过程——Adrian Cockcroft,前Netflix首席云架构师。

对于不同的来说,微服务也是不尽相同的,没有一个硬性规定,只是根据自身实际情况去权衡,复制对一家公司有用的东西,仅仅因为它在这一刻似乎起到了作用,但跳过了过程以及其中的尝试,不会起到决定性的作用,需要注意的一点是,你的公司并不是Netflix,事实上,无论域在Netflix上有多么的复杂,它都要比你的传统企业简单的多。一些互联网公司之所以想实践落地微服务,是因为它们的速度非常快,而且规模庞大(在Twitter上发布一条推文很简单,但为5亿用户发布推文和显示Tweet流是非常复杂的),今天的企业将不得不面对领域和规模的复杂性,因此,接受这样一个事实:对于每个企业来说,都是不同的。

什么是事务边界

回到上文所设定的环境当中,需要一些如领域驱动设计这样的东西来帮助我们理解将要使用的模型来实现系统并在一个上下文中划定这些模型的边界,因此,接受客户、账户、预定等可能对不同的有界上下文意味着不同的东西,但最终,可能会在体系结构中分布这些相关的概念,但当发生变化时,需要一些方法协调这些不同的模型变化,需要对此进行解释,但在这之前,首先需要确定事务边界。

然而不幸,作为开发人员似乎仍然在构建分布式系统:仍然在查看一个单一的、关系型的、ACID的Database。也忽略了一个异步的、不可靠的网络危险,也就是说,做一些事情如编写一些奇特的框架,使我们不必对网络有任何了解(包括RPC框架、数据库抽象也可以忽略网络),并尝试用点对点同步调用(REST、SOAP、其他CORBA,比如对象序列化RPC库等等)来实现所有的东西。在不考虑权威和自治的情况下构建系统,最终试图通过许多独立服务来解决分布式数据问题,比如两阶段提交,或者把这些顾虑都忽略了?这种思维模式会导致构建非常脆弱的系统,而这些系统不具有规模,若把它称为SOA、微服务、迷你服务等等,那就无关紧要了。

交易边界的意思是需要最小的单位原子性,这是业务不变量的,无论使用数据库的ACID属性来实现原子性还是两段提交等都不重要,关键是想要使这些事物边界尽可能小(理想的是单一对象的单一事物:Vernon Vaughn有系列的文章描述了这种方法,用DDD聚集),这样就可以伸缩了,当使用DDD术语构建领域模型时,会识别实体、值对象和聚集,在这个上下文中,聚集的对象是封装其他实体/值对象的对象,并负责执行不变量(在一个有界的上下文中可以有多个聚合体)。

例如,假设有以下用例:

  • 允许客户搜索航班
  • 让客户在特定航班上选择座位
  • 允许客户预定航班

可能会有三个有界的上下文:搜索、预定和票务(也许会有更多的支付、、待机、升级等等,但会将它缩小到这三个)。搜索负责显示特定路线的航班和特定时间的路线(天数、时间等等),预定将负责在预定过程中使用客户信息(姓名、地址、常旅客号等)、作为偏好和支付信息进行预定,票务将负责与航空公司的订位,并签发机票,在每个有界的上下文中,希望确定事务边界,在那里可以执行约束/不变量,不会考虑跨界上下文的原子事务,若想要小的事务边界(这是预定航班的一个非常简化的版本),改如何建模呢?可能是一个包含时间、日期、路线和像客户、飞机以及预定这样的实体航班集合?因为航班都有飞机、作为、客户和预定。为创造预定,飞行总机负责跟踪飞机、座椅等。这可能从数据库内部的数据模型角度(带有约束和外键的关系模型)或者在源代码中创建一个不错的对象模型(继承/组合),但来看看会发生什么:

Markdown

在所有的预定、飞机、航班等方面是否真的存在不变量,只是为了创造一个预定?也就是说,如果在航班总数上增加了一架新飞机,是否应该在这一交易中包含客户和预定?可能不会,在这里所拥有的是一种聚合的聚合以及数据模型的便利,然而,事务的边界太大了,如果对航班、作为、预定等进行大量的更改,就会有巨大的事务冲突(无论是使用乐观的还是悲观的锁定都不重要),而且这显然没有规模(永远不要介意订单的不断下降,因为航班计划正在改变,这是一种糟糕的客户体验)。

如果打破了交易的界限,那就更小了。

也许预定、座椅可用性和航班是它们自己的独立集合,预定封装了客户信息、首选项和支付信息,Seat可利用聚合封装了飞机和飞机的配置,航班集合是由时间表、路线等组成的,但可以在不影响航班时刻表和飞机/座位可用性的情况下进行预定,从领域的角度看,希望能够做到这一点,不需要100%的严格一致性,但确实想要正确地记录飞行时间表的变化,如管理员,飞行的配置,以及客户的预定,那么,如何实现“在飞行上预定一个座位,”这样的事情呢?

在预定过程中,可能会呼叫座椅可用性集合,并要求它在飞机上预定一个作为,这个作为预定将被实现为一个单一的事务,例如(持有作为23A),并返回一个预定ID,可以将这个预定ID于预定联系起来,并且提交,知道这个座位是在一个“预定”的每一个(保留一个座位,并接受预定)都是单独的事务,并且可以独立地进行,而不需要任何两阶段提交或两阶段锁定。注意,这里使用“预定”是一个业务需求,不做座位分配,只保留座位,这个需求可能需要通过模型的迭代而被束缚,因为最初使用用例的语言可能只是简单地说“允许客户选择一个座位”,开发人员可以试着推新,这个需求意味着“从生育的座位中挑选,把它分配给客户,从库存中移除它,并且不出售比座位更多的票”。这将是额外的,不必要的不变量,这会给事务模型增加额外的负担,而业务模型实际上并不是不变的,在没有完全的座位分配情况下,这个业务当然可以接受预定,甚至是超额销售机票。

Markdown

上图为一个示例,允许真正的域引导使用更小的、简化的、完全原子的事务边界来处理单个聚合,现在必须纠正这样一个事实:所有的个体交易都需要在某一时刻聚集在一起,数据的不同部分涉及到(例如,创建了一个预定和座位预定,但这些都不是通过固定的交易来获得登机牌/机票等)。

微服务如何跨边界通信

想要保持真正的业务 不变性,使用DDD可以选择将这些不变量建模为聚合,并使用单个事务进行聚合,在某些情况下,可以在单个事务中更新多聚合(跨单个数据或多个数据库),但这些场景可能是例外,仍然需要在聚合之间保持某种形式的一致性(最终在有界的上下文之间),那么应该怎么做呢?需要理解的一件事是:分布式系统是非常挑剔的,如果能在有限的时间内对分布式系统中的任何东西做出任何保证(事情将会失败,事情是不确定的,或者似乎失败了,系统有非同步的时间边界等等),那么为什么要尝试去解决它呢?如果接受并将其放入领域的一致性模型中会怎样呢?如果说“在必要的事务边界之间,可以与数据和域的其他部分进行协调,并在以后的某个时间点保持一致”?

正如一直说的,对于微服务,我们重视自主权,认为能够独立于其他系统(在可用性、协议、格式等方面)进行更改,时间的解耦和任何有限制的时间之间任何保证之间的任何保证,都允许真正实现这种自治(这不是计算机系统特有的,因此在事务边界和有界上线问之间,使用事件来保持一致性,事件是不可变的结构,它捕获了一个有趣的时间点,应该向对等点广播,同伴们会聆听它们感兴趣的事件,并根据这些数据做出的决定更新自己的数据等等。)

继续以订机票为例,当一个预定通过一个Acid风格的交易存储时,如何结束它?这是前面提到的“退票界”的背景,预定有界上下文将发布类似“New Booking创建”这样的事件,而票务绑定上下文将会消耗该事件,并继续与后端(可能是遗留的)票务系统进行交互,这显然需要某种集成和数据转换,这是Apache Camel非常擅长的。也引出了一些其他的问题,如何对数据库进行写操作,并以原子的方式将其发布到队列/消息传递设备中?

如果在事件之间有排序需求/因果需求呢?每个服务的一个数据库呢?

理想情况下,聚合会直接使用命令和域事件(作为第一个类公民,也就是说,)任何操作都是作为命令来实现的,任何响应都是作为对事件的响应来实现的)可以更清晰地映射使用内部上下文和上下文之间使用的事件之间关系,可以将事件(即Newbooking创建)发布到消息队列中,然后让侦听器从消息队列中使用该队列,并将其插入到数据库中,而无需使用Xa/2pc事务,而不需要将其插入到数据库中,可以将事件插入到一个专用的事件存储中,它就像数据库和消息发布-订阅主题一样(这可能是首选路由),或者可以继续使用一个ACID数据库,并将该数据库的变化流到一个持久的、复制的日志中,就像Apache卡夫卡一样,使用类似于Debezium的东西,并使用某种事件处理器来推断事件,无论哪种方式,关键是想要在时间事件中与不可变点之间的边界进行通信。

Markdown

这带来了一些巨大的优势:

  • 避免了跨边界且昂贵潜在的不可能的事务模型
  • 可以对系统进行更改,而不妨碍系统的其他部分(时间和可用性)的进展
  • 可以将数据存储在自己的数据库中,同时使用适合于服务的技术
  • 可以在空闲时更改模式/数据库

  • 变得更加可伸缩、容错、灵活

必须更加关注CAP定力和选择的技术来实现存储/队列

  • 需要注意的是,这也有一些缺点:
  • 更加复杂
  • 难以调试
  • 由于看到事件时有延迟,所以不能对其他系统所知道的内容作出任何假设
  • 必须更加关注CAP定理和选择的技术来实现队列/存储

从这种方法中产生另一个有趣的概念是,能够实现一种称为“命令查询分离责任”的模式,在这种模式中,将读模型和编写模型分离到单独的服务中,请记住,我们对互联网公司没有非常复杂的领域模型感到遗憾,这在他们的编写模型中很明显(例如,在一个分布式日志中插入一条Tweet)。然而,他们的阅读模式因其规模而变得非常复杂,CQRS帮助分离了这些问题,另一方面,在企业中,编写模型可能非常复杂,而读取模型可能是简单的平面选择查询和扁平DTO对象,CQRS是一种强大的关注点隔离模式,一旦有了适当的边界和在聚集和有界上下文之间进行数据更改的好方法,就可以对其进行评估。

那么服务只有一个数据库,并且不与其他服务共享呢?在这个场景中,可能会侦听事件流的侦听器,并可能将数据插入到一个共享数据库中,而主聚合可能最终会使用这些数据,这个“共享数据库”非常好,记住,没有规则,只有权衡,在这种情况下,可能会有很多服务协同同一个数据库一起工作,只要团队拥有所有的过程,就不会否定自治的优势,当听到有人说“微服务应该有自己的数据库,而不是别人分享”时,你可以回答:好吧,有点。

打开数据库

如果将使用事件/流来进行所有的事情,并且永远的持久化这些事件呢?如果说数据库/缓存/索引实际上只是过去发生的事件的持久日志/流的物化视图,而当前状态是所有这些事件的左折叠?

这种方法带来了更多的好处,可以通过事件(上面列出的)来增加交流的好处:

  • 现在可以将数据库看做是记录的:“当前状态”,而不是真正的纪录
  • 可以引入新的应用,重新阅读过去的事件,并根据“将要发生的事情”来检查它们的行为
  • 免费完善审计日志记录
  • 可以引入应用的新版本,并通过重新播放事件对其执行非常详尽的测试
  • 可以更容易地关于数据库版本/升级/模式变化的事件重演到新数据库中
  • 可以迁移到全新的数据库技术(例如,可能发现已经超越了关系数据库,并且希望切换到专门的数据库/索引)

Markdown

TM体系结构

当在aa.com、Delta.com或united.com上预定航班时,会看到其中的一些概念,当选择一个座位时,并没有实际地分配它。当订机票时,实际上并没有收到实体票,之后会收到一封电子邮件,通知已经被确认,你是否有过一次飞机换班的经历,并分配到一个不同的座位?或者是去登机口,听到工作人员要求放弃座位,因为机票已经超出预定座位,这些都是事务边界的例子,最终的一致性、补偿事务,甚至连工作中的道歉都属于。

Moral Of The Story

这里指的是:数据、数据集成、数据边界、企业使用模式、分布式系统理论、时间等等,都是微服务的难点(因为微服务是真正的分布式系统)在技术上看到了太多的困惑(“如果使用Spring引导,我正在做微服务”,“需要解决服务发现,在云计算之前负载均衡”,必须有一个每个微服务的数据库)和关于使用微服务的无用理论,别担心,因为一旦大的供应商来卖给你所有的高级产仍然需要做上面列出的那些困难部分。

原文作者:Software 原文链接:http://www.tuicool.com/articles/VnIvyyF