spring cloud 之eureka使用详解

spring cloud 之eureka使用详解,从零开始搭建eureka注册中心,进行微服务间调用

Posted by yishuifengxiao on 2019-08-13

服务发现是基于微服务架构的关键原则之一。尝试配置每个客户端或某种形式的约定可能非常困难,可以非常脆弱。Netflix 服务发现服务器和客户端是 Eureka。可以将服务器配置和部署为高可用性,每个服务器将注册服务的状态复制到其他服务器。

一 eureka 注册中心

1.1 单节点 eureka 注册中心

在使用 eureka 之前,需要先搭建一个 eureka 注册中心,以提供给 eureka 客户端注册使用。

搭建 eureka 注册中心的步骤如下:

1 新建一个 spring cloud 项目,在项目的 pom 文件里加入依赖

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

2 配置属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring.application.name=eureka-server

server.port=8761
eureka.instance.hostname=localhost

# 由于是该应用已是注册中心,所以设置为false,表明不向注册中心注册自己
eureka.client.registerWithEureka=false

# 由于注册中心的职责是维护服务实例,它并不需要去检索服务,所以设置为false
eureka.client.fetchRegistry=false

# Turn off self-protection mode
eureka.server.enableSelfPreservation=false

eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/

注意

关于 eureka.client.serviceUrl.defaultZone具体解释请参照第二章中解释

如果项目已经使用 thymeleaf 作为模板引擎,则可能无法正确加载 Eureka 服务器的 FreeMarker 模板。在这种情况下,需要手动配置模板加载器

1
2
spring.freemarker.template-loader-path=classpath:/templates/
spring.freemarker.prefer-file-system-access=false

3 启动项目

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableEurekaServer
public class Application {

public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}

}

启动项目后,可通过 http://ip:port/ 访问注册服务器的主页,服务提供者/消费者可以通过http://ip:port/eureka向注册中心注册。

默认情况下,每个 Eureka 服务器也是 Eureka 客户端,并且至少需要一个服务 URL 来定位对等体。 如果您不提供该服务,虽然该服务将运行并正常运行,但它会在您的日志中填充很多关于无法向对等方注册的噪音。

1.2 增加安全配置

1 在项目的 pom 文件里增加依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

2 增加安全配置

1
2
3
4
5
6
7
8
9
@EnableWebSecurity
class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().ignoringAntMatchers("/eureka/**");
super.configure(http);
}
}

3 属性配置

1
2
spring.security.user.name=admin
spring.security.user.password=yishui

加入上述配置后,再通过http://ip:port/访问注册中心的主页时就需要先进行登录了,登陆时的用户名和密码就是上面配置中配置的值(例如 admin 和 yishui),同时服务提供者/消费者向注册中心注册时的 url 也变成了
http://admin:yishui@ip:port/eureka/

1.3 高可用注册中心

在单节点注册中心的配置中,设置了应用不向服务注册中心注册自己,为了保持高可用,需要注册中心将自己作为服务像其他的注册中心注册自己,这样就可以形成一组互相注册的服务注册中心了。

1
2
3
4
5
eureka.client.registerWithEureka=true

eureka.client.fetchRegistry=true

eureka.client.serviceUrl.defaultZone=其他的eureka注册中心

二 eureka 客户端

2.1 快速启动

2.1.1 映入依赖

在项目的 pom 依赖文件里加入:

1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
2.1.2 属性配置

在项目的属性配置文件里加入以下配置:

1
2
3
4
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/

其中defaultZone是一个默认值,为任何不表示首选项的客户端提供服务 URL。

Environment获取的默认应用程序名称(服务 ID),虚拟主机和非安全端口分别为${spring.application.name}${spring.application.name}${server.port}

@EnableEurekaClient使应用程序成为 Eureka“实例”(即注册自身)和“客户端”(即它可以查询注册表以查找其他服务)。实例行为由eureka.instance.*配置键驱动,但如果您确保您的应用程序具有spring.application.name(这是 Eureka 服务标识或首选的默认值),那么默认值将会很好。

