(十)微服务设计篇:从拥抱微服务到回归大单体经历了什么?究竟该如何用好微服务?

3,592 阅读42分钟

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

引言

纵观整个技术发展史,站在互联网风口上的微服务概念,以迅雷不及掩耳之势迅速崛起,在短短几年的时间里,分布式/微服务成为炙手可热的名词,也曾一度成为技术人追捧的潮流,许多人会以具备“分布式/微服务项目经验”而自豪,甚至在这种风气的影响下,那些身处传统行业的开发者,往往会因为不懂分布式、微服务技术而陷入深深的自卑中……

正所谓分久必合,合久必分,分布式理念出现的时间很早,很久之前就有许多大企业踏足这片领域,可早期的分布式生态并不算太好,尽管很多人都知道这种架构的好处,但需要投入的人力、需要耗费的成本、需要攻克的难关……,足以令任何一家中小企业望而止步。随着时间推移,微服务理念出现并引领了时代的技术浪潮,微服务思想是分布式理念的一种延续,它是顺应时代趋势下的产物,在分布式的基础上提出一整套解决方案,让任何人都具备了涉猎分布式领域的资本,也让那些久经单体架构之苦的人们,彷佛迎来了新的春天。

曾经的我,也是分布式/微服务理念下的狂热信徒,它就像解决高并发、大流量问题的银弹,一站式的分布式解决方案、强大的容错性、明确的边界划分、弹性伸缩背后的性能潜力……,这让当时的我们转身就投入了微服务的怀抱。起初,分布式/微服务让我们在某些场景中尝到许多甜头,但不久后就发现,在有些情况下它也会让人吃不少苦头。而在饱经微服务衍生问题的摧残下,部分企业选择回归大单体架构,这背后到底发生了什么?究竟怎样才能用好分布式/微服务,这将是本文要一起探讨的话题。

PS:个人编写的《技术人求职指南》小册已完结,其中从技术总结开始,到制定期望、技术突击、简历优化、面试准备、面试技巧、谈薪技巧、面试复盘、选Offer方法、新人入职、进阶提升、职业规划、技术管理、涨薪跳槽、仲裁赔偿、副业兼职……,为大家打造了一套“从求职到跳槽”的一条龙服务,同时也为诸位准备了七折优惠码:3DoleNaE,近期需要找工作的小伙伴可以点击:s.juejin.cn/ds/USoa2R3/了解详情!

一、业界“标准”的设计原则

当一个从逻辑上被视为整体的系统,拆分成多个子系统独立开发/部署时,则能被称之为分布式系统,不过值得一提的是:一个分布式系统不一定就是微服务架构,但一个微服务项目必然是分布式架构,因为微服务是分布式的一类变种。与分布式架构相反的是一体化架构,即传统的单体架构,它是一个大而全的功能集合,部署一个节点就具备整个系统的完整功能服务。

我们将时间的齿轮回拨到几年前,当时兴起了一股“逃离大单体、拥抱微服务”的风潮,至于原因是什么?因为单体架构有一系列问题令人头大:

  • 团队所有成员都在一份代码上开发,技术边界不够清晰,代码分支庞大且难以管理,新需求难以快速交付;
  • 所有业务模块糅合在一起,就算只有某块业务流量较大,也只能完整的横向拓展节点,所有模块资源强绑定;
  • 因为所有功能的实现都混合在一起,一旦某个模块的代码存在漏洞(如内存泄漏),会导致整个系统不可用;
  • 当Bug修复需要更新,或功能升级时,只能提前发布更新公告,然后等停机后再发布,极度影响用户体验感;
  • 尽管能靠F5、Nginx等负载器拓展更多集群节点提升系统吞吐量,可对大型门户/C端系统而言还是不够;
  • 单体要求技术高度统一,无法使用多语言异构,引入任何组件/框架都得考虑兼容性,一旦冲突就无法运行;
  • ……

对于复杂的大型系统来说,上述都是一直存在的痛点,而分布式架构则能完美解决这些问题。不过在一开始,分布式生态并不算成熟,许多饱受单体之苦的大企业,都开启了分布式自研之路,可这种自研模式带来的技术成本过高,许多中小企业并不具备这个能力。直到微服务的概念逐步落地,国内外著名大厂将业务脱敏后的自研框架开源后,进一步促就了分布式/微服务架构的繁荣生态。

Dubbo、SpringCloud-Netflix、Cloud-Alibaba……这些饱经生产考验的优秀框架开源后,再加上容器化时代的到来,越来越多的企业转投分布式/微服务的怀抱,不管项目规模大小,新启动的项目80~90%都用微服务构建;早期基于单体架构研发的老系统,也开始逐步计划重构成微服务架构,于是就这样,一股“逃离大单体”的风潮就此兴起了!

