- 简介
- 功能介绍
- 组件概述
- 组件详情
- 注册中心
- 配置中心
- 服务调用
- RestTemplate
- RestTemplate+Ribbon
- 源码分析
- 负载策略
- 重试&超时
- Fegin
- 服务路由
- 熔断降级
- 代码仓库
- 总结
通过本文可以给你带来什么?
- 熟悉掌握SpringCloud,了解其生态及掌握多个组件的原理。
- 上手实践,基于SpringCloud各组件搭建项目。
Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring Cloud并没有重复制造轮子,它只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包。
Spring Cloud的诞生就是为了做分布式,它是一个生态,定义了一套标准,目前基于其落地实现的有:Spring Cloud Netflix、Spring Cloud Alibaba,二者都可以提供一套分布式解决方案,例如:众多服务统一管理,服务之间的通信,服务熔断降级、链路追踪等。
功能介绍 组件概述Spring Cloud为我们提供的解决方案中包含很多组件,每个组件能起到什么作用?下面表格列出Spring Cloud生态常用组件以及其应用。
| Spring Cloud官方 | Spring Cloud Netflix | Spring Cloud Alibaba | |
|---|---|---|---|
| 服务发现/注册 | Consul | Eureka | Nacos |
| 服务调用 | OpenFeign/RestTemplate | Feign | Dubbo RPC |
| 负载均衡 | Loadbalancer | Ribbon | Dubbo LB |
| 服务路由 | Spring Cloud Gateway | Zuul | - |
| 配置中心 | Spring Cloud Config | - | Nacos |
| 服务熔断 | - | Hystrix | Sentinel |
通过下图可以清晰的了解各个组件在系统中的作用
组件详情 注册中心Eureka作为注册中心,分为服务端和客户端,服务端提供客户端注册,客户端可以在服务端拉取注册表到本地(所以客户端不会因为注册中心宕机而找不到其他服务,优先在本地找)。通过下图简单了解:
其中几个核心功能如下:
- Register
服务注册,向Eureka注册信息,以被发现。
#开启服务注册 eureka.client.register-with-eureka = true #注册中心地址 eureka.client.service-url.defaultZone=http://eureka-server1.com:8001/eureka/
- Renew
续租,心跳,30s发送一次心跳续租,通知Eureka实例时活动的,Eureka超过90s没发现更新,在注册表中移除。
#续租时间 eureka.instance.lease-renewal-interval-in-seconds=30 #该时间内未收到下次心跳移除实例 eureka.instance.lease-expiration-duration-in-seconds=90
- Fetch Registry
Eureka客户端从服务器拉取注册表缓存本地,之后客户端使用这些信息查找其他服务,定期(30s)更新。
#在注册中心拉取服务注册列表 eureka.client.fetch-registry=true #更新注册列表时间间隔 eureka.client.registry-fetch-interval-seconds=30
- Cancel
Eureka客户端在关闭时向Eureka服务器发送取消请求,服务器将从注册表删除实例。
- 自我保护机制
默认情况下,Eureka Server在一定时间内接收不到某个服务的续约会将其踢出服务列表。但是在网络故障时,上述行为就非常危险,因为服务正常,不应该被踢出。Eureka Server通过自我保护机制解决这个问题,当短时间内丢失过多服务时(小于85%),会保护注册列表中的服务不被注销,网络恢复后退出保护模式。
#开启自我保护机制 eureka.server.enable-self-preservation=true #比较实际心跳和预计心跳时间频率 eureka.server.eviction-interval-timer-in-ms=60000 #短时间丢失服务比例,小于该值进入保护模式 eureka.server.renewal-percent-threshold = 0.85
- 容错机制
在注册表内的服务一段时间内默认可用,每次调用不实时检查是否可用,提高可用,降低一致。
- 健康检查
server和client通过心跳保持服务状态,心跳正常,服务一直UP。
如果此服务DB连不上无法提供正常服务需要将服务下线,此时需要启动eureka的健康检查,同时需要设置健康状态为DOWN,代码如下:
#启动健康检查 eureka.client.healthcheck.enabled=true
public class HealthStatusEvent implements HealthIndicator {
private Boolean status=true;
@Override
public Health health() {
if(status)
return new Health.Builder().up().build();
return new Health.Builder().down().build();
}
public Boolean getStatus() {
return status;
}
public void setStatus(Boolean status) {
this.status = status;
}
}
根据实际情况通过调用setStatus设置服务健康状态。
- 高可用方式
1.eureka server作为客户端向其他eureka server注册。
server1配置:
eureka.instance.hostname=eureka-server1.com eureka.client.service-url.defaultZone=http://eureka-server2.com:8002/eureka/
server2配置:
eureka.instance.hostname=eureka-server2.com eureka.client.service-url.defaultZone=http://eureka-server1.com:8001/eureka/
2.多个eureka server作为单节点注册到多个eureka server中
server1配置:
eureka.instance.hostname=eureka-server1.com eureka.client.service-url.defaultZone=http://eureka-server1.com:8001/eureka/,http://eureka-server2.com:8002/eureka/,http://eureka-server3.com:8003/eureka/
server2配置:
eureka.instance.hostname=eureka-server2.com eureka.client.service-url.defaultZone=http://eureka-server1.com:8001/eureka/,http://eureka-server2.com:8002/eureka/,http://eureka-server3.com:8003/eureka/
server3配置:
eureka.instance.hostname=eureka-server3.com eureka.client.service-url.defaultZone=http://eureka-server1.com:8001/eureka/,http://eureka-server2.com:8002/eureka/,http://eureka-server3.com:8003/eureka/
注意:
由于是在一台服务器上搭建,所以需要指定eureka.instance.hostname,向系统的hosts文件添加ip映射
共有配置
不在本地读取配置,远程读取配置
手动刷新配置、自动刷新配置
解决的痛点:
服务调用SpringCloud服务之间的通信方式主要是以restful风格进行服务调用,常见组合RestTemplate+Ribbon、Fegin。其中RestTemplate是Spring提供用于访问Rest服务的客户端,Ribbon主要功能是提供客户端在服务调用时的负载均衡,而Fegin是一个声明式的Http客户端,只需要在接口上添加一个注解,使得服务调用更简单。
service-provider工程提供的接口
package com.bg.serviecprovider.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MainController {
@RequestMapping("/hi")
public String hi() {
return "hi,im provider";
}
}
service-consumer工程通过通过以下三种方式进行调用:
RestTemplate@GetMapping("/hi")
public String hi() {
RestTemplate restTemplate = new RestTemplate();
return restTemplate.getForObject("http://127.0.0.1:8081/hi", String.class);
}
RestTemplate+Ribbon
注入RestTemplate时加上@LoadBalanced注解就会具有负载均衡的能力,代码如下:
@Bean
//开启负载均衡
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
@GetMapping("/hi2")
public String hi2() {
return restTemplate.getForObject("http://service-provider/hi", String.class);
}
源码分析
为什么加上@LoadBalanced,RestTemplate就有负载均衡的能力呢?源码分析:
1.先看RestTemplate类,继承了InterceptingHttpAccessor,代码中有个类型为ClientHttpRequestInterceptor拦截器集合,Ribbon的功能就是在这扩展的。
public class RestTemplate extends InterceptingHttpAccessor implements RestOperations {
......
}
public abstract class InterceptingHttpAccessor extends HttpAccessor {
private final List interceptors = new ArrayList();
......
}
2.Interceptor在哪里注入给RestTemplate的?看LoadBalancerAutoConfiguration这个类,该类重点关注这个静态内部类LoadBalancerInterceptorConfig,其往spring注入了LoadBalancerInterceptor这个Bean,实现了上面所讲的ClientHttpRequestInterceptor接口,并赋值给RestTemplate。
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({RestTemplate.class})
@ConditionalOnBean({LoadBalancerClient.class})
@EnableConfigurationProperties({LoadBalancerRetryProperties.class})
public class LoadBalancerAutoConfiguration {
......
@ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"})
static class LoadBalancerInterceptorConfig {
LoadBalancerInterceptorConfig() {
}
@Bean
public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
}
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
return (restTemplate) -> {
List list = new ArrayList(restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
}
}
}
3.接着看RestTemplate请求执行的过程,在执行doExecute()时获取一个ClientHttpRequest->InterceptingClientHttpRequest,调用request.execute()时会做两件事(1)遍历intercept,对请求的URL(http://service-provider/hi)进行处理,这里的intercept也就时步骤2的LoadBalancerInterceptor。(2)对处理后的URL进行真正的HTTP请求。
protectedT doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor responseExtractor) throws RestClientException { Assert.notNull(url, "URI is required"); Assert.notNull(method, "HttpMethod is required"); ClientHttpResponse response = null; Object var14; try { //InterceptingClientHttpRequest ClientHttpRequest request = this.createRequest(url, method); ...... response = request.execute(); ...... } catch (IOException var12) { ...... } finally { ...... } return var14; }
class InterceptingClientHttpRequest extends AbstractBufferingClientHttpRequest {
......
protected final ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException {
InterceptingClientHttpRequest.InterceptingRequestExecution requestExecution = new InterceptingClientHttpRequest.InterceptingRequestExecution();
return requestExecution.execute(this, bufferedOutput);
}
private class InterceptingRequestExecution implements ClientHttpRequestExecution {
......
public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
//这里执行LoadBalancerInterceptor
if (this.iterator.hasNext()) {
ClientHttpRequestInterceptor nextInterceptor = (ClientHttpRequestInterceptor)this.iterator.next();
return nextInterceptor.intercept(request, body, this);
} else {
//拦截器处理完后执行真正的HTTP请求
......
ClientHttpRequest delegate = InterceptingClientHttpRequest.this.requestFactory.createRequest(request.getURI(), method);
......
return delegate.execute();
}
}
}
}
4.intercept是如何处理URL的?调用intercept时有个loadBalancer,其实现为RibbonLoadBalancerClient,在执行loadBalancer的execute时将URL中的“service-provider”映射成对应的ip:port,期间包括负载策略、重试策略、服务列表的获取等功能。
//拦截器的处理
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
......
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
......
return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
}
}
public class RibbonLoadBalancerClient implements LoadBalancerClient {
......
public T execute(String serviceId, LoadBalancerRequest request, Object hint) throws IOException {
//获取“service-provider”的服务列表
ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
//通过负载策略选择具体的服务
Server server = this.getServer(loadBalancer, hint);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
} else {
RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
}
}
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
//这里是BaseLoadBalancer
return loadBalancer == null ? null : loadBalancer.chooseServer(hint != null ? hint : "default");
}
......
}
public class BaseLoadBalancer extends AbstractLoadBalancer implements
PrimeConnections.PrimeConnectionListener, IClientConfigAware {
//默认的负载策略
private final static IRule DEFAULT_RULE = new RoundRobinRule();
protected IRule rule = DEFAULT_RULE;
public BaseLoadBalancer() {
this.name = DEFAULT_NAME;
this.ping = null;
setRule(DEFAULT_RULE);
setupPingTask();
lbStats = new LoadBalancerStats(DEFAULT_NAME);
}
//添加服务
public void addServer(Server newServer) {
...
}
//设置服务列表
public void setServersList(List lsrv) {
......
}
//获取服务列表
public List getServerList(boolean availableOnly) {
return (availableOnly ? getReachableServers() : getAllServers());
}
//根据负载策略选择具体的服务
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}
}
简单提一嘴,Ribbon在2019年停止维护,2020年cloud版本删除了Ribbon的依赖由Loadbalancer顶替负载均衡,个人感觉跟Ribbon一样。
负载策略Ribbon内置了几种负载策略,体现在源码BaseLoadBalancer类中IRule,可以配置其他负载规则或者自定义实现规则。
| 负载策略 | 说明 |
|---|---|
| RoundRobinRule | 轮询策略,默认采⽤的策略 |
| RandomRule | 随机策略,从所有的提供者随机选一个 |
| RetryRule | 重试策略,先按照RoundRobinRule策略获取,若获取失败,则在指定的时限内重试 |
| BestAvailableRule | 最可⽤策略,选择并发量最⼩的提供者 |
| WeightedResponseTimeRule | 权重策略,根据响应时间做权重,时间越快权重越大 |
@Bean
public IRule getRule(){
return new RandomRule();
}
重试&超时
Fegin
pom文件中引入依赖
org.springframework.cloud spring-cloud-starter-openfeign
启动类加入@EnableFeignClients注解
新建FeginService接口并加上@FeignClient注解,value为要调用的服务名称。hi接口url与service-provider提供的接口url一致。
package com.bg.serviceconsumer.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(value = "service-provider")
public interface FeginService {
@GetMapping("/hi")
public String hi();
}
在需要的地方直接注入FeginService进行调用
@Autowired
private FeginService feginService;
@GetMapping("/hi3")
public String hi3() {
return feginService.hi();
}
服务路由
熔断降级
- 降级
- 熔断
- 隔离



