Eureka是负责服务注册与发现的。
Eureka分为服务端Eureka Server和客户端Eureka Client。服务端就是注册中心,可单机,可集群,生产上肯定集群。客户端就是各业务应用,根据调用关系又分为服务提供者和服务调用者,这些应用都要注册到服务端。
Eureka服务端和客户端都是springboot应用。
Eureka服务端:本处讲解单机Eureka Server。其实集群也非常简单,就是把每个Eureka Server节点都注册到其他Eureka server节点上。
1、在应用中引入Eureka Server依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
2、用@EnableEurekaServer标注启动类
3、在配置文件中添加
spring.application.name=spring-cloud-eureka-server
server.port=8000
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8000/eureka/
以上两个false表示不将自己注册到Eureka Server。eureka.client.serviceUrl.defaultZone不能省,不然Eureka Server应用日志会一直报Network level connection to peer localhost; retrying after delay,虽然不影响注册中心的使用。
启动应用,访问127.0.0.1:8000,可以看到注册中心启动成功
Eureka Server有定时任务:
每1分钟会执行AbstractInstanceRegistry的run()方法。AbstractInstanceRegistry有个EvictionTask内部类,EvictionTask其实是个线程类,继承了实现Runnable接口的TimerTask类。
每15分钟更新renewal threshold。PeerAwareInstanceRegistryImpl类中,有一个Timer实例,每15分钟执行一次updateRenewalThreshold()方法,这个15分钟是由EurekaServerConfig接口的getRenewalThresholdUpdateIntervalMs()方法取的,默认15分钟,我们可以在配置文件中用eureka.server.renewal-threshold-update-interval-ms修改间隔时间
Eureka Client之服务提供者
1、在一个对外提供RESTful接口服务的springboot应用中(比如说X管家应用)引入Eureka Client依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2、用@EnableDiscoveryClient标注启动类
3、把自己注册到Eureka Server。在配置文件中添加
spring.application.name=spring-cloud-eureka-client-producer
server.port=9000
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8000/eureka/
spring.application.name很重要,届时服务调用者就根据这个来找服务提供者。eureka.client.serviceUrl.defaultZone值是Eureka Server的地址,如果Eureka Server有多个节点,则用逗号分隔。
启动应用,刷新Eureka Server的页面,发现Instances currently registered with Eureka处多了一行数据,正是刚刚启动的Eureka Client,说明服务提供者注册成功了。
Eureka Client之服务调用者
1、引入Eureka Client依赖,推荐用Feign调用服务。如果当前应用也对外提供RESTful接口,则还需要引入springboot的web starter。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2、用@EnableDiscoveryClient和@EnableFeignClients标注启动类。
3、把自己注册到Eureka Server。在配置文件中添加
spring.application.name=spring-cloud-eureka-client-consumer
server.port=9001
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:8000/eureka/
4、写Feign代码,调用服务提供者的接口。
@FeignClient(name = "spring-cloud-eureka-client-producer")
@Component
public interface HelloRemote {
@RequestMapping(value = "/hello")
String hello(@RequestParam String name);
}
@FeignClient的name属性值必须是Eureka Client服务提供者应用的spring.application.name值。@RequestMapping的value属性值就是要调用的服务提供者的接口,本例中是/hello接口。用@RequestMapping标注的接口,既可以向服务提供者发送GET请求,又可以发送POST请求,默认是GET请求。如果想发起POST请求,则要用@PostMapping。不管是GET请求,还是POST请求,各参数都要用@RequestParam标注,否则发出的请求是不带参数的。如果是发application/json请求,则要用@RequestBody标注参数,参数类型可以是Map,也可以是自定义实体类。服务提供者的接口如果用自定义实体类接收参数的话,则要有无参构造器,否则会报Invalid JSON input: Cannot construct instance。
在Controller或者Service代码中注入HelloRemote即可,如此服务调用者就可以调用服务提供者提供的接口了。例如
@RestController
public class ConsumerController {
@Autowired
HelloRemote helloRemote;
@RequestMapping("/hello/{name}")
public String index(@PathVariable("name") String name) {
return helloRemote.hello(name);
}
}
启动Eureka Client服务调用者,刷新Eureka Server页面,会发现Instances currently registered with Eureka处新增了本应用。
在浏览器中调用服务消费者提供的/hello/{name}接口,如127.0.0.1:9001/hello/zhansan,服务消费者在处理请求过程中会调用服务提供者提供的/hello接口,即127.0.0.1:9000/hello/zhansan,服务提供者响应数据给服务调用者,服务调用者得到数据后再处理,最终响应数据给前端页面。
Eureka client启动的时候会向Eureka server发送注册请求,Eureka server的类会处理这些请求。
客户端的DiscoveryClient在实例化时,会构造几个定时任务,拉取注册信息的任务、发送心跳的任务,默认都是30s执行一次。
Eureka自我保护机制:
在某些情况下,Eureka Server页面会出现
这就说明Eureka自我保护机制在起作用了。
Feign
Feign能够在接口上添加注释,成为一个REST API的客户端。用@FeignClient标注接口,其name属性值表示调用哪个服务,fallback值是当前接口的实现类的class实例,在服务提供者不稳定或宕机导致熔断时,提供降级数据。
上面Feign的配置,在调用第三方接口时,使用的是jdk原生的HttpURLConnection发送http请求,没有连接池。可以在服务提供者应用中查看请求的user-agent请求头来验证这一点,此时user-agent的值是Java。
这里可以优化下,用HttpClient,通过设置连接池、超时时间等对服务之间的调用进行优化。
如果用HttpClient,则需要修改如下
1、引入Feign和HttpClient的整合包
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>10.8</version>
</dependency>
2、在配置文件中添加
feign.httpclient.enabled=true
这样就可以了。在服务提供者应用中查看请求的user-agent请求头,发现user-agent的值变为Apache-HttpClient。连接池的最大连接数、每个连接的存活时间会用一些默认配置,这些默认配置可以在配置文件中用feign.httpclient.来查看,如果想更改这些参数,只需新增配置项覆盖默认配置即可。注意,feign.httpclient.connection-timeout这个配置本是用来指定连接超时时间的,但是实际使用不生效,超时配置需要使用feign.client.config。
超时重试机制:
重试只会在超时的情况下才会发生,404、50X的情况不会重试,每次重试都会根据负载均衡策略重新找服务器。
超时重试配置:
1、在配置文件中添加:
feign.client.config.default.connect-timeout=10000
feign.client.config.default.read-timeout=10000
default表示对所有的服务都适用,本例连接超时和读取超时都是10s。如果想针对不同的服务指定不同的超时时间,把default用特定服务名替代即可。
2、生成一个Retryer实例
@Bean
public Retryer feignRetryer() {
return new Retryer.Default(1000, 2000, 3);
}
Retryer是个接口,可以用其内部实现类Default来构造Retryer实例。第三个参数3表示最多执行3次,最多重试2次。
重试相关的源码在SynchronousMethodHandler的invoke()方法中:
public Object invoke(Object[] argv) throws Throwable { RequestTemplate template = buildTemplateFromArgs.create(argv); Options options = findOptions(argv); Retryer retryer = this.retryer.clone(); while (true) { try { return executeAndDecode(template, options); } catch (RetryableException e) { try { retryer.continueOrPropagate(e); } catch (RetryableException th) { Throwable cause = th.getCause(); if (propagationPolicy == UNWRAP && cause != null) { throw cause; } else { throw th; } } if (logLevel != Logger.Level.NONE) { logger.logRetry(metadata.configKey(), logLevel); } continue; } } }
重试这里有个死循环,第一次调用,假如不抛出超时异常,则跳出循环,否则捕获异常,在catch语句中,我们可以做一些延时操作,使得可以稍微等一会再重试。如果重试次数达到上限,则在catch语句中抛出异常,跳出死循环。在创建Retryer实例时可以指定每次重试之前等多少时间以及重试几次,默认会最多等1s,重试5次。
Ribbon
如果有多个服务提供者,则会自动用Ribbon做负载均衡。Ribbon提供多种负载均衡策略,如默认的ZoneAvoidanceRule、轮询RoundRobinRule、根据响应时间加权轮询WeightedResponseTimeRule、随机RandomRule、可用性过滤AvailabilityFilteringRule、最少连接数BestAvailableRule、RetryRule。
假设一个应用内会调用多种服务,那么如何针对不同的服务指定不同的负载均衡策略呢?
假设想调用service1时根据响应时间加权轮询,调用service2时随机,则只需在配置文件中添加
service1.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.WeightedResponseTimeRule
service2.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
如果想所有的服务都用相同的负载均衡策略,比如说轮询策略,则可以在启动类所在包或其子包下新建一个config类,里面用@Bean生成一个IRule实例:
@Bean
public IRule myRule() {
return new RandomRule();
}
不生成IRule实例的话,默认所有服务的负载均衡策略都是ZoneAvoidanceRule。
我们也可以用spring cloud balancer替换Ribbon(在配置文件中,设置spring.cloud.loadbalancer.ribbon.enabled=false),但是spring cloud balancer目前只提供一个原生的负载策略,即轮询,其他策略需要我们自己实现,所有还是Ribbon香。