为了设计出高质量、高性能的微服务架构,业界给出许多“标准化”的设计原则,我们一起先来看看:

SRP单一职责原则。每个微服务应该只负责一个明确的业务功能,而不是多个功能。这有助于保持服务的内聚性,并减少服务之间的耦合。这样做能确保每个服务都专注于一个明确的业务领域,从而使其更加简单、可维护和可扩展。

②隔离性原则。微服务应该被设计为独立的进程,具有自己的数据库和其他资源。这可以确保一个服务的故障不会影响其他服务的正常运行。这样做能提高系统的稳定性和可靠性,减少故障传播的风险。

③可伸缩性原则。微服务应该能够在需要时水平扩展,以满足高负载和流量的需求。这意味着微服务应该被设计为可重复部署的、自治的服务单元。这样做能确保系统能够灵活地应对负载变化,提高系统的可扩展性和可用性。

④可替换性原则。微服务应该被设计为可替换的。这意味着当一个服务不再满足需求时,应该能够轻松地将其替换为一个新的服务。这样做能提高系统的灵活性和可维护性,允许在不影响整个系统稳定性的情况下进行服务的升级和替换。

⑤可重用性原则。微服务应该被设计为可重用的,这样其他服务或系统可以通过API访问它们。这有助于避免重复的代码和功能。这样做的好处是促进服务的共享和复用,减少重复开发和维护的成本。

⑥容错性原则。微服务应该被设计为容错的,以便在系统的某个部分失败时,整个系统可以继续正常运行。这意味着微服务应该有备用的服务或机制来处理故障。这样做的优势在于:提高系统的可靠性和稳定性,确保在部分服务出现故障时,系统仍然能够继续提供服务。

OCP开放/封闭原则。微服务应该对扩展开放,对修改封闭。这意味着在需要添加新功能时,应该通过添加新服务来实现,而不是修改现有的服务。这样做的好处是:保持系统的稳定性和可维护性,减少因修改现有服务而引入的潜在问题。

SAP服务自治性原则。每个微服务都应该是自治的,即它应该包含其自己的数据和业务逻辑,而不依赖于其他服务。这样能提高服务的独立性和可维护性,减少服务之间的依赖和耦合。

SIP微服务独立性原则。每个微服务应该是独立的,既不应该共享数据库或其他资源。相反,它们应该通过定义接口来进行通信。这样能确保服务的独立性和可扩展性,减少因共享资源而引入的潜在问题。

SOP微服务可观察性原则。每个微服务都应该是可观察的,即可以通过监控、日志记录和指标来追踪其性能和健康状况。这样做的好处是:提高系统的可维护性和可管理性,帮助开发人员和运维人员及时发现和解决问题。

SDRP微服务部署可重复性原则。每个微服务都应该可以重复地进行部署,以确保系统的可靠性和稳定性。这样的好处是:提高系统的可部署性和可维护性,确保在部署过程中不会引入新的问题。

微服务架构设计原则旨在提高系统的可维护性、可扩展性、可靠性和稳定性,除上述列出的内容外,业界还提出了许多其他理论,如AKF、BFF、DDD……原则,不过这里就不做过多展开。因为这些提到的设计原则,乍一听觉得头头是道,可却有点过于理想化了,毕竟实际项目会更加错综复杂,很难将提到的这些完全落地。为此,大家在设计微服务系统时,这些原则可以作为适当参考的准则,但不能奉为必须落实的真理

二、饱经摧残后的感慨

对于我来说,从接触微服务到现在,做过、见过许许多多的微服务项目,有服务规模达到数百个之巨的大型系统,也有用户体量寥寥数万的微小业务,其中不乏有几段令我感到十分荒谬的经历,而这些光怪陆离的现象,也是当下大部分微服务项目的常态,下面一起来看看。

2.1、服务过度拆分带来的痛

