2004年Eric Evans 发表Domain-Driven Design –Tackling Complexity in the Heart of Software (领域驱动设计),简称Evans DDD。领域驱动设计分为两个阶段:

以一种领域专家、设计人员、开发人员都能理解的通用语言作为相互交流的工具,在交流的过程中发现领域概念,然后将这些概念设计成一个领域模型;
由领域模型驱动软件设计,用代码来实现该领域模型;

由此可见,领域驱动设计的核心是建立正确的领域模型。

为什么建立一个领域模型是重要的

领域驱动设计告诉我们,在通过软件实现一个业务系统时,建立一个领域模型是非常重要和必要的,因为领域模型具有以下特点:

  1. 领域模型是对具有某个边界的领域的一个抽象,反映了领域内用户业务需求的本质;领域模型是有边界的,只反应了我们在领域内所关注的部分;
  2. 领域模型只反映业务,和任何技术实现无关;领域模型不仅能反映领域中的一些实体概念,如货物,书本,应聘记录,地址,等;还能反映领域中的一些过程概念,如资金转账,等;
  3. 领域模型确保了我们的软件的业务逻辑都在一个模型中,都在一个地方;这样对提高软件的可维护性,业务可理解性以及可重用性方面都有很好的帮助;
  4. 领域模型能够帮助开发人员相对平滑地将领域知识转化为软件构造;
  5. 领域模型贯穿软件分析、设计,以及开发的整个过程;领域专家、设计人员、开发人员通过领域模型进行交流,彼此共享知识与信息;因为大家面向的都是同一个模型,所以可以防止需求走样,可以让软件设计开发人员做出来的软件真正满足需求;
  6. 要建立正确的领域模型并不简单,需要领域专家、设计、开发人员积极沟通共同努力,然后才能使大家对领域的认识不断深入,从而不断细化和完善领域模型;
  7. 为了让领域模型看的见,我们需要用一些方法来表示它;图是表达领域模型最常用的方式,但不是唯一的表达方式,代码或文字描述也能表达领域模型;
  8. 领域模型是整个软件的核心,是软件中最有价值和最具竞争力的部分;设计足够精良且符合业务需求的领域模型能够更快速的响应需求变化;

领域通用语言(UBIQUITOUS LANGUAGE)

我们认识到由软件专家和领域专家通力合作开发出一个领域的模型是绝对需要的,但是,那种方法通常会由于一些基础交流的障碍而存在难点。开发人员满脑子都是类、方法、算法、模式、架构,等等,总是想将实际生活中的概念和程序工件进行对应。他们希望看到要建立哪些对象类,要如何对对象类之间的关系建模。他们会习惯按照封装、继承、多态等面向对象编程中的概念去思考,会随时随地这样交谈,这对他们来说这太正常不过了,开发人员就是开发人员。但是领域专家通常对这一无所知,他们对软件类库、框架、持久化甚至数据库没有什么概念。他们只了解他们特有的领域专业技能。比如,在空中交通监控样例中,领域专家知道飞机、路线、海拔、经度、纬度,知道飞机偏离了正常路线,知道飞机的发射。他们用他们自己的术语讨论这些事情,有时这对于外行来说很难直接理解。如果一个人说了什么事情,其他的人不能理解,或者更糟的是错误理解成其他事情,又有什么机会来保证项目成功呢?

在交流的过程中,需要做翻译才能让其他的人理解这些概念。开发人员可能会努力使用外行人的语言来解析一些设计模式,但这并一定都能成功奏效。领域专家也可能会创建一种新的行话以努力表达他们的这些想法。在这个痛苦的交流过程中,这种类型的翻译并不能对知识的构建过程产生帮助。

领域驱动设计的一个核心的原则是使用一种基于模型的语言。因为模型是软件满足领域的共同点,它很适合作为这种通用语言的构造基础。使用模型作为语言的核心骨架,要求团队在进行所有的交流是都使用一致的语言,在代码中也是这样。在共享知识和推敲模型时,团队会使用演讲、文字和图形。这儿需要确保团队使用的语言在所有的交流形式中看上去都是一致的,这种语言被称为“通用语言(Ubiquitous Language)”。通用语言应该在建模过程中广泛尝试以推动软件专家和领域专家之间的沟通,从而发现要在模型中使用的主要的领域概念。

将领域模型转换为代码实现的最佳实践

拥有一个看上去正确的模型不代表模型能被直接转换成代码,也或者它的实现可能会违背某些我们所不建议的软件设计原则。我们该如何实现从模型到代码的转换,并让代码具有可扩展性、可维护性,高性能等指标呢?另外,如实反映领域的模型可能会导致对象持久化的一系列问题,或者导致不可接受的性能问题。那么我们应该怎么做呢?

我们应该紧密关联领域建模和设计,紧密将领域模型和软件编码实现捆绑在一起,模型在构建时就考虑到软件和设计。开发人员会被加入到建模的过程中来。主要的想法是选择一个能够恰当在软件中表现的模型,这样设计过程会很顺畅并且基于模型。代码和其下的模型紧密关联会让代码更有意义并与模型更相关。有了开发人员的参与就会有反馈。它能保证模型被实现成软件。如果其中某处有错误,会在早期就被标识出来,问题也会容易修正。写代码的人会很好地了解模型,会感觉自己有责任保持它的完整性。他们会意识到对代码的一个变更其实就隐含着对模型的变更,另外,如果哪里的代码不能表现原始模型的话,他们会重构代码。如果分析人员从实现过程中分离出去,他会不再关心开发过程中引入的局限性。最终结果是模型不再实用。任何技术人员想对模型做出贡献必须花费一些时间来接触代码,无论他在项目中担负的是什么主要角色。任何一个负责修改代码的人都必须学会用代码表现模型。每位开发人员都必须参与到一定级别的领域讨论中并和领域专家联络。

领域建模时思考问题的角度

“用户需求”不能等同于“用户”,捕捉“用户心中的模型”也不能等同于“以用户为核心设计领域模型”。 《老子》书中有个观点:有之以为利,无之以为用。在这里,有之利,即建立领域模型;无之用,即包容用户需求。举些例子,一个杯子要装满一杯水,我们在制作杯子时,制作的是空杯子,即要把水倒出来,之后才能装下水;再比如,一座房子要住人,我们在建造房子时,建造的房子是空的,唯有空的才能容纳人的居住。因此,建立领域模型时也要将用户置于模型之外,这样才能包容用户的需求。

所以,我的理解是:

  1. 我们设计领域模型时不能以用户为中心作为出发点去思考问题,不能老是想着用户会对系统做什么;而应该从一个客观的角度,根据用户需求挖掘出领域内的相关事物,思考这些事物的本质关联及其变化规律作为出发点去思考问题。
  2. 领域模型是排除了人之外的客观世界模型,但是领域模型包含人所扮演的参与者角色,但是一般情况下不要让参与者角色在领域模型中占据主要位置,如果以人所扮演的参与者角色在领域模型中占据主要位置,那么各个系统的领域模型将变得没有差别,因为软件系统就是一个人机交互的系统,都是以人为主的活动记录或跟踪;比如:论坛中如果以人为主导,那么领域模型就是:人发帖,人回帖,人结贴,等等;DDD的例子中,如果是以人为中心的话,就变成了:托运人托运货物,收货人收货物,付款人付款,等等;因此,当我们谈及领域模型时,已经默认把人的因素排除开了,因为领域只有对人来说才有意义,人是在领域范围之外的,如果人也划入领域,领域模型将很难保持客观性。领域模型是与谁用和怎样用是无关的客观模型。归纳起来说就是,领域建模是建立虚拟模型让我们现实的人使用,而不是建立虚拟空间,去模仿现实。

以Eric Evans(DDD之父)在他的书中的一个货物运输系统为例子简单说明一下。在经过一些用户需求讨论之后,在用户需求相对明朗之后,Eric这样描述领域模型:

  1. 一个Cargo(货物)涉及多个Customer(客户,如托运人、收货人、付款人),每个Customer承担不同的角色;
  2. Cargo的运送目标已指定,即Cargo有一个运送目标;
  3. 由一系列满足Specification(规格)的Carrier Movement(运输动作)来完成运输目标;

从上面的描述我们可以看出,他完全没有从用户的角度去描述领域模型,而是以领域内的相关事物为出发点,考虑这些事物的本质关联及其变化规律的。上述这段描述完全以货物为中心,把客户看成是货物在某个场景中可能会涉及到的关联角色,如货物会涉及到托运人、收货人、付款人;货物有一个确定的目标,货物会经过一系列列的运输动作到达目的地;其实,我觉得以用户为中心来思考领域模型的思维只是停留在需求的表面,而没有挖掘出真正的需求的本质;我们在做领域建模时需要努力挖掘用户需求的本质,这样才能真正实现用户需求;

关于用户、参与者这两个概念的区分,可以看一下下面的例子:

试想两个人共同玩足球游戏,操作者(用户)是驱动者,它驱使足球比赛领域中,各个“人”(参与者)的活动。这里立下一个假设,假设操作者A操作某一队员a,而队员a拥有着某人B的信息,那么有以下说法,a是B的镜像,a是领域参与者,A是驱动者。

领域驱动设计的经典分层架构

用户界面/展现层

负责向用户展现信息以及解释用户命令。更细的方面来讲就是:

  1. 请求应用层以获取用户所需要展现的数据;
  2. 发送命令给应用层要求其执行某个用户命令;

应用层

很薄的一层,定义软件要完成的所有任务。对外为展现层提供各种应用功能(包括查询或命令),对内调用领域层(领域对象或领域服务)完成各种业务逻辑,应用层不包含业务逻辑。

领域层

负责表达业务概念,业务状态信息以及业务规则,领域模型处于这一层,是业务软件的核心。

基础设施层

本层为其他层提供通用的技术能力;提供了层间的通信;为领域层实现持久化机制;总之,基础设施层可以通过架构和框架来支持其他层的技术需求;

领域驱动设计过程中使用的模式

所有模式的总揽图

关联的设计

关联本身不是一个模式,但它在领域建模的过程中非常重要,所以需要在探讨各种模式之前,先讨论一下对象之间的关联该如何设计。我觉得对象的关联的设计可以遵循如下的一些原则:

  1. 关联尽量少,对象之间的复杂的关联容易形成对象的关系网,这样对于我们理解和维护单个对象很不利,同时也很难划分对象与对象之间的边界;另外,同时减少关联有助于简化对象之间的遍历;
  2. 对多的关联也许在业务上是很自然的,通常我们会用一个集合来表示1对多的关系。但我们往往也需要考虑到性能问题,尤其是当集合内元素非常多的时候,此时往往需要通过单独查询来获取关联的集合信息;
  3. 关联尽量保持单向的关联;
  4. 在建立关联时,我们需要深入去挖掘是否存在关联的限制条件,如果存在,那么最好把这个限制条件加到这个关联上;往往这样的限制条件能将关联化繁为简,即可以将多对多简化为1对多,或将1对多简化为1对1;

实体(Entity)

实体就是领域中需要唯一标识的领域概念。因为我们有时需要区分是哪个实体。有两个实体,如果唯一标识不一样,那么即便实体的其他所有属性都一样,我们也认为他们两个不同的实体;因为实体有生命周期,实体从被创建后可能会被持久化到数据库,然后某个时候又会被取出来。所以,如果我们不为实体定义一种可以唯一区分的标识,那我们就无法区分到底是这个实体还是哪个实体。另外,不应该给实体定义太多的属性或行为,而应该寻找关联,发现其他一些实体或值对象,将属性或行为转移到其他关联的实体或值对象上。比如Customer实体,他有一些地址信息,由于地址信息是一个完整的有业务含义的概念,所以,我们可以定义一个Address对象,然后把Customer的地址相关的信息转移到Address对象上。如果没有Address对象,而把这些地址信息直接放在Customer对象上,并且如果对于一些其他的类似Address的信息也都直接放在Customer上,会导致Customer对象很混乱,结构不清晰,最终导致它难以维护和理解;

值对象(Value Object)

在领域中,并不是没一个事物都必须有一个唯一标识,也就是说我们不关心对象是哪个,而只关心对象是什么。就以上面的地址对象Address为例,如果有两个Customer的地址信息是一样的,我们就会认为这两个Customer的地址是同一个。也就是说只要地址信息一样,我们就认为是同一个地址。用程序的方式来表达就是,如果两个对象的所有的属性的值都相同我们会认为它们是同一个对象的话,那么我们就可以把这种对象设计为值对象。因此,值对象没有唯一标识,这是它和实体的最大不同。另外值对象在判断是否是同一个对象时是通过它们的所有属性是否相同,如果相同则认为是同一个值对象;而我们在区分是否是同一个实体时,只看实体的唯一标识是否相同,而不管实体的属性是否相同;值对象另外一个明显的特征是不可变,即所有属性都是只读的。因为属性是只读的,所以可以被安全的共享;当共享值对象时,一般有复制和共享两种做法,具体采用哪种做法还要根据实际情况而定;另外,我们应该给值对象设计的尽量简单,不要让它引用很多其他的对象,因为他只是一个值,就像int a = 3;那么”3”就是一个我们传统意义上所说的值,而值对象其实也可以和这里的”3”一样理解,也是一个值,只不过是用对象来表示。所以,当我们在C#语言中比较两个值对象是否相等时,会重写GetHashCode和Equals这两个方法,目的就是为了比较对象的值;值对象虽然是只读的,但是可以被整个替换掉。就像你把a的值修改为”4”(a = 4;)一样,直接把”3”这个值替换为”4”了。值对象也是一样,当你要修改Customer的Address对象引用时,不是通过Customer.Address.Street这样的方式来实现,因为值对象是只读的,它是一个完整的不可分割的整体。我们可以这样做:Customer.Address = new Address(…);

领域服务(Domain Service)

领域中的一些概念不太适合建模为对象,即归类到实体对象或值对象,因为它们本质上就是一些操作,一些动作,而不是事物。这些操作或动作往往会涉及到多个领域对象,并且需要协调这些领域对象共同完成这个操作或动作。如果强行将这些操作职责分配给任何一个对象,则被分配的对象就是承担一些不该承担的职责,从而会导致对象的职责不明确很混乱。但是基于类的面向对象语言规定任何属性或行为都必须放在对象里面。所以我们需要寻找一种新的模式来表示这种跨多个对象的操作,DDD认为服务是一个很自然的范式用来对应这种跨多个对象的操作,所以就有了领域服务这个模式。和领域对象不同,领域服务是以动词开头来命名的,比如资金转帐服务可以命名为MoneyTransferService。当然,你也可以把服务理解为一个对象,但这和一般意义上的对象有些区别。因为一般的领域对象都是有状态和行为的,而领域服务没有状态只有行为。需要强调的是领域服务是无状态的,它存在的意义就是协调领域对象共完成某个操作,所有的状态还是都保存在相应的领域对象中。我觉得模型(实体)与服务(场景)是对领域的一种划分,模型关注领域的个体行为,场景关注领域的群体行为,模型关注领域的静态结构,场景关注领域的动态功能。这也符合了现实中出现的各种现象,有动有静,有独立有协作。

领域服务还有一个很重要的功能就是可以避免领域逻辑泄露到应用层。因为如果没有领域服务,那么应用层会直接调用领域对象完成本该是属于领域服务该做的操作,这样一来,领域层可能会把一部分领域知识泄露到应用层。因为应用层需要了解每个领域对象的业务功能,具有哪些信息,以及它可能会与哪些其他领域对象交互,怎么交互等一系列领域知识。因此,引入领域服务可以有效的防治领域层的逻辑泄露到应用层。对于应用层来说,从可理解的角度来讲,通过调用领域服务提供的简单易懂但意义明确的接口肯定也要比直接操纵领域对象容易的多。这里似乎也看到了领域服务具有Façade的功能,呵呵。

说到领域服务,还需要提一下软件中一般有三种服务:应用层服务、领域服务、基础服务。

应用层服务

  1. 获取输入(如一个XML请求);
  2. 发送消息给领域层服务,要求其实现转帐的业务逻辑;
  3. 领域层服务处理成功,则调用基础层服务发送Email通知;

领域层服务

  1. 获取源帐号和目标帐号,分别通知源帐号和目标帐号进行扣除金额和增加金额的操作;
  2. 提供返回结果给应用层;

基础层服务

按照应用层的请求,发送Email通知;

所以,从上面的例子中可以清晰的看出,每种服务的职责;

聚合及聚合根(Aggregate,Aggregate Root)

聚合,它通过定义对象之间清晰的所属关系和边界来实现领域模型的内聚,并避免了错综复杂的难以维护的对象关系网的形成。聚合定义了一组具有内聚关系的相关对象的集合,我们把聚合看作是一个修改数据的单元。

聚合有以下一些特点:

  1. 每个聚合有一个根和一个边界,边界定义了一个聚合内部有哪些实体或值对象,根是聚合内的某个实体;
  2. 聚合内部的对象之间可以相互引用,但是聚合外部如果要访问聚合内部的对象时,必须通过聚合根开始导航,绝对不能绕过聚合根直接访问聚合内的对象,也就是说聚合根是外部可以保持 对它的引用的唯一元素;
  3. 聚合内除根以外的其他实体的唯一标识都是本地标识,也就是只要在聚合内部保持唯一即可,因为它们总是从属于这个聚合的;
  4. 聚合根负责与外部其他对象打交道并维护自己内部的业务规则;
  5. 基于聚合的以上概念,我们可以推论出从数据库查询时的单元也是以聚合为一个单元,也就是说我们不能直接查询聚合内部的某个非根的对象;
  6. 聚合内部的对象可以保持对其他聚合根的引用;
  7. 删除一个聚合根时必须同时删除该聚合内的所有相关对象,因为他们都同属于一个聚合,是一个完整的概念;

关于如何识别聚合以及聚合根的问题:

我觉得我们可以先从业务的角度深入思考,然后慢慢分析出有哪些对象是:

  1. 有独立存在的意义,即它是不依赖于其他对象的存在它才有意义的;
  2. 可以被独立访问的,还是必须通过某个其他对象导航得到的;

如何识别聚合?

我觉得这个需要从业务的角度深入分析哪些对象它们的关系是内聚的,即我们会把他们看成是一个整体来考虑的;然后这些对象我们就可以把它们放在一个聚合内。所谓关系是内聚的,是指这些对象之间必须保持一个固定规则,固定规则是指在数据变化时必须保持不变的一致性规则。当我们在修改一个聚合时,我们必须在事务级别确保整个聚合内的所有对象满足这个固定规则。作为一条建议,聚合尽量不要太大,否则即便能够做到在事务级别保持聚合的业务规则完整性,也可能会带来一定的性能问题。有分析报告显示,通常在大部分领域模型中,有70%的聚合通常只有一个实体,即聚合根,该实体内部没有包含其他实体,只包含一些值对象;另外30%的聚合中,基本上也只包含两到三个实体。这意味着大部分的聚合都只是一个实体,该实体同时也是聚合根。

