初探SpringCloud

如期而至,关于什么是 SpringCloud 这里不多说,之前的笔记已经把 SpringCloud 生态的主要技术都做了解释,但是这里对笔记进行补充一下,关于微服务、微服务架构,以及 SpringBoot 与 Cloud、Dubbo 之间微妙的关系做个简单的解释~
然后就是实践部分,先搞一个简单的 Demo 出来试试~

微服务:一种架构风格,重点在个体,拆分为单个的模块,具体的一个服务,仅关注自己负责的,比如现在流行使用 SpringBoot 来构建。
微服务架构:重点在整体,关注各个微服务之间的关系,如何治理好这些微服务,她要求各个微服务独立部署,能拥有“独立的空间(例如独立的数据库)”,现在流行使用 SpringCloud 提供的一站式解决方案构建。
微服务目前业内还没有一个准确的概念,上面的是我瞎说的 o( ̄▽ ̄)ゞ)) ̄▽ ̄)o
说到微服务架构就必须要分布式了,其中涉及的还有服务注册发现中心、服务调用方式(轻量级网络协议交互,REST、RPC)、服务监控、断路器、服务网关、分布式配置、服务跟踪、服务总线、数据流、批量任务等等。

可以看出,SpringCloud 作为全局的服务治理框架,它依赖于 SpringBoot,而与 Dubbo 的最显著区别就是 SpringCloud 使用 REST;Dubbo 使用 RPC。
使用 REST 更加灵活,并且语言无关,但是没有 RPC 的效率高,同时 RPC 也存在一些自身的问题。

当前由于 RPC 协议,注册中心元数据不匹配等问题,在面临微服务基础架构选型时,Dubbo 和 SpringCloud 只能二选一,所以才会出现两者的比较。
Dubbo 负责人表示之后会积极寻求适配到 SpringCloud 生态

如果 Dubbo 不停更 5 年的话,说不定又是另一番景象呢;然而现在 SCNetflix 又开始维护模式了,反而 SCAlibaba 活跃起来了。

微服务搭建

这个不是重点,但是确实前提条件,所以需要先用 SpringBoot 搭出至少两个微服务,一个做服务提供,一个做服务消费,然后在这个基础上加 SpringCloud。
关于 SpringCloud 的生态圈涉及的技术太多了,看了不少视频和资料,大部分都是对主要的几个技术来做介绍,实际上也大部分都是用这些技术,其他的也就不多说了,感兴趣的可以去官方或者中文网逛逛,挺全的。

Eureka

使用 Eureka 来实现服务的注册与发现,介绍之前说过了不多说,它分为客户端和服务端,一般会新建一个项目(微服务)作为服务端,这里就需要加入 SpringCloud 的依赖管理来负责做版本仲裁,然后也需要加入 EureKa 服务端的依赖,注意是以结尾的。

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>

然后在启动类使用注解开启功能,这个是 SpringCloud 的通用套路,先加依赖,然后在启动类添加 @EnableXXX 开启相关配置。

1
2
3
4
5
6
7
8
// 声明这是一个Eureka服务
@EnableEurekaServer
@SpringBootApplication
public class EurekaServer {
public static void main(String[] args) {
SpringApplication.run(EurekaServer.class, args);
}
}

最后,配置一下相关参数就可以启动测试了:

1
2
3
4
5
6
7
8
9
10
11
12
server:
# 服务端口
port: 6868
eureka:
client:
# 是否将自己注册到 Eureka 服务中,本身就是,所以无需注册
registerWithEureka: false
# 是否从Eureka中获取注册信息
fetchRegistry: false
# Eureka客户端与Eureka服务端进行交互的地址
serviceUrl:
defaultZone: http://127.0.0.1:${server.port}/eureka/

访问一下就可以看到相关的系统信息了,下面就可以把之前创建的微服务服务方注册到 Eureka 中了,如果某个微服务确定没有其他的微服务依赖它,那可以不用注册进来;
方法和之前的套路一样,加入 SpringCloud 的依赖管理,加入 Eureka 的依赖(可以是客户端也可以是服务端,推荐客户端),然后关键的地方就是配置文件的修改了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server:
port: 8081
spring:
application:
# 指定服务名,非常重要
name: microService-item