有一次在开发某个定制需求时,需要对接其他服务的开放能力(接口、消息等),经过多方评估后确定了方案的可行性,为了方便后续推进就拉了个对接沟通群。可是在跟着敲定的方案落地研发过程中,发生了一件十分抽象的事情:

  • 小竹:@xx,哥,请问这个字段怎么传值呢?文档描述的不是太清晰。
  • 解决方案专家-A:这个接口是支付侧开放出来的,稍等我拉一下对应的技术同学进来哈。
    • 解决方案专家-A邀请了“支付侧-B”进入群聊
  • 支付侧-B:抱歉,文档上写漏了,我这边马上补齐一下枚举值……
  • 小竹:@交易组-B,大佬,这个字段是啥意思呢?文档好像没说明。
  • 支付侧-B:这个字段是上游透传的,具体作用你得咨询交易侧的小伙伴~
    • 支付侧-B邀请了“交易侧-C”进入群聊
  • 交易侧-C:这个字段是xxx作用哈,在xx情况下会做传递。
  • 小竹:hello,请问这个接口为啥调用没反应呀?
    • 解决方案专家-A邀请了“技术中台-D”进入群聊
  • 技术中台-D:这个接口需要提交工单申请开白哦。
  • 小竹:好的,工单已提交,麻烦审核下~
  • 小竹:为啥正常部署后连不上xxx呀?有哪位大哥帮忙看看不[截图]?
    • 项目经理-X邀请了“基础设施组-E”进入群聊
  • 小竹:请问链路走到MQ之后为啥断了?有根据要求传递链路ID等相关信息。
    • 项目经理-X邀请了“中间件团队-G”进入群聊
  • 小竹:请问……
    • “XXX”邀请了“店群业务侧-H”加入群聊
  • 小竹:请问……
    • “XXX”邀请了“消息中心-J”加入群聊
    • “XXX”邀请了“营销中心-K”加入群聊
    • “XXX”邀请了“分销业务-M”加入群聊
    • “XXX”邀请了“搜索中台-O”加入群聊
    • “XXX”邀请了“商品技术支持-N”加入群聊
    • “XXX”邀请了“进销存业务-S”加入群聊
  • …………

每个微服务都由一个专门的团队维护,大家都围绕着漂亮的、向后兼容的API开展工作,对接其他服务时,就好像这个微服务是由一个第三方供应商维护的一样,这是理想化的微服务自治状态。

可真正对接时,就正如同上述提到的一样,群里一开始只有十多个人,在实现业务需求的过程中,会不断拉入其他团队成员,最后群成员数增加到近百……,群聊中充斥着来自不同团队的消息,这些消息涉及发布、错误、配置更新、破坏性更改等。在这种环境下,本来一个半天能做完的小需求,几个团队协调沟通下来,硬生生得两三天才能捣鼓好。

2.2、盲目跟风微服务之殇

平均日活寥寥三位数,用户总体量也才数万,可是技术Leader大手一挥,分布式/微服务技术哐哐往架构图上画,最终拆分出十多个服务,可是整个开发团队的后端只有几个人,一个人就要负责好几个服务。更离谱的是,光产品经理设计的一个列表页面,甚至都要横跨数个服务才能得到数据……

其次,有时想要用到某个功能/逻辑时,回想起自己好像写过了,结果一翻代码,却发现在另外一个服务里,自己调用自己写的代码,还需要封装一层远程接口才行。这时,所谓的服务自治好像成了笑话,所谓的自治,变成了自己治自己~。除开这些与业务有关的问题外,你还会发现,有时代码突然就跑不起来了、服务之间貌似调用不了了、保障并发安全的锁也失效了、中间报错导致数据不一致了……,为了解决这一系列分布式衍生出来的复杂问题,深夜的你又熬掉了一把头发。

熬啊熬,好不容易熬到项目上线时,拆好的十多个服务,加上一些例如注册中心、配置中心、数据库、中间件等基础组件,要部署到线上的节点数接近二十个,项目在线上运营一年,甚至连运维成本都没赚回来。

2.4、跟风微服务背后的真正原因

上述提到的两段经历,相信绝大多数小伙伴都深有体会,一开始使用微服务架构,是奔着更灵活的拓展性、更松散的耦合性、不同业务的自治性等目标去的。可到头来却发现,尽管微服务带来了不少好处,但是需要解决的问题同样不少,如并发安全、链路追踪、服务治理、数据一致性、故障监控等等,这些问题往往要投入大量精力去处理。

除开要花费大量心思去解决非业务性的技术问题外,微服务还会让系统变得更复杂,以及带来更高的运维成本、沟通成本,尤其是当一个需求要基于多个服务实现、且维护各服务的团队相互独立自治,你就会发现沟通成本反而大大超出开发成本,因为你需要去协调各个团队排期支持,这样才能保证需求能正常推进。

分布式/微服务带来的这一系列技术问题、现实问题,放在单体架构里并不存在,那你为什么要上微服务而不用单体架构?也许有人会搬出广为流传的那套观念,如支持异构、边界明确服务自治、更灵活的拓展性……。可除了那些人多、钱多、规模大的项目外,90%中小型项目在这些观点上站不住脚,那么你规模不大的项目为啥用微服务?

  • 为啥用?不知道啊,看外面都在用,领导和同事也说要用,就用了啊……

在刨根问底下,这个答案或许是大多数人内心的想法。所以,其实微服务并不适合所有类型项目,尤其是体量不算大的业务场景,大部分企业选择跟风微服务,背后真正的原因很现实:

因为微服务系统画出来的技术架构图格外高大上,领导们看了拍手称赞,还能在汇报的PPT上增添几笔亮彩;而老板/销售们在客户面前更好吹,甲方听看这么先进的技术名词和技术优势,就直拍大腿夸NB;最后,对于开发团队的马仔来说,既能通过生产项目来实践学到的新技术,还能为自己的简历留下一段宝贵的微服务项目经验、以及满足技术KPI的考核指标……

