閱讀443 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Spring Cloud 接入 EDAS 之服務發現篇

目前 EDAS 已經完全支持 Spring Cloud 應用的部署了,你可以將你的Spring Cloud 應用部署到 EDAS 中。
同時,為了更好地將阿裏中間件的功能以雲服務的方式提供給大家,我們也對 Spring Cloud 中的一些組件進行了加強或替換的工作。
讓我們先來聊聊服務發現。
我們知道原生的 Spring Cloud 支持多種服務注冊與發現的方式,Eureka 、 Consul 、 Zookeeper 等,目前使用最多最廣的就是 Eureka了,那我們就先從一個簡單的 Eureka Demo 說起。

Eureka Demo

創建服務注冊中心

創建一個基礎的 Spring Cloud 工程,命名為 eureka-server,並在 pom.xml 中引入需要的依賴內容:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.8.RELEASE</version>
        <relativePath/>
    </parent>


    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka-server</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Dalston.SR4</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

通過 @EnableEurekaServer 注解來啟動一個服務注冊中心。隻需要在一個普通的 Spring Boot 應用中添加這個注解就能開啟此功能,代碼如下:

    @SpringBootApplication
    @EnableEurekaServer
    public class EurekaServerApplication {
        public static void main(String[] args) {
            SpringApplication.run(EurekaServerApplication.class, args);
        }
    }

這樣啟動時,應用將完全隻用默認配置,如果我們想給服務命名,或者是修改監聽端口,可以在 resource/application.properties 中進行如下配置。
由於我們自己就是唯一的一個 EurekaServer ,這裏就不向自己注冊自己了,將 register-with-eureka 設置成 false。

spring.application.name=eureka-server
server.port=8761
eureka.client.register-with-eureka=false 

隻需要直接運行 EurekaServerApplication 的 main 函數,eureka server 即可啟動成功。
啟動成功後,可以在 https://localhost:8761 頁麵查看詳情。

5a7b6781-f069-46b0-8df0-ab07ae4ca490.png | center

頁麵打開成功,表明服務已經啟動,目前 instances 為空,表明還沒有服務注冊上來。

創建服務提供者

創建一個 Spring Cloud 工程,命名為 service-provider。同樣,首先在 pom.xml 中引入需要的依賴內容:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.8.RELEASE</version>
        <relativePath/>
    </parent>


    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Dalston.SR4</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

接著是服務提供端的代碼,其中 @EnableDiscoveryClient 注解表明此應用需開啟服務注冊與發現功能。

@SpringBootApplication
@EnableDiscoveryClient
public class ServerApplication {

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

既然是服務提供者,所以我們還需要提供一個簡單的服務

@RestController
public class PersonController {
    @RequestMapping(value = "/echo/{string}", method = RequestMethod.GET)
    public String echo(@PathVariable String string) {
        return string;
    }
}

最後同樣是配置,出去配置應用名與監聽端口外,我們還得告訴應用,服務注冊中心的地址。

spring.application.name=service-provider
server.port=18081
eureka.client.serviceUrl.defaultZone=https://localhost:8761/eureka/

啟動 service-provider 服務,在 Eureka 頁麵查看服務是否已經注冊成功.

ab52a743-9e67-4be7-8c75-aa1bce6c9471.png | center

可以看到 instances 中已經存在的實例有了,service-provider,端口是 18081。

創建服務消費者

這個例子中,我們將不僅僅是演示服務發現的功能,同時還將演示 Eureka 服務發現 與 RestTemplate、AsyncRestTemplate、FeignClient這三個客戶端是如何結合的。因為實際使用中,我們更多使用的是用這三個客戶端進行服務調用。

創建一個 Spring Cloud 工程,命名為 service-consumer。同樣,首先在 pom.xml 中引入需要的依賴內容:

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.8.RELEASE</version>
        <relativePath/>
    </parent>


    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-feign</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Dalston.SR4</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

因為在這裏我們要演示 FeignClient 的使用,所以與 service-provider 相比,我們的依賴增加了一個 spring-cloud-starter-feign。

配置好依賴後,我們首先在啟動函數裏完成三件事:啟用服務注冊與發現,激活 FeignClients,將 RestTemplate 與 AsyncRestTemplate 與服務發現結合。

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ConsumerApplication {

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

    @LoadBalanced
    @Bean
    public AsyncRestTemplate asyncRestTemplate(){
        return new AsyncRestTemplate();
    }

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

}

在使用 FeignClient 之前,我們還需要完善它的配置,其中代碼如下

@FeignClient(name = "service-provider")
public interface EchoService {
    @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
    String echo(@PathVariable("str") String str);
}

在這三個客戶端都配置好,並與 Eureka Client 結合後,我們可以在 Controller 中直接使用他們。

@RestController
public class Controller {

    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private AsyncRestTemplate asyncRestTemplate;
    @Autowired
    private  EchoService echoService;

