在这篇文章里, 我计划涵盖微服务架构(MSA)的核心架构概念,以及你如何在实践中使用这些架构理论。
如今,微服务“microservice”已经成为软件架构领域最流行的热词之一。市面上也有很多与微服务的基础知识以及优点相关的学习资料,但是关于如何在真实的企业场景中应用微服务的资料还是不多。
在这篇文章里, 我计划涵盖微服务架构(MSA)的核心架构概念,以及你如何在实践中使用这些架构理论。
单体架构
企业软件设计需要满足多种多样的业务需求。因此,一个特定的应用软件会包括有几百个功能项,而所有这些功能项都打包进了一个单体的应用中。典型的例子有,ERP,
CRM等其他各种各样的软件。对于这种野兽级别的软件应用,部署,排错, 扩展和升级工作都是一个个噩梦。
面向服务架构(SOA) 设计是针对上述问题的一个解决方案, SOA引入了服务的概念,用来将软件中相似的功能进行分组聚合在一起。因此,有了SOA,软件就被设计为一组粗粒度服务的组合。
但是SOA并没有解决所有的问题。在SOA里,一个服务的范围是非常广的。由此带来的弊端是服务本身庞大而复杂,数十个功能点,以及复杂的消息格式和标准(例如所有的WS*规范)
图1 单体架构
在大多数情况下,SOA里面的服是互相独立的,而且是与其他所有的服务部署在同一个运行时上面。(可以想象一下多个web应用部署到同一个tomcat实例当中)。
而且与单体软件类似,这些服务会随时间越长越大,因为累加的功能越来越多。最后,这些应用本身已变成了单体软件,与传统的单体软件(比如ERP)也没啥两样。图1描述了一个零售业的软件,它包含有多个服务,所有这些服务都部署在同一个运行时上。
这是一个很好的单体架构的例子。这里我列出这种基于单体架构软件的一些特点:
单独应用是作为一个整体单元来设计,开发,部署的
单体应用非常复杂,导致的结果就是维护,升级和增加新功能都非常困难
在单体架构下,非常难实践敏捷的开发和部署方法
如果要更新它的某个部署,则需要重新部署整个应用
扩展:必须作为单个软件来扩展,当有资源需求冲突时扩展就变得非常困难(比如 一个服务需要更多的CPU但是其他的服务要更多内存)
可靠性:一个不稳定的服务可能会导致整个应用不可用
阻碍创新: 由于所有的功能都基于同一套技术框架来够构建,想加入新的技术或者框架就非常困难
微服务架构
微服务架构的基础是开发一个应用由一组小但是独立的服务来组成,这些服务运行在自己的进程中,可以被独立开发,独立部署。
在大多数的微服务架构的定义里,这被解释为将一个单体应用里面的服务拆分为一组独立的服务。但是,我觉得,这不是微服务的全部。
核心的观点是通过查看单体服务提供的功能项目,来识别出必须的业务能力。然后这些业务能力可以作为一个完全独立的,细粒度的,自包含的服务来实现(微服务)。他们的实现可以是基于不同的技术栈,而且每个服务描述的是一个明确的特定的有限的业务范围。
因此,我们上文中提到的在线零售系统可以用图2里面的微服务架构来实现。 在微服务架构下,零售软件应用通过一组微服务来实现。所以你看到图2中,我们增加在原来单体应用里面的一组服务的基础上新增加了一个服务。所以很明显,使用微服务架构不是仅仅将单体应用里面的服务拆分那么简单。
图2 微服务架构
接下来,让我们更加深入了解微服务的核心架构原则。更重要的,让我们关注如何将他们应用到实践中。
设计微服务: 大小,范围和能力
你可能在使用微服务架构从头构建一个软件,也可能是要把已有的应用服务转换为微服务。无论哪种,非常重要的一点都是你必须合理的决定微服务的大小,范围和能力。这极有可能是在实践微服务架构初期碰到的最难的事情。
接下来我们来讨论与微服务的大小,范围,能力相关的一些实际的考虑点和错误观点。
代码行数和团队大小事很糟糕的度量指标:基于代码行数或者团队大小来决定微服务的大小已经有多个讨论了。(比如
两个pizza的团队 )。 但是,这些都是非常不切实际而且非常糟糕的度量值,因为我们用更少的代码或者两个pizza的团队开发出来的服务仍然可能完全违背微服务的架构原则。
微micro这个词会导致误解:大多数开发人员倾向于认为们应该将服务做的越小越好。但是这完全是错误的解释。
在SOA的上下文里面,服务通常被实现为包括很多功能的和运营支持的单体结构。所以如果仅仅是将SOA那种服务重新打上微服务的标签不会给你带来微服务架构的如何好处。
那么,我们该如何合地设计微服务架构下的服务呢?
微服务设计原则
单一责任原则(Single Responsibility Principle, SRP): 对于一个微服务而言具有有限的和关注的业务范围可以帮助我们满足服务开发和交付的敏捷性。
在微服务的设计阶段, 我们应该找到他们的边界,并将它们与业务能力相关联。(在 领域驱动设计 里这叫有边界的上下文)
必须保证微服务设计能支持服务的敏捷/独立地开发和部署
我们应该关注微服务的范围,而不是一味的把服务做小。一个服务的(正确的)大小应该等于满足某个特定业务能力所需要的大小。
与SOA里面的服务不同,一个给定的微服务应该有相当少的运营和功能点,以及简单的消息格式
通常一个好的实践是先从一个比较大的服务边界开始,然后随着时间推移基于业务需求来重构成更小的。
在我们的零售系统的案例中,你可以发现我们将原来单体应用的功能分割到了4个不同的微服务中, 'invenory",
"accountng", "shipping", "store"。
它们描述的是一个有限但关注的业务范围,而且服务之间互相完全解耦,保证了开发和部署的敏捷性。
微服务里的消息
在单体应用里面,不同组件的业务功能通过函数调用或者语言级别的方法调用来实现。在SOA中,这转变为更加松耦合的web
service级别的消息,主要是基于HTTP,JMS等不同协议的SOAP。Webservice 包含的几十种操作以及复杂的消息机制是阻碍web
services流行的一个重要因素。对于微服务架构而言,必须要有一个简单且轻量级的消息机制。
同步消息- REST, Thrift
对于微服务领域的同步消息机制而言(可获得需要服务给一个及时的响应否则一直等待), REST 是公认的选择。它提供了一种简单的消息风格,具体实现是HTTP的请求-响应,基于资源的API风格。因此大多数的微服务实现是使用http和基于资源API风格的。(每一个功能都是通过一个资源以及在它之上执行的操作来实现)
图3, 通过REST接口来暴露微服务
Thrift 是REST/HTTP 同步消息之外的另一个选项。使用它你可以给你的微服务定义一个接口定义。
异步消息- AMQP, STOMP, MQTT
在某些微服务场景下,需要使用异步消息技术(可获得不需要立即得到回复,甚至完全不要回复)。在这种场景下,
异步消息 AMPQ , STOMP , MQTT 等被广泛使用
消息格式 - jSON, XML, Thrift,ProtoBuf,Avro
为微服务来决定最适合的消息格式是另一个关键要素。传统的单体的软件使用复杂的二进制的格式,SOA/Web
services的应用使用基于复杂消息格式(SOAP)和schema(xsd)的文本消息。在大多数的微服务里面,它们使用简单的基于文本的消息格式,例如基于HTTP
资源API风格之上的JSON/XML等。在某些情况下它们需要二进制的格式时(文本消息在某些场景下显得啰嗦),可以使用二进制的协议例如二进制的Thrift,
Protobuf , Arvo 。
服务协议 - 定义服务的接口 - Swagger,RAML, Thrift IDL
当你已经有一个业务能力以服务的形式实现之后, 你需要定义和发布服务协议。 在传统单体应用中, 我们很少找到这个功能来定义某个应用的业务能力。
在SOA/Web services的世界里面, WSDL用来描述服务协议,但是,我们都知道,WSDL并不是描述微服务的理想方案,因为它太复杂了而且与SOAP高度耦合。
既然我们是基于REST架构风格来构建的微服务,我们可以使用同样的REST API定义的技术来定义服务协议。因此,微服务使用标准的REST
API定义语言来定义服务协议, 比如 Swagger 和 RAML 。
对于其他一些不是基于HTTP/REST的微服务实现(例如Thrift),我们可以协议级别的接口定义语言。比如
Thrift IDL
集成微服务(跨服务/进程通讯)
在微服务架构里,一个软件应用是基于一组独立的服务构建的。 因此为了实现某个应用场景,需要不同微服务、进程之间的通讯机制。这也是微服务之间跨服务、进程通讯这么重要的原因
在SOA的实现中,服务之间的跨服务通讯是通过企业服务总线ESB来实现的,并且大部分的业务逻辑在中间层中(消息路由,传送,编排)。但是微服务架构推崇去掉中央消息总线将业务逻辑放到服务和客户端去(也称之为smart
endpoints)
因为微服务使用HTTP, JSON等标准协议,当做跨微服务之间的通讯时,需要跟一个不同的协议做集成的需求很少。在微服务里面的另一个可选方案是使用一个轻量级的消息总线或者
网关 ,网关上带最少的路由功能,不带任何业务逻辑实现而仅仅是一个哑管道。基于这些方式,在微服务架构里面就有了如下几种通讯模式。
点对点风格- 直接调用服务
在点对点风格里,整个的消息路由逻辑在端点上,服务之间直接通讯。每个服务暴露一组REST API,外部的服务或者客户端通过REST
API 来调用。
图4, 服务间通讯: 点对点连接
明显的,这种模型对于简单的微服务架构应用有效。但是随着服务数量的增加,它会慢慢变得复杂。这也是为什么在SOA里面要用ESB来避免杂乱的点对点的连接。让我们试着总结一下点对点模式的弊端。
非功能需求 比如用户认证, 流控,监控等必须在每个微服务里实现
由于通用功能的重复,每个微服务的实现变得复杂
在服务和客户端之间没有通讯控制(甚至对于监控,跟踪,过滤等都没有)
对于大的微服务实现来说直接的通讯形式通常被认为是 反模式
因此, 在复杂的微服务应用场景下,不要使用点对点直连或者中央的ESB,我们可以使用一个轻量级的中央消息总线给所有微服务提供一个抽象层,而且可以用来实现各种非功能的能力。这种风格也叫做API
Gateway风格。
API Gateway风格
API Gateway风格的核心理念是使用一个轻量级的消息网关作为所有客户端、消费者的主入口并且在网关层面上实现通用的非功能性需求。
通常,一个API网关允许你通过REST来消费一个受管理的API。 因此我们可以使用它来暴露微服务所实现的业务功能,
以受管理的API的形式。 实际上, 这是微服务架构与API管理的组合,给你带来两种技术的优点。
图5 所有服务通过一个API网关来暴露
在我们零售的例子中,如图5所描述的, 所有的服务通过API 网关来暴露,这是所有客户端访问的唯一入口。
如果一个微服务要访问另一个微服务,也要通过这个网关。
API网关带来以下优点:
在网关层面对存在的微服务提供必要的抽象。例如,网关可以选择不提供一个适用所有的ApI, 而选择对不同的用户暴露不同的API。
在网关层面的轻量级消息路由和转换
一个中心的地方提供非功能性的能力, 比如安全,监控,限流等
通过适用API网关模式,微服务可以变得更加轻量,因为非功能性需求都在网关上实现了。
API网关风格可能是大多数微服务 实现 里最被普遍采用的形式。
消息代理风格
微服务加可以与异步消息场景集成,比如单向的请求和使用队列或者主题的发布订阅消息机制。某个微服务可以使一个消息的制造者,它能将消息异步的发送到一个队列或者主题里面。消费型的微服务可以消费队列或者主题里来的消息。这种方式将消息的制造者和消费者解耦,而且中间的消息代理会缓存消息直到消费者处理它们。
制造消息的微服务对消费消息的微服务完全未知。
图6 异步消息机制, 基于PUB-SUB集成
生产者与消费者直接的通讯由消息代理来完成,基于的是异步消息标准, 比如AMQP, MQTT,等等。
去中心化的数据管理
在单体架构中,应用将数据存在一个集中化的数据库中来实现各种的功能和业务能力。
图7 单体应用使用一个集中化的数据库来实现所有特性
在微服务架构里,功能是跨多个微服务来提供的,这样一来,如果我们继续使用集中化的数据库,那么微服务之间就不是互相独立了(例如数据库的某个schema为了某个服务要更改,那么极有可能会破坏其他的服务)。
因此每个微服务必须有自己的数据库。
图8 微服务有自己的私有数据库,它们无法直接访问其他微服务的数据库
要实现微服务架构下的去中心化数据库管理有如下几个核心关注点:
每个微服务都有一个私有的数据库, 存放的数据用来实现它所要提供的业务功能
一个特定的微服务自己能访问自己的私有专用的数据库,而不能直接访问其他微服务的数据库
在某些业务场景下,为了事务性要求你可能需要一次更新多个数据库。在这种情况下,其他微服务的数据库更新应该通过它的API调用来完成(不允许直接访问它的数据库)
去中心化的数据管理让你可以得到完全解耦的数据库, 并且也有了自由选择各种数据库技术的能力(比如SQL 或者NOSQL,每个服务都可以有不同的数据库管理系统)。
但是, 对于复杂的涉及多个微服务的事务型应用场景下,事务操作应该使用各个微服务提供的API实现,具体逻辑应该在客户端或者中间层(网关)中实现。
去中心化治理
微服务架构适用微服务治理。
总的来说,“治理”的意思是建立和实施“如何让人员和解决方案为了组织目标而一起工作”。在SOA的上下文中,
SOA治理 指导可重用服务的开发,指导服务该如何设计和开发,以及服务如何随时间演进。它在服务的提供者与服务消费者之间建立协议,告诉消费者它们可以期望得到什么;告诉提供者它们有义务提供什么。在SOA治理中,有两种普通采用的治理模型:
设计时治理 - 定义和控制服务的生成,设计以及服务策略的实现
运行时治理 - 在运行时实施服务策略的能力
那么,微服务上下文中的治理到底是什么意思?在微服务架构下,服务是以完全独立解耦的方式构建的,用的技术栈可以完全不同。因此,定义一个通用的服务设计和开发标准没有太大必要。
我们可以将微服务场景下的去中心化的治理能力总结如下:
在微服务架构下, 没有必要拥有一个中心化的设计时治理
微服务可以自己决策自己的设计实现
微服务架构可以共享通用/可重用的服务
某些运行时治理, 比如SLA, 限流,监控,通用的安全需求以及服务发现可以在API网关级别实现
服务注册与服务发现
在微服务架构下, 你需要管理的微服务数量相当之高。而且,由于微服务本身的快速敏捷的开发部署特性,它们的运行地点会动态变化。因此,你需要能够在运行时找到一个微服务运行的位置。这个问题的解决方案是使用一个服务注册表。
服务注册表
服务注册表保持微服务实例以及它们的位置。微服务实例在服务启动时在注册表里面注册,在关闭时注销。消费者可以通过注册表找到可用的微服务以及它们的位置。
服务发现
要找到可用的微服务以及它们的位置,我们需要有一个服务发现机制。 有2种服务发现机制,客户端发现和服务端发现。
客户端发现 - 这种方式下,客户端或者API-GW通过查询服务注册表来得到服务实例的位置
图9 客户端发现
这里,客户端/API-GW通过调用服务注册组件来实现服务发现逻辑。
服务端发现 - 这种方式下,客户端/API-GW向运行在某个公知位置的组件发送请求(例如负载均衡器)。
这个组件调用服务注册表然后得到这个微服务的绝对位置。
图10 服务端发现
微服务部署方案如kubernetes( http://kubernetes.io/v1.1/docs
... s.htm l)提供的就是服务端解决方案
部署
提到微服务架构时,微服务的部署扮演着一个核心角色而且有如下核心要求:
有能力在不依赖其他服务的情况下部署/撤销
能在每个微服务的级别进行扩展(某个服务可能比其他服务有更多的流量)
快速构建和部署微服务
一个微服务的失效不能影响其他服务
Docker (一个开源引擎可以让开发者和系统管理员部署自包含的应用容器到linux环境中)提供了一个满足上述需求的部署方案。里面涉及的核心步骤有:
将微服务打包为docker 镜像
将每个服务实例部署为容器
通过改变容器的数量来实现服务的扩展
使用docker容器时服务的构建,部署和启动都相当快(通常比虚拟机快的多)
kubernetes 扩展了docker的能力:可以像管理一个系统那样管理一个linux容器的集群,跨主机运行和管理docker容器,
提供容器的多地部署,服务发现和复制控制。正如你看到的,这些特性中的大多数在微服务场景下也是特别核心的。因此使用kubernetes(基于docker)来做微服务部署赢成为一种相当强大的方法,特别对于大型的微服务部署而言。
图11 以容器方式构建和部署微服务
在图11中,展示了容器应用中的微服务的部署概览。每个微服务实例部署为一个容器,每个主机上跑了两个容器。
在任意一台主机上你都可以指定跑的容器的数量。
安全
微服务安全是在实际场景中应用微服务的一个普遍要求。在讲微服务安全之前,我们先看看在单体应用下我们通常是如何实现安全的。
在一个单体应用中,安全主要关心‘调用者是谁’, ‘调用者能干什么’, 以及‘我们如何传播这个信息’
这通常在一个公用的安全组件上实现,它部署在请求处理链的首部,通过一个底层的用户数据库来填充必要的信息。
这样, 我们可以将这个模型应用到微服务架构中吗? 可以,但是要求在每个微服务级别实现一个安全组件,查询中心共享的用户库来获得必要的信息。这是一个非常繁琐的方式来解决微服务场景下的安全问题。我们可以利用广泛使用的API-安全标准来做,例如OAuth2,
OpeniD Connect, 这是解决微服务安全问题的更好方式。在我们深入之前,我先总结一下每种标准的目的以及我们该如何使用。
OAuth2 - 是一个访问授权协议。客户端相授权服务器认证得到一个‘访问令牌’,访问令牌里面不包含关于用户或者客户端的任何信息。它仅仅包含一个对客户信息的应用,而且仅仅能被授权服务器查询。因此,也常被称为'引用型令牌,即使在公网、互联网上使用也是安全的。
OpenID Connect如OAuth2行为类似,但是除了访问令牌之外,授权访问也会发出一个ID令牌,其中包含有用户的信息。这常通过JWT(JSON
WEB TOKEN)实现,由授权服务器签名。这样保证了授权服务器与客户端的互相信任。JWT令牌因此也称为“值型令牌”,因为它里面包含有用户信息,通过不适于在公共网络使用。
现在,我们看看如何在零售的案例中使用这些安全标准来实现微服务的安全:
图12 微服务安全,基于OAuth2和OpenID
Connect
如图12, 在实现微服务安全时有如下关雎步骤:
将认证交给OAuth2和OpenID Connect服务器(授权服务器),如此一来用户只要有权使用这些数据微服务就可以提供访问
使用API-GW方式,对于所有的客户请求有单一入口
客户连接到授权服务器得到访问令牌(引用型令牌),然后将令牌和请求一起发给API-GW
网关做令牌翻译 - API-GW提出访问令牌,发送到授权服务器得到JWT(值型令牌)
网关将JWT和请求一起发给微服务层
JWT含有必要的信息来做用户会话保存等。如果每个服务都可以理解JSON web token,那么你就拥有了可以分发身份信息到整个系统中的机制
在每个微服务层,我们可以有一个组件来处理JWT,这个实现通常非常简单。
事务
如何在微服务中支持事务? 实际上, 跨多个微服务来实现分布式事务是一个相当复杂的工作。微服务架构本身鼓励的是服务之间非事务的协调。
这个意思是基于每个服务完全自包含且单一责任的原则。需要跨多个服务之间的分布式事务通常是微服务设计上的缺陷,通常应该通过重构微服务的范围来解决。尽管如此,如果必须要有这种跨服务的分布式事务,
这种场景可以通过在每个微服务层引入‘修正操作’来实现。 核心思想是,某个特定的微服务是根据单一责任设计的,如果它无法完成某个特定操作时,我们可以认为整个微服务都失败了。
这时上游其他的微服务就要起到用它们各自的修正操作来回滚。
为’失效‘设计
微服务架构引入了一组离散的服务集合,与单体架构相比,这增加了在每一个微服务级别失败的可能性。一个微服务的失效可能由于网络问题,底层资源不可用等等因素。单个微服务的不可用或者没响应不应该让整个应用失败。这样,微服务应该是容错的,可能的话有能力自动恢复,客户端也要能优雅处理。
另外,因为服务可能随时失败,快速发现失败(实时监控),可能的话自动恢复服务也十分重要。
在微服务场景下,有几种处理错误的通用模式:
链路断开器
当你对一个微服务做外部调用时,你可以给每一个调用配置一个错误监控组件。当失败达到某个阈值时,组件会停止对那个服务的调用(断开链路)。
在特定数目的请求是open状态之后(可以自己定义),将链路闭合回去。
这个模式对避免无谓的资源消耗特别有用, 请求因为超时被推迟,也让我们有机会监控系统状态(基于活跃的open的链路状态)
隔离墙
由于应用由相当数量的微服务组成,应用的某一部分的微服务失效不应影响应用的其他部分。隔离墙模式就是将应用的不同部分隔离,这样一来,应用的某个部分的某个服务的失败不会影响其他服务。
超时
超时模型是这样一种机制,它允许在当你觉得服务的响应不会回来时停止等待。这样你就可以配置等待的时间间隔。
那么,我们应该在服务中哪里使用及如何使用这些模式呢? 在大多数时候,大多数的模式适用于网关层。也就是说当服务不可用或者没响应时,我们可以在网关级别决定使用链路断开或者超时的模式来给服务发请求。同样的,
在网关级别实现隔离墙的模式也十分重要,因为它是所有请求的唯一入口,所以某个服务的失败不会影响其他服务的调用。
另外,网关也可以用作我们监控每个服务状态的中心点,因为每个服务都是通过网关来调用的。
微服务,企业级集成, API管理,以及其他
我们已经讨论了微服务架构的各种特性,以及如何在现代的企业it里实现它们。尽管如此,我们必须知道微服务不是包治百病的灵丹妙药。盲目的吸收流行概念并不会真正解决企业it的实际问题。你通篇读下来会觉得,微服务确实有很多优点我们应该利用。但是,我们也必须意识到使用微服务来解决所有的IT问题是不切实际的。
例如,微服务架构推崇去除作为中央总线的ESB,但是在实际的IT场景下,我们已经有相当数量的线上应用和服务并不是基于微服务的。因此,为了集成它们,我们必须使用某种集成总线。所以,
理想情况是,一个融合了微服务和其他企业架构理念(例如集成)的方法显然更切合实际。我将在另一篇文章中单独加以阐述。 |