JavaEE 企业级分布式高级架构师(二十)微服务框架 SpringCloudAlibaba (2.2 版)(2)

Nacos源码解析

在这里插入图片描述

Nacos Discovery源码解析

对比Eureka

在这里插入图片描述

Client 的注册与心跳

总思路
  • 注册:Nacos Client 的注册最终是通过 Nacos 自定义的 HttpClient 发出的 POST 请求提交给 Nacos Server 的。
  • 心跳:Nacos Client 的注册最终是通过 Nacos 自定义的 HttpClient 发出的 PUT 请求提交给Nacos Server 的。
  • 封装:Nacos 自定义的 HttpClient 是对 JDK 的 HttpURLConnection 的封装,这些请求最终是通过 HttpURLConnection 的 POST 或 PUT 请求发送给 Nacos Server 的。
Nacos与SpringCloud整合
  • 无论是哪种组件作注册中心,若其要连接到 Spring Cloud 中,都需要遵循其规范。所以,我们在分析 Nacos 与 Spring Cloud 整合之前,就需要了解这个规范。
spring-cloud-commons 依赖
  • Spring Cloud 的 spring cloud commons 依赖中的自动配置类 AutoServiceRegistrationAutoConfiguration 完成了服务注册的自动配置。

在这里插入图片描述

  • 可以看到在第 35-36 行表示注入了 AutoServiceRegistration 的实例,AutoServiceRegistration是一个接口,其具体实现有一个 NacosAutoServiceRegistration,这里有个问题:这个实例是在哪里生成的呢?
spring-cloud-starter-alibaba-nacos-discovery 依赖
  • 我们要使用 Nacos Discovery,就需要导入 spring-cloud-starter-alibaba-nacos-discovery 依赖。而该依赖又依赖于 spring-cloud-commons 依赖。
  • spring-cloud-starter-alibaba-nacos-discovery 依赖加载了一个自动配置类 NacosServiceRegistryAutoConfiguration。

在这里插入图片描述

  • 这里我们就找到了 NacosAutoServiceRegistration 实例创建的地方了。
监听器接口的触发
  • NacosAutoServiceRegistration 类继承自 AbstractAutoServiceRegistration 类,而 AbstractAutoServiceRegistration 实现了 ApplicationListener 接口,即实现了该接口的 onApplicationEvent()方法,那么该方法什么时候触发?

在这里插入图片描述

onApplicationEvent()
bind() 绑定
  • 因为事件为 WebServerInitializedEvent,所以这个方法会在Spring Boot内置的Tomcat启动初始化以后触发执行。

在这里插入图片描述

register() 注册

在这里插入图片描述

  • 首先获取配置信息,判断一下是否允许注册。【类似于Dubbo中的仅注册、仅订阅功能】
  • 然后判断注册端口是否为负数。
  • 最后通过父类的 register() 方法,调用 NacosServiceRegistry 的 register() 方法
register(Registration) 注册Registration

在这里插入图片描述

  • 完成了服务实例的创建。
registerInstance() 注册服务实例

在这里插入图片描述
这里有一个判断instance.isEphemeral(),是否为临时实例。Nacos Client 实例分为两种:

  • 临时实例:默认类型,其会被注册到 Nacos Server 的内存。其健康检测机制是 Client 模式,Client 向 Server 发送心跳以上报其健康状态。
  • 持久实例:其会被注册到 Nacos Server 的内存并被持久化到磁盘。其健康检测机制为Server 模式,即 Server 会主动检测其 Client 的健康状态。
心跳定时任务

在这里插入图片描述

  • 到这里开启了一个一次性的定时任务,那么是如何完成心跳的呢?我们接着看下BeatTask的实现:

在这里插入图片描述

注册
registerInstance()

在这里插入图片描述

  • 组装注册请求所需的参数
reqApi()

在这里插入图片描述

  • 这里对 Nacos Server 的选取采用的是随机策略,并且是写死的。我们知道ZK里面比这里要复杂,是使用了两次 Shuffle 进行选取的。
callServer()

在这里插入图片描述

exchangeForm()

在这里插入图片描述

  • 继续跟踪 JdkHttpClientRequest:

在这里插入图片描述

心跳的发送

