分布式单体架构

当我们需要设计一套在线课程发布和订阅系统(以下简称“在线课程”系统)时,传统做法就是采用早已烂熟的逻辑三层架构:用户界面层、业务逻辑层和数据访问层,如果遵循领域驱动设计(DDD)的经典分层架构,基本上也就是四层:表现层、应用层、业务层以及基础结构层。在系统刚刚开始发布上线的时候,用户量和每日请求量并不是特别大,所以我们可以将来自于各个层的组件全部部署在一台物理机器上,因此,层(Layers,逻辑层)与层(Tiers,物理层)之间并非是一对一的关系。当然,也有可能会将用户界面层、业务逻辑层以及数据访问层分别部署在不同的物理机器上,实现层(Layer)与层(Tier)的一一对应,从而一定程度上减轻单台物理机器的负载。多年来,这种架构形式被反复地实现、反复地验证,并且反复地、成功地被应用在很多生产环境中。在绝大多数情况下,大家并不觉得这样的架构形式有什么问题,只要设计合理,比如通过引入依赖注入框架、面向方面编程等技术,各个层之间可以完全做到解耦,实现热插拔式的功能升级和替换也不是什么难事。其实,这种架构形式还是有很多优点的:

  1. 结构简单,容易理解:对于开发人员而言,这是非常重要的一点。经典的分层架构已经相对比较成熟,更容易被更多的开发人员所理解和接受,学习成本也相对比较低,对团队本身的要求也不是特别高。这不仅使得系统的设计和开发都相对比较容易,而且出错的几率会相对低一些。用现在时髦的词语说,就是“坑相对较少”,开发实现都可以“踩在踩坑人的背上前进”
  2. 实现数据一致性相对比较容易,通过本地事务或者分布式事务可以方便有效地保证数据一致性
  3. 部署简单方便:比如这里的在线课程系统,可以方便快速地打包成WAR包,部署到Jetty或者Tomcat容器中,也可以是一个部署在IIS中的.NET解决方案。无论哪种,一次部署完成即可运行整个应用程序
  4. 持续集成策略的设计相对容易:基本上团队可以根据项目的实际情况很容易地设计出持续集成方案,很多情况下,整套解决方案会放在同一个代码库中,根据持续集成策略,项目的持续交付也不会有太大压力

总结起来,这种架构大致可以使用下图表示,各层组件可以通过相互引用进行相互调用,也可以通过IoC/DI实现解耦,进而实现应用程序“一体化”,这也是“单体架构”一词的由来:

分布式单体架构_第1张图片

单点失败与架构扩展

随着站点知名度的上升,在线用户数量也日益增多,或许有一天,部署在单台物理机上的应用程序无法承载较大的网络流量和计算负荷,于是出现了访问无响应甚至发生异常,应用程序变得不可用,不仅影响到客户,而且也对企业造成了一定的损失。出现这种情况时,或许通过增加内存、提升CPU数量,提高机器配置能够在一定层度上解决问题,然而这不仅会受到机器自身条件的限制,而且还需要关机一段时间以便完成硬件升级,系统瓶颈依然存在,日后宕机的可能性仍不可避免。

此时,你会发现:我们的单台物理机上部署的应用程序成为了一个失败点,而这个点一旦失败,是无法挽救的,当然,重启大法可以挽救,不过我们先不研究重启系统或者重装系统这些挽救措施,我们单从应用系统本身考虑。行业里将这种现象称为“单点失败”。

那么,如果将不同的逻辑层部署到不同的物理服务器上,是不是就不存在单点失败了呢?显然不是。如果业务服务器或者数据服务器失效,整个系统仍然是不可用的。那有没有办法改善呢?当然有。比如可以对业务服务器做多次部署,然后加上负载均衡:

分布式单体架构_第2张图片

当然还可以对数据库本身做集群以及主从备份,以提高数据库的处理能力,并提高容错率,降低单点失败的风险。基于这样的结构,假设其中某个业务层的服务器失效,那么负载均衡器就会将请求转交给另一个工作正常的服务器,虽然单点压力加大,也有可能存在几秒内请求无法响应的“阵痛”,但也不至于导致整个应用系统失败,程序仍能正常运行。另外还有一种系统扩展策略,就是将整个应用程序打包,然后进行多次部署,结构大致如下:

分布式单体架构_第3张图片

无论哪种方式,都是属于应用程序的横向扩展,通过将应用程序的不同组件部署在多个物理服务器上从而解决单点失败的问题。在实践中,要使得已有的单体架构应用程序能够支持横向扩展,还是需要进行一些设计和改装的,主要宗旨就是,被多次部署的组件必须是无状态的,或者是有状态,但经过特殊处理的,也就是要保证组件功能的“幂等性”:无论何时,无论哪个节点,只要接收到的请求相同,那么计算结果必定相等。比如,在经典的http://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问题。或许你会觉得我还在炒冷饭,花这么些篇幅来介绍一些过时几百年的技术,感觉并没有什么价值。其实,单体架构横向扩展的经验,同样也适用于微服务架构,因为我们需要避免单点失败。

单体架构的云端部署