如果其中一个eureka.client.serviceUrl.defaultZone URL 中包含一个凭据(如http://user:password@localhost:8761/eureka)),HTTP 基本身份验证将自动添加到您的 eureka 客户端。对于更复杂的需求,您可以创建@Bean类型DiscoveryClientOptionalArgs并注入ClientFilter实例,所有这些都将应用于从客户端到服务器的调用。

由于 Eureka 中的限制,不可能支持每个服务器的基本身份验证凭据,因此只能使用第一个找到的集合。

2.1.3 启动项目

当客户端注册 Eureka 时,它提供有关自身的元数据,例如主机和端口,运行状况指示符 URL,主页等。Eureka 从属于服务的每个实例接收心跳消息。如果心跳失败超过可配置的时间表,则通常将该实例从注册表中删除。

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

@RequestMapping("/")
public String home() {
return "Hello world";
}

public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}

}

2.2 状态页、健康页和主页

Eureka 实例的状态页面和运行状况指示器分别默认为/actuator/info/actuator/health,它们是 Spring Boot Actuator 应用程序中有用端点的默认位置。如果您使用非默认上下文路径或 servlet 路径(例如server.servletPath=/foo)或管理端点路径(例如management.contextPath=/admin)),则需要更改这些,即使是执行器应用程序

1
2
3
4
5
eureka:
instance:
statusPageUrlPath: ${server.servletPath}/info
healthCheckUrlPath: ${server.servletPath}/health
homePageUrl: https://${eureka.hostname}/

注意,${eureka.hostname}只是在 Eureka 的更高版本中可用的本地占位符,您也可以使用 Spring 占位符实现同样的功能,例如使用${eureka.instance.hostName}

在 spring cloud Dalston 版本中,如果management.contextPath发生的变化,需要重新配置状态页和健康检查页,从 Edgware 版本后移出了这个要求

默认情况下,Eureka 使用客户端心跳来确定客户端是否已启动。除非另有规定,否则 Discovery Client 将不会根据 Spring Boot Actuator 传播应用程序的当前运行状况检查状态。这意味着成功注册后 Eureka 将永远宣布申请处于UP状态。可以通过启用 Eureka 运行状况检查来改变此行为,从而将应用程序状态传播到 Eureka。因此,每个其他应用程序将不会在UP之外的状态下将流量发送到应用程序。

1
2
3
4
eureka:
client:
healthcheck:
enabled: true

特别注意

eureka.client.healthcheck.enabled=true只能在application.yml中设置。设置bootstrap.yml中的值将导致不良副作用,例如在 eureka 中注册UNKNOWN状态

2.3 更改 Eureka 实例 ID

Eureka 实例注册了与其主机名相同的 ID(即每个主机只有一个服务)。Spring Cloud Eureka 提供了一个默认值,如下所示:

1
${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}}}

例如 myhost:myappname:8080

使用 Spring Cloud,您可以通过在eureka.instance.instanceId中提供唯一的标识来覆盖此。例如

1
2
3
eureka:
instance:
instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}}

2.4 服务分区

如果您已将 Eureka 客户端部署到多个区域,您可能希望这些客户端在使用另一个区域中的服务之前,在同一区域内利用服务。为此,您需要正确配置您的 Eureka 客户端。

接下来,您需要告知 Eureka 您的服务所在的区域。您可以使用metadataMap属性来执行此操作。例如,如果将 service 1 部署到 zone 1 和 zone 2,则需要在 service 1 中设置以下 Eureka 属性

1 区服务 1

1
2
eureka.instance.metadataMap.zone = zone1
eureka.client.preferSameZoneEureka = true

第 2 区的服务 1

1
2
eureka.instance.metadataMap.zone = zone2
eureka.client.preferSameZoneEureka = true

2.5 设置偏向 IP