eureka:
client:
# 是否将自己注册到 Eureka 服务中,默认为 true
registerWithEureka: true
# 是否从Eureka中获取注册信息,默认为true
fetchRegistry: true
# Eureka 客户端与 Eureka 服务端进行交互的地址
serviceUrl:
defaultZone: http://127.0.0.1:6868/eureka/
# 将自己的 ip 地址注册到 Eureka 服务中
instance:
prefer-ip-address: true
# 可以手动指定地址,可以可以通过表达式来获取
# ${spring.application.name}:${server.port}
ip-address: 127.0.0.1

最后,在主启动类上加入 @EnableDiscoveryClient 注解,表名这是个客户端即可。
另一个作为消费端的微服务也是一样,唯一不同的是配置文件里就不需要将自己注册到 Eureka 服务中了,也不需要设置了 instance 了。
具体使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class ItemService {
@Autowired
private RestTemplate restTemplate;

@Autowired
private DiscoveryClient discoveryClient;

public Item queryItemById(Long id) {
String serviceId = "microService-item";
List<ServiceInstance> instances = this.discoveryClient.getInstances(serviceId);
if(instances.isEmpty()){
return null;
}
// 为了演示,在这里只获取一个实例
ServiceInstance serviceInstance = instances.get(0);
String url = serviceInstance.getHost() + ":" + serviceInstance.getPort();
return this.restTemplate.getForObject("http://" + url + "/item/" + id, Item.class);
}
}

如果导入的是服务端依赖,某些版本的 SpringCloud 会响应 XML 格式的数据,而我们希望它是 JSON,破坏了 SpringMVC 的默认配置,可以在 eureka server 的依赖中排除 jackson-dataformat-xml。
对于 eureka 来说,这两个微服务都属于客户端,所以还是建议只导客户端依赖就好。
另外,你还可以开启 Eureka 的身份认证,需要导入相应的依赖,一旦开启,需要在客户端配置好凭证。

搭建集群

Eureka 的集群非常好搭建,为了避免单点故障,集群是很有必要的,只要启动多个 Eureka 服务并且让这些服务之间彼此进行注册即可实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
server:
port: 6868
spring:
application:
name: microService-eureka

eureka:
client:
# 是否将自己注册到 Eureka 服务中,这次(集群)选择 true
registerWithEureka: true
# 是否从Eureka中获取注册信息(集群选择 true)
fetchRegistry: true
# Eureka 客户端与 Eureka 服务端进行交互的地址,选择另一台 Eureka 服务端
serviceUrl:
defaultZone: http://loli:[email protected]:6869/eureka/


############## 第二台服务端 ################
server:
port: 6869
spring:
application:
name: microService-eureka
eureka:
client:
# 是否将自己注册到 Eureka 服务中,这次选择 true
registerWithEureka: true
# 是否从Eureka中获取注册信息
fetchRegistry: true
# Eureka 客户端与 Eureka 服务端进行交互的地址,选择另一台 Eureka 服务端
serviceUrl:
defaultZone: http://127.0.0.1:6868/eureka/
security:
basic:
# 开启基于 HTTP basic 的认证
enable: true
user:
name: loli
password: pwd

他们的 defaultZone 互相指向对方,通过端口来区分,而微服务名字都是保持一致的,这样服务端的集群就搭建好了,而客户端注册的时候需要同时向这两台来注册,地址之间使用逗号分割。
搭建集群的时候尽量不要再配 prefer-ip-address 了,默认是 hostname。

自我保护机制

之前说过,Eureka 和 ZK 的一个区别,ZK 是按照 CP 原则来构建的,而 Eureka 是 AP 来做的。
默认情况下,如果 Eureka Server 在一定时间内(默认90秒)没有接收到某个微服务实例的心跳,Eureka Server 将会移除该实例。但是当网络分区故障发生时,微服务与 Eureka Server 之间无法正常通信,而微服务本身是正常运行的,此时不应该移除这个微服务,所以引入了自我保护机制。
当 Eureka Server 节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。一旦进入该模式,Eureka Server 就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。当网络故障恢复后,该 Eureka Server 节点会自动退出自我保护模式。
Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,Eureka Server 会将这些实例保护起来,让这些实例不会过期,但是在保护期内如果服务刚好这个服务提供者非正常下线了,此时服务消费者就会拿到一个无效的服务实例,此时会调用失败,对于这个问题需要服务消费者端要有一些容错机制,如重试,断路器等。
我们在单机测试的时候很容易满足心跳失败比例在 15 分钟之内低于 85%,这个时候就会触发 Eureka 的保护机制,一旦开启了保护机制,则服务注册中心维护的服务实例就不是那么准确了,此时我们可以使用eureka.server.enable-self-preservation=false来关闭保护机制,这样可以确保注册中心中不可用的实例被及时的剔除(不推荐)。

