微服务业务与技术特征

说起微服务,就不不提Martin Fowler 和 James Lewis 合写的文章Microservices: a definition of this new architectural termhttps://martinfowler.com/articles/microservices.html这篇文章虽然不是最早提出“微服务”这个概念的,但却是真正丰富的、广为人知的和可操作的微服务指南,相信我们大多数程序员,也是从这篇文章里面,第一次了解到微服务的。文章里列举了微服务的九个核心的业务与技术特征,我们可以以此作为微服务设计的指导性原则,也可以检验你所在企业的微服务是否满足这些特性

1.围绕业务能力构建

        围绕业务能力、而不是职能构建团队。一个产品的构建,需要负责各种职能(产品经理/UI/前端/后端/测试/运维)的人员沟通协作才能完成,如果按职能将他们被拆分到不同的团队,那就必然会产生大量的跨团队沟通协作,而跨越团队边界,无论是在管理、沟通,还是在工作安排上,都会产生更高的成本

这再次强调了康威定律的重要性

Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization's communication structure.

-- Melvin Conway, 1968

翻译为:一个组织的系统通常被设计成这个组织通信结构的副本

换句话说,组织架构决定了技术架构,有怎样的结构、规模和能力的团队,就会产生出对应结构、规模、能力的产品

其指导意义是:想要什么样的技术架构,就应该设立与之对应的组织架构。一个微服务系统是围绕着一种业务能力来建设的,那就应该围绕业务能力构建团队,在一个团队内安排完成服务建设的各类工种,减少跨团队沟通、管理成本,实现团队自治与团队间松散耦合

图1.孤立的职能团队催生出孤立的技术架构,即使是简单的变更也会存在跨团队沟通

图2.跨职能团队(每个团队包含开发线上的各类工种),催生出围绕业务能力构建的产品

2.产品化思维

做产品、而不是做项目,如今大多数信息中心部门的职能还停留在业务支持的程度,是为企业的业务部门提供IT系统支持的组织,这种做完一个项目接着做下一个项目的模式,使得员工在业务能力上很难得到持续的积累和沉淀

按照业务能力划分团队后,让员工在各自擅长和感兴趣的领域中持续发展,每个团队负责对相应产品的业务能力进行持续"运营"。随着产品功能的丰富、持续的完善、规模的壮大,开发团队的业务理解能力和专业技能也同步提升,这样对于员工的工作积极性和创新意识的提升将会创造一个很好的氛围。在产品成长的过程中,团队的成员也有了足够的时间和机会对于该服务相关的业务领域有了更深入的理解,从而为团队培养出既懂技术,也懂业务的复合型人才创建了良好的生长土壤

从员工的自身出发,要避免把软件研发看作是要去完成某种功能,而要把它当做是一种持续改进、提升的过程。比如,我们不应该把运维看作就是运维团队的事,把开发看作就是开发团队的事。开发团队应该为软件产品的整个生命周期负责。开发者不仅应该知道软件是如何开发的,还应该知道它解决了哪些业务问题、它如何运作、用户如何反馈,乃至售后支持工作是怎样进行的,努力朝着企业稀缺的"既精通业务,又熟悉技术"的复合型人才方向发展

3.分散治理

分散治理并不是鼓励微服务团队进行技术异构,因为大多数的公司都不会在某一个服务用 Java,另一个用Python,下一个用Golang,而是通常都会统一主流语言,甚至会有统一的技术栈或专有的技术平台。而是强调在确实有必要时,微服务团队拥有自主选择的权利,比如做深度学习时,可以选择用Python,需要存储schemaless数据时,可以选择用MongoDB

4.数据去中心化

数据库耦合(多个应用使用一个数据库)是应该极力去避免的一种设计,这会带来很多严重影响服务发展的问题,包括高耦合度、无法独立扩展、无隔离性等问题

  • 抽象耦合:同一个数据实体在不同领域中的抽象形态往往是不一样的,比如订单实体在后厨领域中关注的是菜品,而在物流领域中关注的是送餐时间和地点,如果多个领域的服务共享存储,服务之间可能会互相产生影响,从而丧失了各自的独立性
  • 表结构耦合:更改表结构时(删、改),要保证对所有服务都支持这个更改,这是很难做到的,因为很难确定每个服务对各个字段的理解与定义。比如不同应用对某个状态字段的语义有不同解释,那某个服务想增加、或者更换一个状态位就会很困难。对于只需要使用部分字段的程序来说,还会增加了他们的认知负荷
  • 运行时耦合:对数据库来说,来自不同服务的流量没有任何隔离,任一个服务引起的数据库压力过载会直接拖累其他服务,引发可能的雪崩