在某些情况下,Eureka 最好是通知服务的 IP 地址而不是主机名。将eureka.instance.preferIpAddress设置为true,当应用程序向 eureka 注册时,它将使用其 IP 地址而不是其主机名。

1
2
3
eureka:
instance:
prefer-ip-address: true

2.6 EurekaClient 的使用

一个比较完整的属性配置为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
eureka:
client:
healthcheck:
enabled: true #健康检查
preferSameZoneEureka: true
#serviceUrl:
# defaultZone: ${defaultZone}
instance:
#ip-address: ${ip-address}
# hostname: ${eureka.instance.ip-address}
prefer-ip-address: true #偏向地址
home-page-url-path: ${server.servlet.context-path} #首页
health-check-url-path: ${server.servlet.context-path}/actuator/health #健康检查页
status-page-url-path: ${server.servlet.context-path}/actuator/info #状态页
instanceId: ${spring.application.name}:${server.port}:${random.int(10)} # 服务实例ID
metadata-map:
context-path: ${server.servlet.context-path} #自定义属性
enable: true #是否被收集到swagger-ui里面,自定义属性
zone: yishui
version: ${info.version}
profile: ${spring.profiles.active}
management:
context-path: ${server.servlet.context-path}/actuator

一旦您有一个@EnableDiscoveryClient(或@EnableEurekaClient)的应用程序,您可以使用它来从 Eureka 服务器发现服务实例。一种方法是使用本机com.netflix.discovery.EurekaClient(而不是Spring Cloud DiscoveryClient

1
2
3
4
5
6
7
@Autowired
private EurekaClient discoveryClient;

public String serviceUrl() {
InstanceInfo instance = discoveryClient.getNextServerFromEureka("STORES", false);
return instance.getHomePageUrl();
}

不要使用@PostConstruct方法或@Scheduled方法(或ApplicationContext可能尚未启动的任何地方)EurekaClient。它被初始化为SmartLifecycle(使用 phase=0),所以您可以依赖的最早可用的是另一个具有较高阶段的SmartLifecycle

您还可以使用org.springframework.cloud.client.discovery.DiscoveryClient,它为 Netflix 不具体的发现客户端提供简单的 API,例如

1
2
3
4
5
6
7
8
9
10
@Autowired
private DiscoveryClient discoveryClient;

public String serviceUrl() {
List<ServiceInstance> list = discoveryClient.getInstances("STORES");
if (list != null && list.size() > 0 ) {
return list.get(0).getUri();
}
return null;
}

三 服务间调用

在实现服务间调用之前,需要先按照第二章中的配置完全前置配置,并将服务注册到服务中心。

3.1 ribbon 方式

3.1.1 快速入门

1 在项目的 pom 文件里加入依赖

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

2 开启均衡负载功能

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

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

@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}

}

这里的负载均衡设置对声明性 REST 客户端 Feign 也适用

3 实现服务间调用

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class HelloService {

@Autowired
RestTemplate restTemplate;

public String hiService(String name) {
return restTemplate.getForObject("http://SERVICE-DEMO/hi?name="+name,String.class);
}


}

上述代码中,SERVICE-DEMO是目标服务的服务名字

3.1.2 进阶使用

直接使用 Ribbon 接口

1
2
3
4
5
6
7
8
9
10
public class MyClass {
@Autowired
private LoadBalancerClient loadBalancer;

public void doStuff() {
ServiceInstance instance = loadBalancer.choose("stores");
URI storesUri = URI.create(String.format("http://%s:%s", instance.getHost(), instance.getPort()));
// ... do something with the URI
}
}

3.2 声明性 REST 客户端 Feign

3.2.1 快速入门

1 在项目的 pom 文件里加入依赖

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

2 实现服务间调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SpringBootApplication
@EnableFeignClients
public class Application {

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

}

@FeignClient("stores")
public interface StoreClient {
@RequestMapping(method = RequestMethod.GET, value = "/stores")
List<Store> getStores();

@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
Store update(@PathVariable("storeId") Long storeId, Store store);
}