所以,用了微服务之后,领导的汇报更好看、老板/销售对外更好吹、客户觉得既厉害又靠谱、开发者也提升了个人职业发展的竞争力,四者各有所得,这种皆大欢喜的事情何乐而不为呢?

2.5、尽量避免分布式

作为系统架构设计的第一条原则,就是应该尽量避免使用分布式/微服务架构,Why?我记得在早些年,分布式架构是一种令人望而生畏的技术,也是一种在做架构设计时要尽量避免的方案,因为一旦使用分布式架构,就等价于走上了一条不归路,所需要面临的技术挑战可以说是无穷无尽。为此,分布式架构在一开始,通常是作为迫不得已的情况下,才会使用的最终手段

到了如今,随着各种技术组件、解决方案纷涌而至,分布式/微服务生态进一步成熟,分布式/微服务不再令人恐惧,反而成为了系统架构设计的首选方案。毋庸置疑,分布式/微服务能给系统带来很大的优势,但它并不是一种能解决所有系统架构问题的银弹,它有自己的适用场景,如果系统规模大、项目成员多、硬件资源够、工期时间足,那微服务架构的确是个不二选择

如果用户体量并不大,并且系统的流量峰值并不高,这种中小型项目完全没有必要使用分布式架构!对于中小型系统而言,解决分布式架构产生的一系列弊端,所耗费的代价会更大,这时硬上分布式反而得不偿失。并且单体架构足以支撑大多数中小型系统正常运转,而且不再需要耗费大量精力,去解决哪些根本不必要的技术问题(分布式带来的问题)

有人或许会说,那万一单体架构扛不住怎么办?如果你也有这样的顾虑,只能说你对技术理解不够透彻,单体架构最大的问题是耦合度高,不同业务之间无法单独维护(如升级、拓展等)。而所谓的单点故障可以通过集群解决,吞吐量不够可以升级硬件配置,扛不住就先把机器拉到16C32G,同时多加几台机子即可。

毕竟大多数情况下,业务流量大和项目收益挂钩,业务请求能把系统打到宕机,自然也不会差钱,无脑加机器比啥都好使,而且要铭记:单体架构的接口性能与响应速度,反而比分布式系统快上一个量级。这里分享个亲身经历,之前用单体构建的一个系统,接口平均响应在30~60ms,后面重构成分布式后,平均响应反而变成了50~100ms左右,为啥?因为系统拆分后,不同业务需要走网络来进行交互,而单体里直接调用方法即可,谁快谁慢一想就能明白。

如果你想说,设计时要考虑未来业务增长性呀,万一系统上线后,短期内变成“现象级应用”爆火了咋办?实在要面向未来设计的话,除非这个“未来”是绝对会出现的(如繁荣生态圈的新产品),否则也没必要一开始就上分布式,利用好Maven多模块构建单体项目即可,根据业务划分好模块,这时就算真的出现直线式增长,也能以极小的代价升级到微服务架构

综上所述,做架构设计时能用单体就用单体,这样能避免许多麻烦出现,千万不要乱用、滥用分布式架构,比单体系统更糟糕的是错误的分布式系统。这也是如今许多企业开始回归大单体的原因,越来越多的企业选择将系统重构成单体架构,降低复杂度是一方面,同时单体系统的成本更低,系统扛不住时多加几台机器的钱,也远低于分布式系统的部署成本。

三、分布式系统设计的原则

好了,我们该怎样根据业务背景来设计好整个系统呢?究竟该如何才能用好微服务?身为受害者的我饱经摧残后,结合过往接触分布式/微服务技术的经验,下面给出大家一些架构设计、日常开发的建议,虽然不一定能包罗万象,但绝对都是日常实践一点点积累下来的血汗经验,希望能给大家带来一定程度上的帮助及反思。

3.1、完善的基建设施

如果打算使用分布式架构,首先要考虑并不是如何拆分服务、如何开发需求与设计接口,而是应该先为分布式系统做好全面的基础设施建设,因为这是分布式/微服务系统所需要的“温室”,通常所需的基建设施如下:

  • ①虚拟容器与编排平台:通过虚拟化容器结合容器编排技术,为分布式系统提供更好的运行环境;
  • ②CI/CD流水线:从代码拉取、扫描、编译构建,到打包、单元测试、应用部署的自动化流程;
  • ③服务治理组件:注册中心、配置中心、负载均衡器、服务网关、远程调用、服务保护等核心组件;
  • ④日志收集与链路追踪:自动聚合系统所有节点产生的日志,并通过链路追踪搭建完善的可观测性;
  • ⑤监控系统:能持续观测硬件资源、系统流量、系统应用、中间件、业务指标的全方位监控体系;
  • ⑥数据存储组件:通过关系型/文档型/时序数据库、大数据组件、对象存储等提供多样化存储;
  • ⑦中间件:分布式缓存、消息中间件、搜索引擎、分布式调度中心、数据同步组件、负载均衡等;
  • ⑧授权中心:提供不同类型、维度、终端的认证鉴权服务,为系统提供单点登录、访问控制等支持;
  • ⑨……

