微服务架构的实现首先需要提供一些基础组件,这些基础的功能性组件主要包括服务之间的通信、面向事件驱动的架构设计方法、负载均衡、服务路由、API网关和分布式配置中心等,我们对这六大基本组件进行初步的分析定案。
对于微服务而言,网络通信主要关注于网络连接、IO模型、可靠性设计及服务调用方式。
一般,基于TCP网络连接有两种基本方式,也就是我们常说的长连接和短连接,连接的建立需要三次握手,释放需要四次握手,而每次连接都意味着资源和时间的消耗。
三次建立握手示意图如下:
注:1.TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
2.TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。
四次释放握手示意图如下:
注:1. TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
2.服务器结束TCP连接的时间要比客户端早一些。
而且,一般短连接只会在客户端/服务器端间传递一次读写操作,存在的连接都是有用的连接,不需要额外的控制手段。而长连接则与其不同,当客户端与服务端完成一次读写操作后他们的连接不会主动关闭,用于后续的读写操作。
注意,长连接和短连接的产生在于客户端和服务端所采取的关闭策略,具体的应用场景采用何种策略需要依据情况具体分析,没有十全十美的选择,只有合适的选择。
现代操作系统都包括内核空间和用户空间,其中,内核空间主要存放内核代码和数据,供系统进程使用,用户空间主要存放用户代码和数据,是供用户进程使用。
一般,IO操作都有类似如下两个阶段,第一阶段,当数据分组到来时,将其拷贝到内核空间的临时缓存区,第二个阶段,再将内存空间临时缓存区中的数据复制到用户空间缓存区。围绕着这两个阶段,存在这一下几种主流的IO操作模式:
阻塞IO:所有的套接口都是阻塞的,意味着IO的发起和结束都需等待,任何一个系统调用都会产生一个由用户态到内核态切换,在从内核态到用户态的一个切换过程,而进程的上下文切换也是通过系统中断程序来实现的,需要保存当前进程的上下文,这是一个成本很高的过程。
非阻塞IO:把套接口设置成非阻塞时,用户进程会不停的询问某种操作是否准备就绪,轮询方式,这是种比较浪费CPU的方式。
IO复用:主要依赖于操作系统提供的select和poll机制,这是阻塞进程是在这两个系统调用上,不在真正的IO操作上,IO操作看起来复用阻塞了两次,但是第一次阻塞是在select上监控多个套接口是否已有IO操作准备就绪,加大了性能指标。
信号驱动IO:通过sigaction系统调用注册一个信号处理程序,主程序可以继续向下执行,当所监控的套接口有IO操作准备就绪的时候,由内核触发通知前面的注册信号处理程序执行,将数据从内核空间复制到用户空间。
异步IO:与信号驱动IO的区别是直接由内核告诉IO操作何时完成了。
可以看到,前四种IO模型的主要区别是在第一阶段,因为它们第二阶段都是在阻塞等待数据由内核空间复制到用户空间;而异步IO在第一阶段和第二阶段都不会阻塞。
常见的网络通信保障手段包括链路有效性检测以及断线以后的重连处理。
要确保通信链路的可靠性就必须对链路进行周期性的有效性检测,通用的做法就是心跳检测,通常有两种技术实现方式:
一种是在TCP层通过建立长连接在发送端和接收端之间传递心跳信息;
另一种则是在应用层,心跳信息根据系统要求可能包含一定的业务逻辑。
当发送方检测到通信链路中断,会在事先约定好的重连间隔时间之后发起重连操作,如果重连失败,则周期性地使用该间隔时间进行重连直至重连成功。
4.同步与异步
通信中的两种基本方式,一种是请求应答模式的同步操作,一种是单向模式的异步操作。
同步调用会造成业务线程阻塞,但开发和管理相对简单。
异步调用的目的在于获取高性能,队列思想和事件驱动都是实现异步调用的常见策略,但都主要依赖于基础中间件平台。JDK也为我们提供了Future模式实现异步调用。
Future模式调用可以进一步分为两种模式,Future-Get模式和Future-Listener模式,Future-Get模式通过主动get方式获取Future结果,但是get过程是串行的,会造成执行get方法的线程形成阻塞,Future-Listener模式中需要创建Listener,当Future结果生成时唤醒注册该Future上的Listener对象,从而形成异步回调机制。
基本事件驱动处理架构的优势在于当系统中需要添加另一个业务逻辑来完成整体流程时,只需要对该事件添加一个订阅者即可,不需要对原系统做任何修改。
以下系统进行分析:
如图,一个订单处理的基本场景中需要预约订单处理系统同时知道支付、物流和账单服务,并进行服务的调用和协调,完成业务的闭环,但是,如果当系统业务进行变更后,需要一个新的服务才能完成所需要的业务流程,那么原有交互模式就需要进行调整,不利用系统扩展。
如果我们采用事件驱动架构来实现的话,当一个订单操作完成时,订单系统就可以发送一个事件用于表示这个操作已完成,并触发所有对该事件感兴趣的微服务,这些微服务相当于这个事件的订阅者和消费者,这样一来,一个微服务可以处理支付,一个微服务处理物流,另一个微服务则处理账单,三个事件通过事件进行了解耦,通过消息传递机制,不必花费太大代价就能实现事件驱动架构。
如图,一种领域事件框架围绕领域事件生命周期给出了三种不同的处理方式,体现在不同是事件订阅者。
简单订阅者:直接处理事件,表现为一个独立的事件处理程序,对应于事件的使用阶段。
即时转发订阅者:对应于事件的分发和使用阶段,一方面可以具备简单订阅者的功能,另一方面也可以把事件转发给其他订阅者(消息队列是一个较好地实践方法)。
事件存储订阅者:在处理事件的同时对事件进行持久化,存储的事件可以作为一种历史记录,也可以通过专门的事件转发器转发到消息队列,对应于事件的存车使用阶段。
如下图,在架构设计上,对领域事件的处理就是基本的发布-订阅风格。
其中,DomainEvent本身具备一定的类型,DomainEventSubscriber根据类型订阅某种特定的DomainEvent,领域对象间的交互图一般如下:
应用层AplicationService对某个实体对象进行操作会触发领域事件的生成,领域事件通过DomainEventPublisher进行发布,DomainEventSubscriber则根据需要由AplicationService创建并根据事件类型进行订阅和处理。
而包含事件存储的发布订阅时序图一般如下,针对远程交互本身存在的网络稳定性等各种不可控原因会对事件进行存储以便发生问题时跟踪和重试。支持不同事件冲突、支持领域事件和存储事件之间的转换、检索由领域模型所产生的所有结果的历史记录、使用事件存储中的数据进行业务预测和分析是常见的事件存储需求。
从横向拆分角度讲,分布式是指将不同的业务分布在不同的地方,而集群指的是将几台服务器集中在一起实现同一业务。分布式中的每一个节点都可以做集群,但集群并不一定是分布式的。
集群概念的提出同时考虑到了分布式系统中性能和可用性的问题,一方面,集群的负载均衡机制可以将业务请求分摊到多台单机性能不一定出众的服务器,另一方面,集群的容错机制确保当集群中的某台机器无法正常提供服务时整体集群仍然可用。而负载均衡简单讲就是将请求分摊到多个操作单元上进行执行。
负载均衡建立在现有网络结构上,提供了一种廉价、有效、透明的方法扩展服务器的带宽、增加吞吐量、加强网络数据处理能力,以及提高网络的灵活性。以各种负载均衡算法为基础的分发策略决定了负载均衡的效果,根据服务器地址列表所存放的位置可以分为两大类,一类是服务器负载均衡,另一类是客服端负载均衡。
客户端发送请求到负载均衡器LB,负载均衡器负责将接收到的各个请求转发到运行中的某台服务节点上,然后接收到请求的微服务做响应处理,常见的有Apache、Nginx、HAProxy。
其实现机制比较忙简单,只需要在客服端与各个微服务实例之间架设集中式的负载均衡器即可,负载均衡器动态获取各个微服务运行时的信息,决定负载均衡的目标服务,若负载均衡器检测到某个服务已经不可用的时候就会自动移除该服务。
注意,负载均衡器运行在一台独立的服务器上并充当代理的作用,同时,需要注意的是当服务请求越来越大的时候,负载均衡器就会成为系统的瓶颈,同时若负载均衡器自身发生失败时,整体服务的调用都将发生失败。
客户端负载均衡机制的主要优势就是不会出现集中式负载均均衡所产生的瓶颈问题,因为每个客户端都有自己的负载均衡器,负载均衡器失败也不会造成严重的后果,但是运行时的信息在多个负载均衡器之间进行服务配置信息的传递会在一定程度上加重网络流量负载。
实现上,需要在客服端程序里面自己设定一个调度算法,在向服务器发起请求的时候,先执行调度算法计算出目标服务器地址,具体与原理如下图分析:
其另一种典型的实现方式是把Nginx等能够实现代理功能的负载服务器部署到运行微服务的一台机器上。这时需要考虑实施成本和维护性问题。
客户端负载均衡比较适合于客户端具有成熟的调度库函数、算法以及API的工具和框架。
大致可以分为两大类,即静态负载均衡算法和动态负载均衡算法。
主要指的是各种随机算法和轮询算法。
轮询算法:将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。
随机算法:通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多,其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。
加权轮询算法:不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。
根据服务器的实时性能分配连接是常见的动态策略。所有涉及权重的静态算法都可以转变为动态算法。常见的有以下几种:
最小连接数算法:比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。
源地址哈希算法:根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。
负载均衡的出发点是提供服务分发而不是解决路由问题,常见的静态、动态负载均衡算法也无法实现精细化的路由管理,但是负载均衡也可以简单看做是路由方案的一种。服务路由的管理可归纳为三大类,直接路由、间接路由和路由规则。路由策略整体如下:
服务消费者需要感知服务提供者的地址信息,基本思路是通过配置中心或者数据库,当服务消费者需要调用某个服务时,基于配置中心或者数据库中存储的目标服务的具体地址构建链路完成调用。
存在的问题:(1)增强了服务提供者和消费者之间的耦合度;
(2)创建和维护配置中心或数据库持久化操作需要成本。
强调解耦思想并充分利用了发布-订阅模式作用,发布者发布事件,订阅者关注自身所想关注的事件,发布者和订阅者并不需要感知对方的存在,两者之间通过传输事件的基础设施进行完全解耦。
在为微服务架构里面,实现间接路由的组件一般称为服务注册中心,从概念上讲就是发布-订阅模式中传输事件的基础设施,可以把服务的地址信息理解为事件的具体表现。通过服务注册中心,服务提供者发布服务到注册中心,服务消费者订阅感兴趣的服务,服务消费者只需要知道有哪些服务,而不需要知道服务具体在什么位置,从而实现间接路由。
间接路由解决了路由解耦问题,面向全路由场景。但是在服务故障、高峰期导流、业务相关定制路由等特定场景下,依靠间接路由提供静态路由信息并不能满足要求,这就需要动态路由,而动态路由规则需要通过路由规则进行实现。
路由规则常见的实现方案是白名单和黑名单,即把需要路由的服务地址信息放入到可以控制是否可见的路由池中。更为复杂的可以用Python等脚本语言实现定制化。
在设计模式中,外观模式的设计意图在于为子系统中的一组接口提供一个一致的入口,这个入口使得这一子系统更加容易使用,实现上用户界面不与系统耦合,而外观类与系统耦合。在层次化结构中,可以使用外观模式定义系统中的每一层的入口。
外观模式:封装许多对象,以简化它们的接口,此模式定义了一个高层的接口,这个接口使得这一子系统更加容易使用,如下图:
API网关本质上就是一种外观模式的具体实现,它是一种服务器端应用程序并作为系统访问的唯一入口。在微服务架构中,API网关的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。
微服务提供的API粒度与客户端的要求不一定完全匹配,微服务一般提供细粒度的API,这意味着客户端通常需要与多个服务进行交互,网关要起到客户端与微服务间的隔离作用,随着业务需求的变化和时间的演进,网关背后的各个微服务的划分和实现可能需要做相应的调整和升级,这种调整和升级要求对客户端透明。
API网关的作用主要体现在以下三个方面:
(1)解耦:API网关使客户端和服务器端在调用关系和部署环境上进行解耦,向客户端隐藏了应用如何被划分到微服务的细节。
(2)API优化:要求对每个客户端提供最优的API,在多客户端场景中,对于同一个业务请求,不同的客户端一般需要不同的数据。
(3)简化调用过程:可以对返回数据进行处理,API网关减少了请求往返的次数,从而简化客户端的调用,提高服务访问的性能。
但是需要注意的是,API网关也增加了系统的复杂性和响应时间,通过API网关也多了一层网络跳转,但是我们认为API网关模式的缺点相比优点是微不足道的。
网关的基本功能基本包含五点,具体细分如下图:
功能1-NIO接入和异步接出:由于API网关作为客户端和微服务之间提供了桥梁,增加了一层网络跳转,为了解决网络跳转带来的性能影响,在实现上需要提供NIO接入和异步接出功能。
功能2-报文格式转换:API网关作为单一入口,可构建异构系统,通过协议转换整合后基于REST、AMQP和Dubbo等不同风格和实现技术的微服务。
功能3-安全性控制:API网关是统一管理安全性的绝佳场所,可以将认证的部分抽离到网关层,然后微服务系统无需关注认证的逻辑只关注自身业务即可。
功能4-访问控制:需要控制客户端的访问次数和访问频率等功能。
功能5-业务路由支持:可在网关层制定灵活的路由策略,对于特定API可设置白名单、路由规则等限制,非业务功能的配置及变更在网关层单独操作。
在微服务架构中,一般都需要引入配置中心的设计思想和相关工具。
先来梳理下配置相关的内容和分类:
按配置的来源划分:源代码文件、数据库和远程调用。
按配置的适用环境划分:开发环境、测试环境、预发布环境、生产环境等。
按配置的集成阶段划分:编译时---源代码级的配置+配置文件和源代码一起提交到代码仓库;
打包时---通过某种方式将配置打入最终的应用包中;
运行时---应用启动时并不知道具体配置,需从本地或远程获取配置,在正常启动。
按配置的加载方式划分:单次加载型配置、动态加载型配置。
依照配置相关的内容和分类来看,构建一个合适的配置中心,至少需要满足以下4个核心需求:
(1)非开发环境下应用配置的保密性,避免将关键配置写入源代码;
(2)不同部署环境下应用配置的隔离性,比如非生产环境的配置不能参与生产环境;
(3)同一部署环境下的服务器应用配置的一致性,即所有服务器使用同一份配置;
(4)分布式环境下应用配置的可管理性,即提供远程配置能力。
采取配置中心也就意味着采用集中式配置管理的设计思想。
维度一:在集中式配置中心中,开发、测试和生产等不同环境配置信息统一保存在配置中心中;
维度二;分布式集群环境,需要确保集群中同一类服务的所有服务器保存同一份配置文件并且能够同步更新。
配置中心在实现上需要满足3大需求:高效获取、实时感知、分布式访问。为满足这三大需求,配置中心需要依赖分布式协调机制,即通过一定的方法确保配置信息在分布式环境中的各个服务中能够得到实时、一致管理。目前业界主流的分布式协调框架为Zookeeper。
相关Zookeeper内容参考链接:http://www.cnblogs.com/wuxl360/p/5817471.html .
【1】https://blog.csdn.net/qzcsu/article/details/72861891.
【2】郑天民. 微服务设计原理与架构. 北京:人民邮电出版社,2018.
【3】https://blog.csdn.net/youanyyou/article/details/78990133.
【4】https://blog.csdn.net/wind19/article/details/7373010.
【5】http://www.cnblogs.com/wuxl360/p/5817471.html.