Spring Cloud笔记(三)

一. 前言

​ 上一部分添加了Ribbon实现了负载均衡,并且使用了Feign实现声明式的REST调用。微服务架构中还有很多其他组件,让我们继续学习其他有用的微服务架构的组件。

二. 使用熔断器Hystrix

微服务架构是由多个微服务组成的系统。虽然架构更加清晰,分工更加明确,但也存在相应的问题:出现问题难以排查(到底是哪个服务出现问题?)。而且还会存在微服务所特有的问题:雪崩效应。当其中一个微服务出现故障时,由于微服务之间会存在依赖关系,所以其他服务也会渐渐地受到影响,最后影响到整个系统。如果这时候放任不管,那么请求就会堆积,导致系统彻底瘫痪。(有理论分析,即使每个系统的正常运行时间是99.9%,仍然会因为雪崩效应导致很严重的效果)所以我们需要一个组件来管理微服务,监控微服务,当组件发现(认为)服务出现故障,会把系统调到降级模式,拒绝继续接受请求,避免请求堆积。等到一定时间之后再申请一个请求,如果请求通过,解除降级模式,否则,继续拒绝。这个就是Hystrix,熔断器。逻辑也很简单,其实就相当于电路里的断路器。

sb_11

正常情况下,断路器关闭,可正常请求依赖的服务。

当一段时间内,请求失败率达到一定阈值(例如错误率达到50%,或100次/分钟等),断路器就会打开。此时,不会再去请求依赖的服务。


使用步骤:

①导入依赖包spring-cloud-starter-netflix-hystrix

②启动类添加注解:@EnableHystrix, @EnableCircuitBreaker

③给Controller里的RequestMapping方法添加一个@HystrixCommand(fallback = "xxx")的注解,xxx就是一个fallback方法名。当降级模式触发的时候,停止请求,改为调用fallback方法。

(HystrixCommand上可以进行很多配置)

一两次失败的request并不会导致断路器打开,而多次之后可以看到断路器确实已经开启:

sb_12

Hystrix的隔离策略:线程隔离信号量隔离

一般推荐HystrixCommand使用线程隔离,而HystrixObservableCommand使用信号量隔离。

HystrixCommand和HystrixObservableCommand的区别:

①HystrixCommand用在依赖服务返回单个操作结果的时候。有两种执行方式

   -execute():同步执行。从依赖的服务返回一个单一的结果对象,或是在发生错误的时候抛出异常。

   -queue();异步执行。直接返回一个Future对象,其中包含了服务执行结束时要返回的单一结果对象。

    

②HystrixObservableCommand 用在依赖服务返回多个操作结果的时候。它也实现了两种执行方式

   -observe():返回Obervable对象,他代表了操作的多个结果,他是一个HotObservable

   -toObservable():同样返回Observable对象,也代表了操作多个结果,但它返回的是一个Cold Observable。

参考链接:https://www.cnblogs.com/happyflyingpig/p/8079308.html

线程隔离和信号量隔离的区别:

THREAD —— it executes on a separate thread and concurrent requests are limited by the number of threads in the thread-pool

SEMAPHORE—— it executes on the calling thread and concurrent requests are limited by the semaphore count.

简单地说,就是线程隔离就是HystrixCommand会在单独的线程上执行(new一个线程),受到线程池的线程数量限制。

而信号量隔离,仍然在调用的线程上执行,隔离等级由线程缩小为信号量。