上面列了八大项微服务系统不可或缺的基础设施,这些基建是构建分布式系统的基石,例如提到的第一项,可以提高可移植性、实现资源隔离、快速部署和扩展、版本控制、弹性伸缩、服务发现和治理、高级发布、故障恢复和自我修复等能力,而这是传统部署模式无法具备的能力。又比如上述第二项,可以实现近乎全自动化的发版流程,避免逐个服务手动编译、打包、上传、部署……

如果你喜欢蝴蝶,那你应该去养花,待到春暖花开之时,蝴蝶自来……

正如这句话所说的一样,如果你想要设计一个分布式系统,那你不应该先关注系统本身,这是我刚开始接触分布式/微服务吃的大亏,一上来就先对业务拆拆拆,然后闷头干干干,到头来走哪儿卡哪儿,每次都等问题暴露了再临时抱佛脚般做补救工作……。因此,大家首先更应该把精力放在搭建“温室”上面,当这些基础设施完善后,再构建分布式/微服务系统时会额外轻松。

PS:如果刚开始接触微服务生态,短时间内无法完善这些基建设施也没关系,但前期一定要先确认好方案,不要等到问题暴露出来了,才发现设计时没有考虑这块,然后做亡羊补牢的工作,这样会严重耽搁项目进展……

3.2、服务拆分粒度

接着来聊大家感兴趣的话题,即:微服务里的各个服务究竟怎么拆分才合适呢?根据业内标准的设计原则,每个微服务应该只专注于完成一个特定的业务功能,从而确保服务的高内聚低耦合,可这样真的合适吗?

系统的拆分通常围绕着业务域展开,比如xx业务,可以抽象成一个xx服务,也可以细化出x1、x2、x3、x4服务,两者的区别在哪儿?其实很明显,这就类似于单体与分布式之间的关系,回想单体架构的缺陷:

技术与业务的边界模糊,A业务依赖B业务时,A业务的开发者能直接去改B业务的代码逻辑。同时,业务模块之间耦合度过高,某块业务需要扩容,必须将整个系统一起拓展;某个业务的代码有问题,如内存泄漏,这种风险会被传播至整个系统……

同理,如果将服务拆分的越细致,越便于精细化运维,对整个系统能做到更精准的掌控度。因为整个系统的流量就跟社会上的财富一样,不可能均匀分摊到不同服务里,当你把服务拆分的越精细,面对并发场景时,就可以根据不同的请求规模,规划不同的节点数与硬件配置,从而将资源利用最大化。并且拆分出的不同服务,可以根据业务特性,选用最合适的技术框架、组件、语言来进行实现。

这样一听,好像确实拆的越细越好啊,但实际上并非越微越好,就以前面提到的精细化资源利用来说,现在将一个服务细分成了四个服务,假设每个服务的基础为2C4G,那么目前至少需要四倍的资源才能保障功能的完整性,在这种情况下,精细化运维省下的资源又在这里还了回去。而本地开发则会更加痛苦,有时产品画的一个列表页面,或许就得从五六个服务聚合数据;有时想本地调试一个接口,可能得启一二十个服务,电脑配置稍微差点都会干到黑屏……

综上,服务拆分时要结合业务特性、流量规模、团队现状等多方因素考虑,应该遵循“适当微”原则。从更深层次的角度思考,系统的拆分实际关乎着企业组织架构的调整!毕竟不同服务最终会交给不同团队负责,而微服务又讲究服务自治,但作为同一系统里的微服务,服务间都有着藕断丝连的关系,没有任何一个服务能做到独善其身。于是,出于微服务的自治性,不同团队间只会树立起一道又一道的部门墙,拆分出的服务数量越多,需求推进越难,沟通成本也会越高!

PS:如果应用比较复杂,或者涉及到多语言异构,可以考虑划分多个不同的API网关来处理,一方面可以做协议与数据格式转换,另一方面还能针对不同终端的流量进行定制化处理。

3.3、服务分层

确认好合适的拆分粒度后,还要做好服务的分层设计,学会将业务服务里的共性操作,抽象成单独的、可复用的基层服务。比如A服务需要发送企微/钉钉通知、B服务需要发送微信/小程序通知、C服务需要发送邮件/短信、D服务需要推送APP应用通知……,这些分散在不同服务里的需求,其实具备相同的技术特性:发送消息,唯一的区别就在于触达渠道与方式不同。这时,就可以拆分出一个独立的消息中心/服务,负责对接不同渠道的消息推送能力,而后提供给前台业务服务统一接入,避免多个服务重复开发。