    @RequestMapping(value = "/echo-rest/{str}", method = RequestMethod.GET)
    public String rest(@PathVariable String str) {
        return restTemplate.getForObject("https://service-provider/echo/" + str, String.class);
    }
    @RequestMapping(value = "/echo-async-rest/{str}", method = RequestMethod.GET)
    public String asyncRest(@PathVariable String str) throws Exception{
        ListenableFuture<ResponseEntity<String>> future = asyncRestTemplate.
                getForEntity("https://service-provider/echo/"+str, String.class);
        return future.get().getBody();
    }
    @RequestMapping(value = "/echo-feign/{str}", method = RequestMethod.GET)
    public String feign(@PathVariable String str) {
        return echoService.echo(str);
    }

}

最後,還是不能忘了配置,特別是服務注冊中心的地址

spring.application.name=service-consumer
server.port=18082
eureka.client.serviceUrl.defaultZone=https://localhost:8761/eureka/

啟動服務,開始我們的調用吧,可以看到調用都成功了,

32b244d5-53c6-426c-9edd-13414c8fd91f.png | center

注意:AsyncRestTemplate 接入 服務發現的時間比較晚,需要在 Dalston 之後的版本才能使用,具體詳情參見此 pull request

Eureka 的煩惱

前麵的例子在本機工作起來是很方便的,但是很遺憾,這隻是一個 demo ,實際部署中我們可能都踩過坑或者有這麼一些不爽。

  • 隻有一個服務注冊中心,顯然這不符合高可用的原則,高可用就得加機器,維護成本太高了。
  • 實際生產中,不會將服務注冊中心與業務服務部署在同一台機器上。如果上雲的時候,我們 eureka server 的地址變化了,我還得修改配置文件裏 eureka 服務的地址,太麻煩了。
  • 實際使用中,我的服務注冊中心肯定是需要加密的,總不能隨隨便便一個人就來隨便注冊和看看我都有哪些服務吧,我還得自己實現服務注冊的安全。
  • 為什麼我注冊上去的服務,不是一個 ip ,而是一個奇怪的名字,通過這個名字我怎麼能調通服務。其實隻是因為沒有增加 eureka.instance.prefer-ip-address=true這個配置。依舊需要添加配置。
  • eureka 因為緩存設計的原因,使得服務注冊上去之後,最遲需要兩分鍾後才能發現。

或許你希望有人提供一個 安全、穩定、高可用、高性能、簡單易用的服務注冊中心。
然後,我不想配置那麼一大堆地址了
最後,我也不想修改我的代碼

是的,EDAS 服務注冊中心,就是這樣一個解決方案。
隻需要修改兩行代碼以及 pom 依賴,無縫將服務注冊中心從 Eureka 切換到 EDAS 服務注冊中心。
你將得到

