回头看微服务

一、序言

在我刚刚工作的年月,人们还在热烈地讨论SOA,Web Service仍旧大行其道,在2015年前后,突然“微服务”架构概念突然爆火,当时我对SOA还充满热情,业界也在广泛讨论“微服务”究竟是什么。让我比较认同的观点是:一套轻量化的SOA最佳实践。后来,随着Spring Boot的崛起,带领着Spring Cloud这一JAVA领域下最通用的“微服务”解决方案,伴随着微服务“模块化、可插拔、独立自治”的宣传口号,“微服务架构”开始大放异彩。我接触过几个背景迥异的团队,也见过大家对“微服务”不同认知和多样化的实践路线,也参与开发过内部“微服务”框架,对何时使用“微服务”、如何使用“微服务”以及如何看待“微服务”有了一些自己的认知,针对不同的团队如何开启“微服务之旅”有了一些自己的见解。

二、重看微服务

1.微服务的承诺做到了吗

没有!至少对于大多数团队而言没有。因为主观或客观的原因,我见过很多所谓的“微服务”架构,做成了实质的“分布式单体”架构,只是从过去的代码组织依赖转变为了服务间依赖。牵一发动全身的问题依旧广泛存在。

因为业务的复杂性,具备良好领域边界划分能力的架构师又很稀缺,甚至随着业务的变更,原先设计的领域模型也会随着业务的发展产生领域泄露,结合长期业务迭代、上线时间压力等考验,原有的边界会变得逐渐模糊,维护“低耦合、可插拔、独立自治”的成本会越来越高,再叠加优秀业务架构师和技术架构师的缺位,做到良好微服务架构的成本会高至难以承受。

2.微服务带来了哪些额外支出

随着微服务的全面开花,极易造成微服务的滥用,最典型的表现就是微服务划分颗粒度过细问题。

过细的服务划分导致独立应用数量快速上升,这对运维造成了极大压力,甚至为了解决这些问题,不得不增加更多的运维组件,这大大违背了“奥卡姆剃刀原则”。随着微服务的增多,按照微服务最佳实践,每个微服务都应该操作属于自己的数据库,这又极易引入分布式事务、数据一致性等问题。在实践中,你能明显感受到比传统单体应用多得多的运维成本。

同时,在开发过程、线上运行中,由于调用链跨应用及难以完全杜绝的接口不兼容升级问题,Debug成本明显比单体应用高出不少,毕竟引入了更复杂的调用机制,无法保证编译时安全的调用,切实增加了应用开发、维护成本。
至于这些问题我会在后续的文章中进行详述。

3.我为何依旧推荐微服务

我个人是十分推崇微服务的,也许和我的职业经历有很大关系,曾经就职于中大型的开发团队,这样的团队多人甚至多团队协作是日常工作模式,这样的协作模式下,微服务成为了最自然的选择。同时,管住程序员那双管不住的手在我看来也是微服务架构提供的极大的价值,我很看重这个。同时,微服务的整体设计思想非常强调系统弹性扩容能力,这在开发组件的支持上就有所体现,这能帮助开发人员写出可扩容的代码来。

在团队相关能力支撑到位的前提下,微服务带来的收益十分诱人,在多数情况下他能强制团队内的开发人员进行系统边界的认真思考和系统高内聚、低耦合的设计。而什么样的团队适合微服务?实践微服务时应该关注哪些关键点?应该如何促成微服务的健康落地和持续稳定?这些问题,值得更深入的思考。

三、微服务带来的挑战

1.领域边界划分的挑战

最近几年DDD又变得十分火热,DDD提出时间很早了,一直处于叫好不叫座的尴尬状态。其对开发团队内部成员综合要求较高、缺乏最佳实践都成了阻碍其发展的绊脚石。而随着微服务的兴起,原先单体架构下可以忍受的很多问题逐渐变得不可忍受起来。人们逐渐发现,从业务建模到编程模型的映射变得十分重要,原先的CRUD一把梭的模式已经很难在微服务架构体系下顺利工作。DDD最为强调的业务建模却能很好地帮助微服务落地,帮助微服务划定良好的系统边界。

可惜,能够顺利实践DDD的团队少之又少。意味着,能够很好地划定系统边界的团队也不会多。微服务边界划分不清是不可能带来架构稳定的系统的,随着系统的扩张,极大概率微服务架构最终演变成“分布式单体”应用,甚至某个微服务的独立部署上线都会成为一大挑战。系统间的耦合从应用内调用耦合(单进程组织)变为了应用间耦合(多进程组织),不仅增加了系统部署难度,更降低了系统整体性能和稳定性。

2.系统资源的浪费

这个问题在JAVA体系(尤其是Spring体系)中尤为明显,在微服务划分过细的情况下,大量的JAVA进程需要部署。每个JAVA进程启动后都需要几百MB的初始内存消耗,这对硬件资源本就不宽裕的团队而言就显得较为奢侈了。