需要注意的是,分离的数据库带来了数据一致性的问题,因为我们没法再继续依赖数据库事务保证一致性,但是两害相权取其轻,这个代价是值得的

5.通过服务而不是类库来构建组件

通过服务,而不是类库来构建组件。同样的,尽管远程服务调用引入了新的问题(更高昂的调用成本、更复杂的技术组件),但这是为组件带来隔离与自治能力的必要代价

6.轻量级通讯机制

这里点名批评了ESB这种笨重的消息总线机制,微服务提倡的是类似于经典 Unix 过滤器那样,简单直接的通讯方式。比如说RESTful 风格的通讯,在微服务中就是比较适合的

ESB:在ESB这样一个中心服务总线上,提供了诸如对各种技术接口(HTTP、Socket、JMS、JDBC等)的适配接入、数据格式转换、服务发现、注册、路由、协议转换、接口监听等功能,一揽子解决了几乎所有分布式系统互联互通需要面对的问题。ESB架构确实有效的降低了系统间的耦合、更方便、高效的实现了对新系统的集成,因此在当时那个年代(2010年左右)颇受青睐。

但ESB并不太适用于互联网业务场景,主要体现在三个方面,1.多一倍的网络开销:每次服务请求都比直接交互的方式多一倍的网络开销,2.吞吐能力扩展成本高:虽然ESB服务节点可以通过集群部署的方式分担压力,实现吞吐量的线性扩展,但由于一般的企业服务总线包含的功能非常多,所以其扩容升级的成本往往是很大的。另外,企业服务总线承担了企业内部所有的通讯流量,这对其网络设备的要求是非常高的,甚至会超出目前网络设备的能力范围,3.雪崩风险:如果与ESB服务器连接的任何一个服务,因为其不规范的调用、或者因为其提供的服务异常导致某台ESB服务器压力过载、无法正常提供服务,此时压力落在剩余的ESB服务器上,剩余的服务器也可能会因为压力过载而无法正常提供服务,继而ESB服务全线崩溃

7.容错设计

容错性是指系统中一个或部分组件出现异常时,可以继续对外提供稳定服务(可用性)的能力。互联网的核心诉求是高可用,服务的不可用除了造成企业金钱上的损失外,更严重的是给客户造成商业损失或者糟糕的用户体验,使客户失去对平台的信心,造成客户流失

分布式系统的本质是不可靠的,人总会疏忽犯错,代码总会有缺陷,电脑总会宕机崩溃,网络总会堵塞中断,这决定了软件出错是必然的,如何在承认软件必然会出错的前提下保障系统整体的可用性?容错设计是关键。没有容错能力的系统是及其脆弱的,任何一个服务的崩溃可能会迅速传播到整个系统、导致雪崩,这是任何一个企业都无法承受的,因此对于分布式系统来说,容错性是绝对不容疏忽的

在微服务架构中,要有机制可以检测到出错的服务,进行故障转移(或者快速失败等其他容错策略);要可以检测到持续出错的服务,进行故障隔离;要可以检测到服务已经恢复正常,重新建立连通

容错策略:那么在微服务架构中,面对服务调用错误,我们可以做些什么?这就引出了容错策略

  • failover:故障转移,高可用的系统中,服务都会集群(多节点、多机房、多城市)部署,一个节点故障,就尝试另一个可用节点。这要求被调用的服务保证接口的幂等性,并且要特别注意重试次数限制,过多的重试会导致更高的执行成本、服务方更大的压力等问题
  • failfast:快速失败,并不是所有接口都可以保证幂等,尤其是企业外部的API服务,你不可能要求银行的转账接口具有幂等性。这种场景下,失败后就抛出异常,让调用方自行处理
  • failsafe:安全失败,对于那些不太重要的服务调用(比如记录审计日志,主干逻辑不依赖于其返回结果),失败后也不要对整体结果产生影响,既主干逻辑的结果依然作成功处理
  • failsilent:沉默失败,将失败的服务隔离一段时间,这段时间内不再对其发起请求
  • failback:故障恢复,失败后异步重试,尽量保证成功,由于是异步进行的,因此适用于返回结果对主干逻辑不产生影响的场景。和failover一样,也要求服务保证接口的幂等性,也需要注意重试次数限制

