Java微服务分布式架构
摘自《Java微服务分布式架构企业实战》
1.传统单体应用架构存在的问题
一个完整的单体应用程序通常主要由三部分组成:客户端用户界面、模块和数据库,如图1.1所示。传统单体应用的开发通常是创建一个由几个业务模块组成的项目,然后将项目打成一个包,部署在服务器上。
项目的早期阶段,这种方式很容易开发,部署也很方便。但是,随着用户需求的增加、项目功能的扩展,之前的小应用变得越来越臃肿、越来越复杂,这样项目在开发和维护的过程中就会变得非常困难。一旦项目中出现了Bug,程序员在修改的过程中,会时刻处在“牵一发而动全身”的窘境之中。在单体应用中,每个服务的更新或改变都会导致重新部署整个应用,可维护性、灵活性逐渐降低,维护成本越来越高,效率越来越低。因此,随着业务需求的发展,传统的单体应用很难满足互联网时代不断变化的需求。
2.分布式系统开发的复杂性
分布式系统的重要特性就是面向服务,它把整个系统拆分成不同的服务,然后将这些服务放在不同的服务器上、减少单体服务器的压力,提高并发量和性能。它是由一系列计算机服务器组成的,由于用户并不知道它背后的逻辑,所以感觉就像在访问单个计算机一样。分布式系统虽然在性能上表现很好,但是也带来了系统的复杂性,例如分布式事务、分布式锁、分布式Session、数据一致性等都是现在分布式系统中需要解决的难题。分布式系统设计遵循CAP定理、CAP是Consistency(一致性)、Availability(可用性)和Partition tolerance(分区容错性)的简称。CAP定理认为,CAP的三种特性中,只能同时满足其中两种特性。1. 5节将详细讲解CAP定理。分布式系统如图1. 2所示。
3.传统架构与微服务架构的区别
传统架构与微服务架构在开发、部署方面都有很大的差别,接下来将通过对比两者的缺点,及适用场景帮助更直观地发现传统架构与微服务架构的区别。
1.传统架构
1)优点
(1)结构简单,容易理解。
(2)请求响应快速。
(3)部署简单,所有功能一次打包。
2)缺点
(1)模块之间的耦合度太高,其中一个模块升级其他模块都得升级。
(2)开发困难,开发人员可能需要排队等待。
(3)系统的扩展性差。
(4)不能灵活地进行分布式部署。适用场景:传统架构更适用于对请求响应时间要求较高的应用、简单的小型应用程序以及初创型企业资源紧张的初期项目,能够节省资源,减少成本。
2.微服务架构
1)优点
(1)项目被拆分,降低了耦合度,服务自治。
(2)项目被拆分成若干个子项目,不同的团队负责不同的子项目,提高了开发效率。
(3)技术多样性,可以根据业务上下文选择合适的语言、工具进行构建,每个服务可以使用不同的技术,服务之间互相独立。
(4)易于扩展。增加功能时再增加一个子项目,调用其他系统的接口即可。
(5)服务间采用轻量级通信机制相互沟通(通常是基于HTTP的Restful API).
(6)可以灵活地进行分布式部署。
2)缺点
(1)系统之间交互需要使用远程通信,接口开发会增加工作量。
(2)HTTP请求速度慢,通常一个操作可能涉及多个微服务的相互调用,如果为了完成一个操作而多次从服务端调用不同的微服务,HTTP请求的耗时可能会成为瓶颈。
(3)测试工作更加困难,部署更加复杂。
(4)微服务应用是分布式系统,由此会带来固有的复杂性。适用场景:适用于业务功能需求大、项目版本更新迭代快的大型分布式项目的开发。
4.互联网架构的演变
随着互联网的发展,应用程序的用户越来越多,产生的数据量也越来越大,随之而来的一系列并发问题和存储问题,使传统的架构很难满足开发的需要。因此,互联网架构也逐步发展成为多种形态满足不同的开发需求。目前比较主流的架构都是时代进化的产物,由简单的单体架构到水平分层架构、SOA架构,最后再到微服务架构的开发,这些架构都是顺应时代而生的,也承担着不同的使命。没有最好的架构,只有最合适的架构,在选择一种架构开发应用程序之前就应当仔细了解该架构与要开发程序的应用场景是否匹配。接下来分别讲解这几种架构的特点。
1)单体架构
在应用程序开发完成后,打一个归档包(例如war格式),其中包含了该应用中的所有功能。单体应用结构简单、容易理解,可以方便快速地打包部署到Jetty或者Tomcat 容器中,一次部署完成即可运行整个应用程序,还可以通过运行多个副本和结合负载均衡器扩展应用。单体架构如图1. 4所示。
从图1. 4可以看出,单体架构是一个整体,所有业务逻辑、数据管理都在一起进行,模块的边界模糊,每修改一处代码就可能会面临很多问题的出现,代码开发的效率低、扩展性差。
2)水平分层架构
在单体架构的基础上,水平方向上将应用进行拆分为网关层、业务逻辑层、数据访问层,每一层都是一个独立的进程。常见的MVC模式的开发就是三层结构:模型(Model)、视图(View)和控制器(Controller).每个层都各司其职,模型(Model)是应用程序中用于处理应用程序数据逻辑的部分,通常负责应用程序的持久化操作;视图(View)是应用程序中处理数据显示的部分;控制器(Controller)是应用程序中处理用户交互的部分。MVC将不同职责划分到独立的层次,各个层的依赖关系比较灵活,降低了单体架构中的耦合性,应用程序业务扩展较单体架构更为方便。每一层都可以根据业务需求进行独立部署,如图1. 5所示。
图1. 5可以看出,一个应用程序被拆分为网关层、业务逻辑层、数据访问层。分层架构设计最核心的一点就是需要保证各层之间的差异足够清晰,边界足够明显,并且分层架构能较好地支撑系统扩展性。例如网关层只需要处理PC端的逻辑,业务逻辑层只需要处理相关的业务逻辑,这样在扩展某层的时候,其他层不受影响。
3)SOA架构SOA架构是面向服务的架构。它是在单体架构的基础上按照业务功能进一步垂直拆分的,SOA的组件之间是松耦合的,每一个服务都包含了自己的业务逻辑和多个适配器。把模块拆分,使用接口通信,降低了模块之间的耦合度,把项目拆分成若干个子项目,不同的团队负责不同的子项目,使开发效率得到了提升,增加功能时只需要再增加一个子项目,然后调用其他系统的接口就可以实现,还可以灵活地进行分布式部署。SOA架构通常以独立的形式存在于操作系统进程中,各个服务之间通过网络通信,如图1. 6所示。
图1. 6按照业务功能将项目进行了拆分,每个服务都是一个独立的单体,它们之间通过企业服务总线进行交互,每个服务也可以被其他服务调用,在水平方向上并没有进行拆分。因此,SOA的拆分也不够彻底,是粗粒度、松耦合、无状态的服务。
4)微服务架构微服务架构的出现解决了SOA架构拆分不彻底的问题。微服务首先根据不同的业务功能进行垂直拆分,然后对垂直拆分后的服务,在水平方向继续进行拆分。一个单体项目被划分为一个个网格的样式,它解决了复杂问题,把可能会变得庞大的单体应用程序分解成一套服务。虽然功能数量不变,但是应用程序已经被分解成可管理的块或者服务,每个服务都有一个明确的边界。使用微服务架构模式,个体服务能够被更快地开发,提高了开发效率,并且更容易理解与维护。微服务架构模式还可以实现每个微服务的独立部署,不需要依赖其他微服务及其相关资源,如数据库、内存缓存系统等。例如,前端如果想更改页面样式或者变更实现的技术语言,在完成开发后,重新部署与该改动相关的服务即可,并不需要重新部署整个项目,微服务架构模式使得持续部署成为可能。微服务架构如图1. 7所示。
5.微服务架构常见的设计模式
1 聚合器微服务设计模式
聚合器微服务设计模式如图1. 8所示。
由图1. 8可知,该模式通过负载均衡使用聚合器调用多个服务,其中每个服务都有自己的缓存服务器和数据库。所有的服务的接口都会暴露出来,最终由聚合器把所有检索到的数据进行处理和展示,也可以把检索到的数据增加业务逻辑形成新的微服务。这也是一种最常见也最简单的设计模式。
2 代理微服务设计模式
代理微服务设计模式如图1. 9所示。该模式是由聚合模式变化而来,这种模式在客户端不会聚合数据,但是会根据业务需求的差别来调用不同的微服务。代理可以委派请求,也可以进行数据转换工作。每个微服务都有自己独立的缓存和数据库系统,彼此独立。
3 链式微服务设计模式
链式微服务设计模式如图1. 10所示。由图1. 10可知,当服务A接收到消息后会与服务B进行通信,然后服务B和服务C进行通信。由于是链式的,因此服务之间的消息是同步传递的,客户端发出请求后,在没收到响应的这段时间内一直是处于阻塞的,直到整个链条全部走完,响应给客户端。因此,在使用链式微服务设计模式时应该特别注意服务链不应该太长,以免导致客户端的长时间等待。
4.分支微服务设计模式
分支微服务设计模式如图1.11所示。
由图1. 11可以看出,分支微服务设计模式更像是聚合器微服务设计模式和链式微服务设计模式的结合体。可以同时调用两个服务链,当客户端发送请求调用服务A时,服务A需要调用服务B同时也需要调用服务C,而服务C需要调用服务D.因此,就形成了分支微服务模式。
5.数据共享微服务设计模式
数据共享微服务设计模式如图1. 12所示。
在使用了微服务架构以后,数据库不再设置外键,此时,就开始违反三大范式,即SQL数据库反规范。所以数据就会出现冗余或重复,这是不可避免的。因此,在拆分重构阶段的时候可以采取如图1. 12所示的设计模式。
6.异步消息传递微服务设计模式异步消息传递微服务设计模式如图1. 13所示。
由图1. 13可以看出,服务A请求服务C时,服务C需要请求服务B,服务B也需要请求服务D,此时,虽然REST请求非常流行,但是同步会导致阻塞,因此,部分基于微服务的架构会采用消息队列来替代REST的请求/响应。在图1. 13中服务C是生产者,服务B是消费者,服务C只负责生产,消息队列则负责持久化服务C生产的消息,队列会帮助缓存消息,直到消费服务开始工作。
6.CAP原则
1 CAP的定义
CAP原则又被称为CAP定理,它是指在一个分布式系统中,Consistency(一致性)、Availability(可用性)和Partition Tolerance(分区容错性)三者在实际开发的过程中并不能同时兼顾,如图1. 14所示。
图1. 14中的Consistency(一致性)指的是用户在执行完(一致性)更新操作成功并返回客户端后,所有节点在同一时间能够查询到的数据会保持完全一致,这就是分布式的一致性。例如,在A、B节点下现有某条数据记录值为M,用户向A发起一个写操作,将M更改为N,接下来,用户在执行读操作时,取数据时就会得到N,这就叫一致性。用户还有可能向B节点发起读操作,由于B节点中的值没有发生变化,因此返回的还是M,A节点和B节点读操作的结果不一致,这就不满足一致性了,如图1. 15所示。
为了让M节点中的数据也能变为N,就要在向A节点执行写操作的时候,让A节点向B节点发送一条消息,要求B节点中的数据也改成N,如图1. 16所示
这样,用户向B节点发起读操作,就也能得到N了。数据一致性的问题是分布式系统中需要正视的,对于客户端来说,一致性指的是在进行并发访问时已经更新过的数据如何才能够准确地获取到。从服务端来看,就是如何将更新能够实时地同步到整个分布式系统中,确保数据最终的一致性。
Availability(可用性)是指服务一直处于可用状态,一旦接收到用户的请求,服务器就必须给出回应,而且是正常响应时间。保持可用性主要就是为了使系统能够反馈给用户更好的体验,不会出现如操作失败、访问链接超时等情况。
Partition Tolerance(分区容错性)指的是单台服务器或多台服务器出现问题后,其他正常服务的服务器依然可以正常的提供服务。在分布式系统中如果遇到某些节点或网络分区故障的时候,仍然能够继续对外提供满足一致性和可用性的服务。大多数分布式系统都会存在多个子网络,而每个子网络都可以称为一个区(Partition).如B图1. 7所示,A节点和B节点分别存在于两台跨区的服务器上。A节M点向B节点发送一条消息,B节点可能无法收到该消息,这种情况是系统设计时必须要考虑到的。
一般来说分区容错是不可避免的,所以在CAP中情况中通常认为认P总是成立的。由于CAP定理可知,CAP三者之间不能同时兼顾,因此剩下的C和A必定无法同时做到。
虽然面对的应用是一个分布式系统,但是分区容错性要求整个系统在运行时更像是一个运转正常的整体,当一个分布式系统中的一个机器宕机后,剩下的机器还能够通过运转满足系统的需求,从而让用户在体验上感觉并未受到影响。
2 CAP定理的证明
接下来举个简单的小例子证明CAP不能够同时满足三个特性,并解释其原因。假设现有两台服务器,在其中的一台服务器中存放应用A和数据库a,在另外一台服务器中存放应用B和数据库b,保持这两台服务器之间的连通性,模拟分布式系统的两个子部分。在系统需要满足Consistency(一致性)的情况下数据库a和数据库b中的数据是一样的,此时无论用户请求两台服务器中的哪一台,都能够取得同样的响应数据结果,如果两台服务器中任意一台机器宕机或网络原因导致不可用,也不会影响正常的运作。
当用户在应用A上发起向数据库更改的请求后,数据库a的数据将被更改,此时,用过分布式系统的同步更新的操作也会把数据库b中的数据进行更新,那么当用户再通过应用B向数据库发起查询请求时,得到的响应结果也必然是更新后的数据。
当然,这种情况是理想状况下的正常运作,但是微服务系统开发免不了考虑网络传输过程中所发生的一切不可预测的因素。也就是说,当两台服务器之间的通信网络在不可用的情况下该如何处理,此时还能否同时满足一致性和可用性原则。
面对以上问题,可以想象到当两台服务器之间的通信断开后,如果用户向应用A发送更新请求,紧接着数据库a中的数据固然是改变了,但是数据库b中的数据还是之前的数据,倘若再有用户访问应用B并向数据库发起读操作,那么如果立即响应返回,得到的必然不是准确的数据。这时候就会面临两种选择:要么牺牲数据一致性,立刻返回更新前的数据库b中的数据;要么牺牲可用性,形成阻塞等待,直至网络恢复连接,等待数据库b中的数据更新完成后再响应给用户。
通过以上例子可以看出,在满足容错性的情况下,一致性和可用性只能取其一,即分布式系统不能够同时满足CAP特性。因此,就需要在搭建系统时考虑如何进行取舍。
3.取舍策略
因为分布式系统中CAP三特性只能同时满足其中两个特性,因此,取舍策略就会有三种:CA/CP/AP
如果选择CA策略,就是希望能够使系统同事满足一致性和可用性,这就意味着放弃了分区容错性。在分布式系统中放弃P就不能够部署子节点,放弃了系统的可扩展性,这种做法显然已经违背了分布式系统设计的初衷,不能分区的系统也不能称为分布式系如果选择CP策略,就是希望能够使系统同时满足一致性和分区容错性,这就意味着所统了。要的数据更新操作都必须在同步完成后才能响应返回结果,这就需要在网络发生故障或服务宕机时牺牲可用性,从而影响用户的体验。
如果选择AP策略,就是希望能够使系统同时满足可用性和分区容错性,放弃一致性。这样一来每个节点中的数据只能为本地应用提供服务,导致全局不一致性。典型的例子就是当双11在网上抢购商品时,有时候打开购物页面发现还有商品库存,但是当你立刻抢购时却发现反馈提示商品已经售空,这就是因为在可用性的前提下,牺牲了数据一致性。这样虽然或多或少会影响一些用户的体验,但不会造成严重阻塞。
4 CAP总结
当今大多数的大型互联网应用都是以集群部署的,主机很多,部署也相对分散,随着业务的发展,节点也会变得越来越多,因此,节点的故障和网络不可测导致故障都将会是分布式系统面临的常态问题,所以大多数的应用也就只能在C和P中做取舍。还有一部分例如银行等传统行业,涉及的金钱数据是必须要保证数据一致性的,不能有丝毫偏差,因此,在搭建这种项目之前,首先考虑的是满足一致性,如果出现网络故障,宁可停止继续提供服务,也不能让脏数据产生。总而言之,策略没有好坏之分,一个好的系统是根据业务场景进行架构设计的,只有最合适的策略才是最好策略。