  • 穩定高可用的服務注冊中心
  • 安全的服務注冊、服務發現
  • 秒級的服務發現機製
  • 無需再關心服務注冊中心的地址

EDAS 服務注冊中心

如何接入

源碼的修改,隻有兩行,需要在 main 函數中添加兩行,修改之後的 service-provider 的 main 函數如下。

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

pom.xml 的修改有兩點
首先就是將原來的 eureka的starter 替換成 EDAS 服務注冊中心的starter,並加入 pandora 的依賴。
修改之後的 service-provider 的 dependences 依賴如下。

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-vipclient</artifactId>
        <version>1.1</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-pandora</artifactId>
        <version>1.2</version>
    </dependency>
</dependencies>

在 build 的 plugins 中,也需要修改成 EDAS 的方式,修改後的內容如下,版本號後續可能會升級。

<build>
    <plugins>
        <plugin>
            <groupId>com.taobao.pandora</groupId>
            <artifactId>pandora-boot-maven-plugin</artifactId>
            <version>2.1.7.8</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

service-consumer 的修改方式與 service-provider 的修改方式完全一樣。

使用方式

使用方式?使用方式已經在接入方式裏了,其他方式和原生的完全一樣。
如果你習慣了 Eureka 頁麵查看服務狀態的方式,EDAS 控製台同樣也提供了相關的功能。

本地調試

本地調試時,需要下載輕量級配置中心,並將其啟動,詳情參見 輕量級配置中心

最後在應用的啟動時,配置 JVM 參數,配置如下。

-Dvipserver.client.port=8080 

Demo 下載

server-demo

client-demo

工作原理

換了一個依賴就把 Eureka 替換成了 EDAS 服務注冊中心,雖然方便,但是這對於我來說相當於是一個黑盒啊,黑盒總是讓人很沒有安全感。
下麵我們將從 服務注冊中心尋址、服務注冊與下線、客戶端結合、高可用、安全等多個方麵來分析原理。

服務注冊中心尋址

既然不需要在配置文件裏配置服務注冊中心的地址了,那麼客戶端是如何找到服務中心的呢?
其實是通過一個 http 請求來實現的,https://jmenv.tbsite.net/vipserver/serverlist。
不僅僅是客戶端,服務端也是通過這個地址來互相發現的。
而在 EDAS 的機器上, jmenv.tbsite.net 是自動配置的。如果在結合我們的輕量級配置中心做本地的開發調試時,還是需要做一點額外配置。

服務注冊與下線

服務注冊的通信協議是 HTTP 協議,默認注冊的應用名是 spring.application.name ,如果有需要將某個應用發布成多個服務名的話,可以試試在 /resource/application.properties 中配置 vipserver.register.doms 的方式來實現,多個服務名中間用英文逗號 , 隔開。
服務注冊成功後,client 端將會與 server 端維持長連接,通過心跳來檢查服務的可用性,當超過一定時間內 server 端沒有收到 client 端的心跳時,會將服務標記成不可用,這樣其他 client 在查詢時就能發現此服務當前處於不可用的狀態。
如果短時間內,大量 client 與 server 心跳失敗,則會出發降級保護機製,這時候,這些服務會暫時不被標記成不可用的狀態。

客戶端結合

與客戶端結合的方式, EDAS 服務發現組件與 Eureka 是完全一致的。
對於 RestTemplate 和 AsyncRestTemplate 來說,添加上 @LoadBalanced 注解,即可直接接入服務發現以及負載均衡。
添加了此注解後,他們將分別會被添加 LoadBalancerInterceptor 和 AsyncLoadBalancerInterceptor 這兩個攔截器。
執行的過程中,所有請求都會分別被這兩個 Interceptor 所攔截,通過其所持有的 LoadBalancerClient 對象去執行這個請求。
而這個 LoadBalancerClient 對象,其實就是 RibbonLoadBalancerClient 的一個實例,在其源碼中,execute 方法的執行邏輯如下

    ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
    Server server = getServer(loadBalancer);

首先我們會拿到一個 ILoadBalancer 對象,然後再通過這個 loadBalancer 對象去拿到我們真正需要調用的服務的地址。
ILoadBalancer有這麼幾個實現類,BaseLoadBalancer、DynamicServerListLoadBalancer、ZoneAwareLoadBalancer等。

看看這個類RibbonClientConfiguration,默認注入的是這個,ZoneAwareLoadBalancer

