Spring Cloud(2):服务发现(Eureka)
Spring Cloud Eureka是Spring Cloud Netflix项目下的一个模块,作用是服务的注册和发现,并实现服务治理。它有一个(或一组,以实现高可用)服务注册中心(eureka server)并提供服务注册功能,所有的应用程序将作为服务提供方(eureka client)向eureka server注册服务,当应用程序之间相互调用时,不再通过IP地址调用,将通过eureka server,使用注册的service id来调用。
下面,分别从5个方面来讲Eureka:Eureka Server,Eureka Client,Peer Awareness,Securing The Eureka Server,Discovery Client。
(1)搭建Eureka Server
首先,创建一个SpringBoot Web Aplication,在pom.xml中加入spring-cloud-starter-netflix-eureka-server包。这里使用的是Spring Boot 2.2.5和Spring Cloud Hoxton.SR3。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR3</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- Spring cloud starter: netflix-eureka-server --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies>
其次,在启动类ServerEurekaApplication中加入@EnableEurekaServer注解。
@SpringBootApplication @EnableEurekaServer public class ServerEurekaApplication { public static void main(String[] args) { SpringApplication.run(ServerEurekaApplication.class, args); } }
最后,在配置文件中配置Eureka信息。
bootstrap.yml
spring:
application:
name: server-eureka
application.yml
## Server info server: port: 10000 servlet: context-path: /server-eureka ## Eureka info eureka: instance: hostname: localhost # You need to change these, even for an Actuator application if you use a non-default context path or servlet path # https://cloud.spring.io/spring-cloud-netflix/spring-cloud-netflix.html#_status_page_and_health_indicator statusPageUrlPath: ${server.servlet.context-path}/actuator/info healthCheckUrlPath: ${server.servlet.context-path}/actuator/health client: # 程序启动时不要通过Eureka注册服务,因为它本身就是Eureka服务 registerWithEureka: false # 不会在本地缓存注册表信息 fetchRegistry: false serviceUrl: defaultZone: http://${eureka.instance.hostname}:${server.port}/server-eureka/eureka/ # Eureka不会马上通知任何注册它的服务,默认情况下会等待5min。本地测试时应该注释掉此行,以加快程序运行 # 每次服务注册需要30s的时间才能显示在Eureka服务中,因为Eureka需要接收3此心跳包,每次间隔10s,然后才能使用这个服务。 #server: # waitTimeInMsWhenSyncEmpty: 5 server: waitTimeInMsWhenSyncEmpty: 0
(2)搭建Eureka Client
首先,创建一个SpringBoot Web Aplication,在pom.xml中加入spring-cloud-starter-netflix-eureka-client包。
<!-- Spring cloud starter: netflix-eureka-client --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
然后,在配置文件中配置Eureka信息。
## Server Info
server:
port: 10010
servlet:
context-path: /app-web
## Eureka info
eureka:
instance:
# You need to change these, even for an Actuator application if you use a non-default context path or servlet path
# https://cloud.spring.io/spring-cloud-netflix/spring-cloud-netflix.html#_status_page_and_health_indicator
statusPageUrlPath: ${server.servlet.context-path}/actuator/info
healthCheckUrlPath: ${server.servlet.context-path}/actuator/health
# 注册服务的IP而不是服务器名称
# preferIpAddress: true
client:
# 向Eureka注册服务(default is true)
registerWithEureka: true
# 拉取注册表的本地副本(default is true)
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:10000/server-eureka/eureka/
[注1] preferIpAddress:在默认情况下,Eureka注册服务使用了主机名与外界联系,这种方式在服务器环境中是OK的,因为通过DNS可以解析成IP地址。但是,在基于容器的部署环境中(如Docker),主机名是随机生成的,且并没有DNS记录。将eureka.instance.preferIpAddress设置为true,当应用程序向eureka注册时,它使用其IP地址而不是其主机名。
如果设置eureka.instance.prefer-ip-address为false时,那么注册到Eureka中的IP地址就是本机的IP地址。如果设置了true并且也设置了eureka.instance.ip-address那么就将此ip地址注册到Eureka中。那么调用的时候,发送的请求目的地就是此Ip地址。
参考:
https://www.jianshu.com/p/886947b52cb4
[注2] 从Spring Cloud Edgware开始,@EnableDiscoveryClient或@EnableEurekaClient可省略。只需加上相关依赖,并进行相应配置,即可将微服务注册到服务发现组件上。Spring Cloud中的Discovery Service有多种实现,比如:eureka,consul,zookeeper。
- @EnableDiscoveryClient 注解是基于spring-cloud-commons依赖,并且在classpath中实现
- @EnableEurekaClient 注解是基于spring-cloud-netflix依赖,只能为eureka作用
如果你的classpath中添加了eureka,则它们的作用是一样的。
(3)Peer Awareness(高可用的Eureka集群)
假设我们现在有3台Eureka Server,IP地址分别为:192.168.0.1,192.168.0.2,192.168.0.3。配置文件修改如下:
application-peer1.yml
## Eureka info
eureka:
instance:
# http://cloud.spring.io/spring-cloud-netflix/spring-cloud-netflix.html#spring-cloud-eureka-server-peer-awareness
hostname: 192.168.0.1
client:
# 需要修改下面两个配置,让注册中心可以向另外一个注册中心注册服务,以实现高可用
registerWithEureka: true
fetchRegistry: true
# 向另2个Eureka Server注册自己
serviceUrl:
defaultZone: http://192.168.0.2:10000/server-eureka/eureka/,http://192.168.0.3:10000/server-eureka/eureka/
application-peer2.yml
eureka:
instance:
hostname: 192.168.0.2
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://192.168.0.1:10000/server-eureka/eureka/,http://192.168.0.3:10000/server-eureka/eureka/
application-peer3.yml
eureka:
instance:
hostname: 192.168.0.3
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://192.168.0.1:10000/server-eureka/eureka/,http://192.168.0.2:10000/server-eureka/eureka/
当我们在Eureka Client中向Eureka Server注册时,需要修改配置文件如下:
eureka:
client:
serviceUrl:
defaultZone: http://192.168.0.1:10000/server-eureka/eureka/,http://192.168.0.2:10000/server-eureka/eureka/,http://192.168.0.3:10000/server-eureka/eureka/
(4)使用Spring Security保护Eureka Server
首先,在pom.xml中加入spring-cloud-starter-netflix-eureka-client包。
<!-- Spring cloud starter: security --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> </dependency>
然后,添加一个user和password用于登录Eureka Server自带的页面:http://{host}:{port}/server-eureka。值得注意的是,需要disable'/eureka/**'端点的csrf()。
@EnableWebSecurity public class ServerEurekaWebSecurityConfigurer extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //@formatter:off PasswordEncoder encoder = new BCryptPasswordEncoder(); auth.inMemoryAuthentication() .withUser("eureka-user").password("{bcrypt}" + encoder.encode("eureka-user")).roles("USER"); //@formatter:on } @Override protected void configure(HttpSecurity http) throws Exception { // By default when Spring Security is on the classpath, // it will require that a valid CSRF token be sent with every request to the app. // Eureka clients will not generally possess a valid cross site request forgery (CSRF) token, // you will need to disable this requirement for the /eureka/** endpoints. // https://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.1.2.RELEASE/single/spring-cloud-netflix.html#_securing_the_eureka_server http.csrf().ignoringAntMatchers("/eureka/**"); super.configure(http); } }
最后,所有向Eureka Server注册的URL都要改成这样的形式:http://user:password@localhost:8761/eureka。
上面(3)中Eureka Server集群中的相互注册:
eureka: username: eureka-user password: '{cipher}72cd0bdd18c6928b025e9e5dfa94cce539b555c4b3364590c689df3532fa69bc' client: serviceUrl: defaultZone: http://${eureka.username}:${eureka.password}@192.168.0.2:10000/server-eureka/eureka/,http://${eureka.username}:${eureka.password}@192.168.0.3:10000/server-eureka/eureka/
上面(3)中Eureka Client中的注册:
eureka: username: eureka-user password: '{cipher}72cd0bdd18c6928b025e9e5dfa94cce539b555c4b3364590c689df3532fa69bc' client: serviceUrl: defaultZone: http://${eureka.username}:${eureka.password}@192.168.0.1:10000/server-eureka/eureka/,http://${eureka.username}:${eureka.password}@192.168.0.2:10000/server-eureka/eureka/,http://${eureka.username}:${eureka.password}@192.168.0.3:10000/server-eureka/eureka/
[注] 上面密码使用JCE(Java Cryptography Extension)的对称加密,这部分可以看 Spring Cloud(4):配置服务(Config)。
(5)在Eureka Client中使用Discovery Client来发现并调用其他Client服务
一个微服务架构中会有多个Eureka Client,当它们向Eureka Server注册后,就可以通过下面2种方法相互调用:
1. 使用EurekaClient(DiscoveryClient)
@Autowired private EurekaClient discoveryClient; public String serviceUrl() { InstanceInfo instance = discoveryClient.getNextServerFromEureka("app-name", false); String path = String.format("http://%s:%s/aaa/bbb", instance.getHostName(), instance.getPort()); RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String> response = restTemplate.exchange(path, HttpMethod.GET, null, String.class); return response.getBody(); }
2. 使用带有Ribbon功能的LoadBalancerClient(负载均衡)
@Autowired private LoadBalancerClient loadBalancer; public String serviceUrl() { ServiceInstance instance = loadBalancer.choose("app-db"); String path = String.format("http://%s:%s/aaa/bbb", instance.getHost(), instance.getPort()); RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String> response = restTemplate.exchange(path, HttpMethod.GET, null, String.class); return response.getBody(); }
Hoxton.SR3