一、应用架构演变历史:
单一应用架构 -> 垂直应用架构 -> 分布式服务架构 -> 微服务架构。
单一应用架构
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。 此时,用于简化增删改查工作量的 数据访问框架(ORM) 是关键。
缺点:单一的系统架构,使得在开发过程中,占用的资源越来越多,而且随着流量的增加越来越难以维护。
垂直应用架构
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,垂直应用架构就出现了,垂直应用架构将应用拆成互不相干的几个应用,解决了单一应用架构所面临的扩容问题,流量能够分散到各个子系统当中,且系统的体积可控,一定程度上降低了开发人员之间协同以及维护的成本,提升了开发效率。
缺点:但是在垂直架构中相同逻辑代码需要不断的复制,不能复用。
分布式服务架构
soa
随着业务的发展,垂直应用越来越多,应用之间的交互不可避免,并且这些应用之间的调用和依赖关系也会越来越复杂,为了解决这个问题,同时为了解决多个垂直应用间的代码重复性问题,我们就不得不再次对这些垂直应用做水平拆分了,soa时代也就来临了,soa是面向服务的架构,它允许我们将核心业务和公共功能从多个垂直应用中拆分出来,作为单独的服务进行独立的部署,逐渐形成了稳定的服务中心,使前端应用能更快速的响应多变的市场需求。这个时期也衍生出了一系列服务于soa的技术,如对服务提供、服务调用、连接处理、通信协议、序列化方式、服务发现、服务路由、日志输出等行为进行封装的服务框架。
从部署的角度来看,soa其实就是分布式服务:将垂直架构中的各个子系统再进行水平拆分,将多个子系统中的共同功能拆分为独立的服务,将这些服务部署在不同的主机上,通过网络通信技术来实现服务之间的调用,并通过服务路由、负载均衡等技术来优化服务间的调用过程,提高整体服务效率。
微服务架构
微服务是对于分布式服务进行的更小粒度的拆分,因为分布式服务架构中依然有多个具有调用关系的模块存在于同一个应用中,当被调用的模块出现了异常时,这个异常会被传播到调用它的模块中,也就是模块间具有代码级别的强依赖,耦合度太高了,为了解决这个问题,我们对分布式服务进行了更小粒度的拆分,也就是按照模块拆分服务。微服务架构就是将应用按照业务模块拆分成一个一个的微服务,每个微服务运行在独立的进程中,并使用轻量级的机制进行通信,通常是http restful api,这些服务可以使用不同的编程语言来实现,也可以连接不同的数据库,以保证最低限度的集中式管理,微服务也可以进行集群化部署,以提高服务应对高并发的能力。随着微服务架构出现的是微服务框架,一种用于进行服务治理的框架,微服务框架自成生态,提供了用于治理微服务架构各方面的组件,包括服务的自动注册与发现、服务网关路由、负载均衡、服务调用、服务容错、服务链路追踪、配置的动态管理等。常用的微服务框架如Spring Cloud。
二、rpc
在soa时代出现了用于实现远程过程调用的rpc协议,远程过程调用其实是一种思想,它想要实现的目标为:在分布式系统中,能够像调用本地方法一样调用远程接口,使我们无需进行网络编程,也无需了解网络通信技术,就能通过网络从远程计算机程序上请求服务。
rpc的传输协议
rpc可以使用http作为传输协议,也可以直接使用tcp协议,使用不同的协议一般也是为了适应不同的场景。那么http协议和tcp协议有什么区别呢?
http是应用层协议,应用层的任务是通过应用进程间的交互来实现特定的网络应用。http属于应用层协议。http协议工作于C-S架构之上,客户端基于http协议向web服务端发送请求,服务端处理接收到的请求后,向客户端发送响应信息。http协议建立在tcp协议之上,它会基于TCP/IP通信协议来进行数据的传递(HTML 文件, 图片文件, 查询结果等)。
tcp是传输层协议,传输层的主要任务就是负责为两台主机进程之间的通信提供通用的数据传输服务。tcp是传输层协议,主要解决数据如何在网络中传输。tcp提供的是面向连接的,可靠的数据传输服务。
rpc的实现需要解决的问题
rpc实现的是远程调用,肯定是需要跨网络而非本机调用,所以需要网络编程才能实现,这就带来了以下几个问题:
1、网络通讯问题:主要是通过在客户端和服务器之间建立tcp连接,远程过程调用的所有交换的数据都在这个连接里传输。连接可以是按需连接,调用结束后就断掉,也可以是长连接,多个远程过程调用共享同一个连接。
rpc在大多数情况下,是应用于一个高并发的调用场景,根据系统内核的支持、编程语言的支持以及IO模型本身的特点,在rpc框架的实现中,在网络通信的处理上,大多数会选择 IO多路复用的方式。
2、寻址问题:A服务器上的应用需要告诉底层的rpc框架,要请求的B服务器的ip地址及端口号是多少,接口名称是什么,rpc框架只有得到这些信息才能完成调用。
在本机的函数调用中,函数体是直接通过函数指针来指定的,当函数调用时,编译器会自动调用相应的函数指针。但是在远程调用中,因为是跨服务器的,两个进程的地址空间是完全不一样的。所以,在rpc中,所有的函数都必须有一个自己的Call id,这个Call id在所有的进程中都是唯一确定的,具有全局唯一性。客户端在远程调用时,需要带上这个Call id。同时我们需要在客户端和服务端分别维护一个{ 函数 <-> Call id}的映射表。当客户端需要远程调用的时候,就需要查一下此表,查询出对应的Call id , 将这个Call id带在请求中,服务端在处理请求的时候,也先查询映射表,来确定客户端需要调用的函数是哪个,最终执行相应的函数代码。
3、序列化和反序列化:网络传输的数据必须是二进制数据,但是调用方的请求的参数都是对象,对象是不能直接在网络中传输的,所以必须把它转换成可传输的二进制数据,这个过程就叫做序列化。服务端也要将收到的二进制数据还原为对象,这个过程叫做反序列化。在rpc调用中,对输入参数对象与返回值对象进行的序列化和反序列化是一个必须的过程。
rpc架构
一个基本的rpc架构应该至少包含以下 4 个组件:
客户端::服务调用方,即服务消费者;
客户端存根: 存放服务端地址信息,将客户端的请求参数数据打包成网络消息,再通过网络传输发送给服务端;
服务端存根: 接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理;
服务端: 服务的真正提供者。
一次具体的rpc调用过程:
1、服务消费者通过调用本地服务的方式调用需要消费的服务;
2、客户端存根接收到调用请求后负责将方法Call id、入参等信息序列化,组装成能够进行网络传输的消息体;
3、客户端存根找到远程的服务地址,并且将消息通过网络发送给服务端;
4、服务端存根收到消息后进行解码(反序列化操作),并根据解码结果调用本地方法进行相关处理;
5、服务端方法执行完毕后将处理结果返回给服务端存根;
6、服务端存根将相应结果序列化,打包成消息并通过网络发送至消费方;
7、客户端存根接收到消息,并进行解码(反序列化),将反序列化以后的结果返回给客户端。
rpc框架
除了rpc的基本功能,一个成熟的rpc框架还应该提供其他的用于治理服务的功能,如:服务的自动注册与发现、负载均衡、集群容错机制等等,下面简单介绍一个经典的rpc框架:Dubbo
Dubbo简介
Dubbo是阿里巴巴开源的一款基于Java的高性能rpc分布式服务框架,致力于提供高性能和透明化的rpc远程服务调用方案,以及soa服务治理方案。
为什么使用dubbo?
使用Dubbo可以将核心业务抽取出来,作为独立的服务,形成稳定的服务中心,可提高业务复用性,使前端应用能更快速的响应多变的市场需求。并且rpc支持的分布式架构可以承受更大规模的并发流量。
dubbo的核心功能包括三方面
1、远程通讯:提供基于接口方法的透明化的远程过程调用,包括多协议支持,dubbo内部对多种基于长连接的NIO框架进行了封装,包括多种线程模型,序列化,以及“请求-响应”模式的信息交换方式。
2、负载均衡与集群容错:提供了对于多种负载均衡策略及集群容错策略的支持。
3、服务的自动注册与发现:基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使服务方地址透明,并支持服务提供方动态地增加或减少机器。
服务的发布-订阅-调用过程:
1、服务容器在启动时加载、启动、运行服务提供者。
2、服务提供者在启动时,向注册中心注册自己提供的服务并对外暴露自己的接口,接口的发布支持多协议。
3、服务消费者在启动时,向注册中心订阅自己所需的服务。
4、注册中心返回服务提供者地址列表给消费者,如果服务提供者列表有变更,注册中心将基于长连接推送变更数据给消费者,服务消费者会将服务提供者地址列表保存在本地。
5、服务消费者,从本地保存的提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,会基于提供者设置的容错策略去调用另外一台服务或者直接返回。
6、服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
Dubbo的配置说明(以xml配置方式举例)
服务提供者端的dubbo配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!--当前项目在整个分布式架构里面的唯一名称,计算依赖关系的标签-->
<dubbo:application name="provider" owner="sihai">
<dubbo:parameter key="qos.enable" value="true"/>
<dubbo:parameter key="qos.accept.foreign.ip" value="false"/>
<dubbo:parameter key="qos.port" value="55555"/>
</dubbo:application>
<dubbo:monitor protocol="registry"/>
<!--dubbo这个服务所要暴露的服务地址所对应的注册中心-->
<!--<dubbo:registry address="N/A"/>-->
<dubbo:registry address="zookeeper://localhost:2181" check="false"/>
<!--当前服务发布所依赖的协议;webservice、Thrift、Hessain、http-->
<dubbo:protocol name="dubbo" port="20880"/>
<!--服务发布的配置,需要暴露的服务接口-->
<dubbo:service
interface="com.sihai.dubbo.provider.service.ProviderService"
ref="providerService"/>
<!--Bean bean定义-->
<bean id="providerService" class="com.sihai.dubbo.provider.service.ProviderServiceImpl"/>
</beans>
配置说明:
1、dubbo是基于spring来开发的,使用类似于spring的配置文件进行配置;
2、节点:dubbo:application配置的是应用在分布式架构中的唯一名称,可以在name属性中配置,另外还可以配置owner字段,表示属于谁。
3、节点:dubbo:monitor是监控中心配置, 用于配置连接监控中心相关信息,可以不配置,不是必须的参数。
4、节点:dubbo:registry配置注册中心的信息,比如,这里我们可以配置 zookeeper 作为我们的注册中心。address 是注册中心的地址,dubbo支持使用zk集群作为注册中心,当注册中心是zk集群时,要配置dubbo:registry节点的protocol属性的值为zookeeper,address属性的值为多个节点的地址,多个地址用英文逗号隔开。
5、节点:dubbo:protocol用于定义协议,dubbo可以依赖这些协议进行服务的发布,同一个接口也可以使用多个协议进行发布,在dubbo:service节点中可以用protocol属性指向dubbo:protocol节点定义的某个协议,dubbo:protocol可配置的协议类型有dubbo、http、webservice。
6、节点:dubbo:service这个节点就是我们的重点了,我们就是通过这个配置将服务提供者的服务发布出去的。interface 是接口的包路径,ref指向第 ⑦ 点配置的接口的 bean。
7、最后,我们需要像配置spring的接口一样,配置接口的bean,也就是所发布服务的具体实现。
服务的接口的发布
服务的可以通过启动spring容器将接口发布出去:
package com.sihai.dubbo.provider;
import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ProtocolConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.config.ServiceConfig;
import com.alibaba.dubbo.container.Main;
import com.sihai.dubbo.provider.service.ProviderService;
import com.sihai.dubbo.provider.service.ProviderServiceImpl;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.io.IOException;
/**
* xml方式启动
*
*/
public class App
{
public static void main( String[] args ) throws IOException {
//加载xml配置文件启动
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("META-INF/spring/provider.xml");
context.start();
System.in.read(); // 按任意键退出
}
}
因为dubbo底层就是依赖spring的,所以我们通过spring容器加载dubbo配置然后启动spring容器就将服务发布出去了。
消费者端的dubbo配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!--当前项目在整个分布式架构里面的唯一名称,计算依赖关系的标签-->
<dubbo:application name="consumer" owner="sihai"/>
<!--dubbo这个服务所要暴露的服务地址所对应的注册中心-->
<!--点对点的方式-->
<!--<dubbo:registry address="N/A" />-->
<dubbo:registry address="zookeeper://localhost:2181" check="false"/>
<!--生成一个远程服务的调用代理-->
<!--点对点方式-->
<!--<dubbo:reference id="providerService"
interface="com.sihai.dubbo.provider.service.ProviderService"
url="dubbo://192.168.234.1:20880/com.sihai.dubbo.provider.service.ProviderService"/>-->
<dubbo:reference id="providerService"
interface="com.sihai.dubbo.provider.service.ProviderService"/>
</beans>
配置说明:
与服务提供者端的dubbo配置主要就是dubbo:reference这个节点的不同,在消费端用dubbo:reference这个节点来配置消费端要调用的远程接口信息,它的必填属性interface要指向所要引用的接口类型。
远程调用的发起方式
package com.sihai.dubbo.consumer;
import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ReferenceConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.sihai.dubbo.provider.service.ProviderService;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.io.IOException;
/**
* xml的方式调用
*
*/
public class App
{
public static void main( String[] args ) throws IOException {
ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("consumer.xml");
context.start();
ProviderService providerService = (ProviderService) context.getBean("providerService");
String str = providerService.SayHello("hello");
System.out.println(str);
System.in.read();
}
}
通过spring容器的getBean方法,以调用本地bean的方式去调用远程服务。
dubbo的多种配置方式
dubbo支持多种配置方式:xml配置、api配置、注解配置。dubbo官方推荐的是xml方式,不过在spring的演进过程中一直在去xml化,所以注解配置也是常用的一种配置方式。
注解配置方式:
主要就是服务端的@Service注解和消费端的@Reference注解,注解中都提供了与xml配置相对应的各个配置属性,只不过在使用注解方式时需要注意,要在配置类中用@EnableDubbo注解来开启对于dubbo的支持,并且通过它的scanBasePackages属性来配置容器扫描dubbo服务的包路径,即@Service注解的类所在的包路径。
api配置方式:
api配置方式也是提供了与xml配置相对应的各个api组件,包括与<dubbo:application>对应的ApplicationConfig、与<dubbo:registry>对应的RegistryConfig、与<dubbo:service>对应的ServiceConfig、与<dubbo:reference>对应的ReferenceConfig等等,调用这些api的方法可以对dubbo进行配置。
dubbo的集群容错策略
集群模式 | 说明 | 使用方法 |
---|---|---|
failover | 失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。 | cluster="xxx" xxx:集群模式名称 ,例如cluster="failover" |
failfast | 快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 | |
failsafe | 失败安全,出现异常时,直接忽略。 | |
failback | 失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。 | |
forking | 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。 | |
broadcast | 广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。 |
默认的方案是 failover,也就是重试机制。我们可以在发布服务或者引用服务的时候设置,也就是为<dubbo:service>或者<dubbo:reference>节点设置cluster属性。
dubbo的负载均衡策略
负载均衡模式 | 说明 | 使用方法 |
---|---|---|
random | 随机 按权重设置随机概率 | <dubbo:service loadbalance="xxx"/> xxx:负载均衡方法 |
roundRobin | 轮询 按公约后的权重设置轮询比率。 | |
leastActive | 最少活跃调用数 相同活跃数的随机,活跃数指调用前后计数差。 | |
consistentHash | 一致性 Hash 相同参数的请求总是发到同一提供者。 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。 |
dubbo的多协议支持
在前面我们使用的协议都是 dubbo
协议,但是 dubbo
除了支持这种协议外还支持其他的协议,比如,http、rmi、hessian
等,另外,还可以用多种协议同时暴露一种服务。
先声明多种协议:
<!--当前服务发布所依赖的协议;webserovice、Thrift、Hessain、http-->
<dubbo:protocol name="dubbo" port="20880"/>
<dubbo:protocol name="rmi" port="1099" />
在发布接口时,指定多个协议,多个协议之间用英文逗号隔开:
<!--服务发布的配置,需要暴露的服务接口-->
<dubbo:service cluster="failover" retries="2"
interface="com.sihai.dubbo.provider.service.ProviderService"
ref="providerService"/>
<dubbo:service cluster="failover" retries="2"
interface="com.sihai.dubbo.provider.service.ProviderService"
ref="providerService" protocol="rmi"/>
dubbo的多注册中心支持
dubbo支持同一服务向多注册中心同时注册,或者不同服务分别注册到不同的注册中心上去,甚至可以同时引用注册在不同注册中心上的同名服务。
先声明多个注册中心
<!--多注册中心-->
<dubbo:registry protocol="zookeeper" id="reg1" timeout="10000" address="localhost:2181"/>
<dubbo:registry protocol="zookeeper" id="reg2" timeout="10000" address="localhost:2182"/>
<dubbo:registry protocol="zookeeper" id="reg3" timeout="10000" address="localhost:2183"/>
发布服务:
<!--服务发布的配置,需要暴露的服务接口-->
<dubbo:service cluster="failover" retries="2"
interface="com.sihai.dubbo.provider.service.ProviderService"
ref="providerService" registry="reg1"/>
<dubbo:service cluster="failover" retries="2"
interface="com.sihai.dubbo.provider.service.ProviderService"
ref="providerService" protocol="rmi" registry="reg1,reg2"/>
对于dubbo的一些属性配置,如负载均衡、容错策略等,虽然在服务端和消费端都支持配置,但是dubbo的用户手册推荐的是,尽量在服务端多设置,因为服务端更了解自己所提供服务的特性。不过,如果我们在消费端进行了这些设置,那么在服务调用时,就以消费端的设置为准,即:消费端的设置具有更高优先级。