在这里插入图片描述

Nacos Server 源码基础

  • 在分析 Nacos Server 源码之前,需要先了解一些重要的类、接口,这样才能更好的理解Nacos Sever 对 Client 的处理原理与过程。
InstanceController 控制器
  • 在 Nacos 源码的 naming 模块下的 com.alibaba.nacos.naming.controllers 包下定义了很多的控制器类。其中 InstanceController 用于处理服务实例的心跳、注册等请求
@RestController
// UtilsAndCommons.NACOS_NAMING_CONTEXT = /v1/ns
@RequestMapping(UtilsAndCommons.NACOS_NAMING_CONTEXT + "/instance")
public class InstanceController {
	// 省略...
	@CanDistro
    @PostMapping
    @Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
    public String register(HttpServletRequest request) throws Exception {
    	// 省略...
    }
    // 省略...
    @CanDistro
    @PutMapping("/beat")
    @Secured(parser = NamingResourceParser.class, action = ActionTypes.WRITE)
    public ObjectNode beat(HttpServletRequest request) throws Exception {
		// 省略...
	}
    // 省略...
}
Service 类
  • 在 Nacos 客户端的一个微服务名称定义的微服务,在 Nacos 服务端是以 Service 实例的形式出现的。
  • Nacos 的自我保护机制与 Eureka 的自我保护机制对比:
    • 相同点:保护阈值都是个比例,0-1 范围,表示健康的 instance 占全部 instance 的比例。
    • 不同点:
      • 保护方式不同
      • 范围不同:Nacos 的阈值是针对某个具体 Service 的,而不是针对所有服务的。但 Eureka 的自我保护阈值是针对所有服务的。
/**
 * Service of Nacos server side
 * We introduce a 'service --> cluster --> instance' model, in which service stores a list of clusters, which contain a list of instances.
 */
// 'service --> namespace --> cluster --> instance' model
@JsonInclude(Include.NON_NULL)
public class Service extends com.alibaba.nacos.api.naming.pojo.Service implements Record, RecordListener<Instances> {
	private static final String SERVICE_NAME_SYNTAX = "[0-9a-zA-Z@\\.:_-]+";

    @JsonIgnore
    private ClientBeatCheckTask clientBeatCheckTask = new ClientBeatCheckTask(this);

    /**
     * Identify the information used to determine how many isEmpty judgments the service has experienced.
     */
    private int finalizeCount = 0;

    private String token;

    private List<String> owners = new ArrayList<>();

    private Boolean resetWeight = false;

    private Boolean enabled = true;

    private Selector selector = new NoneSelector();

    private String namespaceId;

    /**
     * IP will be deleted if it has not send beat for some time, default timeout is 30 seconds.
     */
    private long ipDeleteTimeout = 30 * 1000;

    private volatile long lastModifiedMillis = 0L;
    // 用于比较对比的属性信息汇总
    private volatile String checksum;

    /**
     * TODO set customized push expire time.
     */
    private long pushCacheMillis = 0L;
    // key为clusterName value为cluster
    private Map<String, Cluster> clusterMap = new HashMap<>();
	// 方法省略...
}

在这里插入图片描述

RecordListener 接口
  • Service 类实现了 RecordListener 接口,这个接口是一个数据监听的接口。

在这里插入图片描述

Record 接口
  • Record 是一个在 Nacos 集群中传输和存储的记录。

在这里插入图片描述

Cluster 类
  • 隶属于某一 Service 的 Instance 集群。
public class Cluster extends com.alibaba.nacos.api.naming.pojo.Cluster implements Cloneable {
    
    private static final String CLUSTER_NAME_SYNTAX = "[0-9a-zA-Z-]+";
    
    /**
     * a addition for same site routing, can group multiple sites into a region, like Hangzhou, Shanghai, etc.
     */
    private String sitegroup = StringUtils.EMPTY;
    
    private int defCkport = 80;
    
    private int defIpPort = -1;
    
    @JsonIgnore
    private HealthCheckTask checkTask;
    // 持久实例集合
    @JsonIgnore
    private Set<Instance> persistentInstances = new HashSet<>();
    // 临时实例集合
    @JsonIgnore
    private Set<Instance> ephemeralInstances = new HashSet<>();
    
