前面几篇我们陆陆续续介绍了Eureka服务注册中心、Ribbon客户端负载均衡以及Feign声明式REST服务调用组件,那么本篇我们来聊一下有关于这几个组件的常见问题以及解决方案。

一、Eureka常见问题

1.System Status信息修改

我们一般在启动了Eureka Server的时候,在Eureka监控面板的首页会看系统状态信息:

【Spring Cloud总结】15.Eurek Ribbon Feign常见问题及解决_ip-address


可以看到目前环境为“test”测试,数据中心为“default”,当前这一块信息是可以修改的。

在Spring Cloud的文档里没有提到这个,但是在Eureka的文档中有提到(​​https://github.com/Netflix/eureka/wiki/Configuring-Eureka​​):

【Spring Cloud总结】15.Eurek Ribbon Feign常见问题及解决_Eureka开启自我保护_02


上面的意思就是Eureka Client和Eureka Server可以运行在云上和非云上,如果我们运行在云上(如AWS云),需要给它打一个上云的标签,我们可以设定eureka.datacenter=cloud参数,此时Eureka Client和Eureka Server会专门为AWS cloud初始化一些AWS需要的信息(这里其实就是datacenter,即数据中心)。

同理,上面的环境标签,同样可以使用eureka.environment来制定目前服务环境的信息。

我们在之前的microserver-discovery-eureka工程中的application.yml配置文件中添加以下信息:

server:
port: 8761
eureka:
environment: product
datacenter: cloud

重启服务,打开Eureka监控面板,就可以看到我们配置的状态信息了:
 

2、Eureka开启自我保护和解决不踢出问题

我们有时候在一些复杂情况下启动Eureka Server的时候,会在Eureka的控制面板上看到类似的提示:

【Spring Cloud总结】15.Eurek Ribbon Feign常见问题及解决_System Status_03


出现这个提示,就意味着Eureka开启了自我保护机制。有关自我保护机制,我们在官方文档上可以看到以下描述(​​https://github.com/Netflix/eureka/wiki/Understanding-Eureka-Peer-to-Peer-Communication​​):

【Spring Cloud总结】15.Eurek Ribbon Feign常见问题及解决_ip-address_04


翻译一下就是:

当Eureka服务器启动时,它试图从相邻节点获取所有实例注册表信息。如果从节点获取信息时出现问题,服务器将在放弃之前,去尝试所有对等节点。如果服务器能够成功地获取所有实例,它将根据这些信息设置应该接收的续订阈值。如果任何时候,续约都低于为该值配置的百分比(15分钟内低于85%),服务器将停止过期实例以保护当前实例注册表信息。

在Netflix中,上述保护称为自我保护模式,主要用于在一组客户机和Eureka服务器之间存在网络分区的情况下作为保护。在这些场景中,服务器试图保护它已经拥有的信息。在大规模停机的情况下,可能会出现一些场景,这可能会导致客户机获取不再存在的实例。客户机必须确保它们对返回不存在或没有响应的实例的Eureka服务器具有弹性。在这些场景中,最好的保护是快速超时并尝试其他服务器。

总结起来其实就是之前我们在《​​6.将微服务注册到Eureka Server上​​》篇中提到的:
默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳时,EurekaServer将会注销该实例(默认90s)。但是当网络发生故障时,微服务与EurekaServer之间无法通信,这样就会很危险了,因为微服务本身是很健康的,此时就不应该注销这个微服务(即不把这个服务踢出ServerList),而Eureka通过自我保护机制来预防这种情况,当网络健康后,该EurekaServer节点就会自动退出自我保护模式。

我们来模拟一下保护模式的触发,首先我们启动user服务,然后启动eureka服务,此时服务正常:

【Spring Cloud总结】15.Eurek Ribbon Feign常见问题及解决_System Status_05


然后我们强行关闭user,回到Eureka面板观察(刷新一会),发现Eureka进入了保护模式:

【Spring Cloud总结】15.Eurek Ribbon Feign常见问题及解决_ip-address_06


user服务的状态也变为了DOWN。

我们在开发过程中,有时候会需要反复启停一些服务,这个时候我们是希望Eureka Server直接踢出已关停的节点的,可以进行以下配置来解决Eureka Server不踢出已关停节点的问题:
server端:

eureka.server.enable-self-preservation     (设为false,关闭自我保护)
eureka.server.eviction-interval-timer-in-ms 清理间隔(单位毫秒,默认是60*1000,这里设置的小一点)

client端:

eureka.client.healthcheck.enabled = true         开启健康检查(需要spring-boot-starter-actuator依赖)
eureka.instance.lease-renewal-interval-in-seconds = 10 租期更新时间间隔(默认30秒)
eureka.instance.lease-expiration-duration-in-seconds = 30 租期到期时间(默认90秒)

示例:
服务器端配置:

eureka:
server:
enable-self-preservation: false
eviction-interval-timer-in-ms: 4000

 客户端配置:

eureka:
lease-renewal-interval-in-seconds: 10
lease-expiration-duration-in-seconds: 30

这里我们设置服务端关闭自我保护,然后清理服务列表的时间间隔为4秒;服务端租期更新时间间改为10秒,租期到期时间改为30秒,这样Eureka会更快感知到其租期到期,并直接关闭该服务。

我们重启两个服务,一开始我们会在Eureka的监控面板上看到自我保护机制被关闭的警告:

【Spring Cloud总结】15.Eurek Ribbon Feign常见问题及解决_ip-address_07


因为我们就是故意关闭它的,为了方便开发,所以这里该警告不去理会,但是生产上是一定需要关注的。

然后关闭user服务, 刷新Eureka控制面板,发在在没有出现自我保护机制的警告后,服务列表中的user服务已经被剔除(大约30+4=34秒左右):

【Spring Cloud总结】15.Eurek Ribbon Feign常见问题及解决_Eureka开启自我保护_08

注意:生产环境中一定一定一定不要把自我保护模式关闭!!!!!!!!而且,我们擅自修改eureka的自动续约时间,也会打破Eureka的自我保护机制。

3、Eureka配置instanceId显示IP
我们之前在Eureka控制面板看到的服务都是服务实例名称+端口:

实际上它是按照我们在注册的客户端服务的application.yml中配置的instanceId规则显示的:

在没有配置的情况下,instanceId的配置默认为:
${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}}
也就是:主机名:应用名:应用端口。

一般来说很多时候我们做运维工作的时候需要知道机器的ip地址,方便追踪问题,此时我们可以添加以下参数:

${spring.cloud.client.ipAddress}

注意:spring could 2.0版本 需要改成${spring.cloud.client.ip-address}
该参数就可以展示客户端服务所在的ip地址了。

我们在user服务上修改instanceId的配置:

eureka:
instance:
instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}

