在前面我们说了微服务的两个痛点:微服务的职责划分和微服务的粒度拆分痛点,这里接着聊剩下的痛点:
一、没人知道系统整体整体架构的全貌
不知道大家有没有碰到过这种情况:每隔几个月或半年,大领导就会发话让我们汇报下每个部门的微服务数量、公司微服务总数量、每个微服务都用来做什么等情况。因为企业微服务数较多,所以每次给大领导汇报时,都是长长的一条清单。
然后大领导开始抱怨:“几百个微服务?系统这么复杂了吗?谁能知道所有系统的全貌啊,如果出现问题,我们如何快速定位问题点呢?”此时负责人们只好乖乖地低着头,其中一个同事偷偷嘀咕:“我连自己部门的微服务列表都没搞清楚呢。”
在以前,我们首先会把公司的整个架构系统全貌搞清楚,之后一旦出现问题,也就容易定位故障点了。可是自从来到这家使用微服务的公司后,便再也没有这样的冲动了,只要求搞懂自己的一亩三分地就行,如果出现问题临时学习一下相关系统就好了。
因此,在实际工作中,很难找到这么一个人,他能知道系统整体架构的全貌,这就是微服务的一个痛点。
二、重复代码多
在以前的公司,我们把所有的代码放在了同一个工程中,如果发现某些代码可以重复使用,把这些代码抽取出来存放在 Common 包中就行。但是这种代码设计在微服务中经常会出现问题,这里我还是举个例子说明下。
比如某个团队做了一个日志自动埋点的功能,它能自动记录一些特定方法的调用。其他团队知道这个功能后,感觉很不错,想直接拿来用,于是埋点团队开心地给出了 Maven 的声明。但是第一个吃螃蟹的团队使用后,立马报出了一个 JAR 版本冲突问题,这时如果他们将冲突的 JAR 进行升级,原始代码就不能使用了。为节省人力成本,他们只好询问埋点团队如何实现版本兼容。
为了兼容这个螃蟹团队的 JAR 版本,自动埋点团队又重新设计了一版埋点的 JAR,并去掉了一些特定 API 的使用,最终 2 个团队终于可以正常使用了。
不过呢,第三个使用埋点的 JAR 的团队又汇报了一个 JAR 版本冲突问题,此时自动埋点团队从投入产出比角度考虑,不得不放弃维护这个公用的 JAR 了,并直接告知其他团队:代码就在 Git 上,你们自己直接 fork 修改吧。因此,这个代码在不同团队的微服务中最终存在了多个不同版本。
后来我们复盘了下,得出结论:重用 JAR 本身没有错,错就错在我们使用的 JAR 版本太多了,必须改变这个局面。
于是我们将所有 JAR 版本进行统一的项目正式立项了。第二天,因紧急业务需求下来了,大家都忘掉了这回事。又过了一段时间,有人提起了这个中心级项目,结果又被紧急的业务需求 PK 下去了。后来大家逐渐明白,这个项目没法做,因为投入产出比不高。
其实微服务之间存在重复的代码也没事,因为部门之间的重复代码比比皆是,而且技术中心每个部门都有自己的 framework/Common/shared/arc 的 GitLab subgroups,它们可以实现对部门内部的通用代码进行重用。
不过,维护这些小小的重复代码总比统一排期做重构、统一评审 JAR 版本的成本低得多。
三、耗费更多服务器资源
曾经了解到一个小公司。他们原来使用的是单体式架构,一共部署了 5 台服务器,后面他们一直抱怨系统耦合性太强,代码之间经常互相影响,并且强烈要求将架构进行迁移。
于是,根据业务模块,把原来的单体式架构拆分为了 6 个微服务。考虑到高可用,每个服务至少需要部署在 2 个节点上,再加上网关层需要 2 台服务器,最终,一共部署了 15 台服务器。(因为其中一个服务比较耗资源,为了保险起见,多加了一个节点。)
在这个拆分过程中,业务没有变,流量没有变,代码逻辑改动也不大,却无缘无故多出了 9 台服务器,为此事还发生过争执,当时的争议点是如果是这种情形,就不应该一台服务器只部署一种服务,比如我们可以把服务 AB 部署在 1 个节点,BC 服务部署在 1 个节点,AC 再部署在一个节点,如下图所示:
可是这个方案很快就被大家否定了,因为如果每个服务器只部署一种服务,服务器的名字直接以服务的名字命名就行,之后运维排查问题时也比较方便。可是如果我们把不同的微服务混合部署,服务器又该怎么命名呢?
于是,有人提议:“要不这样吧,反正服务器比较便宜,多几台也无所谓。”大家纷纷附和赞同。公司的钱就被这帮程序员浪费了,不过你别以为只有小公司这样做,大公司同样如此。
过了几天后,CTO 召集所有研发人员正式开会:“这个季度,我们的服务器预算太多了,财务部门审核不通过,你们需要想办法缩减一下服务器数量,把不用的服务器都下掉。”
会议结束后,大家各自回到工位,开始对每个服务进行检查,于是就有了下面这段对话。
A:“这个服务怎么使用了这么多台服务器?很耗资源吗?” B:“不是,主要是公司强制要求我们实现多数据中心部署。” A:“这个服务很重要吗?内部使用的吗?” B:“是,这个目前只是开发人员在使用。” A:“那干吗做负载均衡,下了下了,只留一台。” B:“好吧。” A:“现在我们缩减了多少台服务器了?” B:“……”
在大部分公司中,这种情形很常见,因此不得不说微服务真的很耗服务器。
四、分布式事务
分布式事务这个痛点对于微服务来说,简直就是地狱。为了深刻理解这个痛点,我们先以曾经经历的下单流程为例。
原本的下单流程是这样的:插入订单——>修改库存——>插入交易单——>插入财务应收款单——>返回结果给用户,让用户跳转。
在单体式架构中,我们只需要把上面的下单流程包在一个事务里就行了,如果某个流程出错了,直接回滚数据,并通过业务代码告知用户出错了就行,让用户重试就好了。
可是迁移到微服务后,因为这几个流程分别存放在不同的服务中,所以我们需要更新不同的数据库,也就需要纠结以下逻辑。
某个流程出错是否需要将数据全部回滚?如果需要的话,那么我们需要在每个流程中写上回滚代码。那万一回滚失败了呢?我们是不是还需要写回滚的回滚代码,回滚的回滚代码算回滚吗?
要不就某些流程回滚,某些流程不回滚?那哪些流程回滚哪些流程不回滚呢?
要不就统一不回滚,失败就重试?这样岂不是需要做成异步?如果做成异步,会不会出现时间超时?如果超时了,用户怎么办?需要回滚吗?(怎么又要回滚了?)
如果我们只是纠结某些特定的流程也就罢了,问题是这种分布式服务更新数据的场景实在是太多了,如果每个场景都要纠结这些逻辑,我们得疯了。本来业务部门就嫌我们交付太慢,我们还要花时间扯这些逻辑,干脆整个部门解散得了。
因此,针对这种情况,在大部分的场景下我们不考虑回滚和重试,只考虑写 Happy Path,如果报错就记个异常日志,再线下手工处理,So easy!
结果你们也知道,机房网络抖动是常有的事情(运维经常拿这个当理由),以至于三天两头数据更新出现异常,比如上游数据更新了,下游数据没有更新,这时数据就对不上了,特别尴尬。
以至于业务部门经常抱怨:“咦,这个订单怎么找不到收款单了?咦,这个交易单怎么没有交易流水?”然后我们只能回怼过去:“不是跟你解释过了吗?提个工单就好了,嘀嘀咕咕啥,生怕老板不知道吗?”
使用微服务时,分布式事务一直是痛点也是难点,因此我们痛定思痛,决定好好解决这个问题,关于此问题的解决方案我们将在后面的内容中进行说明。