Spring Cloud Alibaba学习笔记
Nacos源码解析
- 源码github下载地址:https://github.com/alibaba/nacos/tree/client-1.3.3
- 导入IDEA后,运行需要配置参数:
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 禁用。