当我们需要设计一套在线课程发布和订阅系统(以下简称“在线课程”系统)时,传统做法就是采用早已烂熟的逻辑三层架构:用户界面层、业务逻辑层和数据访问层,如果遵循领域驱动设计(DDD)的经典分层架构,基本上也就是四层:表现层、应用层、业务层以及基础结构层。在系统刚刚开始发布上线的时候,用户量和每日请求量并不是特别大,所以我们可以将来自于各个层的组件全部部署在一台物理机器上,因此,层(Layers,逻辑层)与层(Tiers,物理层)之间并非是一对一的关系。当然,也有可能会将用户界面层、业务逻辑层以及数据访问层分别部署在不同的物理机器上,实现层(Layer)与层(Tier)的一一对应,从而一定程度上减轻单台物理机器的负载。多年来,这种架构形式被反复地实现、反复地验证,并且反复地、成功地被应用在很多生产环境中。在绝大多数情况下,大家并不觉得这样的架构形式有什么问题,只要设计合理,比如通过引入依赖注入框架、面向方面编程等技术,各个层之间可以完全做到解耦,实现热插拔式的功能升级和替换也不是什么难事。其实,这种架构形式还是有很多优点的:
总结起来,这种架构大致可以使用下图表示,各层组件可以通过相互引用进行相互调用,也可以通过IoC/DI实现解耦,进而实现应用程序“一体化”,这也是“单体架构”一词的由来:
随着站点知名度的上升,在线用户数量也日益增多,或许有一天,部署在单台物理机上的应用程序无法承载较大的网络流量和计算负荷,于是出现了访问无响应甚至发生异常,应用程序变得不可用,不仅影响到客户,而且也对企业造成了一定的损失。出现这种情况时,或许通过增加内存、提升CPU数量,提高机器配置能够在一定层度上解决问题,然而这不仅会受到机器自身条件的限制,而且还需要关机一段时间以便完成硬件升级,系统瓶颈依然存在,日后宕机的可能性仍不可避免。
此时,你会发现:我们的单台物理机上部署的应用程序成为了一个失败点,而这个点一旦失败,是无法挽救的,当然,重启大法可以挽救,不过我们先不研究重启系统或者重装系统这些挽救措施,我们单从应用系统本身考虑。行业里将这种现象称为“单点失败”。
那么,如果将不同的逻辑层部署到不同的物理服务器上,是不是就不存在单点失败了呢?显然不是。如果业务服务器或者数据服务器失效,整个系统仍然是不可用的。那有没有办法改善呢?当然有。比如可以对业务服务器做多次部署,然后加上负载均衡:
当然还可以对数据库本身做集群以及主从备份,以提高数据库的处理能力,并提高容错率,降低单点失败的风险。基于这样的结构,假设其中某个业务层的服务器失效,那么负载均衡器就会将请求转交给另一个工作正常的服务器,虽然单点压力加大,也有可能存在几秒内请求无法响应的“阵痛”,但也不至于导致整个应用系统失败,程序仍能正常运行。另外还有一种系统扩展策略,就是将整个应用程序打包,然后进行多次部署,结构大致如下:
无论哪种方式,都是属于应用程序的横向扩展,通过将应用程序的不同组件部署在多个物理服务器上从而解决单点失败的问题。在实践中,要使得已有的单体架构应用程序能够支持横向扩展,还是需要进行一些设计和改装的,主要宗旨就是,被多次部署的组件必须是无状态的,或者是有状态,但经过特殊处理的,也就是要保证组件功能的“幂等性”:无论何时,无论哪个节点,只要接收到的请求相同,那么计算结果必定相等。比如,在经典的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)来实现。首先通过监控服务来获取单台虚拟机的健壮性,如果存在响应时间延长或者超时,自动可伸缩集会根据已经设置的策略,动态部署一台或多台新的虚拟机,同时修改负载均衡器的配置,将新增的机器加入负载均衡,只要配置得当,所有的事情是无需人工干预的。其实在云端,重启大法和重装大法都是非常常用的方式,重启机器或者重新安装一台新的机器,成本要比调试应用程序所需要的时间、人力低太多。
这里再多聊几句有关云环境下应用程序的实现问题,应该尽量选择云供应商托管的服务,而不是在云中创建虚拟机并让自己的应用程序运行在虚拟机中。选用托管服务不仅方便快捷安全,而且能够做到高可用性,一旦出现故障,可以直接联系云供应商辅助解决。然而如果选择虚拟机的话,部署和维护都要自己处理,还需要自己设计自动伸缩和负载均衡策略,如果出现问题,也只能自己解决,云供应商无法进入虚拟机内部并提供帮助。
总的来说,无论是部署在本地还是部署在云端,要想获得良好的扩展性,都需要遵循一定的设计模式,否则容易导致数据不一致、系统稳定性差等严重问题。
单体架构最主要的优势就是结构简单容易理解,所应用的技术和实践方案都非常成熟。在应用程序规模相对比较小的时候,单体架构还是非常合适的,但随着应用程序体积日趋庞大,慢慢地也就突显出了一些弱势。
在项目的开发过程中,我们或许还可以总结出有关单体架构的更多弱势,在此也就不一一列举了。这里列出的几条中,有不少都跟应用程序本身所立足的业务领域有关,比如希望能够根据业务来决定系统的伸缩策略,根据业务来决定系统的技术方案等等。
有帮助到你的点赞、收藏关注一下吧
需要更多教程,微信扫码即可
别忘了扫码领资料哦【高清Java学习路线图】
和【全套学习视频及配套资料】