也因为 AP 的特性,会导致其注册慢的问题,也就是 Client 可能延迟注册(30s),Server 的响应缓存(30s),Server 刷新缓存(30s)极端情况加起来就是 90s。

常用配置

这里再以 properties 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#================================服务端==============================
#应用名称
spring.application.name=eureka-server-v1
#应用端口
server.port=7000
#=======eureka中心配置=======
#主机名
eureka.instance.hostname=localhost
# 注册时显示ip
#eureka.instance.prefer-ip-address=true
#是否注册为服务
eureka.client.register-with-eureka=false
#是否检索服务
eureka.client.fetch-registry=false
#eureka默认空间的地址
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
#关闭自我保护(生产时打开该选项)
eureka.server.enable-self-preservation=false
#扫描失效服务的间隔时间(缺省为60*1000ms)
eureka.server.eviction-interval-timer-in-ms=5000

#================================客户端==============================
#端口号
server.port=8081
#服务名
spring.application.name=produce-service-v1
#=======eureka配置========
#注册到eureka中心,获取到配置服务
eureka.client.service-url.defaultZone=http://localhost:7000/eureka/
#设置实例的ID为ip:port
eureka.instance.instance-id=${spring.cloud.client.ipAddress}:${server.port}
#========续约配置=========
# 心跳时间,即服务续约间隔时间(缺省为30s)
eureka.instance.lease-renewal-interval-in-seconds=5
# 发呆时间,即服务续约到期时间(缺省为90s)
eureka.instance.lease-expiration-duration-in-seconds=10
# 开启健康检查(依赖spring-boot-starter-actuator)
eureka.client.healthcheck.enabled=true

这些应该够用了吧…..

Ribbon

使用 Ribbon 实现客户端负载均衡,说到负载均衡,可以简单分为两类:

  • 集中式
    消费方和服务方中间使用独立的 LB 设施,例如 F5、nginx 这类就是。
  • 进程内
    一般集成到消费方,Ribbon 就是如此。

使用前的老一套不说了,导入依赖(eureka-server/client 中已经包含了 Ribbon 的依赖),在主启动类使用 @RibbonClient (简单使用可以不加)进行配置工具类。
然后,在 Config 创建 RestTemplate 对象上设置 @LoadBalanced 注解就表示已经启用负载均衡啦!

开启后,在执行请求前会经过 org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor 这个拦截器,并且通过 org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient 的时候会根据 serverId 查找服务地址,然后在去做真正的请求;
所以 RestTemplate 请求的 URL 可以直接使用服务名,而不需要手动获取地址了。

Spring Cloud Ribbon 虽然只是一个工具类框架,它不像服务注册中心、 配置中心、 API 网关那样需要独立部署, 但是它几乎存在于每一个Spring Cloud 构建的微服务和基础设施中。 因为微服务间的调用,API 网关的请求转发等内容实际上都是通过 Ribbon 来实现的,包括后续我们将要介绍的 Feign, 它也是基于 Ribbon 实现的工具(即 Feign 已经集成 Ribbon,所以注解 @FeignClient 的类,默认实现了 ribbon 的功能)。
SpringCloud 服务调用的方式一般就是两种:

  • Ribbon + RestTemplate
  • Feign

常用 IRule 默认实现有以下几种:

  • RoundRobinRule(默认)
    轮询规则,默认规则。同时也是更高级 rules 的回退策略

  • AvailabilityFilteringRule
    可用性敏感策略,这个负载均衡器规则,会先过滤掉以下服务:

    1. 由于多次访问故障而处于断路器跳闸状态
    2. 并发的连接数量超过阈值

    然后对剩余的服务列表按照 RoundRobinRule 策略进行访问

  • WeightedResponseTimeRule
    权重轮询策略,根据平均响应时间计算所有服务的权重,响应时间越快,服务权重越重、被选中的概率越高。刚启动时,如果统计信息不足,则使用 RoundRobinRule 策略,等统计信息足够,会切换到 WeightedResponseTimeRule。

  • RetryRule
    在选定的策略基础上进行重试,先按照 RoundRobinRule 的策略获取服务,如果获取服务失败,则在指定时间内会进行重试,获取可用的服务

  • BestAvailableRule
    最小并发策略,此负载均衡器会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务

  • RandomRule
    随机获取一个服务

