模块分解:微服务架构设计的关键决策

如今,越来越多的公司采用微服务架构,微服务也在许多组织中发挥着重要作用,但并非所有组织都适合采用微服务,它有自己的适用场景。在这里,想通过我的个人经历聊聊我对微服务架构的思考与理解,希望对你有所启发。
大概从5年前开始实践微服务,那时 Spring Cloud 才刚到1.4,我们算是国内较早把 Spring Cloud 应用于生产环境的团队。当时选择实践微服务架构主要基于两点原因,其一是团队成员在多个地区办公,甚至还有部分同学在家办公,且各个同学精通的技术栈也有所不同;第二个原因是项目本身就需要对接外部系统,且对接语言只能使用C++,还有部分底层系统选择使用Go语言开发。
正是基于这两点原因,我们开始尝试微服务架构,到今天为止,这套系统为公司创造营收接近两个亿,开发迭代上百个版本,稳定运行四年多。今天来看,项目的整体架构设计算不上优秀,代码也有点惨不忍睹,但不妨碍它成为一个比较成功的商业系统。这一路走来,可以说,大部分中小团队在实践微服务上踩的坑,我们都踩过。
接下来,主要会从四个维度,聊聊我们踩过的坑。
基础设施维度
在考虑采用微服务之前,需要在持续交付和基础设施自动化实践等方面达到一定的成熟度。虽然不用太高,但至少得有,而我们也只能算是“有”一点。我们使用自建的 Gitlab 作为代码仓库,然后通过 Jenkins + Shell 脚本的方式把代码从仓库拉下来并构建好,发布到测试和QA环境。而生产环境的发布,则需要运维手动将QA环境构建好的可执行文件,发布到对应的生产服务器。而生产环境的日志则是通过脚本复制到对应的日志服务器上,方便开发排查问题。而对生产环境的监控与告警,也主要靠运维编写的 Shell 脚本。
虽然,整个交付过程的大部分操作可以使用脚本来处理,但离自动化还有很大的差距。而且监控系统的缺失,导致很多的线上问题只能靠客户反馈,完全不能主动预防。这还仅仅是发布和监控方面遇到的问题,其实整个开发过程中面临的挑战也不少。
由于团队成员在多地办公,开发过程中的联调测试也异常痛苦。要么把服务发布到专门的开发环境来调试,要么在本地把所依赖的服务全都启动起来,反正,整个过程的效率非常低。这基本上无解,只能尽量拆分服务,让两地间的调用尽可能少。而我们的解决方案就很粗暴,服务上搞不定,就弄人吧,最后把人搞一起办公了。
后面,随着阿里云推出的云数据库和监控产品不断成熟,我们也及时跟进,把项目所有数据都迁移到云MySQL和云MongoDB,不再使用自建的数据库,而且整个系统也进行了容器化,尽量屏蔽掉各个环境间的环境差异,方便运维自动发布,这样就给运维减轻了很大的压力。而且阿里云各种产品都自带监控,对大部分中小团队来说,基本够用。
因此,在实践微服务时,如果自身的基础设施成熟度不够,就尽量使用云服务吧,虽然有坑,但绝大部分时间还是靠谱的。
技术维度
使用微服务架构会出现很多单体应用完全不会有的问题。微服务本质上是分布式的,业务流程通常需要通过多个服务的交互来完成。在单体式应用中,业务流程通常在同一个服务内完成,可以通过传统的事务来保证所有的操作要么全部执行,要么全都不执行。但在微服务架构中,为了解决这些问题会引入大量的复杂度,不仅仅需要考虑分布式事务问题,还需要考虑服务的升降级、负载等等问题。所以,在考虑采用微服务架构时,一定要在微服务的灵活性与单体式应用的简单性之间进行权衡。
而我们的项目,最开始没有实现升降级、限流、熔断、分布式事务等常用的功能,我们默认所有的操作都可以按顺序执行成功,默认同一个服务的所有节点不可能同时挂掉…… 如果一个“事务”中有部分操作失败,会通过手动调用接口的方式来补偿;如果有服务挂掉,会自动触发运维的脚本重启程序,如果还有问题,则会短信通知相应开发人员,手动处理。
这样的系统离我们想象中的高可用有不少差距,但实际上,整个系统还是能保持很高的可用性,基本上很少会出现服务不可用的问题。当然,这与我们的业务有很大关系,基本不会出现像电商系统那样的瞬时大流量,除非遇上黑天鹅事件。对于我们来说,做好日志记录现场,远比引入分布式事务框架要简单得多。
当前,最基本的限流和熔断已经做完,至少保证在某些服务挂掉时有兜底的数据展示或友好的提示,而升降级等功能对我们来说,还真是可有可无,也许后面会做起吧。
因此,在实践微服务时,技术也许是最不重要的一个因素,毕竟,不管何种语言,都有成套的解决方案,我们也只是学习如何使用而已。更何况,像 Spring 生态里,还有多种解决方案可以选择。
其实,一直以来,我都不认为技术是个问题,如果我不会,去问问公司其他牛人就好;如果公司没有,外面付费咨询就好,总有比我牛逼的人存在。因此,在任何情况下,都不要在技术层面有畏难的心态,技术解决不了就从业务角度解决,自己解决不了,总有人能解决。况且,只要不是在做一个非常新的领域,你遇到的所有问题,基本都有现成的解决方案。
服务边界维度
微服务的边界问题,可能是整个微服务架构设计的最关键决策。如果服务之间的边界模糊,会对整个系统造成巨大灾难。相互调用是一个最常见的问题,但更咂舌的调用是这样的:服务A需要调用服务B的某个接口,而服务B在实现这个接口时又调用了服务A,即在同一个接口中出现了相互调用。
出现相互调用或者网状调用问题,就偏离了采用微服务架构的初衷,也会对后期的开发维护造成巨大的障碍。目前,我们的系统就出现了这样的问题,总结起来,原因还挺多,已经不仅仅是设计上的问题。
比如,团队成员的变动,导致某些服务和接口成为了一个黑盒,新接手的同学不懂也不敢随意改动里面的逻辑,只能在外面再包一层,久而久之,很容易出现相互调用问题。
还有一些客观上的原因,比如,在项目初期,对各个服务的边界划分其实挺清晰的。但随着业务越来越重,服务之间的边界就变得越来越模糊。举个简单的例子,比如消息服务,其功能就是简单的发邮件和短信。如果有其他服务需要发消息,就调用这个服务。但有一天,需求上需要对发消息进行收费,在发消息前要检查客户的余额,发消息后需要扣款。那这个检查余额和扣款的操作应该放到消息服务里面吗?
如果为了简单起见,肯定应该把这部分逻辑放到消息服务里,这样上层应用的发送逻辑无需做任何修改。但这又打破了服务之间的边界,因为基础的消息服务依赖其他服务了,比如依赖的这个服务叫客户服务。而如果后期,需要统计每个客户的消息发送情况以及消费情况,可能又需要客户服务来调用消息服务来获取统计数据。
当然,最好的方式肯定是重新划分服务边界,像电商系统那样,有专门的订单服务、消息服务、客户服务等,上层的应用通过组合这几个服务来实现发消息的功能。但是,它毕竟不是一个电商系统,真的也没办法轻易的增加服务。
这也许就是理论与现实的差距吧。
测试维度
最后,可能很多人会忽略微服务架构给测试带来的挑战。
在我们项目,最令测试崩溃的一句话是:“这个版本对某个基础服务作了升级,你们需要做一次全量测试”。看着测试无奈的眼神,也不知道他们在心里面想些什么。
看吧,把某些基础服务提取出来,不单单只有好处。对基础服务的每次改动,都会带来巨大的影响,它绝不是很多开发者以为得那么简单。
微服务还会让整个测试周期变长,毕竟查找一个BUG,可能需要从网关的日志开始,一层一层往下找。找到问题后,修复BUG也是一个耗时的操作。因为某些BUG可能涉及好几个服务,如果这几个服务是同一个人负责还好,如果是多个人或多个团队负责,这个时间成本会更高。
一些实践
微服务的灵活性、强大的封装、独立可扩展性、不断自我演进的特性仍然是微服务不可不说的优点,也正是基于这些优点,仍有越来越多的组织开始尝试微服务架构。如果目前还是一个巨大的单体应用,想要尝试微服务,可以参考:如何将单体应用分解为微服务。
这里,我也说说,个人总结的一些好的实践:
如果服务已经容器化,尽量去掉注册中心,k8s的调度器完全可以代替注册中心,也更方便运维。
中间件能不用就不用,能少用就少用。比如引入消息队列,就要考虑消息的堆积、消息丢失补偿等一系列问题,这又会引入大量的复杂度,一定要谨慎评估引入某些中间件带来的收益。
尽量不要使用分布式事务,接口实现时保持幂等性,打印关键日志,方便手动补偿。
服务的熔断和限流非常重要,如果有时间,还是尽量做起来。
中小团队尽量使用云产品代替自建系统,比如:数据库、监控、日志等。
一定要有监控和报警,即使是最简单的shell脚本也要能在服务挂掉的时候通知到相关人员。
开发不要只顾自己爽,一定要兼顾运维和测试,比如,可以专门暴露接口给运维和测试,方便人家采集数据或测试相关功能。
生产环境最好能有多套物理隔离的环境,可以方便灰度,也可以当冗灾使用。
服务边界的划分是一个权衡问题,也是一个不断演进的问题,没有办法在一开始就做到完美。
最重要的一条放到最后,一定要控制服务数量,且每个服务至少保证有两个同学熟悉相关业务和代码。
以上完全是个人经验总结,绝非最佳实践。
其实,这几年还遇到过很多问题,有些问题涉及一些复杂的业务逻辑,有些问题涉及公司战略、组织架构调整、人员流动等等,没办法在这里一一列出说明。但不管怎样,在采用微服务架构时,一定要谨慎的评估其实施成本和缺点是否能够带来足够的收益。
最后
微服务架构的设计决策绝非简单的技术问题,更多的在于组织的成熟度、基础设施的投入、服务间的边界设置等。
参考资料
版权声明: 本文为 InfoQ 作者【NORTH】的原创文章。
原文链接:【http://xie.infoq.cn/article/cb2efe2f74ef9043cce160cdc】。未经作者许可,禁止转载。
评论