    @JsonIgnore
    private Service service;
    
    @JsonIgnore
    private volatile boolean inited = false;
    
    private Map<String, String> metadata = new ConcurrentHashMap<>();
    // 方法省略...
}

在这里插入图片描述

Instance 类
  • 注册到 Nacos 中的具体服务实例。
/**
 * IP under service.
 *
 * @author nkorange
 */
@JsonInclude(Include.NON_NULL)
public class Instance extends com.alibaba.nacos.api.naming.pojo.Instance implements Comparable {
    
    private static final double MAX_WEIGHT_VALUE = 10000.0D;
    
    private static final double MIN_POSITIVE_WEIGHT_VALUE = 0.01D;
    
    private static final double MIN_WEIGHT_VALUE = 0.00D;
    
    private volatile long lastBeat = System.currentTimeMillis();
    
    @JsonIgnore
    private volatile boolean mockValid = false;
    
    private volatile boolean marked = false;
    
    private String tenant;
    
    private String app;
    
    private static final Pattern IP_PATTERN = Pattern
            .compile("(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}):?(\\d{1,5})?");
    
    private static final Pattern ONLY_DIGIT_AND_DOT = Pattern.compile("(\\d|\\.)+");
    
    private static final String SPLITER = "_";
    // 方法省略...
}

在这里插入图片描述

ServiceManager 类
  • Nacos 中所有 service 的核心管理者。
/**
 * Core manager storing all services in Nacos.
 */
@Component
public class ServiceManager implements RecordListener<Service> {

    /**
     * Map(namespace, Map(group::serviceName, Service)).
     * 注册表:这是一个双层map
     * 外层map的key为namespaceId,value为内层map
     * 内层map的key为serviceName, value为Service
     */
    private final Map<String, Map<String, Service>> serviceMap = new ConcurrentHashMap<>();

    // 该队列中存放的是来自于其它nacos中所有发生了变更的service
    private final LinkedBlockingDeque<ServiceKey> toBeUpdatedServicesQueue = new LinkedBlockingDeque<>(1024 * 1024);

    private final Synchronizer synchronizer = new ServiceStatusSynchronizer();

    private final Lock lock = new ReentrantLock();

    @Resource(name = "consistencyDelegate")
    private ConsistencyService consistencyService;

    private final SwitchDomain switchDomain;

    private final DistroMapper distroMapper;

    private final ServerMemberManager memberManager;

    private final PushService pushService;

    private final RaftPeerSet raftPeerSet;

    private int maxFinalizeCount = 3;

    private final Object putServiceLock = new Object();

    @Value("${nacos.naming.empty-service.auto-clean:false}")
    private boolean emptyServiceAutoClean;

    @Value("${nacos.naming.empty-service.clean.initial-delay-ms:60000}")
    private int cleanEmptyServiceDelay;

    @Value("${nacos.naming.empty-service.clean.period-time-ms:20000}")
    private int cleanEmptyServicePeriod;
    // 方法省略...
}

在这里插入图片描述

ConsistencyService 接口
  • 该实例用于提供 Nacos 集群节点间的数据同步服务。

在这里插入图片描述

Synchronizer 接口
  • 同步器,是当前 Nacos 主动发起的同步操作。

在这里插入图片描述

Server处理注册请求

  • 分析入口:在 Nacos 源码的 naming 模块下的 com.alibaba.nacos.naming.controllers 包下的控制器中的 InstanceController

在这里插入图片描述

  • 查看下 WebUtils 的两个获取 request 参数的方法:

在这里插入图片描述

创建并初始化 instance

在这里插入图片描述

注册 instance

在这里插入图片描述

public void addOrReplaceService(Service service) throws NacosException {
    // 同步到其它nacos
    consistencyService.put(KeyBuilder.buildServiceMetaKey(service.getNamespaceId(), service.getName()), service);
}
  • 跟踪下 recalculateChecksum() 方法:计算 checksum

在这里插入图片描述

在这里插入图片描述

将instance注册到注册表并同步给其它nacos

在这里插入图片描述

  • 继续跟踪 addIpAddresses() 方法:

在这里插入图片描述

在这里插入图片描述