更改默认的策略也很简单,可以使用 JavaConfig 的方式也可以使用配置文件的方式:

1
2
3
4
5
6
7
8
9
10
11
12
@EnableEurekaClient
@SpringBootApplication
public class ConsumerApplication {
@Bean
public RandomRule createRule() {
return new RandomRule();
}

public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 设置负载均衡策略 eureka-provider 为调用的服务的名称
eureka-provider:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

# 禁用 Eureka 支持,禁用后 Ribbon 不会获取 Eureka 中的服务列表
ribbon:
eureka:
enabled: false
# 禁用 Eureka 后手动配置服务地址
ribbon-demo:
ribbon:
listOfServers: localhost:8081,localhost:8083

# 其他常见配置
ribbon:
# 请求连接的超时时间
ConnectTimeout: 2000
# 请求处理的超时时间
ReadTimeout: 5000
# 最大连接数
MaxTotalConnections: 500
# 每个host最大连接数
MaxConnectionsPerHost: 500
#Ribbon更新服务注册列表的频率
ServerListRefreshInterval: 2000
# 预加载配置,默认为懒加载
eager-load:
enabled: true
clients: mima-cloud-producer,xxx

# 这里使用服务提供者的instanceName
mima-cloud-producer:
ribbon:
# 代表Ribbon使用的负载均衡策略
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
# 每台服务器最多重试次数,但是首次调用不包括在内,默认 0
MaxAutoRetries: 1
# 同一个服务其他实例的最大重试次数,不包括第一次调用的实例。默认值为 1
MaxAutoRetriesNextServer: 1
# 无论是请求超时或者socket read timeout都进行重试
# 配置为 true, 则任何请求方法都进行重试
# 配置为 false,GET请求方式也会进行重试,非GET方法只有在连接异常时才会进行重试。
OkToRetryOnAllOperations: true
# Interval to refresh the server list from the source
ServerListRefreshInterval: 2000
# Connect timeout used by Apache HttpClient
ConnectTimeout: 3000
# Read timeout used by Apache HttpClient
ReadTimeout: 3000

禁用 Eureka 以后 LoadBalanced 的根据服务名查找 ip 就会失效,可以通过指定 来达到点对点直连测试。
关于重试的配置要格外小心,避免重复数据。

自定义

Ribbon 自带了七中负载均衡的算法。默认轮询,当想选择其他算法时,在配置类里使用 @Bean 声明需要的官方提供的 IRule 其他实现类即可,这是 SpringBoot 自动配置的知识了。
也正是因为这个原因,如果我们想自己实现负载均衡算法,除了需要在启动类使用 @RibbonClient 注解指定服务名和负载均衡算法具体实现类(需要在 @Configuration 下)外,还要求这个类不能在包扫描范围内
首先,可以自定义负载均衡规则,可以在配置文件里设置也可以使用注解:

1
2
3
microService-consumer:  
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

在启动类上设置:

1
2
3
4
5
@SpringBootApplication
@EnableEurekaClient
// 在启动该微服务的时候就能去加载我们的自定义 Ribbon 配置类,从而使配置生效
@RibbonClient(name="MICROSERVICECLOUD-DEPT",configuration=MySelfRule.class)
public class Main{}

然后就是实现自己的负载均衡算法:

1
2
3
4
5
6
7
8
9
10
@Configuration
public class MySelfRule{
@Bean
public IRule myRule(){
// 随机使用 RoundRobinRule();
return new TestLB();
}
}

public class TestLB extends AbstractLoadBalancerRule{}

一般来说,默认的轮询就已经够用了。自定义的可以用于灰度发布等场景。
常用组件:IPing(探测服务存活状态),其他概念:allServerList、upServerList。

SCLoadBalancer

可以看出 SC 在逐步去 Netflix 化,Ribbon 宣布进入维护阶段,SC 早年也发布了自己的负载均衡框架 SpringCloudLoadBalancer。
不过它并不是一个独立的项目,而是 spring-cloud-commons 其中的一个模块。 项目中用了 Eureka 以及相关的 starter,想完全剔除 Ribbon 的相关依赖基本是不可能的,Spring 社区的人也是看到了这一点,通过配置去关闭 Ribbon 启用 Spring-Cloud-LoadBalancer。

1
2
3
4
5
spring: 
cloud:
loadbalancer:
ribbon:
enabled: false

关闭 ribbon 之后,Spring Cloud LoadBalancer 就会加载成为默认的负载均衡器。
在使用 API 上他们两个基本一致,不过嘛,这个项目的活跃度不算高,也不如 Ribbon 完善,目前阶段还是老老实实用 Ribbon 比较好。

OpenFeign

使用 Feign 实现声明式的 REST 调用,是为了简化 RestTemplate 的使用,让我们的代码更优雅,添加依赖就不多说了,主启动类加上 @EnableFeignClients 注解,这也是加在客户端(消费端)的。
然后声明一个接口,然后可以像写 SpringMVC 哪样来定义这个接口的方法啦!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 声明这是一个Feign客户端,并且指明服务 id
// 测试需要的情况,可以通过 url 参数指定地址
// Path 属性可以抽取 url 的公共部分
@FeignClient(value = "microService-item")
public interface ItemFeignClient {
// 这里定义了类似于SpringMVC用法的方法,就可以进行RESTful的调用了
@RequestMapping(value = "/item/{id}", method = RequestMethod.GET)
public Item queryItemById(@PathVariable("id") Long id);

// 多参数构造-1
@RequestMapping(value = "/get", method = RequestMethod.GET)
public User get1(@RequestParam("id") Long id, @RequestParam("username") String username);
// 多参数构造-2
@RequestMapping(value = "/get", method = RequestMethod.GET)
public User get2(@RequestParam Map<String, Object> map);
}

Feign 使用,简单说:创建一个接口(使用 @FeignClient 标注),在上面使用 SpringMVC 注解即可;相当于封装了 RestClient。
只需要定义接口就可以完成调用,很显然是用了动态代理,在 FeignClient 中的定义方法以及使用了 SpringMVC 的注解,Feign 就会根据注解中的内容生成对应的 URL,然后基于 Ribbon 的负载均衡去调用 REST 服务

Feign 最开始是 Netflix 的一套独立的框架,也并不能与 SpringMVC 的注解配合使用,它有自己的注解,SC 团队为了方便使用和整合,基于它开发了 OpenFeign,现在基本都是用的 OpenFeign,支持 SpringMVC 注解,SC 维护。

本质来说,Feign 就是一个 Http 客户端,并且整合了 Ribbon 和 Hystrix。
老版本中不支持 SpringMVC 的新组合注解,例如 @GetMapping,高版本没问题。
再说 Feign 中的 http 客户端,Feign 就相当于是 http 客户端的一层包装,为了更方便使用,默认它支持三种:

