1. 分布式服务框架Dubbo

1.1. Dubbo原理

分布式服务框架

1.2. Dubbo支持哪些序列化协议

  1. Dubbo支持哪些通信协议
    • dubbo://协议
      • 默认协议,单一长连接,进行的NIO异步通信,基于hessian作为序列化协议
      • 特性,传输数据量小(每次100k以下);并发量高,适用多个消费者的情况
    • rmi://协议
      • 采用JDK标准的Java.rmi.*实现
      • 特性,多个短链接,适合消费者和提供者差不多的情况,适用文件传输
    • hessian://协议
      • http通信,采用servlet暴露服务,基于hessian序列化协议
      • 多个短链接,适用于提供者数量多的情况,适用文件传输
    • http://协议
      • 基于http表单的远程通用协议,走单表序列化
    • thrift://协议
      • 在原生协议的基础上添加了一些额外的头信息,如service name,magic number等
    • webservice://协议
      • 基于apache cxf的fronted-simple和transports-http实现,SOA文本序列化
    • memcached://协议
      • RPC协议
    • redis://协议
      • RPC协议
    • rest://协议
      • 实现REST调用支持
    • grpc://协议
      • 适用HTTP/2通信,想利用Stream、反压、Reactive变成能力的开发者适用
  2. 支持的序列化协议

支持hessian,java二进制序列化,SOAP文本序列化,json多种序列化协议

1.3. Dubbo的负载均衡策略和集群容错策略

4中负载均衡策略,6种集群容错策略

1.3.1. Dubbo的负载均衡策略

  1. RandomLoadBalance,默认策略,随机调用;可以对provider设置不同的权重,按照权重来负载均衡。

    算法思想:假设有一组服务器servers=[A,B,C],对应权重为weights=[5,3,2],总和为10.把权重平铺在一纬坐标值上,则[0,5]服务器A,[5,8]服务器B,[8,10] 属于服务器C.随机数生成器生成一个范围在[0,10]之间的随机数,然后计算会落在哪个区间的服务器,坐标轴上区间范围大的,随机数生成的数字就有更大概率落到此区间

  2. RoundRobinLoadBalance,均匀分发

    算法思想:均匀地将流量打到各个机器上,但是如果各个机器性能不同,容易导致性能差的机器负载过高。所以需要调整权重,让性能差的机器承载权重小一些,流量少一些

  3. LeastActiveLoadBalance,最小活跃数负载均衡,活跃调用数越小,表明该服务提供者效率越高,单位时间内可处理更多请求,此时请求会优先分配给该服务提供者

    算法思想:每个provider对应一个活跃数active。初识情况,provider的active均为0.每收到一个请求,对应的provider的active会加1,处理完请求后,active会减1.所以,如果provider性能好,处理请求的效率就越高,active下降的越快。也引入了权重值,所以此算法是基于加权最小活跃数算法实现

  4. ConsistenHashLoadBalance,一致性hash

    算法思想:一致性Hash算法,相同参数请求一定分发到一个provider去。如果需要的不是随机负载均衡策略,要一类请求都到一个节点,那就走一致性Hash策略

1.3.2. 集群容错策略

  1. Failover Cluster模式,默认这个,失败自动切换,自动重试其它机器,常见于读操作
  2. Failfast Cluster模式,一次调用失败就立即失败,常见幂等性的写操作,如新增一条记录
  3. Failsafe Cluster模式,出现异常时忽略,用于不重要的接口调用,如记录日志
  4. Failback Cluster模式,失败了后台自动记录请求,然后定时重发,适合写消息队列失败
  5. Forking Cluster模式,并行调用多个provider,只要一个成功就立即返回。用于实时性要求较高的读操作,但是会浪费更多服务资源
  6. Broadcast Cluster模式,逐个调用所有的provider,任何一个出错则报错。用于通知所有提供者更新缓存或日志等本地资源信息

1.4. Dubbo的SPI思想

service provider interface

1.4.1. spi概念

