文章目录
概述
- 分布式架构的发展历史与背景
- 如何着手架构一套分布式系统
- Dubbo结构与设计说明
一:分布式架构的发展历史与背景
理解分布式架构:
分布式架构是建立在网络之上的软件系统。正是因为软件的特性,所以分布式系统具有高度的内聚性和透明性。因此,网络和分布式系统之间的区别更多的在于高层软件(特别是操作系统),而不是硬件。
- 我们简单理解就是原来是一个节点干活,只有一个JVM进行操作。现在分布式系统之后,变为多个节点多个JVM进行干活。
为什么会发展分布式架构?
- 稳定性和可用性这两个指标很难达到。比如:单点问题,一旦大型主机出现了故障,那么整个系统就会处于不可用的状态。但是对于大型机的使用机构来说,这种不可用导致的损失是非常巨大的。
- 单机处理能力存在瓶颈
- 升级单机处理能力的性价比越来越低
架构的发展历史:
随着互联网的发展,网站应用的规模不对扩大,常规的垂直应用架构已经无法应对,分布式服务架构以及流动计算架构势在必行,急需要一个治理系统确保架构有条不紊的演进。
单体架构
垂直架构
垂直架构的业务,数据库等都进行拆分,垂直拆分
分布式架构
在垂直架构的基础上,将公有的功能抽象出来,然后在每个需要用到的服务进行远程调用,这就是分布式架构
分布式架构所带来的成本:
分布式事务
分布式事务指的是一个操作,分成了几个小的操作在多个服务器上进行执行,要么多成功,要么多失败这些分布式事务要做的
不允许服务有状态
无状态服务就是指对单词请求的处理,不会依赖上次的请求结果,就是说,我们处理一次请求所需要的全部的信息,要么都包含在这个请求里,要么可以从外部获取到(比如说数据库),服务器本身是不存储任何信息的。因为我们如果是分布式架构,不一定下次请求过来的还是这个机子,也就是说可能JVM是有多个的,不一定下次请求的是同一个。
服务依赖关系复杂
服务A–>B–>C,如果我们对服务C进行了修改,那么我们可能会影响服务B和服务C,事实上当服务越来越多的时候,C的变动将会越来越困难。
部署运维成本增加
之前我们只有一个war包需要部署,只部署到一个节点,现在节点新增,成本变高。
源码管理的成本增加
原本一套或者几套源码现在拆分成几十个源码库,其中分支,tag都要进行相对应的管理
如何保证系统的伸缩性
伸缩性是指,当前服务器硬件升级之后或者新增服务器处理能力就应该相对应的提升
分布式会话
此仅仅针对应用层服务,不能够将session存储在一个服务器上,我们通常是将session直接存到redis中。
分布式JOB
通常定时任务只需要在一台机器上触发执行,分布式的情况下在哪台执行呢?
- 最后我们可以直接通过一张图直观感受下,单体到分布式的区别:
二: 如何选型分布式架构
- 首先我们需要明确一点分布式框架最核心的功能就是远程调用,我们将公用的服务全部进行抽取,在使用的时候进行远程调用。
RPC远程调用
我们这里距离几种远程调用的方式。
这里距离几种比较熟悉的:RMI,Web Service,Http,Hessian
协议 | 描述 | 优点 | 缺点 |
---|---|---|---|
RMI | Java远程调用,使用的是原生的二进制方式进行序列化 | 简单易用,SDK支持,提高开发效率 | 不支持跨语言 |
Web Service | 比较早的系统的调用解决方案,跨语言,其基于WSDL生成SOAP进行消息的传递 | SAK支持,跨语言 | 实现比较繁重,发布繁琐 |
Http | 采用http+json实现 | 简单,轻量,跨语言 | 不支持SDK |
Hessian | 采用http+hessian序列化实现 | 简单,轻量,sdk支持 | 不能跨语言 |
RMI远程调用架构
Java RMI,即远程方法调用,一种用于实现远程过程调用(RPC)的Java API,能够直接传输序列化后的java对象和分布式垃圾收集。他的实现依赖于Java虚拟机,因此他不能够跨语言,只能一个JVM到另一个JVM。
SDK其实就是我们已经开发好的包,软件开发包,比如JDK就是一个SDK,不支持SDK我的理解就是不能用调用接口的方式来远程调用
RPC是一种抽象概念,RMI只是其中的一种实现
RMI注册的时候有两种方式:
(1)直接将server的实例进行注册,将其注册到注册中心。
(2)将IP+端口号进行注册
- 第一种方式是将实例进行注册,这种方式没有进行远程调用,我们因为是将实例进行序列化的所以需要实现序列化的接口,这种实际上不是远程调用,因为我们在本地拿到这个对象的时候会将其反序列化,如果当前的服务没有远程服务的方法或者属性等我们实际上是不能成功调用的,这种凡事我们的远程server是不能有第三方依赖的,因为本地没有,这样反序列化就不存在,这时候调用失败。后面会代码演示
- 第二种方法是将IP+端口号进行注册,然后客户端进行远程的调用,这个是真正的远程调用,但是这个远程调用的时候服务名不能够重复,也就是说不能发布多个同名服务,这样子我们是无法实现负载均衡的。
RMI不能实现负载均衡,也不能跨语言调用
架构图
RMI是非常简单的,没有什么学习成本,具体代码如下(这里是注册端口和IP的):
- 注册中心
package com.xiyou.rmi;
import java.io.IOException;
import java.rmi.registry.LocateRegistry;
/**
* @author 92823
* 自己实现一个rmi的注册中心
*/
public class Registry {
public static void main(String[] args) throws IOException {
// 创建一个远程对象
LocateRegistry.createRegistry(8080);
System.out.println("===注册中心启动===");
System.in.read(new byte[1024]);
}
}
- 客户端
package com.xiyou.rmi;
import java.lang.management.ManagementFactory;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
/**
* @author 92823
* RMI的客户端
*/
public class RmiClient {
public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
String remoteAddr = "rmi://localhost:8080/UserService";
UserService userService = (UserService) Naming.lookup(remoteAddr);
System.out.println(String.format("引用远程服务成功,当前主机:%s ", ManagementFactory.getRuntimeMXBean().getName()));
String response = userService.getName(11);
System.out.println("=======> " + response + " <=======");
}
}
- 服务端
package com.xiyou.rmi;
import java.io.IOException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
/**
* @author 92823
* RMI的服务端
*/
public class RmiServer {
public static void main(String[] args) throws IOException, AlreadyBoundException {
// 创建一个远程对象
UserService hello = new UserServiceImpl();
//绑定的URL标准格式为:rmi://host:port/name
Naming.bind("rmi://localhost:8080/UserService", hello);
System.out.println("======= 启动RMI服务注册成功! =======");
System.in.read(new byte[1024]);
}
}
- 接口
package com.xiyou.rmi;
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* @author 92823
*/
public interface UserService extends Remote {
String getName(Integer id) throws RemoteException;
}
- 实现类
package com.xiyou.rmi;
import java.lang.management.ManagementFactory;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
/**
* @author 92823
*/
public class UserServiceImpl extends UnicastRemoteObject implements UserService {
protected UserServiceImpl() throws RemoteException {
}
@Override
public String getName(Integer id) {
return String.format("Thread :%s", ManagementFactory.getRuntimeMXBean().getName());
}
}
- 输出结果
我们可以发现线程名不一样,所以是远程调用,如果此时我们再次启动了一个一模一样的服务端,名字是
Naming.bind("rmi://localhost:8080/UserService", hello);
我们会发现启动报错
此时也进一步反映了不支持负载均衡。
分布式架构的三种解决方案
7. 基于反向代理的中心化架构
8. 嵌入应用内部的去中心化架构
9. 基于独立代理进程的Service Mesh架构
基于反向代理的集中式分布式架构
这是最简单和最传统的方案,在服务消费者和生产者之间,代理作为独立一层集中部署,由独立团队负责治理和运维。常用的集中式代理有硬件负载均衡器(F5),或者软件负载均衡器(Nginx),这种软硬结合两层代理也是业内常见的做法,兼顾配置的灵活性(Nginx比F5更加易于配置)
Http+Nginx方案的总结:
- 优点:简单快速,几乎没有什么学习成本
- 适用场景:轻量级分布式系统,局部分布式架构
- 瓶颈:Nginx中心负载,Http传输,Json序列化,开发效率,运维效率
嵌入应用内部的去中心化架构
这是很多互联网公司比较流行的一种做法,代理(包括服务发现和负载均衡逻辑)以客户库的形式嵌入在应用程序中。这种模式一般需要独立的服务注册中心组件配合,在服务启动的时候自动注册到注册中心并定期报心跳,客户端代理则发现服务并做负载均衡。我们熟悉的dubbo和Spring Cloud的Eureka+Ribbon就是这种方式实现的。
相比较第一代的架构他有以下特点:
- 去中心化,客户端直接连接服务端
- 动态注册和发现服务
- 高效稳定的网络传输
- 高效可容错的序列化
基于独立代理进程的架构(Service Mesh)
这种做法就是上面两种模式的一个这种,代理既不是独立集中部署,也不是嵌入到客户的应用程序中,而是作为独立进程部署在每一个主机上,一个主机上的多个消费者应用可以公用这个代理,实现服务发现和负载均衡,如下图所示。这个模式一般也需要独立的服务注册中心组件配合
三种架构的比较
模式 | 优点 | 缺点 | 适用场景 | 案例 |
---|---|---|---|---|
集中式负载架构 | 简单,集中式治理,与语言无关(通过Nginx等实现) | 配置维护成本高,多了一层IO(nginx),单点问题 | 大部分公司都使用,对运维有要求 | 亿贝,携程,早期互联网公司 |
客户端嵌入式架构 | 无单点,性能更好 | 客户端复杂,语言栈要求 | 中大规模公司,语言栈统一 | Dubbo,SpringCloud |
独立进程代理架构 | 无单点,性能更好,与语言无关 | 运维部署复杂,开发联调复杂 | 中大规模公司,对运维有要求 | Smart, Service Mesh |
三:Dubbo架构与设计说明
dubbo架构简要讲解
流程说明:
- Provider(提供者)绑定指定端口并启动服务
- 提供者连接注册中心,并发本机IP,端口,应用信息和提供服务信息发送至注册中心存储
- Consumer(消费者),连接注册中心,并发送应用信息,所求服务信息到注册中心
- 注册中心根据消费者所求服务信息匹配对应的提供者列表发送到Consumer应用缓存
- Consumer在发起远程调用的时候基于缓存的消费者列表选择其中一个Provider的IP进行调用
- Provider状态变更会实时通知注册中心,再由注册中心实时推送给Consumer
设计的意义:
- Consumer与Provider解耦,双方都可以横向增减节点数目
- 注册中心对本身可做对等集群,可以动态增减节点,并且任意一台宕掉之后,自动进行主从切换
- 去中心化(Consumer直接调用Provider),双方不直接依赖注册中心,即使注册中心全部宕机短时间内也不会影响服务的调用
- Provider状态变更会实时通知注册中心,再由注册中心实时推送给Consumer
Dubbo的整体设计
- config配置层:对外配置接口,以ServiceConfig,ReferenceConfig为中心,可以直接初始化配置类,也可以通过Spring解析配置生成配置类
- proxy服务代理层:服务接口透明代理,生成动态代理扩展接口为ProxyFactory
- registry注册中心层:封装服务地址的注册与发现,以服务URL为中心,扩展接口为RegistryFactory,Registry,RegistryService
- cluser路由层:封装多个提供者的路由以及负载均衡,并桥接注册中心,以Invoker为中心,扩展接口为Cluster,Directory,Router,LoadBalance
- monitor监控层:RPC调用次数和调用时间监控,以Statistics为中心,扩展接口为MonitorFactory,Monitor,MonitorService
- protocol远程调用层:封装RPC调用,以Invocation,Result为中心,扩展接口为Protocol,Invoker,Exporter
- exchange信息交换层:封装请求响应模式,同步转异步,以Request,Response为中心,扩展接口为Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer
- transport 网络传输层:抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel, Transporter, Client, Server, Codec
- serialize 数据序列化层:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool
流程图如下:
Dubbo中的SPI机制(重要)
在了解Dubbo的SPI之前,我们先了解下Java自带的SPI
java SPI的具体约定是:当服务的提供者,提供了服务的接口实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能够通过该jar包的META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就可以很好的找到服务接口的实现类,而不需要在代码里面制定。jdk提供服务实现查找的一个工具类java.util.ServiceLoader
演示JAVA SPI机制
- 编写接口
- 编写实现类
- 编辑META-INF/services/XXXX文件(这里其实就是在resources目录下新建META-INF等)注意这里的XXX文件名是接口的全路径,里面的实现类也是全路径
- 演示SPI
- 代码架构:
- SpiService
package com.xiyou.spi.service;
/**
* @author 92823
* 测试Java的Spi 先写一个接口 对应的是META-INF/services/com.xiyou.spi.service.SpiService
*/
public interface SpiService {
/**
* 对Java Spi的测试
* @return
*/
public String testSpi();
}
- SpiServiceImpl
package com.xiyou.spi.service.impl;
import com.xiyou.spi.service.SpiService;
/**
* @author 92823
* 实现的接口 对应的是对应的是META-INF/services/com.xiyou.spi.service.SpiService文件中的内容
*/
public class SpiServiceImpl implements SpiService {
@Override
public String testSpi() {
return "ok, This is Java SPI";
}
}
- 主启动类
package com.xiyou.spi.main;
import com.xiyou.spi.service.SpiService;
import java.util.Iterator;
import java.util.ServiceLoader;
/**
* @author 92823
* 主启动类
*/
public class Main {
public static void main(String[] args) {
// 直接利用ServiceLoader加载我们的接口,返回的是一个迭代器
// 我们会将META-INF/services/com.xiyou.spi.service.SpiService(与接口的名字一致)文件中的内容注入到这里
Iterator<SpiService> iterator = ServiceLoader.load(SpiService.class).iterator();
// 我们只有一个实现类
SpiService spiService = iterator.next();
// 如果成功的话,会调用SpiService的实现类的testSpi方法
String s = spiService.testSpi();
System.out.println(s);
}
}
- 在resources下新建目录/META-INF/services/接口的全类名,在里面写上其具体的实现类的全路径
文件中的内容
com.xiyou.spi.service.impl.SpiServiceImpl
Dubbo的SPI机制(这里没有做实验,我是直接拷贝的)
dubbo spi是在Java自带的SPI基础上加入了扩展点的功能,即每一个实现类都会对应一个扩展点的名称,其目的是应用可以基于这个扩展点进行相应的装配
演示Dubbo SPI机制
- 编写Filter过滤器
- 编写dubbo spi配置文件
- 装配自定义的Filter
dubbo的spi目录文件
dubbo spi文件内容:
luban=tuling.dubbo.server.LubanFilter
装配自定义的Filter
本篇博客参考了图灵学院的鲁班老师讲解,感谢老师的分享。