在云环境中,应用程序的纵向扩展是非常容易的,只需要修改虚拟机的配置即可。其实在云上有很多种玩法,光是修改虚拟机配置就不一定、甚至通常情况下也不会通过人工的方式完成。云托管虚拟机都有监控和自动伸缩的能力,可以根据设置的策略实现纵向动态扩展。

应用程序横向扩展也是非常容易的,比如可以使用自动可伸缩集(Auto-scale Set)来实现。首先通过监控服务来获取单台虚拟机的健壮性,如果存在响应时间延长或者超时,自动可伸缩集会根据已经设置的策略,动态部署一台或多台新的虚拟机,同时修改负载均衡器的配置,将新增的机器加入负载均衡,只要配置得当,所有的事情是无需人工干预的。其实在云端,重启大法和重装大法都是非常常用的方式,重启机器或者重新安装一台新的机器,成本要比调试应用程序所需要的时间、人力低太多。

这里再多聊几句有关云环境下应用程序的实现问题,应该尽量选择云供应商托管的服务,而不是在云中创建虚拟机并让自己的应用程序运行在虚拟机中。选用托管服务不仅方便快捷安全,而且能够做到高可用性,一旦出现故障,可以直接联系云供应商辅助解决。然而如果选择虚拟机的话,部署和维护都要自己处理,还需要自己设计自动伸缩和负载均衡策略,如果出现问题,也只能自己解决,云供应商无法进入虚拟机内部并提供帮助。

总的来说,无论是部署在本地还是部署在云端,要想获得良好的扩展性,都需要遵循一定的设计模式,否则容易导致数据不一致、系统稳定性差等严重问题。

单体架构的弱势

单体架构最主要的优势就是结构简单容易理解,所应用的技术和实践方案都非常成熟。在应用程序规模相对比较小的时候,单体架构还是非常合适的,但随着应用程序体积日趋庞大,慢慢地也就突显出了一些弱势。

  1. 庞大的代码体系使得代码库也变得庞大,团队合作变得越来越复杂,比如代码冲突发生的可能性会大大增加,解决代码冲突的成本也随之增大
  2. 庞大的代码体系会使得代码编辑工具和IDE变得不堪重负
  3. 系统构建和系统部署的时间越来越长,构建一次需要花几个小时甚至一天的时间
  4. 所有业务逻辑都实现在同一层中,无法根据业务划分进行更细粒度的扩展。基于业务的扩展需求依然来自于实际场景,比如在线课程系统中,通常情况下查询和浏览的访问量会要大于注册和下订单的流量,于是,我们应该可以根据实际情况,适当增加查询和浏览相关模块的部署。而单体架构却要求整个业务层统一部署,会对硬件资源造成一定的浪费:如果查询和下订单能够独立部署,那么我可以使用两台机器来运行查询服务,并能刚好满足需求;但如果是放在一起统一部署,那么两台机器就未必能够达到查询所需的系统吞吐量,因为还有另一部分流量需要分给订单系统,此时,可能就需要部署三台机器
  5. 单体架构在高可用性方面也有一定的缺陷,比如,倘若数据访问层出现问题,那么系统中的所有业务都无法正常完成,应用程序站点也就完全失效了。但如果可以将不同的业务领域拆分开来独立运行,或许其中一个部分无法正常工作,但其它的业务逻辑仍然可以正常工作,应用程序站点也不至于完全无法访问。比如,在线课程系统中,用户账户管理服务如果出现问题,充其量也就是新用户无法正常注册,老用户暂时无法登录,但站点还是能够正常运行,访客仍然可以查询课程
  6. 整个系统的技术选型从一开始就已经定好,并且在开发的过程中很难变更,即使市面上出现了更好的解决方案,也无法轻易地将新的更优秀的技术方案引入项目,因为成本会非常大。慢慢地,等应用程序功能基本完善后,或许所采用的技术已经不再主流。你或许会觉得这并不是什么严重的问题,当然,不是特别严重,只是当新生事物出现时,我们或许会失去一些机会,比如,老的系统如何接入到云端,以获得自动部署、自动伸缩等优势。到最后,或许只能用新的技术重写整个系统,但这样的循环永远终结不了
  7. 根据不同业务场景选择不同的基础结构服务变得相对比较困难。比如,在线课程查询服务可以基于Elastic Search进行快速查询,然而用户信息的创建却又无需使用Elastic Search组件,或许使用关系型数据库会更合适。将两套数据存储机制都引入应用程序,又使得应用程序本身变得更为臃肿

在项目的开发过程中,我们或许还可以总结出有关单体架构的更多弱势,在此也就不一一列举了。这里列出的几条中,有不少都跟应用程序本身所立足的业务领域有关,比如希望能够根据业务来决定系统的伸缩策略,根据业务来决定系统的技术方案等等。

有帮助到你的点赞、收藏关注一下吧

                                                                          需要更多教程,微信扫码即可

                                                                                 

                                                                                         

                                                        别忘了扫码领资料哦【高清Java学习路线图】

                                                                     和【全套学习视频及配套资料】
 
 

你可能感兴趣的:(架构,分布式,数据库)