在小团队,尤其是项目型团队,我有一个概念叫作“贫穷架构”,这样的团队组织里通常硬件资源极其紧张,要尽最大的努力减少硬件投入,小团队成本敏感,由不得你放肆地“用机器的时间换取开发人员的时间”这样的项目通常对应用性能要求不高,但是对硬件资源的给与是不会太多的。在这样的前提下,单机多进程微服务架构就很常见,而这样的架构体系下,多进程导致的系统资源浪费就会变得难以接受,而微服务一旦滥用,在这样的架构体系下危害也将变得更加突出。

3.开发成本和兼容性成本的上升

Spring Cloud体系下,API通常使用RestAPI,RestAPI鼓励不提供SDK,每一次对接都要自行实现相关接口调用。如果单纯实现接口调用本身工作量尚可接受,但是如果调用里还加入了身份验证、身份标识等机制,就显得不那么容易接受了。而如果提供了SDK,又会面临Spring Cloud/Spring Boot升级所带来的不兼容风险。我曾遭遇过两次这样的问题,至今心有余悸。开发过程中的接口对接、错误处理对接等相关工作带来的工作量都不能忽视,尤其和单体应用相比微服务的开发效率和可靠性更是难以望其项背。

在单体应用下,接口兼容性只需要重点关注对外暴露的接口即可。而在微服务架构下,所需要重点关注的兼容性就扩展到了内部模块,并且由于无法获得编译时安全检查,接口需要变更时所需要考虑的兼容性包袱会变得更加沉重。

4.Debug及运维成本的上升

单体应用开发过程中,如果遇到bug,过程跟踪十分便利,哪怕是第三方包,多数也都能进行很好地跟踪排查。但在微服务架构下,这一过程变得不那么透明,当你的跟踪到达服务边界后,就无法进行进一步跟踪了。尤其在引入了类似熔断工具后,还容易将跟踪路线带偏。经常在本地调试时需要同时启动多个微服务进程,也增加了开发环境配置难度,由于注册中心的引入,开发过程中不进行特殊配置很容易调用到非目标服务节点,这也降低了开发效率。

线上运维不得不加入更多的自动化处理工具来定位线上问题。因为链路更长、可能出现的故障点更多,所以需要加入链路跟踪工具;因为应用节点的增多,需要加入更完善、处理能力更强的日志收集、处理工具。为了顺利完成多节点、不同启动方式的应用发布,更完善的自动化发布工具将成为必选项。全应用系统的监控也将变得必不可少。

很多在单体应用下的可选工作在微服务架构下变为了必选,这样的建设投入在不同的公司、不同的项目体系下也带来了不同的成本负担。在大型团队里,这些基础运维设施就本就非常必要且完善,小公司、小系统在这些运维设施不够完善的条件下如果强行上马微服务,为了保证系统运行过程中的可靠性和可用性,系统维护成本将不得不有所上升。

5.数据一致性的挑战

在微服务的最佳实践中,通常建议每个微服务独享db实例,不建议单db实例同时服务于多个微服务,在需要共享数据时,尽量通过微服务接口进行数据交互。这在典型的事务型业务中是可以的,但是如果涉及到数据量较多、数据一致性要求较高的场景,这种模式的服务质量则很难令人满意。此时,采取数据冗余策略就成了自然而然的选择。结果“数据冗余一时爽,数据一致火葬场”。大量数据冗余在多个不同的数据库实例中,后期的数据维护、数据一致性、有效性管理所产生的成本将难以估量。

例如我见过的一个微服务拆分案例,多个微服务共同依赖用户中心微服务,因为存在较多的三方登录,某个微服务的开发者选择在自己的库实例中建立三方账号表,同时冗余了部分用户基础信息到自己的库中。后来,随着用户中心迭代升级,相关用户可用性、安全性策略进行了升级,而这些升级效果并没有应用到这个依赖服务中去,原因就是他们一直使用的就是自己维护的一套用户信息,并没有和用户中心微服务达成联动,导致了最终应用行为未达到预期。

随着微服务对数据的管理独立化,为了保证数据一致性,需要对服务调用者进行严格的调用流程审计、数据流审计,提升了开发管理成本。长期看来,面对纷繁复杂的数据集,数据治理也将面临更大的挑战。

四、微服务带来的收益

1.系统强制解耦,管住程序员那双管不住的手

在大型单体应用中,我最怕见到的问题就是各种“黑魔法”的使用,曾经见过一个模块化做得很好的系统,不知是开发人员偷懒还是对框架使用不够熟悉,模块间信息传递采用了大量全局“黑魔法”。Debug起来异常困难,你完全不知道他在什么时候,什么神秘的地点加上了神秘的数据,最终能让目标模块获取数据。甚至,为了达成目的,采用反射等编译时不安全的方法进行反模式方法调用。我对这一现象深恶痛绝。

采用了微服务以后,很多“黑魔法”被连根拔起。程序员那双管不住的手被很大程度地管了起来。强迫程序员进行模块化思考,必须认真考虑模块间的通信设计,这样能帮助程序模块间降低耦合,延长架构生命周期。

2.单一模块更低的代码复杂度