与之相同的还有很多,如支付服务、认证授权中心、用户服务、营销中心、数据中心等等,把这些复用性强的服务作为系统基层服务后,能快速支撑前台的业务服务开展,这也类似早些年互联网比较流行的“大中台、小前台”架构,两者相同的特点在于:都是为了整合技术资源去为业务提供更好的服务

到了微服务架构中,服务通常会被分为基础服务、业务服务两大类,通过这种分层设计思想能将系统模块化,既能提升业务开发效率,也能增强可维护性及拓展性。不过要注意,基础服务是多数业务服务中的技术沉淀,不同领域下沉的基础服务亦不相同,比如支付功能在电商领域中,团购、直播、在线商城、门店、抢购等前台业务都会用到,所以我们能将其下沉到基础服务层。但如果你换到OA领域,支付功能还属于可复用的共性功能嘛?答案显而易见。

如何评判一个服务是否要下沉到基础服务层,这点因不同系统的需求而异,但有一点诸位要铭记:作为基础服务层的应用,通常会被整个系统几十上百个服务接入,因此稳定性是重中之重。一个业务服务有些小Bug无伤大雅,可如果某个基础服务存在问题,一旦发生故障会波及整个系统,这也是分布式微服务的一大特点:一个应用所处的层级越靠下,它对整个系统的影响越广泛

例如上回的阿里云崩溃事件,由于偏底层的AK鉴权组件异常,不仅阿里内部应用接近全系应用崩溃,而且基于阿里云部署的无数大小项目,或多或少都受到了一定程度的影响……

3.4、无状态设计

分布式/微服务架构的核心优势之一,就是运行期间能够弹性伸缩节点配置,比如目前业务发展迅猛,业务流量日益激增,作为核心服务之一的订单服务,处理大量业务请求时逐渐变得吃力,此时就可以动态扩容节点数量,以此提升吞吐量来满足业务增长所需。

但是,一个服务想要支持弹性伸缩,前提是它必须被设计成无状态服务,啥叫无状态服务?

  • 有状态服务:在处理请求时,会依赖和修改特定的状态信息,如存储在服务内存中的数据;
  • 无状态服务:在处理请求时,只依赖于请求自身携带的信息,不依赖任何请求之外的状态信息;

这样看有点难理解,下面举个具体例子,比如你在A服务中,通过ScheduledThreadPoolExecutor、SpringTask这类定时调度技术,定时触发某项业务逻辑的执行,这就是一种有状态的表现,为什么?因为定时调度的状态,只存在于某个具体的服务实例中,一旦将单实例的服务扩容成多个节点,这时会发生什么?

A服务部署多个节点时,每个节点的内存中都有各自的调度器,最终多个节点会一起定时触发业务逻辑的执行,好比这个业务逻辑对应的具体操作,就是定时推送数据至三方,那此时会出现多次调度推送重复数据。

再举个更加通俗的例子,比如你在A服务里,把用户的登录态存放在服务器Session里,后续直接从Session中获取用户登录信息,这时当服务扩容成多个节点,会造成登录状态丢失。比如用户第一次请求分发到A1节点,此时将登录态放在A1Session(内存)中;而第二次请求被分发到A2节点,显然无法从A2Session里获取用户的登录信息。

结合上面两个例子,需要依赖先前请求状态、或依赖实例内存中的特定信息,来处理请求服务被称之为有状态服务,而有状态服务天生与分布式相悖,它无法支持集群式部署,这显然是分布式系统无法接受。正因如此,大家在做分布式系统开发时,除开特殊的服务外,业务服务一定要做好无状态设计

3.5、搭建容灾体系

世界上不存在完美无缺的东西,互联网也不存在不发生故障的产品,因此,容灾设计成了大型系统不可避免的问题。设计容灾方案时,不仅要考虑代码Bug、组件崩溃……这种内部故障,还要考虑外部环境因素造成的突发状况,如光缆被挖断、机房发生故障、洪水/火灾/地震等自然灾害等。

而分布式系统通常会部署在同一个内网,毕竟内网交互更快,并且没有公网带宽成本,但所有服务部署在同一个内网,遇到上述突发状况时系统会瞬间瘫痪。因此,为了大型系统的稳定性保障,容灾方案通常会考虑许多方面,如:

  • 数据备份:定时同步全量与增量数据,确保发生故障时能及时恢复;
  • 主从热备:通过主从或集群方案,解决单点故障带来的不可用问题;
  • 灾难告警:搭建完善的监控体系,能实时观测并反馈健康状态,达到阈值时自动通知相关人员;
  • 多机房灾备:全量冗余部署一套机房环境,当主用机房故障时能自动切流至备用机房;
  • 地理灾备:通过搭建异地多中心方案,确保某地发生自然灾害时,也能将流量切至其他中心;
  • 网络灾备:通过融合仓避免单一运营商故障导致网络不可用,及通过专线避免网络阻塞等问题;
  • 应用容灾:蓝绿发布确保升级时故障,能及时回滚到旧版本,金丝雀发布验证新功能稳定性;
  • 多云策略:基于多个云服务提供商,降低单一云服务商故障导致的系统不可用风险;
  • ……

