从0到1整体认知分布式系统

概述

  1. 分布式架构的发展历史与背景
  2. 如何着手架构一套分布式系统
  3. Dubbo结构与设计说明

一:分布式架构的发展历史与背景


理解分布式架构:

分布式架构是建立在网络之上的软件系统。正是因为软件的特性,所以分布式系统具有高度的内聚性和透明性。因此,网络和分布式系统之间的区别更多的在于高层软件(特别是操作系统),而不是硬件。

  • 我们简单理解就是原来是一个节点干活,只有一个JVM进行操作。现在分布式系统之后,变为多个节点多个JVM进行干活。

为什么会发展分布式架构?

  1. 稳定性和可用性这两个指标很难达到。比如:单点问题,一旦大型主机出现了故障,那么整个系统就会处于不可用的状态。但是对于大型机的使用机构来说,这种不可用导致的损失是非常巨大的。
  2. 单机处理能力存在瓶颈
  3. 升级单机处理能力的性价比越来越低
架构的发展历史:

随着互联网的发展,网站应用的规模不对扩大,常规的垂直应用架构已经无法应对,分布式服务架构以及流动计算架构势在必行,急需要一个治理系统确保架构有条不紊的演进。
在这里插入图片描述
在这里插入图片描述
单体架构
在这里插入图片描述
垂直架构
在这里插入图片描述
垂直架构的业务,数据库等都进行拆分,垂直拆分

分布式架构
在这里插入图片描述
在垂直架构的基础上,将公有的功能抽象出来,然后在每个需要用到的服务进行远程调用,这就是分布式架构

分布式架构所带来的成本:

分布式事务
分布式事务指的是一个操作,分成了几个小的操作在多个服务器上进行执行,要么多成功,要么多失败这些分布式事务要做的

不允许服务有状态
无状态服务就是指对单词请求的处理,不会依赖上次的请求结果,就是说,我们处理一次请求所需要的全部的信息,要么都包含在这个请求里,要么可以从外部获取到(比如说数据库),服务器本身是不存储任何信息的。因为我们如果是分布式架构,不一定下次请求过来的还是这个机子,也就是说可能JVM是有多个的,不一定下次请求的是同一个。

服务依赖关系复杂
服务A–>B–>C,如果我们对服务C进行了修改,那么我们可能会影响服务B和服务C,事实上当服务越来越多的时候,C的变动将会越来越困难。

部署运维成本增加
之前我们只有一个war包需要部署,只部署到一个节点,现在节点新增,成本变高。

源码管理的成本增加
原本一套或者几套源码现在拆分成几十个源码库,其中分支,tag都要进行相对应的管理

如何保证系统的伸缩性
伸缩性是指,当前服务器硬件升级之后或者新增服务器处理能力就应该相对应的提升

分布式会话
此仅仅针对应用层服务,不能够将session存储在一个服务器上,我们通常是将session直接存到redis中。

分布式JOB
通常定时任务只需要在一台机器上触发执行,分布式的情况下在哪台执行呢?

  • 最后我们可以直接通过一张图直观感受下,单体到分布式的区别:
    在这里插入图片描述

二: 如何选型分布式架构


  • 首先我们需要明确一点分布式框架最核心的功能就是远程调用,我们将公用的服务全部进行抽取,在使用的时候进行远程调用。

RPC远程调用
在这里插入图片描述
我们这里距离几种远程调用的方式。
这里距离几种比较熟悉的:RMI,Web Service,Http,Hessian

协议描述优点缺点
RMIJava远程调用,使用的是原生的二进制方式进行序列化简单易用,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的):

  1. 注册中心
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]);
    }
}

  1. 客户端
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 + " <=======");

    }
}

  1. 服务端
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]);
    }
}

  1. 接口
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;
}

  1. 实现类
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());
    }
}

  1. 输出结果
    在这里插入图片描述
    我们可以发现线程名不一样,所以是远程调用,如果此时我们再次启动了一个一模一样的服务端,名字是
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架构简要讲解

在这里插入图片描述
流程说明:

  1. Provider(提供者)绑定指定端口并启动服务
  2. 提供者连接注册中心,并发本机IP,端口,应用信息和提供服务信息发送至注册中心存储
  3. Consumer(消费者),连接注册中心,并发送应用信息,所求服务信息到注册中心
  4. 注册中心根据消费者所求服务信息匹配对应的提供者列表发送到Consumer应用缓存
  5. Consumer在发起远程调用的时候基于缓存的消费者列表选择其中一个Provider的IP进行调用
  6. Provider状态变更会实时通知注册中心,再由注册中心实时推送给Consumer

设计的意义:

  1. Consumer与Provider解耦,双方都可以横向增减节点数目
  2. 注册中心对本身可做对等集群,可以动态增减节点,并且任意一台宕掉之后,自动进行主从切换
  3. 去中心化(Consumer直接调用Provider),双方不直接依赖注册中心,即使注册中心全部宕机短时间内也不会影响服务的调用
  4. 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
  1. 代码架构:
    在这里插入图片描述
  2. 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();
}

  1. 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";
    }
}

  1. 主启动类
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);
    }
}

  1. 在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
在这里插入图片描述

本篇博客参考了图灵学院的鲁班老师讲解,感谢老师的分享。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值