  1. JDK HttpURLConnnection(默认)
  2. Apache HttpClient
    启用:feign.httpclient.enabled=true
  3. Square OkHttp
    开启:feign.okhttp.enabled=true

换用其他 http 客户端时候,需要加入 feign-okhttp 或者 feign-httpclient(io.github.openfeign) 依赖;相关自动配置参考 FeignRibbonClientAutoConfiguration。
其中还有个比较特殊的 LoadBalancerFeignClient 类,它内部先使用 Ribbon 负载均衡算法计算 server 服务器,然后使用包装的 delegate 客户端实例,去完成 HTTP URL 请求处理,所以它具备了负载均衡的能力。
以及可以进行压缩处理,在服务之间的调用上显然不能使用 Nginx 了,Feign 的压缩就派上用场了,只需要在配置中开启即可,小数据量其实没有必要,可以配最小阈值。
为了避免重复定义,可以把 Feign 相关的定义抽取到单独的 Api 模块,可以在新模块中继承使用。

Hystrix

使用 Hystrix 的熔断机制保护应用,在微服务架构中通常会有多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应。
它是在服务端(服务提供方)使用的技术,使用起来非常简单,除了必要的依赖、在主启动类使用 @EnableHystrix 注解开启功能,只需要在 Service 的方法上加上 @HystrixCommand(fallbackMethod = "fallbackMethod") 注解就可以了,其中 fallbackMethod 是具有类似方法签名的备用方法,当此方法的调用不可用时就会走这个备用方法,如果确实需要抛出异常,可以通过注解属性来配置忽略那些异常(或者抛出 HystrixBadRequestException 异常,当抛出这个异常时,Hystrix 不会处理)。
但是这里会有一个问题,如果一个核心方法对应一个备用方法,很容易就会造成方法膨胀,耦合性还很高,这样也太不优雅了,所以可以使用 AOP 的思想来解决这个问题嘛~其实这样可以做服务降级,配合 Feign 来使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 该接口下哪个方法抛异常,会调 fallbackFactory
// 或者直接使用 fallback 属性指定一个其实现类,当作备用
@FeignClient(value = "MICROSERVICECLOUD-DEPT",fallbackFactory=MyFallbackFactory.class)
public interface DeptClientService{}


@Component // 不要忘记添加
public class MyFallbackFactory implements FallbackFactory<DeptClientService>{}


// 使用 Hystrix 原生注解
@HystrixCommand(groupKey = "PoiInfoServiceCommand", commandKey=“getStagedPoiBase”, fallbackMethod = "getStagedPoiBaseFallBack",
commandProperties = {
//指定多久超时,单位毫秒。超时进fallback
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000"),
//判断熔断的最少请求数,默认是10;只有在一个统计窗口内处理的请求数量达到这个阈值才会进行熔断与否的判断
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
//判断熔断的阈值,默认值50,表示在一个统计窗口内有50%的请求处理失败,会触发熔断
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
//熔断多少毫秒后开始尝试请求 默认5000ms
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds" , value = "10000")
},
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "8"),
@HystrixProperty(name = "maxQueueSize", value = "100"),
@HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1440")
}
)
public List<PoiBaseTf> getStagedPoiBase(List<Long> poiIds) throws Exception{
//todo
}