容灾的目的是为了保障高可用,毕竟对于大型业务系统来说,短短几分钟的瘫痪会带来巨大损失,而且会对整个品牌造成负面影响,给用户/客户留下较差的观感,这种损失更是不可估量的!所以,越大型的系统越重视容灾设计,预设的灾备方案也会更加充分。但如果是中小型系统没必要面面俱到,因为容灾等于烧钱99%的时间里,准备的容灾资源都会处于闲置状态。

3.6、约定大于一切

前面聊的五点内容都在围绕架构设计展开,下面来说几点开发分布式/微服务项目时的建议。

相信大家在学习分布式/微服务时,一定听过一句话:约定大于配置,配置大于编码,前半句的意思是,在微服务开发过程中,大家都遵循既定的约定来实现系统功能,比如:

  • 包结构的约束:所有业务逻辑类放service包、控制器放controller包、数据访问类放mapper包等;
  • 接口风格的约束:所有接口都遵循RESTful+前后端分离风格,如POST对应保存、PUT对应修改等;

以接口风格的约束为例,假设现在团队中,大部分人按RESTful+前后端分离风格在定义接口,但极个别人偏偏要用ModelAndView去跳转页面并附带数据,那这意味着需要为这少数人单独配置视图解析器……。反之,如果所有人都遵循既定的约束,就能避免每次进行繁琐的配置。

后半句配置大于编码的含义,是指通过外部配置来管理和调整系统行为,而不是在代码中硬编码,这样能使系统更加灵活,可维护性更高!举个例子,比如通过配置文件来管理数据库连接信息项,而不是以硬编码形式写死在代码中~

好了,上面这句话是做微服务开发的第一准则,但对于“约定”这个词汇,在这里会有更广泛的解读。所谓的约定,是指双方或多方都会遵循的准则,在实际的微服务开发中,约定将大于一切

首先,在正式开始开发前,应当先给出统一的技术规范,这份技术规范包括但不限于:编码规范、数据库规范、安全规范、性能规范、Git规范、项目管理规范……,而这份技术规范将是整个团队所有成员都要遵循的约定,这样才能确保团队工作期的高度统一,否则会出现各种鸡毛蒜皮的小事相互扯皮。

其次,“约定”还应该包括公共类库!这点虽然不起眼,但却格外重要,先来看个例子:

背景:开发过程中各类Entity、BO、VO、DTO……对象的转换。
实际情况:有的人用Spring.BeanUtils转换、有的人用Apache.BeanUtils、有的人用cglib.BeanCopier、有的人手动Get/Setings、有的人用MapStruct、有的人用orika.MapperFacade,甚至有人先通过Json序列化,再反序列化回来……

这个例子整体看下来,可谓是八仙过海各显神通,光常见的Bean拷贝操作,不同成员的风格都不同,更别提值校验、报表导出等共性操作了,这种不同类库的交织在一起产生的代码,会给阅读者带来很强的割裂感,以及增加代码阅读成本,正因如此,对于常用的公共类库理当统一。

有人或许会说,微服务不是倡导服务自治吗?我用个不同的类库实现需求还不行啊?理论上可以,但除开极少数规范管理的项目外,很少有项目能做到每个服务完全自治。更多的情况是,一个人会在多个服务之间来回蹿,毕竟大部分接口都会涉及多个服务的交互,所有服务使用相同的公共类库,能在最大程度上保障代码风格统一,也能降低代码调试难度,以及代码交接的成本。

好了,最后再来说一点,“约定”可以被理解成契约精神,在实际的微服务开发中,经常会面临需求滞后问题,比如你需要依赖X服务的接口,但该服务对应的团队将优先级排的很低,这时你就只能阻塞等待。往往在这种情况下,就只能申请PM或Leader协调资源进行推进,这无疑又是个相互扯皮的过程。

正因如此,微服务开发中较好的策略就是“接口先行,语言中立,通过契约驱动开发”。说简单点,无论依赖多少个服务,在面临一个需求时,都应该先把所需的接口识别及定义出来,然后基于已经定义的接口结构Mock数据,多个团队之间并行开发,从而在最大程度上提升交付能效。

3.7、面向失败设计

做微服务开发最重要的一点已经在上面讲明白了,下面来看开发中容易犯的一个错误,无保留的信任外部接口,这是许多人都会存在的通病,因为以单体开发的经验而言,需要依赖其他模块的功能,直接调用对应的方法即可。