@FeignClient注释中,stores是一个服务的名字,用于创建 Ribbon 负载平衡器。您还可以使用 url 属性(绝对值或只是主机名)指定 URL。应用程序上下文中的 bean 的名称是该接口的完全限定名称。要指定您自己的别名值,您可以使用@FeignClient注释的qualifier值。

3.2.2 覆盖 Feign 默认值

Spring Cloud 可以通过使用@FeignClient声明额外的配置(FeignClientsConfiguration)来完全控制假客户端。例:

1
2
3
4
@FeignClient(name = "stores", configuration = FooConfiguration.class)
public interface StoreClient {
//..
}

在这种情况下,客户端由FeignClientsConfiguration中的组件与FooConfiguration中的任何组件组成(后者将覆盖前者)

注意

FooConfiguration不要被@Configuration 注解,如果注解了,请保证不要被@ComponentScan扫描到。

Spring Cloud Netflix 默认情况下不提供以下 bean,但是仍然从应用程序上下文中查找这些类型的 bean 以创建假客户机:

  • Logger.Level
  • Retryer
  • ErrorDecoder
  • Request.Options
  • Collection
  • SetterFactory

创建一个类型的 bean 并将其放置在@FeignClient 配置(例如上面的FooConfiguration)中)允许您覆盖所描述的每个 bean。例:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class FooConfiguration {
@Bean
public Contract feignContract() {
return new feign.Contract.Default();
}

@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("user", "password");
}
}

这将SpringMvcContract替换为feign.Contract.Default,并将RequestInterceptor添加到RequestInterceptor的集合中。

@FeignClient也可以通过一下的属性进行配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
feign:
client:
config:
feignName:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
errorDecoder: com.example.SimpleErrorDecoder
retryer: com.example.SimpleRetryer
requestInterceptors:
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
decode404: false
encoder: com.example.SimpleEncoder
decoder: com.example.SimpleDecoder
contract: com.example.SimpleContract

其他的一些参数配置

1
2
3
4
5
6
7
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic
3.2.3 手动创建 Feign 客户端

在某些情况下,可能需要以上述方法不可能自定义您的 Feign 客户端。在这种情况下,您可以使用 Feign Builder API 创建客户端 。下面是一个创建两个具有相同接口的 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
@Import(FeignClientsConfiguration.class)
class FooController {

private FooClient fooClient;

private FooClient adminClient;

@Autowired
public FooController(Decoder decoder, Encoder encoder, Client client, Contract contract) {
this.fooClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
.target(FooClient.class, "http://PROD-SVC");

this.adminClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
.target(FooClient.class, "http://PROD-SVC");
}
}

PROD-SVC是客户端将要求的服务的名称。

3.2.4 Feign Hystrix 支持

如果Hystrix在类路径上,feign.hystrix.enabled=true,Feign 将用断路器包装所有方法。还可以返回com.netflix.hystrix.HystrixCommand

要在每个客户端基础上禁用 Hystrix 支持创建一个带有prototype范围的Feign.Builder,例如

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

Hystrix 支持回退的概念:当电路断开或出现错误时执行的默认代码路径。要为给定的@FeignClient启用回退,请将 fallback 属性设置为实现回退的类名。

1
2
3
4
5
6
7
8
9
10
11
12
@FeignClient(name = "hello", fallback = HystrixClientFallback.class)
protected interface HystrixClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello iFailSometimes();
}

static class HystrixClientFallback implements HystrixClient {
@Override
public Hello iFailSometimes() {
return new Hello("fallback");
}
}

如果需要访问导致回退触发的原因,可以使用@FeignClient内的fallbackFactory属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@FeignClient(name = "hello", fallbackFactory = HystrixClientFallbackFactory.class)
protected interface HystrixClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello iFailSometimes();
}

