重新定义Spring Cloud实战
上QQ阅读APP看书,第一时间看更新

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。