    @Bean
    @ConditionalOnMissingBean
    public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
            ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
            IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
        if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
            return this.propertiesFactory.get(ILoadBalancer.class, config, name);
        }
        return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
                serverListFilter, serverListUpdater);
    }

關於ServerList的來源,我們看這個類 EurekaRibbonClientConfiguration

    @Bean
    @ConditionalOnMissingBean
    public ServerList<?> ribbonServerList(IClientConfig config, 
                                          Provider<EurekaClient> eurekaClientProvider) {
        if (this.propertiesFactory.isSet(ServerList.class, serviceId)) {
            return this.propertiesFactory.get(ServerList.class, config, serviceId);
        }
        DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(
                config, eurekaClientProvider);
        DomainExtractingServerList serverList = new DomainExtractingServerList(
                discoveryServerList, config, this.approximateZoneFromHostname);
        return serverList;
    }

可以看到,是通過 EurekeClient 來維護實例的地址列表的。
負載均衡選擇策略?ZoneAware,字麵上可以看出是和 zone 有關的,不過 zone 都是默認的情況下,退化成 RoundRobin。

目前的植入做的很簡單,單純地重新搶先注入了上文中提到的兩個 Bean。

    @Bean
    @ConditionalOnMissingBean
    public ServerList<Server> ribbonServerList(IClientConfig config) {
        return new VipserverList(config.getClientName());
    }

    @Bean
    @ConditionalOnMissingBean
    public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
                                            ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
                                            IRule rule, IPing ping) {
        return new DynamicServerListLoadBalancer<Server>(config, rule, ping, serverList, serverListFilter,
                new PollingServerListUpdater(1000L, 1000L));
    }

負載均衡使用的也是默認的RoundRobin。後續我們會把權重等支持添加進來。

高可用實現

服務端高可用

  • eureka
    Eureka的多個server 是對等的實體,在 CAP 中選擇了 AP。
    節點間的數據使用的是最終一致性,eureka 會將注冊的信息同步到 peer 節點,但是 peer 節點不會二次傳播。
    peer節點需要顯示地在配置中設置。如果 peer 節點配置的不全,那麼集群的概念也不存在了,節點之間的關係是通過 peer 節點的顯示配置來維護的。
  • EDAS 服務注冊發現組件
    EDAS 服務注冊中心的多個 server,存在主從,各節點之間使用 raft 協議保證一致性。
    server 之間的互相感知是通過訪問 https://jmenv.tbsite.net/vipserver/serverlist 來獲取其他 peer 節點地址來實現的。
    然後通過自定義的端口和協議來進行選舉和數據同步等操作。CAP 中選擇的是 CP。

客戶端高可用

  • eureka
    通過本地緩存來實現,當 server 連接不上時,直接使用本地緩存。每 30s 異步更新一次緩存,避免了每次請求都強依賴於服務注冊中心。
  • EDAS 服務注冊發現組件
    通過本地緩存來實現,當 server 連接不上時,直接使用本地緩存。異步更新緩存,避免了每次請求都強依賴於服務注冊中心。同時,還提供了通過 UDP 主動 push 的方式在新服務節點加入時及時通知。

安全的實現

EDAS 服務注冊發現組件,結合 EDAS 已有的安全功能,在每次注冊、心跳和查詢請求中都添加了驗簽鑒權的操作,保護了服務的安全性。

最後更新:2017-11-17 18:04:42

  上一篇:go  開源大數據周刊-第72期
  下一篇:go  解密:天貓雙十一1682億背後的“霸下-七層流量清洗”係統