3.5 Eureka实战
3.5.1 Eureka Server在线扩容
(1)准备工作
由于我们需要动态去修改配置,因此这里引入config-server工程,如代码清单3-1所示。
代码清单3-1 ch3-1\ch3-1-config-server\pom.xml
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency> </dependencies>
启动类如代码清单3-2所示:
代码清单3-2 ch3-1\ch3-1-config-server\src\main\java\cn\springcloud\book\Ch31Config ServerApplication.java
@SpringBootApplication @EnableConfigServer public class Ch31ConfigServerApplication { public static void main(String[] args) { SpringApplication.run(Ch31ConfigServerApplication.class, args); } }
配置文件如代码清单3-3所示:
代码清单3-3 ch3-1\ch3-1-config-server\src\main\resources\bootstrap.yml
spring: application: name: config-server profiles: active: native server: port: 8888
这里为了简单演示,我们使用native的proflie,即使用文件来存储配置,默认放在resources\config目录下。
另外由于要演示Eureka Server的动态扩容,这里还建立了一个eureka-server工程及eureka-client工程,分别见ch3-1\ch3-1-eureka-server、ch3-1\ch3-1-eureka-client。与第2章的工程的区别在于这里引入了spring-cloud-starter-config,另外两个工程都添加了一个QueryController用于实验,如代码清单3-4所示。
代码清单3-4 ch3-1\ch3-1-eureka-server\src\main\cn\springcloud\book\controller\QueryController.java
@RestController @RequestMapping("/query") public class QueryController { @Autowired EurekaClientConfigBean eurekaClientConfigBean; @GetMapping("/eureka-server") public Object getEurekaServerUrl(){ return eurekaClientConfigBean.getServiceUrl(); } }
(2)1个Eureka Server
eureka-client的配置文件如代码清单3-5所示:
代码清单3-5 ch3-1\ch3-1-config-server\src\main\resources\config\eureka-client.yml
server: port: 8081 spring: application: name: eureka-client1 eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ # one eureka server
eureka-server的配置文件如代码清单3-6所示:
代码清单3-6 ch3-1\ch3-1-config-server\src\main\resources\config\eureka-server-peer1.yml
server: port: 8761 eureka: instance: hostname: localhost preferIpAddress: true client: registerWithEureka: true fetchRegistry: true serviceUrl: defaultZone: http://localhost:8761/eureka/ # one eureka server server server: waitTimeInMsWhenSyncEmpty: 0 enableSelfPreservation: false
然后分别启动config-server、eureka-server(使用peer1的profile)、eureka-client,打开localhost:8761,可以观察注册的实例。
(3)2个Eureka Server
现在我们开始把Eureka Server的实例扩充一下,在ch3-1-eureka-server工程目录下,使用peer2的profile启动第二个Eureka Server,命令行如下:
mvn spring-boot:run -Dspring.profiles=active=peer2
其配置文件如代码清单3-7所示:
代码清单3-7 ch3-1\ch3-1-config-server\src\main\resources\config\eureka-server-peer2.yml
server: port: 8762 eureka: instance: hostname: localhost preferIpAddress: true client: registerWithEureka: true fetchRegistry: true serviceUrl: defaultZone: http://localhost:8761/eureka/ # two eureka server server: waitTimeInMsWhenSyncEmpty: 0 enableSelfPreservation: false
第二个Eureka Server启动起来之后,要修改eureka-client.yml、eureka-server-peer1.yml的配置文件:
其中eureka-client.yml变为:
server: port: 8081 spring: application: name: eureka-client1 eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/, http://localhost:8762/ eureka/ # two eureka server
修改eureka.client.serviceUrl,新增第二个Eureka Server的地址。
eureka-peer1.yml变为:
server: port: 8761 spring: application: name: eureka-server eureka: instance: hostname: localhost preferIpAddress: true client: registerWithEureka: true fetchRegistry: true serviceUrl: defaultZone: http://localhost:8762/eureka/ # two eureka server server: waitTimeInMsWhenSyncEmpty: 0 enableSelfPreservation: false
修改eureka.client.serviceUrl,指向第二个Eureka Server。
之后就是重启config-server,使配置生效。然后使用如下命令分别刷新eureka-client以及eureka-server-peer1,加载新配置:
~ curl -i -X POST localhost:8761/actuator/refresh HTTP/1.1200 Content-Type: application/vnd.spring-boot.actuator.v2+json; charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 20 Jun 2018 11:10:10 GMT ["eureka.client.serviceUrl.defaultZone"]% ~ curl -i -X POST localhost:8081/actuator/refresh HTTP/1.1200 Content-Type: application/vnd.spring-boot.actuator.v2+json; charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 20 Jun 2018 11:10:17 GMT ["eureka.client.serviceUrl.defaultZone"]%
之后分别调用queryController的方法,如下:
~ curl -i http://localhost:8761/query/eureka-server HTTP/1.1200 Content-Type: application/json; charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 20 Jun 2018 11:12:07 GMT {"defaultZone":"http://localhost:8762/eureka/"}% → ~ curl -i http://localhost:8081/query/eureka-server HTTP/1.1200 Content-Type: application/json; charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 20 Jun 2018 11:12:15 GMT {"defaultZone":"http://localhost:8761/eureka/, http://localhost:8762/eureka/"}%
可以看到Eureka Client端已经成功识别到两个Eureka Server,而原来peer1的Eureka Server请求的eureka.client.serviceUrl也指向了peer2, Eureka Server扩容成功。
(4)3个Eureka Server
将Eureka Server扩容到3个实例的话,这里使用peer3配置,如代码清单3-8所示。
代码清单3-8 ch3-1\ch3-1-config-server\src\main\resources\config\eureka-server-peer3.yml
server: port: 8763 eureka: instance: hostname: localhost preferIpAddress: true client: registerWithEureka: true fetchRegistry: true serviceUrl: defaultZone: http://localhost:8761/eureka/, http://localhost:8762/ eureka/ # three eureka server server: waitTimeInMsWhenSyncEmpty: 0 enableSelfPreservation: false
启动命令如下:
mvn spring-boot:run -Dspring.profiles.active=peer3
接下来修改eureka-client.yml配置,如下所示:
server: port: 8081 spring: application: name: eureka-client1 eureka: client: serviceUrl: defa ultZone: http ://localhost:8761/eureka/, http://localhost:8762/eureka/, http://localhost: 8763/eureka/ # three eureka server
这里新增了peer3的Eureka Server地址。
修改eureka-server-peer1.yml,如下所示:
server: port: 8761 spring: application: name: eureka-server eureka: instance: hostname: localhost preferIpAddress: true client: registerWithEureka: true fetchRegistry: true serviceUrl: defaultZone: http://localhost:8762/eureka/, http://localhost:8763/ eureka/ # three eureka server server: waitTimeInMsWhenSyncEmpty: 0 enableSelfPreservation: false
其eureka.client.serviceUrl.defaultZone指向了peer2和peer3。
修改eureka-server-peer2.yml的配置如下:
server: port: 8762 eureka: instance: hostname: localhost preferIpAddress: true client: registerWithEureka: true fetchRegistry: true serviceUrl: defaultZone: http://localhost:8761/eureka/, http://localhost:8763/ eureka/ # three eureka server server: waitTimeInMsWhenSyncEmpty: 0 enableSelfPreservation: false
其eureka.client.serviceUrl.defaultZone指向了peer1和peer3。
接下来重启config-server,然后分别刷新Eureka Client、Eureka Server的peer1和peer2的配置,如下所示:
~ curl -i -X POST http://localhost:8081/actuator/refresh HTTP/1.1200 Content-Type: application/vnd.spring-boot.actuator.v2+json; charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 20 Jun 2018 11:24:57 GMT ["eureka.client.serviceUrl.defaultZone"]% → ~ curl -i -X POST http://localhost:8761/actuator/refresh HTTP/1.1200 Content-Type: application/vnd.spring-boot.actuator.v2+json; charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 20 Jun 2018 11:25:17 GMT ["eureka.client.serviceUrl.defaultZone"]% → ~ curl -i -X POST http://localhost:8762/actuator/refresh HTTP/1.1200 Content-Type: application/vnd.spring-boot.actuator.v2+json; charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 20 Jun 2018 11:25:36 GMT ["eureka.client.serviceUrl.defaultZone"]%
之后分别访问各个实例的/query/eureka-server,可以看到Eureka Server的列表已经成功变为3个,扩容成功。
本小节举例的Eureka Server在线扩容,需要依赖配置中心的动态刷新功能,具体的就是/actuator/refresh这个endpoint。这里为了方便,使用的config-server是native的profile,因此修改后重启才生效,如果是使用git仓库,则无须重启config-server。
3.5.2 构建Multi Zone Eureka Server
前面的小节简单介绍了Eureka的Zone及Region设计,这里我们来演示下如何构建Multi Zone的Eureka Server,同时演示下默认的ZoneAffinity特性。
1. Eureka Server实例
这里我们启动四个Eureka Server实例,配置两个zone:zone1及zone2,每个zone都有两个Eureka Server实例,这两个zone配置在同一个region:region-east上。
它们的配置文件分别如代码清单3-9、代码清单3-10、代码清单3-11、代码清单3-12所示:
代码清单3-9 ch3-2\ch3-2-eureka-server\src\main\resources\application-zone1a.yml
server: port: 8761 spring: application: name: eureka-server eureka: instance: hostname: localhost preferIpAddress: true metadataMap.zone: zone1 client: register-with-eureka: true fetch-registry: true region: region-east service-url: zone1: http://localhost:8761/eureka/, http://localhost:8762/eureka/ zone2: http://localhost:8763/eureka/, http://localhost:8764/eureka/ availability-zones: region-east: zone1, zone2 server: waitTimeInMsWhenSyncEmpty: 0 enableSelfPreservation: false
代码清单3-10 ch3-2\ch3-2-eureka-server\src\main\resources\application-zone1b.yml
server: port: 8762 spring: application: name: eureka-server eureka: instance: hostname: localhost preferIpAddress: true metadataMap.zone: zone1 client: register-with-eureka: true fetch-registry: true region: region-east service-url: zone1: http://localhost:8761/eureka/, http://localhost:8762/eureka/ zone2: http://localhost:8763/eureka/, http://localhost:8764/eureka/ availability-zones: region-east: zone1, zone2 server: waitTimeInMsWhenSyncEmpty: 0 enableSelfPreservation: false
代码清单3-11 ch3-2\ch3-2-eureka-server\src\main\resources\application-zone2a.yml
server: port: 8763 spring: application: name: eureka-server eureka: instance: hostname: localhost preferIpAddress: true metadataMap.zone: zone2 client: register-with-eureka: true fetch-registry: true region: region-east service-url: zone1: http://localhost:8761/eureka/, http://localhost:8762/eureka/ zone2: http://localhost:8763/eureka/, http://localhost:8764/eureka/ availability-zones: region-east: zone1, zone2 server: waitTimeInMsWhenSyncEmpty: 0 enableSelfPreservation: false
代码清单3-12 ch3-2\ch3-2-eureka-server\src\main\resources\application-zone2b.yml
server: port: 8764 spring: application: name: eureka-server eureka: instance: hostname: localhost preferIpAddress: true metadataMap.zone: zone2 client: register-with-eureka: true fetch-registry: true region: region-east service-url: zone1: http://localhost:8761/eureka/, http://localhost:8762/eureka/ zone2: http://localhost:8763/eureka/, http://localhost:8764/eureka/ availability-zones: region-east: zone1, zone2 server: waitTimeInMsWhenSyncEmpty: 0 enableSelfPreservation: false
上面我们配置了四个Eureka Server的配置文件,可以看到我们通过eureka.instance. metadataMap.zone设置了每个实例所属的zone。接下来分别使用这四个proflie启动这四个Eureka Server,如下:
mvn spring-boot:run -Dspring.profiles.active=zone1a mvn spring-boot:run -Dspring.profiles.active=zone1b mvn spring-boot:run -Dspring.profiles.active=zone2a mvn spring-boot:run -Dspring.profiles.active=zone2b
2. Eureka Client实例
这里我们配置两个Eureka Client,分别属于zone1及zone2,其配置文件如代码清单3-13、代码清单3-14、代码清单3-15所示:
代码清单3-13 ch3-2\ch3-2-eureka-server\src\main\resources\application.yml
management: endpoints: web: exposure: include: '*'
这里我们暴露所有的endpoints,方便后面验证。
代码清单3-14 ch3-2\ch3-2-eureka-server\src\main\resources\application-zone1.yml
server: port: 8081 spring: application: name: client eureka: instance: metadataMap.zone: zone1 client: register-with-eureka: true fetch-registry: true region: region-east service-url: zone1: http://localhost:8761/eureka/, http://localhost:8762/eureka/ zone2: http://localhost:8763/eureka/, http://localhost:8764/eureka/ availability-zones: region-east: zone1, zone2
代码清单3-15 ch3-2\ch3-2-eureka-server\src\main\resources\application-zone2.yml
server: port: 8082 spring: application: name: client eureka: instance: metadataMap.zone: zone2 client: register-with-eureka: true fetch-registry: true region: region-east service-url: zone1: http://localhost:8761/eureka/, http://localhost:8762/eureka/ zone2: http://localhost:8763/eureka/, http://localhost:8764/eureka/ availability-zones: region-east: zone1, zone2
接着使用如下命令分别启动这两个client:
mvn spring-boot:run -Dspring.profiles.active=zone1 mvn spring-boot:run -Dspring.profiles.active=zone2
3. Zuul Gateway实例
这里我们新建一个zuul工程来演示Eureka使用metadataMap的zone属性时的ZoneAffinity特性。
配置文件如代码清单3-16、3-17所示:
代码清单3-16 ch3-2\ch3-2-zuul-gateway\src\main\resources\application-zone1.yml
server: port: 10001 eureka: instance: metadataMap.zone: zone1 client: register-with-eureka: true fetch-registry: true region: region-east service-url: zone1: http://localhost:8761/eureka/, http://localhost:8762/eureka/ zone2: http://localhost:8763/eureka/, http://localhost:8764/eureka/ availability-zones: region-east: zone1, zone2
代码清单3-17 ch3-2\ch3-2-zuul-gateway\src\main\resources\application-zone2.yml
server: port: 10002 eureka: instance: metadataMap.zone: zone2 client: register-with-eureka: true fetch-registry: true region: region-east service-url: zone1: http://localhost:8761/eureka/, http://localhost:8762/eureka/ zone2: http://localhost:8763/eureka/, http://localhost:8764/eureka/ availability-zones: region-east: zone1, zone2
接下来使用这两个profile分别启动gateway如下:
mvn spring-boot:run -Dspring.profiles.active=zone1 mvn spring-boot:run -Dspring.profiles.active=zone2
4.验证ZoneAffinity
访问http://localhost:10001/client/actuator/env,部分结果如下:
{ "activeProfiles": [ "zone1" ] //...... }
访问http://localhost:10002/client/actuator/env,部分结果如下:
{ "activeProfiles": [ "zone2" ] //...... }
可以看到,通过请求gateway的/client/actuator/env,访问的是Eureka Client实例的/actuator/env接口,处于zone1的gateway返回的activeProfiles为zone1,处于zone2的gateway返回的activeProfiles为zone2。从这个表象看gateway路由时对client的实例是ZoneAffinity的。有兴趣的读者可以去研读源码,看看gateway是如何实现Eureka的ZoneAffinity的。
3.5.3 支持Remote Region
1. Eureka Server实例
这里我们配置4个Eureka Server,分4个zone,然后属于region-east、region-west两个region,其region-east的配置文件如代码清单3-18、3-19所示:
代码清单3-18 ch3-3\ch3-3-eureka-server\src\main\resources\application-zone1.yml
server: port: 8761 spring: application: name: eureka-server eureka: server: waitTimeInMsWhenSyncEmpty: 0 enableSelfPreservation: false remoteRegionUrlsWithName: region-west: http://localhost:8763/eureka/ client: register-with-eureka: true fetch-registry: true region: region-east service-url: zone1: http://localhost:8761/eureka/ zone2: http://localhost:8762/eureka/ availability-zones: region-east: zone1, zone2 instance: hostname: localhost metadataMap.zone: zone1
代码清单3-19 ch3-3\ch3-3-eureka-server\src\main\resources\application-zone2.yml
server: port: 8762 spring: application: name: eureka-server eureka: server: waitTimeInMsWhenSyncEmpty: 0 enableSelfPreservation: false remoteRegionUrlsWithName: region-west: http://localhost:8763/eureka/ client: register-with-eureka: true fetch-registry: true region: region-east service-url: zone1: http://localhost:8761/eureka/ zone2: http://localhost:8762/eureka/ availability-zones: region-east: zone1, zone2 instance: hostname: localhost metadataMap.zone: zone2
这里zone1及zone2属于region-east,配置其remote-region为region-west,具体如代码清单3-20、代码清单3-21所示。
代码清单3-20 ch3-3\ch3-3-eureka-server\src\main\resources\application-zone3-region-west.yml
server: port: 8763 spring: application: name: eureka-server eureka: server: waitTimeInMsWhenSyncEmpty: 0 enableSelfPreservation: false remoteRegionUrlsWithName: region-east: http://localhost:8761/eureka/ client: register-with-eureka: true fetch-registry: true region: region-west service-url: zone3: http://localhost:8763/eureka/ zone4: http://localhost:8764/eureka/ availability-zones: region-west: zone3, zone4 instance: hostname: localhost metadataMap.zone: zone3
代码清单3-21 ch3-3\ch3-3-eureka-server\src\main\resources\application-zone4-region-west.yml
server: port: 8764 spring: application: name: eureka-server eureka: server: waitTimeInMsWhenSyncEmpty: 0 enableSelfPreservation: false remoteRegionUrlsWithName: region-east: http://localhost:8761/eureka/ client: register-with-eureka: true fetch-registry: true region: region-west service-url: zone3: http://localhost:8763/eureka/ zone4: http://localhost:8764/eureka/ availability-zones: region-west: zone3, zone4 instance: hostname: localhost metadataMap.zone: zone4
这里zone3及zone4属于region-west,配置其remote-region为region-east。
由于源码里EurekaServerConfigBean的remoteRegionAppWhitelist默认为null,而getRemoteRegionAppWhitelist(String regionName)方法会直接调用,如果不设置,则会报空指针异常:
2018-06-25 16:09:43.414 ERROR 13032 --- [nio-8764-exec-2] o.a.c.c.C.[.[.[/]. [dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception java.lang.NullPointerException: null at org.springframework.cloud.netflix.eureka.server.EurekaServerConfigBean. getRemoteRegionAppWhitelist(EurekaServerConfigBean.java:226) ~[spring- cloud-netflix-eureka-server-2.0.0.RELEASE.jar:2.0.0.RELEASE] at com.netflix.eureka.registry.AbstractInstanceRegistry.shouldFetchFro mRemoteRegistry(AbstractInstanceRegistry.java:795) ~[eureka-core- 1.9.2.jar:1.9.2] at com.netflix.eureka.registry.AbstractInstanceRegistry.getApplicationsFr omMultipleRegions(AbstractInstanceRegistry.java:767) ~[eureka-core- 1.9.2.jar:1.9.2] at com.netflix.eureka.registry.AbstractInstanceRegistry.getApplicationsFr omAllRemoteRegions(AbstractInstanceRegistry.java:702) ~[eureka-core- 1.9.2.jar:1.9.2] at com.netflix.eureka.registry.AbstractInstanceRegistry.getApplications(Abst ractInstanceRegistry.java:693) ~[eureka-core-1.9.2.jar:1.9.2] at com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl.getSortedA pplications(PeerAwareInstanceRegistryImpl.java:555) ~[eureka-core- 1.9.2.jar:1.9.2]
为了避免空指针,需要初始下该对象,如代码清单3-22所示:
代码清单3-22 ch3-3\ch3-3-eureka-server\src\main\java\cn\springcloud\book\config\RegionConfig.java
@Configuration @AutoConfigureBefore(EurekaServerAutoConfiguration.class) public class RegionConfig { @Bean @ConditionalOnMissingBean public EurekaServerConfig eurekaServerConfig(EurekaClientConfig clientConfig) { EurekaServerConfigBean server = new EurekaServerConfigBean(); if (clientConfig.shouldRegisterWithEureka()) { // Set a sensible default if we are supposed to replicate server.setRegistrySyncRetries(5); } server.setRemoteRegionAppWhitelist(new HashMap<>()); return server; } }
然后使用如下命令启动4个Eureka Server:
mvn spring-boot:run -Dspring.profiles.active=zone1 mvn spring-boot:run -Dspring.profiles.active=zone2 mvn spring-boot:run -Dspring.profiles.active=zone3-region-west mvn spring-boot:run -Dspring.profiles.active=zone4-region-west
2. Eureka Client实例
这里我们配置4个Eureka Client,也是分了4个zone,属于region-east、region-west两个region。其region-east的配置文件如代码清单3-23、3-24所示:
代码清单3-23 ch3-3\ch3-3-eureka-client\src\main\resources\application-zone1.yml
server: port: 8071 spring: application.name: demo-client eureka: client: prefer-same-zone-eureka: true region: region-east service-url: zone1: http://localhost:8761/eureka/ zone2: http://localhost:8762/eureka/ availability-zones: region-east: zone1, zone2 instance: metadataMap.zone: zone1
代码清单3-24 ch3-3\ch3-3-eureka-client\src\main\resources\application-zone2.yml
server: port: 8072 spring: application.name: demo-client eureka: client: prefer-same-zone-eureka: true region: region-east service-url: zone1: http://localhost:8761/eureka/ zone2: http://localhost:8762/eureka/ availability-zones: region-east: zone1, zone2 instance: metadataMap.zone: zone2
zone1及zone2属于region-east, zone3及zone4属于region-west,具体如代码清单3-25、3-26所示。
代码清单3-25 ch3-3\ch3-3-eureka-client\src\main\resources\application-zone3.yml
server: port: 8073 spring: application.name: demo-client eureka: client: prefer-same-zone-eureka: true region: region-west service-url: zone3: http://localhost:8763/eureka/ zone4: http://localhost:8764/eureka/ availability-zones: region-west: zone3, zone4 instance: metadataMap.zone: zone3
代码清单3-26 ch3-3\ch3-3-eureka-client\src\main\resources\application-zone4.yml
server: port: 8074 spring: application.name: demo-client eureka: client: prefer-same-zone-eureka: true region: region-west service-url: zone3: http://localhost:8763/eureka/ zone4: http://localhost:8764/eureka/ availability-zones: region-west: zone3, zone4 instance: metadataMap.zone: zone4
然后使用如下命令启动4个Client:
mvn spring-boot:run -Dspring.profiles-active=zone1 mvn spring-boot:run -Dspring.profiles-active=zone2 mvn spring-boot:run -Dspring.profiles-active=zone3 mvn spring-boot:run -Dspring.profiles-active=zone4
3. Zuul Gateway实例
这里我们使用2个zuul gateway实例来演示fallback到remote region的应用实例的功能。这两个gateway,一个属于region-east,一个属于region-west。其配置分别如代码清单3-27、3-28所示:
代码清单3-27 ch3-3\ch3-3-3-zuul-gateway \src\main\resources\application-zone1.yml
server: port: 10001 eureka: instance: metadataMap.zone: zone1 client: register-with-eureka: true fetch-registry: true region: region-east service-url: zone1: http://localhost:8761/eureka/ zone2: http://localhost:8762/eureka/ availability-zones: region-east: zone1, zone2
代码清单3-28 ch3-3\ch3-3-zuul-gateway \src\main\resources\application-zone.yml
server: port: 10002 eureka: instance: metadataMap.zone: zone3 client: register-with-eureka: true fetch-registry: true region: region-west service-url: zone3: http://localhost:8763/eureka/ zone4: http://localhost:8764/eureka/ availability-zones: region-west: zone3, zone4
然后使用如下命令启动gateway:
mvn spring-boot:run -Dspring.profiles.active=zone1 mvn spring-boot:run -Dspring.profiles.active=zone3-region-west
启动之后访问:
curl -i http://localhost:10001/demo-client/actuator/env curl -i http://localhost:10002/demo-client/actuator/env
可以看到如3.5.2节讲的zoneAffinity特性,zone1的gateway访问的是zone1的demo-client, zone3的gateway访问的是zone3的demo-client。
接下来关闭到zone1及zone2的Eureka Client,再继续访问curl -i http://localhost:10001/demo-client/actuator/env,可以看到经过几个报错之后,自动fallback到了remote-region的zone3或者zone4的实例,实现了类似异地多活自动转移请求的效果。有兴趣的读者可以阅读相关源码,看下Eureka Server到底是怎么实现跨region的fallback的。
3.5.4 开启HTTP Basic认证
在实际生产部署的过程中,往往需要考虑一个安全问题,比如Eureka Server自己有暴露REST API,如果没有安全认证,别人就可以通过REST API随意修改信息,造成服务异常。这一小节,我们来看一看Eureka Server是如何启用HTTP Basic校验的,以及Eureka Client是如何配置相应鉴权信息的。
1. Eureka Server配置
要启动Eureka Server的HTTP Basic认证,则需要引入spring-boot-starter-security,如代码清单3-29所示。
代码清单3-29 ch3-4\ch3-4-eureka-server\pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
另外需要在配置文件中指定账户密码,这块可以跟config-server的加密功能结合,如代码清单3-30所示。
代码清单3-30 ch3-4\ch3-4-eureka-server\src\main\resources\application-security.yml
server: port: 8761 spring: security: basic: enabled: true user: name: admin password: Xk38CNHigBP5jK75 eureka: instance: hostname: localhost client: registerWithEureka: false fetchRegistry: false serviceUrl: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ server: waitTimeInMsWhenSyncEmpty: 0 enableSelfPreservation: false
另外,由于spring-boot-starter-security默认开启了csrf校验,对于Client端这类非界面应用来说不合适,但是又没有配置文件的方式可以禁用,需要自己通过Java的配置文件禁用下,如代码清单3-31所示。
代码清单3-31 ch3-4\ch3-4-eureka-server\src\main\java\cn\springcloud\book\config\SecurityConfig.java
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { super.configure(http); http.csrf().disable(); } }
然后使用security的profile启动Eureka Server:
mvn spring-boot:run -Dspring.profiles.active=security
然后如下所示访问:
curl -i http://localhost:8761/eureka/apps HTTP/1.1401 Set-Cookie: JSESSIONID=D7D019318B2E5D011C3000759659FE1C; Path=/; HttpOnly WWW-Authenticate: Basic realm="Realm" X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Frame-Options: DENY Content-Type: application/json; charset=UTF-8 Transfer-Encoding: chunked Date: Mon, 25 Jun 2018 09:11:07 GMT {"timestamp":"2018-06-25T09:11:07.832+0000", "status":401, "error":"Unauthorized" ,"message":"Unauthorized", "path":"/eureka/apps"}
可以看到,没有传递Authorization的header,返回401。
接下来使用HTTP Basic的账号密码传递Authorization的header,如下:
curl -i --basic -u admin:Xk38CNHigBP5jK75 http://localhost:8761/eureka/apps HTTP/1.1200 Set-Cookie: JSESSIONID=8B745BEA3606E8F4856A6197407D8433; Path=/; HttpOnly X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Frame-Options: DENY Content-Type: application/xml Transfer-Encoding: chunked Date: Mon, 25 Jun 2018 09:28:29 GMT <applications> <versions__delta>1</versions__delta> <apps__hashcode></apps__hashcode> </applications>
可以看到请求成功返回。
2. Eureka Client配置
由于Eureka Server开启了HTTP Basic认证,Eureka Client也需要配置相应的账号信息来传递,这里我们通过配置文件来指定,相关的密码也结合config-server的加密功能来加密,如代码清单3-32所示。
代码清单3-32 ch3-4\ch3-4-eureka-client\src\main\resources\application-security.yml
server: port: 8081 spring: application: name: client1 eureka: client: security: basic: user: admin password: Xk38CNHigBP5jK75 serviceUrl: defaultZone: http://${eureka.client.security.basic.user}:${eureka. client.security.basic.password}@localhost:8761/eureka/
然后使用如下命令启动:
mvn spring-boot:run -Dspring.profiles.active=security
之后执行如下命令查看:
curl -i --basic -u admin:Xk38CNHigBP5jK75 http://localhost:8761/eureka/apps HTTP/1.1200 Set-Cookie: JSESSIONID=0CCE2E3E092CF499CF509F1B799ED837; Path=/; HttpOnly X-Content-Type-Options: nosniff X-XSS-Protection: 1; mode=block Cache-Control: no-cache, no-store, max-age=0, must-revalidate Pragma: no-cache Expires: 0 X-Frame-Options: DENY Content-Type: application/xml Transfer-Encoding: chunked Date: Mon, 25 Jun 2018 09:22:49 GMT <applications> <versions__delta>1</versions__delta> <apps__hashcode>UP_1_</apps__hashcode> <application> <name>CLIENT1</name> <instance> <instanceId>10.2.238.79:client1:8081</instanceId> <hostName>10.2.238.79</hostName> <app>CLIENT1</app> <ipAddr>10.2.238.79</ipAddr> <status>UP</status> <overriddenstatus>UNKNOWN</overriddenstatus> <port enabled="true">8081</port> <securePort enabled="false">443</securePort> <countryId>1</countryId> <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataC enterInfo"> <name>MyOwn</name> </dataCenterInfo> <leaseInfo> <renewalIntervalInSecs>30</renewalIntervalInSecs> <durationInSecs>90</durationInSecs> <registrationTimestamp>1529917089388</registrationTimestamp> <lastRenewalTimestamp>1529918469122</lastRenewalTimestamp> <evictionTimestamp>0</evictionTimestamp> <serviceUpTimestamp>1529917089389</serviceUpTimestamp> </leaseInfo> <metadata> <management.port>8081</management.port> <jmx.port>49950</jmx.port> </metadata> <homePageUrl>http://10.2.238.79:8081/</homePageUrl> <statusPageUrl>http://10.2.238.79:8081/actuator/info</statusPageUrl> <healthCheckUrl>http://10.2.238.79:8081/actuator/health</healthCheckUrl> <vipAddress>client1</vipAddress> <secureVipAddress>client1</secureVipAddress> <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer> <lastUpdatedTimestamp>1529917089389</lastUpdatedTimestamp> <lastDirtyTimestamp>1529917088523</lastDirtyTimestamp> <actionType>ADDED</actionType> </instance> </application> </applications>
可以看到Client已经注册成功。
3.5.5 启用https
对于上面开启HTTP Basic认证来说,从安全角度讲,基于base64编码很容易被抓包然后破解,如果暴露在公网会非常不安全,这里就讲述一下如何在Eureka Server及Client开启https,来达到这个目的。
1.证书生成
keytool -genkeypair -alias server -storetype PKCS12-keyalg RSA -keysize 2048 -keystore server.p12-validity 3650 输入密钥库口令: 再次输入新口令: 您的名字与姓氏是什么? [Unknown]: 您的组织单位名称是什么? [Unknown]: 您的组织名称是什么? [Unknown]: 您所在的城市或区域名称是什么? [Unknown]: 您所在的省/市/自治区名称是什么? [Unknown]: 该单位的双字母国家/地区代码是什么? [Unknown]: CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown是否正确? [否]: Y
这里使用的密码是Spring Cloud,然后会在当前目录下生成一个名为server.p12的文件。
下面生成Client端使用的证书:
keytool -genkeypair -alias client -storetype PKCS12-keyalg RSA -keysize 2048 -keystore client.p12-validity 3650 输入密钥库口令: 再次输入新口令: 您的名字与姓氏是什么? [Unknown]: 您的组织单位名称是什么? [Unknown]: 您的组织名称是什么? [Unknown]: 您所在的城市或区域名称是什么? [Unknown]: 您所在的省/市/自治区名称是什么? [Unknown]: 该单位的双字母国家/地区代码是什么? [Unknown]: CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown是否正确? [否]: Y
这里使用的密码是Client,然后会在当前目录下生成一个名为client.p12的文件。
下面分别导出两个p12的证书,如下:
keytool -export -alias server -file server.crt --keystore server.p12 输入密钥库口令: 存储在文件 <server.crt> 中的证书 keytool -export -alias client -file client.crt --keystore client.p12 输入密钥库口令: 存储在文件 <client.crt> 中的证书
接下来将server.crt文件导入client.p12中,使Client端信任Server的证书:
keytool -import -alias server -file server.crt -keystore client.p12 输入密钥库口令: 所有者:CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown 发布者: CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown 序列号: 5249cc11 有效期为Mon Jun 25 18:53:20 CST 2018 至Thu Jun 22 18:53:20 CST 2028 证书指纹: MD5: 4D:27:25:0E:A2:6A:7A:0C:81:D2:89:35:12:61:3E:16 SHA1: A3:5E:8E:09:F8:B1:44:9C:B5:AC:AB:2E:F2:7A:58:95:7F:02:69:C4 SHA256: 93:3B:9F:CA:74:D3:88:19:69:7F:65:E0:4F:DF:E0:71:C6:3E:5F:BC:FF:7F:4 F:0F:39:43:D7:22:A6:87:96:8C 签名算法名称: SHA256withRSA 主体公共密钥算法: 2048 位RSA密钥 版本: 3 扩展: #1: ObjectId: 2.5.29.14 Criticality=false SubjectKeyIdentifier [ KeyIdentifier [ 0000: BE A8 E5 3D D6 8E 58 47 CB C4 17 2A 8D 4F 50 1D ...=..XG...*.OP. 0010: 83 B8 3E 24 ..>$ ] ] 是否信任此证书? [否]: Y 证书已添加到密钥库中
这里需要输入的是client.p12的密钥。
然后将client.crt导入server.p12中,使得Server信任Client的证书:
keytool -import -alias client -file client.crt -keystore server.p12 输入密钥库口令: 所有者: CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown 发布者: CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown 序列号: 5c68914d 有效期为Tue Jun 26 09:30:47 CST 2018 至Fri Jun 23 09:30:47 CST 2028 证书指纹: MD5: 4C:0F:35:63:CE:47:A9:C5:90:7C:B2:7D:07:CE:67:DC SHA1: E2:E0:DD:E3:3F:84:DF:21:F5:FB:CA:F5:A9:FB:3C:CD:08:AE:3E:C3 SHA256: 65:B4:49:C0:1D:C3:7B:0C:1B:4D:13:67:91:1F:5E:18:6F:F7:0E:A D:64:D4:D 9:11:97:DB:55:BB:D4:E3:3F:D2 签名算法名称: SHA256withRSA 主体公共密钥算法: 2048 位RSA密钥 版本: 3 扩展: #1: ObjectId: 2.5.29.14 Criticality=false SubjectKeyIdentifier [ KeyIdentifier [ 0000: 1F 49 7D 24 2F E0 7B 2E F2 F7 19 A2 48 23 4D 73 .I.$/.......H#Ms 0010: 1D DC 99 0B .... ] ] 是否信任此证书? [否]: Y 证书已添加到密钥库中
这里需要输入的是server.p12的密钥。
2. Eureka Server配置
把生成的server.p12放到Maven工程的resources目录下,然后指定相关配置如代码清单3-33所示。
代码清单3-33 ch3-5\ch3-5-eureka-server\src\main\resources\application-https.yml
server: port: 8761 ssl: enabled: true key-store: classpath:server.p12 key-store-password: springcloud key-store-type: PKCS12 key-alias: server eureka: instance: hostname: localhost securePort: ${server.port} securePortEnabled: true nonSecurePortEnabled: false homePageUrl: https://${eureka.instance.hostname}:${server.port}/ statusPageUrl: https://${eureka.instance.hostname}:${server.port}/ client: registerWithEureka: false fetchRegistry: false serviceUrl: defaultZone: https://${eureka.instance.hostname}:${server.port}/eureka/ server: waitTimeInMsWhenSyncEmpty: 0 enableSelfPreservation: false
这里主要是指定server.ssl配置,以及eureka.instance的securePortEnabled及eureka. instance.securePort配置。
使用https的profile启动如下:
mvn spring-boot:run -Dspring.profiles.active=https
之后访问http://localhost:8761/,可以看到https已经启用。
3. Eureka Client配置
把生成的client.p12放到Maven工程的resources目录下,然后指定相关配置如代码清单3-34所示。
代码清单3-34 ch3-5\ch3-5-eureka-client\src\main\resources\application-https.yml
server: port: 8081 spring: application: name: client1 eureka: client: securePortEnabled: true ssl: key-store: client.p12 key-store-password: client serviceUrl: defaultZone: https://localhost:8761/eureka/
这里我们没有指定整个应用实例启用https,仅仅是开启访问Eureka Server的https配置。通过自定义eureka.client.ssl.key-store以及eureka.client.ssl.key-store-password两个属性,指定Eureka Client访问Eureka Server的sslContext配置,这里需要在代码里指定DiscoveryClient. DiscoveryClientOptionalArgs,配置如代码清单3-35所示。
代码清单3-35 ch3-5\ch3-5-eureka-client\src\main\java\cn\springcloud\book\config\EurekaHttpsClientConfig.java
@Configuration public class EurekaHttpsClientConfig { @Value("${eureka.client.ssl.key-store}") String keyStoreFileName; @Value("${eureka.client.ssl.key-store-password}") String keyStorePassword; @Bean public DiscoveryClient.DiscoveryClientOptionalArgs discoveryClientOptionalArgs() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException { EurekaJerseyClientImpl.EurekaJerseyClientBuilder builder = new EurekaJerseyClientImpl.EurekaJerseyClientBuilder(); builder.withClientName("eureka-https-client"); SSLContext sslContext = new SSLContextBuilder() .loadTrustMaterial( this .getClass().getClassLoader().getResource(keyStoreFileName), keyStorePassword.toCharArray() ) .build(); builder.withCustomSSL(sslContext); builder.withMaxTotalConnections(10); builder.withMaxConnectionsPerHost(10); DiscoveryClient.DiscoveryClientOptionalArgs args = new DiscoveryClient. DiscoveryClientOptionalArgs(); args.setEurekaJerseyClient(builder.build()); return args; } }
然后使用如下命令启动:
mvn spring-boot:run -Dspring.profiles.active=https
查询Eureka Server可以看到已经成功注册上:
curl --insecure https://localhost:8761/eureka/apps <applications> <versions__delta>1</versions__delta> <apps__hashcode>UP_1_</apps__hashcode> <application> <name>CLIENT1</name> <instance> <instanceId>10.2.238.208:client1:8081</instanceId> <hostName>10.2.238.208</hostName> <app>CLIENT1</app> <ipAddr>10.2.238.208</ipAddr> <status>UP</status> <overriddenstatus>UNKNOWN</overriddenstatus> <port enabled="true">8081</port> <securePort enabled="false">443</securePort> <countryId>1</countryId> <dataCenterInfo class="com.netflix.appinfo.InstanceInfo$DefaultDataC enterInfo"> <name>MyOwn</name> </dataCenterInfo> <leaseInfo> <renewalIntervalInSecs>30</renewalIntervalInSecs> <durationInSecs>90</durationInSecs> <registrationTimestamp>1529977873082</registrationTimestamp> <lastRenewalTimestamp>1529978503987</lastRenewalTimestamp> <evictionTimestamp>0</evictionTimestamp> <serviceUpTimestamp>1529977873082</serviceUpTimestamp> </leaseInfo> <metadata> <management.port>8081</management.port> </metadata> <homePageUrl>http://10.2.238.208:8081/</homePageUrl> <statusPageUrl>http://10.2.238.208:8081/actuator/info</statusPageUrl> <hea lthCheckUrl>http://10.2.238.208:8081/actuator/health</ healthCheckUrl> <vipAddress>client1</vipAddress> <secureVipAddress>client1</secureVipAddress> <isCoordinatingDiscoveryServer>false</isCoordinatingDiscoveryServer> <lastUpdatedTimestamp>1529977873082</lastUpdatedTimestamp> <lastDirtyTimestamp>1529977872969</lastDirtyTimestamp> <actionType>ADDED</actionType> </instance> </application> </applications>
3.5.6 Eureka Admin
由于在Spring Cloud的技术栈中,Eureka Server是微服务架构中必不可少的基础组件,所以Spring Cloud中国社区开放了Eureka Server在线服务供大家学习或测试,访问地址为http://eureka.springcloud.cn/。
除此之外,Spring Cloud中国社区为Eureka注册中心开源了一个节点监控、服务动态启停的管控平台:Eureka Admin,其在GitHub的地址为http://github.com/SpringCloud/eureka-admin/。
该项目的页面预览图如图3-2所示。
图3-2 eureka admin首页
通过管控平台的界面可以对注册的服务实例进行上线、下线、停止等操作,省得自己再去调用Eureka Server的REST API,非常方便。
3.5.7 基于metadata路由实例
对于Eureka来说,最常见的就是通过metadata属性,进行灰度控制或者是不宕机升级。这里结合Netflix Ribbon的例子,介绍一下这类应用场景的实现。
1. ILoadBalancer接口
Netflix Ribbon的ILoadBalancer接口定义了loadBalancer的几个基本方法,如下:
public interface ILoadBalancer public void addServers(List<Server> newServers); public Server chooseServer(Object key); public void markServerDown(Server server); @Deprecated public List<Server> getServerList(boolean availableOnly); public List<Server> getReachableServers(); public List<Server> getAllServers(); }
可以看到这里有个chooseServer方法,用于从一堆服务实例列表中进行过滤,选取一个Server出来,给客户端请求用。
在Ribbon中,ILoadBalancer选取Server的逻辑主要由一系列IRule来实现。
2. IRule接口
public interface IRule{ public Server choose(Object key); public void setLoadBalancer(ILoadBalancer lb); public ILoadBalancer getLoadBalancer(); }
最常见的IRule接口有RoundRobinRule,采用轮询调度算法规则来选取Server,其主要代码如下:
public Server choose(ILoadBalancer lb, Object key) { if (lb == null) { log.warn("no load balancer"); return null; } Server server = null; int count = 0; while (server == null && count++ < 10) { List<Server> reachableServers = lb.getReachableServers(); List<Server> allServers = lb.getAllServers(); int upCount = reachableServers.size(); int serverCount = allServers.size(); if ((upCount == 0) || (serverCount == 0)) { log.warn("No up servers available from load balancer: " + lb); return null; } int nextServerIndex = incrementAndGetModulo(serverCount); server = allServers.get(nextServerIndex); if (server == null) { /* Transient. */ Thread.yield(); continue; } if (server.isAlive() && (server.isReadyToServe())) { return (server); } // Next. server = null; } if (count >= 10) { log.warn("No available alive servers after 10 tries from load balancer: " + lb); } return server; }
3. MetadataAwarePredicate
这里,由于我们需要根据实例的metadata进行过滤,因此,可以自定义实现自己的rule。Netflix提供了PredicateBasedRule,可以基于Guava的Predicate进行过滤。jmnarloch在《Spring Cloud: Ribbon dynamic routing》(http://jmnarloch.wordpress.com/2015/11/25/spring-cloud-ribbon-dynamic-routing/)中给出了针对metadata过滤的rule,如下:
public class MetadataAwarePredicate extends DiscoveryEnabledPredicate { @Override protected boolean apply(DiscoveryEnabledServer server) { final RibbonFilterContext context = RibbonFilterContextHolder. getCurrentContext(); final Set<Map.Entry<String, String>> attributes = Collections. unmodifiableSet(context.getAttributes().entrySet()); final Map<String, String> metadata = server.getInstanceInfo(). getMetadata(); return metadata.entrySet().containsAll(attributes); } }
这个Predicate将Server的metadata跟上下文传递的attributes信息进行匹配,全部匹配上才返回true。比如attributes的map有个entry, key是env, value是canary,表示该实例是canary实例,如果请求上下文要求路由到canary实例,可以从request url参数或者header中标识这个路由请求,然后携带到上下文中,最后由Predicate进行判断,完成整个ILoadBalancer的choose。