显然,线程更安全,开销也更高,而信号量可以更高效,开销较低,但限制也较大(信号量个数限制

sb_13

Feign使用Hystrix:

前面使用Hystrix是手动定义一个fallback方法,如果需要为Feign整合Hystrix,那么也很简单,为自定义的FeignClient接口提供一个实现类,然后接口的定义@FeignClient增加一个fallback属性即可。

看起来是没有任何问题的,但却出错了,而且一直解决不了,差点直接弃坑。

当中可能存在的问题:

①没有配置Hystrix?

网上一堆互联网垃圾互抄都是说这个问题,但并不是,我第一时间就在yml里设置好了

②Feign已经包含了Hystrix,所以把启动类的@EnableHystrix去掉?

难得看到一个不是提①的帖子,表示了是这个问题,但我去掉了还是存在500的问题

③CustomizeFeignClientImpl的问题?路径?另外两个方法?

都无关

④@EnableFeignClients注解的问题?@EnableHystrix的影响?

没有影响。

⑤primary = false ? @primary等等的原因?

都不是关键,如果是注入出错的时候,Spring启动的时候就会抛出注入失败的错误。显式给fallback类使用@primary其实就是直接调用它的方法了,那样虽然可以fallback但已经失去了意义。

⑥上一章节Feign的decoder,encoder,contract的问题?

有可能,换了一个全新的Controller(简化了各种东西),并且把自定义FeignConfiguration也去掉了,RequestLine也换回了RequestMapping。但这时报错无法Autowire,因为它不知道要注入的是CustomizeFeignClient,还是CustomizeFeignClientImpl(注入类型是CustomizeFeignClient,所以两个都是可行的)。这时候我不懂为什么之前没有这个问题,就因为之前有自定义FeignConfiguration?

但是这时候我给CustomizeFeignClient设置了primary = false(官方文档里的解法),还是不行。只能给Impl添加@Primary,可这时候又有问题了,因为优先是Impl,所以无论如何都是fallback,那么就失去了原本的意义。而如果是给接口设置@Primary,那么就是继续500错误。而且之前把Configuration去掉,那么Feign会直接失效的,也可能是这个问题。关于这部分,只好先用着Hystrix,而不是Feign整合Hystrix,具体还得看文档才能解决。


Feign使用Hystrix:(解决篇

问题原因:

①Feign依赖包已经包括了Ribbon包,所以需要把Ribbon包去掉。

1
2
3
4
5
6
7
<!--
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
-->

(Feign整合了很多内容,包括Eureka,Ribbon,Hystrix,但这里只有Ribbon起到了冲突,具体原因不明。其他的诸如spring-cloud-starter-netflix-hystrix等包,不去掉也没有影响。

②上一节在创建FeignClient实例的时候,用到了Feign.builder( ),然后认证内容都直接写在里面了,导致后面fallback是没有认证的。从而会导致401错误,最后变成500错误。

1
2
3
4
5
6
userUserFeignClient = HystrixFeign.builder().client(client).decoder(decoder).
requestInterceptor(new BasicAuthRequestInterceptor("user", "password1")).
target(UserFeignClient.class, "http://microservice-simple-provider-user");
adminUserFeignClient = HystrixFeign.builder().client(client).decoder(decoder).
requestInterceptor(new BasicAuthRequestInterceptor("admin", "password2")).
target(UserFeignClient.class, "http://microservice-simple-provider-user");

改成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class TestController {
private UserFeignClient userFeignClient;

@Autowired
public TestController(Client client, Decoder decoder, UserFeignClient userFeign) {
this.userFeignClient = userFeign;
}

@GetMapping("/user1/{id}")
public User findById(@PathVariable String id) {
return this.userFeignClient.findById(id);
}
}

(无须decoder,encoder等等,也不需要那版本烦人的xxx.FeignClientsConfiguration.class了。

如何确定是这里的关键问题?如何看到401错误?

把上述代码改了之后,访问URL,会返回401错误,因为这时候其实已经可以访问到fallback了,但因为是最后的findById的时候没有认证,所以导致出错。之前写Feign刚好有一个搁置的配置文件,用于对Feign的认证,把它加上,问题彻底解决:

1
2
3
4
5
6
7
// when we need to add Interceptor for Feign, we need it to add the Http Basic auth.
public class FooConfiguration {
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("admin", "password2");
}
}
1
2
3
4
5
6
7
@FeignClient(name = "microservice-simple-provider-user",
configuration = {FeignConfiguration.class, FooConfiguration.class}
fallbackFactory = FeignClientFallbackFactory.class, primary = false)
public interface UserFeignClient {
@RequestLine("GET /{id}")
public User findById(@Param("id") String id);
}

使用fallbackFactory而不是fallback类,使得可以打印错误日志(fallback类实现FallbackFactory<xx>接口,实现create方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class FeignClientFallbackFactory implements FallbackFactory<UserFeignClient> {
private static final Logger LOGGER =
LoggerFactory.getLogger(FeiginClientFallbackFactory.class);

@Override
public UserFeignClient create(Throwable meg) {
return new UserFeignClient() {
@Override
public User findById(String id) {
FeignClientFallback.LOGGER.info("fallback;reason was " + meg.toString());
meg.printStackTrace();
User user = new User();
user.setId("-1");
user.setName("Default user");
user.setAge(-1);
user.setBalance(-111);
user.setUsername("default");
return user;
}
}
}
}

这样当前消费者service也能通过printStackTrace查看提供者service的错误信息(值得注意的是直接LOGGER.info(throwable)并没什么用,不会打印出任何东西。。至此,问题解决。


对某个Feign禁用Hystrix:

(先编写一个配置类,然后在@FeignClient中导入即可)

1
2
3
4
5
6
7
8
@Configuration
public class FeignDisableHystrixConfiguration {
@Bean
@Scope("prototype")
public Feign.Builder feignBuilder() {
return Feign.builder();
}
}

想要禁用Hystrix的@FeginClient引用该配置类即可,例如:

1
2
3
4
@FeignClient(name = "user", configuration = FeignDisableHystrixConfiguration.class)
public interface UserFeignClient {
// ...
}

也可以直接全局禁用Hystrix,在yml里,一般不会使用。

三. 使用Hystrix的监控

Hystrix除了实现容错,还有强大的实时监控功能。

步骤:

①首先,给Consumer服务添加Hystrix监控节点。

并不需要再导入依赖包了,因为spring-cloud-starter-netflix-hystrix就已经足够。另外,书上的例子是根据@HystrixCommand的继续的,它直接就拥有了hystrix.stream节点,不清楚为何。但我觉得可能是版本的原因,也可能是@HystrixCommand和Feign的区别,总而言之,上网查了一下,发现最可靠的办法还是:自己亲手创建者一个节点。这个过程很简单,而且也能对节点有更具体的认识,而不是像之前的,为什么会有info节点,health节点,到现在的hystrix.stream节点?都是系统自带的么?最好还是自己创建。

参考:https://blog.csdn.net/dangshushu/article/details/80416042

当然,这里添加的是``/actuator/hystrix.stream`,自己改一下就好,很简单。

这里当时遇到过问题,无法打开这个节点,忘了是什么原因了。一开始以为是@EnableHystrix或者@EnableCircuitBreaker跟@EnableHystrixDashboard冲突,因为删掉前面两个就可以了。但其实并不是,主要原因是,我没有导入Hystrix依赖包!可能是一开始以为Hystrix依赖包跟Hystrix-dashboard依赖包冲突,然后去掉了Hystrix依赖包。所以当时的原因是:没有加Hystrix依赖包,导致前两个注解出现问题而已,这同属一个包的注解如果不兼容,那一般不可能。

然后无论是打开浏览器查看hystrix.stream节点,还是直接查看hystrix查看dashboard界面,都可行了。这里又有一个问题,hystrix.stream一直ping,但没有内容。因为需要先在服务里执行至少一次操作,这里才会有信息,所以使用服务一次,比如xxx:8010/user/1,然后再看hystrix.stream节点就好。

②关于可视化监控Dashboard

首先需要导入依赖的:

1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>

只需要在启动类加上@EnableHystrixDashboard即可。

对了,这里没有把Dashboard注册到Eureka上,一开始想着就在本身上监控,所以不注册也OK。

③Dashboard可以监控多个服务实例,但当存在很多个实例的时候,只能一次一次地在hystrix节点上修改url,比较麻烦,而且不能对比。这时候就需要Turbine来聚合监控数据,其实也就是一次显示所有的/hystrix.stream节点,更方便,而且可以横向对比了。

然后这时候发现前面的错了,我的架构搞错了,因为之前无论是Ribbon还是Feign等都直接在Consumer上添加,所以我也就直接都加到Consumer上了。原来Consumer上只需要添加一个Hystrix.stream节点即可(就是那个只有文字的Hystrix监控信息),然后对于dashboard跟Turbine,最好另外分别起两个modules,所以现在的项目架构应该是这样的:

sb_14

这时就遇到了一个问题,太久没有创建子module,都忘了哪些是必须要添加的了,大概如下:

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

PS:我觉得<dependencies>这部分应该是要放到父项目的,但现在懒得搞,后面重构再说。但h2数据库为什么也要导入我也不是很清楚。而且<dependencyManagement>也是必须的。还有启动类记得不能放在默认的package,不然无法scan。

然后Dashboard项目还好,其实跟上面说的是一样的,只是我们把consumer的那些内容都去掉了而已。然后刚刚又不小心解除了技能,之前在project左方找文件的时候,尤其是External Libraries,一堆文件找得头疼,各种快捷键也没有。刚刚才发现,无须快捷键,指到那个区域,直接输入便是搜索,无语了,一下就能把什么jackson,commonxxx揪出来了。

整个Dashboard项目结构:

sb_15

Dashboard启动类前除了@SpringBootApplication,还要增加一个@EnableHystrixDashboard

1
2
3
4
5
6
7
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApplication.class, args);
}
}

yml只需要配置一个端口号即可:

1
2
server:
port: 8030

Turbine项目的结构同理,只是除了上面Dashboard要导入的资源,还有:

1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-turbine</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>

yml:(这个Turbine需要注册到Eureka,所以还要配置一下Eureka相关属性。但Eureka的依赖包前面是直接放在父项目了,所以是无须在这个module特别导入的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 8031
spring:
application:
name: microservice-hystrix-turbine
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka
instance:
prefer-ip-address: true
turbine:
app-config: microservice-simple-consumer-user
cluster-name-expression: "'default'"
instanceUrlSuffix: /hystrix.stream # 没有的话会出现异常

这里的app-config就是指定要监控哪些服务,可以指定多个,逗号分隔开即可,甚至同一个实例的不同端口号启动也可以一起监控。由于我没有编写多个consumer,后面就直接拿双端口号实例来测试了。

而且,因为Turbine需要注册到Eureka,所以启动类应该是这样的:(exclude也是必须的,否则它会去yml里找,找不到就又生成了默认密钥了! PS:去掉Turbine的security就不需要exclude了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@SpringBootApplication(exclude {org.springframework.boot.autoconfigure.security.
servlet.SecurityAutoConfiguration.class, org.springframework.boot.actuate.
autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration.class})
@EnableTurbine
public class TurbineApplication {
@Bean
public DiscoveryClient.DiscoveryClientOptionalArgs discoveryClientOptionalArgs() {
DiscoveryClient.DiscoveryClientOptionalArgs discoveryClientOptionalArgs =
new DiscoveryClient.DiscoveryClientOptionalArgs();
List<ClientFilter> additionalFilters = new ArrayList<>();
additionalFilters.add(new HTTPBasicAuthFilter("hong", "root"));
discoveryClientOptionalArgs.setADditionalFilters(additionalFilters);
return discoveryClientOptionalArgs;
}

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

填坑

然后启动类加上@EnableTurbine,启动,理论上应该就OK了,但果然还是踩坑了,然后下面开始讲述填坑的过程:

①首先必须清晰这几个项目之间是如何工作的,不能只知道它这样写work,而不清楚为何这样写,不求甚解的结果都懂的,后面稍微出现了点状况你也不知道是哪里出现问题,错误得不到正确的定位,就会花费极多的事件去做无用功(这里又要吐槽一次前面的Feign使用Hystrix,定位错误两天,解决一小时)

首先,给Consumer添加了Hystrix.stream节点。这个节点就是可被后面的Dashboard跟Turbine检测的,而且我们看一下配置节点的代码:

1
2
3
4
5
6
7
8
9
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean regisBean = new ServletRegistrationBean(streamServlet);
regisBean.setLoadOnStartup(1);
regisBean.addUrlMappings("/hystrix.stream");
regisBean.setName("HystrixMetricsStreamServlet");
return regisBean;
}

即使不清楚它的源码,但看了也大概知道是啥了,addUrlMappings就是添加节点的URL路径,然后设置了一个BeanName。而且这个Bean也不是瞎创建的,使用了HystrixMetricsStreamServlet来创建,所以很自然地,我们在8010/hystrix.stream里就可以看到一些关于Hystrix监控的信息了,尽管现在还不清楚到底是啥。

然后,Dashboard是一个单独的结点,不需要配置Eureka信息,它也确实无须注册到Eureka,毕竟它是在主页里手动输入url来获取Hystrix信息的(如localhost:8010/hystrix.stream)。PS:启动的时候可能会报错,说无法注册到服务器,不需要管,因为本来就不打算注册到Eureka,不影响。

最后,Turbine是需要注册到Eureka的,因为它不是单独地输入一个URL,然后就检测这一个,它是检测Eureka里所有具备了某个结点的服务(一开始以为默认就是hystrix.stream结点),然后Turbine本身又是一个结点,/turbine.stream,最后只需要把这个结点放到Dashboard里查询,那么就可以在Dashboard里显示所有的Turbine检测到的Hystrix.stream结点。总体逻辑就是这样。

②Dashboard can not connect xxx:

输入了某个hystrix.stream的URL之后,发现无法connect。这时候看一下日志就知道:401,未授权。所以这里也直接很简单粗暴了,URL前面加上xxx:yyy@即可 (这个好像叫OAuth协议,后面再说)。前面最开始为什么没有这个问题?因为当时Dashboard直接就在Consumer服务里,而Consumer服务是有配置这个认证的代码的,所以自然它也自动认证了。

③在Dashboard里输入turbine.stream的时候,一直loading。

一开始直接打开turbine.stream,发现一直在打印:”reportingHostsLast10Seconds”:0 ……

很显然,它没有发现到任何的Host,自然也没有信息。这时候再回看turbine的yml配置:

1
2
3
4
turbine:
app-config: microservice-simple-consumer-user
cluster-name-expression: "'default'"
instanceUrlSuffix: /hystrix.stream # 没有的话会出现异常

我当时consumer偷懒,一直没有给它加名字,所以consumer服务在Eureka里是叫UNKNOWN的。于是我把这里改成了UNKNOWN,确实就找到了。然后我给consumer加回这个正常的名字:

1
2
3
spring:
application:
name: microservice-simple-consumer-user

发现是发现到了,但仍然是loading,这时候日志就看到错误原因了:401

仍然是未授权。这时候应该怎么解决?我前面是直接在Dashboard里的URL添加了认证信息,但这里显然是不行的,为什么?因为Dashboard里的URL直接就是我要监控的那个URL的地址,它具体地调用了某个Provider,然后这个Provider需要认证信息,所以我直接在URL里添加认证信息就可以了。但这里我在Dashboard里输入的URL是turbine.stream的URL,如果我在这个URL里加入了认证信息,那么只是传递给了Turbine。但后续Turbine还要自己去监控各种的服务,显然因为Turbine是连接多个服务的,如果不同的服务认证信息不一致,那么这种在URL前缀加认证信息其实就没必要,所以Turbine也很聪明地直接就不会把这个参数传递到服务(尽管我这里用的是同一个服务实例,如果它传是可以通过认证的,但显然它直接不这么尝试,我认为这个设计是合理的)

最标准的解法应该是,给Turbine增加一个配置类,然后通过认证,但我前面的认证都是针对注册到Eureka 的,而Consumer里的认证是使用Feign的,不可能给Turbine为了认证也加这么一个不需要的东西吧。不清楚如何解决,最后直接在源头:在Provider上配置例外,这样Turbine就不需要认证了:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
web.ignoring().
antMatchers("/hystrix.stream", "turbine.stream", "/actuator/hystrix.stream");
}
}

结果还是不行,一开始说找不到/actuator/hystrix.stream。然后加上了这个ignoring也不行。其实当时就猜到了,默认路径应该是/actuator/hystrix.stream,而不是/hystrix.stream(可能是2.x改的)

所以正确的做法,给yml手动配置Turbine要监控的结点(不知道默认是什么,最好就直接自己配置):

1
2
3
4
turbine:
app-config: microservice-simple-consumer-user
cluster-name-expression: "'default'"
instanceUrlSuffix: /hystrix.stream # 没有的话会出现异常(验证了这个不可或缺)

这里的turbine:instanceUrlSuffix: 就是配置了要监控的后缀。果然401问题解决了,就全部都解决了。

效果图:

①Eureka:一共启动了6个项目。其中一个Provider,两个Consumer(通过不同端口启动的),一个Turbine,剩下的就是Eureka,还有没注册到Eureka的Dashboard。

sb_16

②Turbine.stream可以监控到数据:

sb_17

③Dashboard的界面:

sb_18

④给Dashboard输入我们要访问的URL,随便起一个Title(URL可以是单个的hystrix.stream,也可以是监控多个的turbine.stream,这里直接看turbine.stream)

sb_19

可以看到监控了两个Host,说明成功了。如果是多个不同的实例,会有多个图:

sb_20

-------------本文结束感谢您的阅读-------------