分布式架构是建立在网络之上的软件系统。正是因为软件的特性,所以分布式系统具有高度的内聚性和透明性。因此,网络和分布式系统之间的区别更多的在于高层软件(特别是操作系统),而不是硬件。
为什么会发展分布式架构?
随着互联网的发展,网站应用的规模不对扩大,常规的垂直应用架构已经无法应对,分布式服务架构以及流动计算架构势在必行,急需要一个治理系统确保架构有条不紊的演进。
单体架构
垂直架构
垂直架构的业务,数据库等都进行拆分,垂直拆分
分布式架构
在垂直架构的基础上,将公有的功能抽象出来,然后在每个需要用到的服务进行远程调用,这就是分布式架构
分布式事务
分布式事务指的是一个操作,分成了几个小的操作在多个服务器上进行执行,要么多成功,要么多失败这些分布式事务要做的
不允许服务有状态
无状态服务就是指对单词请求的处理,不会依赖上次的请求结果,就是说,我们处理一次请求所需要的全部的信息,要么都包含在这个请求里,要么可以从外部获取到(比如说数据库),服务器本身是不存储任何信息的。因为我们如果是分布式架构,不一定下次请求过来的还是这个机子,也就是说可能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支持 | 不能跨语言 |
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方案的总结:
这是很多互联网公司比较流行的一种做法,代理(包括服务发现和负载均衡逻辑)以客户库的形式嵌入在应用程序中。这种模式一般需要独立的服务注册中心组件配合,在服务启动的时候自动注册到注册中心并定期报心跳,客户端代理则发现服务并做负载均衡。我们熟悉的dubbo和Spring Cloud的Eureka+Ribbon就是这种方式实现的。
相比较第一代的架构他有以下特点:
这种做法就是上面两种模式的一个这种,代理既不是独立集中部署,也不是嵌入到客户的应用程序中,而是作为独立进程部署在每一个主机上,一个主机上的多个消费者应用可以公用这个代理,实现服务发现和负载均衡,如下图所示。这个模式一般也需要独立的服务注册中心组件配合
模式 | 优点 | 缺点 | 适用场景 | 案例 |
---|---|---|---|---|
集中式负载架构 | 简单,集中式治理,与语言无关(通过Nginx等实现) | 配置维护成本高,多了一层IO(nginx),单点问题 | 大部分公司都使用,对运维有要求 | 亿贝,携程,早期互联网公司 |
客户端嵌入式架构 | 无单点,性能更好 | 客户端复杂,语言栈要求 | 中大规模公司,语言栈统一 | Dubbo,SpringCloud |
独立进程代理架构 | 无单点,性能更好,与语言无关 | 运维部署复杂,开发联调复杂 | 中大规模公司,对运维有要求 | Smart, Service Mesh |
设计的意义:
在了解Dubbo的SPI之前,我们先了解下Java自带的SPI
java SPI的具体约定是:当服务的提供者,提供了服务的接口实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能够通过该jar包的META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就可以很好的找到服务接口的实现类,而不需要在代码里面制定。jdk提供服务实现查找的一个工具类java.util.ServiceLoader
演示JAVA SPI机制
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();
}
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);
}
}
com.xiyou.spi.service.impl.SpiServiceImpl
Dubbo的SPI机制(这里没有做实验,我是直接拷贝的)
dubbo spi是在Java自带的SPI基础上加入了扩展点的功能,即每一个实现类都会对应一个扩展点的名称,其目的是应用可以基于这个扩展点进行相应的装配
演示Dubbo SPI机制
luban=tuling.dubbo.server.LubanFilter
本篇博客参考了图灵学院的鲁班老师讲解,感谢老师的分享。