一个接口,有三个实现类,在系统运行时这个接口该选择哪个实现类?就需要根据指定的配置或者是默认的配置,找对应的实现类加载进来,用这个实现类的实例对象。插件扩展的场景

1.4.2. java spi思想的体现

经典思想JDBC。java定义了一套jdbc的接口,并没有提供jdbc的实现类,所以可以使用mysql-jdbc-connector.jar引入或者oracle-jdbc-connector.jar

1.4.3. dubbo的spi思想

Protocol 接口,在系统运行的时候,,dubbo 会判断一下应该选用这个 Protocol 接口的哪个实现类来实例化对象来使用,它会去找一个你配置的 Protocol,将你配置的 Protocol 实现类,加载到 jvm 中来,然后实例化对象,就用你的那个 Protocol 实现类就可以了

Protocol protocol=ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol http=com.alibaba.dubbo.rpc.protocol.http.HttpProtocol hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol

1.5. 基于Dubbo的服务治理

服务治理:调用链路自动生成;服务访问压力以及时长统计

1.5.1. 服务降级

接口+Mock后缀,如:


<dubbo:service interface="com.zhss.service.HelloService" ref="helloServiceImpl" timeout="10000"/><bean
id="helloServiceImpl" class="com.zhss.service.HelloServiceImpl"/>

