最近微服务架构特别火爆,就跟人工智能、区块链一样,软件架构设计如果不提微服务,感觉就像是与世界先进的架构风格和开发技术脱了节似的,各方各面都无法彰显高大上的气质。
本来再打算使用一套系列文章来讨论微服务的方方面面,但仔细考量之后发现,事情并没那么简单:首先抛开系列文章烂尾现象不说,单是微服务架构本身,又岂是一套系列文章能够完全介绍清楚的?我觉得更多还是需要在微服务架构落地过程中,遇到具体问题时,根据项目实际情况进行反思、讨论与深挖,进而再进行总结,以免在后续项目的实践中重复“踩坑”,这样才能很好地掌握微服务的理念,成功地实践微服务架构。今天,就先简单介绍一下我对微服务架构的理解吧。而要讨论微服务,首先回顾一下在微服务架构以各种体系结构模式的形式被提出之前,我们是如何设计服务于各行各业的软件系统的。
相信很多读者朋友在阅读这篇文章的时候,已经了解过什么是“单体架构”了,甚至有很多项目已经在正确地或者错误地使用了微服务,并且不管实践方式正确与否,大家的“微服务”也都已经在生产环境正常地服务了。然而,我仍然希望能够在讨论“微服务”之前,介绍一下单体架构,毕竟它也是众多架构形式中的一种,它仍然有着自己的应用场景。
当我们需要设计一套在线课程发布和订阅系统(以下简称“在线课程”系统)时,传统做法就是采用早已烂熟的逻辑三层架构:用户界面层、业务逻辑层和数据访问层,如果遵循领域驱动设计(DDD)的经典分层架构,基本上也就是四层:表现层、应用层、业务层以及基础结构层。在系统刚刚开始发布上线的时候,用户量和每日请求量并不是特别大,所以我们可以将来自于各个层的组件全部部署在一台物理机器上,因此,层(Layers,逻辑层)与层(Tiers,物理层)之间并非是一对一的关系。当然,也有可能会将用户界面层、业务逻辑层以及数据访问层分别部署在不同的物理机器上,实现层(Layer)与层(Tier)的一一对应,从而一定程度上减轻单台物理机器的负载。多年来,这种架构形式被反复地实现、反复地验证,并且反复地、成功地被应用在很多生产环境中。在绝大多数情况下,大家并不觉得这样的架构形式有什么问题,只要设计合理,比如通过引入依赖注入框架、面向方面编程等技术,各个层之间可以完全做到解耦,实现热插拔式的功能升级和替换也不是什么难事。其实,这种架构形式还是有很多优点的:
结构简单,容易理解:对于开发人员而言,这是非常重要的一点。经典的分层架构已经相对比较成熟,更容易被更多的开发人员所理解和接受,学习成本也相对比较低,对团队本身的要求也不是特别高。这不仅使得系统的设计和开发都相对比较容易,而且出错的几率会相对低一些。用现在时髦的词语说,就是“坑相对较少”,开发实现都可以“踩在踩坑人的背上前进”
实现数据一致性相对比较容易,通过本地事务或者分布式事务可以方便有效地保证数据一致性
部署简单方便:比如这里的在线课程系统,可以方便快速地打包成WAR包,部署到Jetty或者Tomcat容器中,也可以是一个部署在IIS中的.NET解决方案。无论哪种,一次部署完成即可运行整个应用程序
持续集成策略的设计相对容易:基本上团队可以根据项目的实际情况很容易地设计出持续集成方案,很多情况下,整套解决方案会放在同一个代码库中,根据持续集成策略,项目的持续交付也不会有太大压力
总结起来,这种架构大致可以使用下图表示,各层组件可以通过相互引用进行相互调用,也可以通过IoC/DI实现解耦,进而实现应用程序“一体化”,这也是“单体架构”一词的由来:
随着站点知名度的上升,在线用户数量也日益增多,或许有一天,部署在单台物理机上的应用程序无法承载较大的网络流量和计算负荷,于是出现了访问无响应甚至发生异常,应用程序变得不可用,不仅影响到客户,而且也对企业造成了一定的损失。出现这种情况时,或许通过增加内存、提升CPU数量,提高机器配置能够在一定层度上解决问题,然而这不仅会受到机器自身条件的限制,而且还需要关机一段时间以便完成硬件升级,系统瓶颈依然存在,日后宕机的可能性仍不可避免。
此时,你会发现:我们的单台物理机上部署的应用程序成为了一个失败点,而这个点一旦失败,是无法挽救的,当然,重启大法可以挽救,不过我们先不研究重启系统或者重装系统这些挽救措施,我们单从应用系统本身考虑。行业里将这种现象称为“单点失败”。
那么,如果将不同的逻辑层部署到不同的物理服务器上,是不是就不存在单点失败了呢?显然不是。如果业务服务器或者数据服务器失效,整个系统仍然是不可用的。那有没有办法改善呢?当然有。比如可以对业务服务器做多次部署,然后加上负载均衡:
当然还可以对数据库本身做集群以及主从备份,以提高数据库的处理能力,并提高容错率,降低单点失败的风险。基于这样的结构,假设其中某个业务层的服务器失效,那么负载均衡器就会将请求转交给另一个工作正常的服务器,虽然单点压力加大,也有可能存在几秒内请求无法响应的“阵痛”,但也不至于导致整个应用系统失败,程序仍能正常运行。另外还有一种系统扩展策略,就是将整个应用程序打包,然后进行多次部署,结构大致如下:
无论哪种方式,都是属于应用程序的横向扩展,通过将应用程序的不同组件部署在多个物理服务器上从而解决单点失败的问题。在实践中,要使得已有的单体架构应用程序能够支持横向扩展,还是需要进行一些设计和改装的,主要宗旨就是,被多次部署的组件必须是无状态的,或者是有状态,但经过特殊处理的,也就是要保证组件功能的“幂等性”:无论何时,无论哪个节点,只要接收到的请求相同,那么计算结果必定相等。比如,在经典的ASP.NET应用程序中,我们经常会使用Session对象,默认情况下,Session对象是保存在服务器内存中的,这样的应用程序如果做横向扩展,两台服务器之间是不幂等的:第一个请求过来,通过负载均衡被分配到服务器A处理,此时改变了Session对象,而下一次请求过来,准备读取Session对象中的值时,该请求很有可能被负载均衡分配到服务器B上执行,结果可想而知:该请求无法读取Session值,因为所需的Session对象在服务器B上不存在。
解决这样的问题有三种方法,第一种方法是通过Web.config配置文件,将Session对象指定保存到SQL Server数据库,由于数据库同步机制,Session对象亦可被另一个服务器读取访问,于是,也就保证了即使存在负载均衡,客户端请求仍然可以得到所需的Session对象值。这种方法还是有一定弊端的:除非两台应用服务器都连接同一台数据库服务器,否则数据库之间的同步还是会存在一定的延迟,客户端请求仍然有可能得不到所需的Session值。
第二种方法是在保存Session值的服务器返回执行结果的时候,在返回对象上做一次标记,而在后续的客户端请求上都带上这个标记,同时配置负载均衡策略,使得当有相应标记的请求进来时,保证它永远都只会被指派到对应的服务器上执行,这样也就确保了客户端请求能够得到Session的数据。这种做法也有弊端,它干预了负载均衡策略,造成负载均衡失效。
第三种方法就比较让人不舒服了,那就是禁用Session机制,以其它方案代替。在这里我很难说清楚“其它方案”是什么,还是得根据实际情况进行选择,一句话:it depends。
我曾经成功地使用ASP.NET+IIS+Windows NT Network Load Balancer实现了应用程序的横向扩展,总体来说效果还是不错的,前提就是遵循微软推荐的最佳操作(follow the best practices recommended by Microsoft),而这些最佳操作当中,就有我这里讨论的Session问题。或许你会觉得我还在炒冷饭,花这么些篇幅来介绍一些过时几百年的技术,感觉并没有什么价值。其实,单体架构横向扩展的经验,同样也适用于微服务架构,因为我们需要避免单点失败。
说起应用程序扩展,这里我们提到了通过增加内存、CPU等硬件资源来提高系统吞吐量和处理能力的纵向扩展,也提到了将一个或多个组件甚至是整个应用程序幂等地部署到多台服务器上的横向扩展。但即使是这两种不同的扩展方式,在实际项目中具体选择哪种,还是有一定讲究的,详细可以参考《横向扩展与纵向扩展的对比与选择》这篇文章(抱歉是E文的)。
在云环境中,应用程序的纵向扩展是非常容易的,只需要修改虚拟机的配置即可。其实在云上有很多种玩法,光是修改虚拟机配置就不一定、甚至通常情况下也不会通过人工的方式完成。云托管虚拟机都有监控和自动伸缩的能力,可以根据设置的策略实现纵向动态扩展。
应用程序横向扩展也是非常容易的,比如可以使用自动可伸缩集(Auto-scale Set)来实现。首先通过监控服务来获取单台虚拟机的健壮性,如果存在响应时间延长或者超时,自动可伸缩集会根据已经设置的策略,动态部署一台或多台新的虚拟机,同时修改负载均衡器的配置,将新增的机器加入负载均衡,只要配置得当,所有的事情是无需人工干预的。其实在云端,重启大法和重装大法都是非常常用的方式,重启机器或者重新安装一台新的机器,成本要比调试应用程序所需要的时间、人力低太多。
这里再多聊几句有关云环境下应用程序的实现问题,应该尽量选择云供应商托管的服务,而不是在云中创建虚拟机并让自己的应用程序运行在虚拟机中。选用托管服务不仅方便快捷安全,而且能够做到高可用性,一旦出现故障,可以直接联系云供应商辅助解决。然而如果选择虚拟机的话,部署和维护都要自己处理,还需要自己设计自动伸缩和负载均衡策略,如果出现问题,也只能自己解决,云供应商无法进入虚拟机内部并提供帮助。
总的来说,无论是部署在本地还是部署在云端,要想获得良好的扩展性,都需要遵循一定的设计模式,否则容易导致数据不一致、系统稳定性差等严重问题。
单体架构最主要的优势就是结构简单容易理解,所应用的技术和实践方案都非常成熟。在应用程序规模相对比较小的时候,单体架构还是非常合适的,但随着应用程序体积日趋庞大,慢慢地也就突显出了一些弱势。
庞大的代码体系使得代码库也变得庞大,团队合作变得越来越复杂,比如代码冲突发生的可能性会大大增加,解决代码冲突的成本也随之增大
庞大的代码体系会使得代码编辑工具和IDE变得不堪重负
系统构建和系统部署的时间越来越长,构建一次需要花几个小时甚至一天的时间
所有业务逻辑都实现在同一层中,无法根据业务划分进行更细粒度的扩展。基于业务的扩展需求依然来自于实际场景,比如在线课程系统中,通常情况下查询和浏览的访问量会要大于注册和下订单的流量,于是,我们应该可以根据实际情况,适当增加查询和浏览相关模块的部署。而单体架构却要求整个业务层统一部署,会对硬件资源造成一定的浪费:如果查询和下订单能够独立部署,那么我可以使用两台机器来运行查询服务,并能刚好满足需求;但如果是放在一起统一部署,那么两台机器就未必能够达到查询所需的系统吞吐量,因为还有另一部分流量需要分给订单系统,此时,可能就需要部署三台机器
单体架构在高可用性方面也有一定的缺陷,比如,倘若数据访问层出现问题,那么系统中的所有业务都无法正常完成,应用程序站点也就完全失效了。但如果可以将不同的业务领域拆分开来独立运行,或许其中一个部分无法正常工作,但其它的业务逻辑仍然可以正常工作,应用程序站点也不至于完全无法访问。比如,在线课程系统中,用户账户管理服务如果出现问题,充其量也就是新用户无法正常注册,老用户暂时无法登录,但站点还是能够正常运行,访客仍然可以查询课程
整个系统的技术选型从一开始就已经定好,并且在开发的过程中很难变更,即使市面上出现了更好的解决方案,也无法轻易地将新的更优秀的技术方案引入项目,因为成本会非常大。慢慢地,等应用程序功能基本完善后,或许所采用的技术已经不再主流。你或许会觉得这并不是什么严重的问题,当然,不是特别严重,只是当新生事物出现时,我们或许会失去一些机会,比如,老的系统如何接入到云端,以获得自动部署、自动伸缩等优势。到最后,或许只能用新的技术重写整个系统,但这样的循环永远终结不了
根据不同业务场景选择不同的基础结构服务变得相对比较困难。比如,在线课程查询服务可以基于Elastic Search进行快速查询,然而用户信息的创建却又无需使用Elastic Search组件,或许使用关系型数据库会更合适。将两套数据存储机制都引入应用程序,又使得应用程序本身变得更为臃肿
在项目的开发过程中,我们或许还可以总结出有关单体架构的更多弱势,在此也就不一一列举了。这里列出的几条中,有不少都跟应用程序本身所立足的业务领域有关,比如希望能够根据业务来决定系统的伸缩策略,根据业务来决定系统的技术方案等等。这里也就给我们一个启示:我们是不是可以根据业务将单体架构的应用程序拆开来,让它们能够被独立开发、独立运维,最后又以某种方式糅合在一起呢?当然有办法,这就是接下来我想谈谈的微服务架构。
限于篇幅,就不在这篇文章中说微服务架构的事情了,因为肯定是说不完的。微服务架构可以说是针对单体架构的弱势提供了完美的解决方案,然而,这并不是说单体架构就一无是处,大家可以不管三七二十一直奔微服务架构了。单体架构的魅力就在于它的简单,如果你的应用程序没有上面描述的这些单体架构所做不到的需求,那么,或许继续使用单体架构会更合适,也会让你更舒服。当你读完我接下来这篇讨论微服务架构的文章后,或许你会同意回归单体架构的,因为微服务架构的成功实践是比较困难的,不仅需要对架构整体以及独立的微服务进行细致的考虑和设计,而且还要求团队有着较高的素质。事实上,已经有一些企业和项目开始从微服务架构转向单体架构,因为各种因素使得团队无法看到微服务架构成功实践的曙光。
无论怎样,软件系统架构选择没有对错,只有合适合理,甚至可以说是没有架构:某些部分使用A技术更合适,某些部分使用B技术更合理。写到这里,我没有捧高单体架构而贬低微服务架构的意思,各种架构风格都有它们适用的地方,撰写此文也只不过是给关注架构设计的读者做个参考。总之一句话,架构没有银弹。
在《为什么Segment会从微服务退回单体架构?》一文中,有以下这段话,在此引用,也算是为下文做个铺垫吧:
“比这还糟呢。据我观察多数微服务架构根本就没考虑一致性(“我们才不要乱七八糟的事务!”),盲目地随大流还乐在其中。我搞不懂为啥子人们会觉得,把软件模块拆分开来然后用缓慢不可靠的网络和弱爆的手动连接REST处理串起来,就能神奇地让架构面目一新哩?我觉得人们产生这种生产力幻觉的原因是:”我把这些都搞定啦,现在我也有一套’管它是什么即服务‘的先进玩意儿喽!看看那酷毙的数据面板上闪烁的小绿灯吧,我们可是为了它干了好几个月呢!“”