如何识别聚合根?

如果一个聚合只有一个实体,那么这个实体就是聚合根;如果有多个实体,那么我们可以思考聚合内哪个对象有独立存在的意义并且可以和外部直接进行交互。

工厂(Factory)

DDD中的工厂也是一种体现封装思想的模式。DDD中引入工厂模式的原因是:有时创建一个领域对象是一件比较复杂的事情,不仅仅是简单的new操作。正如对象封装了内部实现一样(我们无需知道对象的内部实现就可以使用对象的行为),工厂则是用来封装创建一个复杂对象尤其是聚合时所需的知识,工厂的作用是将创建对象的细节隐藏起来。客户传递给工厂一些简单的参数,然后工厂可以在内部创建出一个复杂的领域对象然后返回给客户。领域模型中其他元素都不适合做这个事情,所以需要引入这个新的模式,工厂。工厂在创建一个复杂的领域对象时,通常会知道该满足什么业务规则(它知道先怎样实例化一个对象,然后在对这个对象做哪些初始化操作,这些知识就是创建对象的细节),如果传递进来的参数符合创建对象的业务规则,则可以顺利创建相应的对象;但是如果由于参数无效等原因不能创建出期望的对象时,应该抛出一个异常,以确保不会创建出一个错误的对象。当然我们也并不总是需要通过工厂来创建对象,事实上大部分情况下领域对象的创建都不会太复杂,所以我们只需要简单的使用构造函数创建对象就可以了。隐藏创建对象的好处是显而易见的,这样可以不会让领域层的业务逻辑泄露到应用层,同时也减轻了应用层的负担,它只需要简单的调用领域工厂创建出期望的对象即可。

仓储(Repository)

  1. 仓储被设计出来的目的是基于这个原因:领域模型中的对象自从被创建出来后不会一直留在内存中活动的,当它不活动时会被持久化到数据库中,然后当需要的时候我们会重建该对象;重建对象就是根据数据库中已存储的对象的状态重新创建对象的过程;所以,可见重建对象是一个和数据库打交道的过程。从更广义的角度来理解,我们经常会像集合一样从某个类似集合的地方根据某个条件获取一个或一些对象,往集合中添加对象或移除对象。也就是说,我们需要提供一种机制,可以提供类似集合的接口来帮助我们管理对象。仓储就是基于这样的思想被设计出来的;
  2. 仓储里面存放的对象一定是聚合,原因是之前提到的领域模型中是以聚合的概念去划分边界的;聚合是我们更新对象的一个边界,事实上我们把整个聚合看成是一个整体概念,要么一起被取出来,要么一起被删除。我们永远不会单独对某个聚合内的子对象进行单独查询或做更新操作。因此,我们只对聚合设计仓储。
  3. 仓储还有一个重要的特征就是分为仓储定义部分和仓储实现部分,在领域模型中我们定义仓储的接口,而在基础设施层实现具体的仓储。这样做的原因是:由于仓储背后的实现都是在和数据库打交道,但是我们又不希望客户(如应用层)把重点放在如何从数据库获取数据的问题上,因为这样做会导致客户(应用层)代码很混乱,很可能会因此而忽略了领域模型的存在。所以我们需要提供一个简单明了的接口,供客户使用,确保客户能以最简单的方式获取领域对象,从而可以让它专心的不会被什么数据访问代码打扰的情况下协调领域对象完成业务逻辑。这种通过接口来隔离封装变化的做法其实很常见。由于客户面对的是抽象的接口并不是具体的实现,所以我们可以随时替换仓储的真实实现,这很有助于我们做单元测试。
  4. 尽管仓储可以像集合一样在内存中管理对象,但是仓储一般不负责事务处理。一般事务处理会交给一个叫“工作单元(Unit Of Work)”的东西。关于工作单元的详细信息我在下面的讨论中会讲到。
  5. 另外,仓储在设计查询接口时,可能还会用到规格模式(Specification Pattern),我见过的最厉害的规格模式应该就是LINQ以及DLINQ查询了。一般我们会根据项目中查询的灵活度要求来选择适合的仓储查询接口设计。通常情况下只需要定义简单明了的具有固定查询参数的查询接口就可以了。只有是在查询条件是动态指定的情况下才可能需要用到Specification等模式。

设计领域模型的一般步骤

  1. 根据需求建立一个初步的领域模型,识别出一些明显的领域概念以及它们的关联,关联可以暂时没有方向但需要有(1:1,1:N,M:N)这些关系;可以用文字精确的没有歧义的描述出每个领域概念的涵义以及包含的主要信息;
  2. 分析主要的软件应用程序功能,识别出主要的应用层的类;这样有助于及早发现哪些是应用层的职责,哪些是领域层的职责;
  3. 进一步分析领域模型,识别出哪些是实体,哪些是值对象,哪些是领域服务;
  4. 分析关联,通过对业务的更深入分析以及各种软件设计原则及性能方面的权衡,明确关联的方向或者去掉一些不需要的关联;
  5. 找出聚合边界及聚合根,这是一件很有难度的事情;因为你在分析的过程中往往会碰到很多模棱两可的难以清晰判断的选择问题,所以,需要我们平时一些分析经验的积累才能找出正确的聚合根;
  6. 为聚合根配备仓储,一般情况下是为一个聚合分配一个仓储,此时只要设计好仓储的接口即可;
  7. 走查场景,确定我们设计的领域模型能够有效地解决业务需求;
  8. 考虑如何创建领域实体或值对象,是通过工厂还是直接通过构造函数;
  9. 停下来重构模型。寻找模型中觉得有些疑问或者是蹩脚的地方,比如思考一些对象应该通过关联导航得到还是应该从仓储获取?聚合设计的是否正确?考虑模型的性能怎样,等等;

领域建模是一个不断重构,持续完善模型的过程,大家会在讨论中将变化的部分反映到模型中,从而是模型不断细化并朝正确的方向走。领域建模是领域专家、设计人员、开发人员之间沟通交流的过程,是大家工作和思考问题的基础。

在分层架构中其他层如何与领域层交互

从经典的领域驱动设计分层架构中可以看出,领域层的上层是应用层,下层是基础设施层。那么领域层是如何与其它层交互的呢?

对于会影响领域层中领域对象状态的应用层功能

一般应用层会先启动一个工作单元,然后:

  1. 对于修改领域对象的情况,通过仓储获取领域对象,调用领域对象的相关业务方法以完成业务逻辑处理;
  2. 对于新增领域对象的情况,通过构造函数或工厂创建出领域对象,如果需要还可以继续对该新创建的领域对象做一些操作,然后把该新创建的领域对象添加到仓储中;
  3. 对于删除领域对象的情况,可以先把领域对象从仓储中取出来,然后将其从仓储中删除,也可以直接传递一个要删除的领域对象的唯一标识给仓储通知其移除该唯一标识对应领域对象;
  4. 如果一个业务逻辑涉及到多个领域对象,则调用领域层中的相关领域服务完成操作;

注意,以上所说的所有领域对象都是只聚合根,另外在应用层需要获取仓储接口以及领域服务接口时,都可以通过IOC容器获取。最后通知工作单元提交事务从而将所有相关的领域对象的状态以事务的方式持久化到数据库;

关于Unit of Work(工作单元)的几种实现方法

  1. 基于快照的实现,即领域对象被取出来后,会先保存一个备份的对象,然后当在做持久化操作时,将最新的对象的状态和备份的对象的状态进行比较,如果不相同,则认为有做过修改,然后进行持久化;这种设计的好处是对象不用告诉工作单元自己的状态修改了,而缺点也是显而易见的,那就是性能可能会低,备份对象以及比较对象的状态是否有修改的过程在当对象本身很复杂的时候,往往是一个比较耗时的步骤,而且要真正实现对象的深拷贝以及判断属性是否修改还是比较困难的;
  2. 不基于快照,而是仓储的相关更新或新增或删除接口被调用时,仓储通知工作单元某个对象被新增了或更新了或删除了。这样工作单元在做数据持久化时也同样可以知道需要持久化哪些对象了;这种方法理论上不需要ORM框架的支持,对领域模型也没有任何倾入性,同时也很好的支持了工作单元的模式。对于不想用高级ORM框架的朋友来说,这种方法挺好;
  3. 不基于快照,也不用仓储告诉工作单元数据更改了。而是采用AOP的思想,采用透明代理的方式进行一个拦截。在NHibernate中,我们的属性通常要被声明为virtual的,一个原因就是NHibernate会生成一个透明代理,用于拦截对象的属性被修改时,自动通知工作单元对象的状态被更新了。这样工作单元也同样知道需要持久化哪些对象了。这种方法对领域模型的倾入性不大,并且能很好的支持工作单元模式,如果用NHibernate作为ORM,这种方法用的比较多;
  4. 一般是微软用的方法,那就是让领域对象实现.NET框架中的INotifiyPropertyChanged接口,然后在每个属性的set方法的最后一行调用OnPropertyChanged的方法从而显示地通知别人自己的状态修改了。这种方法相对来说对领域模型的倾入性最强。

对于不会影响领域层中领域对象状态的查询功能

可以直接通过仓储查询出所需要的数据。但一般领域层中的仓储提供的查询功能也许不能满足界面显示的需要,则可能需要多次调用不同的仓储才能获取所需要显示的数据;其实针对这种查询的情况,我在后面会讲到可以直接通过CQRS的架构来实现。即对于查询,我们可以在应用层不调用领域层的任何东西,而是直接通过某个其他的用另外的技术架构实现的查询引擎来完成查询,比如直接通过构造参数化SQL的方式从数据库一个表或多个表中查询出任何想要显示的数据。这样不仅性能高,也可以减轻领域层的负担。领域模型不太适合为应用层提供各种查询服务,因为往往界面上要显示的数据是很多对象的组合信息,是一种非对象概念的信息,就像报表;

为什么面向对象比面向过程更能适应业务变化

对象将需求用类一个个隔开,就像用储物箱把东西一个个封装起来一样,需求变了,分几种情况,最严重的是大变,那么每个储物箱都要打开改,这种方法就不见得有好处;但是这种情况发生概率比较小,大部分需求变化都是局限在一两个储物箱中,那么我们只要打开这两个储物箱修改就可以,不会影响其他储物柜了。

而面向过程是把所有东西都放在一个大储物箱中,修改某个部分以后,会引起其他部分不稳定,一个BUG修复,引发新的无数BUG,最后程序员陷入焦头烂额,如日本东京电力公司员工处理核危机一样,心力交瘁啊。

所以,我们不能粗粒度看需求变,认为需求变了,就是大范围变,万事万物都有边界,老子说,无欲观其缴,什么事物都要观察其边界,虽然需求可以用“需求”这个名词表达,谈到需求变了,不都意味着最大边界范围的变化,这样看问题容易走极端。

其实就是就地画圈圈——边界。我们小时候写作文分老三段也是同样道理,各自职责明确,划分边界明确,通过过渡句实现承上启下——接口。为什么组织需要分不同部门,同样是边界思维。画圈圈容易,但如何画才难,所以OO中思维非常重要。

需求变化所引起的变化是有边界,若果变化的边界等于整个领域,那么已经是完全不同的项目了。要掌握边界,是需要大量的领域知识的。否则,走进银行连业务职责都分不清的,如何画圈圈呢?

面向过程是无边界一词的(就算有也只是最大的边界),它没有要求各自独立,它可以横跨边界进行调用,这就是容易引起BUG的原因,引起BUG不一定是技术错误,更多的是逻辑错误。分别封装就是画圈圈了,所有边界都以接口实现。不用改或者小改接口,都不会牵一发动全身。若果面向过程中考虑边界,那么也就已经上升到OO思维,即使用的不是对象语言,但对象已经隐含其中。说白了,面向对象与面向过程最大区别就是:分解。边界的分解。从需求到最后实现都贯穿。

面向对象的实质就是边界划分,封装,不但对需求变化能够量化,缩小影响面;因为边界划分也会限制出错的影响范围,所以OO对软件后期BUG等出错也有好处。

软件世界永远都有BUG,BUG是清除不干净的,就像人类世界永远都存在不完美和阴暗面,问题关键是:上帝用空间和时间的边界把人类世界痛苦灾难等不完美局限在一个范围内;而软件世界如果你不采取OO等方法进行边界划分的话,一旦出错,追查起来情况会有多糟呢?

软件世界其实类似人类现实世界,有时出问题了,探究原因一看,原来是两个看上去毫无联系的因素导致的,古人只好经常求神拜佛,我们程序员在自己的软件上线运行时,大概心里也在求神拜佛别出大纰漏,如果我们的软件采取OO封装,我们就会坦然些,肯定会出错,但是我们已经预先划定好边界,所以,不会产生严重后果,甚至也不会出现难以追查的魔鬼BUG。

领域驱动设计的其他一些主题

上面只是涉及到DDD中最基本的内容,DDD中还有很多其他重要的内容在上面没有提到,如:

  1. 模型上下文、上下文映射、上下文共享;
  2. 如何将分析模式和设计模式运用到DDD中;
  3. 一些关于柔性设计的技巧;
  4. 如果保持模型完整性,以及持续集成方面的知识;
  5. 如何精炼模型,识别核心模型以及通用子领域;

这些主题都很重要,因为篇幅有限以及我目前掌握的知识也有限,并且为了突出这篇文章的重点,所以不对他们做详细介绍了,大家有兴趣的可以自己阅读一下。

一些相关的扩展阅读

CQRS架构

核心思想是将应用程序的查询部分和命令部分完全分离,这两部分可以用完全不同的模型和技术去实现。比如命令部分可以通过领域驱动设计来实现;查询部分可以直接用最快的非面向对象的方式去实现,比如用SQL。这样的思想有很多好处:

  1. 实现命令部分的领域模型不用经常为了领域对象可能会被如何查询而做一些折中处理;
  2. 由于命令和查询是完全分离的,所以这两部分可以用不同的技术架构实现,包括数据库设计都可以分开设计,每一部分可以充分发挥其长处;
  3. 高性能,命令端因为没有返回值,可以像消息队列一样接受命令,放在队列中,慢慢处理;处理完后,可以通过异步的方式通知查询端,这样查询端可以做数据同步的处理;

Event Sourcing(事件溯源)

对于DDD中的聚合,不保存聚合的当前状态,而是保存对象上所发生的每个事件。当要重建一个聚合对象时,可以通过回溯这些事件(即让这些事件重新发生)来让对象恢复到某个特定的状态;因为有时一个聚合可能会发生很多事件,所以如果每次要在重建对象时都从头回溯事件,会导致性能低下,所以我们会在一定时候为聚合创建一个快照。这样,我们就可以基于某个快照开始创建聚合对象了。

DCI架构

DCI架构强调,软件应该真实的模拟现实生活中对象的交互方式,代码应该准确朴实的反映用户的心智模型。在DCI中有:数据模型、角色模型、以及上下文这三个概念。数据模型表示程序的结构,目前我们所理解的DDD中的领域模型可以很好的表示数据模型;角色模型表示数据如何交互,一个角色定义了某个“身份”所具有的交互行为;上下文对应业务场景,用于实现业务用例,注意是业务用例而不是系统用例,业务用例只与业务相关;软件运行时,根据用户的操作,系统创建相应的场景,并把相关的数据对象作为场景参与者传递给场景,然后场景知道该为每个对象赋予什么角色,当对象被赋予某个角色后就真正成为有交互能力的对象,然后与其他对象进行交互;这个过程与现实生活中我们所理解的对象是一致的;

DCI的这种思想与DDD中的领域服务所做的事情是一样的,但实现的角度有些不同。DDD中的领域服务被创建的出发点是当一些职责不太适合放在任何一个领域对象上时,这个职责往往对应领域中的某个活动或转换过程,此时我们应该考虑将其放在一个服务中。比如资金转帐的例子,我们应该提供一个资金转帐的服务,用来对应领域中的资金转帐这个领域概念。但是领域服务内部做的事情是协调多个领域对象完成一件事情。因此,在DDD中的领域服务在协调领域对象做事情时,领域对象往往是处于一个被动的地位,领域服务通知每个对象要求其做自己能做的事情,这样就行了。这个过程中我们似乎看不到对象之间交互的意思,因为整个过程都是由领域服务以面向过程的思维去实现了。而DCI则通用引入角色,赋予角色以交互能力,然后让角色之间进行交互,从而可以让我们看到对象与对象之间交互的过程。但前提是,对象之间确实是在交互。因为现实生活中并不是所有的对象在做交互,比如有A、B、C三个对象,A通知B做事情,A通知C做事情,此时可以认为A和B,A和C之间是在交互,但是B和C之间没有交互。所以我们需要分清这种情况。资金转帐的例子,A相当于转帐服务,B相当于帐号1,C相当于帐号2。因此,资金转帐这个业务场景,用领域服务比较自然。有人认为DCI可以替换DDD中的领域服务,我持怀疑态度。

四色原型分析模式

时刻-时间段原型(Moment-Interval Archetype)

表示在某个时刻或某一段时间内发生的某个活动。使用粉红色表示,简写为MI。

参与方-地点-物品原型(Part-Place-Thing Archetype)

表示参与某个活动的人或物,地点则是活动的发生地。使用绿色表示。简写为PPT。

描述原型(Description Archetype)

表示对PPT的本质描述。它不是PPT的分类!Description是从PPT抽象出来的不变的共性的属性的集合。使用蓝色表示,简写为DESC。

举个例子,有一个人叫张三,如果某个外星人问你张三是什么?你会怎么说?可能会说,张三是个人,但是外星人不知道“人”是什么。然后你会怎么办?你就会说:张三是个由一个头、两只手、两只脚,以及一个身体组成的客观存在。虽然这时外星人仍然不知道人是什么,但我已经可以借用这个例子向大家说明什么是“Description”了。在这个例子中,张三就是一个PPT,而“由一个头、两只手、两只脚,以及一个身体组成的客观存在”就是对张三的Description,头、手、脚、身体则是人的本质的不变的共性的属性的集合。但我们人类比较聪明,很会抽象总结和命名,已经把这个Description用一个字来代替了,那就是“人”。所以就有所谓的张三是人的说法。

角色原型(Role Archetype)