除了失败后的补救措施外,有没有提前预防失败的措施呢?也是有的

  •  forking:并行调用,同时发起对多个或者所有可用服务的调用,拿到任何一个返回就算成功,并行调用除了可以降低失败率外,还可以加快响应速度,不过也要付出更高的执行成本

容错设计模式:那么要实现某个容错策略,我们可以怎么做?这就引出了容错设计模式

重试模式:重试是最容易想到、也是最简单的手段,failover、failback两种容错策略都会进行重试。大多数程序员都手写过重试逻辑,不过任何场景都可以重试吗?你重试的姿势对吗?

  • 重试的代价是更高的延时,只应该对核心逻辑选择重试,非核心逻辑应该优先考虑其他处理方式
  • 只应该对瞬时的、确实可以恢复的故障进行重试,同样是调用失败,网络异常和明确返回"非法参数"显然不是一回事,后者重试100次也无法成功
  • 如果服务方没向你承诺保证幂等,重试出问题后你猜是谁的锅?
  • 和while循环一样,重试必须有明确的终止条件,超时终止:即使没有重试,也应该为每个服务调用设置合理的超时时间,过长的等待时间暗示着可能的低可用性,次数终止:过多的重试会导致更高的执行成本、服务方更大的压力等问题
  • 不要在多个环节上都开启重试,客户端、网关、负载均衡器都可以方便的开启重试,不过你要知道最终的重试次数等于各环节重试次数的乘积

断路器模式

        断路器会持续收集服务的调用结果,一旦故障(超时、拒绝、失败)频率超限,就进入open状态,所有请求不再发送给服务方,直接返回预制好的降级响应,将错误的服务隔离开来,防止故障扩散;open状态持续一段时间后,自动进入half open状态,此时再有请求过来,就会先放行一个,若返回结果正常,进入close状态,以便于从隔离状态恢复到正常状态

船舱壁模式

        断路器模式针对超时、拒绝、失败三种故障情况进行处理,而船舱壁模式只针对超时。拒绝和失败场景下,请求方可能很快就能拿到响应,而超时场景下,请求方在到达超时时间前要一直等待,谁来等呢?自然是线程了,而线程是典型的系统全局性资源,全局性资源的使用如果不加以控制,就可能出现被某一个使用方耗尽而导致其他使用方无法使用的情况

        比如服务A的tomcat线程数300,某一时刻起,由于服务A依赖的服务B过载导致A发到B的请求全部超时,随后请求数增多,更多的线程开始阻塞在等待B响应的过程中,由于并发量较高,300个线程迅速耗尽,此时A服务全面瘫痪,无法再提供服务。要解决这个问题,就要对全局性资源的使用加上控制,常用的控制手段有两个:

  • 独立线程池:针对每个使用方,使用独立的线程池,比如上述场景中,为B服务设置大小为10的线程池,B故障后,最坏情况也就是B对应的10个线程不可用而已
  • 信号量:独立线程池的问题是线程数越多,操作系统进行上下文切换的开销越大,信号量则是更轻量级的方式,为每个使用方设置一个计数器,每次开始使用时计数器+1,使用完成时计数器-1,计数器的值不得超过预定好的阈值,如此也可以达到控制的目的

8.基础设施自动化

基础设施自动化技术(如CI/CD)在过去几年中的长足发展,大大降低了构建、发布、运维工作的复杂性。由于微服务架构下,运维的服务数量比起单体架构来说,要有数量级的增长,所以使用微服务的团队,会更加依赖于基础设施的自动化

9.演进式设计

进化论告诉我们,当环境持续变化的时候,唯有不断调整自己以适应新环境的生物方能生存下去。同样的,为了让软件架构拥有持续的生命力,我们需要主动让其演进以适应软件环境的变化

软件行业曾经有这样一个共识,架构一旦确定,“日后很难改变”。演进式架构将支持增量式变更作为第一原则,由于历来变更都是很难预测的,改造的成本也极其昂贵,所以演进式架构听上去很吸引人。如果真的可以在架构层次做到演进式地变更,那么变更就会更容易、成本更低,也能发生在开发实践、发布实践和整体敏捷度等多个地方

微服务满足这一定义,仔细拆分出的微服务遵循强边界上下文的原则,边界划分明确的组件,显然可以给希望做出非破坏性变更的开发人员更大的便利,每个服务在结构层面与其他服务都是解耦的,替换服务的成本更低、更容易预测。而毫无架构元素的大型单体系统就无法做到演进式变更,因为它缺少模块化

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值