Server 处理心跳请求

  • 分析入口:在 Nacos 源码的 naming 模块下的 com.alibaba.nacos.naming.controllers 包下的控制器中的 InstanceController

在这里插入图片描述

在这里插入图片描述

  • 继续跟踪处理心跳的任务:

在这里插入图片描述

  • 继续跟踪,获取PushService,推送更新:

在这里插入图片描述

OpenFeign与负载均衡

概述

OpenFeign 简介

在这里插入图片描述

  • 声明式 REST 客户端:Feign 通过使用 JAX-RS 或 SpringMVC 注解的装饰方式,生成接口的动态实现。
  • OpenFeign 可以将提供者提供的 Restful 服务伪装为接口进行消费,消费者只需使用“feign接口 + 注解”的方式即可直接调用提供者提供的 Restful 服务,而无需再使用 RestTemplate。

Ribbon 与 OpenFeign

  • 说到 OpenFeign,不得不提的就是 Ribbon。OpenFeign 默认 Ribbon 作为负载均衡组件。OpenFeign 直接内置了 Ribbon,即在导入 OpenFeign 依赖后,无需再专门导入 Ribbon 依赖了。
  • Ribbon 是 Netflix 公司的一个开源的负载均衡项目,是一个客户端负载均衡器,运行在消费者端。即在消费者端配置对提供者的负载均衡。这是与 Dubbo 不同的,Dubbo 可以在消费者端与提供者端均可配置负载均衡。

声明式Rest客户端OpenFeign

  • 父工程添加依赖:
<!-- openfeign 依赖 -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

创建消费者工程 04-consumer-feign-8080

  • 定义工程:复制 02-consumer-nacos-8080,并重命名为 04-consumer-feign-8080。
  • 定义 Feign 接口:
/**
 * Feign接口名及接口中的方法名一般会与业务接口的相同,但并不是必须的。
 * Feign接口名可以随意,方法名也可以随意,但方法参数及返回值必须与业务接口中相应方法的相同。
 */
@FeignClient(value = "msc-provider-depart") //该参数指定的是提供者的微服务名称,这个名称也称为Feign客户端名称
@RequestMapping("/provider/depart")
public interface DepartService {
    @PostMapping("/save")
    boolean saveDepart(@RequestBody Depart depart);
    @DeleteMapping("/del/{id}")
    boolean deleteById(@PathVariable("id") int id);
    @PutMapping("/update")
    boolean update(@RequestBody Depart depart);
    @GetMapping("/get/{id}")
    Depart findById(@PathVariable("id") int id);
    @GetMapping("list")
    List<Depart> list();
}
  • 修改 Controller 接口:
@RestController
@RequestMapping("/consumer/depart")
public class DepartController {
    @Autowired
    private DepartService departService;

    @PostMapping("/save")
    public Boolean save(@RequestBody Depart depart) {
        return departService.save(depart);
    }

    @DeleteMapping("/del/{id}")
    public Boolean delete(@PathVariable("id") int id) {
        return departService.deleteById(id);
    }

    @PutMapping("/update")
    public boolean update(@RequestBody Depart depart) {
        return departService.update(depart);
    }

    @GetMapping("/get/{id}")
    public Depart findById(@PathVariable("id") int id) {
        return departService.findById(id);
    }

    @GetMapping("/list")
    public List<Depart> list() {
        return departService.list();
    }
}
  • 修改启动类:

在这里插入图片描述

超时设置

  • OpenFeign 支持超时设置
feign:
  client:
    config:
      # 设置全局超时阈值
      default:
        connectTimeout: 5000
        # 读超时,从Feign客户端请求发出到接收到提供者响应,这段时间的超时阈值
        readTimeout: 2000
      # 设置指定Feign客户端的超时阈值,其优先级要高于全局的
      msc-provider-depart:  
        readTimeout: 5000
  • 为了方便 04-consumer-feign-8080 的测试,创建提供者工程 04-provider-nacos-8081,该工程复制于 02-provider-nacos-8081。该工程仅修改了 service 接口实现类的 findById()方法,为该方法添加一个 sleep(),使其运行超出 Feign 的 readTimeout 超时时限。

在这里插入图片描述

  • 测试:http://localhost:8080/consumer/depart/get/1