因为所有代码都部署在一个进程里,此时调用其他模块的代码必定不可能失败,然而,在分布式系统中,所有外部资源都是不可靠的,每一步必须要设置兜底方案!因为服务之间交互依靠网络通信,就算所有服务都部署在同一个内网,对于服务调用方而言,也有可能出现失败的情况,比如:对端服务故障、网络出现分区、接口响应超时、对端接口熔断降级等。

面对上述问题,如果像信任本地方法那般信任远程接口,比如远程调用其他服务的接口获取数据后,不做任何校验就直接使用,假设接口超时导致无数据返回,就会引发空指针异常。又或者调用了其他服务的写接口,自身写入成功但对端服务失败,最终导致系统数据不一致……

正如提到的两个示例一般,如果不对依赖的外部资源做容错设计,就会使得你的代码存在诸多潜在风险,因此,在做微服务开发时一定要面向失败设计,对于不同类型的外部资源,加入不同的容错方案,如结果校验、回滚机制、重试补偿、异常捕获等,但这里不做过展开,后续会开单独的章节讲述《分布式接口设计》。

3.8、摆脱单体开发思维

除了遵循契约开发、面向失败设计外,在做微服务开发时还有至关重要的一点,摆脱传统的单体开发思维

《漫谈分布式专栏》的开篇,我曾说过一段话:

很多人做过分布式项目,但却不具备分布式经验,为啥?对大多数人来说,无非是换了一个环境继续写CURD,唯一的区别可能是,之前调其他模块的方法是直接注入Bean调用本地方法,现在包了一层远程调用~

上述这段话可能有点难听,但却是一个不争的事实,因为许多人依旧沿用着单体开发思维在做分布式开发,举个例子说明:

现在要统计不同店铺、每天各个支付渠道的支付总金额,列表支持店铺、日期、支付渠道筛选及汇总。

上面是一个简单的单日汇总对账单需求,如果是你来负责,你会怎么实现?

  • ①出一个对账单接口,通过SQL语句实时查询订单表统计对账数据,这种方式存在性能问题;
  • ②改造支付链路,在支付成功后加个统计逻辑,数据存入对账表,可这种方式对现有代码侵入性高;
  • ③写个定时任务,定期扫描订单表,把支付成功的订单统计到对账表,但这种方式不具备实时性。

如果你想到的也是上述三种方案之一,那说明你对分布式理解还不够熟练,具体怎么做呢?所有分布式电商系统里,订单支付成功都会发送MQ消息,那咱们能否订阅这个消息来完成对账统计呢?答案是当然可以,即对业务代码没有侵入,也不会有性能问题,并且能够有接近实时的对账数据,三者皆得!

这个例子只是起到抛砖引玉的作用,实际开发过程中,要学会合理使用不同中间件处理业务,比如有些业务通过Redis就能实现,没有必要非得通过数据库完成,这样还能获得更好的性能,担心数据丢失做个异步落库即可。又比如某些检索条件复杂、数据量过大的场景,可以清洗数据放入ES来优化性能与体验感。又比如某些业务场景下,完全可以通过多线程、MQ异步处理提升性能与解耦……

熟练使用不同中间件满足业务场景后,还需要考虑分布式下的各种极端问题,例如写入链路从中间断开,如何保障数据的一致性?又比如某个接口出现并发请求,如何避免线程安全问题?又比如请求链路过长,如何降低耦合度及优化性能等等。实现业务需求时,养成潜意识下思考这些问题的习惯,这将是你玩转分布式领域的必经之路!

四、分布式/微服务系统设计总结

前面的内容中,先以标准的分布式/微服务设计原则为引子,逐渐讲到了微服务架构的潮起潮落,然后分享了几段痛苦的微服务经历,最后对如何在实践中用好微服务给出了些许建议。看到这里,也代表本篇内容走进了尾声,

微服务很好,但它并非银弹,在技术演进的过程中,微服务架构无疑带来了创新和灵活性,但也伴随着挑战和复杂性。从最初追求的独立自治和可伸缩性,到后来的实施困难和性能问题,想用好微服务需要在实践中不断总结经验教训。现如今,许多企业选择回归单体架构并非倒退,而是一种在实际业务需求和技术可行性之间取得平衡的选择。

技术架构是由业务发展所处的时期和阶段决定的,要能够解决业务发展过程中的痛点。在进行架构选型时,综合考虑这个架构是否能满足当前业务的需求即可。水满则溢,月满则亏,设计技术架构时,考虑能否满足未来的业务发展空间是没错,可没有必要一开始就上终极方案,否则技术复杂度超出业务几个维度,反而会给整个团队带来更多的麻烦。我们要做的,就是满足当下,在未来技术发展、需求变化时,做到灵活调整架构策略,更好地为业务提供服务即可。