微服务的痛点
在产品研发过程中,引入一种技术来解决一个业务问题并不难,难的是能否合理评估技术风险,这个观点对微服务同样适用。
因此,本节将专门讨论微服务会带来哪些问题,这部分内容不管是在面试中还是日常架构设计中,对大家的帮助都会非常大。
痛点:微服务职责划分
微服务的难点在于无法对一些特定职责进行清晰划分,比如某个特定职责应该归属于服务A还是服务B?为了方便理解,下面举几个例子说明一下,微服务的职责划分是怎么演变成公司技术部门的陷阱的。
首先,微服务的划分原则都是很容易理解的。
1.根据存放主要数据的服务所在进行划分
比如一个能根据商品ID找出商品信息的接口,把它放在商品服务中即可。
再比如获取单个用户的所有订单,把它放在订单服务中即可。
2.业务逻辑服务归属与业务人员的划分可能存在关系
比如每个商品在每个门店的库存应该放在商品服务还是门店服务?因为各个门店的商品库存由该门店的运营人员管理,最终可以把它放在门店系统中。
3.业务逻辑服务归属与产品人员的划分可能存在关系比如业务部门提出一个新需求,需要设计一个能对商品进行相关设置的功能,使得某些门店只能卖某些商品。
此时这个功能应该放在门店服务还是放在商品服务呢?这就需要看这个功能由哪条业务线的产品负责人负责了,比如,由商品系统的产品经理负责,就把它放在商品服务中;由门店的产品经理负责,就把它放在门店服务中。
4.业务逻辑服务归属与工期可能存在关系
紧接着上面的例子——实现某些门店只能卖某些商品的需求。根据前面产品从属原则的划分逻辑,特定门店特定商品的上架功能放在门店服务中,因为特定商品由门店的运营人员负责上架。
但是这种划分逻辑会出现这样的情况:门店服务的开发人员很忙,没空接这个需求,而商品服务的开发人员刚好有空,但他们对门店服务的逻辑不了解。于是,商品服务的开发人员提议,如果想在两周内实现这个需求,则必须把这个功能放在商品服务中。这种方案看起来很不通用,不过最终他们确实把这个功能放在了商品服务中,因为再优雅的设计也抵不过业务部门要求的上线时间压力。
5.业务逻辑服务归属还可能与组织架构存在关系
通过康威定律就能很快明白这一概念。
康威是个程序员,他在1967年提出:设计系统的组织在设计系统时,会设计出基于这些组织的沟通结构的系统。
关于微服务职责划分的痛点,通过前面几个例子的介绍,大家应该有所体会了,接下来再讲一个进销存供应链系统的例子,加深理解。
这里的“进”指的是供应商的采购,“销”指的是门店的销售单,“存”指的是一些中央仓库的库存,且进销存供应链系统与新零售系统之间紧密结合,对应的架构图如图12-3所示。
• 图12-3 新零售和供应链系统关系
在这个架构中,原本门店的商品库存由门店运营人员(即新零售业务)负责,中央仓库库存由供应链人员管理。后来,不知什么原因领导要求更改供应链总监职责,此时供应链总监需要同时负责门店商品库存+中央仓库库存。
先来看看原职责划分情况,对应关系如图12-4所示。
• 图12-4 原职责划分
在图12-4中可以看到,在原有的组织架构中,新零售业务的产研只对接新零售业务,供应链业务的产研只对接供应链业务。现如今,门店库存管理职责需要划分到供应链业务中,也就是说新零售业务的产研不再负责这个需求,而是交由供应链业务的产研负责了。此时供应链业务的产研会把门店库存积极地迁移到供应链的库存管理中,因为门店库存管理好了,供应链业务方的绩效就好了,产研的绩效也高了,年终奖也就更多了。
因此,在现实场景中,微服务职责的划分会受太多人为因素的影响,大家也就能理解为什么现在关于服务职责划分原则的相关资料不太多。
痛点:微服务粒度拆分
微服务还有一个痛点,就是服务太多。这里通过一个加盟商的例子把服务粒度的内容详细介绍一下。
还是以上面的新零售系统为例。最初该系统只有登录和信息管理功能,把这些功能存放在一个服务中即可,实现起来比较简单。随着加盟商的加入,因为加盟商准入、开店、退出都涉及费用问题,因此又需要增加财务功能(如应收、应付、实收、实付、退款、对账等)。
随着业务的逐步开展,又需要增加加盟商员工管理(员工管理、部门管理、权限管理)、返点、加盟商子门店管理等功能,而此时的加盟商管理系统只有一个服务,这是不合适的。那微服务的粒度到底拆分到多细比较合适呢?
比如,什么时候拆分加盟商服务比较合适,做加盟商的财务功能时还是加盟商的员工管理功能时?做加盟商的返点功能时还是加盟商的子门店管理功能时?
一般来说,在设计新功能之前,会遵循一个大致原则:根据新的微服务的大小,安排3~4人设计即可。
但是当一个微服务设计出来后,它的改动成本一般不高,除非实现大规模重构。为了防止开发人员出现闲置的情况,公司会安排他们设计新的功能,而设计新功能时,开发人员倾向于将独立的功能存放在新的服务中,导致加盟商的财务、员工管理及返点功能都被独立出来了。为了避免这种情况的发生,在对微服务粒度进行拆分时,还需要考虑另外一个因素——绩效。
大家都知道,开发人员的绩效很难实现量化,而微服务数可谓是一个难得的可量化指标。在规章制度上,虽然不会把微服务数列为一个KPI(这样微服务数绝对会大增),但是开发人员在阐述个人工作量时偶尔还是会提微服务数,如果其他同事听后开始留心,潜意识里也喜欢做微服务,随着时间的推移,微服务就会越来越多,甚至出现人均5个微服务数的情况。
笔者公司后来注意到了这个问题。当这个问题在公开场合提出以后,团队又开始人为控制微服务数量,这种方式确实起到了一定的效果。
那微服务的粒度大小控制在什么范围比较合适呢?这就是一个痛点,因为没有确切的答案。
痛点:没人知道系统整体架构的全貌
是否碰到过这种情况:每隔几个月或半年,领导就会让汇报一下每个部门的微服务数量、公司微服务总数量、每个微服务都用来做什么等情况。因为企业微服务数较多,所以每次给领导汇报时,都有一个很长的清单。
然后领导开始抱怨:“几百个微服务?系统这么复杂了吗?谁知道所有系统的全貌?如果出现问题,你们如何快速定位问题?”此时几个负责人都难以回应,可能在想:“我连自己部门的微服务列表都没搞清楚。”
笔者在没有使用微服务的公司,首先会把公司的系统架构全貌搞清楚,之后一旦出现问题,也就容易定位故障点了。可是自从来到使用微服务的公司后,便再也没有这样的冲动了,只要熟悉自己负责的部分即可,如果出现问题就临时学习一下相关系统。
因此,在实际工作中,很难找到了解微服务系统架构全貌的人员,这就是微服务的一个痛点。
痛点:重复代码多
没有使用微服务的公司会把所有的代码放在了同一个工程中,如果发现某些代码可以重复使用,把这些代码抽取出来存放在Common包中即可。但是这种代码设计在微服务中经常会出现问题,这里还是举个例子说明一下。
比如某个团队做了一个日志自动埋点的功能,它能自动记录一些特定方法的调用。其他团队知道这个功能后,觉得很不错,想直接拿来用,于是埋点团队给出了Maven的声明。但是第一个吃螃蟹的团队使用后,很快出现了一个JAR版本冲突问题,这时如果他们将冲突的JAR进行升级,原始代码就不能使用了。为节省人力成本,他们只好询问埋点团队如何实现版本兼容。
为了兼容这个团队的JAR版本,自动埋点团队又重新设计了一版埋点的JAR,并去掉了一些特定API的使用,两个团队终于可以正常使用了。
不过,第三个使用埋点JAR的团队又汇报了一个JAR版本冲突问题,此时自动埋点团队从投入产出比角度考虑,不得不放弃维护这个公用的JAR,并直接告知其他团队:代码就在Git上,自己直接复制修改吧。因此,这个代码在不同团队的微服务中最终存在多个版本。
后来这些团队在复盘中得出结论:重用JAR本身没有错,错就错在使用的JAR版本太多了,必须改变这个局面。于是他们将所有JAR版本进行统一的项目正式立项了,但是第二天,因为有紧急业务需求,项目搁置。又过了一段时间,有人提起了这个重要项目,结果因其他的紧急业务需求再次搁置。后来大家逐渐明白,这个项目没法做,因为投入产出比不高。
其实微服务之间存在重复的代码也没关系,因为部门之间的重复代码比比皆是,而且技术中心每个部门都有自己framework/Common/shared/arc的GitLabsubgroups,它们可以实现对部门内部的通用代码进行重用。
不过,在当时的项目里,维护这些不多的重复代码总比统一排期做重构、统一评审JAR版本的成本低得多。
痛点:耗费更多服务器资源
笔者曾在一家小公司做过一段时间技术顾问。该公司原来使用的是单体式架构,一共部署了5台服务器,后来他们一直抱怨系统耦合性太强,代码之间经常互相影响,并且强烈要求将架构进行迁移。
于是,他们根据业务模块把原来的单体式架构拆分成了6个微服务。考虑到高可用,每个服务至少需要部署在两个节点上,再加上网关层需要两台服务器,最终一共部署了15台服务器(因为其中一个服务比较耗资源,为了安全起见,多加了一个节点)。
在这个拆分过程中,业务没有变,流量没有变,代码逻辑改动也不大,却多出了9台服务器,为此,团队也发生过争执,当时的争议点是,如果是这种情形,就不应该一台服务器只部署一种服务,而是把服务A、B部署在一个节点,服务B、C部署在一个节点,服务A、C再部署在一个节点,如图12-5所示。
• 图12-5 混合部署示意图
可是这个方案很快就被大家否决了,因为如果每个服务器只部署一种服务,服务器的名字直接以服务的名字命名就行,之后运维排查问题时也比较方便。那么,如果把不同的微服务混合部署,服务器又该如何命名呢?
于是有人提出:“要不就这样吧,反正服务器比较便宜,多几台也无所谓。”大家纷纷附和赞同。公司就这样多了一笔开销(不过并不是只有小公司这样做,大公司同样如此,也经常会有人抱怨服务器资源不够用)。过了几天后,CTO召集所有开发人员开会:“这个季度的服务器预算太多了,财务部门审核不通过,你们需要想办法缩减一下服务器数量。”
会议结束后,大家各自回到工位,开始对每个服务进行检查,于是就有了下面这段对话。
甲:“这个服务怎么用了这么多台服务器?很耗资源吗?”
乙:“不是,主要是公司强制要求我们实现多数据中心部署。”
甲:“这个服务很重要吗?是内部使用的吗?”
乙:“是,这个目前只是开发人员在使用。”
甲:“那为什么要做负载均衡?只留一台吧。”
乙:“好吧。”
甲:“现在我们缩减多少台服务器了?”
乙:“……”
在笔者任职或合作过的公司中,这种情形很常见,因此不得不说微服务真的很耗服务器。
痛点:分布式事务
分布式事务这个痛点对于微服务来说,简直就是地狱。为了让你深刻理解这个痛点,先以一个笔者经历过的实际下单项目为例。原本的下单流程是这样的:插入订单——>修改库存——>插入交易单——>插入财务应收款单——>返回结果给用户,让用户跳转。
单体式架构中,只需要把上面的下单流程包含在一个事务里就可以了,如果某个流程出错,直接回滚数据,并通过业务代码告知用户出错,让用户重试即可。
可是迁移到微服务后,因为这几个流程分别存放在不同的服务中,所以需要更新不同的数据库,也就需要考虑以下逻辑。
1)某个流程出错是否需要将数据全部回滚?如果需要,就要在每个流程中写上回滚代码。那万一回滚失败了呢?是不是还需要写回滚代码,回滚代码算回滚吗?或者是某些流程回滚,某些流程不回滚?那哪些流程回滚,哪些流程不回滚呢?
2)是否选择统一不回滚,失败就重试?这样岂不是需要做成异步?如果做成异步,会不会出现时间超时?如果超时了,用户怎么办?需要回滚吗?
如果只是某些特定的流程让人难以抉择也就罢了,但是这种分布式服务更新数据的场景实在是太多了,如果每个场景都要考虑这些逻辑,团队肯定更痛苦,而且还要花时间沟通这些逻辑。
因此,针对这种情况,在大部分场景下不考虑回滚和重试,只考虑写“HAppy Path”,如果报错就记录异常日志,再线下手工处理。
使用微服务时,分布式事务一直是痛点也是难点,因此痛定思痛,团队决定尽快解决这个问题,关于此问题的解决方案将在后面进行说明。
痛点:服务之间的依赖
在设计类时,往往需要遵循类与类之间不可循环依赖的原则,因此最终设计出来的类关系是层次分明的结构,如图12-6所示。
图12-6 预期服务关系结构图
如果把依赖关系转移至微服务,结果会怎样呢?先举个例子。
比如商品系统针对不同门店类型设置不同价格时需要调用门店系统中的类,这时商品系统就依赖了门店系统;同时因门店系统中存在商品库存,门店系统也就依赖了商品系统的商品信息,从而形成了循环依赖。
再比如最底层的财务系统,从理论上讲,它不需要依赖其他系统。而实际上刚好相反,它必须依赖订单信息,比如费用由什么订单产生,同时它还需要依赖会员信息和门店信息,比如是谁付的钱和谁收的钱。
因此,随着需求越来越多,服务之间的依赖关系就会变成千丝万缕、难以理清的架构,如图12-7所示。
• 图12-7 服务关系现实结构
通过图12-7发现,服务之间的依赖可谓是“你中有我,我中有你”。
那么这种复杂的依赖关系一般会出现什么问题呢?下面先来说说笔者的两次架构经历。
1.重构时,为了评估影响面让各个团队鸡飞狗跳项目组需要重构两个服务,因为系统已经上线了,为了保证重构不影响业务,就需要在测试过程中评估哪些服务会受影响。
因为之前一段时间线上环境已经出现了两次一级故障,所以CTO要求此次务必认真评估影响面,不能再出现类似问题。
于是一个Leader提出要求:先根据重构的代码找到受影响的接口,然后根据接口找到所有调用这些接口的上游代码,再找到那些调用上游接口的接口,以此类推。
由于该方案分析成本过高,且一旦出现遗漏就会前功尽弃,所以直接被CTO否决了。
最终有人提出了一个较合理的方案:根据全链路日志系统中的服务间依赖,找到这两个服务的所有上游服务及上游的上游服务。
对这个方案进行评估后,项目组发现重构后大半的微服务都会受到影响,于是要上线的那几天,很多其他团队的人不得不一起通宵达旦地做回归测试。
2.重构时,为了不影响其他团队,出现了很多版本
有了之前的教训,后续遇到新的重构需求时,项目组就直接把原来的服务abcServiceV1写成新服务abcServiceV2,此时新的代码直接调用V2版本,而旧的代码继续调用V1版本,有时间再下架abcServiceV1,这样就不用很多人陪着加班了。
后来V1、V2的形式越来越流行,服务数量出现暴涨。而且在实际开发工作中,开发人员很少在后期下架旧版服务,最终导致服务数量越来越多且新旧版本并存,维护起来更痛苦了。
以上就是服务之间的依赖导致的问题,这个问题的解决方案将在第15章进行讲解。
痛点:联调的痛苦
以往的需求排期是这样的:需求评审时间——>开发完成时间——>测试完成时间——>上线时间。
迁移到微服务后,需求排期增加了两个环节:需求评审时间——>接口设计时间——>开发完成时间——>联调完成时间——>测试完成时间——>上线时间。
在这种变化下,每次遇到比较紧急的需求时,都需要额外问一句:接口文档好了吗?联调怎么样了?
为什么这么在意联调?因为在一个软件项目中,影响项目排期的往往不是技术问题而是第三方依赖问题,一旦涉及沟通、协调等问题就会特别耗时间。
这里举一个例子简单说明。
有一次,门店系统正在进行小的需求改动,此时需要商品系统开发人员配合提供一个简单的接口,而商品系统的开发人员说:“我们正在忙另外一个项目,周二抽空提供这个接口。”
门店系统开发人员简单评估一下上线周期:周二拿到接口,周三进行联调,周四、周五测试两天,应该周五晚上就可以上线了,于是向业务人员进行了相关反馈。但是把门店系统的功能设计好后,因商品系统的开发人员开展的项目临时修改了一个紧急需求,要求周二务必通宵完成,为此,他们无法在周二给出接口,最终门店系统周五上线的计划就被延误了。
而这种事情在实际开发过程中经常发生,对工作效率影响不小。
下面再举一个例子来说明。
有一次,笔者所在团队正在做一个涉及30个服务的大项目。周五完成所有需求评审后,首要目标是核对接口文档。
因为接口文档是由各个项目组根据实际需求汇总的各自需要提供的接口,总计300多个接口,导致这个过程花了整整两周时间。
核对完接口文档后,十几个项目组之间又开始协调接口联调时间,这个过程又花了3天时间。对完接口后,各自开发功能的速度就很快了,这个阶段也花了两周时间。
对完接口后,在实际开发过程中接口还会修改吗?肯定会,而且增加、修改、删除接口都有可能。但是对完接口后,至少可以保证在大概一致的方向上前进,如果确实需要调整,修改的也只是一些小细节,并不会影响开发进度。
所以这个核对接口的工作是值得的。
功能设计完成后,就需要进行联调了,而这个过程往往最耗时,因为需要耗费大量的时间在沟通上。可以通过下面这段对话感受一下。
调用方:“addXXX的接口怎么样了?” 被调用方:“好了,你可以调调看。” 调用方:“不行啊,返回了404。” 被调用方:“哎呀,环境部署错了,稍等一下。” 调用方:“赶紧的。” 被调用方:“好的。” …… 被调用方:“好了。” 这时,调用方在联调时发现需要增加一个字段,就说:“addXXX的接口需要增加一个修改时间字段,你帮我加一下。”被调用方:“可以,不过我正忙着另外一个项目,要不明天给你?” 调用方:“别啊,今天必须联调完。” 被调用方:“那我晚上赶一赶,9点给你行吗?” 调用方:“好吧。”
所以,在做项目时最麻烦的事情之一就是协调时间,因为它不可控。毕竟每个开发人员的需求优先级都不一样,除非所有相关项目组的第一优先级都相同,否则协调时间会是一件很让人头疼的事情。
而这个大项目共包含300多个接口,也就是说300多个接口都需要协调,这就使得联调的时间一点不比开发功能的时间少。
关于这个痛点,将在后续中给出解决方案。
痛点:部署上的难题
使用单体式架构时,每个开发人员都想在本地把整个系统部署完后再调试,此时部署方式非常简单。可是迁移到微服务后,项目经常涉及10个以上的微服务,此时,如果让开发人员将这些微服务在本地部署完后再联调,根本无法实现。首先内存很可能不够,即使内存足够,也几乎没有开发人员会熟悉所有微服务的部署。
为此,可以专门建立一套测试环境供开发人员进行联调,这样开发人员就可以将本地正在开发的服务接入联调环境,如图12-8所示。
• 图12-8 联调环境示意图
但是,这种架构有时候会出现以下3种问题。
1.联调环境的数据缺漏非常多
因为联调接入的服务是本地开发过程中的服务,即数据是开发数据,所以单个服务中的数据不具备完整性。
而且因为是开发环境,导致上下游服务之间没有调通,比如订单系统中会出现上下游的单据也不一致、不完整的情况,即不是出现订单少了收款单的情况,就是出现准入少了审批单的情况。
2.服务调用错误
经常会有人发出类似下面的抱怨。
甲:“这个接口怎么有问题?你看,A字段和B字段都缺失了。” 乙:“怎么会呢?我明明加上去了。” 甲:“你是不是忘记部署了?还是部署失败了?” 乙:“我看看。” 甲:“你是不是调用了XXX的服务?问一下XXX。”过了一会儿,乙过来说:“还真是,他刚好在接入这个服务,我找他 去。”
3.联调环境极度不稳定
因为开发人员常常需要对联调中的服务进行部署,或者将不稳定的开发服务接入联调环境,再加上前面提及单个服务中的数据不具备完整性,所以,在联调环境下走完整个流程是不太可能的。为此,只能将联调环境用作接口间的局部联调。
这就是联调环境难以部署带来的痛点,导致太多时间花在协调问题上。那么有没有一个办法可以简单地创建一套相对独立的测试环境?
小结
讲到这里,微服务的9个痛点就讲完了。
有一段时间,笔者和同事们都抱怨领导太保守了,总是用一些守旧的技术,我们在公司学不到什么新技术。而笔者带团队的时候,组员也经常会反馈这个问题。
从个人立场来说,笔者也的确想尽可能地使用新技术,这样自身才能成长。但是从公司的角度来说,新旧系统兼容带来的额外维护成本,新技术的学习、试错成本,可能是个人感受不到的。但是,当你要对整个团队的技术负责时,你也会变得保守。
有位同事说得很好:“程序员喜欢用新技术我能理解。但新技术层出不穷,迁移新技术用3年,结果3年后,更新的技术又出来了,这么多的技术负债谁来解决?你学到了新技术,去新公司用这些技术面试获得更高的岗位,可是旧公司的烂摊子谁来接?”
笔者并不反对使用前沿一些的技术,但对于新技术,要有敬畏心,除了知道它的优点,也要了解它的缺点。
所以这一章花了很长的篇幅来介绍笔者团队使用微服务时碰到的痛点。那么,关于微服务的优势前面只讲了5点,而微服务的痛点讲了9点,为什么还要使用微服务?如果使用单体式架构的话,随着业务的复杂化,将会出现无论怎么加人都无法迭代的情况。而如果使用微服务,虽然它存在很多问题,但是至少可以通过增加人力的方式来保持迭代。原因就这么简单,跟那些痛点无关。
微服务架构的痛点介绍完了,接下来开始讲一些进阶的微服务实战场景,看看如何解决本章提到的这些痛点。