【摘要】本文,我们将会涉及到微服务开发过程中最困难部分:数据。使用Spring Boot/Dropwizard/Docker,并不意味着采用了微服务架构。更好地了解自己的领域和数据对实现微服务很有帮助。
众多选择微服务架构的原因中,最重要一条就是团队可以在系统之间以不同速度工作而不影响其它团队成员。我们也期望团队能够自主决定如何以最佳方式实现服务,更快地实现业务上的需求。如果我们希望按照以上方式组织团队,那么我们的系统架构将会开始演进成微服务模式。
为了获得这种自主,需要解决以上问题,但是说总是容易的。我曾经听同事们提过这样的思路:“每个微服务都应该有自己的数据库,服务之间不应该共享数据库”。这个想法意味着:不要在服务间共享服务,因为有可能在并发读写时发生冲突和一致性问题。但是单一数据库模式也可以提供安全和方便性,包括ACID交易,单一查看接口,单一管理接口,等等。
那么应用微服务的时候,如何既能享用原来的安全性,又能将数据库分解成多个不相干的小数据库呢?
首先,对企业级微服务,需要弄清楚如下问题:
专业领域现状是什么样的?
哪里是转换边界?
微服务如何通过边界通信?
仅是把数据共享出来会发生什么?
什么是Domain
许多时候Domain这个概念被忽视了。但是互联网公司和传统企业之间如何实现微服务是非常不同的。
我们基于本领域数据实现微服务之前,必须对数据代表什么有一个清晰的认识。例如,存放关于“Ticket Monster”应用的“booking”的信息到数据库,迁移到微服务之前,我们需要理解什么是“booking”,深入到自己的领域,需要了解什么是“account”,或者一个员工,或者一个报销等等。
要做到这点,看看我们在现实世界中怎么做的。例如:什么事一本书?仔细考虑一下,因为这是一个最简单的例子。我们如何将它用数据模式表示出来呢?
书是由纸张组成的东西吗?报纸是书吗(报纸也有纸张)?也许书有硬质封面?或者并不是每天都发行?如果写一本书,发行商只需要用一行来代表我的书。但是书店有五本这样书,每一个都是一本书吗?或者他们只是拷贝?如何代表他们?如果一本书太长需要分成若干卷,每一卷是一本书吗?还是所有卷合起来是一本书?如果需要把很多构成部分组合在一起,这个组合体是一本书吗?还是每个构成部分就是一本书?本质上,我可以发行一本有很多份拷贝,每本有多个卷的书。
那么,到底什么是一本书?
事实是,没有事实。现实中并没有一个“什么是一本书”的定义,因此回答这类问题,必须知道:谁在问这个问题,上下文是什么?
上人类可以很快(甚至无意识地)解决歧义问题,因为我们头脑中有相关问题的上下文,而计算机却无法实现,我们编辑软件和建模时候需要将上下文清晰化,使用书的例子有些简化这个问题。(企业)领域内的Accounts,Customers,Bookings,Claims等概念只会更复杂或者更有歧义,因此需要一个边界。
我们应该在哪里划界?Domain驱动设计(DDD)社区的工作帮我们解决这些复杂性。我们围绕Entities, Value Objects, 和 Aggregates等帮助我们领域建模的元素画出上下文。换句话说,我们定义了一种代表Domain的模型,此模型被定义上下文的边界包围了。这种方式很明确,边界之内为微服务或者边界之内的元素构成微服务,或者两种都有。每种方式,微服务都与边界相关,DDD也是一样。
我们的数据模型(我们想如何将概念在物理数据库中表现。。。注意这里明确的不同)由我们的域模型驱动,而跟其他因素关系不大。我们一旦有了边界,就可以判断什么是正确的,什么是不正确的。边界也提供了一定程度的自治区域。“A”上下文边界对“书”的理解可能跟“B”上下文边界完全不同(例如,A上下文可能是对某本书的名称做搜索;而B则是对若干书(包括若干拷贝)进行结账,等等)。
有可能有人会说“等一下……Netflix并没有说Domain Driven Design,Twitter和LinkedIn也都没说,为什么要采用DDD呢?”
那么原因在于:
“人们总是看到表面,他们从Netflix拷贝走的是结果,而不是流程”,Adrian Cockcroft,前Netflix首席云架构师说。
微服务之路是一条很不一样的路,每个公司都不一样,并没有普遍适用或者快速的方法,只有折中的办法。因为对某个公司适用的方法就拷贝过来就是只看重结果不注重过程,是不会成功的。而且你的公司也不是Netflix,应用也不会那么复杂。搜索和显示视频,贴tweet,更新linkedin等操作比起保险公司理赔流程简单多了。互联网公司采用微服务是因为市场扩展速度以及扩展的需要(贴一个tweet很简单……而同时为5亿用户贴tweet和显示tweet就是很复杂的事情了)。今天的企业无论在邻域内还是在扩展性方面都遇到前所未有的复杂性。因此接受改变是一个长期过程的概念是明智的,而且每个组织都不同,不能忽略它。
交易的边界是什么?
回到这个故事,我们需要DDD帮助我们理解用于实现系统和画出上下文边界的模型,因此我们需要接受Customer,Account,Booking等概念对不同边界上下文有不同意义。到本文后边,我们会在架构内部完成相关概念,而且我们也需要一种当发生变化时,在不同模型之间反应变化的方式。我们需要考虑这一点,但是首先需要分辨出交易边界。
不幸的是,作为开发者我们都采用一种一直错误的方法建立分布式系统:我们始终围绕着单一的,关系型的, ACID,数据库,我们忽略了异步不可靠网络的危险。换句话说,我们一直采用一种不需要知道网络具体信息的框架(包括RPC框架,数据库抽象层),以便用点对点同步方式完成任何事情(REST, SOAP, other CORBA like object serialization RPC libraries, 等等)。我们开发系统时也不区分是否授权或者自治的状态,最终在多种服务之间通过两阶段提交实现分布式数据问题。或者我们能否忽略所有这些问题?这种观念模式会我们引向不可扩展的系统,不管称呼他们为SOA,微服务,小服务等等。
那么我说的交易性边界是什么含义呢?它意味着商业逻辑上最小不变原子单位。无论是使用数据库ACID属性还是两阶段提交,都没有区别。关键在于交易性边界尽量小(理想条件下是一个对象;Vernon Vaughn有一系列短文描述DDD方法),这样就可以很容易地扩展。当创建域模型时,使用DDD术语,我们可以定义Entities, Value Objects 和 Aggregates。Aggregates在上下文中是包含其他Entities/Value的对象,负责保持原子的不变性(边界内上下文会有多种Aggregates)
例如,假设我们有如下用例:
允许客户搜索航班信息
允许客户在特定航班选座位
允许客户预定航班
这里我们有三种边界上下文:搜索,订单和出票(应该还有其它诸如支付、会员、等待,升舱等操作,但这里我们假设就这三种)。搜索定义为显示特定时段和起始地之间航班信息;订单定义为将客户信息(客户名、地址,常旅客号码等),座位偏好和支付信息与订票流程挂钩;出票定义为从航线预留座位并确定票。每个边界上下文,我们想确定原子交易的边界,我们并不考虑跨边界的原子交易(会在下一节中讨论)。
考虑到我们希望很小交易边界(这是很简单的订机票流程),如何建模呢?也许是一个航班集合,其中包含例如时间,日期,航线以及如客户,航班和订单信息?看起来很合理:一架航班有机型、座位等生成订票信息目的的信息。从数据库内数据角度来看是有帮助的,或者从源码实现很好的对象模型(构成或者继承),但是让我们来看看会发生什么。
在订票过程中真有不变量吗?航班,机型等?如果我们添加了新机型到航班类中,我们真的需要加入客户和订票信息吗?也许不需要。我们真正需要的是一个集合具有组合以及方便数据模型。然而,交易性边界太大,如果有很多航班、座位、定位信息改变,就会产生大量交易冲突(使用乐观或者悲观所并不重要),而且也不支持扩展(定位失败并不重要因为航班改变对用户来说更加难以接受)。
那么我们试图稍微改变一下交易性边界。
也许定位、座位有效和航班是各自独立的类别。定位包括客户信息,偏好和交易信息;座位有效则包括飞机和机型配置。航班则包括计划,路线等。。。但是我们可以在不影响航班计划和飞机座位有效情况下创建定位信息。从域角度来看,我们期望能够实现。我们不希望100%在航班、机型和定位之间保持一致,但是对于管理员来说能够正确记下航班时刻,提供商能够获得机型配置,客户能够获得定位信息。因此我们如何实现例如“从某个航班选位”的需求呢?
在定位流程中我们有可能需要访问座位有效集,要求预留一个座位。定位操作会座位单一交易完成,例如,(保留座位23A)返回一个预留ID。我们可以将此ID跟定位关联起来,提交定位系统知道座位被“预留”了。这种操作(预留座位,接受定位)每个都是独立交易,可以互不相关,因此也不需要两阶段提交或者锁定。注意,使用“预留”是业务需求,我们不需要预先指定座位,只需要预留一个座位。这种需求解脱了潜在的问题因为这种使用场景只是简单要求“允许客户选择一个座位”。开发人员可能会理解为“从剩余座位中挑一个,指定给某个客户,从可用项中删除,不要超售票”,这些都是额外的需求,会给业务无端添加麻烦。对业务端来说预定而不是指定一个座位以及超售座位都是没关系的。
以上只是一个简单的,允许真实的域朝更小、更简单但是不同集合之间有完全原子级边界的例子。但是并不能就这样结束了,因为这些并行的交易必须被修正。数据不同部分被引入(也就是说,我创建了订票和座位预留,但是他们并不能解决打印登机牌和票据等交易。
微服务如何在边界之间通信
我们试图保证真正业务不变量不受影响。有了DDD我们可以选择将这些不变量作为一个集合,使他们使用单一交易集合的模型。有时候可能在单一交易里更新多个集合的场景(通过一个或者多个数据库),但是这种场景应该是特例。我们仍然需要在集合之间维持某种程度的一致性(最终在边界上下文中达到一致性),那么我们应该怎么办?
我们应该理解:分布式系统有很多限制。在给定时间里分布式系统能确保的事情很少(例如,异常地慢或者表现起来失败,系统时间不同步等),需要及时去解决这些问题吗?如果能把这些问题跟跨域一致性模型捆绑起来会不会更好?如果我们能够在“必要交易边界之间我们可以容忍这些问题,而在以后的某个时间实现一致性”是不是更好?
如我们所说,微服务更适合自治区域。以便实现无关联升级。这种从时间上,服务之间以及边界时间充分解耦使得我们可以获得这种自治(这对计算机系统来说不是独特的……)。因此,在交易边界和边界上下文之间,使用事件通信来获得一致性。事件是在不同之间传递信息不可变的结构。大家侦听感兴趣事件并相应作出对数据的处理。
继续讨论订票系统例子。假设订票系统。当一个订票操作通过ACID风格交易,我们如何最终出票呢?这也是为什么需要上述的订票边界上下文。订票上下文发布一个类似于“NewBOokingCreated”的事件,出票系统上下文会收到这样的事件并与后台票务系统联系。很显然这需要类似于Apache Camel这类数据转换。而且也带来其它一些问题,我们如何写入数据库并且发布消息序列?事件之间如果有需求怎么办?每个数据库都有一个服务吗?
理想环境下,我们的结合会直接使用命令和域事件(对于头等舱旅客……操作都以命令方式实现,响应都以对事件的回应方式实现),我们可以更加清晰地在内部使用和上下文之间使用的事件之间做出映射。我们应该只将事件发布到消息队列,然后消费者从队列侦听,将数据插入数据库,而不需要使用XA/2PC。我们可以将事件插入一个专用事件商店,用起来就像一个数据库或者事件发布专题。或者只能继续使用ACID数据库和流改变数据库达到一致性,像Apache kafka用Debezium复制日志,通过使用某种事件/留处理来减少事件。无论哪种方式,目的是想在边间之间传递不可更改的时序事件。
可能的交易模型
组件之间更改互不影响
可以决定或快或慢影响外界,最终实现一致性
可以把数据存放在数据库中,然而使用更好的技术方式
可以很方便更改模板/数据库
更加健壮。然后你还必须将注意力放在CAP理论以及选择的技术实践。
同样,也有不足支持:
* 更负责
* 拍错更困难
* 因为事件接受有时差,不能预测别的系统知道什么
* 使用起来更困难
* 你还必须将注意力放在CAP理论以及选择的技术实践。
这里我特意将“CAP理论”都列出来是因为尽管带来了一些困难,但是还是迫切需求的。我们总是对不同形态数据一致性和并发性更感兴趣。完全依靠“ACID中的数据库”不再能接受。
另外一个有趣的概念是是否能够实现“Command Query Separation Responsibility”,我们将读写模式分开到不同服务中。记住我们很遗憾互联网公司并没有很复杂的域模型。从他们写模型很简单就可以得到证明(例如在分布式日志中插入一个tweet)。然而因为其规模,读模式则是非常复杂。CQRS帮我们分解这种问题。一方面,企业内部写模型可能比读模型更加复杂,CQRS是一个很强大的在边界之间处理读写的分隔工具
那么对于只有一个数据库,并且不对外分享的服务呢?这种情况下,必须有侦听者监听这个事件流,并且将数据插入共享数据库中,最终主集合会用到他们。这种“共享数据库”很棒。记住,没有规则,只有折中。这个实例中可能会有多个服务跟同一个数据库打交道,只要我们自己看护自己的进程,就不要用否定自治区域的好处。因此当听见某人说“一个微服务应该有自己的数据库,不与人共享任何东西”时,应该置之一笑。
如果我们把数据库外露会怎么样?
如果我们把上一节中的内容发挥到极致会怎么样?如果我们坚持使用事件/流,并且这些时间都不更改?如果我们坚持认为数据库/caches/indexes都是过去出现的一致性日志/流的现代视角?
这种方法带来比通过事件通讯更多的好处(如上所列):
现在可以认为数据库是“当前状态”的记录,而不是一条真记录:
可以重新读过去事件,用“什么会发生”的视角检视它们
可以监控日志
重新检视事件可以带来一种全新视角
可以更容易看待数据库升级变化,认为他们可以通过在数据库中回放事件来实现。
可以方便迁移到新数据库技术
更多信息可以参看Martin Kleppmann的博客“http://www.confluent.io/blog/t ... amza/”
或者united.com订机票时,可以感受到以上的概念。当选择座位时候,才最终被分配一个座位,而之前只是预留了一个。当定机票时,并没有真拿了一张票,之后会通过邮件通知确认。你一定也有改票而被分配了一张完全不同的座位经历?或者在登机口听见航空公司因为超售而征询自愿者放弃本架飞机?这些都是交易边界的例子,最终一致,补偿性措施,甚至道歉。
总结
本文中心是数据,数据整合,数据边界,企业使用模式,分布式系统理论,时序等等,他们都是微服务最困难的部分(因为微服务就是分布式系统)。技术上我看到过太多迷惑(“如果我使用Spring Boot,我会使用微服务”,“使用微服务前,我需要在云端解决服务发现,负载均衡”,“必须为每个微服务配置一个数据库”)和无用的规则。别急,一旦选择了提供商,购买了产品,仍然会碰到上面提到的所有难题。