从单一服务的视角看,独立架构,独立演进这一特性十分吸引人。因为天生的隔离性,避免了历史代码所需要背负的历史包袱,从单个微服务的代码量看,代码量大大减少,维护难度降低。对于新进组的开发人员快速上手,快速进入角色具有很大的优势。

3.更有针对性的扩容方案

应用大量部署后,通常只会有很少几个模块成为应用热点。当大负载场景来临时,如果采用单体应用,扩容的性价比比不上微服务这样的细粒度扩容方案。有针对性的扩容,对于高负载应用场景具备极大的成本诱惑力。
同时,微服务的使用,也有意地提醒着开发人员时刻注意着后期的扩容问题,特意引导着程序员将程序设计成无状态、可横向扩容的模式。这对应用整体的负载能力提升具有很明显的正向的价值。

4.异构系统的支持

这是很多宣传微服务时的一个极具诱惑性的卖点,虽然我对这个观点一直保留态度,君不见Dubbo发展了那么多年,好好支持了几门语言?但这并不妨碍微服务架构确实能够达成这一目标。并且整个微服务技术栈的发展也正在向着这一目标快速迈进,随着Service Mesh相关技术栈的成熟,相信完全达成这一目标绝非难事。
回归当下,如果我们将异构应用当作一个外部应用看待,通过API Gateway进行服务调用,也能达到异构系统集成的目的。在微服务架构下,对外开放的接口天生比单体应用架构的接口要丰富,外部异构系统集成的可能性将变得更多。

五、如何使用微服务

1.我们是不是真的需要微服务?

这是一切问题的起点。结合我上述一系列的问题,先问自己,我是否真的需要微服务?如何思考,我建议你先回答以下几个问题:

  1. 我的后端开发团队是否已经超过了10人?
  2. 我的服务边界是否已经能够明确划分?
  3. 我的服务器资源是否足够使用?尤其是内存资源。
  4. 我的运维资源是否能够有效应对即将到来的挑战?尤其是自动化运维能力,是否达标。
  5. 我是否有足够的技术积累来应对开发集成过程中诸多的问题?
  6. 是否有技术和业务能力均过硬的技术领导能够统领系统架构的演化?

如果您的答案依旧摸棱两可,那最后一个问题:如果不采用微服务架构,你会遇到多数令你难以忍受的问题?

并且,在我上面所提到的微服务所带来的好处里,对于绝大多数中小型团队而言,并没有任何一条是必须得用微服务才能达成的。对于最吸引我的“模块化”其实大多数编程语言已经做了很多努力。例如JAVA的模块、C#的类库,都已经能实现很多隔离特性,你所需要做的,仅需要更注重编码规范即可。

2.针对业务特点的服务划分是重点

如果您已经做出抉择,决定使用微服务搭建您的系统,那么我不得不提醒您“服务划分”是重中之重。在诸多微服务架构失败的案例中,在导致失败的技术原因统计里,服务边界划分失误导致的失败一定首当其冲。在架构搭建的初期,服务边界划分相对容易,但也需要领域经验丰富的工程师进行完整的业务梳理后进行相应的业务划分。随着系统复杂度的提升,微服务架构也需要跟随系统共同升级演化。我坚信“优秀的架构是演化出来的而不是设计出来的”业务复杂度总会超过技术复杂度,深入业务流程、参与业务规划是团队Leader/架构师不可遗漏的工作任务。

如果您的团队里找不到具备相应经验的技术领导者,依旧因为种种原因需要上马微服务架构,那么我强烈建议最大限度扩大单个微服务的业务领域范围,尽量不要做盲目拆分,不过可以在应用内使用一些现有的模块化技术进行模块代码拆分,为后期可能存在的服务拆分做一些准备。

3.完善的运维体系作为支撑

微服务架构下,从关注单一应用节点变为了关注多个应用节点,每个应用节点都有可能成为故障点。对自动化运维体系的要求变得高了很多,连某个微服务未自动清理的日志都有可能成为压垮整个系统的故障点。快速排错,快速定位问题变得比过去更具挑战。对每个服务的运行状况,对每个宿主节点的各项硬件指标的监控、报警、自动化处置也是必不可少的关键工作内容。

同时,微服务的调用链需要跨应用甚至跨服务器,如何把整条调用链信息进行整合、可视化快速跟踪故障,也是不可忽略的系统运维要求。

4.放弃不切实际的期望

正如我第一部分所说,大多数微服务架构并没有做到它所宣传的种种诱人的特性,尤其在中小团队开发中,甚至业务本身就不具备能够很好利用微服务特性的客观条件。我们怎么能够期望使用了微服务以后我们的整个应用架构就能如它所宣传的那样美好?

“没有银弹”这是世界运行的真理,不存在任何一种技术能够适合所有场景,能够解决所有问题。“复杂性不会凭空产生,也不会凭空消失,只会从一种形式转换为另一种形式。”我们要相信技术的发展所带来的红利,但也要客观看待每种技术自身的局限性和适用场景的局限性。打破不切实际的幻想,用更脚踏实地的视角看待软件工程技术的发展,我们一直在路上。

我的微信公众号
我的公众号