角色就是我们平时所理解的“身份”。使用黄色表示,简写为Role。为什么会有角色这个概念?因为有些活动,只允许具有特定角色(身份)的PPT(参与者)才能参与该活动。比如一个人只有具有教师的角色才能上课(一种活动);一个人只有是一个合法公民才能参与选举和被选举;但是有些活动也是不需要角色的,比如一个人不需要具备任何角色就可以睡觉(一种活动)。当然,其实说人不需要角色就能睡觉也是错误的,错在哪里?因为我们可以这样理解:一个客观存在只要具有“人”的角色就能睡觉,其实这时候,我们已经把DESC当作角色来看待了。所以,其实角色这个概念是非常广的,不能用我们平时所理解的狭义的“身份”来理解,因为“教师”、“合法公民”、“人”都可以被作为角色来看待。因此,应该这样说:任何一个活动,都需要具有一定角色的参与者才能参与。

用一句话来概括四色原型就是:一个什么什么样的人或组织或物品以某种角色在某个时刻或某段时间内参与某个活动。 其中“什么什么样的”就是DESC,“人或组织或物品”就是PPT,“角色”就是Role,而”某个时刻或某段时间内的某个活动”就是MI。

以上这些东西如果在学习了DDD之后再去学习会对DDD有更深入的了解,但我觉得DDD相对比较基础,如果我们在已经了解了DDD的基础之上再去学习这些东西会更加有效和容易掌握。

希望本文对大家有所帮助。

原文地址:领域驱动设计之领域模型

任何一个在互联网上提供其服务的公司都可以叫做云计算公司。其实云计算分几层的,分别是Infrastructure(基础设施)-as-a- Service,Platform(平台)-as-a-Service,Software(软件)-as-a-Service。

IaaS, PaaS和SaaS是云计算的三种服务模式,基础设施(infrastructure)、平台(platform)和软件(software):

IaaS: Infrastructure-as-a-Service(基础设施即服务)提供给消费者的服务是对所有计算基础设施的利用,包括处理CPU、内存、存储、网络和其它基本的计算资源,用户能够部署和运行任意软件,包括操作系统和应用程序。是云服务的最底层,主要提供一些基础资源。它与 PaaS 的区别是,用户需要自己控制底层,实现基础设施的使用逻辑。

PaaS:Platform-as-a-Service(平台即服务)提供给消费者的服务是把客户采用提供的开发语言和工具(例如Java,python, .Net等)开发的或收购的应用程序部署到供应商的云计算基础设施上去。客户不需要管理或控制底层的云基础设施,包括网络、服务器、操作系统、存储等,但客户能控制部署的应用程序,也可能控制运行应用程序的托管环境配置。提供软件部署平台(runtime),抽象掉了硬件和操作系统细节,可以无缝地扩展(scaling)。开发者只需要关注自己的业务逻辑,不需要关注底层

SaaS:Software-as-a-Service(软件即服务)提供给客户的服务是运营商运行在云计算基础设施上的应用程序,用户可以在各种设备上通过客户端界面访问,如浏览器。消费者不需要管理或控制任何云计算基础设施,包括网络、服务器、操作系统、存储等。是软件的开发、管理、部署都交给第三方,不需要关心技术问题,可以拿来即用。普通用户接触到的互联网服务,几乎都是 SaaS。

IaaS > PaaS > SaaS:从左到右,自己承担的工作量越来越少。

目录

  1. openSSH的ssh-keygen工具
  2. openSSL
  3. keytool
  4. GPG
  5. PGP
  6. Putty
  7. 其他未整理内容

一、OpenSSH和ssh-keygen

OpenSSH是SSH协议的开源版本(SSH:Secure SHell)。使用SSH透过计算机网络实现加密通讯,可以进行远程控制,在计算机之间传送文件等等。SSH传输的数据都进行了加密,比telnet,rcp,ftp,rlogin,rsh等以明文传输密码的工具更安全。

OpenSSH提供了实现SSH协议的很多工具。其中ssh-keygen用于生成,管理和转换用于认证的密钥和证书。

ssh-keygen

  • -b 密钥长度,默认为2048
  • -t 密钥类型, rsa|des… (默认类型为rsa)
    • SSH1: RSA
    • SSH2: RSA, DSA, ECDSA
  • -N 新密码
  • -f 指定密钥文件,创建时会同时生成一个.pub结尾的公钥文件。
  • -C
  • -c 修改公钥或私钥文件中的注释
  • -p 修改私钥文件密码
  • -P 旧密码
  • -e 导出为其它格式的密钥文件,可以转换密钥类型
  • -i 从其他格式的密钥文件导入,可以转换密钥类型
  • -m <PEM|PKCS8|RFC4716> 与-e,-i配合使用,指明导出或导入的密钥文件格式
  • -y 读入密钥并显示公钥

二、OpenSSL

OpenSSL 是一个强大的安全套接字层密码库,包括了加密算法,常用密钥和证书管理,SSL协议等功能。OpenSSL提供的命令非常多,这里只简单列出OpenSSL生成密钥和证书的一些操作(Window需要以管理员身份运行cmd)。

v1.0.1+密钥默认采用PKCS#8格式(之前版本为PEM)。

查看openSSL版本

openssl version
openssl version -a

enc 对称加密

1.密钥生成(genrsa,genpkey,req在证书请求时同时生成密钥, gendh, gendsa)

(1) openssl genrsa [options] [bits_num]

  • -out 指定输出文件,不指定时在终端显示密钥内容
  • -passout pass: 设置私钥文件密码
  • -f4 使用F4(0x10001)作为公钥的E参数(默认)
  • -3 使用3作为公钥的E参数
  • -des , -des3, -aes128, -aes192, -aes256 指定加密算法(默认不加密)

默认生产的密钥格式为PEM。openssl默认只生成了私钥文件,当需要提取公钥时使用rsa命令。

示例

#生成RSA密钥对。位长度为2048,保持到rsakey0.pem文件中。
openssl genrsa -out rsakey0.pem 2048 #生成RSA密钥对。使用DES3加密,密钥使用密码保护,位长度为1024
openssl genrsa -des3 -out rootca.key -passout pass:123456 1024

(2) openssl genpkey [options] v1.0.1+

示例

#生成RSA密钥,位长度为2048,格式为DER
openssl genpkey -algorithm RSA -out rsapriKey.pem -pkeyopt rsa_keygen_bits:2048 -outform DER

(3) openssl req请求时生成新的密钥对

openssl req -x509 -days 365 -newkey rsa:2048 -keyout private.pem -out public.pem -nodes

2.密钥文件管理和转换(rsa, pkey)

(1) openssl rsa [options] outfile

  • -in 输入密钥文件
  • -passin pass: 输入密钥保护密码
  • -inform <DER|NET|PEM> 输入密钥格式,默认为PEM
  • -out 输出密钥文件
  • -outform <DER|NET|PEM> 输出密钥格式,默认为PEM
  • -passout pass: 输出密钥保护密码
  • -pubin 指示输入的是公钥,默认输出的是密钥对或私钥
  • -pubout 输出公钥到文件(公钥一般无需加密)
  • -des, -des3, -aes128, -aes192, -aes256 指定密钥加密算法
  • -text 明文输出密钥参数
  • -noout 不输出密钥到文件
  • -check 验证一致性
  • -modulus 显示RSA密钥模值

示例

#提取密钥公钥到单独的文件
openssl rsa -in rsakey0.pem -pubout -out rsakey0.pub

#转换密钥格式(DER->PEM)
openssl rsa -in rsakeypair.der -inform DER -out rsakeypair.pem

#改加密算法,移除密码保护
openssl rsa -in rsakeypair.pem -passin pass:123456 -des3 -out rsakeypair1.pem

(2) openssl pkey [options] v1.0.1+

(3) 将PEM格式密钥转换成Java JCE 能使用的DER格式密钥的另一种方式

openssl pkcs8 -topk8 -inform PEM -outform DER -in <rsa_pem.key> -out <pkcs8_der.key> -nocrypt

(4) OpenSSL公钥和OpenSSH公钥格式转换

OpenSSL生成的公钥格式和OpenSSH公钥格式不一致,把OpenSSL生成的公钥用于配置SSH连接,验证会失败。

OpenSSL公钥(PEM)格式为:

—–BEGIN PUBLIC KEY—– MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC7vbqajDw4o6gJy8UtmIbkcpnk
O3Kwc4qsEnSZp/TR+fQi62F79RHWmwKOtFmwteURgLbj7D/WGuNLGOfa/2vse3G2
eHnHl5CB8ruRX9fBl/KgwCVr2JaEuUm66bBQeP5XeBotdR4cvX38uPYivCDdPjJ1
QWPdspTBKcxeFbccDwIDAQAB —–END PUBLIC KEY—–

OpenSSH公钥格式为:

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC7vbqajDw4o6gJy8UtmIbkcpnkO3Kwc4qsEnSZp/TR+fQi62F79RHWmwKOtFmwteURgLbj7D/WGuNLGOfa/2vse3G2eHnHl5CB8ruRX9fBl/KgwCVr2JaEuUm66bBQeP5XeBotdR4cvX38uPYivCDdPjJ1QWPdspTBKcxeFbccDw==

  • 从私钥重新生成OpenSSH格式公钥

ssh-keygen -y -f priKey.pem > sshPubkey.pub

  • 将OpenSSL格式公钥转换成OpenSSH格式

ssh-keygen -i -m PKCS8 -f sslPubKey.pub 〉 sshPubKey.pub #-m支持 PEM,PKCS8,RFC4716

  • 将OpenSSH格式公钥转换成OpenSSL格式公钥

ssh-keygen -e -m PEM -f sshPubKey.pub >sslPubKey.pub #-m支持 PEM,PKCS8,RFC4716

3.使用密钥(rsautl),不支持大文件

openssl rsautl [options]

  • -in 输入需加密解密文件
  • -out 输出加密或解密后的文件
  • -inkey 输入密钥文件
  • -passin pass: 输入密钥文件的保护密码
  • -keyform <PEM|DER|NET> 输入密钥文件格式,默认为PEM
  • -pubin 指明输入的是公钥文件
  • -certin 指明输入的是包含公钥的证书
  • -sign 使用私钥签名签名(加密)
  • -verify 似乎用公钥验证签名(解密)
  • -encrypt 使用公钥加密
  • -decrypt 使用私钥解密
  • -hexdump 输出十六进制
  • -pkcs, -ssl, -raw, -oaep 数据补齐方式,默认为pkcs

  使用RSA加密时,要求被加密的数据长度与RSA密钥长度一致,数据过短时会补齐。过长时会分段加密。实际应用中很少会使用非对称加密算法对大的文件进行加密操作,而是使用对称加密算法加密文件,然后再使用<非对称加密算法>对<对称算法的密钥>进行加密

示例:

#使用公钥加密
openssl rsautl -in test.txt -out test_enc.txt -inkey rsakeypair.pub -pubin -encrypt

#使用私钥解密
openssl rsautl -in test_enc.txt -out text_dec.txt -inkey rsakeypair.pem -decrypt

#使用私钥签名
openssl rsautl -in test.txt -out test_sign.txt -inkey rsakeypair.pem -sign

#使用公钥验证签名
openssl rsautl -in test_sign.txt -out test_unsign.txt -inkey rsakeypari.pub -pubin -verify

4.证书管理

  • 生成X509格式的自签名证书

openssl req -x509 -new -days 365 -key rsakey.pem -out cert0.crt

会要求输入区别名DN的各项信息(国家,城市,组织,姓名,email等)。

根证书是认证中心机构(Certificate Authority)给自己签发的证书,签发者就是自身,是信任链的起点。里面包含了CA信息、CA公钥、用自身的私钥对这些信息的签名。下载并使用根证书就表示你信任它的来源机构,自然也信任证书以下签发的所有证书。某个证书可以用签发他的证书中的公钥验证,签发他的证书又需要上一层签发证书来验证,直到通过根证书中的公钥验证,那么这个证书就是可信任的。

  • 生成要求根证书签发子证书的请求文件

openssl req -new -key rsakey1.pem -out subcertreq.csr

会要求输入区别名DN的各项信息(国家,城市,组织,姓名,email等),还需要额外属性:密码 和 可选公司名。

  • 使用根证书签发子证书

openssl x509 -req -in subcertreq.csr -CA cert0.crt -CAkey rsakey0.pem -CAcreateserial -days 365 -out subcert.crt

也可创建一个ca的配置文件,通过ca管理子命令来签发子证书(未试验)

openssl ca -config ca.config -out user.crt -infiles user.csr

  • 将证书和密钥打包为pkcs12格式的库中

openssl pkcs12 -export -in subcert.crt -inkey rsakey1.pem -out subcert.p12

需要输入pkcs12文件密码。

  • 查看证书内容

openssl x509 -noout -text -in rootca.crt

  • 验证证书

    openssl verify -CAfile rootca.crt subcert.crt

用rootca.crt的公钥验证subcert.crt中的签名

从证书中提取公钥

openssl x509 -in cert.pem -noout -pubkey > pubkey.pem

提取密钥对

openssl pkcs12 -in cert.pfx -nocerts -nodes -out keypari.pem

从PKCS#8提取公钥

openssl req -in public.pem -noout -pubkey

查看pkcs7签名内容的证书信息

openssl pkcs7 -in -inform DER -print_certs

三、Keytool工具管理证书

keytool是Java提供的密钥、证书和证书库管理工具。可以完成生成密钥,生成证书等各种操作。

keytool的子命令如下:

-certreq     生成证书请求
-changealias   更改条目的别名
-delete      删除条目
-exportcert 导出证书
-genkeypair 生成密钥对
-genseckey 生成密钥
-gencert 根据证书请求生成证书
-importcert 导入证书或证书链
-importkeystore 从其他密钥库导入一个或所有条目
-keypasswd 更改条目的密钥口令
-list 列出密钥库中的条目
-printcert 打印证书内容
-printcertreq 打印证书请求的内容
-printcrl 打印 CRL 文件的内容
-storepasswd 更改密钥库的存储口令

还可以使用 keytool -_command_name_ -help 查看各个子命令的帮助信息

  • pkcs12库中证书导入jks证书库(java keystore格式)

keytool -importkeystore -srckeystore subcert.p12 -destkeystore subcert.jks -srcstoretype pkcs12

需要输入目标库的密码和源库的密码,如果jks库文件不存在的话会自动生成。

  • 导入证书到jks库

keytool -importcert -keystore subcert.jks -alias rootca -file rootcert.crt

需要输入目标库密码和是否信任添加的证书。-alias可以省略,如果jks库文件不存在的话会自动生成。

证书库或者说密钥库中即可以存放密钥也可存放证书,如果只包含证书(证书中有公钥)而不包含私钥,这样生成的库就是trust库。

查看证书指纹

keytool -list -keystore -alias

等待补充……

四、GPG

先整理下三个概念:

  • OpenPGP 是一种加密标准和协议(RFC4880)。
  • PGP(Pretty Good Privacy)是这个标准的闭源商业实现。
  • GPG(GnuPG)是这个标准的开源实现。

可见PGP和GPG是基于同一个标准的两套独立的工具,都能处理加解密、签名等功能,GPG是开源免费的。

官方文档:https://www.gnupg.org/documentation/manuals/gnupg/

GPG使用~/.gnupg目录。GPG除了基本的GnuPG核心命令功能外,GPG提供了一个名为Kleopatra的GUI密钥和证书管理工具,使用起来非常方便。Kleopatra的基本用法

GPG命令详解

-a 以ASCII输出

功能选择

–sign | -s 可以结合–encrypt或–symmetric或两者同时使用。使用默认的密钥。选择密钥使用–local-user和–default-key选项。

–clearsign 签名,内容可读。选择密钥使用–local-user和–default-key选项。

–detach-sign | -b 创建独立的签名

–encrypt | -e 非对称加密,可以结合–sign,–symmetric或两者同时使用

-c | –symmetric 对称加密。默认输出扩展名为.gpg,可以用-o指定输出文件,默认加密算法为CAST5,可以结合–cipher-algo指定算法。也可以配合–sign或–encrypt或两者同时使用。

等待补充……