然后重启user服务,在eureka上观察一下该服务的Status,可以看到IP显示出来了:

【Spring Cloud总结】15.Eurek Ribbon Feign常见问题及解决_Eureka开启自我保护_09

二、Ribbon常见问题

1、自定义配置时,@Configuration和@ComponentScan包不应重叠
这个问题我们在讲Ribbon以及Feign的时候都提到过,自定义的RibbonConfiguration类必须用@Configuration注解标注,但是它不应该在主Application Context的组件扫描之中,否则它将被所有的Ribbon客户端共享。如果你用@ComponentScan(或者@SpringBootApplication),那么你应该采取措施来避免它被包含到扫描的范围中。

2、使用RestTemplate时,想要获得一个List时,应该用数组,而不应该直接用List
准确的来说,这个问题不是Ribbon的坑,而是RestTemplate的坑。
我们在user中编写代码来反映这个问题。我们在user的Controller类中添加这样一个服务:

@GetMapping("/list-all")
public List<User> listAll(){
List<User> userList = new ArrayList();
User u1 = new User();
u1.setId(1L);
u1.setName("jack1");
User u2 = new User();
u2.setId(2L);
u2.setName("jack2");
User u3 = new User();
u3.setId(3L);
u3.setName("jack3");
userList.add(u1);
userList.add(u2);
userList.add(u3);
return userList;
}

启动user,直接访问“list-all”得到的结果如下:

