服务发现是基于微服务架构的关键原则之一。尝试配置每个客户端或某种形式的约定可能非常困难,可以非常脆弱。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 上下文中查找您的实现,并将其包装在自己的插件中。