@Component
static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClient> {
@Override
public HystrixClient create(Throwable cause) {
return new HystrixClient() {
@Override
public Hello iFailSometimes() {
return new Hello("fallback; reason was: " + cause.getMessage());
}
};
}
}
3.2.6 Feign 请求/响应压缩

您可以考虑为 Feign 请求启用请求或响应 GZIP 压缩。您可以通过启用其中一个属性来执行此操作

1
2
feign.compression.request.enabled=true
feign.compression.response.enabled=true

Feign 请求压缩为您提供与您为 Web 服务器设置的设置相似的设置

1
2
3
feign.compression.request.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048

这些属性可以让您对压缩介质类型和最小请求阈值长度有选择性

四 服务容错保护

4.1 快速入门

1 首先在项目的 pom 文件里加入依赖

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

2 使用示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@SpringBootApplication
@EnableCircuitBreaker
public class Application {

public static void main(String[] args) {
new SpringApplicationBuilder(Application.class).web(true).run(args);
}

}

@Component
public class StoreIntegration {

@HystrixCommand(fallbackMethod = "defaultStores")
public Object getStores(Map<String, Object> parameters) {
//do stuff that might fail
}

public Object defaultStores(Map<String, Object> parameters) {
return /* something useful */;
}
}

@HystrixCommand由名为 javanica的 Netflix contrib 库提供 。Spring Cloud 使用该注释在连接到 Hystrix 断路器的代理中自动包装 Spring bean。断路器计算何时打开和关闭电路,以及在发生故障时应该做什么。

要配置@HystrixCommand,您可以使用commandProperties属性列出@HystrixProperty注释。请参阅 这里 了解更多详情。有关 可用属性的详细信息

4.2 在 ribbon 使用断路器

1 在程序的启动类 ServiceRibbonApplication 加@EnableHystrix 注解开启 Hystrix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@EnableHystrix
public class Application {

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

@Bean
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}

}

2 使用断路器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public class DemoService {

@Autowired
RestTemplate restTemplate;

@HystrixCommand(fallbackMethod = "fallback")
public String hiService(String name) {
return restTemplate.getForObject("http://SERVICE-HI/hi?name="+name,String.class);
}

public String fallback(String name) {
return "hi,"+name+",fallback";
}

}

4.3 Feign 中使用断路器

Feign 是自带断路器的它没有默认打开。需要在配置文件中配置打开它,在配置文件加以下代码

1 开启断路器功能

1
feign.hystrix.enabled=true

2 使用断路器

1
2
3
4
5
6
7
8
9
10
11
12
13
@FeignClient(value = "DEMO_SERVICE",fallback = DemoServiceHystric.class)
public interface DemoService {
@RequestMapping(value = "/demo",method = RequestMethod.GET)
String demo(@RequestParam(value = "name") String name);
}

@Component
public class DemoServiceHystric implements DemoService {
@Override
public String demo(String name) {
return "sorry "+name;
}
}

4.4 传播安全上下文

如果您希望某些线程本地上下文传播到@HystrixCommand,默认声明将不起作用,因为它在线程池中执行命令(超时)。您可以使用某些配置或直接在注释中使用与使用相同的线程来调用 Hystrix,方法是要求使用不同的“隔离策略”。例如:

1
2
3
4
5
@HystrixCommand(fallbackMethod = "stubMyService",
commandProperties = {
@HystrixProperty(name="execution.isolation.strategy", value="SEMAPHORE")
}
)

如果您使用@SessionScope@RequestScope,同样的事情也适用。您将知道何时需要执行此操作,因为运行时异常说它找不到范围的上下文。

您还可以选择将hystrix.shareSecurityContext属性设置为true。这样做会自动配置一个Hystrix并发策略插件钩子,他将SecurityContext从主线程传输到Hystrix命令使用的钩子。Hystrix不允许多个hystrix并发策略被注册,因此通过将自己的HystrixConcurrencyStrategy声明为 Spring bean 可以使用扩展机制。Spring Cloud 将在 Spring 上下文中查找您的实现,并将其包装在自己的插件中。