降级逻辑 public class HelloServiceMock implements HelloService { public void sayHello() { // 降级逻辑 }}

1.5.2. 失败重试/超时重试


<dubbo:reference id="xxxx" interface="xx" check="true" async="false" retries="3" timeout="2000"/>

timeOut:一般设置为200ms,retries:设置重试次数

1.6. 分布式服务的幂等性如何设计

场景:假如有个服务部署在5台机器上,有个接口是付款接口,用户在前端操作,一个订单不小心发起了两次支付请求,然后分散在了这个服务部署的不同机器上

1.6.1. 如何保证幂等性?

  • 对于每个请求必须有一个唯一的标识,如:订单支付请求,订单id需要唯一
  • 每次处理完请求后,必须有一个记录标识这个请求处理过了,如在mysql中记录个状态
  • 每次接收请求需要进行判断,判断之前是否处理过。唯一键约束

1.7. 分布式服务接口请求顺序如何保证

场景:服务A调用服务B,先插入再删除。结果俩请求过去,落在不同机器上,可能因为插入请求因为某些原因执行慢了,导致删除请求先执行了

1.7.1. 如何保证顺序

  • 尽量合并成一个操作,避免此问题产生
  • 可以使用Dubbo的一致性hash负载均衡策略,将比如某一个订单id对应的请求都分发到某个机器上,然后可以将某个订单id对应的请求放到一个内存队列中,强制排队,来确保顺序性

1.8. 自己设计一个类似Dubbo的RPC框架

  1. 注册中心服务注册,可以用zk
  2. 消费者需要去注册中心拿对应服务信息,而且每个服务可能存在于多台机器上
  3. 发起一次请求,基于动态代理,面向接口获取到一个动态代理,然后这个代理找到服务对应的机器地址
  4. 使用简单的负载均衡策略确定向哪个机器发送请求
  5. 使用netty,nio方式,使用hessian序列化协议
  6. 服务器侧,针对自己的服务生成一个动态代理,监听某个网络端口

1.9. CAP理论

CAP,Consistency一致性,Availability可用性,Partition tolerance分区容错性

  • C:所有节点访问同一份最新的数据副本
  • A:每次请求都能获取到正常响应,不保证获取的数据为最新数据
  • P:分区相当于对通信的时限要求。系统不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择

常用的CAP框架

  1. eureka(AP):保证可用,实现最终一致性。使用内置轮询负载均衡器去注册,有一个检测间隔时间,如果在一定时间没有收到心跳,才会移除该节点注册信息。eurekaAP的特性和请求间隔同步机制
  2. zookeeper(CP):强一致性。在选举leader时会停止服务,只有成功选举后才能提供服务

1.10. 分布式事务

这里列举五种事务方案,具体得看本身业务

1.10.1. 两阶段提交方案/XA方案

有一个事务管理器,负责协调多个数据库的事务,事务管理器先问各个数据库你准备好了吗?如果都回复ok,就正式提交事务,在各个数据库上执行操作;如果任何其中一个数据库回答不ok,那么就回滚

  • 场景应用:常见单应用实例内跨多个库的分布式场景

(缺点是严重依赖数据库层面来搞定复杂的事务,效率很低,不适合高并发场景)

1.10.2. TCC方案,try,confirm,cancel(金融场景选择方案)

  • try阶段,各个服务的资源做检测以及对资源进行锁定或者预留
  • confirm阶段,在各个服务中执行实际的操作
  • cancel阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,已经执行成功的业务逻辑回滚

对于一致性要求高、短流程、并发高的场景,会考虑TCC方案

1.10.3. saga方案(长事务)

业务流程中每个参与者都提交本地事务,若某一个参与者失败,则补偿前面已经成功的参与者

  • 适用场景:业务流程长、业务流程多;参与者包含其它公司或遗留系统服务,无法提供TCC模式要求的三个接口
  • 优势:
    • 一阶段提交本地事务,无锁高性能
    • 参与者可异步,高吞吐
    • 补偿服务易于实现,因为一个更新操作的反向操作易于理解
  • 缺点:不保证事务的隔离性

1.10.4. 可靠消息最终一致性方案

直接使用MQ来实现事务,如阿里的RocketMQ支持事务

1.10.5. 最大努力通知方案

  1. A本地事务执行完成后,发送消息到MQ
  2. 有专门消费MQ的最大努力通知服务,这个服务消费MQ然后写入数据库中记录,或者放入内存队列,接着调用系统B接口
  3. B执行成功就ok,要是执行失败,最大努力通知服务就定时尝试重新调用系统B,反复N次,最后还是不行就放弃

1.10.6. 本地消息表

严重依赖数据库的消息来管理事务,高并发场景并不适合

  1. A系统在自己本地一个事务里操作同时,插入一条数据到消息表
  2. A系统将这个消息发送到MQ
  3. B系统收到消息后,在一个事务里,往自己的本地消息表插入一条数据,同时执行其它业务操作,如果消息被处理过了,那此时这个事务回滚,保证不会重复处理消息
  4. B系统执行成功后,更新自己本地消息表状态以及A系统消息表状态
  5. B如果处理失败,那不更新消息表状态,此时A系统定时扫描自己的消息表,如果有未处理消息,再次发送到MQ,让B再次处理
  6. 保证了最终一致性,B失败,A会不断发消息,直至B成功

1.11. 为什么要拆分分布式系统

  1. 拆分服务,减少代码改动冲突,影响范围变小,开发效率高
  2. 减少服务发布影响范围,减少代码测试范围

1.11.1. 如何进行拆分

多轮拆分,根据业务性质拆分,如订单系统,商品系统等等

1.11.2. dubbo和服务拆分有什么关系

可以不用dubbo,纯http通信,但是就要考虑负载等问题,所以dubbo其实就是一个rpc框架,本地调用接口,dubbo会代理这个调用请求,跟远程机器网络通信,处理掉负载均衡、服务实例上下线自动感知、超时重试等

1.11.3. zk的一些场景

  1. 分布式协调,A系统发送了个消息到mq里去,然后B系统消费处理,那B系统消费处理后A如何知道结果?

解决:用ZK实现分布式系统之间的协调 A系统发送后在Zk上对某个节点的值注册个监听器,一旦B处理完就修改ZK那个节点的值,A系统立马就可以收到通知

  1. 分布式锁

1.12. 基于Hystrix实现高可用

Hystrix让我们在分布式系统中对服务间调用进行控制,加入调用延迟或者依赖故障的容错机制;还提供故障时的fallback降级机制

1.12.1. 有哪些限流算法

  1. 计数器

控制单位时间内的请求数量。

劣势:设每分钟请求数量60个,每秒处理1个请求,用户在 00:59 发送 60 个请求,在 01:00 发送 60 个请求 此时 2 秒钟有 120 个请求(每秒 60 个请求),远远大于了每秒钟处理数量的阈值。(突刺现象)

import java.util.concurrent.atomic.AtomicInteger;

public class Counter {

    /**
     * 最大访问数量
     */
    private final int limit = 10;

    /**
     * 访问时间差
     */
    private final long timeout = 1000;

    /**
     * 请求时间
     */
    private long time;

    /**
     * 当前计数器
     */
    private AtomicInteger reqCount = new AtomicInteger(0);

    public boolean limit() {
        long now = System.currentTimeMillis();
        if (now < time + timeout) {
            //单位时间内
            reqCount.addAndGet(1);
            return reqCount.get() <= limit;
        } else {
            //超出单位时间
            time = now;
            reqCount = new AtomicInteger(0);
            return true;
        }
    }

}
  1. 滑动窗口

对计数器的一个改进,增加一个时间粒度的度量单位,一分钟分为若干份(如6份,没10秒一份),在每份上设置独立计数器,

  1. leaky bucket漏桶

规定固定容量的桶,进入的水无法管控数量、速度,但是对于流出的水我们可以控制速度

劣势:无法应对短时间突发流量(桶满了就丢弃)


public class LeakBucket {

    /**
     * 时间
     */
    private long time;

    /**
     * 总量
     */
    private Double total;

    /**
     * 水流出速度
     */
    private Double rate;

    /**
     * 当前总量
     */
    private Double nowSize;

    public boolean limit() {
        long now = System.currentTimeMillis();

        nowSize = Math.max(0, (nowSize - (now - time) * rate));
        time = now;

        if ((nowSize + 1) < total) {
            nowSize++;
            return true;
        } else {
            return false;
        }

    }
}
  1. Token Bucket令牌桶

规定固定容量的桶,token 以固定速度往桶内填充, 当桶满时 token 不会被继续放入, 每过来一个请求把 token 从桶中移除, 如果桶中没有 token 不能请求。 可以准备一个队列,用来保存令牌,另外通过一个线程池定期生成令牌放到队列中,每来一个请求,就从队列中获取一个令牌,并继续执行。

public class TokenBucket {

    /**
     * 时间
     */
    private long time;

    /**
     * 总量
     */
    private Double total;

    /**
     * 水流出速度
     */
    private Double rate;

    /**
     * 当前总量
     */
    private Double nowSize;


    public boolean limit() {

        long now = System.currentTimeMillis();

        nowSize = Math.min(total, (nowSize + (now - time) * rate));
        time = now;

        if (nowSize < 1) {
            //桶里没有token
            return false;
        } else {
            //存在token
            nowSize -= 1;
            return true;
        }
    }

}

1.13. Dubbo如何在RPC调用时异步转同步?

TCP协议本身异步,发送完RPC请求后,线程不会等待RPC的响应结果

等待-通知机制,在dubbo之间RPC发起调用时,线程进入Timed_WAITING状态,阻塞调用线程;当rpc返回结果后,唤醒等待线程

// 创建锁与条件变量
private final Lock lock=new ReentrantLock();
private final Condition done=lock.newCondition();
// 调用方通过该方法等待结果
        Object get(int timeout){
        long start=System.nanoTime();
        lock.lock();
        try{
        while(!isDone()){
        done.await(timeout);
        long cur=System.nanoTime();
        if(isDone()||
        cur-start>timeout){
        break;
        }
        }
        }finally{
        lock.unlock();
        }
        if(!isDone()){
        throw new TimeoutException();
        }
        return returnFromResponse();
        }

        // RPC结果是否已经返回
        boolean isDone(){
        return response!=null;
        }

// RPC结果返回时调用该方法   
private void doReceived(Response res){
        lock.lock();
        try{
        response=res;
        if(done!=null){
        done.signal();
        }
        }finally{
        lock.unlock();
        }
        }
Copyright & copy lviter@163.com            updated 2024-02-06 09:54:56

results matching ""

    No results matching ""