示例:(http://blog.chinaunix.net/uid-9525959-id-2001824.html)

gpg -c myfile [-o myfile.gpg] #对称加密
gpg -d myfile.gpg -o myfile.decrypt #对称解密
gpg –genkey #交互式创建密钥
gpg –list-keys #查看已有的密钥
gpg -e -a -r privkey file [-o descfile] #加密(需要公钥)。 -e | –encryt, -a ASCII输出, -r 指定加密的用户ID。
gpg -o descfile –decrypt file.asc #解密(需要私钥),会提示输入密钥密码。
gpg -o pubkeyfile –export KeyID [-a] #导出公钥(证书),如果未指定KeyID,则备份所有公钥, -a ASCII输出否则为二进制格式。
gpg -o subkeyfile –export-secret-keys KeyID [-a] #导出私钥
gpg –import subkeyfile #导入私钥
gpg -o signedfile -s file #签名(需要私钥)
gpg -o txtsignedfile –clearsign file #保护原文的签名
gpg –verify signedfile #验证签名(需要公钥)
gpg -o encryptsignedfile -ser prikey file #签名并加密(恢复时验证,不能通过–verify直接验证)
gpg -o file –decrypt encryptsignedfile #恢复加密的签名文件并验证
gpg -o filesign -b -a file #-b分离的签名,生成文件只包含签名信息
gpg –verify filesign file #分离的签名验证
gpg –edit-key userID #编辑公钥,进入自命令环境:fpr 查看指纹, sign 签署公钥(加密是不再产生警告),check 检查已有的钥匙的签名, quit 退出

五、PGP

PGP是OpenPGP标准的闭源商业版本。pgp提供了图形化工具和命令行工具,命令行工作主要有以下几个:

  • pgpk.exe 密钥生成和管理
  • pgpe.exe 加密
  • pgps.exe 签名
  • pgpv.exe 解密和校验
  • pgpo.exe 模拟2.6.3老版本命令行方式

示例(http://blog.chinaunix.net/uid-7673620-id-2598657.html)

pgpk -g #交互式生产密钥
pgpk -x keyname [-o pubkeyfile] #导出公钥
pgpk -a pubkeyfile #将密钥加入密钥环(pubring.pkr文件) pgpk -r keyname #从密钥环删除公钥
pgpe -r keyname originfile [-o encryedfile] [-a] #加密,-a表示生成ASCII文档而不是二进制
pgpv encrypedfile -o decryedfile #解密(需要密钥密码)
pgps -u keyname originfile [-o signedfile] [-a] #签名(需要密钥密码),-a表示生成ASCII文档而不是二进制
pgpv signedfile #签名验证并恢复原始文件

pgp -ka KEYS #导入密钥
pgp file.asc #验证签名

六、PuTTY工具

Putty是一个免费小巧(主程序只有300多K)的Window/Unix平台的ssh,telnet客户端,附带xterm终端模拟器。功能丝毫不逊于商业的SecureCrt。官方网站:http://www.chiark.greenend.org.uk/~sgtatham/putty/

PuTTY包含多个工具。

  • PuTTY telnet和SSH客户端
  • PSCP SCP客户端
  • PSFTP SFTP客户端
  • PuTTYtel telnet客户端
  • Plink PuTTY后端的命令行接口
  • Pageant SSH认证代理,(PuTTY、PSCP、PSFTP、Plink都会用到)
  • PuTTYgen RSA和DSA密钥生成转换工具

PuTTYgen

本文主要内容是密钥的管理,所有只说明PuTTYgen的使用。

PuTTYgen使用的密钥存储格式类似于OpenSSL(密钥文件扩展名.ppk,其中同时包含公钥和私钥)。

公钥格式

---- BEGIN SSH2 PUBLIC KEY —- Comment: “imported-openssh-key” AAAAB3NzaC1yc2EAAAABIwAAAQEA5of83WbXgRVTUFsOI49wacuEp253YuzZW1Lk …… vw7Oc6o357ipDPgWk9c82s+7Asov4OgX5o6hg3R7418Gc0lkqw==
---- END SSH2 PUBLIC KEY —-

私钥格式(.ppk)

PuTTY-User-Key-File-2: ssh-rsa
Encryption: aes256-cbc
Comment: imported-openssh-key
Public-Lines: 6 AAAAB3NzaC1yc2EAAAABIwAAAQEA5of83WbXgRVTUFsOI49wacuEp253YuzZW1Lk …4行…
……
vw7Oc6o357ipDPgWk9c82s+7Asov4OgX5o6hg3R7418Gc0lkqw== Private-Lines: 14 xc2m3BR30d7GZG/QVatu8Xm5PfYYMXRfPobGtbqLsWWI9Wzfjs/InEVFPcG8kJ6v …12行… GANvUGQcgCyQTKVnqXmSMwPNZkqkJYZmRA43PBfiufVLV4JItNxY8ol+enUfWwde
Private-MAC: 647a57a82038c05113fbd6a1758871b138a7a001

可以导入和导出OpenSSH和ssh.com格式的私钥(Conversions菜单下)。程序主界面中也会显示OpenSSH格式的公钥。

—————————–未整理———————————-

其他

OpenSSL生成的密钥文件(.pem),签发请求文件(.csr)和证书文件(.cet)都是用纯文本格式保存了Base64编码后的信息(PEM格式)。可以用文本编辑器打开,内容如下:

—–BEGIN RSA PRIVATE KEY—–
MIIEogIBAAKCAQEArlUX2v998Y+Ek/AzoDdsbW7ILyRpwXVBMDqmOf3ZZpbP/4vo
…..省略若干行……
buL0a0BOFi7dyJjWgtEyYTFQgYSeezHl/+XyOhuJlTyvZWUpm5w=
—–END RSA PRIVATE KEY—–

首行和末尾行会被忽略,首行中显示了文件中存储的信息类型。

查看签名信息

jarsigner -verify -verbose -certs

PEM格式 (PKCS#8)

pem是最常用的私钥和证书存储格式。其中通常会包含 ‘—–BEGIN XXXX—–” and “—–END XXXX—–” 字符串,中间存放了Base64编码过的二进制数据。多个PEM证书或私钥可以包含在一个文件中,但最好还是将各个证书和私钥分开存放。扩展名可能为.pem, .crt, .cer, .key。

ASN格式

DER格式

主要用在Java,所有类型的证书和私钥都能用DER格式存放。文件中直接存储二进制数据,未进行Base64编码,无可读性的描述文字。扩展名可能为.cer, .der。

PVK格式(微软)

NET格式

P7B/PKCS#7文件(PEM)

包含 “—–BEGIN PKCS—–” & “—–END PKCS7—–” 字符串,只能存储证书和链证书,不能存放私钥。内容经过Base64编码,扩展名可能为.p7b,.p7c

PFX/PKCS#12文件(PEM)

可以加密的文件,可在一个文件中存放多个证书或密钥,主要用于证书和私钥的导入导出。扩展名一般为.pfx, .p12

PEM转DER

openssl x509 -outform der -in certificate.pem -out certificate.der

PEM格式证书转P7B

openssl crl2pkcs7 -nocrl -certfile certificate.cer -out certificate.p7b -certfileCAcert.cer

PEM格式转PFX

openssl pkcs12 -export -out certificate.pfx -inkey privateKey.key -in certificate.crt-certfile CAcert.crt

DER转PEM

openssl x509 -inform der -in certificate.cer -out certificate.pem

P7B转PEM

openssl pkcs7 -print_certs -in certificate.p7b -out certificate.cer

P7B转PFX(先转成PEM)

$ openssl pkcs7 -print_certs -in certificate.p7b -out certificate.cer
$ openssl pkcs12 -export -in certificate.cer -inkey privateKey.key -outcertificate.pfx -certfile CAcert.cer

PFX转PEM(PFX中所有内容被存放到一个pem格式文件中)

openssl pkcs12 -in certificate.pfx -out certificate.cer -nodes

PKCS#8定义了私钥信息语法和加密私钥语法,X509定义证书规范,密钥通常使用DER和PEM进行编码存储,Java中JCE使用的是DER。openssl主要用的是PEM编码。PEM相对DER可读性更强,以BASE64编码,外围包上类似—-BEGIN RSA PRIVATE KEY—-。 JCE没有对PEM直接支持,但是第三方那个包如bouncycastle可以解析。

很多SSH公钥使用openssh格式。其内容为ssh-rsa打头,RSA-1024结尾,中间是Base64编码:

ssh-rsa AAAAB3Nza…………………….cySYqQ== RSA-1024

参考资料:

OppenSSL文档: https://www.openssl.org/docs/apps/openssl.html

Keytool文档:http://docs.oracle.com/javase/8/docs/technotes/tools/unix/keytool.html

http://zhtx168.blog.163.com/blog/static/41601548200812503248/

http://blog.csdn.net/kimylrong/article/details/43525333

http://blog.csdn.net/jiftlixu/article/details/19836405

http://blog.csdn.net/zhymax/article/details/7683925

http://www.360doc.com/content/13/1203/17/14797374_334193770.shtml

转:密钥、证书生成和管理总结

最近在项目中需要支持用户输入emoji字符(如🚀),由于数据库的编码用的是utf8,所以需要做一些改造来予以支持。

1. 背景知识

  • UTF8
    • 在MySQL中utf8编码的字符会占用1-3个字节来存储数据
    • 只支持BMP#Basic_Multilingual_Plane)字符
  • UTF8MB4
    • 在MySQL中utf8mb4编码的字符会占用1-4个字节来存储数据
    • 支持BMP#Basic_Multilingual_Plane)字符
    • 还支持Supplementary#Supplementary_Multilingual_Plane)字符,比如emoji符号
    • MySQL 5.5以上才有utf8mb4编码

所以,从上面可以看出,utf8mb4是utf8的超集,所以从utf8升级到utf8mb4,理论上是没有任何问题的,不过对数据库版本有要求(5.5以上)。

2. 改造步骤

2.1 MySQL服务端

2.1.1 修改/etc/my.cnf并重启MySQL服务器

这一步非必须,如果实在由于某些原因无法修改数据库配置,可以通过后面2.3.3部分的客户端手动配置来解决。

[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci

2.2 MySQL数据库

2.2.1 修改数据库编码为utf8mb4

ALTER DATABASE `DBName` CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci;

可以通过以下SQL来生成alter语句:

use information_schema;
SELECT concat("ALTER DATABASE `",table_schema,"` CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci;") as _sql
FROM `TABLES` where table_schema like "yourDbName" group by table_schema;

2.2.2 修改数据表编码为utf8mb4

ALTER TABLE `TableName` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

可以通过以下SQL来生成所有表的alter语句:

use information_schema;
SELECT concat("ALTER TABLE `",table_schema,"`.`",table_name,"` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;") as _sql
FROM `TABLES` where table_schema like "yourDbName" group by table_schema, table_name;

2.2.3 修改表字段编码为utf8mb4

如果表字段没有特别指定编码的,就会默认使用表的编码,所以这步一般都可以省略。不过如果有不一样的话,可以通过下面SQL来修改。

ALTER TABLE `TableName ` CHANGE `ColumnName ` `ColumnName` `DataType(Length)` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT `xx` Comment `xx`;

2.3 Java客户端

2.3.1 升级JDBC驱动

确保mysql connector版本高于5.1.13,参见release note

2.3.2 JDBC数据库连接串

由于目前的java mysql connector还不支持utf8mb4,所以在jdbc的数据库连接串还只能使用utf8

jdbc:mysql://database_ip:3306/database_name?characterEncoding=utf8

2.3.3 数据库连接配置

如果在2.1部分,已经修改了数据库的配置并重启,那么这一步可以省略,因为mysql connector会自动继承服务器的设置。

如果2.1部分由于各种原因没有修改的话,那么就需要在客户端这里做设置了。

具体来说,就是在每个数据库连接建立的时候,通过运行set names utf8mb4来显式设置客户端连接的encoding为utf8mb4

如果客户端用的是tomcat-jdbc的话,可以通过initSQL属性来做设置。

initSQL - the ability to run a SQL statement exactly once, when the connection is created

对spring-boot应用而言,就是设置spring.datasource.initSQL属性:

spring.datasource.initSQL=set names utf8mb4

3. 使用utf8mb4后需要注意的地方

由于utf8mb4的字节长度是1-4个字节,而utf8的字节长度是1-3个字节,所以会有一些限制的变化。

3.1 字段长度限制

MySQL中的字段类型都有长度限制,比如varchar的最长字节长度是65535,所以使用utf8编码的时候,可以指定字段最长为65535/3=21845。如果使用utf8mb4编码的话,由于字符最长会占用4个字节,所以字段最长只能为65535/4=16383。

3.2 索引长度限制

MySQL中的索引也有长度限制: 767字节,所以使用utf8编码的的时候,可以指定索引字段最长为255字节,但是指定utf8mb4的话,只能索引191字节。

KEY XX_Index (XX(191))

3.3 行长度限制

MySQL中的行也有长度限制: 65535字节,所以当字段编码从utf8变为utf8mb4的时候,可能也会需要缩短部分字段的长度来满足行的长度限制。

4. 总结

从上面的改造步骤来看,从utf8到utf8mb4还是比较容易的,而且理论上来说是没有风险的,所以推荐大家尽快采用utf8mb4。另外,对于新的应用可以一上来就使用utf8mb4编码,以免真的碰到问题了再来改造过于被动。

原文地址:MySQL支持emoji字符

一、简介

阿波罗手机端是所有销售和销售主管(5000多人)使用的销售终端。销售可以使用阿波罗手机随时随地接收通知、查看业绩、录入拜访、维护客户、方案等信息,大大提高销售的工作效率(日活4000多人)。同时,阿波罗手机端还在公司内率先采用混合模式开发app,在提高开发效率的同时也为其他手机团队积累了框架和经验。

目前阿波罗各个服务提供方都提供了Restful服务供手机端调用,由于Restful接口通常是无状态的,所以我们设计了一种借助oAuth 2.0来对请求进行统一认证的机制。

下文就会对这一机制做一个详细的介绍。

二、概览

2.1 名词解释

在详细介绍之前,先来了解下会涉及到的几个名词

  • SSO服务
    • 公司的统一认证服务
    • 负责对用户身份进行认证
  • oAuth服务
    • 公司统一的oAuth服务
    • 负责签发、校验accessToken和refreshToken
  • 阿波罗oAuth服务
    • 用于把阿波罗项目和公司oAuth服务对接的服务,主要负责accessToken的获取和刷新
    • 我们单独设立一个服务,而不是把该项职责下放到客户端的原因是:
      • 公司oAuth服务对每个应用会分配一个appId和secret
      • 每次获取/刷新accessToken的时候都需要把appId和secret传给公司oAuth服务
      • 如果职责下放到客户端,就意味着secret也得暴露给客户端,不安全!
  • 阿波罗后端服务
    • 提供Restful服务的各个业务单元的后端,如客户服务,协议服务,团单服务,拜访服务,业绩服务等
  • 阿波罗Native
    • 阿波罗App的Native部分
    • 包括阿波罗的Native代码以及Efte框架
  • 阿波罗前端
    • 阿波罗App的前端部分
    • 主要是各个业务单元部署在手机端的前端代码(Javascript等)
    • 这些代码负责手机端的页面展现以及后端的数据交互
  • 授权码
    • oAuth服务签发的用于获取accessToken的凭证,时效性一般在几分钟
  • accessToken
    • oAuth服务签发的用于访问服务的凭证,时效性目前配置为1天
  • refreshToken
    • oAuth服务签发的用户刷新accessToken的凭证,如果连续3天未使用阿波罗App,就会失效

2.2 认证机制概览

下图为App整体和各项服务之间的依赖关系 阿波罗App统一认证整体概览

其中主要涉及以下两个方面,所以后面会通过这两块的流程图来简要介绍阿波罗统一认证机制的实现。

  1. App获取accessToken和refreshToken
  2. App通过accessToken来调用Restful服务以及通过refreshToken来刷新accessToken

2.2.1 App获取accessToken和refreshToken

App启动过程中,会去检查本地是否有认证信息存在,如果没有(第一次启动),就会去服务端请求该信息。

下图简要描述了App如何获取到accessToken和refreshToken App获取accessToken和refreshToken

2.2.2 App调用Restful服务过程

阿波罗App中有很多业务子模块(如业绩、拜访、客户、协议、团单等),每个业务子模块都会有很多和后端服务交互的过程。由于涉及到了敏感的业务数据和身份信息,所以我们必须要有一种统一的机制来对这一过程加以保障。

下图简要描述了页面调用Restful服务过程中的认证部分,主要是阿波罗Native如何统一处理认证相关的工作。 App调用Restful服务过程

三、详细步骤介绍

3.1 App获取accessToken和refreshToken

在App启动过程中,Native代码会去检查本地是否有认证信息存在。如果有,那么就会直接打开主页面。如果没有(第一次启动),就会去服务端请求该信息。

请求认证信息的详细过程如下:

  • Native新建一个webview,加载oAuth授权URL
    • 授权URL中会传入appId和callback url,如
    • appId是oAuth服务分配给应用的唯一标识符
    • callBackUrl是授权成功后的重定向地址,重定向的时候会带上授权码信息。Native代码通过拦截这个callBackUrl来获得授权码
    • callBackUrl需要事先在oAuth服务那里注册,后面每次认证的时候,oAuth服务都会做校验,判断传入的callBackUrl是否和注册的一致。
  • oAuth服务在授权之前会先要求用户登录,所以它会跳转到SSO来做安全认证
  • 如果用户已经登陆过,SSO就会跳转回oAuth服务页面。如果没有登陆过,那么就会要求用户登录。
  • 登录成功后,oAuth服务会根据appId和用户的身份,签发一个授权码。后面我们会通过这个凭证,appId和secret来获取accessToken。需要注意的是,这个凭证是有有效期的,一般配置在几分钟。
  • oAuth服务把callback url拼上授权码做重定向
  • Native代码拦截到callback url,解析出授权码。然后调用阿波罗的oAuth服务
  • 阿波罗oAuth服务通过授权码,appId和secret,就能从公司oAuth服务获取到accessToken和refreshToken并返回给客户端
  • Native代码把这两个token存储于本地数据空间,以备后面使用

3.2 App调用Restful服务过程

阿波罗App中有很多业务子模块(如业绩、拜访、客户、协议、团单等),每个业务子模块都会有很多和后端服务交互的过程。由于涉及到了敏感的业务数据和身份信息,所以我们必须要有一种统一的机制来对这一过程加以保障。

另外,阿波罗App是一个混合应用,所有的业务代码都是通过Javascript实现的,尽管JS本身有直接请求Restful服务的能力,我们最后还是决定通过Native来调用HTTP服务。主要原因有以下几点:

  1. 通过Native调用,可以解决跨域问题
  2. 在使用过程中,由于accessToken的时效性(目前配置是1天过期),会涉及到刷新accessToken,而在刷新accessToken的时候,我们需要能阻塞住全局所有的Restful调用,等accessToken刷新完再继续。这个需求通过Native能很简单的实现,而用JS会非常复杂(因为每个页面都是独立的web view)。

App调用Restful服务的详细过程如下:

  • 页面发起ajax请求,由于需要调用Native,我们封装了Efte.ajax组件来统一和Native交互,示例代码如下:

    Efte.ajax({
    method: ‘GET’,
    url: ‘https://xxx.com/customers',
    success: function(data) {

    //handle data
    

    },
    error: function(status, message) {

    //handle error
    

    }
    });

  • Efte.ajax通过jsBridge来调用Native方法(jsBridge涉及到Efte框架,如果感兴趣可以看一下Efte介绍

  • Native代码收到请求后,会组装HTTP请求,设置其中的Authorization header的值为accessToken,然后发出请求。示例代码如下:

    AFHTTPClient client = [[AFHTTPClient alloc] initWithBaseURL:url];
    [client setDefaultHeader:@”Authorization” value:[EFTEOAuthViewController accessToken]];
    NSMutableURLRequest
    request = [client requestWithMethod:method path:urlString parameters:params];

  • 我们要求每个阿波罗后端服务都写一个filter来对请求做校验,不过校验逻辑很简单。首先取出请求中的Authorization信息,然后调用oAuth服务来校验。如果成功,就继续处理请求,如果失败,直接返回HTTP状态码401。示例代码如下:

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp,

                     FilterChain filterChain) throws IOException, ServletException {
    HttpServletRequest request = (HttpServletRequest) req;
    HttpServletResponse response = (HttpServletResponse) resp;
    
    String authToken = request.getHeader("Authorization");
    if(!oauthUtil.validate(authToken)) {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
        return;
    }
    
    filterChain.doFilter(req, resp);
    

    }

  • Native端判断服务返回是否401,如果不是401,就把结果返回给JS端处理。

  • 如果返回的是401,就说明accessToken过期了,执行刷新accessToken逻辑
  • 首先启动全局阻塞Restful调用逻辑,把当前请求以及后续的所有Restful调用都存入队列等待。同时之前已经发出的请求由于accessToken过期也会陆续的回来,也会被放入队列等待。
  • 从本地取出refreshToken,然后调用阿波罗oAuth服务
  • 阿波罗oAuth服务通过refreshToken,appId和secret,调用公司oAuth服务来刷新accessToken
  • 如果刷新成功的话,Native端把新的accessToken更新到本地数据空间,解除全局阻塞Restful调用逻辑,同时对队列中的所有请求重发
  • 如果刷新失败的话,就说明refreshToken也过期了(连续3天未使用),需要重新进行oAuth授权。这一过程和3.1节的App获取accessToken和refreshToken过程是一样的,在此就不在赘述了。

四、小结

通过这样一套机制,我们解决了阿波罗App的安全认证问题,同时还收获了以下好处:

  • 安全认证统一实现,维护性和扩展性较好
    • 整个认证逻辑基本都在Native统一实现,所以后续不管是做维护还是扩展新特性都会比较容易
  • 对业务基本无侵入,各业务单元接入成本很低
    • 安全认证对前端JS透明,完全无感知
    • 后端接入很轻量,只需实现一个简单的filter逻辑即可
  • 借助oAuth 2.0,安全性有保障

原文地址:手机App的统一认证机制

7、重构单体应用为微服务

本书主要介绍如何使用微服务构建应用程序,这是本书的第七章,也是最后一章。第一章介绍了微服务架构模式,讨论了使用微服务的优点与缺点。随后的章节讨论了微服务架构的方方面面:使用 API ​​网关进程间通信服务发现事件驱动数据管理部署微服务。在本章中,我们将介绍单体应用迁移到微服务的策略。

我希望这本电子书能够让您对微服务架构、其优点和缺点以及何时使用它有很好的了解。微服务架构也许很适合您的组织。

您正工作于大型复杂的单体应用程序上,这是相当不错的机会。然而,您开发和部署应用程序的日常经历是缓慢而痛苦的。微服务似乎是一个遥不可及的天堂。幸运的是,有一些策略可以用来逃离单体应用的地狱。在本文中,我将描述如何将单体应用程序逐渐重构为一组微服务。

7.1、微服务重构概述

单体应用程序转换为微服务的过程是应用程序现代化的一种形式。这是几十年来开发人员一直在做的事情。因此,在将应用程序重构为微服务时,有一些想法是可以重用的。

一个不要使用的策略是“大爆炸”重写。就是您将所有的开发工作都集中在从头开始构建新的基于微服务的应用程序。虽然这听起来很吸引人,但非常危险,有可能会失败。据 Martin Fowler 讲到:“大爆炸重写的唯一保证就是大爆炸!”(”the only thing a Big Bang rewrite guarantees is a Big Bang!”)。

您应该逐步重构单体应用程序,而不是通过大爆炸重写。您可以逐渐添加新功能,并以微服务的形式创建现有功能的扩展 — 以互补的形式修改单体应用,并且一同运行微服务和修改后的单体应用。随着时间推移,单体应用程序实现的功能量会缩小,直到它完全消失或变成另一个微服务。这种策略类似于在 70公里/小时的高速公路上驾驶一辆汽车,很具挑战性,但比尝试大爆炸改写的风险要小得多。

葡萄树

Martin Fowler 将这种应用现代化策略称为杀手应用(Strangler Application)。这个名字来自发现于热带雨林中的葡萄树(也称为绞杀榕)。一棵葡萄树生长在一棵树上,以获取森林冠层之上的阳光。有时,树死了,留下一个树形的腾。应用现代化也遵循相同的模式。我们将构建一个新的应用程序,包括了围绕遗留应用的微服务(它将会慢慢缩小或者最终消亡)。

让我们来看看能做到这点的不同策略。

7.2、策略一:停止挖掘

洞穴定律说到,每当您身处在一个洞穴中,您应该停止挖掘。当您的单体应用变得难以管理时,这是一个很好的建议。换句话说,您应该停止扩张,避免使单体变得更大。这意味着当您要实现新功能时,您不应该向单体添加更多的代码。相反,这一策略的主要思想是将新代码放在独立的微服务中。

应用此方法后,系统架构如图 7-1 所示。

图 7-1、将新功能实现为单独的服务,而不是将模块添加到单体

除了新服务和遗留的单体应用,还有另外两个组件。第一个是请求路由,它处理传入的(HTTP)请求,类似于第二章中描述的 API 网关。路由向新服务发送与新功能相对应的请求。它将遗留的请求路由到单体应用。

另一个组件是粘合代码,它将微服务与单体应用集成。一个服务很少孤立存在,通常需要访问单体应用的数据。位于单体应用、微服务或两者中的粘合代码负责数据集成。该微服务使用粘合代码来读取和写入单体应用数据。

服务可以使用三种策略来访问单体应用的数据:

  • 调用由单体应用提供的远程 API
  • 直接访问单体应用的数据库
  • 维护自己的数据副本,与单体应用的数据库同步

粘合代码有时被称为防护层(anti-corruption layer)。这是因为粘合代码阻止了服务被遗留的单体应用领域模型的概念所污染,这些服务拥有属于自己的新的领域模型。粘合代码在两种不同的模型之间转换。防护层一词首先出现于埃里克·埃文斯(Eric Evans)所著的必读图书《领域驱动设计》(Domain Driven Design)中,并在白皮书中进行了改进。开发一个防护层并不是一件简单的事情。但是,如果您想要从单体应用地狱中走出来,这是必不可少的步骤。

使用轻量级服务来实现新功能有几个好处。它防止单体变得更加难以管理。该服务可以独立于单体应用开发、部署和扩展。可让您创建的每个新服务体验到微服务架构的优势。

然而,这种方法没有解决单体应用的问题。要解决这些问题,您需要分解单体应用。让我们来看看这样做的策略。

7.3、策略二:前后端分离

缩小单体应用的一个策略是从业务逻辑层和数据访问层拆分出表现层。一个典型的企业应用由至少三种不同类型的组件组成:

  • 表现层(Presentation Layer,PL)

    处理 HTTP 请求并实现(REST)API 或基于 HTML 的 Web UI 组件。在具有复杂用户界面的应用中,表现层通常存在大量代码。

  • 业务逻辑层(Business Logic Layer,BLL)

    作为应用程序核心,实现业务规则的组件。

  • 数据访问层(Data Access Layer,DAL)

    访问基础架构组件的组件,如数据库和消息代理。

表现逻辑部分与业务和数据访问逻辑部分之间通常有一个清晰的界限。业务层具有由一个或多个门面组成的粗粒度 API,其封装了业务逻辑组件。这个 API 是一个天然的边界,您可以沿着该边界将单体应用拆分成两个较小的应用程序。一个应用程序包含表现层。另一个应用程序包含业务和数据访问逻辑。分割后,表现逻辑应用程序对业务逻辑应用程序进行远程调用。

重构之前和之后的架构如图 7-2 所示。

图 7-2、重构现有的应用程序

以这种方式拆分单体应用有两个主要优点。它使您能够独立于彼此开发、部署和扩展这两个应用。特别是它允许表现层开发人员在用户界面上快速迭代,并且可以轻松执行 A/B 测试。这种方法的另一个优点是它暴露了可以被微服务调用的远程 API。

然而,这一策略只是一个局部解决方案。两个应用程序中的一个或两个很可能在将来膨胀成一个无法管理的单体应用。您需要使用第三种策略来继续消减单体应用。

7.4、策略三:提取服务

第三个重构策略是将庞大的现有模块转变为独立的微服务。每次提取一个模块并将其转换成微服务时,单体就会缩小。一旦您转换了足够的模块,单体应用将不再是一个问题。或者它完全消失,或者变得小到可以被当做一个服务看待。

7.4.1、优先将哪些模块转换为微服务

一个庞大而复杂的单体应用由几十个或几百个模块组成,所有模块都是提取的候选项。弄清楚要先转换哪些模块往往存在一定的挑战。一个好的方法是从容易提取的几个模块开始。这将给你的微服务开发带来好的体验,特别是在提取过程方面。之后,您应该提取那些能给您最大收益的模块。

将模块转换为服务通常是耗时的。按照您将获得的收益对模块进行排列。提取频繁更改的模块通常是有益的。一旦将模块转换为服务,您就可以独立于单体应用开发和部署,这将加快开发工作。

提取单体应用中相对来说耗资源的模块也是有益的。例如,将有一个有内存数据库的模块转换为服务是很有用的,这样可以部署在具有大量内存的主机上,无论是裸机服务器、虚拟机还是云服务器。同样,提取实现了大量复杂算法的模块也是值得的,因为该服务可以部署在具有大量 CPU 的主机上。通过将具有特定资源需求的模块转换为服务,您可以使应用程序更加容易、廉价地扩展。

当找到要提取的模块时,寻找现有的粗粒度边界(又称为接缝)是有用的。它们使模块转成服务更容易。有关这种边界的一个例子是一个仅通过异步消息与应用程序的其他部分进行通信的模块。将该模块转变为微服务相对比较廉价和简单。

7.4.2、如何提取模块

提取模块的第一步是在模块和单体应用之间定义一个粗粒度的接口。它很可能是一个双向 API,因为单体应用需要服务拥有的数据,反之亦然。由于该模块和应用程序的其余模块之间存在着复杂的依赖关系和细粒度的交互模式,因此实现这样的 API 通常存在挑战。由于领域模型类之间的众多关联,使用领域模型模式实现的业务逻辑的重构尤其具有挑战性。您通常需要进行重大的代码更改才能打破这些依赖。图 7-3 展示了重构。

图7-3、单体模块可转换为微服务

一旦实现了粗粒度的接口,您就可以将模块变成独立的服务。要做到这点,您必须编写代码以使单体应用和服务通过使用进程间通信(IPC)机制的 API 进行通信。图 7-3 显示了重构前、重构中和重构后的架构。

在此例中,模块 Z 是要提取的候选模块。它被模块 X 调用,并且它调用了模块 Y。第一个重构步骤是定义一对粗粒度的 API。第一个接口是一个使用模块 X 来调用模块 Z 的入站接口。第二个接口是一个使用模块 Z 调用模块 Y 的出站接口。

第二个重构步骤是将模块转换为一个独立服务。入站和出站接口使用 IPC 机制的代码来实现。您很可能需要把 Module Z 与 Microservice Chassis 框架相结合来构建服务,该框架负责处理诸如服务发现之类的横切点。

一旦您提取了一个模块,您就可以独立于单体应用和任何其他服务开发、部署和扩展服务。您甚至可以从头开始重写服务。在这种情况下,整合服务与单体应用的 API 代码成为在两个领域模型之间转换的防护层。每次提取服务时,您都会朝微服务方向迈近一步。随着时间的推移,单体将缩小,您将拥有越来越多的微服务。

7.5、总结

将现有应用程序迁移到微服务的过程是应用程序现代化的一种形式。您不应该从头开始重写您的应用来迁移到微服务。相反,您应该将应用程序逐渐重构为一组微服务。可以使用这三种策略:将新功能实现为微服务;从业务组件和数据访问组件中分离出表现组件;将单体中的现有模块转换为服务。随着时间推移,微服务的数量将会增长,您的开发团队的灵活性和速度也同样会增加。

微服务实战:使用 NGINX 征服单体

by Floyd Smith

如本章所述,将单体转换为微服务可能是一个缓慢而具有挑战性的过程,但这同样具有许多好处。使用 NGINX,您可以在实际开始转换过程之前获得微服务器的一些优势。

您可以通过将 NGINX 放在您现有的单体应用之前,以节省迁移微服务所花费的大量时间。以下简要说明与微服务有关的好处:

  • 更好地支持微服务

    如第五章尾栏所述,NGINX 和 NGINX Plus 具有利于开发基于微服务的应用的功能。当您开始重新设计单体应用时,由于 NGINX 的功能,您的微服务将执行得更好、更易于管理。

  • 跨环境的功能抽象

    从您管理的服务器甚至是各种公共云、私有云和混合云上将功能迁移到 NGINX 作为反向代理服务器可以减少部署在新环境中的设施数量变化。这补充扩展了微服务所固有的灵活性。

  • NGINX 微服务参考架构可用性

    当您迁移到 NGINX 时,您可以借鉴 NGINX 微服务参考架构(MRA,Microservices Reference Architecture),以便在迁移到微服务之后定义应用程序的最终结构,并根据需要使用的 MRA 部分应用于您创建的每个新的微服务。

总而言之,实现使用 NGINX 作为您转型的第一步,压倒您的单体应用程序,使其更容易获得微服务的所有优势,并为您提供用于进行转换的模型。您可以了解有关 MRA 的更多信息,并获得 NGINX Plus 的免费试用版。

转:《微服务:从设计到部署》中文版

6、选择部署策略

本书主要介绍关于如何使用微服务构建应用程序,这是本书的第六章。第一章介绍了微服务架构模式,讨论了使用微服务的优点与缺点。之后的章节讨论了微服务架构的方方面面:使用 API 网关进程间通信服务发现事件驱动数据管理。在本章中,我们将介绍部署微服务的策略。

6.1、动机

部署单体应用程序意味着运行一个或多个相同副本的单个较大的应用程序。您通常会配置 N 个服务器(物理或虚拟),每台服务器上会运行 M 个应用程序实例。单体应用程序的部署并不总是非常简单,但它比部署微服务应用程序要简单得多。

微服务应用程序由数十甚至上百个服务组成。服务以不同的语言和框架编写。每个都是一个迷你的应用程序,具有自己特定的部署、资源、扩展和监视要求。例如,您需要根据该服务的需求运行每个服务的一定数量的实例。此外,必须为每个服务实例提供相应的 CPU、内存和 I/O 资源。更具挑战性的是尽管如此复杂,部署服务也必须快速、可靠和具有成本效益。

有几种不同的微服务部署模式。我们首先看看单主机多服务实例模式。

6.2、单主机多服务实例模式

部署微服务的一种方式是使用单主机多服务实例(Multiple Service Instances per Host)模式。当使用此模式时,您可以提供一个或多个物理主机或虚拟主机,并在每个上运行多个服务实例。从多方面来讲,这是应用程序部署的传统方式。每个服务实例在一个或多个主机的标准端口上运行。主机通常被当作宠物对待

图 6-1 展示了该模式的结构:

图 6-1、主机可支持多个服务实例

这种模式有几个变体。一个变体是每个服务实例都是一个进程或进程组。例如,您可以在 Apache Tomcat 服务器上将 Java 服务实例部署为 Web 应用程序。一个 Node.js 服务实例可能包含一个父进程和一个或多个子进程。

此模式的另一个变体是在同一进程或进程组中运行多个服务实例。例如,您可以在同一个 Apache Tomcat 服务器上部署多个 Java Web 应用程序,或在同一 OSGI 容器中运行多个 OSGI 软件包。

单主机多服务实例模式有优点也有缺点。主要优点是其资源使用率相对较高。多个服务实例共享服务器及其操作系统。如果进程或进程组运行了多个服务实例(例如,共享相同的 Apache Tomcat 服务器和 JVM 的多个 Web 应用程序),则效率更高。

这种模式的另一个优点是部署服务实例相对较快。您只需将服务复制到主机并启动它。如果服务是使用 Java 编写的,则可以复制 JAR 或 WAR 文件。对于其他语言,例如 Node.js 或 Ruby,您可以直接复制源代码。在任一情况下,通过网络复制的字节数都是相对较小的。

另外,由于缺乏开销,通常启动一个服务是非常快的。如果该服务是自己的进程,你只需要启动它即可。如果服务是在同一容器进程或进程组中运行的几个实例之一,则可以将其动态部署到容器中或者重新启动容器。

尽管这很有吸引力,但单主机多服务实例模式有一些明显的缺点。一个主要的缺点是服务实例很少或者没有隔离,除非每个服务实例是一个单独的进程。虽然您可以准确地监视每个服务实例的资源利用率,但是您不能限制每个实例使用的资源。一个行为不当的服务实例可能会占用掉主机的所有内存或 CPU。

如果多个服务实例在同一进程中运行,那么将毫无隔离可言。例如,所有实例可能共享相同的 JVM 堆。行为不当的服务实例可能会轻易地破坏在同一进程中运行的其他服务。此外,您无法监控每个服务实例使用的资源。

这种方式的另一个重要问题是部署服务的运维团队必须了解部署的具体细节。服务可以用多种语言和框架编写,因此开发团队必须与运维交代许多细节。这种复杂性无疑加大了部署过程中的错误风险。

正如您所见,尽管这种方式简单,但单主机多服务实例模式确实存在一些明显的缺点。现在让我们来看看可以绕过这些问题部署微服务的其他方式。

6.3、每个主机一个服务实例模式

部署微服务的另一种方式是使用每个主机一个服务实例(Service Instance per Host)模式。当使用此模式时,您可以在主机上单独运行每个服务实例。这种模式有两种不同形式:每个虚拟机一个服务实例模式和每个容器一个服务实例模式。

6.3.1、每个虚拟机一个服务实例模式

当您使用每个虚拟机一个服务实例模式时,将每个服务打包成一个虚拟机(VM)镜像(如 Amazon EC2 AMI)。每个服务实例都是一个使用该 VM 镜像启动的 VM(例如,一个 EC2 实例)。

图 6-2 展示了该模式的结构:

图 6-2、服务可以各自运行在自己的虚拟机中

这是 Netflix 部署其视频流服务的主要方式。Netflix 使用 Aminator 将每个服务打包为 EC2 AMI。每个运行的服务实例都是一个 EC2 实例。

您可以使用多种工具来构建自己的虚拟机。您可以配置您的持续集成(CI)服务器(比如 Jenkins)来调用 Aminator 将服务打包为一个 EC2 AMI。Packer 是自动化虚拟机镜像创建的另一个选择。与 Aminator 不同,它支持各种虚拟化技术,包括 EC2、DigitalOcean、VirtualBox 和 VMware。

Boxfuse 公司有一种非常棒的方式用来构建虚拟机镜像,其克服了我将在下面描述的虚拟机的缺点。Boxfuse 将您的 Java 应用程序打包成一个最小化的 VM 镜像。这些镜像可被快速构建、快速启动且更加安全,因为它们暴露了一个有限的攻击面。

CloudNative 公司拥有 Bakery,这是一种用于创建 EC2 AMI 的 SaaS 产品。您可以配置您的 CI 服务器,在微服务通过测试后调用 Bakery。之后 Bakery 将您的服务打包成一个 AMI。使用一个如 Bakery 的 SaaS 产品意味着您不必浪费宝贵的时间来设置 AMI 创建基础架构。

每个虚拟机一个服务实例模式有许多优点。VM 的主要优点是每个服务实例运行是完全隔离的。它有固定数量的 CPU 和内存,且不能从其他服务窃取资源。

将微服务部署为虚拟机的另一个优点是可以利用成熟的云基础架构。如 AWS 之类的云提供了很多有用的功能,例如负载平衡和自动扩缩。

将服务部署为虚拟机的另一个好处是它封装了服务的实现技术。一旦服务被打包成一个虚拟机,它就成为一个黑匣子。VM 的管理 API 成为部署服务的 API。部署变得更加简单、可靠。

然而,每个虚拟机一个服务实例模式也有一些缺点。一个缺点是资源利用率较低。每个服务实例都有一整个 VM 开销,包括操作系统。此外,在一个典型的公共 IaaS 中,VM 具有固定大小,并且 VM 可能未被充分利用。

此外,公共 IaaS 中的 VM 通常是收费的,无论它们是处于繁忙还是空闲。如 AWS 之类的 IaaS 虽然提供了自动扩缩功能,但很难快速响应需求变化。因此,您经常需要过度配置 VM,从而增加部署成本。

这种方法的另一缺点是部署新版本的服务时通常很慢。由于大小原因,通常 VM 镜像构建很慢。此外,VM 实例化也很慢,同样是因为它们的大小。而且,操作系统也需要一些时间来启动。但请注意,这并不普遍,因为已经存在由 Boxfuse 构建的轻量级 VM。

每个虚拟机一个服务实例模式的另一个缺点是通常您(或组织中的其他人)要对很多未划分的重担负责。除非您使用 Boxfuse 这样的工具来处理构建和管理虚拟机的开销,否则这将是您的责任。这个必要而又耗时的活动会分散您的核心业务。

接下来让我们看看另一种部署更轻量级微服务的替代方式,它也有许多与虚拟机一样的优势。

6.3.2、每个容器一个服务实例模式

当您使用每个容器一个服务实例模式(Service Instance per Container)模式时,每个服务实例都在其自己的容器中运行。容器是一个操作系统级虚拟化机制。一个容器是由一个或多个运行在沙箱中的进程组成。从进程的角度来看,它们有自己的端口命名空间和根文件系统。您可以限制容器的内存和 CPU 资源。一些容器实现也具有 I/O 速率限制。容器技术的相关例子有 DockerSolaris Zones

图 6-3 展示了该模式的结构:

图 6-3、服务可以各自运行在自己的容器中

要使用此模式,请将您的服务打包成一个容器镜像。容器镜像是由运行服务所需的应用程序和库组成的文件系统镜像。一些容器镜像由完整的 Linux 根文件系统组成。其它的则更加轻便。例如,要部署一个 Java 服务,您可以构建一个包含了 Java 运行时的容器镜像,或许也包含一个 Apache Tomcat 服务器和编译好的 Java 应用程序。

将服务打包成一个容器镜像后,您将启动一个或多个容器。通常在每个物理或虚拟主机上运行多个容器。您可以使用集群管理工具(如 KubernetesMarathon)来管理容器。集群管理工具将主机视为一个资源池。它根据容器所需的资源和每个主机上可用的资源来决定每个容器放置的位置。

每个容器一个服务实例模式模式有优点和缺点。容器的优点与虚拟机的类似。它们将服务实例彼此隔离。您可以轻松地监控每个容器所消耗的资源。此外,与 VM 一样,容器封装了服务的实现技术。容器管理 API 作为管理您的服务的 API。

然而,与虚拟机不同,容器是轻量级技术。容器镜像通常可以非常快速地构建。例如,在我的笔记本电脑上,将一个 Spring Boot 应用程序打包成一个 Docker 容器只需要 5 秒钟的时间。容器也可以很快地启动,因为没有繁琐的操作系统引导机制。当一个容器启动时,它所运行的就是服务。

使用容器有一些缺点。虽然容器基础架构正在快速发展走向成熟,但它并不像虚拟机的基础架构那么成熟。此外,容器不像 VM 那样安全,因为容器彼此共享了主机的 OS 内核。

容器的另一个缺点是您需要负责未划分的容器镜像管理的负担。此外,除非您使用了托管容器解决方案[如 Google Container EngineAmazon EC2 Container Service(ECS)],否则您必须自己管理容器基础设施,可能还有运行容器的 VM 基础架构。

此外,容器通常部署在一个按单个 VM 收费的基础设施上。因此,如之前所述,可能会产生超额配置 VM 的额外成本,以处理负载峰值。

有趣的是,容器和 VM 之间的区别可能会有些模糊。如之前所述,Boxfuse VM 可以很快地构建和启动。Clear Containers 项目旨在创建轻量级虚拟机。Unikernels 也正在蓬勃发展。Docker 公司于 2016 年初收购了 Unikernel 系统。

还有一个日益流行的 server-less(无服务器)部署概念,这是一种避免了“在容器中还是在虚拟机中部署服务”问题的方法。接下来我们来看看。

6.4、Serverless 部署

AWS Lambda 就是一个 serverless 部署技术示例。它支持 Java、Node.js 和 Python 服务。要部署微服务,请将其打包成 ZIP 文件并将上传到 AWS Lambda。您还要提供元数据,其中包括了被调用来处理请求(又称为事件)的函数的名称。AWS Lambda 自动运行足够的微服务服务实例来处理请求。您只需根据每个请求所用时间和内存消耗来付费。当然,问题往往出现在细节上,您很快注意到了 AWS Lambda 的局限性。但是,作为开发人员的您或组织中的任何人都无需担心服务器、虚拟机或容器的任何方面 ,这非常有吸引力,足以令人难以置信。

Lambda 函数是无状态服务。它通常通过调用 AWS 服务来处理请求。例如,当图片上传到 S3 存储桶时Lambda 函数将被调用,该函数可插入一条记录到 DynamoDB 图片表中,并将消息发布到 Kinesis 流以触发图片处理。 Lambda 函数还可以调用第三方 Web 服务。

有四种方法调用 Lambda 函数:

  • 直接使用 Web 服务请求
  • 自动响应一个 AWS 服务(如 S3、DynamoDB、Kinesis 或 Simple Email Service)生成的事件
  • 通过 AWS API 网关自动处理来自应用程序客户端的 HTTP 请求
  • 按照一个类似 cron 的时间表,定期执行

正如您所见,AWS Lambda 是一个便捷的微服务部署方式。基于请求的定价意味着您只需为服务实际执行的工作付费。另外,由于您不需要对 IT 基础架构负任何责任,因此可以专注于开发应用程序。

然而,其也存在一些明显的局限性。Lambda 函数不适用于部署长时间运行的服务,例如消耗第三方消息代理消息的服务。请求必须在 300 秒内完成。服务必须是无状态的,因为理论上,AWS Lambda 可能为每个请求运行一个单独的实例。他们必须使用受支持的语言来编写。服务启动速度也必须快,否则,他们可能会因超时而终止。

6.5、总结

部署微服务应用程序充满着挑战。您可能有数个甚至数百个使用了各种语言和框架编写的服务。每个应用程序都是一个迷你应用程序,有自己特定的部署、资源、扩展和监视需求。有几个微服务部署模式,包括每个虚拟机一个服务实例和每个容器一个服务实例模式。部署微服务的另一个有趣的选择是 AWS Lambda,一种 serverless 方式。在本书的下一章也是最后一章中,我们将介绍如何将单体应用程序迁移到微服务架构。

微服务实战:使用 NGINX 在不同主机上部署微服务

by Floyd Smith

NGINX 对于各种类型的部署具有许多优势 — 无论是单体应用程序、微服务应用程序还是混合应用程序(将在下一章介绍)。使用 NGINX,您可以智能抽取不同的部署环境出来并整合入 NGINX。如果您使用针对不同部署环境的工具,则有许多应用程序功能将以不同的方式工作,但如果使用 NGINX,那么在所有环境中都可以使用相同的方式进行工作。

这一特性也为 NGINX 和 NGINX Plus 带来了第二个优势:通过在多个部署环境中同时运行应用程序来扩展应用程序的能力。假设您拥有和管理着的本地服务器,但是您的应用程序使用情况正在增长,并且预计将超出这些服务器可以处理的峰值。如果你已经使用了 NGINX,您就有了一个强大的选择:扩展到云端 — 例如,扩展到 AWS 上,而不是购买、配置和保持额外的服务器来为了以防万一。也就是说,当您的本地服务器上的流量达到容量限制时,可根据需要在云中启动其他微服务实例来处理。

这只是因使用 NGINX 变得更加灵活的一个例子。维护单独的测试和部署环境、切换环境的基础设施、以及管理各种环境中的应用程序组合都变得更加现实和可实现。

NGINX 微服务参考架构被明确设计为支持这种灵活部署,其假设在开发和部署期间使用容器技术。如果您还没尝试,可以考虑转移到容器、NGINX 或 NGINX Plus,以轻松地转移到微型服务,以及使您的应用程序、开发和部署灵活性以及人员更具前瞻性。

转:《微服务:从设计到部署》中文版

5、事件驱动数据管理

本书主要介绍如何使用微服务构建应用程序,这是本书的第五章。第一章介绍了微服务架构模式,讨论了使用微服务的优点与缺点。第二第三章描述了微服务架构内通信方式的对比。第四章探讨了与服务发现相关的内容。在本章中,我们稍微做了点调整,研究微服务架构中出现的分布式数据管理问题。

5.1、微服务和分布式数据管理问题

单体应用程序通常具有一个单一的关系型数据库。使用关系型数据库的一个主要优点是您的应用程序可以使用 ACID 事务,这些事务提供了以下重要保障:

  • 原子性(Atomicity) — 所作出的更改是原子操作,不可分割
  • 一致性(Consistency) — 数据库的状态始终保持一致
  • 隔离性(Isolation) — 即使事务并发执行,但他们看起来更像是串行执行
  • 永久性(Durable) — 一旦事务提交,它将不可撤销

因此,您的应用程序可以很容易地开始事务、更改(插入、更新和删除)多行记录,并提交事务。

使用关系型数据库的另一大好处是它提供了 SQL,这是一种丰富、声明式和标准化的查询语言。您可以轻松地编写一个查询,组合来自多个表的数据,之后,RDBMS 查询计划程序将确定执行查询的最佳方式。您不必担心如何访问数据库等底层细节。因为您所有的应用程序数据都存放在同个数据库中,因此很容易查询。

很不幸的是,当我们转向微服务架构时,数据访问将变得非常复杂。因为每个微服务所拥有的数据对当前微服务来说是私有的,只能通过其提供的 API 进行访问。封装数据可确保微服务松耦合、独立演进。如果多个服务访问相同的数据,模式(schema)更新需要对所有服务进行耗时、协调的更新。

更糟糕的是,不同的微服务经常使用不同类型的数据库。现代应用程序存储和处理着各种数据,而关系型数据库并不总是最佳选择。在某些场景,特定的 NoSQL 数据库可能具有更方便的数据模型,提供了更好的性能和可扩展性。例如,存储和查询文本的服务使用文本搜索引擎(如 Elasticsearch)是合理的。类似地,存储社交图数据的服务应该可以使用图数据库,例如 Neo4j。因此,基于微服务的应用程序通常混合使用 SQL 和 NoSQL 数据库,即所谓的混合持久化(polyglot persistence)方式。

分区的数据存储混合持久化架构具有许多优点,包括了松耦合的服务以及更好的性能与可扩展性。然而,它也引入了一些分布式数据管理方面的挑战。

第一个挑战是如何实现业务的事务在多个服务之间保持一致性。要了解此问题,让我们先来看一个在线 B2B 商店的示例。Customer Service (顾客服务)维护客户相关的信息,包括信用额度。Order Service (订单)负责管理订单,并且必须验证新订单,不得超过客户的信用额度。在此应用程序的单体版本中,Order Service 可以简单地使用 ACID 事务来检查可用信用额度并创建订单。

相比之下,在微服务架构中,ORDER (订单)和 CUSTOMER (顾客)表对其各自的服务都是私有的,如图 5-1 所示:

图 5-1、每个微服务都有各自的数据

Order Service 无法直接访问 CUSTOMER 表。它只能使用客户服务提供的 API。订单服务可能使用了分布式事务,也称为两阶段提交(2PC)。然而,2PC 在现代应用中通常是不可行的。CAP 定理要求您在可用性与 ACID 式一致性之间做出选择,可用性通常是更好的选择。此外,许多现代技术,如大多数 NoSQL 数据库,都不支持 2PC。维护服务和数据库之间的数据一致性至关重要,因此我们需要另一套解决方案。

第二个挑战是如何实现从多个服务中检索数据。例如,我们假设应用程序需要显示一个顾客和他最近的订单。如果 Order Service 提供了用于检索客户订单的 API,那么您可以使用应用程序端连接以检索数据。应用程序从 Customer Service 中检索客户,并从 Order Service 中检索客户的订单。但是,假设 Order Service 仅支持通过主键查找订单(也许它使用了仅支持基于主键检索的 NoSQL 数据库)。在这种情况下,没有有效的方法来检索所需的数据。

5.2、事件驱动架构

许多应用使用了事件驱动架构作为解决方案。在此架构中,微服务在发生某些重要事件时发布一个事件,例如更新业务实体时。其他微服务订阅了这些事件,当微服务接收到一个事件时,它可以更新自己的业务实体,这可能导致更多的事件被发布。

您可以使用事件实现跨多服务的业务的事务。一个事务由一系列的步骤组成。每个步骤包括了微服务更新业务实体和发布一个事件来触发下一步骤。下图依次展示了如何在创建订单时使用事件驱动方法来检查可用信用额度。

微服务通过 Message Broker (消息代理)进行交换事件:

  • Order Service (订单服务)创建一个状态为 NEW 的订单,并发布一个 Order Created (订单创建)事件。

图 5-2、Order Service 发布一个事件

  • Customer Service (客户服务)消费了 Order Created 事件,为订单预留信用额度,并发布 Credit Reserved 事件。

图 5.3、Customer Service 响应

  • Order Service 消费了 Credit Reserved (信用预留)事件并将订单的状态更改为 OPEN。

图 5-4、Order Service 作用于响应

更复杂的场景可能会涉及额外的步骤,例如在检查客户信用的同时保留库存。

假设(a)每个服务原子地更新数据库并发布一个事件 (稍后再详细说明),(b)Message Broker 保证事件至少被送达一次,然后您就实现了跨多服务的业务事务。需要注意的是,这些并不是 ACID 事务。它们只提供了更弱的保证,如最终一致性。该事务模型称为 BASE 模型

属于多个微服务的数据构成的物化视图,你也可以用事件来维护。维护视图的服务订阅了相关事件并更新视图。图 5-5 展示了 Customer Order View Updater Service (客户订单视图更新服务)根据 Customer
Service 和 Order Service 发布的事件更新 Customer Order View (客户订单视图)。

图 5-5 Customer Order View 被两个服务访问

当 Customer Order View Updater Service 接收到 Customer 或 Order 事件时,它会更新 Customer Order View 的数据存储。您可以使用如 MongoDB 之类的文档数据库实现 Customer Order View,并为每个 Customer 存储一个文档。Customer Order View Query Service(客户订单视图查询服务)通过查询 Customer Order View 数据存储来处理获取一位客户和最近的订单的请求。

事件驱动的架构有几个优点与缺点。它能够实现跨越多服务并提供最终一致性的事务。另一个好处是它还使得应用程序能够维护物化视图

一个缺点是其编程模型比使用 ACID 事务更加复杂。通常,您必须实现补偿事务以从应用程序级别的故障中恢复。例如,如果信用检查失败,您必须取消订单。此外,应用程序必须处理不一致的数据。因为未提交的事务所做的更改是可见的。如果从未更新的物化视图中读取,应用程序依然可以看到不一致性。另一个缺点是订阅者必须要检测和忽略重复的事件。

5.3、实现原子性

在事件驱动架构中,同样存在着原子更新数据库和发布事件相关问题。例如,Order Service 必须在 ORDER 表中插入一行数据,并发布 Order Created 事件。这两个操作必须原子完成。如果在更新数据库后但在发布事件之前发生服务崩溃,系统将出现不一致性。确保原子性的标准方法是使用涉及到数据库和 Message Broker 的分布式事务。然而,由于上述原因,如 CAP 定理,这并不是我们想做的。

5.4、使用本地事务发布事件

实现原子性的一种方式是应用程序使用仅涉及本地事务的多步骤过程来发布事件。诀窍在于存储业务实体状态的数据库中有一个用作消息队列的 EVENT 表。应用程序开启一个(本地)数据库事务,更新业务实体状态,将事件插入到 EVENT 表中,之后提交事务。一个单独的应用程序线程或进程查询 EVENT 表,将事件发布到 Message Broker,然后使用本地事务将事件标记为已发布。设计如图 5-6 所示。

图 5-6、本地事务实现原子性

Order Service 将一行记录插入到 ORDER 表中,并将一个 Order Created 事件插入到 EVENT 表中。Event Publisher(事件发布者)线程或进程从 EVENT 表中查询未发布的事件,之后发布这些事件,最后更新 EVENT 表将事件标记为已发布。

这种方法有好有坏。好处是它保证了被发布的事件每次更新都不依赖于 2PC。此外,应用程序发布业务级事件,这些事件可以消除推断的需要。这种方法的缺点是它很容易出错,因为开发人员必须要记得发布事件。这种方法的局限性在于,由于其有限的事务和查询功能,在使用某些 NoSQL 数据库时,实现起来将是一大挑战。

该方法通过让应用程序使用本地事务更新状态和发布事件来消除对 2PC 的依赖。现在我们来看一下通过应用程序简单地更新状态来实现原子性的方法。

5.5、挖掘数据库事务日志

不依靠 2PC 来实现原子性的另一种方式,是用一个对数据库的事务或者提交日志进行挖掘的线程或进程来发布事件。当应用程序更新数据库时,更改信息被记录到数据库的事务日志中。事务日志挖掘器(Transaction Log Miner) 线程或进程读取事务日志并向 Message Broker 发布事件。设计如图 5-7 所示。

图 5-7、Message Broker 可以公断数据事务

一个使用此方法的示例是 LinkedIn Databus 开源项目。Databus 挖掘 Oracle 事务日志并发布与更改相对应的事件。LinkedIn 使用 Databus 保持与系统的记录一致的各种派生数据存储。

另一个例子是 AWS DynamoDB 中的流机制,它是一个托管的 NoSQL 数据库。DynamoDB 流包含了在过去 24 小时内对 DynamoDB 表中的项进行的更改(创建、更新和删除操作),其按时间顺序排列。应用程序可以从流中读取这些更改,比如,将其作为事件发布。

事务日志挖掘有各种好处与坏处。一个好处是它能保证被发布的事件每次更新都不依赖于 2PC。事务日志挖掘还可以通过将事件发布与应用程序的业务逻辑分离来简化应用程序。一个主要的缺点是事务日志的格式对于每个数据库来说都是专有的,甚至在不同数据库版本之间格式就发生了改变。而且,记录于事务日志中的低级别更新可能难以对高级业务事件进行逆向工程。

事务日志挖掘消除了应用程序在做一件事时对 2PC 的依赖:更新数据库。现在我们来看看另一种可以消除更新并仅依赖于事件的不同方式。

5.6、使用事件溯源

事件溯源通过使用不同于之前的、以事件为中心的方式来持久化业务实体,实现无 2PC 原子性。应用程序不存储实体的当前状态,而是存储一系列状态改变事件。应用程序通过回放事件来重建实体的当前状态。无论业务实体的状态何时发生变化,其都会将新事件追加到事件列表中。由于保存事件是一个单一操作,因此具有原子性。

要了解事件溯源的工作原理,以 Order(订单)实体为例。在传统方式中,每个订单都与 ORDER 表中的某行记录相映射,也可以映射到例如 ORDER_LINE_ITEM 表中的记录。

但当使用事件溯源时,Order Service 将以状态更改事件的形式存储 Order:Created(创建)、Approved(批准)、Shipped(发货)、Cancelled(取消)。每个事件包含足够的数据来重建 Order 的状态。

图 5-8、事件有完整的数据

事件被持久化在事件存储中,事件存储是一个事件的数据库。该存储有一个用于添加和检索实体事件的 API。事件存储还与我们之前描述的架构中的 Message Broker 类似。它提供了一个 API,使得服务能够订阅事件。事件存储向所有感兴趣的订阅者派发所有事件。可以说事件存储是事件驱动微服务架构的支柱。

事件溯源有几个好处。它解决了实现事件驱动架构的关键问题之一,可以在状态发生变化时可靠地发布事件。因此,它解决了微服务架构中的数据一致性问题。此外,由于它持久化的是事件,而不是领域对象,所以它主要避免了对象关系阻抗失配问题。事件溯源还提供了对业务实体所做更改的 100% 可靠的审计日志,可以实现在任何时间点对实体进行时间查询以确定状态。事件溯源的另一个主要好处是您的业务逻辑包括松耦合的交换事件业务实体,这使得从单体应用程序迁移到微服务架构将变得更加容易。

事件溯源同样有缺点。这是一种不同而陌生的编程风格,因此存在学习曲线。事件存储仅支持通过主键查找业务实体。您必须使用命令查询责任分离(CQRS)来实现查询。因此,应用程序必须处理最终一致的数据。

5.7、总结

在微服务架构中,每个微服务都有私有的数据存储。不同的微服务可能会使用不同的 SQL 或者 NoSQL 数据库。虽然这种数据库架构具有明显的优势,但它创造了一些分布式数据管理挑战。第一个挑战是如何实现维护多个服务间的业务事务一致性。第二个挑战是如何实现从多个服务中检索数据。

大部分应用使用的解决方案是事件驱动架构。实现事件驱动架构的一个挑战是如何以原子的方式更新状态以及如何发布事件。有几种方法可以实现这点,包括了将数据库作为消息队列、事务日志挖掘和事件溯源。

微服务实战:NGINX 与存储优化

by Floyd Smith

基于微服务的存储方式涉及大数量和各种数据存储,访问和更新数据将变得更加复杂,DevOps 在维护数据一致性方面面临着更大的挑战。NGINX 为这种数据管理提供了重要支持,主要有三个方面:

  1. 数据缓存与微缓存(microcaching)

    使用 NGINX 缓存静态文件和微缓存应用程序生成的内容可减轻应用程序的负载、提高性能并减少问题的发生。

  2. 数据存储的灵活性与可扩展性

    一旦将 NGINX 作为反向代理服务器,您的应用程序在创建、调整大小、运行和调整数据存储服务器的大小时可获得很大的灵活性,以满足不断变化的需求 — 每个服务都拥有自己的数据存储是很重要的。

  3. 服务监控与管理,包括数据服务

    随着数据服务器数量的增加,支持复杂操作和具有监控和管理工具显得非常重要。NGINX Plus 内置了这些工具和应用程序性能管理合作伙伴的接口,如 Data Dog、Dynatrace 和 New Relic。

微服务相关的数据管理示例可在 NGINX 微服务参考架构的三大模型中找到,其为您设计决策和实施提供了起点。

4、服务发现

本书主要介绍如何使用微服务来构建应用程序,现在是第四章。第一章已经介绍了微服务架构模式,并讨论了使用微服务的优点与缺点。第二章第三章介绍了微服务间的通信,并对不同的通信机制作出对比。在本章中,我们将探讨服务发现(service discovery)相关的内容。

4.1、为何使用服务发现

我们假设您正在编写某些代码,这些代码调用了有 REST API 或 Thrift API 的服务。为了发送一个请求,您的代码需要知道服务实例的网络位置(IP 地址与端口)。在运行于物理硬件上的传统应用中,服务实例的网络位置是相对静态的。例如,您的代码可以从偶尔更新的配置文件中读取网络位置。

然而,在现代基于云的微服务应用中,这是一个更难解决的问题,如图 4-1 所示。

服务实例具有动态分配的网络位置。此外,由于自动扩缩、故障与升级,整组服务实例会动态变更。因此,您的客户端代码需要使用更精确的服务发现机制。

图 4-1、需要服务寻找帮助的客户端或 API 网关

有两种主要的服务发现模式:客户端发现(client-side discovery)与服务端发现(server-side discovery)。让我们先来看看客户端发现。

4.2、客户端发现模式

当使用客户端发现模式时,客户端负责确定可用服务实例的网络位置和请求负载均衡。客户端查询服务注册中心(service registry),它是可用服务实例的数据库。之后,客户端利用负载均衡算法选择一个可用的服务实例并发出请求。

图 4-2 展示了该模式的结构

客户端可以承担发现服务任务

服务实例的网络位置在服务注册中心启动时被注册。当实例终止时,它将从服务注册中心中移除。通常使用心跳机制周期性地刷新服务实例的注册信息。

Netflix OSS 提供了一个很好的客户端发现模式示例。Netflix Eureka 是一个服务注册中心,它提供了一组用于管理服务实例注册和查询可用实例的 REST API。Netflix Ribbon 是一个 IPC 客户端,可与 Eureka 一起使用,用于在可用服务实例之间使请求负载均衡。本章稍后将讨论 Eureka。

客户端发现模式存在各种优点与缺点。该模式相对比较简单,除了服务注册中心,没有其他移动部件。此外,由于客户端能发现可用的服务实例,因此可以实现智能的、特定于应用程序的负载均衡决策,比如使用一致性哈希。该模式的一个重要缺点是它将客户端与服务注册中心耦合在一起。您必须为您使用的每种编程语言和框架实现客户端服务发现逻辑。

现在我们已经了解了客户端发现,接下来让我们看看服务端发现。

4.3、服务端发现模式

服务发现的另一种方式是服务端发现模式。图 4-3 展示了该模式的结构:

服务器间也可以处理服务发现

客户端通过负载均衡器向服务发出请求。负载均衡器查询服务注册中心并将每个请求路由到可用的服务实例。与客户端发现一样,服务实例由服务注册中心注册与销毁。

AWS Elastic Load Balancer(ELB)是一个服务端发现路由示例。ELB 通常用于负载均衡来自互联网的外部流量。然而,您还可以使用 ELB 来负载均衡虚拟私有云(VPC)内部的流量。客户端通过 ELB 使用其 DNS 名称来发送请求(HTTP 或 TCP)。ELB 负载均衡一组已注册的 Elastic Compute Cloud(EC2)实例或 EC2 Container Service(ECS)容器之间的流量。这里没有单独可见的服务注册中心。相反,EC2 实例与 ECS 容器由 ELB 本身注册。

HTTP 服务器和负载均衡器(如 NGINX Plus 和 NGINX)也可以作为服务端发现负载均衡器。例如,此博文描述了使用 Consul Template 动态重新配置 NGINX 反向代理。Consul Template 是一个工具,可以从存储在 Consul 服务注册中心中的配置数据中定期重新生成任意配置文件。每当文件被更改时,它都会运行任意的 shell 命令。在列举的博文描述的示例中,Consul Template 会生成一个 nginx.conf 文件,该文件配置了反向代理,然后通过运行一个命令告知 NGINX 重新加载配置。更复杂的实现可以使用其 HTTP API 或 DNS 动态重新配置 NGINX Plus。

某些部署环境(如 KubernetesMarathon)在群集中的每个主机上运行着一个代理。这些代理扮演着服务端发现负载均衡器角色。为了向服务发出请求,客户端通过代理使用主机的 IP 地址和服务的分配端口来路由请求。之后,代理将请求透明地转发到在集群中某处运行的可用服务实例。

服务端发现模式有几个优点与缺点。该模式的一大的优点是其把发现的细节从客户端抽象出来。客户端只需向负载均衡器发出请求。这消除了为服务客户端使用的每种编程语言和框架都实现发现逻辑的必要性。另外,如上所述,一些部署环境免费提供此功能。然而,这种模式存在一些缺点。除非负载均衡器由部署环境提供,否则您需要引入这个高可用系统组件,并进行设置和管理。

4.4、服务注册中心

服务注册中心(service registry)是服务发现的一个关键部分。它是一个包含了服务实例网络位置的数据库。服务注册中心必须是高可用和最新的。虽然客户端可以缓存从服务注册中心获得的网络位置,但该信息最终会过期,客户端将无法发现服务实例。因此,服务注册中心由使用了复制协议(replication protocol)来维护一致性的服务器集群组成。

如之前所述,Netflix Eureka 是一个很好的服务注册中心范例。它提供了一个用于注册和查询服务实例的 REST API。服务实例使用 POST 请求注册其网络位置。它必须每隔 30 秒使用 PUT 请求来刷新其注册信息。通过使用 HTTP DELETE 请求或实例注册超时来移除注册信息。正如您所料,客户端可以使用 HTTP GET 请求来检索已注册的服务实例。

Netflix 通过在每个 Amazon EC2 可用性区域(Availability Zone)中运行一个或多个 Eureka 服务器来实现高可用。每个 Eureka 服务器都运行在具有一个 弹性 IP 地址的 EC2 实例上。DNS TEXT 记录用于存储 Eureka 集群配置,这是一个从可用性区域到 Eureka 服务器的网络位置列表的映射。当 Eureka 服务器启动时,它将会查询 DNS 以检索 Eureka 群集配置,查找其对等体,并为其分配一个未使用的弹性 IP 地址。

Eureka 客户端 — 服务与服务客户端 — 查询 DNS 以发现 Eureka 服务器的网络位置。客户端优先使用相同可用性区域中的 Eureka 服务器,如果没有可用的,则使用另一个可用性区域的 Eureka 服务器。

以下列举了其他服务注册中心:

  • etcd

    一个用于共享配置和服务发现的高可用、分布式和一致的键值存储。使用了 etcd 的两个著名项目分别为 Kubernetes 和 Cloud Foundry

  • Consul

    一个发现与配置服务工具。它提供了一个 API,可用于客户端注册与发现服务。Consul 可对服务进行健康检查,以确定服务的可用性。

  • Apache ZooKeeper

    一个被广泛应用于分布式应用程序的高性能协调服务。Apache ZooKeeper 最初是一个 Hadoop 子项目,但现在已经成为一个独立的顶级项目。

另外,如之前所述,部分系统,如 Kubernetes、Marathon 和 AWS,没有明确的服务注册中心。相反,服务注册中心只是基础设施的一个内置部分。

现在我们已经了解服务注册中心的概念,接下来让我们看看服务实例是如何被注册到服务注册中心。

4.5、服务注册方式

如之前所述,服务实例必须在服务注册中心中注册与注销。有几种不同的方式来处理注册和注销。一是服务实例自我注册,即自注册模式。另一个是使用其他系统组件来管理服务实例的注册,即第三方注册模式。我们先来了解自注册模式。

4.6、自注册模式

当使用自注册模式时,服务实例负责在服务注册中心注册和注销自己。此外,如果有必要,服务实例将通过发送心跳请求来防止其注册信息过期。

图 4-4 展示了该模式的结构。

图 4-4、服务可以自我处理注册

该方式的一个很好的范例就是 Netflix OSS Eureka 客户端。Eureka 客户端负责处理服务实例注册与注销的所有方面。实现了包括服务发现在内的多种模式的 Spring Cloud 项目可以轻松地使用 Eureka 自动注册服务实例。您只需在 Java Configuration 类上应用 @EnableEurekaClient 注解即可。

自注册模式有好有坏。一个好处是它相对简单,不需要任何其他系统组件。然而,主要缺点是它将服务实例与服务注册中心耦合。您必须为服务使用的每种编程语言和框架都实现注册代码。

将服务与服务注册中心分离的替代方法是第三方注册模式。

4.7、第三方注册模式

当使用第三方注册模式时,服务实例不再负责向服务注册中心注册自己。相反,该工作将由被称为服务注册器(service registrar)的另一系统组件负责。服务注册器通过轮询部署环境或订阅事件来跟踪运行实例集的变更情况。当它检测到一个新的可用服务实例时,它会将该实例注册到服务注册中心。此外,服务注册器可以注销终止的服务实例。

图 4-5 展示了该模式的结构:

图 4-5、一个单独的服务注册器可负责注册其他服务

开源的 Registrator 项目是一个很好的服务注册器示例。它可以自动注册和注销作为 Docker 容器部署的服务实例。注册器支持多种服务注册中心,包括 etcd 和 Consul。

另一个服务注册器例子是 NetflixOSS Prana。其主要用于非 JVM 语言编写的服务,它是一个与服务实例并行运行的附加应用。Prana 使用了 Netflix Eureka 来注册和注销服务实例。

服务注册器在部分部署环境中是一个内置组件。Autoscaling Group 创建的 EC2 实例可以自动注册到 ELB。Kubernetes 服务能够自动注册并提供发现。

第三方注册模式同样有好有坏。一个主要的好处是服务与服务注册中心之间解耦。您不需要为开发人员使用的每种编程语言和框架都实现服务注册逻辑。相反,仅需要在专用服务中以集中的方式处理服务实例注册。

该模式的一个缺点是,除非部署环境内置,否则您同样需要引入这样一个高可用的系统组件,并进行设置和管理。

4.8、总结

在微服务应用程序中,运行的服务实例集会动态变更。实例具有动态分配的网络位置。因此,为了让客户端向服务发出请求,它必须使用服务发现机制。

服务发现的一个关键部分是服务注册中心。服务注册中心是一个可用服务实例的数据库。服务注册中心提供了管理 API 和查询 API 的功能。服务实例通过使用管理 API 从服务注册中心注册或者注销。系统组件使用查询 API 来发现可用的服务实例。

有两种主要的服务发现模式:客户端发现与服务端发现。在使用了客户端服务发现的系统中,客户端查询服务注册中心,选择一个可用实例并发出请求。在使用了服务端发现的系统中,客户端通过路由进行请求,路由将查询服务注册中心,并将请求转发到可用实例。

服务实例在服务注册中心中注册与注销有两种主要方式。一个是服务实例向服务注中心自我注册,即自注册模式。另一个是使用其他系统组件代表服务完成注册与注销,即第三方注册模式

在某些部署环境中,您需要使用如 Netflix Eureka ,ectd 或 Apache ZooKeeper 等服务注册中心来设置您自己的服务发现基础设施。在其他部署环境中,服务发现是内置的,例如,KubernetesMarathon,可以处理服务实例的注册与注销。他们还在每一个扮演服务端发现路由角色的集群主机上运行一个代理。

一个 HTTP 反向代理和负载均衡器(如 NGINX)也可以用作服务端发现负载均衡器。服务注册中心可以将路由信息推送给 NGINX,并调用一个正常的配置更新,例如,您可以使用 Consul Template。NGINX Plus 支持额外的动态重新配置机制 — 它可以使用 DNS 从注册中心中提取有关服务实例的信息,并为远程重新配置提供一个 API。

微服务实战:灵活的 NGINX

by Floyd Smith

在微服务环境中,由于自动扩缩、故障和升级,您的后端基础设施可能会不断变化,这些变化包括了服务的创建,部署和扩展。如本章所述,在动态重新分配服务位置的环境中需要服务发现机制。

将 NGINX 应用于微服务的一部分好处是,您可以轻松地配置其自动响应后端基础设施作出的变更。NGINX 配置不仅简单灵活,而且兼容 Amazon Web Services 使用的模板,可以更轻松地管理特定的服务变更与受负载均衡的变更服务组。

NGINX Plus 具有即时重新配置 API,无需重新启动 NGINX Plus 或手动重新加载配置就能感知受负载均衡服务组的变更。在 NGINX Plus Release 8 及更高版本中,您可以将对 API 所做的变更配置为在重新启动和配置重新加载时保持不变。(重新加载不需要重新启动,不要断开连接)NGINX Plus Release 9 及更高版本支持使用 DNS SRV 记录进行服务发现,可与现有服务器发现平台(如 Consul 和 etcd)进行更紧密地集成。

我们在 NGINX 创建了一个用于管理服务发现的模型:

  1. 为几个应用程序每个应用运行单独的 Docker 容器,包括如 etcd 的服务发现应用程序、服务注册工具、一个或多个后端服务器以及用于负载均衡其他容器的 NGINX Plus 本身。
  2. 注册工具监控 Docker 的新容器,并使用服务发现工具注册新服务,此外,还可以删除消失的容器。
  3. 容器及其运行的服务将自动添加到负载均衡上游服务器中或从中删除。

此 Demo 应用程序可用于多个服务发现应用程序:Consul API来自 Consul 的 DNS SRV 记录etcd 以及 ZooKeeper 等。

转:《微服务:从设计到部署》中文版

3、进程间通信

本书主要介绍如何使用微服务架构构建应用程序,这是本书的第三章。第一章介绍了微服务架构模式,将其与单体架构模式进行对比,并讨论了使用微服务的优点与缺点。第二章描述了应用程序客户端通过扮演中间人角色的 API 网关与微服务进行通信。在本章中,我们来了解一下系统中的服务是如何相互通信的。第四章将详细探讨服务发现方面的内容。

3.1、简介

在单体应用程序中,组件可通过语言级方法或者函数相互调用。相比之下,基于微服务的应用程序是一个运行在多台机器上的分布式系统。通常,每个服务实例都是一个进程。

因此,如图 3-1 所示,服务必须使用进程间通信(IPC)机制进行交互。

稍后我们将了解到多种 IPC 技术,但在此之前,我们先来探讨一下涉及到的各种设计问题。

使用进程间通信交互的微服务

3.2、交互方式

当为服务选择一种 IPC 机制时,首先需要考虑服务如何交互。有许多种客户端 — 服务交互方式。可以从两个维度对它们进行分类。第一个维度是交互方式是一对一还是一对多:

  • 一对一 — 每个客户端请求都由一个服务实例处理。
  • 一对多 — 每个请求由多个服务实例处理。

第二个维度是交互是同步的还是异步的:

  • 同步 — 客户端要求服务及时响应,在等待过程中可能会发生阻塞。
  • 异步 — 客户端在等待响应时不会发生阻塞,但响应(如果有)不一定立即返回。

下表展示了各种交互方式。

- 一对一 一对多
同步 请求/响应 -
异步 通知 发布/订阅
异步 请求/异步响应 发布/异步响应

表 3-1、进程间通信方式

一对一交互分为以下列举的类型,包括同步(请求/响应)与异步(通知与请求/异步响应):

  • 请求/响应

    客户端向服务发出请求并等待响应。客户端要求响应及时到达。在基于线程的应用程序中,发出请求的线程可能在等待时发生阻塞。

  • 通知(又称为单向请求)

    客户端向服务发送请求,但不要求响应。

  • 请求/异步响应

    客户端向服务发送请求,服务异步响应。客户端在等待时不发生阻止,适用于假设响应可能不会立即到达的场景。

一对多交互可分为以下列举的类型,它们都是异步的:

  • 发布/订阅

    客户端发布通知消息,由零个或多个感兴趣的服务消费。

  • 发布/异步响应

    客户端发布请求消息,之后等待一定时间来接收消费者的响应。

通常,每个服务都组合着使用这些交互方式。对一些服务而言,单一的 IPC 机制就足够了,但其他服务可能需要组合多个 IPC 机制。

图 3-2 显示了当用户请求打车时,打车应用中的服务可能会发生交互。

使用了多种 IPC 机制的服务交互

服务使用了通知、请求/响应和发布/订阅组合。例如,乘客的智能手机向 Trip Management 微服务发送一条通知以请求一辆车。Trip Management 服务通过使用请求/响应来调用 Passenger Management 服务以验证乘客的帐户是否可用。之后,Trip Management 服务创建路线,并使用发布/订阅通知其他服务,包括用于定位可用司机的 Dispatcher。

现在我们来看一下交互方式,我们先来看看如何定义 API。

3.3、定义 API

服务 API 是服务与客户端之间的契约。无论您选择何种 IPC 机制,使用接口定义语言(interface definition language,IDL)来严格定义服务 API 都是非常有必要的。有论据证明使用 API 优先法定义服务更加合理。在对需要实现的服务的 API 定义进行迭代之后,您可以通过编写接口定义并与客户端开发人员进行审阅来开始开发服务。这样设计可以增加您成功的机率,以构建出符合客户端需求的服务。

正如您将会在后面看到,定义 API 的方式取决于您使用何种 IPC 机制。如果您正在使用消息传递,那么 API 是由消息通道和消息类型组成。如果您使用的是 HTTP,那么 API 是由 URL、请求和响应格式组成。稍后我们将详细地介绍关于 IDL 方面的内容。

3.4、演化 API

服务 API 总是随着时间而变化。在单体应用程序中,更改 API 和更新所有调用者通常是一件直截了当的事。但在基于微服务的应用程序中,即使 API 的所有消费者都是同一应用程序中的其他服务,要想完成这些工作也是非常困难的。通常,您无法强制所有客户端与服务升级的节奏一致。此外,您可能需要逐步部署服务的新版本,以便新旧版本的服务同时运行。因此,制定这些问题的处理策略还是很重要的。

处理 API 变更的方式取决于变更的程度。某些更改是次要或需要向后兼容以前的版本。例如,您可能会向请求或响应添加属性。此时设计客户端与服务遵守鲁棒性原则就显得很有意义了。新版本的服务要能兼容使用旧版本 API 的客户端。该服务为缺少的请求属性提供默认值,并且客户端忽略所有多余的响应属性。使用 IPC 机制和消息格式非常重要,他们可以让您轻松地演化 API。

但有时候,您必须对 API 作出大量不兼容的更改。由于您无法强制客户端立即升级,服务也必须支持较旧版本的 API 一段时间。如果您使用了基于 HTTP 的机制(如 REST),则一种方法是将版本号嵌入到 URL 中。每个服务实例可能同时处理多个版本。或者,您可以部署多个不同的实例,每个实例用于处理特定版本。

3.5、处理局部故障

正如第二章中关于 API 网关所述,在分布式系统中始终存在局部故障的风险。由于客户端进程与服务进程是分开的,服务可能无法及时响应客户端的请求。由于故障或者维护,服务可能需要关闭。也有可能因服务过载,造成响应速度变得极慢。

例如,请回想第二章中的产品详细信息场景。我们假设推荐服务没有响应。低级的客户端实现可能会无限期地阻塞以等待响应。这不仅会导致用户体验糟糕,而且在许多应用程序中,它会消耗线程等宝贵的资源。最终导致运行时系统将线程消耗完,造成无法响应,如图 3-3 所示。

因无响应服务引起的线程阻塞

为了防止出现此类问题,在设计服务时必须考虑处理局部故障。以下是一个由 Netflix 给出的好办法。处理局部故障的策略包括:

  • 网络超时

    在等待响应时,不要无限期地阻塞,始终使用超时方案。使用超时方案确保资源不被无限地消耗。

  • 限制未完成的请求数量

    对客户端请求的特定服务,设置未完成请求的数量上限。如果达到了上限,发出的额外请求可能是毫无意义的,因此这些尝试需要立即失败。

  • 断路器模式

    追踪成功和失败请求的数量。如果错误率超过配置阈值,则断开断路器,以便后续的尝试能立即失败。如果出现大量请求失败,则表明服务不可用,发送请求将是无意义的。发生超时后,客户端应重新尝试,如果成功,则关闭断路器。

  • 提供回退

    请求失败时执行回退逻辑。例如,返回缓存数据或者默认值,如一组空白的推荐数据。

Netflix Hystrix 是一个实现上述和其他模式的开源库。如果您正在使用 JVM,那么您一定要考虑使用 Hystrix。如果您在非 JVM 环境中运行,则应使用相等作用的库。

3.6、IPC 技术

有多种 IPC 技术可供选择。服务可以使用基于同步请求/响应的通信机制,比如基于 HTTP 的 REST 或 Thrift。或者,可以使用异步、基于消息的通信机制,如 AMQP 或 STOMP。

还有各种不同的消息格式。服务可以使用可读的、基于文本的格式,如 JSON 或 XML。或者,可以使用如 Avro 或 Protocol Buffers 等二进制格式(更加高效)。稍后我们将讨论同步 IPC 机制,但在此之前让我们先来讨论一下异步 IPC 机制。

3.7、异步、基于消息的通信

当使用消息传递时,进程通过异步交换消息进行通信。客户端通过发送消息向服务发出请求。如果服务需要回复,则通过向客户端发送一条单独的消息来实现。由于通信是异步的,因此客户端不会阻塞等待回复。相反,客户端被假定不会立即收到回复。

一条消息由头部(如发件人之类的元数据)和消息体组成。消息通过通道进行交换。任何数量的生产者都可以向通道发送消息。类似地,任何数量的消费者都可以从通道接收消息。有两种通道类型,分别是点对点(point‑to‑point)与发布订阅(publish‑subscribe):

  • 点对点通道发送一条消息给一个切确的、正在从通道读取消息的消费者。服务使用点对点通道,就是上述的一对一交互方式。
  • 发布订阅通道将每条消息传递给所有已订阅的消费者。服务使用发布订阅通道,就是上述的一对多交互方式。

图 3-4 展示了打车应用程序如何使用发布订阅通道。

使用了发布/订阅通道的打车应用

Trip Management 服务通过向发布订阅通道写入 Trip Created 消息来通知已订阅的服务,如 Dispatcher。Dispatcher 找到可用的司机并通过向发布订阅通道写入 Driver Proposed 消息来通知其他服务。

有许多消息系统可供选择,您应该选择一个支持多种编程语言的。

一些消息系统支持标准协议,如 AMQP 和 STOMP。其他消息系统有专有的文档化协议。

有大量的开源消息系统可供选择,包括 RabbitMQApache KafkaApache ActiveMQNSQ。从高层而言,他们都支持某种形式的消息和通道。他们都力求做到可靠、高性能和可扩展。然而,每个代理的消息传递模型细节上都存在着很大差异。

使用消息传递有很多优点:

  • 将客户端与服务分离

    客户端通过向相应的通道发送一条消息来简单地发出一个请求。服务实例对客户端而言是透明的。客户端不需要使用发现机制来确定服务实例的位置。

  • 消息缓冲

    使用如 HTTP 的同步请求/响应协议,客户端和服务在交换期间必须可用。相比之下,消息代理会将消息排队写入通道,直到消费者处理它们。这意味着,例如,即使订单执行系统出现缓慢或不可用的情况,在线商店还是可以接受客户的订单。订单消息只需要简单地排队。

  • 灵活的客户端 — 服务交互

    消息传递支持前面提到的所有交互方式。

  • 毫无隐瞒的进程间通信

    基于 RPC 的机制试图使调用远程服务看起来与调用本地服务相同。然而,由于物理因素和局部故障的可能性,他们实际上是完全不同的。消息传递使这些差异变得非常明显,所以开发人员不会被这些虚假的安全感所欺骗。

然而,消息传递也存在一些缺点:

  • 额外的复杂操作

    消息传递系统是一个需要安装、配置和操作的系统组件。消息代理程序必须高度可用,否则系统的可靠性将受到影响。

  • 实现基于请求/响应式交互的复杂性

    请求/响应式交互需要做些工作来实现。每个请求消息必须包含应答通道标识符和相关标识符。该服务将包含相关 ID 的响应消息写入应答信道。客户端使用相关 ID 将响应与请求相匹配。通常使用直接支持请求/响应的 IPC 机制更加容易。

现在我们已经了解了使用基于消息的 IPC,让我们来看看请求/响应的 IPC。

3.8、同步的请求/响应 IPC

当使用基于同步、基于请求/响应的 IPC 机制时,客户端向服务器发送请求。该服务处理该请求并返回响应。

在许多客户端中,请求的线程在等待响应时被阻塞。其他客户端可能会使用异步、事件驱动的客户端代码,这些代码可能是由 FuturesRx Observables 封装的。然而,与使用消息传递不同,客户端假定响应能及时到达。

有许多协议可供选择。有两种流行协议分别是 REST 和 Thrift。我们先来看一下 REST。

3.8.1、REST

如今,开发 RESTful 风格的 API 是很流行的。REST 是一种使用了 HTTP (几乎总是)的 IPC 机制。

资源是 REST 中的一个关键概念,它通常表示业务对象,如客户、产品或这些业务对象的集合。REST 使用 HTTP 动词(谓词)来操纵资源,这些资源通过 URL 引用。例如,GET 请求返回一个资源的表述形式,可能是 XML 文档或 JSON 对象形式。POST 请求创建一个新资源,PUT 请求更新一个资源。

引用 REST 创建者 Roy Fielding:

“REST 提供了一套架构约束,当应用作为整体时,其强调组件交互的可扩展性、接口的通用性、组件的独立部署以及中间组件,以减少交互延迟、实施安全性和封装传统系统。” — Roy Fielding,《架构风格与基于网络的软件架构设计》

图 3-5 展示了打车应用程序可能使用 REST 的方式之一。

使用了 RESTful 交互的打车应用

乘客的智能手机通过向 Trip Management 服务的 /trips 资源发出一个 POST 请求来请求旅程。该服务通过向 Passenger Management 服务发送一个获取乘客信息的 GET 请求来处理该请求。在验证乘客有权限创建旅程后,Trip Management 服务将创建旅程,并向智能手机返回 201 响应。

许多开发人员声称其基于 HTTP 的 API 就是 RESTful。然而,正如 Fielding 在这篇博文中所描述的那样,并不是都是这样。

Leonard Richardson 定义了一个非常有用的 REST 成熟度模型,包括以下层次:

  • 级别 0

    级别 0 的 API 的客户端通过向其唯一的 URL 端点发送 HTTP POST 请求来调用该服务。每个请求被指定要执行的操作、操作的目标(如业务对象)以及参数。

  • 级别 1

    级别 1 的 API 支持资源概念。要对资源执行操作,客户端会创建一个 POST 请求,指定要执行的操作和参数。

  • 级别 2

    级别 2 的 API 使用 HTTP 动词(谓词)执行操作:使用 GET 检索、使用 POST 创建和使用 PUT 进行更新。请求查询参数和请求体(如果有)指定操作的参数。这使服务能够利用得到 Web 的基础特性,如缓存 GET 请求。

  • 级别 3

    级别 3 的 API 基于非常规命名原则设计,HATEOAS(Hypermedia as the engine of application state,超媒体即应用程序状态引擎)。基本思想是 GET 请求返回的资源的表述,包含用于执行该资源上允许的操作的链接。例如,发送 GET 请求检索订单,返回的订单响应中包含取消订单链接,客户端可以用该链接来取消订单。HATEOAS 的一个好处是不再需要将 URL 硬编码在客户端代码中。另一个好处是,由于资源的表示包含可允许操作的链接,所以客户端不必猜测可以对当前状态的资源执行什么操作。

使用基于 HTTP 的协议有很多好处:

  • HTTP 简单易懂。
  • 您可以使用浏览器扩展(如 Postman)来测试 HTTP API,或者使用 curl 命令行测试 HTTP API(假设使用了 JSON 或其他一些文本格式)。
  • 它直接支持请求/响应式通信。
  • HTTP 属于防火墙友好。
  • 它不需要中间代理,简化了系统架构。

使用 HTTP 也存在一些缺点:

  • HTTP 仅直接支持请求/响应的交互方式。您可以使用 HTTP 进行通知,但服务器必须始终发送 HTTP 响应。
  • 因为客户端和服务直接通信(没有一个中间者来缓冲消息),所以它们必须在交换期间都运行着。
  • 客户端必须知道每个服务实例的位置(即 URL)。如第二章关于 API 网关所述,这是现代应用程序中的一个复杂问题。客户端必须使用服务发现机制来定位服务实例。

开发人员社区最近重新发现了 RESTful API 接口定义语言的价值。有几个可以选择,包括 RAMLSwagger。一些 IDL(如 Swagger)允许您定义请求和响应消息的格式。其他如 RAML,需要您使用一个单独的规范,如 JSON 模式。除了用于描述 API 之外,IDL 通常还具有可从接口定义生成客户端 stub 和服务器 skeleton 的工具。

3.8.2、Thrift

Apache Thrift 是 REST 的一个有趣的替代方案。它是一个用于编写跨语言 RPC 客户端和服务器框架。Thrift 提供了一个 C 风格的 IDL 来定义您的 API。您可以使用 Thrift 编译器生成客户端 stub 和服务器端 skeleton。编译器可以生成各种语言的代码,包括 C++、Java、Python、PHP、Ruby、Erlang 和 Node.js。

Thrift 接口由一个或多个服务组成。服务定义类似于一个 Java 接口。它是强类型方法的集合。

Thrift 方法可以返回一个(可能为 void)值,或者如果它们被定义为单向,则不会返回值。返回值方法实现了请求/响应的交互方式,客户端等待响应,并可能会抛出异常。单向方式对应通知交互方式,服务器不发送响应。

Thrift 支持多种消息格式:JSON,二进制和压缩二进制。二进制比 JSON 更有效率,因为其解码速度更快。而且,顾名思义,压缩二进制是一种节省空间的格式。当然,JSON 是可读的和浏览器友好的。Thrift 还为您提供了包括原始 TCP 和 HTTP 在内的传输协议选择。原始 TCP 可能比 HTTP 更有效率。然而,HTTP 是防火墙友好的、浏览器友好的和可读的。

3.9、消息格式

我们已经了解了 HTTP 和 Thrift,现在让我们来看看消息格式的问题。如果您使用的是消息系统或 REST,则可以选择自己的消息格式。其他 IPC 机制如 Thrift 可能只支持少量的消息格式,甚至只支持一种。在任一种情况下,使用跨语言的消息格式都很重要了。即使您现在是以单一语言编写您的微服务,您将来也可能会使用到其他语言。

有两种主要的消息格式:文本和二进制。基于文本格式的例子有 JSON 和 XML。这些格式的优点在于,它们不仅是人类可读的,而且是自描述的。在 JSON 中,对象的属性由一组键值对表示。类似地,在 XML 中,属性由命名元素和值表示。这使得消息消费者能够挑选其感兴趣的值并忽略其余的值。因此,稍微修改消息格式就可以轻松地向后兼容。

XML 文档的结构由 XML 模式(schema)定义。随着时间的推移,开发人员社区已经意识到 JSON 也需要一个类似的机制。一个选择是使用 JSON Schema,无论独立或作为 IDL 的一部分,如 Swagger。

使用基于文本的消息格式的缺点是消息往往是冗长的,特别是 XML。因为消息是自描述的,每个消息除了它们的值之外还包含属性的名称。另一个缺点是解析文本的开销。因此,您可能需要考虑使用二进制格式。

有几种二进制格式可供选择。如果您使用的是 Thrift RPC,您可以使用 Thrift 的二进制格式。如果你可以选择消息格式,比较流行的有 Protocol BuffersApache Avro。这两种格式都提供了一种类型化的 IDL 用于定义消息结构。然而,一个区别是 Protocol Buffers 使用标记字段,而 Avro 消费者需要知道模式才能解释消息。因此,Protocol Buffers 的 API 演化比 Avro 更容易使用。这里有篇博文对 Thrift、Protocol Buffers 和 Avro 作出了极好的比较。

3.10、总结

微服务必须使用进程间通信机制进行通信。在设计服务如何进行通信时,您需要考虑各种问题:服务如何交互、如何为每个服务指定 API、如何演变 API 以及如何处理局部故障。微服务可以使用两种 IPC 机制:异步消息传递和同步请求/响应。为了进行通信,一个服务必须能够找到另一个服务。在第四章中,我们将介绍微服务架构中服务发现问题。

微服务实战:NGINX 与应用程序架构

by Floyd Smith

NGINX 使您能够实现各种伸缩和镜像操作,使您的应用程序更加灵敏和高度可用。您为伸缩和镜像所作的选择会影响到您如何进行进程间通信,这是本章的主题。

我们在 NGINX 方面建议您在实现基于微服务的应用程序时考虑使用四层架构。Forrester 在这方面有详细的报告,您可以从 NGINX 上免费下载。这些层代表客户端(包括台式机或笔记本电脑、移动、可穿戴或 IoT 客户端)、交付、聚合(包括数据存储)和服务,其中包括应用功能和特定服务,而不是共享数据存储。

四层架构比以前的三层架构更加灵活,具有可扩展、响应灵敏、移动友好,并且内在支持基于微服务的应用程序开发和交付等优点。像 Netflix 和 Uber 这样的行业引领者能够通过使用这种架构来实现用户所需的性能水平。

NGINX 本质上非常适合四层架构,从客户端层的媒体流,到交付层的负载均衡与缓存、聚合层的高性能和安全的基于 API 的通信的工具,以及服务层中支持灵活管理的短暂服务实例。

同样的灵活性使得 NGINX 可以实现强大的伸缩和镜像模式,以处理流量变化,防止安全攻击,此外还提供可用的故障配置切换,从而实现高可用。

在更为复杂的架构中,包括服务实例实例化和需求不断的服务发现,解耦的进程间通信往往更受青睐。异步和一对多通信方式可能比高耦合的通信方式更加灵活,它们最终提供更高的性能和可靠性。

转:《微服务:从设计到部署》中文版