目录
什么是 Spring Cloud
构建分布式系统不仅复杂而且容易出错。Spring Cloud 为最常见的分布式系统模式提供了一种简单且易于接受的编程模型,帮助开发人员构建有弹性的、可靠的、协调的应用程序。Spring Cloud 构建于 Spring Boot 之上,使得开发者很容易入手并快速应用于生产中。
简单来说:SpringCloud提供了一套简易的编程模型(实现了服务发现注册 、配置中心 、消息总线 、负载均衡 、断路器 、数据监控),让我们可以在使用springboot的基础上简单实现微服务的构建。
Eureka服务的注册与发现
Eureka是基于REST的服务,用于定位服务,以实现云端中间件层服务发现和故障转移,可以与前面的RPC框架进行对比,类似于Dubbo的注册中心。
房东们可以将信息注册到Eureka Service中,客户们只需要找到Eureka Service得到注册的数据就可以了。
Eureka 包含两个组件:Eureka Server 和 Eureka Client
Eureka Server 提供服务注册,各个节点启动后,会在EurekaServer中进行注册,这样Eureka Server中的服务注册表中将会储存所有课用服务节点的信息,服务节点的信息可以在界面中直观的看到.
Eureka Client 是一个Java客户端,用于简化EurekaServer的交互,客户端同时也具备一个内置的,使用轮询负载算法的负载均衡器。在应用启动后,将会向EurekaServer发送心跳(默认周期为30秒) 。如果EurekaServer在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除掉 (默认周期为90s).
Eureka的配置
server:
port: 7001
#Eureka
eureka:
instance:
hostname: localhost # 服务端的实例名称
client:
register-with-eureka: false #表示是否向 Eureka 注册中心注册自己,这里是作为服务器不需要注册
fetch-registry: false # false 表示自己是注册中心,客户端为true
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
下面是源码里面默认的地址
public EurekaClientConfigBean() {
this.serviceUrl.put("defaultZone", "http://localhost:8761/eureka/");
记得在启动类里面开启EurekaServer
Register服务注册
Eureka Client会启动的时候,将自身的服务的信息发送到Eureka Server当中进行注册。
在Maven的依赖包下,找到eureka-client依赖包,在其中的DiscoveryClient类,该类中含有Eureka Client向Eureka Server的注册的方法。
查看接口LookupService,这个接口提供查找活动实例的查找服务。
/**
* Lookup service for finding active instances.
*
* @author Karthik Ranganathan, Greg Kim.
* @param <T> for backward compatibility
*/
public interface LookupService<T>
EurekaClient里面定义了一些注册接口,返回当前在EurekaServer上的实例状态。
public Applications getApplicationsForARegion(@Nullable String region);
/**
* Get all applications registered with a specific eureka service.
*
* @param serviceUrl The string representation of the service url.
* @return The registry information containing all applications.
*/
public Applications getApplications(String serviceUrl);
public InstanceInfo.InstanceStatus getInstanceRemoteStatus();
客户端注册的是在DiscoveryClient类的注册方法register(),通过Http请求向Eureka Server注册,最后返回状态码204.
boolean register() throws Throwable {
logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
EurekaHttpResponse<Void> httpResponse;
try {
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}
该方法也被InstanceInfoReplicator 类的run()方法调用。
在DiscoveryClient类中的initScheduledTasks()初始化任务方法实例化了InstanceInfoReplicator 类如下
// InstanceInfo replicator
instanceInfoReplicator = new InstanceInfoReplicator(
this,
instanceInfo,
clientConfig.getInstanceInfoReplicationIntervalSeconds(),
2); // burstSize
而在DiscoveryClient类有参构造中地调用了
initScheduledTasks()方法。如下
@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
// finally, init the schedule tasks (e.g. cluster resolvers, heartbeat, instanceInfo replicator, fetch
initScheduledTasks();
Register服务注册过程应是客户端->调用DiscoveryClient类的有参构造->初始化任务initScheduledTasks()->初始化InstanceInfoReplicator 类->启动run()方法->调用DiscoveryClient类的注册方法register(),最后会在DiscoveryClient类中判断是否注册成功。
Eureka集群
主要是帮助我们预防注册中心出现问题,试想你只有一个注册中心,并且突然出现问题,那会让人非常崩溃。Eureka集群可以很好的帮助我们解决这个问题。
你可以把它们想象成两两连接的物体,当服务注册到一个节点里面时,这个节点会调用有关注册类更新自己内部的信息,同时把请求发送到其他节点,把所有节点都遍历一遍。
Ribbon
Spring Cloud Ribbon 是基于Netflix Ribbon 实现的一套客户端负载均衡的工具。
负载均衡(LB)简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA (高用)
Ribbon主要工作:
- 从Eureka集群中查询可用的服务列表
- 通过负载均衡算法(默认轮询)得到一个比较好的服务提供者
在接口IRule
里面是定义的负载均衡的规则,可以修改我们默认的负载均衡的算法。
public interface IRule{
/*
* choose one alive server from lb.allServers or
* lb.upServers according to key
*
* @return choosen Server object. NULL is returned if none
* server is available
*/
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}
轮询算法在RoundRobinRule类中,继承关系如下
进入RoundRobinRule类,发现其重写了choose()方法
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
返回的是自己定义的choose方法,下面就是自定义的choose(),知道这之后我们也可以简单的写一个choose()算法。
public Server choose(ILoadBalancer lb, Object key) {
//省略部分代码
return server;
}
自定义Ribbon客户端
您可以使用.ribbon.*中的外部属性来配置Ribbon客户端的某些位,这与使用Netflix API本身没有什么不同,只能使用Spring Boot配置文件。本机选项可以在CommonClientConfigKey(功能区内核心部分)中作为静态字段进行检查。
Spring Cloud还允许您通过使用@RibbonClient声明其他配置(位于RibbonClientConfiguration之上)来完全控制客户端。
这里要注意将这个配置文件不要主启动类所在的包同级。
在这种情况下,客户端由RibbonClientConfiguration中已经存在的组件与FooConfiguration中的任何组件组成(后者通常会覆盖前者)
我们可以自己设置一个配置文件来覆盖掉原有的文件
@Configuration
public class MyRule {
@Bean
public IRule myRule(){
return new MyRandomRule();
}
}
自己写一个算法去替代默认的轮询算法
@Configuration
public class MyRandomRule extends AbstractLoadBalancerRule {
// 先随机访问前面一半的服务,再随机访问后面一半的服务
private int total=1; //标志位
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
return null;
}
Server server = null;
while (server == null) {
if (Thread.interrupted()) {
return null;
}
List<Server> upList = lb.getReachableServers(); //获得存活的服务
List<Server> allList = lb.getAllServers();//获得全部服务
int serverCount = allList.size();
if (serverCount == 0) {
/*
* No servers. End regardless of pass, because subsequent passes
* only get more restrictive.
*/
return null;
}
if(total>0){
int index = chooseRandomInt((serverCount>>1));//在区间内随机获得一个地址
}
else{
int index = chooseRandomInt((serverCount>>1),serverCount);//在区间内随机获得一个地址
}
int index = chooseRandomInt(serverCount);//在区间内随机获得一个地址
server = upList.get(index);//从存活的列表中获得
//=================================
total = -total;
//=================================
if (server == null) {
/*
* The only time this should happen is if the server list were
* somehow trimmed. This is a transient condition. Retry after
* yielding.
*/
Thread.yield();
continue;
}
if (server.isAlive()) {
return (server);
}
// Shouldn't actually happen.. but must be transient or a bug.
server = null;
Thread.yield();
}
return server;
}
protected int chooseRandomInt(int serverCount) {
return ThreadLocalRandom.current().nextInt(serverCount);
}
protected int chooseRandomInt(int start,int end) {
return ThreadLocalRandom.current().nextInt(start, end);
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
// TODO Auto-generated method stub
}
}
Hystrix
什么是Hystrix?
在分布式环境中,不可避免地会有许多服务依赖项中的某些失败。Hystrix 是一个库,可通过添加等待时间容限和容错逻辑来帮助您控制这些分布式服务之间的交互。Hystrix 通过隔离服务之间的访问点,停止服务之间的级联故障并提供后备选项来实现此目的,所有这些都可以提高系统的整体弹性。
简单来说,Hystrix是处理分布式系统延迟和容错的开源库。它能保证在其中一个微服务出现故障时,不会导致整体服务出现问题,避免级联故障提高系统弹性。
服务雪崩
当用户发出请求时,正常会经过下面的几个服务
但是一旦某个服务发生异常
这个服务将会阻塞用户的请求,对前面的微服务(如A)的调用就会消耗更多的系统资源,从而引起系统崩溃,这被成为雪崩效应。
服务熔断
用简单的 @HystrixCommand 注解来标注某个方法,这样 Hystrix 就会使用断路器对这个方法包装。每当调用时间超过指定时间时,断路器将会中断对这个方法的调用。
我们可以设置注解的很多属性,比如设置超时时间等等
@HystrixCommand(
commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1200")})
public User get(@PathVariable("id") Long id){
//省略
}
服务降级
这个注解可以对某些属性进行设置,可以对备用方法进行设置。
/**
* Specifies a method to process fallback logic.
* A fallback method should be defined in the same class where is HystrixCommand.
* Also a fallback method should have same signature to a method which was invoked as hystrix command.
* for example:
* <code>
* @HystrixCommand(fallbackMethod = "getByIdFallback")
* public String getById(String id) {...}
*
* private String getByIdFallback(String id) {...}
* </code>
* Also a fallback method can be annotated with {@link HystrixCommand}
* <p/>
* default => see {@link com.netflix.hystrix.contrib.javanica.command.GenericCommand#getFallback()}
*
* @return method name
*/
String fallbackMethod() default "";
这里可以看下源码中的例子,fallbackMethod = "getByIdFallback"
,修改下后面的方法名"getByIdFallback"
就可以使用。
@HystrixCommand(fallbackMethod = "hystrixGet")
public User get(@PathVariable("id") Long id){
//代码省略
}
public User hystrixGet(@PathVariable("id") Long id){
// 做服务降级
// 返回当前人数太多,请稍后使用
}
Zuul微服务网关
什么是Zuul?
ZUUL 是从设备和 web 站点到 Netflix 流应用后端的所有请求的前门。作为边界服务应用,ZUUL 是为了实现动态路由、监视、弹性和安全性而构建的。它还具有根据情况将请求路由到多个 Amazon Auto Scaling Groups 的能力
Zuul包含了对请求的路由(用来跳转的)和过滤。路由功能负责将外部请求转发到具体的微服务实例上,可以用来实现外部访问统一入口,而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验,服务聚合等功能的基础。
首先,Zuul会向 Eureka 进行注册,注册完后可以拿到consumer的信息(ip端口等),可以用作路由映射来保护真实的端口。
这里的mydept.path
可以看做是mydept.serviceId的别名,可以通过两个不同的网址来访问。使用ignored-services
后就不同通过springcloud-provider去访问了。
zuul:
routes:
mydept.serviceId: springcloud-provider
mydept.path: /path/**
ignored-services: springcloud-provider # 不能再使用该路径访问
服务名屏蔽
上面也提到了ignored-services
可以屏蔽指定的服务名字,通过这里可以屏蔽所有的服务名。
ignored-services: "*" # 隐藏全部的
路径屏蔽
通过设置ignore-patterns
,Zuul 还可以指定屏蔽掉的路径 URI,通过该方式可以限制用户的权限。da
的请求我们就可以过滤掉了
zuul:
ignore-patterns: **/da/**
Spring Cloud Config
随着微服务越来越庞大,消费者、提供者、Zuul等系统都会有自己的配置文件,这样就会导致配置文件在修改的时候需要去每个应用下一个一个寻找配置文件然后修改配置文件再重启应用,假如能够对配置文件进行统一的管理就好了。
Spring Cloud Config就是能够让我们统一对配置文件进行动态的管理,在项目运行时也可以进行修改。
SpringCloud Config分为服务端和客户端两部分
- 服务端:也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口.
- 客户端:则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。
那么在应用运行时去更改远程配置仓库(Git)中的对应配置文件,那么依赖于这个配置文件的已启动的应用如何进行动态更改呢?
利用Spring Cloud Bus
当git文件更改的时候,通过pc端用post请求向config-client发送请求/bus/refresh/请求,更新请求会传递到消息总线,在由消息总线传播到其他的Config Client
端,Config Client
端会发送更新请求到Config Server
端,Config Server
端则向云端发送请求,返回更新的配置。
参考资料
https://javaguide.cn/…
https://www.kuangstudy.com/…
https://juejin.cn/…
https://blog.csdn.net/…