在这里插入图片描述

Gzip 压缩设置

  • Feign 支持对请求和响应进行 Gzip 压缩以提高通信效率。注意,这里的请求是指 Feign 向提供者所提交的请求,响应是指 Feign 向客户端作出的响应。

在这里插入图片描述

在这里插入图片描述

Ribbon负载均衡

  • 前面的消费者例子是通过 Feign 接口来消费微服务的,但没体现出其负载均衡的功能。

示例演示

  • 修改提供者接口实现类:

在这里插入图片描述

  • 启动多个提供者:通过修改端口号启动多次 04-provider-nacos-8081,启动多个提供者。

在这里插入图片描述

  • 测试:启动消费者,访问 http://localhost:8080/consumer/depart/get/1,可以看到默认采用轮询的负载均衡

更换负载均衡策略

  • 若要更换负载均衡策略,则首先要了解负载均衡策略的定义接口 IRule。
  • Ribbon 默认采用的是 RoundRobinRule,即轮询策略。但通过修改消费者工程的配置文件,或修改消费者的启动类或 JavaConfig 类可以实现更换负载均衡策略的目的。
  • 方式一:修改配置文件
# <clientName>.<clientConfigNameSpace>.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.WeightedResponseTimeRule
msc-provider-depart:
  ribbon:   # 指定负载均衡策略为随机策略
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
  • 方法二:JavaConfig 类
@Configuration
public class LoadBalanceConfig {
	// 当代码与配置文件中均设置了负载均衡策略时,代码中的优先级高
    @Bean
    public IRule loadBalanceRule() {
        return new RandomRule();
    }
}

定义负载均衡策略

  • 该负载均衡策略的思路是:从所有可用的 provider 中排除掉指定端口号的 provider,剩余 provider 进行随机选择。
/**
 * 从所有可用的provider中排除掉指定端口号的provider,剩余provider进行随机选择
 */
public class CustomRule implements IRule {
    private ILoadBalancer lb;
    private List<Integer> excludePorts;

    public CustomRule() {
    }

    public CustomRule(List<Integer> excludePorts) {
        this.excludePorts = excludePorts;
    }

    @Override
    public void setLoadBalancer(ILoadBalancer lb) {
        this.lb = lb;
    }

    @Override
    public ILoadBalancer getLoadBalancer() {
        return lb;
    }

    @Override
    public Server choose(Object key) {
        // 获取到所有可用的server
        List<Server> servers = lb.getReachableServers();
        // 获取从所有server中排除指定的端口号后剩余的server
        List<Server> availableServers = this.getAvailableServers(servers);
        // 从剩余server中随机选择一个
        return this.getAvailableRandomServers(availableServers);
    }

    /**
     * 获取从所有server中排除指定的端口号后剩余的server
     */
    private List<Server> getAvailableServers(List<Server> servers) {
        if (excludePorts == null || excludePorts.size() == 0) {
            return servers;
        }

        return servers.stream()
                .filter(server -> excludePorts.stream().noneMatch(port -> server.getPort() == port))
                .collect(Collectors.toList());
    }

    private Server getAvailableRandomServers(List<Server> availableServers) {
        int index = new Random().nextInt(availableServers.size());
        return availableServers.get(index);
    }
}
  • 修改JavaConfig
@Configuration
public class LoadBalanceConfig {
//    @Bean
//    public IRule loadBalanceRule() {
//        return new RandomRule();
//    }

    @Bean
    public IRule loadBalancer() {
        List<Integer> ports = new ArrayList<>();
        ports.add(8082);
        return new CustomRule(ports);
    }
}

OpenFeign、Ribbon源码解析

Spring Cloud LoadBalancer

  • 由于 Netflix 对于 Ribbon 的维护已经暂停,所以 Spring Cloud 对于负载均衡建议使用由其自己定义的 Spring Cloud LoadBalancer。对于 Spring Cloud LoadBalancer 的使用非常简单。
  • 添加依赖:
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
  • 修改配置文件:直接在 04-consumer-feign-8080 工程上修改,在配置文件中直接添加如下配置,将 Ribbon 禁用。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

讲文明的喜羊羊拒绝pua

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值