【Spring Cloud总结】15.Eurek Ribbon Feign常见问题及解决_Eureka开启自我保护_10


然后我们需要在movie的服务中通过ribbon去调用这个服务,所以在movie工程的Controller方法中添加这样一个服务:

@GetMapping("/list-all")
public List<User> listAll(){
List<User> list = this.restRemplate.getForObject("http://microserver-provider-user/list-all/", List.class);
for(User user:list) {
System.out.println(user.getId());
}
return list;
}

注意,这里重点就是遍历list,如果不遍历,这个list还是可以打印到页面上的(转换json的过程不会出错)。

启动movie服务,访问一下它的“list-all”服务:

【Spring Cloud总结】15.Eurek Ribbon Feign常见问题及解决_System Status_11


可以看到报错了,报错原因我们查看控制台信息:

【Spring Cloud总结】15.Eurek Ribbon Feign常见问题及解决_ip-address_12


上面的意思是LinkedHashMap不能转换为User类,由此我们可以得知,通过restRemplate拿过来的List中的对象,不是我们期望的User类,而是LinkedHashMap。这是因为当我们调用postForObject方法时,restRemplate因为无法知道具体的实例化类型,所以将List中的实体解析为了LinkedHashMap,

解决办法,将接收的List类型数据,使用目标实体类的数组来接收:

@GetMapping("/list-all")
public List<User> listAll(){
User[] userArrays = this.restRemplate.getForObject("http://microserver-provider-user/list-all/", User[].class);
List<User> list = Arrays.asList(userArrays);//将数组转换为list
for(User user:list) {
System.out.println(user.getId());
}
return list;
}

重启项目,重新访问“list-all”服务,发现不再报错:

【Spring Cloud总结】15.Eurek Ribbon Feign常见问题及解决_System Status_13

三、Feign常见问题

1、自定义配置时,@Configuration和@ComponentScan包不应重叠
这一块和Ribbon一样,这里不再赘述。

2、@FeignClient所在的接口中,不支持@GetMapping等组合注解
通常我们在FeignClient中使用的接口修饰都是@RequestMapping或者@RequestLine(主要看Contract 契约的设定,默认是SpringMVC的@RequestMapping,配置成Default就是@RequestLine),例如之前编写的UserFeignClient:

package com.microserver.cloud;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestBody;

import com.microserver.cloud.entity.User;
import com.microserver.config.TestFeignConfiguration;

import feign.Param;
import feign.RequestLine;

@FeignClient(name="microserver-provider-user",configuration = TestFeignConfiguration.class )
public interface UserFeignClient {

@RequestLine("GET /findById/{id}")
public User findById(@Param("id") Long id);

@RequestLine("POST /postUser")
public String postUser(@RequestBody User user);

// @RequestMapping(value="/findById/{id}", method=RequestMethod.GET)
// public User findById(@PathVariable("id") Long id);

// @RequestMapping(value="/postUser", method=RequestMethod.POST)
// public String postUser(@RequestBody User user);

}

3、使用@PathVariable时,需要指定其value
我们在FeignClient中使用@PathVariable时,即使参数不使用别名,也需要为@PathVariable指定value。

4、Feign暂不支持复杂对象作为一个参数
例如之前我们在浏览器上调用testPullUser方法。url路径是这么拼写的:
http://localhost:7901/testPullUser?id=888&username=杰克&name=jack&age=25&balance=2500
在后台服务是这么写的:

@GetMapping("/testPullUser")
public String testPullUser(User user){
return userFeignClient.postUser(user);
}

userFeignClient的postUser方法如下:

@RequestMapping(value="/postUser", method=RequestMethod.POST)
public String postUser(@RequestBody User user);

我们在url后面拼接的参数,会被Spring MVC自动转换为User这个复杂对象,然后使用Feign接口进行数据的传输,此时是成功的。
那么当我们将“method=RequestMethod.POST”改为“method=RequestMethod.GET”的时候,即使服务提供端也改为GetMapping,此时请求也不会成功,因为只要参数是复杂对象,即使指定了GET方法,Feign依然会以POST方法进行请求的发送。

参考:《51CTO学院Spring Cloud高级视频》