这样就可以达到容错的目的了,说到这里,不得不提涉及的两个概念,服务降级和服务熔断。

  • 服务熔断
    主逻辑因短期内多次失败(也有可能是由于自我保护机制),而被暂时性的忽略,不再尝试使用,这种叫熔断。
    相关概念:时间窗口、错误率(包含超时)、滑动窗口;
    例如,根据 1s 内的统计(请求、超时、错误数等)封装为一个 block,取 10 个 block 计算平均值,达到错误率阈值后熔断(同时满足设定的错误率和请求数),这 10 个block 是滑动选取,前面去掉一个,后面就加一个。

  • 服务降级:
    主逻辑失败采用备用逻辑的过程叫做降级(也就是服务降级发生在服务熔断之后)。
    当整体资源快消耗殆尽的时候(例如内存、CPU等),将某些服务临时关掉一大部分以释放资源(一般留下一个来维持运行返回给用户友好的提示),减轻主模块的压力,待资源恢复可用再开启。

看上去熔断和降级是非常相似的,都是调用失败后调用备用方法;但是他们的着重点是不同的。
不管是服务降级还是熔断,他们的目的都是为了保证程序的健壮性,对于一些非核心服务,如果出现大量的异常,可以通过技术手段,对服务进行降级并提供有损服务,保证服务的柔性可用,避免引起雪崩效应。
开启熔断之后,如何实现自动恢复?
每隔一段时间(默认 5s),会释放一个请求到服务端进行探测,如果后端服务已经恢复,则自动恢复,这也称为半开状态。

此外,Hystrix 还具有服务监控的功能,它提供了准实时的调用监控 HystrixDashboard,是可视化界面,可以进行实时监测。
这个监控使用非常简单,新建一个微服务,加入相关依赖,在启动类上加上 @EnableHystrixDashboard 就可以用啦!


Hystrix 的执行流程非常简单,它基本是业务无关性,在调用服务之前判断是否命中缓存、是否熔断、是否限流,都不满足才真正去调用,然后还会判断是不是调用失败了(超时、错误),失败就执行兜底方法,所有都正常才返回原始的结果。

img

根据架构图,扩展的 HystrixCommand 中主要的四个方法分别对应单次处理(同步、异步)、多次处理(阻塞、非阻塞(回调))的方式去执行(run 方法),其中 observe 称为 hot 处理,toObserve 称为 cold 处理(每次订阅都是一个新 Command 对象,后执行回调),主要区别是顺序问题。
两种 Command 的执行区别为一个新线程执行,一个本线程执行。
Hystrix 隔离规则有两种,线程隔离和信号量(轻量级),官方推荐使用线程隔离;线程隔离可以保证其中一个失败不会影响其他线程的执行。可配置项里有线程池的相关设置。
目前,Hystrix 也是维护模式。

Zuul

因为之前笔记有,介绍咱们还是略过,主要负责请求转发和请求过滤,简单理解为它相当于是一系列的过滤器或者拦截器就好了,继续新建一个微服务,导入相关依赖,在主启动类加入 @EnableZuulProxy 注解。
服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供 REST API 的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能,将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。
从上面的介绍可以看出,它还需要 Eureka 等依赖加强功能,让后面的微服务专心做自己的事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
server:
port: 6677
spring:
application:
name: microservice-api-gateway

zuul:
ignored-services: "*"
prefix: /atguigu
routes:
# 名字可以随意起,或者使用微服务的名称
item-service:
# 配置请求 URL 的请求规则
path: /item-service/**
# 真正的微服务地址,可以使用 url ,也可以指定 Eureka 注册中心中的服务 id
# url: http://127.0.0.1:8081
serviceId: microservice-item

eureka:
client:
# 是否注册自己,默认为true
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://127.0.0.1:6868/eureka/
instance:
# 将自己的ip地址注册到Eureka服务中
prefer-ip-address: true

info:
app.name: microcloud
company.name: www.bfchengnuo.com
build.artifactId: $project.artifactId$
build.version: $project.version$

匹配规则:? 单个字符、 任意数量字符、* 匹配多级目录;
同时,Zuul 还支持正则匹配、拦截器、路由前缀等功能,基于 Servlet 来实现,进阶可以看看自定义 Filter (实现 ZuulFilter 接口)相关知识。
网关中也可以使用 Hystrix 做负载均衡,只不过写起来有点麻烦,还是习惯于在业务层里进行处理。
Zuul 会默认过滤一些头信息,例如认证相关,如果不想让它过滤,可以配置 sensitive-headers 为空。

SpringCloudGateway

Spring 官方最终还是推出了自己的网关组件:Spring Cloud Gateway ,相比之前我们使用的 Zuul 是基于 Servlet,使用阻塞 API,它不支持任何长连接,如 WebSockets,Spring Cloud Gateway 使用非阻塞 API,支持 WebSockets,支持限流等新特性。

SpringCloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
目的为替代 Zuul。

它基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty。
它不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

学习它需要先了解三个术语:

  1. Filter(过滤器)
    和 Zuul 的过滤器在概念上类似,可以使用它拦截和修改请求,并且对上游的响应,进行二次处理。过滤器为 org.springframework.cloud.gateway.filter.GatewayFilter 类的实例。
  2. Route(路由)
    网关配置的基本组成模块,和 Zuul 的路由配置模块类似。一个 Route 模块由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配,目标 URI 会被访问。
  3. Predicate(断言):
    这是一个 Java 8 的 Predicate,可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。断言的输入类型是一个 ServerWebExchange。

因为也是基于过滤链,流程简单来看都差不多,也很简单:客户端向 Spring Cloud Gateway 发出请求;如果 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler;Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
配置路由可以使用 yml 配置文件,也可以使用 Java 配置,不过还是推荐使用 yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
server:
port: 8080
spring:
cloud:
gateway:
routes:
- id: neo_route
uri: https://bfchengnuo.com
predicates:
- Path=/amy
-id: message-provider-route
uri: lb://message-provider
predicates:
-Path=/message-provider/**

application:
name: cloud-gateway

eureka:
instance:
prefer-ip-address: true
client:
service-url:
defaultZone: http://localhost:8888/eureka/

以上配置的解释:

  • id:我们自定义的路由 ID,保持唯一
  • uri:目标服务地址,可以结合负载均衡、配置中心 ID 使用
  • predicates:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
  • filters:过滤规则,本示例暂时没用。可以配置对应的过滤处理 url 或者使用 Hystrix 熔断降级。

所以效果就是当访问 localhost:8080/amy/1 的时候,会自动转发到 https://bfchengnuo.com/1 这个地址;
基于 JavaConfig 的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootApplication
public class GatewayApplication {

public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/amy")
.uri("https://bfchengnuo.com"))
.build();
}
}

SpringCloudGateway 的匹配规则非常丰富,请求路径、参数、时间、host、cookie 等等,并且支持组合使用。

SpringCloudConfig

使用 SpringCloudConfig 统一管理微服务的配置,可以让我们统一管理配置文件,以及实时同步更新,并不需要重新启动应用程序,默认使用 Git 存储配置文件内容。
同样,它也分为客户端和服务端,服务端可以新建一个微服务,加入相应的依赖,在主启动类加上 @EnableConfigServer 注解就可以使用了,当然还是要写一点配置的。

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 6688
spring:
application:
name: microservice-config-server
cloud:
config:
server:
git:
uri: http://172.16.55.138:10080/bfchengnuo/config-server.git
#username: loli
#password: 123456

还是推荐使用 SSH 密钥认证的方式,这样就可以通过 SpringCloudConfig 直接访问 Git 上的配置文件,同时它支持 properties 和 yml 的互相转换,通过请求地址的后缀实现。
客户端的使用也是类似,导入没有 server 后缀的依赖,另外为了避免地址的硬编码,可以将服务端使用 @EnableDiscoveryClient 也注册到 Eureka 中,然后在客户端使用服务名来访问。
需要新建配置文件:bootstrap.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
config:
# 对应的配置服务中的应用名称
name: microservice
# uri: http://127.0.0.1:6869/
# 对应配置服务中的 {profile}
profile: dev
label: master
discovery:
# 启用发现服务功能
enabled: true
# 指定服务名称
service-id: microservice-config-server

因为 bootstrap.yml 优先与 application.yml 加载(先读取了配置才能启动啊),所以把发现服务配置在 bootstrap 里。
然后你可以使用 @Value 来注入配置,就是和配置文件在本地是一样使用。
为了能够让配置自动更新,还需要为 Config Client 添加 refresh 支持,就是导入一个 spring-boot-starter-actuator 依赖,然后在配置类对应的实体类上加上 @RefreshScope 注解(测试可以临时把 actuator 安全认证关掉 management.security.enabled)。
然后就可以使用 post 请求 /refresh 地址来更新配置内容了。
更新后还需要手动访问下这个地址未免太麻烦了,所以,可以借助 git 的 webhook(web 钩子)实现自动更新。

SpringCloudBus

消息总线 Spring Cloud Bus 也是很重要的,例如它可以更优雅的完成自动更新配置文件,简单的你可以理解为它就是个消息的交换机,所有的微服务模块都监听它,所以可以实现配置、缓存等的更新。
以 RabbitMQ 为例,就先在 ConfigServer 中来加入吧,导入 spring-cloud-starter-bus-amqp 依赖,在 application.yml 添加 rabbitmq 的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
eureka:
client:
serviceUrl:
defaultZone: http://127.0.0.1:6868/eureka/
spring:
cloud:
config:
name: microservice
# uri: http://127.0.0.1:6869/
profile: dev
label: master
discovery:
# 启用发现服务功能
enabled: true
service-id: microservice-config-server
# RabbitMQ 相关的配置
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest

然后,会自动注册一个 /bus/refresh 的请求,这个请求就是由消息总线来处理的,那么我们可以设置当配置更新后 web 钩子向统一配置服务器发送 /bus/refresh 请求,然后配置服务器会将消息发送到 springCloudBus 的交换机,由于其他微服务的队列也绑定到交换机,所以也就都获取到了更新的通知,然后去 ConfigServer 获取最新的数据。
需要注意,其他的微服务(客户端)这个 bus 配置是要写在 bootstrap.yml 中的,保证优先加载。

其他

因为 Eureka 很早之前就宣布进入维护模式,目前可以选择使用 Alibaba 套件替代或者使用 Spring Cloud Consul。
此外,SC 的其他一些项目也不错,例如 Spring Cloud Security / OAuth2。
参考:https://github.com/macrozheng/springcloud-learning

参考

https://www.jianshu.com/p/3463571febc2

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~